Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .delivery-status.local.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Copy outside git or export locally. Never commit real private coordinates.
HELM_STATUS_CANDIDATE_URL=http://PRIVATE_GUEST_ADDRESS:8123
HELM_STATUS_CANDIDATE_HOST=LOCAL_PROXMOX_SSH_ALIAS
HELM_STATUS_CANDIDATE_ID=LOCAL_GUEST_ID
175 changes: 175 additions & 0 deletions .github/workflows/candidate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
name: Candidate dress rehearsal

on:
workflow_run:
workflows: [CI]
types: [completed]
branches: [main]

permissions:
contents: read

concurrency:
group: 1helm-private-dress-rehearsal
cancel-in-progress: false

jobs:
build:
name: Build exact trusted-main Linux candidate
if: >-
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_branch == 'main' &&
github.event.workflow_run.head_repository.full_name == github.repository &&
github.event.repository.full_name == github.repository &&
github.sha == github.event.workflow_run.head_sha &&
github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
contents: read
id-token: write
attestations: write
outputs:
artifact-name: ${{ steps.identity.outputs.artifact_name }}
commit: ${{ steps.identity.outputs.commit }}
steps:
- name: Check out the exact successful CI commit
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
ref: ${{ github.event.workflow_run.head_sha }}
fetch-depth: 1
persist-credentials: false

- name: Re-verify trusted repository, ref, event, and SHA
env:
CI_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
CI_HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
CI_HEAD_REPOSITORY: ${{ github.event.workflow_run.head_repository.full_name }}
CI_EVENT: ${{ github.event.workflow_run.event }}
CI_CONCLUSION: ${{ github.event.workflow_run.conclusion }}
CI_WORKFLOW: ${{ github.event.workflow_run.name }}
run: |
set -euo pipefail
test "$GITHUB_REPOSITORY" = "gitcommit90/1Helm"
test "$CI_HEAD_REPOSITORY" = "$GITHUB_REPOSITORY"
test "$CI_HEAD_BRANCH" = "main"
test "$CI_EVENT" = "push"
test "$CI_CONCLUSION" = "success"
test "$CI_WORKFLOW" = "CI"
test "$GITHUB_REF" = "refs/heads/main"
test "$GITHUB_SHA" = "$CI_HEAD_SHA"
test "$(git rev-parse HEAD)" = "$CI_HEAD_SHA"
test -z "$(git status --porcelain)"

- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "22"
cache: npm

- name: Install exact dependencies and builder runtime
run: |
set -euo pipefail
PUPPETEER_SKIP_DOWNLOAD=1 npm ci
sudo apt-get update
sudo apt-get install -y podman

- name: Build sealed OCI image and ready-to-run Linux archive
env:
HELM_CANDIDATE_REPOSITORY: gitcommit90/1Helm
HELM_CANDIDATE_REF: refs/heads/main
HELM_CANDIDATE_COMMIT: ${{ github.event.workflow_run.head_sha }}
HELM_CANDIDATE_SOURCE_STATE: trusted-main
HELM_CANDIDATE_BUILD_ID: candidate-${{ github.event.workflow_run.id }}-${{ github.run_id }}.${{ github.run_attempt }}
HELM_CANDIDATE_CREATED_AT: ${{ github.event.workflow_run.updated_at }}
HELM_CANDIDATE_CI_WORKFLOW: CI
HELM_CANDIDATE_CI_RUN_ID: ${{ github.event.workflow_run.id }}
HELM_CANDIDATE_CI_CONCLUSION: success
run: |
set -euo pipefail
npm run package:channel-image
npm run package:linux

- name: Generate candidate manifest and evidence
id: identity
env:
CI_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
set -euo pipefail
version="$(node -p 'require("./package.json").version')"
archive="dist/1Helm-${version}-linux-node.tgz"
evidence="dist/candidate-evidence"
mkdir -p "$evidence"
HELM_CANDIDATE_ARCHIVE="$archive" \
HELM_CANDIDATE_MANIFEST="$evidence/candidate.json" \
node scripts/candidate-manifest.mjs
cp "$archive.sha256" "$evidence/archive.sha256"
sha256sum "$evidence/candidate.json" > "$evidence/manifest.sha256"
printf 'artifact_name=1helm-candidate-%s\n' "$CI_HEAD_SHA" >> "$GITHUB_OUTPUT"
printf 'commit=%s\n' "$CI_HEAD_SHA" >> "$GITHUB_OUTPUT"

