Skip to content

Commit 521d73d

Browse files
gitcommit90claude
andauthored
Build fast previews and trusted candidate delivery (#67)
* build: add preview and candidate delivery Add preview-only agent guardrails, fast private previews, read-only status and cleanup reports, and a trusted-main dress-rehearsal workflow with verified rollback. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Joseph Yaksich <gitcommit90@users.noreply.github.com> * test: use exact status URL comparisons Resolve CodeQL's incomplete URL substring sanitization findings in the delivery status test stub. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Joseph Yaksich <gitcommit90@users.noreply.github.com> --------- Signed-off-by: Joseph Yaksich <gitcommit90@users.noreply.github.com> Co-authored-by: Joseph Yaksich <gitcommit90@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0b1dadb commit 521d73d

31 files changed

Lines changed: 2524 additions & 47 deletions

.delivery-status.local.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Copy outside git or export locally. Never commit real private coordinates.
2+
HELM_STATUS_CANDIDATE_URL=http://PRIVATE_GUEST_ADDRESS:8123
3+
HELM_STATUS_CANDIDATE_HOST=LOCAL_PROXMOX_SSH_ALIAS
4+
HELM_STATUS_CANDIDATE_ID=LOCAL_GUEST_ID

.github/workflows/candidate.yml

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
name: Candidate dress rehearsal
2+
3+
on:
4+
workflow_run:
5+
workflows: [CI]
6+
types: [completed]
7+
branches: [main]
8+
9+
permissions:
10+
contents: read
11+
12+
concurrency:
13+
group: 1helm-private-dress-rehearsal
14+
cancel-in-progress: false
15+
16+
jobs:
17+
build:
18+
name: Build exact trusted-main Linux candidate
19+
if: >-
20+
github.event.workflow_run.conclusion == 'success' &&
21+
github.event.workflow_run.event == 'push' &&
22+
github.event.workflow_run.head_branch == 'main' &&
23+
github.event.workflow_run.head_repository.full_name == github.repository &&
24+
github.event.repository.full_name == github.repository &&
25+
github.sha == github.event.workflow_run.head_sha &&
26+
github.ref == 'refs/heads/main'
27+
runs-on: ubuntu-latest
28+
timeout-minutes: 60
29+
permissions:
30+
contents: read
31+
id-token: write
32+
attestations: write
33+
outputs:
34+
artifact-name: ${{ steps.identity.outputs.artifact_name }}
35+
commit: ${{ steps.identity.outputs.commit }}
36+
steps:
37+
- name: Check out the exact successful CI commit
38+
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
39+
with:
40+
ref: ${{ github.event.workflow_run.head_sha }}
41+
fetch-depth: 1
42+
persist-credentials: false
43+
44+
- name: Re-verify trusted repository, ref, event, and SHA
45+
env:
46+
CI_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
47+
CI_HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
48+
CI_HEAD_REPOSITORY: ${{ github.event.workflow_run.head_repository.full_name }}
49+
CI_EVENT: ${{ github.event.workflow_run.event }}
50+
CI_CONCLUSION: ${{ github.event.workflow_run.conclusion }}
51+
CI_WORKFLOW: ${{ github.event.workflow_run.name }}
52+
run: |
53+
set -euo pipefail
54+
test "$GITHUB_REPOSITORY" = "gitcommit90/1Helm"
55+
test "$CI_HEAD_REPOSITORY" = "$GITHUB_REPOSITORY"
56+
test "$CI_HEAD_BRANCH" = "main"
57+
test "$CI_EVENT" = "push"
58+
test "$CI_CONCLUSION" = "success"
59+
test "$CI_WORKFLOW" = "CI"
60+
test "$GITHUB_REF" = "refs/heads/main"
61+
test "$GITHUB_SHA" = "$CI_HEAD_SHA"
62+
test "$(git rev-parse HEAD)" = "$CI_HEAD_SHA"
63+
test -z "$(git status --porcelain)"
64+
65+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
66+
with:
67+
node-version: "22"
68+
cache: npm
69+
70+
- name: Install exact dependencies and builder runtime
71+
run: |
72+
set -euo pipefail
73+
PUPPETEER_SKIP_DOWNLOAD=1 npm ci
74+
sudo apt-get update
75+
sudo apt-get install -y podman
76+
77+
- name: Build sealed OCI image and ready-to-run Linux archive
78+
env:
79+
HELM_CANDIDATE_REPOSITORY: gitcommit90/1Helm
80+
HELM_CANDIDATE_REF: refs/heads/main
81+
HELM_CANDIDATE_COMMIT: ${{ github.event.workflow_run.head_sha }}
82+
HELM_CANDIDATE_SOURCE_STATE: trusted-main
83+
HELM_CANDIDATE_BUILD_ID: candidate-${{ github.event.workflow_run.id }}-${{ github.run_id }}.${{ github.run_attempt }}
84+
HELM_CANDIDATE_CREATED_AT: ${{ github.event.workflow_run.updated_at }}
85+
HELM_CANDIDATE_CI_WORKFLOW: CI
86+
HELM_CANDIDATE_CI_RUN_ID: ${{ github.event.workflow_run.id }}
87+
HELM_CANDIDATE_CI_CONCLUSION: success
88+
run: |
89+
set -euo pipefail
90+
npm run package:channel-image
91+
npm run package:linux
92+
93+
- name: Generate candidate manifest and evidence
94+
id: identity
95+
env:
96+
CI_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
97+
run: |
98+
set -euo pipefail
99+
version="$(node -p 'require("./package.json").version')"
100+
archive="dist/1Helm-${version}-linux-node.tgz"
101+
evidence="dist/candidate-evidence"
102+
mkdir -p "$evidence"
103+
HELM_CANDIDATE_ARCHIVE="$archive" \
104+
HELM_CANDIDATE_MANIFEST="$evidence/candidate.json" \
105+
node scripts/candidate-manifest.mjs
106+
cp "$archive.sha256" "$evidence/archive.sha256"
107+
sha256sum "$evidence/candidate.json" > "$evidence/manifest.sha256"
108+
printf 'artifact_name=1helm-candidate-%s\n' "$CI_HEAD_SHA" >> "$GITHUB_OUTPUT"
109+
printf 'commit=%s\n' "$CI_HEAD_SHA" >> "$GITHUB_OUTPUT"
110+
111+
- name: Attest archive provenance on the hosted builder
112+
id: attest
113+
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3
114+
with:
115+
subject-path: dist/1Helm-*-linux-node.tgz
116+
117+
- name: Retain signed provenance bundle
118+
env:
119+
BUNDLE_PATH: ${{ steps.attest.outputs.bundle-path }}
120+
run: |
121+
set -euo pipefail
122+
test -s "$BUNDLE_PATH"
123+
install -m 0644 "$BUNDLE_PATH" dist/candidate-evidence/provenance.bundle.json
124+
125+
- name: Upload exact candidate and evidence
126+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
127+
with:
128+
name: ${{ steps.identity.outputs.artifact_name }}
129+
path: |
130+
dist/1Helm-*-linux-node.tgz
131+
dist/candidate-evidence/candidate.json
132+
dist/candidate-evidence/archive.sha256
133+
dist/candidate-evidence/manifest.sha256
134+
dist/candidate-evidence/provenance.bundle.json
135+
if-no-files-found: error
136+
retention-days: 30
137+
138+
deploy:
139+
name: Install only on private Phase 2 dress rehearsal
140+
needs: build
141+
runs-on: [1helm-dress-rehearsal-phase2]
142+
timeout-minutes: 20
143+
permissions:
144+
contents: read
145+
actions: read
146+
steps:
147+
- name: Download this workflow's exact candidate
148+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
149+
with:
150+
name: ${{ needs.build.outputs.artifact-name }}
151+
path: candidate-download
152+
153+
- name: Submit fixed candidate inputs to the root-owned boundary
154+
env:
155+
EXPECTED_COMMIT: ${{ needs.build.outputs.commit }}
156+
run: |
157+
set -euo pipefail
158+
test "$GITHUB_REPOSITORY" = "gitcommit90/1Helm"
159+
test "$EXPECTED_COMMIT" = "${{ github.event.workflow_run.head_sha }}"
160+
test "${{ github.event.workflow_run.head_branch }}" = "main"
161+
test "${{ github.event.workflow_run.head_repository.full_name }}" = "$GITHUB_REPOSITORY"
162+
test "${{ github.event.workflow_run.conclusion }}" = "success"
163+
archive="$(find candidate-download -maxdepth 1 -type f -name '1Helm-*-linux-node.tgz' -print -quit)"
164+
test -n "$archive"
165+
install -m 0600 "$archive" /var/lib/1helm-candidate/inbox/candidate.tgz
166+
install -m 0600 candidate-download/candidate-evidence/candidate.json /var/lib/1helm-candidate/inbox/candidate.json
167+
install -m 0600 candidate-download/candidate-evidence/provenance.bundle.json /var/lib/1helm-candidate/inbox/provenance.bundle.json
168+
sudo -n /usr/local/sbin/1helm-candidate-install
169+
170+
- name: Publish private installation evidence in the job log
171+
if: always()
172+
run: |
173+
test -r /var/lib/1helm-candidate/evidence/status.json
174+
python3 /usr/local/lib/1helm-candidate/candidate-boundary.py summary \
175+
/var/lib/1helm-candidate/evidence/status.json

.gitignore

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,16 @@ data-refactored/
2727
.deploy-backups/
2828
.claude/
2929
.design/
30+
31+
# generated local delivery/test state
32+
# Host tooling may ignore agent instruction files globally; these are tracked.
33+
!/AGENTS.md
34+
!/CLAUDE.md
35+
/.release-tmp/
36+
/.native-test-data/
37+
/.preview-data/
38+
/.test-state/
39+
/.delivery-status.local
40+
__pycache__/
41+
*.py[cod]
42+
/src/server/agent.ts.bak-normal-terminal-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]

