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
59 changes: 57 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ jobs:
ci: ${{ steps.scope.outputs.ci }}
gui: ${{ steps.filter.outputs.gui }}
packaging: ${{ steps.filter.outputs.packaging }}
docs: ${{ steps.filter.outputs.docs }}
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
Expand Down Expand Up @@ -207,6 +208,20 @@ jobs:
- '.github/workflows/stale-needs-info.yml'
gui:
- 'gui/**'
# The docs site is built by nothing else on a pull request. `ci` above
# deliberately omits `docs-site/**` -- a prose edit has no business
# starting the cross-platform suite -- and `deploy-docs.yml` triggers
# only on `push` to `main`. That left the Astro toolchain with no
# pull-request build gate at all, so a dependency bump under
# `docs-site/` could only be proven by an author's local run and would
# otherwise surface at promotion.
#
# `.github/workflows/ci.yml` is here so an edit to the job below
# verifies itself. Without it this filter's own pull request would
# skip the thing it adds.
docs:
- 'docs-site/**'
- '.github/workflows/ci.yml'
# Everything that ends up inside `npm pack`, or that decides what
# does. `src/**` belongs here because package.json ships `src` and
# bin/ocx.mjs executes it: without that entry an ordinary source PR
Expand Down Expand Up @@ -936,6 +951,40 @@ jobs:
- name: Build, start, and recreate the container
run: bun scripts/ci/docker-smoke.ts

# Proves the Astro toolchain still builds the site, on the only event that can
# prove it before promotion.
#
# Linux only, and one leg. The site is static output from a Node/Bun toolchain
# with no OS-specific behaviour to promise, so a Windows or macOS leg would buy
# queue time rather than coverage. `docs-site` keeps its own manifest and
# lockfile, so this installs there and nowhere else.
#
# `--frozen-lockfile` is the point of the job as much as the build is: it fails
# on a manifest and lockfile that disagree, which is exactly the shape a
# hand-edited override introduces.
docs-site-build:
name: docs site build
needs: changes
if: needs.changes.outputs.docs == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false

- name: Setup project Bun
uses: ./.github/actions/setup-project-bun
Comment on lines +977 to +978

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Match the deployment toolchain in the docs gate

The PR gate installs the root-pinned Bun version (1.4.0 in this commit), but .github/workflows/deploy-docs.yml still builds with Node 22 and package-manager: bun@latest. Once bun@latest differs—or if the Node-backed Astro action behaves differently—a docs change can pass this gate and then fail during the post-promotion Pages build, defeating the gate's stated purpose. Pin the deploy workflow to the same toolchain or exercise the deployment action/configuration here.

Useful? React with 👍 / 👎.


- name: Install docs-site dependencies
working-directory: docs-site
run: bun install --frozen-lockfile

- name: Build the docs site
working-directory: docs-site
run: bun run build

npm-global-smoke:
name: npm-global ${{ matrix.os }}
needs: changes
Expand Down Expand Up @@ -1014,7 +1063,7 @@ jobs:
# direct dependencies only, so a failing `select-windows-runner` would
# otherwise reach this gate as nothing at all while its dependents report
# `skipped`, which is the shape the step below is written to catch.
needs: [changes, select-windows-runner, test, storage-policy, api-usage, gates, platform-macos, macos-control, platform-windows, keyring-smoke, docker-smoke, npm-global-smoke]
needs: [changes, select-windows-runner, test, storage-policy, api-usage, gates, platform-macos, macos-control, platform-windows, keyring-smoke, docker-smoke, docs-site-build, npm-global-smoke]
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
Expand All @@ -1031,6 +1080,7 @@ jobs:
LANE: ${{ github.event.inputs.lane }}
CHANGES_CI: ${{ needs.changes.outputs.ci }}
CHANGES_PACKAGING: ${{ needs.changes.outputs.packaging }}
CHANGES_DOCS: ${{ needs.changes.outputs.docs }}
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
Expand All @@ -1055,6 +1105,10 @@ jobs:
if [ "$CHANGES_PACKAGING" = "true" ]; then
packaging=requested
fi
docs=not-requested
if [ "$CHANGES_DOCS" = "true" ]; then
docs=requested
fi
dispatch=not-requested
windows=not-requested
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
Expand All @@ -1069,14 +1123,15 @@ jobs:
# without adding it here fails the gate by name rather than passing unnoticed.
GATED_JOBS="changes select-windows-runner test storage-policy api-usage gates"
GATED_JOBS="$GATED_JOBS platform-macos keyring-smoke docker-smoke npm-global-smoke"
GATED_JOBS="$GATED_JOBS macos-control platform-windows"
GATED_JOBS="$GATED_JOBS macos-control platform-windows docs-site-build"

