Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 7 additions & 56 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -174,20 +174,24 @@ 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:
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
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) || '' }}"
}
},
{
Expand All @@ -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> · 📦 <https://www.npmjs.com/package/@przeprogramowani/10x-cli|npm package>"
"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' || '' }}"
}
]
}
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <SHA>` 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 <SHA>` for branch changes, and `bun run quality:gate` for the complete matching offline scope; see `.quality/README.md`.

## Architecture

Expand Down
24 changes: 14 additions & 10 deletions tests/release-workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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]) =>
Expand All @@ -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");
});
});

Expand Down
Loading