AGENTS.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Delivery contract for coding agents
2+
3+
The default delivery mode in this repository is **PREVIEW ONLY**. Implement and
4+
verify a small, explicitly requested change, then report it for review.
5+
6+
Unless the owner explicitly requests a stable promotion, do not:
7+
8+
- bump versions or edit release notes for a release;
9+
- create or publish tags, releases, or artifacts;
10+
- deploy the public website or update stable or its release metadata;
11+
- change production data, infrastructure, containers, VMs, or services; or
12+
- broaden the requested scope.
13+
14+
Keep changes focused. Run the narrowest relevant tests while iterating, then run
15+
the full CI contract (`npm run ci`) before merge. Never weaken a check to make a
16+
change pass.
17+
18+
Every handoff must briefly name changed files, checks run and their results,
19+
known risks, rollback steps, and whether stable or any external system was
20+
touched.
21+
22+
Maintainer policy and release mechanics remain authoritative in
23+
[`docs/GOVERNANCE.md`](docs/GOVERNANCE.md) and
24+
[`docs/release-lifecycle.md`](docs/release-lifecycle.md). This file adds the
25+
agent default; it does not replace those documents.

CLAUDE.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Claude Code instructions
2+
3+
Follow [AGENTS.md](AGENTS.md) as the authoritative delivery contract for all
4+
work in this repository.

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,25 @@ npm start # http://127.0.0.1:8123
412412
A fresh data directory opens first-run setup. The source runtime defaults to
413413
`./data`; do not point development at an existing production data directory.
414414

415+
### Private development preview
416+
417+
Run `npm run preview`, then open **http://127.0.0.1:8124**. Stop it with
418+
Ctrl+C. Client CSS/JavaScript and server source changes are watched; refresh
419+
the private page manually after a change.
420+
421+
The command refuses Stable's port and normal app data paths. It always uses
422+
separate generated test data under `.preview-data/`, so it never changes or
423+
restarts Stable. If Stable uses a locally customized port, set
424+
`HELM_STABLE_PORT` before launching the preview so that port is refused too.
425+
426+
### Private Linux dress rehearsal
427+
428+
After trusted `main` passes CI, the candidate workflow prepares one exact,
429+
attested Linux build for a dedicated private guest and installs it through a
430+
digest-verified, rollback-aware root boundary. It does not publish a release,
431+
change Stable, or run PR code on the guest. Private coordinates stay in local
432+
status configuration. See [Private Linux dress rehearsal](docs/dress-rehearsal.md).
433+
415434
### Core configuration
416435

417436
| Environment variable | Default | Meaning |

docs/canary-plan.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Delivery canary plan
2+
3+
## Phase 0 boundary
4+
5+
Phase 0 creates documentation and read-only visibility only. It does not
6+
create or modify infrastructure, containers, VMs, services, deployment targets,
7+
releases, stable metadata, or production data.
8+
9+
LXC 112 on `pve2` remains unchanged as the legacy **v0.0.38 updater fixture**.
10+
It is evidence for the prior-version update path, not a general-purpose canary.
11+
No experiment, candidate install, reset, or cleanup may repurpose it.
12+
13+
## Phase 2 private dress rehearsal
14+
15+
Phase 2 was separately approved. It creates one fresh, unprivileged LXC with its
16+
own identity, storage, private network address, and lifecycle. It does not share
17+
production data, credentials, release metadata, or the legacy fixture. The
18+
guest ID, address, and hypervisor access stay in local operator configuration,
19+
not this public repository.
20+
21+
After `CI` succeeds for a push to trusted `main`, `Candidate dress rehearsal`
22+
checks out that exact CI SHA on a GitHub-hosted builder. It builds the sealed OCI
23+
image and ready-to-run Linux archive without a version bump or GitHub Release,
24+
embeds source/build identity, emits a digest manifest, signs GitHub artifact
25+
provenance, and retains all evidence as a workflow artifact. Only then does its
26+
deployment job select the uniquely labelled repository runner in the dedicated
27+
guest.
28+
29+
The runner cannot install arbitrary bytes. A fixed root-owned command copies
30+
the fixed inbox files, requires signed provenance from the trusted candidate
31+
workflow on `main`, rejects self-hosted provenance, and requires the outer
32+
manifest, embedded identity, archive digest, source SHA, version, and sealed OCI
33+
digest to agree. It then reuses the immutable release store, service health
34+
check, and automatic rollback in the Linux installer. The status record contains:
35+
36+
- canary role, hypervisor/guest identity, and health endpoint;
37+
- current version and candidate version;
38+
- exact source commit plus artifact name and SHA-256 digest;
39+
- service and application health, check time, result, and any uncertainty;
40+
- CI workflow/run result and candidate build identity;
41+
- install health and time; and
42+
- previous candidate plus rollback result/time.
43+
44+
Unknown metadata must be reported as unknown, never inferred from a nearby
45+
checkout, tag, or responding port.
46+
47+
## Rollback gate
48+
49+
The dedicated guest starts from a documented clean baseline. Each accepted
50+
candidate becomes an immutable, digest-named release directory. Before changing
51+
the current symlink or host contract, the existing Linux transaction snapshots
52+
the prior current release, runtime files, units, and unit state. A failed or
53+
uncertain service/API health check restores that exact prior contract and proves
54+
the restored service healthy. The candidate evidence records both failure and
55+
rollback outcome. This is application rollback inside the dedicated guest; no
56+
Proxmox snapshot, Stable change, or LXC 112 action is part of the automation.

0 commit comments

Comments
 (0)