From e6bca6faee61c98ec219c2eefcf8061c26f26f71 Mon Sep 17 00:00:00 2001 From: "XXV.CC" Date: Mon, 27 Jul 2026 20:37:03 +0800 Subject: [PATCH] ci: serialize root integration packages --- .github/workflows/go.yml | 4 +- .github/workflows/mirror-release.yml | 2 +- .github/workflows/release.yml | 2 +- CHANGELOG.md | 9 +++++ docs/releasing.md | 42 ++++++++++++-------- internal/selfmanage/release_pipeline_test.go | 22 ++++++++++ 6 files changed, 61 insertions(+), 20 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index f9098b8..e354780 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -50,7 +50,9 @@ jobs: go-version: '1.26.x' check-latest: true - name: Integration tests (root) - run: sudo -E env "PATH=$PATH" go test -race -tags integration ./... + # Root integration packages share the host account databases and must + # not run useradd/userdel concurrently across package processes. + run: sudo -E env "PATH=$PATH" go test -race -p 1 -tags integration ./... static-cross-build: name: Static cross-build (${{ matrix.goarch }}) diff --git a/.github/workflows/mirror-release.yml b/.github/workflows/mirror-release.yml index faad884..da91d27 100644 --- a/.github/workflows/mirror-release.yml +++ b/.github/workflows/mirror-release.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: inputs: tag: - description: Immutable release tag to mirror, for example v2.8.1 + description: Immutable release tag to mirror, for example v2.8.2 required: true type: string diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0f3cfc7..900495f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -109,7 +109,7 @@ jobs: GOPROXY="$GOPROXY" GOSUMDB="$GOSUMDB" GONOSUMDB= GOPRIVATE= GONOPROXY= GOINSECURE= \ GOCACHE="$root_go_root/gocache" GOMODCACHE="$root_go_root/gomodcache" \ GOPATH="$root_go_root/gopath" GOTMPDIR="$root_go_root/gotmp" \ - go test -mod=readonly -count=1 -race -tags integration ./... + go test -mod=readonly -count=1 -race -p 1 -tags integration ./... test -z "$(gofmt -l .)" sh -n scripts/install.sh bash -n scripts/*.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 70b75b4..b2e2123 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this project are documented here. +## v2.8.2 - 2026-07-27 + +- Serialize root integration test packages because they share the host account + databases. The signed `v2.8.1` tag exposed a cross-package `useradd` lock + timeout in the release gate before any binary, draft, or Release was created + and remains only as an audit record; no published v2.8.1 artifacts exist. +- Record that the current v1 ed25519 release key has historical network-host + exposure instead of claiming an air-gapped custody history it does not have. + ## v2.8.1 - 2026-07-27 - Keep the release-only root integration workspace below a fresh root-owned diff --git a/docs/releasing.md b/docs/releasing.md index 8bc56f0..cd95749 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -1,9 +1,12 @@ # Releasing linux-temp-admin v2 -Release binaries are reproducibly rebuilt and signed with ed25519. The release -private key is never present on a networked machine, candidate source is never -executed on the signing machine, and CI output is never signed merely because -its own checksum file matches. +Release binaries are reproducibly rebuilt and signed with ed25519. The target +release boundary keeps the private key off networked machines, never executes +candidate source on the signing machine, and never signs CI output merely +because its own checksum file matches. The current v1 ed25519 key predates that +custody boundary and has historical exposure on a networked maintainer host; +releases using it must retain that residual-risk record until the planned key +rotation completes. ## Maintainer model @@ -419,10 +422,10 @@ git -c user.name='XXV.CC' \ -c user.signingkey="${TAG_SIGNING_FPR}!" \ -c gpg.format=openpgp \ -c gpg.program=/usr/bin/gpg \ - tag -s v2.8.1 "$RELEASE_COMMIT" -m 'linux-temp-admin v2.8.1' + tag -s v2.8.2 "$RELEASE_COMMIT" -m 'linux-temp-admin v2.8.2' git -c gpg.format=openpgp -c gpg.program=/usr/bin/gpg \ - verify-tag --raw v2.8.1 -git push origin v2.8.1 + verify-tag --raw v2.8.2 +git push origin v2.8.2 ``` Before pushing, the `VALIDSIG` record from `verify-tag --raw` must identify the @@ -432,7 +435,8 @@ an armored OpenPGP signature before continuing. Use that same fingerprint as `LTA_EXPECTED_TAG_SIGNER_FINGERPRINT` during preparation and publication. The `Release` workflow uses exactly Go 1.26.5. Its read-only `gate-build` job -runs vet, uncached race tests, root integration tests, formatting, shell checks, +runs vet, uncached race tests, package-serialized root integration tests, +formatting, shell checks, the mirror receiver policy tests, a clean-worktree check, and `govulncheck v1.6.0`, then builds static amd64/arm64 binaries with fixed tuning, `GOWORK=off`, `-mod=readonly`, and @@ -485,7 +489,7 @@ printf '\n' >/dev/tty || fail "GH_TOKEN must be one non-empty token without whitespace" export GH_TOKEN exec /opt/lta-release-tools/prepare-release.sh \ - v2.8.1 /srv/linux-temp-admin /srv/release-transfer/v2.8.1-prepared + v2.8.2 /srv/linux-temp-admin /srv/release-transfer/v2.8.2-prepared LTA_PREPARE_RELEASE ``` @@ -505,7 +509,7 @@ the candidate or transfer media: LTA_SIGN_KEY=/offline/keys/release-v1.key LTA_TRUSTED_SIGNER=/opt/lta-release-tools/lta-release LTA_TRUSTED_SIGNER_SHA256='' -LTA_EXPECTED_TAG=v2.8.1 +LTA_EXPECTED_TAG=v2.8.2 LTA_EXPECTED_COMMIT='' LTA_EXPECTED_PREPARED_MANIFEST_SHA256='' LTA_EXPECTED_RELEASE_SIGNER_PUBKEY='' @@ -517,7 +521,7 @@ LTA_EXPECTED_RELEASE_SIGNER_PUBKEY='/dev/tty || fail "GH_TOKEN must be one non-empty token without whitespace" export GH_TOKEN exec /opt/lta-release-tools/publish-release.sh \ - /srv/release-transfer/v2.8.1-signed /srv/linux-temp-admin + /srv/release-transfer/v2.8.2-signed /srv/linux-temp-admin LTA_PUBLISH_RELEASE ``` @@ -633,7 +637,7 @@ noncanonical published stable tag, excludes the failed `TAG`, and verifies the exact resulting Latest state: ```bash -TAG=v2.8.1 # the failed release; verify this value before running +TAG=v2.8.2 # the failed release; verify this value before running /usr/bin/sudo /usr/bin/env -i \ HOME=/root PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin LC_ALL=C \ TAG="$TAG" /bin/bash -p <<'LTA_LATEST_RECOVERY' @@ -992,7 +996,7 @@ the release audit/signing record and a separate authenticated channel, then run: ```bash INSTALLER_COMMIT='replace-with-the-audited-40-hex-commit' INSTALLER_SHA256='replace-with-the-independent-64-hex-script-hash' -LTA_RELEASE_TAG='v2.8.1' +LTA_RELEASE_TAG='v2.8.2' /usr/bin/sudo /usr/bin/env -i \ HOME=/root PATH=/usr/sbin:/usr/bin:/sbin:/bin LC_ALL=C \ INSTALLER_COMMIT="$INSTALLER_COMMIT" INSTALLER_SHA256="$INSTALLER_SHA256" \ @@ -1136,9 +1140,13 @@ metadata are already safe; otherwise it is atomically repaired. ## Trust boundaries and residual risk -- The private key is protected from candidate code, CI, GitHub, and networked - preparation/publication. The air-gapped OS, fixed signer binary, trusted - offline script, and physical transfer procedure remain trusted. +- The signing phase protects the private key from candidate code, CI, GitHub, + and networked preparation/publication. The current v1 key was historically + stored on a networked maintainer host, so its past confidentiality cannot be + inferred from the isolated signing ceremony. Complete the planned overlap + rotation before claiming an air-gapped custody history for the active key. + The air-gapped OS, fixed signer binary, trusted offline script, and physical + transfer procedure remain trusted for keys that actually follow that model. - Reproducible comparison binds CI bytes to the audited tag under the fixed Go toolchain. The audited source, signed-tag identity, trusted preparation copy, Go distribution, and preparation workstation remain trusted. diff --git a/internal/selfmanage/release_pipeline_test.go b/internal/selfmanage/release_pipeline_test.go index c6fa916..e2e9c65 100644 --- a/internal/selfmanage/release_pipeline_test.go +++ b/internal/selfmanage/release_pipeline_test.go @@ -3338,6 +3338,28 @@ func TestVulnerabilityScannerIsPinnedAndRunsInReleaseGate(t *testing.T) { } } +func TestRootIntegrationPackagesRunSerially(t *testing.T) { + workflows := map[string]struct { + path string + want string + }{ + "Go": { + path: "../../.github/workflows/go.yml", + want: `go test -race -p 1 -tags integration ./...`, + }, + "Release": { + path: "../../.github/workflows/release.yml", + want: `go test -mod=readonly -count=1 -race -p 1 -tags integration ./...`, + }, + } + for name, workflow := range workflows { + content := readReleaseFile(t, workflow.path) + if !strings.Contains(content, workflow.want) { + t.Errorf("%s workflow does not serialize root integration packages", name) + } + } +} + func TestReleaseKeyringValidationIsPortableAcrossAwkImplementations(t *testing.T) { const portableCheck = `length($0) != 64 || $0 !~ /^[0-9A-Fa-f]+$/` const nonPortableCheck = `$0 !~ /^[0-9A-Fa-f]{64}$/`