Skip to content

Minecraft: Discord chat bridge, the first Ansible Vault, and a patch that never applied - #35

Merged
shenanigansd merged 5 commits into
mainfrom
claude/discordsrv-ansible-integration-mpmoyb
Sep 3, 2026
Merged

shenanigansd merged 5 commits into
mainfrom
claude/discordsrv-ansible-integration-mpmoyb

Conversation

@shenanigansd

Copy link
Copy Markdown
Member

Adds a Discord ↔ Minecraft chat bridge with DiscordSRV, installed from Modrinth like every other plugin. Along the way it fixes the patch mechanism, which turned out never to have worked, and introduces the repo's first Ansible Vault.

patches/spigot.yaml has never been applied

Worth reviewing first, since it is a live issue rather than part of the feature.

PATCH_DEFINITIONS: "/patches" is directory mode, and PatchCommand.loadPatchSet() enumerates it with:

if (Files.isRegularFile(entry)
        && entry.getFileName().toString().endsWith(".json")) {
    patchSet.getPatches().add(
            patchSetMapper.readValue(entry.toFile(), PatchDefinition.class)...

Three independent reasons the existing file is skipped: it is named .yaml; patchSetMapper is a plain JSON-only ObjectMapper; and directory mode reads each file as a bare PatchDefinition, so the patches: wrapper only works when PATCH_DEFINITIONS names a single file (itzg/docker-minecraft-server#2834, open since 2024).

The mismatch is silent — the listing simply does not match, an empty patch set is processed, mc-image-helper patch exits 0, and Applying patch definitions from /patches still appears in the log. So spigot.yml's commands.log has stayed true, which is what the compose comment relies on being false to keep SimpleLogin's /login <password> out of logs/latest.log in the clear.

Converted to JSON in the shape directory mode expects, with the stale file removed from the host and check-json added to pre-commit. Worth confirming the damage and rotating existing logs:

sudo grep -A2 '^commands:' /opt/letsbuilda/minecraft/data/spigot.yml

DiscordSRV

The bot token never enters a config file. DiscordSRV resolves it from the DISCORDSRV_TOKEN environment variable before falling back to BotToken in config.yml, so Ansible writes it into .env from the vault and it stays out of the data volume and out of /discord debug uploads. It is deliberately not CFG_-prefixed, so REPLACE_ENV_VARIABLE_PREFIX cannot interpolate it into any file under /data.

The channel mapping is not a secret and lives in a patch definition, re-applied on every start like the spigot one so it cannot drift out of the repo. DiscordConsoleChannelId is explicitly set to "" rather than left at its dummy default — running server commands from Discord would be a second, weaker path to the console beside SSH.

Discord-side setup is DiscordSRV's own documentation and is not restated here.

First Ansible Vault

This is the first secret that has to live on a host rather than on the runner. Values are encrypted per value with encrypt_string rather than by encrypting the whole file, which keeps the rest of the file lintable and its diffs reviewable; the cost is that rekey does not work on inline values, so they stay together in one file.

vault_password_file stays out of ansible.cfg. Ansible resolves it on every run whether or not that run touches vaulted data and treats a missing file as fatal — which is why 9a65f59 removed it. CI passes a path via ANSIBLE_VAULT_PASSWORD_FILE; local runs pass --vault-password-file. There is no ANSIBLE_VAULT_PASSWORD variable in ansible-core, only a path, so CI writes the secret to a file in $RUNNER_TEMP, which the runner empties at the end of every job.

The .env task gets no_log: CI runs with --diff, this repository is public, and GitHub only masks the literal secret values it issued — a token decrypted from the vault is not one of them.

group_vars is excluded from yamlfix. It rewrites the block scalar (| → |-, reindented); that still decrypts, but a formatter has no business rewriting ciphertext when the failure mode is a broken production deploy.

Before merging

  • Add ANSIBLE_VAULT_PASSWORD to the ansible GitHub Environment (not a repository secret). Without it the next unattended Sunday run fails.

After deploying

Merging does not deploy; dispatch Deploy Ansible. Then restart the container once:

sudo docker compose -f /opt/letsbuilda/minecraft/compose.yaml restart minecraft

plugins/DiscordSRV/config.yml does not exist until DiscordSRV has enabled once, and patches are applied before the server starts, so the first converge logs Unable to patch ... it is not an existing file and does nothing. A repeat dispatch will not fix it on its own — the image digest is pinned, so Compose recreates nothing.

Verification

prek run --all-files, ansible-lint and ansible-playbook --syntax-check all pass with no vault password available, which is the condition the Lint job runs under. zizmor passes offline under --persona=auditor with no zizmor.yml change — the new secret goes through a step-level env: block, never inline in run:.

Two things to watch on the first patched start, neither of which I could verify without deploying: the patcher decodes the whole of config.yml and re-encodes it with Jackson's YAMLMapper, destroying comments and re-resolving every scalar (YAML 1.1 reads yes/no/on/off as booleans), and an op whose JSONPath stops resolving becomes a startup-blocking failure rather than a no-op.

🤖 Generated with Claude Code

https://claude.ai/code/session_016sBzgNzdYA8b3YrXR3DJrA


Generated by Claude Code

claude and others added 5 commits September 3, 2026 03:30
PATCH_DEFINITIONS names a directory, and in directory mode mc-image-helper
enumerates *.json only, parsing each file with a plain JSON ObjectMapper as a
bare patch definition rather than a patch set. patches/spigot.yaml matched
none of those: wrong extension, wrong format, and wrapped in a `patches:` key.

The mismatch is silent. The directory listing simply does not match the file,
so an empty patch set is processed, `mc-image-helper patch` exits 0, and the
"Applying patch definitions from /patches" line still appears in the log. The
patch has therefore never been applied, and spigot.yml's commands.log has
stayed true -- which is what the compose comment relies on being false to keep
SimpleLogin's "/login <password>" out of logs/latest.log in the clear.

Convert the definition to JSON in the shape directory mode expects, remove the
superseded file from the host so it cannot be mistaken for a working patch,
and move the rationale into the task, since JSON cannot carry comments.

Add check-json to pre-commit: an unresolvable path or malformed definition is
a startup-blocking failure (PatchOperationException propagates out of
mc-image-helper under set -e), so a syntax error is worth catching at commit
time rather than at container start.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016sBzgNzdYA8b3YrXR3DJrA
…ault

Install DiscordSRV from Modrinth alongside SimpleLogin. Release 1.30.5 is the
only one tagged for game version 26.2 and lists the paper loader, so it
resolves against the pinned VERSION without a "?" escape.

The bot token never enters a config file. DiscordSRV resolves its token from
the DISCORDSRV_TOKEN environment variable before falling back to BotToken in
config.yml, so Ansible writes it into .env from the vault and it stays out of
the data volume and out of /discord debug uploads. It is deliberately not
CFG_-prefixed, so REPLACE_ENV_VARIABLE_PREFIX will not interpolate it into any
file under /data.

The channel mapping is not a secret and goes in a patch definition, applied on
every start like the spigot one so it cannot drift out of the repo. The Discord
console channel is explicitly disabled rather than left at its dummy default:
running server commands from Discord would be a second, weaker path to the
console beside SSH.

This is the repo's first secret that has to live on a host rather than on the
runner, so it introduces Ansible Vault. Values are encrypted individually with
encrypt_string rather than by encrypting the whole file. Both survive the Lint
job, which has no vault password, but a whole-file vault makes ansible-lint
give up on the file -- it runs with a dummy password and logs "Ignored
exception from JinjaRule / VariableNamingRule ... Decryption failed", reporting
a pass without linting it. Inline values keep the rest of the file checked,
keep names greppable and diffs reviewable, and avoid ansible-vault edit and its
editor swap files. The cost is that rekey does not work on them, so they are
kept together in one file.

vault_password_file stays out of ansible.cfg. Ansible resolves it whether or
not a run touches vaulted data and treats a missing file as fatal, which is why
9a65f59 removed it; CI passes a path via ANSIBLE_VAULT_PASSWORD_FILE instead,
and local runs pass --vault-password-file. There is no ANSIBLE_VAULT_PASSWORD
variable in ansible-core, only a path, so CI writes the secret to a file in
RUNNER_TEMP, which the runner empties at the end of every job.

The .env task gets no_log: CI runs with --diff, this repository is public, and
GitHub only masks the literal secret values it issued -- a token decrypted from
the vault is not one of them.

group_vars is excluded from yamlfix. It rewrites the block scalar (| to |-,
reindented); that still decrypts, but a formatter has no business rewriting
ciphertext when the failure mode is a broken production deploy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016sBzgNzdYA8b3YrXR3DJrA
Signed-off-by: Bradley Reynolds <bradley.reynolds@tailstory.dev>
Signed-off-by: Bradley Reynolds <bradley.reynolds@tailstory.dev>
The DiscordSRV change had grown docs/ansible.md from 79 lines to 180.

Drop the Discord bridge section entirely. docs/ansible.md documents Ansible --
running it, CI deploys, bootstrapping, the settings that live outside the repo.
How a Minecraft plugin is configured is application config and does not belong
in it, and the first-time setup steps only restated DiscordSRV's own
documentation, which covers gateway intents and the bot invite better and will
not drift out of date the way a copy here would.

Cut the Secrets section to the rules and leave out the derivations: where
secrets live, how to add one, no_log, the vault_password_file landmine, and the
rotation caveat. Shorten the vault notes under Running locally and CI deploys,
and fold the ANSIBLE_VAULT_PASSWORD bullet into the ansible Environment bullet
that already explains the main-only rationale.

One fact from the deleted section was worth keeping because it is specific to
this repo rather than to DiscordSRV: the patch only lands on the second
container start, and a repeat converge will not trigger one. That moves into
the comment above the task that installs the patch.

107 lines, down from 180.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016sBzgNzdYA8b3YrXR3DJrA
@shenanigansd
shenanigansd requested a review from a team as a code owner September 3, 2026 04:06
@shenanigansd
shenanigansd merged commit 86ebe89 into main Sep 3, 2026
4 checks passed
@shenanigansd
shenanigansd deleted the claude/discordsrv-ansible-integration-mpmoyb branch September 3, 2026 04:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants