Skip to content

Prepare Discord bot for production - #1

Merged
Maximooch merged 3 commits into
mainfrom
codex/discord-bot-production-readiness
Aug 11, 2026
Merged

Maximooch merged 3 commits into
mainfrom
codex/discord-bot-production-readiness

Conversation

@Maximooch

@Maximooch Maximooch commented Aug 9, 2026 •

Copy link
Copy Markdown
Owner

What changed

  • replaces prefix commands with ten consistent Discord slash commands
  • centralizes management and team-leader authorization using configured role IDs
  • replaces mutable JSON writes with transactional SQLite storage and one-time legacy JSON import
  • fixes draft picking so the selected player is recorded and added to the picking team atomically
  • protects team membership, leadership, roster capacity, match uniqueness, and score invariants
  • adds strict environment validation, startup/deployment scripts, graceful shutdown, and safe error replies
  • adds a multi-stage non-root Docker image, heartbeat health check, and /data persistent-volume contract for Coolify
  • documents local setup, command registration, configuration, SQLite backups, and Coolify deployment
  • adds GitHub Actions plus tests for configuration, permissions, slash-command definitions, teams, drafts, and matches

Why

The imported prototype had a guaranteed crash in /draft pick, allowed any server member to change tournament matches, advertised commands that were not registered, depended on privileged gateway intents, and stored live state in JSON files that could be corrupted or lost during a Coolify redeploy.

User and operator impact

  • users interact through discoverable slash commands, including names containing spaces
  • management-only actions now enforce the configured management role at execution time
  • the bot needs only the non-privileged Guilds gateway intent
  • Coolify must mount persistent storage at /data
  • run npm run deploy:commands once after deployment or whenever command definitions change
  • the first startup imports the tracked legacy JSON data into an empty SQLite database

Validation

  • 11 Node test-runner tests pass inside the Linux Node 22 Docker test stage
  • production Docker image builds successfully on the target OVH VPS
  • legacy migration smoke test imports 3 teams and active Week 2
  • final image verified as user node, with a /data volume and heartbeat health check
  • all JavaScript files pass node --check
  • npm audit --omit=dev reports zero known vulnerabilities
  • staged secret scan is clean; .env, node_modules, and the source archive were never committed

Summary by CodeRabbit

  • New Features

    • Added a revamped Discord tournament bot with slash commands for teams, player identities, drafts, matches, scores, weeks, results, and help.
    • Added permission-based management and team-leader controls, including role synchronization.
    • Added reliable persistent league data with validation, backups, migration safeguards, and recovery.
    • Added Docker and Compose deployment support with persistent storage and health monitoring.
  • Documentation

    • Added setup, configuration, command, testing, and deployment guidance.
  • Tests

    • Added comprehensive coverage for commands, permissions, configuration, persistence, drafts, teams, and matches.

@coderabbitai

coderabbitai Bot commented Aug 9, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@Maximooch, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 50 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e8dae6f3-0e67-486d-b721-493df932c20e

📥 Commits

Reviewing files that changed from the base of the PR and between 4df1cb5 and 50fb617.

📒 Files selected for processing (2)
  • discord-bot/src/store.js
  • discord-bot/test/store.test.js
📝 Walkthrough

Walkthrough

The PR replaces the inline Discord bot with a modular slash-command system. It adds LeagueStore for JSON state, transactional recovery, validation, and draft management. It also adds configuration, deployment, health checks, Docker support, CI, tests, and documentation.

Changes

Discord tournament bot