- name: Attest archive provenance on the hosted builder
id: attest
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3
with:
subject-path: dist/1Helm-*-linux-node.tgz

- name: Retain signed provenance bundle
env:
BUNDLE_PATH: ${{ steps.attest.outputs.bundle-path }}
run: |
set -euo pipefail
test -s "$BUNDLE_PATH"
install -m 0644 "$BUNDLE_PATH" dist/candidate-evidence/provenance.bundle.json

- name: Upload exact candidate and evidence
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: ${{ steps.identity.outputs.artifact_name }}
path: |
dist/1Helm-*-linux-node.tgz
dist/candidate-evidence/candidate.json
dist/candidate-evidence/archive.sha256
dist/candidate-evidence/manifest.sha256
dist/candidate-evidence/provenance.bundle.json
if-no-files-found: error
retention-days: 30

deploy:
name: Install only on private Phase 2 dress rehearsal
needs: build
runs-on: [1helm-dress-rehearsal-phase2]
timeout-minutes: 20
permissions:
contents: read
actions: read
steps:
- name: Download this workflow's exact candidate
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: ${{ needs.build.outputs.artifact-name }}
path: candidate-download

- name: Submit fixed candidate inputs to the root-owned boundary
env:
EXPECTED_COMMIT: ${{ needs.build.outputs.commit }}
run: |
set -euo pipefail
test "$GITHUB_REPOSITORY" = "gitcommit90/1Helm"
test "$EXPECTED_COMMIT" = "${{ github.event.workflow_run.head_sha }}"
test "${{ github.event.workflow_run.head_branch }}" = "main"
test "${{ github.event.workflow_run.head_repository.full_name }}" = "$GITHUB_REPOSITORY"
test "${{ github.event.workflow_run.conclusion }}" = "success"
archive="$(find candidate-download -maxdepth 1 -type f -name '1Helm-*-linux-node.tgz' -print -quit)"
test -n "$archive"
install -m 0600 "$archive" /var/lib/1helm-candidate/inbox/candidate.tgz
install -m 0600 candidate-download/candidate-evidence/candidate.json /var/lib/1helm-candidate/inbox/candidate.json
install -m 0600 candidate-download/candidate-evidence/provenance.bundle.json /var/lib/1helm-candidate/inbox/provenance.bundle.json
sudo -n /usr/local/sbin/1helm-candidate-install

- name: Publish private installation evidence in the job log
if: always()
run: |
test -r /var/lib/1helm-candidate/evidence/status.json
python3 /usr/local/lib/1helm-candidate/candidate-boundary.py summary \
/var/lib/1helm-candidate/evidence/status.json
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,16 @@ data-refactored/
.deploy-backups/
.claude/
.design/

# generated local delivery/test state
# Host tooling may ignore agent instruction files globally; these are tracked.
!/AGENTS.md
!/CLAUDE.md
/.release-tmp/
/.native-test-data/
/.preview-data/
/.test-state/
/.delivery-status.local
__pycache__/
*.py[cod]
/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]
25 changes: 25 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Delivery contract for coding agents

The default delivery mode in this repository is **PREVIEW ONLY**. Implement and
verify a small, explicitly requested change, then report it for review.

Unless the owner explicitly requests a stable promotion, do not:

- bump versions or edit release notes for a release;
- create or publish tags, releases, or artifacts;
- deploy the public website or update stable or its release metadata;
- change production data, infrastructure, containers, VMs, or services; or
- broaden the requested scope.

Keep changes focused. Run the narrowest relevant tests while iterating, then run
the full CI contract (`npm run ci`) before merge. Never weaken a check to make a
change pass.

Every handoff must briefly name changed files, checks run and their results,
known risks, rollback steps, and whether stable or any external system was
touched.

Maintainer policy and release mechanics remain authoritative in
[`docs/GOVERNANCE.md`](docs/GOVERNANCE.md) and
[`docs/release-lifecycle.md`](docs/release-lifecycle.md). This file adds the
agent default; it does not replace those documents.
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Claude Code instructions

Follow [AGENTS.md](AGENTS.md) as the authoritative delivery contract for all
work in this repository.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,25 @@ npm start # http://127.0.0.1:8123
A fresh data directory opens first-run setup. The source runtime defaults to
`./data`; do not point development at an existing production data directory.

