Skip to content

Modernize: Bolt 4, async/error handling, Node 24 + GHCR/Coolify deploy - #53

Open
ddbruce wants to merge 1 commit into
masterfrom
modernization
Open

Modernize: Bolt 4, async/error handling, Node 24 + GHCR/Coolify deploy#53
ddbruce wants to merge 1 commit into
masterfrom
modernization

Conversation

@ddbruce

@ddbruce ddbruce commented Jul 24, 2026

Copy link
Copy Markdown
Member

Summary

Behavior-preserving modernization of at-channel. Same /channel workflow: blank-request guard, moderated-channel approval flow (mod message + requester ephemeral + cancel DM), non-moderated immediate post, the approve / approve-without-@channel / reject / cancel button actions, and moderator permission checks (including "can't approve your own request"). All Block Kit message content is byte-for-byte unchanged. Verified: 6 unit tests pass; boots on Bolt 4 with /health → 200 and unsigned /slack/events → 401; amd64 image builds with 0 npm vulnerabilities.

Changes

  • SDK/runtime: @slack/bolt 1.2 → 4.4 — this is the substantive change. Rewrote the command/action middleware chains for v4: await ack() and await next() (were unawaited, which v4 requires). Custom HTTPReceiver adds GET /health; /slack/events and signing-secret verification are unchanged, so no Slack app config change. dotenv 8→17, emoji-regex 8→10, md5 2.2→2.3; Dockerfile node:latest → pinned node:24-alpine, npm ci, non-root, HEALTHCHECK; moved ESLint out of runtime deps; nodemon 1→3.
  • Robustness: awaited the previously fire-and-forget Slack calls up the handler chain so failures go through app.error instead of crashing (same messages, deterministic order). Fixed the unreachable randomEmoji default branch (concat results were discarded) — not hit by current callers, so no behavior change.
  • Testability: extracted the duplicated channel/user-ID regex parsing into utilities/parse.js (identical regex) and randomEmoji into utilities/emoji.js; added Jest tests.
  • Tooling/CI: ESLint 6 (EOL) → 9 flat config matching the repo's original rule set (this repo never had a lint script, so two pre-existing >90-char user-facing strings carry a targeted max-len disable to stay byte-identical); dead Drone CI → GitHub Actions.
  • Deploy: scripts/deploy.sh + config.example.sh; secrets in gitignored scripts/config.sh.

Env notes (docs only, no new required vars)

  • .env.example now documents SLACK_USER_TOKEN (the code always used it for usergroup lookups; it was missing from the example).
  • Corrected PORTNODE_PORT in .env.example (the code reads NODE_PORT; the old PORT entry was never read).

Recommended follow-ups (NOT done — would change behavior)

  • IDs are still recovered by regex-parsing rendered block text (fragile if layout changes); the sturdier fix is to carry channel_id/user_id in button values. Left as-is to preserve exact behavior.
  • Moderated channels are still a hardcoded switch on names (general/alerts/random/scheduling), and sendForApproval still posts to the literal channel name "at-channel-requests" rather than MOD_CHANNEL_ID. Both preserved verbatim; worth making config-driven later.
  • Minor user-facing typos ("requsted", "messasge") left untouched to avoid changing output.

… deploy

Behavior-preserving modernization. Same /channel workflow: blank-request
guard, moderated-channel approval flow (mod message + requester ephemeral +
cancel DM), non-moderated immediate post, approve/approve-without-@channel/
reject/cancel button actions, and moderator permission checks (incl. the
"can't approve your own request" case). All Block Kit message content is
byte-for-byte unchanged.

Runtime / SDK:
- @slack/bolt 1.2 -> 4.4. Rewrote the command/action middleware chains for v4:
  await ack() and await next() (were unawaited). Custom HTTPReceiver adds a GET
  /health route; /slack/events and signing-secret verification are unchanged.
- dotenv 8 -> 17; emoji-regex 8 -> 10; md5 2.2 -> 2.3. Dockerfile node:latest ->
  node:24-alpine, npm ci, non-root, HEALTHCHECK.
- Move eslint out of runtime dependencies; nodemon 1 -> 3 (dev).

Correctness / robustness:
- Await the previously fire-and-forget Slack calls (postMessage/postEphemeral/
  update) up the handler chain so failures surface via app.error instead of
  crashing on an unhandled rejection. Same messages, deterministic order.
- Fix the unreachable randomEmoji default branch (concat results were
  discarded). Not hit by current callers, so no behavior change.

Testability / tooling:
- Extract the duplicated channel/user ID regex parsing into utilities/parse.js
  (identical regex) and randomEmoji into utilities/emoji.js; add Jest tests (6).
- ESLint 6 (EOL) -> ESLint 9 flat config, matching the repo's original rule set
  (this repo never had a lint script; two pre-existing >90-char user-facing
  strings keep a targeted max-len disable to stay byte-identical).
- Replace dead Drone CI with GitHub Actions (lint + test + amd64 build).
- Add scripts/deploy.sh + config.example.sh (amd64 -> GHCR -> Coolify redeploy);
  secrets in gitignored scripts/config.sh.
- Document SLACK_USER_TOKEN in .env.example (required by the code, was missing)
  and correct PORT -> NODE_PORT (the code reads NODE_PORT).

Verified: 6 unit tests pass; boots on Bolt 4, /health -> 200, unsigned
/slack/events -> 401; amd64 image builds with 0 npm vulnerabilities.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ddbruce
ddbruce requested a review from lramos15 as a code owner July 24, 2026 16:42
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