Layer / File(s) Summary
LeagueStore persistence and domain state
discord-bot/src/store.js, discord-bot/data/*, discord-bot/test/store.test.js
Adds team, match, draft, validation, JSON persistence, transaction recovery, seeding, and migration safeguards.
Slash command factories
discord-bot/src/commands/*, discord-bot/src/permissions.js, discord-bot/commands/team.js, discord-bot/test/commands.test.js, discord-bot/test/permissions.test.js
Adds basic, team, draft, and tournament slash commands with shared authorization and role synchronization. Removes the previous team command implementation.
Configuration and bot lifecycle
discord-bot/src/config.js, discord-bot/src/index.js, discord-bot/src/deploy-commands.js, discord-bot/index.js, discord-bot/deploy-commands.js, discord-bot/package.json, discord-bot/scripts/healthcheck.js
Adds validated configuration, command registration, startup, heartbeat updates, interaction handling, graceful shutdown, and health checks.
Container deployment and repository integration
discord-bot/Dockerfile, discord-bot/docker-compose.example.yml, discord-bot/.env.example, discord-bot/.dockerignore, .github/workflows/discord-bot.yml, README.md, discord-bot/README.md, .gitignore
Adds Node.js 22 container builds, persistent /data storage, CI execution, environment examples, ignore rules, and deployment documentation.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Discord
  participant BotEntrypoint
  participant LeagueStore
  participant Healthcheck
  Discord->>BotEntrypoint: send slash command
  BotEntrypoint->>LeagueStore: execute domain operation
  LeagueStore-->>BotEntrypoint: return state or DomainError
  BotEntrypoint-->>Discord: send public or private reply
  BotEntrypoint->>Healthcheck: refresh healthy heartbeat
Loading

Poem

I’m a rabbit with a tidy new bot,
JSON shelves hold every lot.
Teams and drafts now hop in line,
Docker keeps the burrow fine.
Tests thump softly: all is bright!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the pull request's primary objective of preparing the Discord bot for production.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/discord-bot-production-readiness

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Maximooch
Maximooch marked this pull request as ready for review August 11, 2026 07:45
@Maximooch
Maximooch merged commit 0aeab04 into main Aug 11, 2026
1 of 2 checks passed
@Maximooch
Maximooch deleted the codex/discord-bot-production-readiness branch August 11, 2026 07:51

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/discord-bot.yml:
- Around line 30-33: Add vulnerability scanning to the workflow by inserting an
npm audit step using `npm audit --omit=dev` alongside the existing npm and
Docker validation steps. Also add a secret-scanning step if supported by the
repository’s existing tooling, ensuring both checks run automatically for future
changes.
- Around line 3-12: Add a workflow-level concurrency configuration alongside the
existing on.push and on.pull_request triggers in the discord-bot workflow, using
a group derived from the workflow’s ref and enabling cancellation of in-progress
runs. Keep independent refs in separate groups so only superseded runs for the
same ref are canceled.
- Around line 24-25: Update the actions/checkout@v4 step to set
persist-credentials to false, since this workflow only builds Docker images and
does not push to the repository. Also pin both actions/checkout and
actions/setup-node to immutable commit SHAs instead of mutable v4 tags.

In `@discord-bot/data/teams.json`:
- Around line 3-18: Remove the real Discord IDs and usernames from the committed
teams seed data. Replace discord-bot/data/teams.json with an empty or anonymized
roster, and document that operators provide the real roster through
LeagueStore’s seedDirectory or DATA_DIR persistence; ensure no real identities
remain in the tracked file.
- Line 55: Prevent backslashes in team names from breaking Discord markdown by
updating `#validateTeamName` to reject them, or by consistently escaping team
names before bold interpolation. Ensure every reply that wraps team.name in
**...** uses the shared escapeMarkdown helper, including the draft command,
while preserving valid team-name behavior.

In `@discord-bot/Dockerfile`:
- Line 39: Update the Dockerfile CMD to launch the Node application directly
rather than through npm, ensuring the Node process runs as PID 1 and receives
SIGTERM for graceful shutdown and cleanup.
- Around line 32-34: Remove the VOLUME instruction for "/data" from the
Dockerfile while preserving the mkdir/chown command and USER node configuration;
rely on deployment configuration such as the named volume in
docker-compose.example.yml or Coolify for persistent storage.

In `@discord-bot/README.md`:
- Around line 45-53: Implement the SQLite persistence contract across all
affected sites: update discord-bot/README.md lines 45-53 and README.md line 96
to describe SQLite as the live source of truth and JSON as one-time migration
input; replace discord-bot/test/config.test.js lines 14-25 with coverage for the
SQLite path, transactional persistence, and one-time JSON import; and update
.gitignore lines 9-15 to ignore the SQLite database plus its -wal and -shm
sidecars. Ensure the application configuration and persistence implementation
provide the documented SQLite path and behavior.

In `@discord-bot/scripts/healthcheck.js`:
- Around line 7-12: Update the heartbeat interval and healthcheck flow to write
only when client.ws.status equals Status.Ready, and invalidate the heartbeat
when Events.ShardDisconnect occurs. Ensure a permanent gateway disconnect cannot
leave a previously written heartbeat reporting the bot as healthy.

In `@discord-bot/src/commands/basic.js`:
- Around line 44-47: Add one shared line-splitting helper and use it before
interaction.reply() for /teams in basic.js, /draft view and /draft status in
draft.js, and /results in tournament.js; ensure every generated message stays
within Discord’s 2,000-character limit while preserving line boundaries and
existing empty responses. Add boundary tests covering responses exceeding 2,000
characters for all affected commands. Affected sites:
discord-bot/src/commands/basic.js:44-47, discord-bot/src/commands/draft.js:54-55
and 61-64, and discord-bot/src/commands/tournament.js:43-49; update each site to
paginate through the shared helper.

In `@discord-bot/src/index.js`:
- Around line 31-32: Update the missing-command branch after
client.commands.get(interaction.commandName) to send an ephemeral
unavailable-command reply before returning. Preserve the existing command
execution flow when a handler is found.

In `@discord-bot/src/permissions.js`:
- Around line 1-2: The hasRole function in discord-bot/src/permissions.js must
support both Discord cached roles via roles.cache.has(roleId) and API-shaped
role ID arrays via roles.includes(roleId). Add array-shaped management and Team
Leader permission cases in discord-bot/test/permissions.test.js at lines 14-16
to verify both roles are recognized.

In `@discord-bot/src/store.js`:
- Around line 231-239: Update renameTeam to replace the inline changes Map,
redundant state.draft assignment, `#validateCore`, and `#commitValues` sequence with
the shared `#saveCore`(state, ["teams", "draft"]) call. Preserve the existing
return through `#hydrateTeam`(team, index).
- Around line 454-461: Update pickDraftPlayer and its command-layer caller so
the drafted member’s Discord display name is resolved via
interaction.guild.members.fetch and passed into the draft operation. Store that
resolved display name in found.team.members.name and retain player.ign only for
the players.json-backed draft data, keeping hydrated roster entries consistent
with createTeam and addTeamMember.
- Around line 680-690: Update `#syncDirectory` to include EPERM among the
fsyncSync error codes it ignores, while continuing to propagate EACCES and all
other unexpected errors.

In `@discord-bot/test/commands.test.js`:
- Around line 12-16: Extend the command registration test beyond the existing
draft assertion: validate option names for every command in the payload that
declares subcommands or options, including team, match, results, and week. Reuse
the established payload lookup and deep-equality assertion pattern, preserving
the expected declaration order for each command.

In `@discord-bot/test/store.test.js`:
- Around line 200-214: Extend the store tests to directly cover roster
reservation and substitute limits. Add a locked-draft case where a participating
team with three main-roster members cannot add another member before picking,
and add setMemberRole coverage verifying substitute assignments work only up to
the limit of two and further assignments are rejected.
- Around line 270-281: Remove the vacuous rosteredIds/draft-player assertion
from the shipped-data test and rename the test to reflect only active-week and
archived-match validation. Preserve the existing store.getActiveWeek() and
store.listMatches(1) assertions unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3ac3a424-7620-4e1e-b65b-1ee667429f07

📥 Commits

Reviewing files that changed from the base of the PR and between 0fdb498 and 4df1cb5.

⛔ Files ignored due to path filters (1)
  • discord-bot/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (39)
  • .github/workflows/discord-bot.yml
  • .gitignore
  • README.md
  • discord-bot/.dockerignore
  • discord-bot/.env.example
  • discord-bot/Dockerfile
  • discord-bot/README.md
  • discord-bot/commands/draft.js
  • discord-bot/commands/help.js
  • discord-bot/commands/ign.js
  • discord-bot/commands/matches.js
  • discord-bot/commands/ping.js
  • discord-bot/commands/r.js
  • discord-bot/commands/results.js
  • discord-bot/commands/team.js
  • discord-bot/commands/teams.js
  • discord-bot/commands/week.js
  • discord-bot/data/draft.json
  • discord-bot/data/teams.json
  • discord-bot/deploy-commands.js
  • discord-bot/docker-compose.example.yml
  • discord-bot/index.js
  • discord-bot/package.json
  • discord-bot/scripts/healthcheck.js
  • discord-bot/src/commands/basic.js
  • discord-bot/src/commands/draft.js
  • discord-bot/src/commands/helpers.js
  • discord-bot/src/commands/index.js
  • discord-bot/src/commands/team.js
  • discord-bot/src/commands/tournament.js
  • discord-bot/src/config.js
  • discord-bot/src/deploy-commands.js
  • discord-bot/src/index.js
  • discord-bot/src/permissions.js
  • discord-bot/src/store.js
  • discord-bot/test/commands.test.js
  • discord-bot/test/config.test.js
  • discord-bot/test/permissions.test.js
  • discord-bot/test/store.test.js
💤 Files with no reviewable changes (10)
  • discord-bot/commands/team.js
  • discord-bot/commands/draft.js
  • discord-bot/commands/teams.js
  • discord-bot/commands/ign.js
  • discord-bot/commands/r.js
  • discord-bot/commands/results.js
  • discord-bot/commands/help.js
  • discord-bot/commands/matches.js
  • discord-bot/commands/week.js
  • discord-bot/commands/ping.js

Comment on lines +3 to +12
on:
pull_request:
paths:
- "discord-bot/**"
- ".github/workflows/discord-bot.yml"
push:
branches: [main]
paths:
- "discord-bot/**"
- ".github/workflows/discord-bot.yml"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Add a concurrency group to cancel superseded runs.

The job runs npm test plus two docker build steps. Every push to an open pull request starts a full duplicate run, and the previous run keeps consuming a runner. Cancel in-progress runs for the same ref.

♻️ Proposed change
 permissions:
   contents: read
+
+concurrency:
+  group: discord-bot-${{ github.ref }}
+  cancel-in-progress: ${{ github.event_name == 'pull_request' }}
🧰 Tools
🪛 zizmor (1.29.0)

[warning] 3-12: insufficient job-level concurrency limits (concurrency-limits): workflow is missing concurrency setting

(concurrency-limits)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/discord-bot.yml around lines 3 - 12, Add a workflow-level
concurrency configuration alongside the existing on.push and on.pull_request
triggers in the discord-bot workflow, using a group derived from the workflow’s
ref and enabling cancellation of in-progress runs. Keep independent refs in
separate groups so only superseded runs for the same ref are canceled.

Source: Linters/SAST tools

Comment on lines +24 to +25
- uses: actions/checkout@v4
- uses: actions/setup-node@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Disable credential persistence on checkout.

actions/checkout writes an extended http.extraheader credential into .git/config by default. The job then runs two docker build steps. Set persist-credentials: false, because no step in this job pushes to the repository.

Consider also pinning both actions to a commit SHA. This is what the unpinned-uses hint asks for, and it removes the risk of a moved v4 tag.

🔒 Proposed change
-      - uses: actions/checkout@v4
-      - uses: actions/setup-node@v4
+      - uses: actions/checkout@v4
+        with:
+          persist-credentials: false
+      - uses: actions/setup-node@v4
         with:
           node-version: 22
           cache: npm
           cache-dependency-path: discord-bot/package-lock.json
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: discord-bot/package-lock.json
🧰 Tools
🪛 zizmor (1.29.0)

[warning] 24-24: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 24-24: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 25-25: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/discord-bot.yml around lines 24 - 25, Update the
actions/checkout@v4 step to set persist-credentials to false, since this
workflow only builds Docker images and does not push to the repository. Also pin
both actions/checkout and actions/setup-node to immutable commit SHAs instead of
mutable v4 tags.

Source: Linters/SAST tools

Comment on lines +30 to +33
- run: npm ci
- run: npm test
- run: docker build --target test .
- run: docker build .

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

The workflow does not perform the vulnerability scanning or secret scanning that the PR objectives claim.

The PR objectives state that the implementation was validated with vulnerability scanning and secret scanning. This job runs only npm ci, npm test, and two docker build steps. If the scans were manual one-time checks, they do not protect later changes.

Add npm audit --omit=dev as a step, or state in discord-bot/README.md that the scans are manual.

Do you want me to add the audit and secret-scanning steps to this workflow?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/discord-bot.yml around lines 30 - 33, Add vulnerability
scanning to the workflow by inserting an npm audit step using `npm audit
--omit=dev` alongside the existing npm and Docker validation steps. Also add a
secret-scanning step if supported by the repository’s existing tooling, ensuring
both checks run automatically for future changes.

Comment on lines +3 to +18
"name": "GREAT WHITES",
"leader": "909918639191392316",
"members": [
{
"id": "165732283167408128",
"name": ".bluwu",
"id": "909918639191392316",
"name": "paris_labrador",
"role": "leader"
},
{
"id": "717748215469834263",
"name": "mekrozz",
"id": "396401739126800384",
"name": "supermoar",
"role": "player"
},
{
"id": "564980967719043078",
"name": "higuy333",
"id": "1091498015107321979",
"name": "ryxei",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not commit real Discord identities to the repository.

This seed file pairs persistent Discord user IDs with usernames for 11 real accounts. Discord IDs are stable account identifiers, so this is personal data in version control and in every clone, fork, and image layer built from this repository.

The PR already supports a separate data path. LeagueStore accepts a seedDirectory and DATA_DIR persistence, so the seed does not need to live in git.

Choose one of these options:

  • Move the real roster to the /data volume and ship an anonymized or empty discord-bot/data/teams.json.
  • Keep the file but replace member name values with placeholders, and document that operators seed real rosters through DATA_DIR.

Note that removing the data in a later commit does not remove it from git history.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@discord-bot/data/teams.json` around lines 3 - 18, Remove the real Discord IDs
and usernames from the committed teams seed data. Replace
discord-bot/data/teams.json with an empty or anonymized roster, and document
that operators provide the real roster through LeagueStore’s seedDirectory or
DATA_DIR persistence; ensure no real identities remain in the tracked file.

{
"name": "Escape Artists",
"leader": "653965827883794435",
"name": "b9\\",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The team name b9\ breaks Discord markdown in bot replies.

The JSON value is the literal string b9\. Commands interpolate team names into bold spans. For example, discord-bot/src/commands/draft.js line 35 builds `**${team.name}** joined the draft.`. That produces **b9\**, where the backslash escapes the first closing *. Discord then renders a literal * and leaves the text unbolded, and the trailing content of the message can also change.

Escape user-controlled team names before interpolation, or reject backslashes in #validateTeamName.

🐛 Proposed escaping helper for reply formatting
+// discord-bot/src/commands/helpers.js
+function escapeMarkdown(value) {
+    return String(value).replace(/[\\*_`~|>]/g, character => `\\${character}`);
+}

Then use escapeMarkdown(team.name) in every reply that wraps a team name in **...**.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@discord-bot/data/teams.json` at line 55, Prevent backslashes in team names
from breaking Discord markdown by updating `#validateTeamName` to reject them, or
by consistently escaping team names before bold interpolation. Ensure every
reply that wraps team.name in **...** uses the shared escapeMarkdown helper,
including the draft command, while preserving valid team-name behavior.

Comment thread discord-bot/src/store.js
Comment on lines +454 to +461
found.team.members.push({ id: player.id, name: player.ign, role: "player" });
state.draft.picked.push({
pick: state.draft.currentPick + 1,
player: player.ign,
id: player.id,
pickedBy: leaderId,
team: found.team.name
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Drafted members store the IGN in the display-name field.

Line 454 sets name to player.ign. createTeam line 115 and addTeamMember line 130 set name to the Discord display name. #hydrateTeam line 1040 exposes that field as displayName for both cases.

The result is a mixed roster listing. Manually added members show a Discord display name. Drafted members show a Minecraft IGN.

Pass the display name into pickDraftPlayer from the command layer, which can resolve it from the guild, and keep the IGN in players.json only.

🐛 Proposed signature change
-    pickDraftPlayer(leaderId, ign) {
+    pickDraftPlayer(leaderId, ign, resolveDisplayName = player => player.ign) {
@@
-        found.team.members.push({ id: player.id, name: player.ign, role: "player" });
+        found.team.members.push({ id: player.id, name: resolveDisplayName(player), role: "player" });

The caller in discord-bot/src/commands/draft.js then supplies a resolver backed by interaction.guild.members.fetch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@discord-bot/src/store.js` around lines 454 - 461, Update pickDraftPlayer and
its command-layer caller so the drafted member’s Discord display name is
resolved via interaction.guild.members.fetch and passed into the draft
operation. Store that resolved display name in found.team.members.name and
retain player.ign only for the players.json-backed draft data, keeping hydrated
roster entries consistent with createTeam and addTeamMember.

Comment thread discord-bot/src/store.js
Comment on lines +680 to +690
#syncDirectory(directory) {
let descriptor;
try {
descriptor = fs.openSync(directory, "r");
fs.fsyncSync(descriptor);
} catch (error) {
if (!["EINVAL", "ENOTSUP", "EBADF"].includes(error.code)) throw error;
} finally {
if (descriptor !== undefined) fs.closeSync(descriptor);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Node.js fs.openSync on a directory Windows EISDIR fsync directory not supported

💡 Result:

In Node.js, attempting to perform operations on a directory that are designed for files—such as fs.fsyncSync—will typically fail on Windows with errors like EPERM (operation not permitted) because the underlying Windows APIs do not support these operations for directory handles [1][2][3]. The issues you are encountering stem from fundamental platform differences: 1. fs.openSync on a directory: On Windows, it is possible to obtain a file descriptor for a directory using fs.openSync [4]. This behavior differs from many POSIX-compliant systems (like Linux or macOS), which typically throw an EISDIR error when attempting to open a directory as a file [5][4][6]. While this may allow you to access the directory handle, it does not guarantee that subsequent operations on that handle will succeed [1][7]. 2. fsync on a directory: The fs.fsync (or fsyncSync) method is intended to flush file data and metadata buffers to storage [8]. On Windows, the system does not provide a supported way to perform an equivalent synchronization (like FlushFileBuffers) on a directory handle [3]. Consequently, calling fsync on a directory handle in Node.js on Windows will result in an EPERM error [1][2][7]. Recommendations: - Avoid fsyncing directories: Since this is not supported on Windows and does not have a reliable, cross-platform equivalent, you should structure your code to avoid calling fsync on directory handles [1][3]. - Platform-specific handling: If your application requires this functionality on POSIX systems but must run on Windows, use a try-catch block to handle or ignore the EPERM error when running on Windows [1]. - Use appropriate APIs: For operations involving directories (such as reading contents or checking metadata), use the dedicated Node.js fs directory APIs like fs.opendir, fs.readdir, or fs.stat, which are designed to handle directories correctly across all platforms [8][9].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- repository files ---'
git ls-files 'discord-bot/src/store.js' 'discord-bot/package.json' 'package.json' '.github' | head -80

printf '%s\n' '--- relevant symbols and calls ---'
rg -n -C 8 '`#syncDirectory`|`#atomicReplace`|openSync|fsyncSync|renameSync|mkdirSync' discord-bot/src/store.js

printf '%s\n' '--- runtime and platform context ---'
rg -n -C 3 '"(engines|node)"|Node.js|Windows|Docker|docker' discord-bot/package.json package.json README.md discord-bot 2>/dev/null | head -160

Repository: Maximooch/DECL

Length of output: 13638


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- deployment workflow ---'
cat -n .github/workflows/discord-bot.yml

printf '%s\n' '--- package metadata ---'
cat -n discord-bot/package.json | sed -n '1,35p'

printf '%s\n' '--- local POSIX probe for directory open/fsync ---'
python3 - <<'PY'
import errno, os, tempfile

with tempfile.TemporaryDirectory() as directory:
    descriptor = None
    try:
        descriptor = os.open(directory, os.O_RDONLY)
        print("open:", "success")
        try:
            os.fsync(descriptor)
            print("fsync:", "success")
        except OSError as error:
            print("fsync:", error.errno, errno.errorcode.get(error.errno), error.strerror)
    except OSError as error:
        print("open:", error.errno, errno.errorcode.get(error.errno), error.strerror)
    finally:
        if descriptor is not None:
            os.close(descriptor)
PY

Repository: Maximooch/DECL

Length of output: 1898


🌐 Web query:

Node.js v22 fsyncSync directory Windows EPERM EISDIR EACCES fs.openSync directory exact error codes

💡 Result:

In Node.js on Windows, attempting to call fs.fsyncSync on a file descriptor opened for a directory will result in an EPERM (Operation not permitted) error [1][2][3]. This behavior occurs because the underlying Windows API used by Node.js, FlushFileBuffers, does not support directory handles [1]. While this is a standard and supported POSIX idiom on Unix-like operating systems (Linux, macOS), Windows does not permit syncing directory metadata in this manner [1][3]. Key takeaways regarding this error: - Exact Error: EPERM: operation not permitted, fsync [1][4][3]. - Technical Cause: Windows prevents FlushFileBuffers from operating on directory handles [1]. - Node.js Behavior: Node.js surfaces this operating system limitation directly to the developer [1][3]. - EISDIR/EACCES: While EPERM is the specific error encountered when attempting the sync operation itself, developers may encounter other error codes depending on how the directory is initially opened (e.g., using incorrect flags) or how the filesystem handles directory access, but EPERM is the documented result of the fsync attempt [1][4][3]. Developers encountering this issue should wrap the fsync call in a try/catch block to handle the error gracefully or implement logic to skip directory fsync operations specifically when running on Windows [1][2].

Citations:


🌐 Web query:

site:github.com/nodejs/node fsyncSync directory Windows EPERM fs.openSync source

💡 Result:

In Node.js, attempting to call fs.fsyncSync on a directory descriptor on Windows results in an EPERM (Operation not permitted) error [1]. This behavior stems from fundamental differences between Windows and POSIX-compliant operating systems regarding file system operations. On many POSIX systems, it is possible to open a directory for read-only access and successfully perform an fsync operation [1]. However, Windows does not support this in the same way; attempting to open a directory for this purpose and subsequently syncing it is not a supported or reliable operation [1]. Key points regarding this behavior include: 1. OS Differences: The underlying implementation of fsync is handled by libuv, which maps to operating system-specific system calls [2][3]. Windows does not allow flushing directory handles via fsync in the manner often used on Unix-like platforms [1]. 2. Permission Errors: When you open a directory using fs.openSync (e.g., with 'r' or 'r+' flags) and then call fs.fsyncSync on the resulting file descriptor, Windows may reject the fsync call with EPERM because the operation is invalid for directory handles in this context [1]. 3. Alternative Approaches: If your goal is to ensure directory metadata or changes are persisted, there is no direct cross-platform equivalent to fsyncing a directory. Developers encountering this issue are generally advised that this is a limitation of the Windows file system API and not a Node.js-specific bug [1]. If you are using fs.openSync to check permissions or handle directories, be aware that Windows behavior often diverges from POSIX. For checking permissions specifically, relying on fs.accessSync or fs.openSync can be unreliable due to how Windows handles Access Control Lists (ACLs); the most robust way to determine if an operation is permitted is to perform the operation (e.g., writing a file) and handle any resulting errors [4][5].

Citations:


🌐 Web query:

site:nodejs.org/api/fs.html fsyncSync Windows directory EPERM

💡 Result:

On Windows, calling fsyncSync on a directory file descriptor will result in an EPERM error because the underlying operating system does not support the fsync operation for directory handles [1]. The fsync operation is intended to flush data for an open file descriptor to the storage device [1]. Because this implementation is operating-system-specific, Node.js follows the behavior of the host OS. On Windows, while fsync is supported for regular files, it is not a valid operation for directories, leading the system to return an EPERM (operation not permitted) error [1]. To avoid this error, ensure that fsyncSync is only called on file descriptors associated with files rather than directories.

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- all directory-sync references and error handling ---'
rg -n -C 5 'syncDirectory|EINVAL|ENOTSUP|EBADF|EISDIR|EPERM|EACCES' discord-bot --glob '!package-lock.json'

printf '%s\n' '--- Docker platform declarations ---'
rg -n -C 4 'FROM|platform|node:' discord-bot/Dockerfile discord-bot 2>/dev/null | head -100

Repository: Maximooch/DECL

Length of output: 8247


Ignore Windows EPERM from directory fsyncSync.

On Windows, fsyncSync on a directory descriptor returns EPERM. Since this code does not ignore that error, persistent writes fail. Ignore EPERM while preserving unexpected permission errors such as EACCES.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@discord-bot/src/store.js` around lines 680 - 690, Update `#syncDirectory` to
include EPERM among the fsyncSync error codes it ignores, while continuing to
propagate EACCES and all other unexpected errors.

Comment on lines +12 to +16
const draft = payloads.find(command => command.name === "draft");
assert.deepEqual(
draft.options.map(option => option.name),
["join", "participate", "pick", "view", "status", "open", "lock", "start", "stop"]
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extend the subcommand assertions beyond draft.

The test pins the 9 draft subcommand names. It does not pin the subcommands of team, or the options of match, results, and week. A renamed or dropped team subcommand still passes, and the failure would only appear after npm run deploy:commands registers the wrong surface with Discord.

Assert the option names for every command that declares subcommands or options.

♻️ Proposed extension
     const draft = payloads.find(command => command.name === "draft");
     assert.deepEqual(
         draft.options.map(option => option.name),
         ["join", "participate", "pick", "view", "status", "open", "lock", "start", "stop"]
     );
+
+    const team = payloads.find(command => command.name === "team");
+    assert.deepEqual(
+        team.options.map(option => option.name),
+        [/* expected team subcommand names */]
+    );
+
+    for (const name of ["match", "results", "week"]) {
+        const command = payloads.find(payload => payload.name === name);
+        assert.ok(command.description, `${name} needs a description`);
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@discord-bot/test/commands.test.js` around lines 12 - 16, Extend the command
registration test beyond the existing draft assertion: validate option names for
every command in the payload that declares subcommands or options, including
team, match, results, and week. Reuse the established payload lookup and
deep-equality assertion pattern, preserving the expected declaration order for
each command.

Comment on lines +200 to +214
test("full-roster teams cannot enter and deadlock the draft", () => withStore(store => {
store.createTeam("Red Dragons", member("leader-a"));
store.addTeamMember("Red Dragons", member("player-a"));
store.addTeamMember("Red Dragons", member("player-b"));
store.addTeamMember("Red Dragons", member("player-c"));
store.openDraft();
store.joinDraft({ id: "draft-player", ign: "DraftPlayer" });
store.lockDraft();

assert.throws(
() => store.joinDraftAsTeam("leader-a"),
error => error instanceof DomainError && /roster|full|4/i.test(error.message)
);
assert.throws(() => store.startDraft(() => 0), /No teams have joined/);
}));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add direct coverage for the roster-reservation and substitute rules.

The suite exercises the hard limit of 4 main-roster players. It does not cover two new invariants from discord-bot/src/store.js:

  • The reservation rule at line 1011. A team with 3 main-roster members that is participating in a locked draft must be blocked from addTeamMember until it picks.
  • setMemberRole and the substitute limit of 2 at line 1007.

Both rules gate user-facing commands and have no direct test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@discord-bot/test/store.test.js` around lines 200 - 214, Extend the store
tests to directly cover roster reservation and substitute limits. Add a
locked-draft case where a participating team with three main-roster members
cannot add another member before picking, and add setMemberRole coverage
verifying substitute assignments work only up to the limit of two and further
assignments are rejected.

Comment on lines +270 to +281
test("shipped JSON keeps archived matches readable and draft players unrostered", () => withStore(store => {
const teams = JSON.parse(fs.readFileSync(path.join(SHIPPED_DATA_DIRECTORY, "teams.json"), "utf8"));
const draft = JSON.parse(fs.readFileSync(path.join(SHIPPED_DATA_DIRECTORY, "draft.json"), "utf8"));
const rosteredIds = new Set(teams.flatMap(team => (team.members || []).map(person => person.id)));

assert.equal(draft.players.some(player => rosteredIds.has(player.id)), false);
assert.equal(store.getActiveWeek(), 2);
assert.deepEqual(
store.listMatches(1).map(({ team1, team2, score1, score2 }) => ({ team1, team2, score1, score2 })),
[{ team1: "N64", team2: "WinnersPOV", score1: 109, score2: 61 }]
);
}, { shipped: true }));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check shipped league data against the assertions in the shipped-data test.
set -euo pipefail

fd -t d 'data' discord-bot -d 2 || true
fd -t f -e json . discord-bot/data | sort

for file in $(fd -t f -e json . discord-bot/data | sort); do
  echo "=== $file ==="
  jq -c '.' "$file" | head -c 2000
  echo
done

Repository: Maximooch/DECL

Length of output: 2093


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== test context ==='
sed -n '240,290p' discord-bot/test/store.test.js

echo '=== shipped-data references ==='
rg -n "SHIPPED_DATA_DIRECTORY|shipped:|draft\.players|listMatches|getActiveWeek" discord-bot/test discord-bot --glob '!discord-bot/data/*.json' --glob '!discord-bot/data/tournaments/**/*.json' | head -n 200

echo '=== relevant data shapes ==='
python3 - <<'PY'
import json
from pathlib import Path

root = Path("discord-bot/data")
teams = json.loads((root / "teams.json").read_text())
draft = json.loads((root / "draft.json").read_text())
active = json.loads((root / "tournaments/active.json").read_text())
week1 = json.loads((root / "tournaments/weeks/week1.json").read_text())

rostered = {member["id"] for team in teams for member in team.get("members", [])}
print("draft_players:", len(draft["players"]))
print("draft_players_rostered:", [p for p in draft["players"] if p["id"] in rostered])
print("active_week:", active["week"])
print("week1_matches:", week1)
print("week1_match_count:", len(week1))
print("week1_team_names:", sorted({name for match in week1 for name in (match["team1"], match["team2"])}))
print("shipped_team_names:", [team["name"] for team in teams])
PY

Repository: Maximooch/DECL

Length of output: 8977


Remove the vacuous draft-player assertion.

discord-bot/data/draft.json contains no players, so line 275 always passes. Remove the assertion and rename the test, or add a non-rostered draft player to make the invariant testable. The active-week and archived-match assertions match the shipped data.

🧰 Tools
🪛 ast-grep (0.45.1)

[warning] 270-270: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.readFileSync(path.join(SHIPPED_DATA_DIRECTORY, "teams.json"), "utf8")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(detect-non-literal-fs-filename)


[warning] 271-271: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.readFileSync(path.join(SHIPPED_DATA_DIRECTORY, "draft.json"), "utf8")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(detect-non-literal-fs-filename)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@discord-bot/test/store.test.js` around lines 270 - 281, Remove the vacuous
rosteredIds/draft-player assertion from the shipped-data test and rename the
test to reflect only active-week and archived-match validation. Preserve the
existing store.getActiveWeek() and store.listMatches(1) assertions unchanged.

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.

1 participant