mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-26 22:37:03 +00:00
7c9f29b2f3
* fix(ci): Go toolchain 1.26.10 for GO-2026-6809, markdown-it 14.3.3 for npm audit (TASK-4007) Main CI is red on both gates. - govulncheck: GO-2026-6809 (crypto/x509) is fixed in go1.26.10. The go.mod toolchain line and the Dockerfile's golang image move to 1.26.10. The workflow comments that name the toolchain value follow it, and go.mod's comment records the raise. - Nix: its artifact is built with nixos-26.05's go, which predates 1.26.10, so GO-2026-6809 joins nix/accepted-advisories.txt the way the 1.26.9 block did, with the same removal condition. - npm audit: markdown-it up to 14.3.2 has two high quadratic-time DoS advisories (GHSA-vfgh-jcmw-g37q, GHSA-gfr2-rv44-r23c). The overrides pin goes ^14.3.0 -> ^14.3.3; the lock moves markdown-it 14.3.2 -> 14.3.3 and nothing else. Claude-Session: https://claude.ai/code/session_01JRXHP9ZVfEN3YEuzmucxP3 * ci(deps): allow markdown-it 14.3.3 under the 7-day age check, a security fix (TASK-4007) Dependency age (advisory) flagged only markdown-it@14.3.3, published 2026-10-16: the fix for GHSA-vfgh-jcmw-g37q and GHSA-gfr2-rv44-r23c. pad-merge-pr requires every check at the head to be SUCCESS, so the advisory red blocked the merge. A security fix cannot wait out the window. The allow line is the file's own remedy for this case; its removal after the version ages past 7 days is a filed follow-up. Claude-Session: https://claude.ai/code/session_01JRXHP9ZVfEN3YEuzmucxP3
240 lines
12 KiB
YAML
240 lines
12 KiB
YAML
name: Nix
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
branches: [main]
|
|
|
|
concurrency:
|
|
# PR runs share a group per ref and supersede each other — a newer push to the
|
|
# same PR makes the older run's answer worthless.
|
|
#
|
|
# PUSH runs get a group of their OWN, one per commit, and this is deliberate
|
|
# (TASK-2954). A run on `main` may owe main a vendorHash heal, and an evicted
|
|
# run never performs it. `cancel-in-progress: false` is not enough on its own —
|
|
# GitHub holds only ONE pending run per group, so a third push evicts the
|
|
# queued second, and the eviction looks exactly like a run that decided there
|
|
# was nothing to do. A per-commit group cannot be evicted by anything.
|
|
#
|
|
# What an evicted heal costs is now DELAY, not the fix. Since BUG-2974 the
|
|
# heal is gated on the committed hash rather than on what the push touched, so
|
|
# the next push to main meets the same stale hash and heals it from its own
|
|
# run. The per-commit group is what makes the heal land on the merge that
|
|
# caused it instead of waiting for whatever lands next.
|
|
#
|
|
# What that trades for: two main runs can reach the push step at once, and the
|
|
# loser gets a non-fast-forward rejection and goes red. That is the right
|
|
# direction — a red job is read, and the next push to main heals anyway.
|
|
group: nix-${{ github.event_name == 'push' && github.sha || github.ref }}
|
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
# All third-party Actions are pinned to a 40-char commit SHA with a trailing
|
|
# '# vX.Y.Z' comment so a compromised maintainer or moved tag cannot silently
|
|
# execute attacker code in CI. Bump the SHA + comment together when updating.
|
|
|
|
jobs:
|
|
nix:
|
|
name: Nix build & check
|
|
runs-on: ubuntu-latest
|
|
# NO write permission here, deliberately — see `push-vendor-hash` below.
|
|
# This job builds; it never pushes.
|
|
outputs:
|
|
vendor_hash_bumped: ${{ steps.vendorhash.outputs.bumped }}
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
|
|
- uses: DeterminateSystems/nix-installer-action@3138316df39ed29be04236d7ffc686fa525866aa # v23
|
|
|
|
# THE RED-BASELINE FIX (TASK-2954). `vendorHash` pins the Go module set by
|
|
# content hash; Dependabot updates go.mod/go.sum and has no idea this file
|
|
# exists, so EVERY Go bump PR failed the steps below on a hash mismatch,
|
|
# structurally and forever. A check that is always red is not a check —
|
|
# a bump that genuinely breaks the build looked identical, at a glance, to
|
|
# one that only moved the hash.
|
|
#
|
|
# This recomputes the hash in the WORKING TREE, so the steps below judge
|
|
# the build with this ref's actual module set. Nothing is pushed from here
|
|
# and no token is used: a Dependabot PR run gets a read-only GITHUB_TOKEN
|
|
# (it runs as if from a fork), and the `permissions` key does NOT lift that
|
|
# — only a repository-wide setting does, which would hand fork PRs write
|
|
# tokens too. The push therefore happens on `push: main` instead, in the
|
|
# job below, where the run is not Dependabot's.
|
|
#
|
|
# It runs on every run, not just Dependabot's: a human's own go.mod change
|
|
# moves the hash the same way, and a check that is honest only for one
|
|
# author is the shape this item exists to remove.
|
|
#
|
|
# It runs BEFORE `nix flake check`, which builds the package too and would
|
|
# hit the same mismatch first. It is deliberately not a gate: if the build
|
|
# fails for any reason other than a go-modules hash mismatch, the script
|
|
# exits non-zero, this step still succeeds, and the real steps below report
|
|
# the real failure. The gate stays exactly where it was.
|
|
- name: Recompute vendorHash before judging the build
|
|
id: vendorhash
|
|
run: |
|
|
set -o pipefail
|
|
if nix build .#default --print-build-logs 2>&1 | tee /tmp/nix-build.log; then
|
|
echo "Build is green; vendorHash is current."
|
|
exit 0
|
|
fi
|
|
if ! new_hash="$(nix/bump-vendor-hash.sh /tmp/nix-build.log)"; then
|
|
echo "Not a go-modules hash mismatch — leaving it to the build steps below."
|
|
exit 0
|
|
fi
|
|
echo "vendorHash -> $new_hash"
|
|
echo "bumped=true" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Carry the recomputed package.nix to the push job
|
|
if: steps.vendorhash.outputs.bumped == 'true'
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: recomputed-package-nix
|
|
path: nix/package.nix
|
|
retention-days: 1
|
|
|
|
- name: nix flake check
|
|
run: nix flake check --print-build-logs
|
|
|
|
- name: nix build
|
|
run: nix build .#default --print-build-logs
|
|
|
|
- name: Smoke test binary
|
|
run: |
|
|
./result/bin/pad --version
|
|
./result/bin/pad --help
|
|
|
|
# The Go toolchain here exists only to install govulncheck for the
|
|
# scan step below — the pad binary under test was built by Nix above.
|
|
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
|
with:
|
|
go-version: "1.26"
|
|
|
|
- name: Scan Nix-built binary with govulncheck
|
|
# CI's main govulncheck job scans a `go build` binary, which honours
|
|
# go.mod's `toolchain go1.26.10` line — but nixpkgs pins
|
|
# GOTOOLCHAIN=local, so the Nix artifact is built with whatever Go
|
|
# patch the nixos-26.05 channel ships and can carry stdlib advisories
|
|
# the main gate can no longer see (BUG-2567). This scans the artifact
|
|
# that actually ships via Nix, against nix/accepted-advisories.txt:
|
|
# known-accepted advisories stay green and visible, any NEW advisory
|
|
# fails the job, and a warning fires when an accepted advisory clears
|
|
# (the signal to prune the list and eventually close BUG-2567).
|
|
#
|
|
# govulncheck pinned to the same release as ci.yml — bump the two
|
|
# together. GOTOOLCHAIN=auto on the install mirrors ci.yml's
|
|
# rationale: setup-go exports GOTOOLCHAIN=local, and if govulncheck's
|
|
# own go.mod ever requires a patch newer than setup-go's resolved
|
|
# one, `auto` lets Go fetch it instead of failing the install.
|
|
run: |
|
|
GOTOOLCHAIN=auto go install golang.org/x/vuln/cmd/govulncheck@v1.2.0
|
|
GOVULNCHECK="$(go env GOPATH)/bin/govulncheck" nix/vulnscan.sh result/bin/pad
|
|
|
|
# THE ONLY JOB WITH A WRITE TOKEN, and it exists as a separate job precisely so
|
|
# that the token is never minted in a context an untrusted change could reach.
|
|
#
|
|
# An `if:` on a STEP is not a security boundary — the job would still hold a
|
|
# write-capable GITHUB_TOKEN, and `actions/checkout` persists it in the
|
|
# workspace for every later command, so any change to this file inside a PR
|
|
# could use it. An `if:` on a JOB is different in kind: the job does not start,
|
|
# and no write token is issued for that run at all.
|
|
#
|
|
# WHY ON `push: main` AND NOT ON THE PR (ruled on TASK-2954's trail). A
|
|
# Dependabot PR run gets a read-only token whatever `permissions` says. The
|
|
# merge, though, is authored by a human, so the `push` run that follows is an
|
|
# ordinary run with an ordinary token. The PR's own check was already honest,
|
|
# because the job above recomputes in the working tree; `main` is what needs
|
|
# the committed fix, and it gets it from the run of the merge itself — or,
|
|
# when that run loses the push race, from the run of the next push to main.
|
|
# The two rejected alternatives were a PAT in the Dependabot secret store (a
|
|
# standing credential for a once-a-week fix) and enabling write tokens for
|
|
# pull-request workflows (which would give them to FORK PRs on a public repo —
|
|
# the surface CONVE-2438 exists to keep closed).
|
|
#
|
|
# THE LOOP TERMINATES BECAUSE THIS PUSH TRIGGERS NOTHING. A push made with
|
|
# the default `GITHUB_TOKEN` creates no workflow runs, so the commit this job
|
|
# pushes gets no run at all. Observed on the first heal this fix produced
|
|
# (BUG-2974, day 62): `gh run list --branch main` spans cbfc073e ->
|
|
# 31d11e76 -> 0b16be49 and skips the heal commit 249a8f87 entirely. Read the
|
|
# BRANCH listing, not `gh run list --commit` — that query also returned
|
|
# nothing for 0b16be49, which demonstrably has runs, so it cannot tell
|
|
# "no runs" from "no answer". The loop is closed by construction rather than
|
|
# by being green.
|
|
#
|
|
# THE CORRECTED-TREE TERMINATOR IS THE BACKSTOP, AND IT HAS NEVER RUN. If
|
|
# this push did trigger a run, the build job above would find the hash
|
|
# correct, print "Build is green; vendorHash is current.", never set
|
|
# `bumped`, and this job would not start — the FIX's own effect on the build,
|
|
# holding regardless of what any push touched. That is an argument, not a
|
|
# measurement: nothing has ever exercised it. It becomes load-bearing the
|
|
# moment this push uses a PAT or a GitHub App token, which is the obvious
|
|
# edit for anyone who wants CI to validate the healed tree. Making that
|
|
# change inherits an untested terminator; exercise it before relying on it.
|
|
#
|
|
# A CONSEQUENCE OF THE SAME MECHANISM, not a defect: the healed tip carries
|
|
# no CI of its own. Its tree is the merge commit plus the one hash line that
|
|
# merge's own Nix run computed and built against, and the next push to main
|
|
# builds on it normally.
|
|
#
|
|
# THE RANGE-VS-STATE DISTINCTION IS LOAD-BEARING, AND IT WAS GOT WRONG ONCE
|
|
# (BUG-2974). This
|
|
# comment used to name the loop guard as a gate asking whether the push had
|
|
# touched `go.mod`/`go.sum`, and that gate existed as a step below. It is the
|
|
# right question for loop prevention and the WRONG one for recovery: after a
|
|
# lost push race the tree that needs healing was written by an EARLIER push,
|
|
# so every later merge that did not itself move the module set refused to fix
|
|
# it, exited GREEN, and left main carrying a hash a clean nix build rejects.
|
|
# The gate is gone. The state comparison below is the only gate, and being a
|
|
# question about state it is equally true for a hash this push broke and one
|
|
# an earlier push broke and could not land.
|
|
#
|
|
# If another merge lands while this job runs, the push is REJECTED as a
|
|
# non-fast-forward and this job goes RED rather than overwriting anything —
|
|
# and the heal is genuinely not lost now, because the next push to main meets
|
|
# the same stale hash, recomputes it, and heals it from its own run.
|
|
#
|
|
# `needs: nix` without `if: always()` is the second half: this runs only when
|
|
# the build job SUCCEEDED, so a module set whose corrected tree still fails
|
|
# `nix flake check`, the smoke test or the vulnerability scan is never
|
|
# committed to main.
|
|
push-vendor-hash:
|
|
name: Heal main's vendorHash
|
|
needs: nix
|
|
if: >-
|
|
github.event_name == 'push' &&
|
|
github.ref == 'refs/heads/main' &&
|
|
needs.nix.outputs.vendor_hash_bumped == 'true'
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: write
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
# Nothing here reads history any more — BUG-2974 removed the gate that
|
|
# did. The depth stays full rather than dropping to the default shallow
|
|
# clone: this is the clone the heal commit is pushed from, full depth is
|
|
# what every run of this job has ever used, and a shallow push is not a
|
|
# variable worth introducing on a job that runs only when a Go bump
|
|
# actually moves the module set.
|
|
fetch-depth: 0
|
|
|
|
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
name: recomputed-package-nix
|
|
path: nix/
|
|
|
|
# THE GATE, and the only one, lives in the script: does what main COMMITTED
|
|
# differ from what the build job RECOMPUTED? The artifact exists only
|
|
# because the build job found a mismatch, but comparing there keeps the job
|
|
# honest about the tree it is actually looking at rather than about what
|
|
# another job reported. It is a file rather than a `run:` block so that
|
|
# nix/heal-vendor-hash_test.sh can drive it against a real git remote —
|
|
# BUG-2974 shipped green for a day because nothing could exercise it.
|
|
- name: Commit and push the recomputed hash
|
|
run: nix/heal-vendor-hash.sh
|
|
env:
|
|
SHA: ${{ github.sha }}
|