expected_for() {
case "$1" in
changes|select-windows-runner) echo requested ;;
test|storage-policy|api-usage|gates|platform-macos|keyring-smoke|docker-smoke)
echo "$scoped" ;;
npm-global-smoke) echo "$packaging" ;;
docs-site-build) echo "$docs" ;;
macos-control) echo "$dispatch" ;;
platform-windows) echo "$windows" ;;
*) echo undeclared ;;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Unit C — a pull-request build gate for the docs site

Opened because unit B's review could not honestly close. The Astro 7.2 to 7.3 bump
in #4873 had no pull-request build gate anywhere, so no amount of green CI on that
head was evidence the docs site still built. The alternative was to accept an
author's local result as the record, and that is not a standard this repository
applies elsewhere.

## The gap

`.github/workflows/ci.yml` contained no `docs-site` reference and built no docs.
`deploy-docs.yml` triggers only on `push` to `main`, which is after promotion. So
the first machine to discover a broken docs build was the deploy, and the only
pre-merge evidence available was an unverifiable attestation.

## Shape

A new `docs` filter in the existing `changes` job, and one job selected by it.

`docs-site/**` is deliberately **not** added to the `ci` filter. Widening `ci`
would start the whole cross-platform matrix for a prose edit, which is a cost with
no matching evidence: a docs change has to build, not to pass the runtime suite.
A separate filter output keeps the two questions apart.

`.github/workflows/ci.yml` is in the `docs` filter so an edit to the job verifies
itself. Without that entry this unit's own pull request would skip the job it adds,
which is the failure mode the unit exists to remove.

One Linux leg. The site is static output from a Node/Bun toolchain with no
OS-specific behaviour to promise, so a Windows or macOS leg would spend queue time
without buying coverage. `--frozen-lockfile` carries as much of the value as the
build does: it fails on a manifest and lockfile that disagree, which is exactly the
shape a hand-edited override introduces.

## Constraints honoured

No existing job's sharding, timeout, or runner selection is touched. No new Windows
leg. Workflow-level `permissions` stay `contents: read` and the job adds none.
Actions stay pinned to immutable SHAs with their version comments.

The aggregate gate is the part that is easy to get wrong. `ci` is event-aware since
#4837: it derives what the event requested and demands `success` from each requested
job and `skipped` from every other one, and it fails by name on any job with no
declared expectation. So the job is added in four places that must agree:
`needs`, `CHANGES_DOCS`, `GATED_JOBS`, and `expected_for`. And
`tests/ci-workflows/ci-workflows.test.ts` independently derives the expected
`needs` list from the workflow's own job keys, so a missing entry fails rather than
passing quietly.

`structure/ops/docs-and-release.md` owns `docs-site/` and is updated in the same
change, including the workflow map.

## Sequencing

This unit lands before #4873 merges. #4873 then needs a fresh run for the new job
to appear on its head; a re-run is the cheap way to find out whether the merge-ref
workflow already carries it, before considering anything that rewrites that branch.
Both calls belong to the host.
13 changes: 12 additions & 1 deletion structure/ops/docs-and-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,17 @@ https://opencodex.me/
The workflow runs on `main` pushes touching `docs-site/**` or the workflow itself, builds
`docs-site`, uploads the artifact, and deploys with GitHub Pages.

That workflow is the deploy path, not a review gate: it first runs after promotion to `main`,
so on its own it can only report a broken site once the change has already left review. The
pull-request gate is the `docs-site-build` job in `.github/workflows/ci.yml`, selected by the
`changes` job's `docs` filter (`docs-site/**` and the workflow itself). One Linux leg installs
`docs-site` with `--frozen-lockfile` and runs the Astro build, so a manifest and lockfile that
disagree fail before the build does. The `ci` aggregate treats it exactly like the other scoped
jobs: requested when the filter is true, required `skipped` otherwise.

