diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8d5d374..ef77578 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -174,6 +174,10 @@ jobs: echo "failed=false" >> "$GITHUB_OUTPUT" fi + # Every CI, build and release outcome goes to #pipelines only (operator + # decision 2026-10-06). A failed release is flagged with ๐Ÿšจ because it can + # leave a version on npm without its tag or GitHub release, which blocks + # every following release PR. - name: "Post to #pipelines" uses: slackapi/slack-github-action@v2 with: @@ -181,13 +185,13 @@ jobs: webhook-type: incoming-webhook payload: | { - "text": "${{ steps.payload.outputs.failed == 'true' && '๐Ÿ”ด' || needs.release.outputs.proceed == 'true' && '๐Ÿ“ฆ' || '๐ŸŸข' }} 10x-cli pipeline", + "text": "${{ needs.release.result == 'failure' && '๐Ÿšจ 10x-cli release failure' || format('{0} 10x-cli pipeline', steps.payload.outputs.failed == 'true' && '๐Ÿ”ด' || needs.release.outputs.proceed == 'true' && '๐Ÿ“ฆ' || '๐ŸŸข') }}", "blocks": [ { "type": "header", "text": { "type": "plain_text", - "text": "${{ steps.payload.outputs.failed == 'true' && '๐Ÿ”ด' || needs.release.outputs.proceed == 'true' && '๐Ÿ“ฆ' || '๐ŸŸข' }} 10x-cli${{ needs.release.outputs.proceed == 'true' && format(' โ€” v{0}', needs.release.outputs.version) || '' }}" + "text": "${{ needs.release.result == 'failure' && '๐Ÿšจ' || steps.payload.outputs.failed == 'true' && '๐Ÿ”ด' || needs.release.outputs.proceed == 'true' && '๐Ÿ“ฆ' || '๐ŸŸข' }} 10x-cli${{ needs.release.result == 'failure' && ' โ€” RELEASE FAILURE' || '' }}${{ needs.release.outputs.proceed == 'true' && format(' โ€” v{0}', needs.release.outputs.version) || '' }}" } }, { @@ -202,60 +206,7 @@ jobs: "elements": [ { "type": "mrkdwn", - "text": "๐Ÿ“ <${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}|`${{ github.sha }}`> ${{ steps.payload.outputs.commit_msg }} ยท ๐Ÿ‘ค ${{ github.actor }} ยท <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|๐Ÿ”— Workflow>${{ needs.release.result == 'failure' && ' ยท ๐Ÿ”” see #alerting' || '' }}" - } - ] - } - ] - } - - # #alerting carries production problems only. A test failure on master is - # reported above; a failed release can leave a version on npm without its - # tag or GitHub release, which blocks every following release PR. - - name: "Post to #alerting" - if: needs.release.result == 'failure' - uses: slackapi/slack-github-action@v2 - with: - webhook: ${{ secrets.SLACK_ALERTS_WEBHOOK_URL }} - webhook-type: incoming-webhook - payload: | - { - "text": "๐Ÿšจ 10x-cli release failure", - "blocks": [ - { - "type": "header", - "text": { - "type": "plain_text", - "text": "๐Ÿšจ 10x-cli โ€” RELEASE FAILURE" - } - }, - { - "type": "section", - "fields": [ - { "type": "mrkdwn", "text": "*npm + release*\nโŒ failed${{ needs.release.outputs.version != '' && format(' (v{0}, {1})', needs.release.outputs.version, needs.release.outputs.reason) || '' }}" } - ] - }, - { - "type": "divider" - }, - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": "๐Ÿ“ *Commit:* <${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}|`${{ github.sha }}`>\n${{ steps.payload.outputs.commit_msg }}\n\n๐Ÿ‘ค *Author:* ${{ github.actor }}" - }, - "accessory": { - "type": "button", - "text": { "type": "plain_text", "text": "View Run" }, - "url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" - } - }, - { - "type": "context", - "elements": [ - { - "type": "mrkdwn", - "text": "๐Ÿ” <${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}|Commit diff> ยท ๐Ÿ“œ <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Full logs> ยท ๐Ÿ“ฆ " + "text": "๐Ÿ“ <${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}|`${{ github.sha }}`> ${{ steps.payload.outputs.commit_msg }} ยท ๐Ÿ‘ค ${{ github.actor }} ยท <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|๐Ÿ”— Workflow>${{ needs.release.result == 'failure' && ' ยท ๐Ÿšจ release failure: check npm for a version without its tag or GitHub release' || '' }}" } ] } diff --git a/CLAUDE.md b/CLAUDE.md index 5c94cbc..6260b84 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,7 +24,7 @@ bun run generate-types # refetch /openapi.json โ†’ src/generated/api-types.t `generate-types` hits the production delivery API by default. To regenerate against a local backend: `API_BASE_URL=http://localhost:8787 bun run generate-types`. The same env var is read at CLI runtime by `resolveApiBase()` to point the CLI at a non-production API. **The allowlist is strict**: only the exact production host or `http://localhost` / `http://127.0.0.1` (any port) are accepted โ€” any other URL throws and exits 2. If you need a staging host, add it explicitly to `PROD_HOSTNAME` / `DEV_HOSTNAMES` in `src/lib/api-client.ts`. -CI (`.github/workflows/ci.yml`) retains typecheck, lint, helper validation, unit/integration tests, both builds and smoke on every PR. The Linux unit suite also exercises the shared runner regressions; Windows retains native checks, which run on master pushes only (PRs skip `check-windows`). Pipeline status goes to Slack #pipelines; only a failed release also posts to #alerting. Use `bun run quality:fast` after edits, `bun run quality:affected --base ` for branch changes, and `bun run quality:gate` for the complete matching offline scope; see `.quality/README.md`. +CI (`.github/workflows/ci.yml`) retains typecheck, lint, helper validation, unit/integration tests, both builds and smoke on every PR. The Linux unit suite also exercises the shared runner regressions; Windows retains native checks, which run on master pushes only (PRs skip `check-windows`). Pipeline status, including a failed release (flagged ๐Ÿšจ), goes to Slack #pipelines only. Use `bun run quality:fast` after edits, `bun run quality:affected --base ` for branch changes, and `bun run quality:gate` for the complete matching offline scope; see `.quality/README.md`. ## Architecture diff --git a/tests/release-workflow.test.ts b/tests/release-workflow.test.ts index 9c359e4..83d0d6e 100644 --- a/tests/release-workflow.test.ts +++ b/tests/release-workflow.test.ts @@ -99,16 +99,21 @@ describe("the notification reports the publication that actually exists", () => expect(rendered).toContain("needs.release.outputs.version"); }); - // #alerting is for production problems. Test failures stay in #pipelines; - // only a failed release (possibly a half-finished publication) pages. - it("sends only a failed release to #alerting", () => { + // Operator decision 2026-10-06: CI, build and release failures go to + // #pipelines only. The alerts webhook (#observability) is not for CI, and a + // failed release must stand out there instead of pointing elsewhere. + it("posts every outcome, including a failed release, to #pipelines only", () => { + const source = readFileSync(new URL("../.github/workflows/ci.yml", import.meta.url), "utf8"); + expect(source.includes("SLACK_ALERTS_WEBHOOK_URL"), "ci.yml must not post to the alerts webhook").toBe(false); const steps = workflow.jobs["notify-slack"].steps; - const alerting = steps.filter((step: any) => JSON.stringify(step).includes("SLACK_ALERTS_WEBHOOK_URL")); - expect(alerting).toHaveLength(1); - expect(alerting[0].if).toBe("needs.release.result == 'failure'"); - const pipelines = steps.find((step: any) => step.with?.webhook === "${{ secrets.SLACK_WEBHOOK_URL }}"); + const slack = steps.filter((step: any) => step.uses?.startsWith("slackapi/slack-github-action@")); + expect(slack).toHaveLength(1); + const pipelines = slack[0]; + expect(pipelines.with.webhook).toBe("${{ secrets.SLACK_WEBHOOK_URL }}"); expect(pipelines.if).toBeUndefined(); - expect(pipelines.with.payload).toContain("needs.release.result == 'failure' && ' ยท ๐Ÿ”” see #alerting'"); + expect(pipelines.with.payload).toContain("needs.release.result == 'failure' && '๐Ÿšจ 10x-cli release failure'"); + expect(pipelines.with.payload).toContain("needs.release.result == 'failure' && ' โ€” RELEASE FAILURE'"); + expect(pipelines.with.payload).not.toContain("#alerting"); }); it("keeps the commit message out of the payload expression", () => { @@ -156,7 +161,7 @@ describe("version preparation stays a trusted, narrow writer", () => { }); describe("step and job names render as written", () => { - // An unquoted `name: Post to #alerting` is a YAML comment from ` #` on, so the + // An unquoted `name: Post to #pipelines` is a YAML comment from ` #` on, so the // run page showed both Slack steps as "Post to" (VG33). const prepare = parse(readFileSync(new URL("../.github/workflows/prepare-version.yml", import.meta.url), "utf8")); const names = Object.entries({ "ci.yml": workflow, "publish-npm.yml": publish, "prepare-version.yml": prepare }).flatMap(([file, wf]) => @@ -174,7 +179,6 @@ describe("step and job names render as written", () => { it("names the Slack channel each notification step posts to", () => { const steps = (workflow.jobs["notify-slack"].steps as Array<{ name?: string }>).map((s) => s.name); expect(steps).toContain("Post to #pipelines"); - expect(steps).toContain("Post to #alerting"); }); });