Repository navigation
deps(container): certifi 2026.10.19, dropping the emSign roots with c… #20
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 | |
| # commitwork had NO CI at all until 2026-08-09 — 115 test files run only when a human remembered | |
| # to type the command. That is external overwatch layer's BACKLOG F4, open since 2026-08-02 and re-measured on the | |
| # day this file was written. A suite nothing runs is a memory of quality, not a control over it, | |
| # and this repository's entire product is the argument that unenforced claims are defects. | |
| # | |
| # WHAT MAKES THIS RUNNABLE AT ALL. Fifteen tests read the LIVE FLEET off the machine they run on: | |
| # the registry's declared repository paths, ~/.cloudflared, and a checkout declared in | |
| # monitor/projects.json. A runner has one checkout at a path that changes per run and no fleet, so | |
| # those tests used to fail here for reasons no commit could fix — five red tests nobody can go | |
| # green is exactly how a check gets switched off. They now SKIP WITH A STATED REASON when the fleet | |
| # is absent, and run in full on the fleet machine. Nothing is silently excluded: `npm test` runs | |
| # every file, and the reasons appear in this job's log. | |
| # | |
| # Deliberately NOT here: a real sweep. The sweep has no per-check timeout (F3) and once blocked | |
| # 8h31m on one repository, taking the remaining 5 of 20 with it. CI is the worst place to | |
| # rediscover that, and a 6-hour job that gets cancelled teaches nobody anything. | |
| on: | |
| # release.yml runs this file as its gate on a v* tag. | |
| workflow_call: | |
| secrets: | |
| CW_RELEASE_SCOPE: | |
| required: false | |
| push: | |
| # Branches only: a tag is tested by release.yml's call above, and a second run of the same tag | |
| # here would take the same concurrency group and cancel it. | |
| branches: ['**'] | |
| # Skip pushes that change only prose. Test fixtures are markdown too, so they are re-included last | |
| # (later patterns win). Measured 2026-09-18: 287 CI runs in 35 days, most pushes to main. | |
| paths: | |
| - '**' | |
| - '!**/*.md' | |
| - '!docs/**' | |
| - '**/test/fixtures/**' | |
| pull_request: | |
| # Skip pushes that change only prose. Test fixtures are markdown too, so they are re-included last | |
| # (later patterns win). Measured 2026-09-18: 287 CI runs in 35 days, most pushes to main. | |
| paths: | |
| - '**' | |
| - '!**/*.md' | |
| - '!docs/**' | |
| - '**/test/fixtures/**' | |
| # A newer push to the same ref cancels the run it supersedes; only the latest commit's result is read, | |
| # and every superseded minute is billed on a private repository. | |
| concurrency: | |
| group: ci-${{ github.event_name }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| # Every job reads the tree and publishes nothing; a token with write scopes here is the credential a | |
| # poisoned dependency or a compromised action would move on. | |
| permissions: | |
| contents: read | |
| jobs: | |
| tests: | |
| name: Test suite (${{ matrix.os }}, Node ${{ matrix.node }}) | |
| # Supported majors: 22 is the floor package.json engines.node declares (maintenance LTS until | |
| # 2027-04-30) and 24 is active LTS. bin/test/node-floor.test.mjs holds the lowest version here, | |
| # the engines floor and the CLI's refusal to one declaration, and requires every OS to run it. | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest] | |
| node: ['22', '24'] | |
| runs-on: ${{ matrix.os }} | |
| # macOS has never run this suite on a hosted runner. It reports without blocking until its first | |
| # measured green runs; then this line goes. Linux stays required. | |
| continue-on-error: ${{ matrix.os == 'macos-latest' }} | |
| timeout-minutes: 30 | |
| steps: | |
| # Actions pinned to commit SHAs, not tags: a tag can be silently repointed by the action | |
| # owner (the trivy-action / tj-actions compromise class). The trailing comment names the | |
| # release each SHA was resolved from via `git ls-remote --tags`. | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| # the history guards read real history; a depth-1 clone makes them skip with the reason | |
| fetch-depth: 0 | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| # node:sqlite, zlib zstd and node --test's glob form are all used. The floor is real, not habitual. | |
| node-version: ${{ matrix.node }} | |
| # The release-name gates need the private redaction maps, which never enter the tree. They come | |
| # from one secret, a base64 gzipped tar of the three maps, set from the sidecar with: | |
| # tar -czf - -C <sidecar> monitor/release-redactions.json monitor/publish-redactions.json \ | |
| # identity/repo-identities.json | base64 | gh secret set CW_RELEASE_SCOPE | |
| # This log is public. Every report those gates print masks the name it found, which | |
| # bin/test/release-names-output.test.mjs holds. Pushes only: a pull request's code never runs | |
| # with the maps. | |
| - name: Load the release-name maps | |
| # Linux only: the maps are about tree content, not platform, so one OS is enough to hold | |
| # the secret; elsewhere the release-name gates skip and say so. | |
| if: github.event_name == 'push' && runner.os == 'Linux' | |
| env: | |
| RELEASE_SCOPE: ${{ secrets.CW_RELEASE_SCOPE }} | |
| run: | | |
| if [ -z "$RELEASE_SCOPE" ]; then | |
| echo "::notice::CW_RELEASE_SCOPE is not set, so the release-name gates skip and say so" | |
| exit 0 | |
| fi | |
| dir="$RUNNER_TEMP/release-scope" | |
| mkdir -p "$dir" | |
| printf '%s' "$RELEASE_SCOPE" | base64 -d | tar -xzf - -C "$dir" | |
| { | |
| echo "CW_RELEASE_REDACTIONS=$dir/monitor/release-redactions.json" | |
| echo "CW_PUBLISH_REDACTIONS=$dir/monitor/publish-redactions.json" | |
| echo "CW_REPO_IDENTITIES=$dir/identity/repo-identities.json" | |
| } >> "$GITHUB_ENV" | |
| # No `npm ci`. commitwork declares zero runtime dependencies, and this job is part of what | |
| # keeps it that way — an install step would quietly absorb the day that changes. | |
| - name: Run the suite | |
| run: npm test | |
| # The Linux lane sandbox, measured. The suite above runs without bwrap, so its sandbox tests skip | |
| # with a reason; this job installs bubblewrap and passt and runs them for real, so open lanes | |
| # reaching host loopback, the host's addresses or /run sockets (review 2026-10-07 D1) is a red | |
| # build here rather than a skip. A skip of a Linux confinement test in this job is a failure. | |
| linux-sandbox: | |
| name: Linux lane sandbox (bwrap + pasta) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: '24' | |
| - name: Install bubblewrap and passt | |
| run: | | |
| sudo apt-get update -qq | |
| sudo apt-get install -y --no-install-recommends bubblewrap passt | |
| # Ubuntu 24.04 can restrict unprivileged user namespaces through AppArmor. This job measures | |
| # the sandbox, not the host policy, so it lifts the restriction and says so. | |
| if [ "$(sysctl -n kernel.apparmor_restrict_unprivileged_userns 2>/dev/null)" = "1" ]; then | |
| echo "::notice::lifting kernel.apparmor_restrict_unprivileged_userns for this job" | |
| sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 | |
| fi | |
| bwrap --ro-bind / / --unshare-net --die-with-parent -- /bin/true | |
| pasta --version | |
| - name: Sandbox tests, with no Linux skip allowed | |
| run: | | |
| set -o pipefail | |
| node --test bin/test/sandbox-netns-live.test.mjs bin/test/sandbox-net.test.mjs \ | |
| bin/test/sandbox-host.test.mjs bin/test/lane-env-confinement.test.mjs 2>&1 | tee "$RUNNER_TEMP/sandbox.log" | |
| if grep -E 'bwrap and pasta confinement is NOT verified|bwrap cannot create a namespace|pasta \(package passt\) is absent' "$RUNNER_TEMP/sandbox.log"; then | |
| echo "::error::a Linux confinement test skipped in the job that exists to run it" | |
| exit 1 | |
| fi | |
| syntax: | |
| name: Every module parses (Node ${{ matrix.node }}) | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| node: ['22', '24'] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: ${{ matrix.node }} | |
| # Cheap, and it catches the class of breakage that makes the suite above fail to even start, | |
| # with a clearer message than a test runner gives for the same fault. | |
| # | |
| # workflows/ IS EXCLUDED, AND THE REASON IS THAT THE CHECK IS WRONG THERE, not the code. | |
| # Those files are Workflow scripts, not Node modules: the runtime wraps the body in an async | |
| # function, so `export const meta = {...}` followed by a top-level `return` or `await` is | |
| # legal and `node --check` rejects it. Running it anyway would have made this job red on its | |
| # first run over a correct file — the same false positive external overwatch layer's own artifact gate hit | |
| # on day one (G7 counting markup inside a JS string), and the lesson recorded there holds: a | |
| # check that cries wolf gets switched off, which is worse than not having it. Excluded by | |
| # path, declared here, and the count is printed so the exclusion cannot grow unnoticed. | |
| - name: node --check every module | |
| run: | | |
| fail=0 checked=0 skipped=0 | |
| # An assignment, not a pipe in the for list: bash -e fails the step when git does, where a | |
| # failing git inside `$(git … | grep …)` yielded zero files and a green job. | |
| modules=$(git ls-files -- '*.mjs' ':(exclude,glob)**/node_modules/**') | |
| for f in $modules; do | |
| case "$f" in | |
| workflows/*) skipped=$((skipped+1)); continue ;; | |
| esac | |
| checked=$((checked+1)) | |
| node --check "$f" || { echo "::error file=$f::does not parse"; fail=1; } | |
| done | |
| echo "checked $checked module(s); skipped $skipped Workflow script(s) under workflows/ — see the note in this file" | |
| [ "$checked" -gt 0 ] || { echo "::error::no module was checked"; exit 1; } | |
| exit $fail | |
| windows: | |
| name: Windows portability (Node ${{ matrix.node }}) | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| node: ['22', '24'] | |
| runs-on: windows-latest | |
| # 30, not the 20 the Linux jobs use: a Windows runner's process spawn is markedly slower and | |
| # this job runs the suite twice — once serially for verdicts, once for side effects. | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: ${{ matrix.node }} | |
| # WHY THIS JOB EXISTS. On 2026-09-04 a Windows cycle closed TWENTY-THREE defects, every one | |
| # of them invisible on Linux and none of them findable by reading: separator mismatches that | |
| # made BOTH structural gates flag everything, CRLF that silently disabled seven line-based | |
| # parsers, `import(absolutePath)` that stopped most of the suite from running at all, and | |
| # `npm` — which is `npm.cmd` here — being unspawnable. The root cause was singular and | |
| # boring: the code had never been EXECUTED on its second platform. Fixing that without a | |
| # runner just schedules defect twenty-four. | |
| # | |
| # WHY IT IS NOT `npm test`, YET. The full suite is not reproducible on any machine right now: | |
| # two consecutive runs of the same tree gave 191 and 504 unique failures, because ~83 test | |
| # files read live ambient state (`reports/`, the ledgers, the lock files) instead of fixtures. | |
| # Gating on a number that moves by 300 between runs would be red forever, and this file | |
| # already records what happens then — "a check that cries wolf gets switched off, which is | |
| # worse than not having it". So this job gates on what IS deterministic, and the full Windows | |
| # suite joins it once the fixtures work lands. That is a stated sequence, not an exemption. | |
| # 1. Every module parses. The Linux job does this with a shell loop; PowerShell needs its own, | |
| # and having it here is not duplication — it is the check catching Windows-only syntax and | |
| # path faults at the cheapest possible point. | |
| - name: node --check every module | |
| shell: pwsh | |
| run: | | |
| $fail = 0; $checked = 0; $skipped = 0 | |
| foreach ($f in (git ls-files '*.mjs')) { | |
| if ($f -like 'workflows/*') { $skipped++; continue } # Workflow scripts, not modules — see the Linux job | |
| $checked++ | |
| node --check $f | |
| if ($LASTEXITCODE -ne 0) { Write-Output "::error file=$f::does not parse"; $fail = 1 } | |
| } | |
| Write-Output "checked $checked module(s); skipped $skipped Workflow script(s)" | |
| if ($checked -eq 0) { Write-Output "::error::no module was checked"; exit 1 } | |
| exit $fail | |
| # 2. The platform contract itself. These suites ARE the Windows contract — shell resolution, | |
| # the Node repo walk, the process-tree kill, batch-shim spawning, the path hazards, the | |
| # listener lens, the installer's manager shapes, and the two structural gates that were | |
| # blind here. All are fixture-driven and deterministic, so this list must be GREEN. | |
| - name: The Windows contract holds | |
| shell: pwsh | |
| run: | | |
| node --test --test-concurrency=1 ` | |
| lib/test/posix-shell.test.mjs ` | |
| lib/test/proc-tree.test.mjs ` | |
| lib/test/repo-walk.test.mjs ` | |
| lib/test/win-spawn.test.mjs ` | |
| lib/test/win-path-safety.test.mjs ` | |
| lib/test/untrusted-text.test.mjs ` | |
| monitor/test/port-bind-windows.test.mjs ` | |
| monitor/test/port-bind.test.mjs ` | |
| bin/test/install-catalog-managers.test.mjs ` | |
| bin/test/install-catalog.test.mjs ` | |
| bin/test/esm-specifier-shape.test.mjs ` | |
| bin/test/tracked-imports.test.mjs ` | |
| bin/test/docs-doctor.test.mjs ` | |
| bin/test/measured.test.mjs ` | |
| bin/test/test-run.test.mjs ` | |
| bin/test/scan-exclusions-regex.test.mjs | |
| # 3. THE SUITE MUST NOT MODIFY THE WORKING TREE. Cheap, deterministic, and it would have | |
| # caught two of the twenty-three on their first run: | |
| # W19 — a Windows path interpolated unquoted into a POSIX shell lost its backslashes, | |
| # became RELATIVE, and the redirect wrote into the repository root. | |
| # W23 — cmd.exe read the `>` of an arrow function `()=>{}` on a command line as an output | |
| # redirection and created a file literally named `{}` in the repository root. | |
| # Both wrote junk into `git status` on every run for months, on a project whose whole | |
| # subject is noticing what changed. A test that dirties the tree is a defect regardless of | |
| # whether it passes. | |
| # | |
| # NOTE WHAT THIS STEP CANNOT SEE, because the same blind spot has bitten this repo before: | |
| # `reports/` and `.claude/` are BOTH GITIGNORED, so `git status` is structurally incapable | |
| # of reporting the pollution that actually breaks reproducibility — production code spawned | |
| # by tests appending to the live `reports/perf-feedback.jsonl`. That is the identical | |
| # ignore-rule blind spot CLAUDE.md records for the durability check. The ignored paths are | |
| # covered instead by bin/test-run.mjs, which fingerprints them around the run and fails if | |
| # one moved; the two checks together cover tracked AND ignored, and neither alone does. | |
| - name: The suite leaves the tree clean | |
| shell: pwsh | |
| run: | | |
| # Default concurrency, not the runner's serial mode: this step reads SIDE EFFECTS, never | |
| # assertions, so results are discarded and there is nothing for parallelism to | |
| # destabilise. The serial run belongs in the step above, where the verdicts matter. | |
| node --test "bin/**/*.test.mjs" "lib/**/*.test.mjs" 2>&1 | Out-Null | |
| $dirty = (git status --porcelain) | |
| if ($dirty) { | |
| Write-Output "::error::the test suite modified the working tree:" | |
| $dirty | ForEach-Object { Write-Output " $_" } | |
| exit 1 | |
| } | |
| Write-Output "working tree clean after the suite" | |
| # 4. The install path a Windows operator actually walks. `setup` with no TTY and no --yes is | |
| # report-only by contract, so this asserts the REPORT rather than installing anything: it | |
| # must run, and it must resolve a POSIX shell from the Git that the runner already has. | |
| # `doctor` is where a missing shell becomes visible, and a Windows operator's first signal | |
| # used to be 68 unexplained check failures instead. | |
| - name: The install path reports honestly | |
| shell: pwsh | |
| run: | | |
| node bin/commitwork.mjs setup | |
| $out = node bin/commitwork.mjs doctor --manifest security-baseline 2>&1 | Out-String | |
| Write-Output $out | |
| if ($out -notmatch 'POSIX shell') { Write-Output '::error::doctor does not report the POSIX shell prerequisite'; exit 1 } | |
| if ($out -match '(?m)^\s*✗ POSIX shell') { | |
| Write-Output '::error::no POSIX shell resolved on a runner that has Git for Windows — the git-adjacent lookup regressed' | |
| exit 1 | |
| } | |
| smoke: | |
| name: It starts | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: '24' | |
| # The suite above is entirely unit-level: it proves functions behave, never that the CLI | |
| # runs or the panel serves. commitwork has no compile step — zero runtime dependencies is | |
| # the point — so "does it build" has no meaning here and "does it boot" is the equivalent | |
| # question. Nothing was asking it. | |
| # | |
| # bin/smoke.mjs asserts the panel's AUTH BOUNDARY, not merely that something responded: | |
| # the operator port must serve a real HTML document and the published port must refuse an | |
| # unauthenticated request. The second is the one worth having — a smoke test that only | |
| # checks for a response passes just as happily when auth has been switched off, on the port | |
| # a tunnel publishes. The smoke pins an absent auth store, so the panel it boots is | |
| # unbootstrapped on every runner and the refusal is the 503 bootstrap answer; a box with an | |
| # operator account answers 401. | |
| - name: CLI resolves and the panel boots | |
| run: node bin/smoke.mjs | |
| installer: | |
| name: Installer on Linux | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: '24' | |
| # On Linux `commitwork setup` installs through Homebrew when brew is on PATH, else pipx, npm, | |
| # cargo, gem, composer and go, with apt or dnf after them. This job exercises the pipx route | |
| # end to end: a report-only run must exit 0 without a prompt, and a real install must leave | |
| # a tool that runs and that doctor then sees. | |
| - name: Report-only setup never prompts | |
| run: mkdir -p "$HOME" && node bin/commitwork.mjs setup < /dev/null | |
| env: | |
| HOME: ${{ runner.temp }}/home | |
| - name: Install one scanner through pipx | |
| run: | | |
| node bin/commitwork.mjs setup --yes --only flawfinder | |
| echo "$HOME/.local/bin" >> "$GITHUB_PATH" | |
| - name: The installed scanner runs and doctor sees it | |
| run: | | |
| flawfinder --version | |
| report=$(node bin/commitwork.mjs doctor --manifest security-baseline) | |
| printf '%s\n' "$report" | grep -E '✓ flawfinder' | |
| docs: | |
| name: Documentation freshness | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| # docs-doctor compares each durable doc's `verified-against` stamp against the commits | |
| # that have touched it since. With the default shallow clone it cannot see them and | |
| # reports drift that does not exist. | |
| fetch-depth: 0 | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: '24' | |
| # THIS JOB GATES ON GREY, WARNS ON ORANGE, AND THE ASYMMETRY IS DELIBERATE. | |
| # | |
| # docs-doctor exits 1 for orange (a stamped doc whose stamp is behind) and 2 for grey-only | |
| # (a durable doc with NO stamp at all). | |
| # | |
| # This shape was chosen because docs-doctor was measured ORANGE at HEAD on 2026-08-12 | |
| # (workflows/MULTI-AGENT-PROTOCOL.md behind a committed change) — a job gating on exit 0 | |
| # would have been red on its first run over another session's drift, which is the failure | |
| # the syntax job above names in its own words: a check that cries wolf gets switched off, | |
| # which is worse than not having it. By the time this job was written that stamp had been | |
| # refreshed by another session and docs-doctor was green. | |
| # | |
| # The asymmetry stays, and the re-measurement strengthens the case rather than removing it: | |
| # on a tree where many sessions commit concurrently, a stamp falls behind its doc routinely | |
| # and transiently. Gating on orange would put CI red on a condition that resolves itself, | |
| # and train everyone to ignore it. Grey is gated because grey is the worse class — an | |
| # unstamped durable doc has never been verified AT ALL, which is this repository's entire | |
| # thesis about unenforced claims. Orange ratchets to a gate when orange stops being the | |
| # normal condition of a shared tree; the gate exists and is runnable before it is enforced, | |
| # exactly as quality-gates does for qg-boot-test-pass. | |
| # | |
| # `rc=0` and `|| rc=$?` ARE LOAD-BEARING. GitHub runs `run:` steps as `bash -e {0}`, so a | |
| # bare invocation aborts the step the instant docs-doctor exits non-zero and the case | |
| # statement below is never reached — the job would fail on orange and reintroduce exactly | |
| # the problem this shape exists to avoid. `|| ...` suppresses -e; initialising rc keeps a | |
| # green run (which never assigns it) from falling through to the catch-all. | |
| - name: docs-doctor — grey blocks, orange warns | |
| run: | | |
| rc=0 | |
| node bin/docs-doctor.mjs || rc=$? | |
| case "$rc" in | |
| 0) echo "docs: all durable docs stamped and fresh" ;; | |
| 1) echo "::warning title=Documentation drift::orange — one or more stamps are behind their doc. Re-verify and refresh the stamp. Not blocking yet." ;; | |
| 2) echo "::error title=Unstamped durable doc::grey — a durable doc carries no verified-against stamp, so it has never been verified. Blocking."; exit 1 ;; | |
| *) echo "::error title=docs-doctor failed::exited $rc, which is neither 0, 1 nor 2 — treating an unrecognised outcome as failure rather than assuming it passed."; exit 1 ;; | |
| esac |