From 78755be32346c1df4fd9c2c658a0b0301db4c4e9 Mon Sep 17 00:00:00 2001 From: Joseph Yaksich Date: Tue, 4 Aug 2026 23:38:42 +0000 Subject: [PATCH] fix(phase4): repair candidate artifact layout and guard Linux acceptance The first full three-platform candidate run failed in every downstream lane from one root cause: the Linux candidate and channel-image artifacts each mixed `dist/...` and `container/...` upload paths. actions/upload-artifact uses the least-common-ancestor of its inputs as the archive root, so a single `container/` entry made the root the repository root and nested every file under `dist/` on download. Consumers look at the download root, so Linux, Windows, and Phase 2 all reported "candidate archive is missing", and the promotion assembler would have failed the same way on the channel image. Remove the redundant `container/channel-machine.oci.json` from the candidate artifact and `container/channel-machine.oci.sha256` from the channel-image artifact. Both duplicates remain retained elsewhere (the immutable channel image artifact and each release's own container tree); no consumer reads them from these artifacts. The exact candidate and image bytes are unchanged. Also clear the retained self-hosted dress-rehearsal workspace before download so a stale prior candidate can never mask a layout error again. Harden ops/platform-acceptance/linux.sh: it performs a real root install (port 8123, /var/lib/1helm-oci-v1, systemd units), which is only safe on a disposable hosted runner. Refuse unless RUNNER_ENVIRONMENT=github-hosted, no 1Helm/standalone host state already exists, and port 8123 is free. This encodes the prior production-clobber lesson without changing the shipped installer's paths. Tests updated to assert single-rooted uploads, the workspace clear, and the Linux isolation guard. Full `npm run ci` passes. No release, tag, version bump, website deploy, or production change. Co-Authored-By: Claude --- .github/workflows/candidate.yml | 5 +++-- ops/platform-acceptance/linux.sh | 17 +++++++++++++++++ test/phase2-candidate.mjs | 5 +++++ test/phase4-platform-acceptance.mjs | 10 ++++++++++ 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/.github/workflows/candidate.yml b/.github/workflows/candidate.yml index d159b15..82cd432 100644 --- a/.github/workflows/candidate.yml +++ b/.github/workflows/candidate.yml @@ -190,7 +190,6 @@ jobs: dist/1Helm-*-linux-split.json dist/artifact-size-report.json dist/artifact-size-report.txt - container/channel-machine.oci.json dist/candidate-evidence/candidate.json dist/candidate-evidence/archive.sha256 dist/candidate-evidence/manifest.sha256 @@ -205,7 +204,6 @@ jobs: path: | dist/1Helm-channel-machine-v1-*.oci.tar dist/1Helm-channel-machine-v1-*.json - container/channel-machine.oci.sha256 if-no-files-found: error retention-days: 90 @@ -308,6 +306,9 @@ jobs: actions: read attestations: read steps: + - name: Clear retained runner workspace from prior candidates + run: rm -rf -- candidate-download candidate-result + - name: Download this workflow's exact candidate uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: diff --git a/ops/platform-acceptance/linux.sh b/ops/platform-acceptance/linux.sh index 7e31e57..59bea8e 100755 --- a/ops/platform-acceptance/linux.sh +++ b/ops/platform-acceptance/linux.sh @@ -24,6 +24,23 @@ export HELM_ACCEPTANCE_STARTED_AT="$STARTED_AT" export HELM_PHASE4_RUNNER_LABEL=ubuntu-latest node "$ROOT/scripts/pending-acceptance-evidence.mjs" [[ "$(id -u)" -ne 0 ]] || { echo "Linux acceptance must begin as the hosted ordinary runner user." >&2; exit 1; } + +# This lane performs a REAL root install of 1Helm on its runner: it binds port +# 8123, writes /var/lib/1helm-oci-v1, and installs the 1helm systemd units. That +# is only safe on a disposable GitHub-hosted runner that holds no user or +# production data. Refuse anywhere that looks persistent, self-hosted, or +# already-inhabited so a misrouted job can never clobber a real host's live +# 1Helm or standalone state. Blocked evidence was already retained above. +[[ "${RUNNER_ENVIRONMENT:-}" == "github-hosted" ]] \ + || { echo "Linux acceptance refuses to boot a real 1Helm outside a disposable GitHub-hosted runner." >&2; exit 1; } +for guarded in /var/lib/1helm-oci-v1 /var/lib/1helm-standalone /opt/1helm; do + [[ ! -e "$guarded" ]] \ + || { echo "Linux acceptance refuses to run where 1Helm host state already exists: $guarded" >&2; exit 1; } +done +if command -v ss >/dev/null 2>&1 && ss -ltn 2>/dev/null | grep -qE '[:.]8123[[:space:]]'; then + echo "Linux acceptance refuses to run while port 8123 is already in use." >&2 + exit 1 +fi [[ "$(sha256sum "$ARCHIVE" | awk '{print $1}')" == "$DIGEST" ]] \ || { echo "Linux candidate digest mismatch." >&2; exit 1; } [[ "$(sha256sum "$OFFLINE_ARCHIVE" | awk '{print $1}')" == "$OFFLINE_DIGEST" ]] \ diff --git a/test/phase2-candidate.mjs b/test/phase2-candidate.mjs index 799c04b..fd68cc8 100644 --- a/test/phase2-candidate.mjs +++ b/test/phase2-candidate.mjs @@ -115,6 +115,7 @@ test("rollback fixtures remain local-only and cannot satisfy normal candidate va test("candidate workflow and guest boundary exclude PR code and broad root access", () => { const workflow = read(".github/workflows/candidate.yml"); + const candidateUpload = workflow.match(/- name: Upload exact candidate and evidence[\s\S]*?retention-days: 30/)?.[0] || ""; const helper = read("ops/dress-rehearsal/1helm-candidate-install"); const hook = read("ops/dress-rehearsal/runner-job-started.sh"); const sudoersExample = "%actions ALL=(root) NOPASSWD: /usr/local/sbin/1helm-candidate-install \"\"\n"; @@ -122,10 +123,14 @@ test("candidate workflow and guest boundary exclude PR code and broad root acces assert.match(workflow, /workflow_run\.event == 'push'/); assert.match(workflow, /head_repository\.full_name == github\.repository/); assert.match(workflow, /runs-on: \[1helm-dress-rehearsal-phase2\]/); + assert.match(workflow, /Clear retained runner workspace[\s\S]*rm -rf -- candidate-download candidate-result[\s\S]*Download this workflow's exact candidate/); assert.match(workflow, /github\.sha == github\.event\.workflow_run\.head_sha/); assert.match(workflow, /attest-build-provenance@[a-f0-9]{40}/); assert.match(workflow, /candidate-download\/candidate-evidence\/candidate\.json/); assert.match(workflow, /candidate-download\/candidate-evidence\/provenance\.bundle\.json/); + assert.match(candidateUpload, /dist\/1Helm-\*-linux-node\.tgz/); + assert.match(candidateUpload, /dist\/candidate-evidence\/candidate\.json/); + assert.doesNotMatch(candidateUpload, /container\/channel-machine\.oci\.json/); assert.match(helper, /--signer-workflow gitcommit90\/1Helm\/\.github\/workflows\/candidate\.yml/); assert.match(helper, /--source-ref refs\/heads\/main/); assert.match(helper, /--source-digest "\$commit"/); diff --git a/test/phase4-platform-acceptance.mjs b/test/phase4-platform-acceptance.mjs index e4e735c..15a2f0f 100644 --- a/test/phase4-platform-acceptance.mjs +++ b/test/phase4-platform-acceptance.mjs @@ -112,6 +112,16 @@ test("workflow routes no PR/fork code, uses unique labels, fans acceptance out, assert.doesNotMatch(workflow, /pull_request_target|workflow_dispatch/); assert.doesNotMatch(workflow.match(/assemble-promotion:[\s\S]*?(?=\n candidate-status:)/)?.[0] || "", /npm (ci|install|run build|run package)/); assert.match(workflow, /Upload exact Linux acceptance evidence\n if: always\(\)/); + // Both retained artifacts must be single-rooted under dist/ so consumers find + // files at the download root, not nested under dist/ (the layout bug that + // broke Linux, Windows, and Phase 2 candidate discovery in one run). + const channelUpload = workflow.match(/- name: Retain immutable digest-addressed channel image candidate[\s\S]*?retention-days: 90/)?.[0] || ""; + assert.doesNotMatch(channelUpload, /container\//); + assert.match(channelUpload, /dist\/1Helm-channel-machine-v1-\*\.oci\.tar/); + const linuxAccept = read("ops/platform-acceptance/linux.sh"); + assert.match(linuxAccept, /RUNNER_ENVIRONMENT.*==.*"github-hosted"/); + assert.match(linuxAccept, /1helm-standalone/); + assert.match(linuxAccept, /refuses to run while port 8123 is already in use/); assert.match(workflow.match(/accept-macos:[\s\S]*?(?=\n accept-windows:)/)?.[0] || "", /if: always\(\)[\s\S]*name: 1helm-macos-acceptance-/); assert.match(workflow.match(/accept-windows:[\s\S]*?(?=\n assemble-promotion:)/)?.[0] || "", /if: always\(\)[\s\S]*name: 1helm-windows-acceptance-/); });