fix(bot): survive a create whose body does not arrive (#164) #493
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| pull_request: | |
| push: | |
| branches: [main] | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| concurrency: | |
| group: ci-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| validate: | |
| runs-on: ubuntu-latest | |
| env: | |
| SKIP_ENV_VALIDATION: "1" | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| - uses: ./.github/actions/setup | |
| - run: bunx oxfmt --check . | |
| # `--deny-warnings` is CLI-only — oxlint's config has no equivalent — and it is the | |
| # difference between the ~50 warn-level oxclippy rules gating CI and gating nothing. | |
| # It has been dropped from the script twice; fail loudly rather than quietly weaken. | |
| - name: Lint gate is intact | |
| run: | | |
| lint=$(bun -e 'console.log((await Bun.file("package.json").json()).scripts.lint)') | |
| case "$lint" in | |
| *--deny-warnings*) ;; | |
| *) echo "::error::lint script lost --deny-warnings; warn-level rules would stop gating CI"; exit 1 ;; | |
| esac | |
| # oxlint is type-aware (tsgolint), so it reports TS errors too — there is no separate tsc pass. | |
| - run: bun run lint | |
| # Cross-file invariants a code review cannot see: a skill referencing a tool the registry | |
| # does not define, a tool no role can reach, a half-wired subagent. Each package script does | |
| # one thing, so CI composes them here rather than one script invoking another. | |
| - run: bun run check:capabilities | |
| working-directory: packages/agents | |
| - run: bun run check:serialization | |
| working-directory: packages/agents | |
| - name: Validate database migrations | |
| run: | | |
| database="$(mktemp /tmp/wack-hacker-ci-XXXXXX.db)" | |
| trap 'rm -f "$database"' EXIT | |
| cd packages/shared | |
| TURSO_DATABASE_URL="file:$database" bunx drizzle-kit check | |
| TURSO_DATABASE_URL="file:$database" bun run db:migrate | |
| - run: bun run build | |
| - run: docker build --platform linux/amd64 -f packages/bot/Dockerfile -t wack-hacker-bot:ci . | |
| # A schema change is the one thing CI cannot undo after the fact, so it is | |
| # reviewed before it merges rather than discovered when `main` applies it. | |
| migrations: | |
| if: github.event_name == 'pull_request' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| with: | |
| # The reviewer diffs against the base branch, so a shallow clone has | |
| # nothing to compare with. | |
| fetch-depth: 0 | |
| - uses: ./.github/actions/setup | |
| - name: Review schema changes | |
| id: review | |
| run: | | |
| set -euo pipefail | |
| bun packages/shared/scripts/review-migrations.ts \ | |
| "origin/${{ github.base_ref }}" | tee review.md | |
| - name: Post the review to the pull request | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| PR: ${{ github.event.pull_request.number }} | |
| run: | | |
| set -euo pipefail | |
| marker="<!-- migration-review -->" | |
| body="$marker"$'\n'"$(cat review.md)" | |
| existing="$(gh api "repos/${GITHUB_REPOSITORY}/issues/${PR}/comments" \ | |
| --jq "[.[] | select(.body | startswith(\"$marker\"))][0].id // empty")" | |
| if [ -n "$existing" ]; then | |
| gh api --method PATCH "repos/${GITHUB_REPOSITORY}/issues/comments/${existing}" \ | |
| -f body="$body" >/dev/null | |
| else | |
| gh api --method POST "repos/${GITHUB_REPOSITORY}/issues/${PR}/comments" \ | |
| -f body="$body" >/dev/null | |
| fi | |
| # Destructive statements are not forbidden, but they stop being something | |
| # that can happen without anyone noticing: production migrates on merge. | |
| - name: Block on data loss | |
| if: steps.review.outputs.blocking == 'true' | |
| run: | | |
| echo "::error::this branch drops or rebuilds existing data — see the migration review comment" >&2 | |
| exit 1 | |
| # Slash commands are guild-scoped and registered by an explicit PUT that replaces the whole | |
| # set, so this is idempotent and safe to repeat. It runs only after a merge to main, and only | |
| # once `validate` has passed — never on a pull request, where a fork could otherwise reach a | |
| # real Discord application. | |
| register-commands: | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| needs: validate | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| - uses: ./.github/actions/setup | |
| - run: bun run register-commands | |
| working-directory: packages/bot | |
| env: | |
| DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN }} | |
| DISCORD_BOT_CLIENT_ID: ${{ secrets.DISCORD_BOT_CLIENT_ID }} | |
| # The script refuses to run unless this matches the guild id compiled into | |
| # @repo/shared/discord, so a misconfigured secret cannot register elsewhere. | |
| CONFIRM_COMMAND_GUILD: "772576325897945119" |