### Private development preview

Run `npm run preview`, then open **http://127.0.0.1:8124**. Stop it with
Ctrl+C. Client CSS/JavaScript and server source changes are watched; refresh
the private page manually after a change.

The command refuses Stable's port and normal app data paths. It always uses
separate generated test data under `.preview-data/`, so it never changes or
restarts Stable. If Stable uses a locally customized port, set
`HELM_STABLE_PORT` before launching the preview so that port is refused too.

### Private Linux dress rehearsal

After trusted `main` passes CI, the candidate workflow prepares one exact,
attested Linux build for a dedicated private guest and installs it through a
digest-verified, rollback-aware root boundary. It does not publish a release,
change Stable, or run PR code on the guest. Private coordinates stay in local
status configuration. See [Private Linux dress rehearsal](docs/dress-rehearsal.md).

### Core configuration

| Environment variable | Default | Meaning |
Expand Down
56 changes: 56 additions & 0 deletions docs/canary-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Delivery canary plan

## Phase 0 boundary

Phase 0 creates documentation and read-only visibility only. It does not
create or modify infrastructure, containers, VMs, services, deployment targets,
releases, stable metadata, or production data.

LXC 112 on `pve2` remains unchanged as the legacy **v0.0.38 updater fixture**.
It is evidence for the prior-version update path, not a general-purpose canary.
No experiment, candidate install, reset, or cleanup may repurpose it.

## Phase 2 private dress rehearsal

Phase 2 was separately approved. It creates one fresh, unprivileged LXC with its
own identity, storage, private network address, and lifecycle. It does not share
production data, credentials, release metadata, or the legacy fixture. The
guest ID, address, and hypervisor access stay in local operator configuration,
not this public repository.

After `CI` succeeds for a push to trusted `main`, `Candidate dress rehearsal`
checks out that exact CI SHA on a GitHub-hosted builder. It builds the sealed OCI
image and ready-to-run Linux archive without a version bump or GitHub Release,
embeds source/build identity, emits a digest manifest, signs GitHub artifact
provenance, and retains all evidence as a workflow artifact. Only then does its
deployment job select the uniquely labelled repository runner in the dedicated
guest.

The runner cannot install arbitrary bytes. A fixed root-owned command copies
the fixed inbox files, requires signed provenance from the trusted candidate
workflow on `main`, rejects self-hosted provenance, and requires the outer
manifest, embedded identity, archive digest, source SHA, version, and sealed OCI
digest to agree. It then reuses the immutable release store, service health
check, and automatic rollback in the Linux installer. The status record contains:

- canary role, hypervisor/guest identity, and health endpoint;
- current version and candidate version;
- exact source commit plus artifact name and SHA-256 digest;
- service and application health, check time, result, and any uncertainty;
- CI workflow/run result and candidate build identity;
- install health and time; and
- previous candidate plus rollback result/time.
Comment on lines +34 to +42

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ—„οΈ Data Integrity & Integration | 🟑 Minor | ⚑ Quick win

Align the status field list with the implemented schema.

status.json does not contain the canary role, hypervisor identity, guest identity, health endpoint, or artifact name.

Remove these fields from this list, or state that Phase 0 combines them from local configuration. Keep the candidate evidence fields separate from locally configured environment metadata.

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/canary-plan.md` around lines 34 - 42, Update the status field list in
the canary plan to match the implemented status.json schema: remove canary role,
hypervisor identity, guest identity, health endpoint, and artifact name, or
explicitly identify them as Phase 0 metadata combined from local configuration.
Keep candidate evidence fields separate from locally configured environment
metadata.


Unknown metadata must be reported as unknown, never inferred from a nearby
checkout, tag, or responding port.

## Rollback gate

The dedicated guest starts from a documented clean baseline. Each accepted
candidate becomes an immutable, digest-named release directory. Before changing
the current symlink or host contract, the existing Linux transaction snapshots
the prior current release, runtime files, units, and unit state. A failed or
uncertain service/API health check restores that exact prior contract and proves
the restored service healthy. The candidate evidence records both failure and
rollback outcome. This is application rollback inside the dedicated guest; no
Proxmox snapshot, Stable change, or LXC 112 action is part of the automation.
Loading
Loading