The deliberate omission is that `docs-site/**` is not in the `ci` filter. A prose edit has no
business starting the cross-platform suite; it only has to build.

> Decision record: [ADR-0080](../decisions/ADR-0080-github-pages.md)

Local validation:
Expand Down Expand Up @@ -92,7 +103,7 @@ Those controls still have no owner, so there is no image-publish workflow or off
| `.github/workflows/ci.yml` | Any `pull_request`; runtime/package `push` to `main`/`preview`/`dev`; manual dispatch | Linux runs four suite shards plus `gates`; macOS runs two shards. Windows runs nine shards only on manual dispatch with `lane=all` (or empty), not on push events. Linux runs at-most-12-file processes with a 120-second process bound; Windows uses measured six-file/480-second processes and all-file scope so its full-suite contract is unchanged. The dedicated Windows batch step sets `OCX_TEST_NO_QUEUE=1` because its sequential processes are one logical runner; each process still creates an isolated home and arms the test guards before the lock boundary. No lane retries: a test failure, a process timeout and a Bun runtime crash each fail their job on the first occurrence. Aggregate `ci` is event-aware — it derives which jobs this event requested and requires `success` from each of them and `skipped` from the rest, and on a `lane=all` dispatch it reads the run's own job list and requires nine concrete successful `windows N/9` results. `npm-global-smoke` remains GitHub-hosted because it mutates the global package prefix. |
| `.github/workflows/dev-version-bump.yml` | Manual dispatch with an intended version and `pre-move` or `repair` mode | Opens the reviewed pull request that moves `dev` past a release target. The default `pre-move` mode runs before promotion and publication; explicit `repair` mode retains the post-publish catch-up path. It is neither called by `release.yml` nor triggered by publication. |
| `.github/workflows/release.yml` | Manual dispatch only | npm publish/dry-run workflow. It requires successful Cross-platform CI for the exact `GITHUB_SHA`, requires `dev` to outrank the target, then checks the target against the freshly fetched global tag set before publish or dry-run. |
| `.github/workflows/deploy-docs.yml` | `push` to `main` touching `docs-site/**` or the workflow, or manual dispatch | Build and publish the Astro/Starlight docs site to GitHub Pages. |
| `.github/workflows/deploy-docs.yml` | `push` to `main` touching `docs-site/**` or the workflow, or manual dispatch | Build and publish the Astro/Starlight docs site to GitHub Pages. This is the deploy path; the pull-request build gate is the `docs-site-build` job in `ci.yml`. |
| `.github/workflows/service-lifecycle.yml` | `pull_request` to `main`/`dev` and `push`, both filtered on the service path set (`src/service.ts`, `src/cli.ts`, `src/cli/index.ts`, `src/lib/bun-runtime.ts`, `package.json`, `bun.lock`, the workflow), or manual dispatch | Service-lifecycle smoke on three platforms: Linux systemd, macOS launchd, and Windows Scheduled Tasks. Each installs, verifies, stops via `ocx stop`, and uninstalls. The path list is kept in sync with the `release.yml` service-gate regex. |
| `.github/workflows/enforce-pr-target.yml` | `pull_request_target` (opened, reopened, edited, labeled, unlabeled, ready_for_review, synchronize) plus default-branch `status` events filtered to successful `CodeRabbit` statuses | The `enforce-target` gate: rejects pull requests whose head ancestry sits on the `main` tip while far behind `dev`, rejects empty or malformed descriptions, requires a GUI screenshot when the title/body mentions `gui` (immediately waivable with the maintainer-controlled `gui-screenshot-waived` label; legacy maintainer comments remain compatibility evidence on later PR events), keeps contributor PRs in draft until a four-box readiness checklist is complete, verifies the CI / latest-dev / Codex+CodeRabbit-findings claims (review threads plus current-head CodeRabbit review-body findings outside the diff range), and adds a `review-ready` status label at the ready moment. CodeRabbit status SHAs must resolve to exactly one open current-head PR before writes. Stacked child PRs targeting another open PR's head skip the wrong-base gate. |
| `.github/workflows/enforce-issue-quality.yml` | `issues` (opened, edited, reopened), `issue_comment` (created, edited), or manual dispatch with an issue number | Issue-template compliance gate. |
Expand Down
Loading