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
85 changes: 83 additions & 2 deletions .github/workflows/candidate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ jobs:
commit: ${{ steps.identity.outputs.commit }}
version: ${{ steps.identity.outputs.version }}
ci-run-id: ${{ steps.identity.outputs.ci_run_id }}
image-digest: ${{ steps.identity.outputs.image_digest }}
steps:
- name: Check out the exact successful CI commit
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
Expand Down Expand Up @@ -76,6 +77,39 @@ jobs:
sudo apt-get update
sudo apt-get install -y podman

- name: Resolve exact OCI and production dependency cache identities
id: packaging-cache
run: |
set -euo pipefail
builder_image=docker.io/library/node:22
podman pull "$builder_image"
builder_digest="$(podman image inspect "$builder_image" --format '{{.Digest}}' | sed 's/^sha256://')"
node_abi="$(podman run --rm "$builder_image" node -p process.versions.modules)"
case "$(uname -m)" in x86_64|amd64) image_arch=amd64; native_arch=x64 ;; aarch64|arm64) image_arch=arm64; native_arch=arm64 ;; *) exit 1 ;; esac
base_digest="$(sed -n 's/^FROM .*@sha256:\([a-f0-9]\{64\}\)$/\1/p' container/Containerfile.oci)"
containerfile_sha="$(sha256sum container/Containerfile.oci | awk '{print $1}')"
context_sha="$(git ls-files -z container | while IFS= read -r -d '' file; do
case "$file" in container/channel-machine.oci.tar|container/channel-machine.oci.sha256|container/channel-machine.oci.json) continue ;; esac
printf '%s\0' "$file"
sha256sum "$file" | awk '{printf "%s\0", $1}'
done | sha256sum | awk '{print $1}')"
oci_key="$(printf '1helm-channel-image-v1\n%s\n%s\n%s\n%s\n' "$image_arch" "$base_digest" "$containerfile_sha" "$context_sha" | sha256sum | awk '{print $1}')"
dependency_key="$(printf '%s\n%s\n%s\n%s\n%s' "$(sha256sum package-lock.json | awk '{print $1}')" "$(sha256sum config/linux-runtime-package.json | awk '{print $1}')" "$node_abi" "$native_arch" "$builder_digest" | sha256sum | awk '{print $1}')"
[[ "$builder_digest" =~ ^[a-f0-9]{64}$ && "$node_abi" =~ ^[0-9]+$ && "$oci_key" =~ ^[a-f0-9]{64}$ && "$dependency_key" =~ ^[a-f0-9]{64}$ ]]
printf 'oci_key=%s\ndependency_key=%s\n' "$oci_key" "$dependency_key" >> "$GITHUB_OUTPUT"

- name: Restore only the exact sealed OCI cache
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: dist/cache/channel-images
key: 1helm-phase5-channel-image-${{ steps.packaging-cache.outputs.oci_key }}

- name: Restore only the exact production dependency cache
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: dist/cache/production-dependencies
key: 1helm-phase5-production-dependencies-${{ steps.packaging-cache.outputs.dependency_key }}

- name: Build sealed OCI image and ready-to-run Linux archive
env:
HELM_CANDIDATE_REPOSITORY: gitcommit90/1Helm
Expand All @@ -91,6 +125,17 @@ jobs:
set -euo pipefail
npm run package:channel-image
npm run package:linux
image_name="$(node -p 'require("./container/channel-machine.oci.json").artifact.name')"
cp container/channel-machine.oci.tar "dist/$image_name"
cp container/channel-machine.oci.json "dist/${image_name%.oci.tar}.json"

- name: Measure split artifact composition and enforce regression budgets
run: |
set -euo pipefail
node scripts/artifact-size-report.mjs \
--json dist/artifact-size-report.json \
--text dist/artifact-size-report.txt \
--check

- name: Generate candidate manifest and evidence
id: identity
Expand All @@ -101,22 +146,31 @@ jobs:
set -euo pipefail
version="$(node -p 'require("./package.json").version')"
archive="dist/1Helm-${version}-linux-node.tgz"
offline="dist/1Helm-${version}-linux-node-offline.tgz"
split="dist/1Helm-${version}-linux-split.json"
evidence="dist/candidate-evidence"
mkdir -p "$evidence"
HELM_CANDIDATE_ARCHIVE="$archive" HELM_CANDIDATE_MANIFEST="$evidence/candidate.json" \
HELM_CANDIDATE_ARCHIVE="$archive" \
HELM_CANDIDATE_OFFLINE_ARCHIVE="$offline" \
HELM_CANDIDATE_SPLIT_MANIFEST="$split" \
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"
printf 'version=%s\n' "$version" >> "$GITHUB_OUTPUT"
printf 'ci_run_id=%s\n' "$HELM_CANDIDATE_CI_RUN_ID" >> "$GITHUB_OUTPUT"
printf 'image_digest=%s\n' "$(node -p 'require("./container/channel-machine.oci.json").sha256')" >> "$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
subject-path: |
dist/1Helm-*-linux-node.tgz
dist/1Helm-*-linux-node-offline.tgz
container/channel-machine.oci.tar

- name: Retain signed provenance bundle
env:
Expand All @@ -132,13 +186,29 @@ jobs:
name: ${{ steps.identity.outputs.artifact_name }}
path: |
dist/1Helm-*-linux-node.tgz
dist/1Helm-*-linux-node-offline.tgz
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
dist/candidate-evidence/provenance.bundle.json
if-no-files-found: error
retention-days: 30

- name: Retain immutable digest-addressed channel image candidate
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: 1helm-channel-image-${{ steps.identity.outputs.image_digest }}
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

build-macos:
name: Build signed notarized exact Mac candidate
if: >-
Expand Down Expand Up @@ -259,6 +329,9 @@ jobs:
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
offline="$(find candidate-download -maxdepth 1 -type f -name '1Helm-*-linux-node-offline.tgz' -print -quit)"
test -n "$offline"
install -m 0600 "$offline" /var/lib/1helm-candidate/inbox/candidate-offline.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
Expand Down Expand Up @@ -318,7 +391,9 @@ jobs:
test "${{ github.event.workflow_run.event }}" = push
test "${{ github.event.workflow_run.head_repository.full_name }}" = "$GITHUB_REPOSITORY"
export HELM_CANDIDATE_ARCHIVE="$(find candidate-download -maxdepth 1 -type f -name '1Helm-*-linux-node.tgz' -print -quit)"
export HELM_CANDIDATE_OFFLINE_ARCHIVE="$(find candidate-download -maxdepth 1 -type f -name '1Helm-*-linux-node-offline.tgz' -print -quit)"
test -n "$HELM_CANDIDATE_ARCHIVE"
test -n "$HELM_CANDIDATE_OFFLINE_ARCHIVE"
bash ops/platform-acceptance/linux.sh

- name: Upload exact Linux acceptance evidence
Expand Down Expand Up @@ -431,6 +506,9 @@ jobs:
$archive = Get-ChildItem candidate-download -Filter '1Helm-*-linux-node.tgz' | Select-Object -First 1 -ExpandProperty FullName
if (-not $archive) { throw 'Exact Linux candidate archive is missing.' }
$env:HELM_CANDIDATE_ARCHIVE = $archive
$offline = Get-ChildItem candidate-download -Filter '1Helm-*-linux-node-offline.tgz' | Select-Object -First 1 -ExpandProperty FullName
if (-not $offline) { throw 'Exact Linux offline candidate archive is missing.' }
$env:HELM_CANDIDATE_OFFLINE_ARCHIVE = $offline
& .\ops\platform-acceptance\windows.ps1
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

Expand Down Expand Up @@ -463,6 +541,8 @@ jobs:

- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with: { name: "${{ needs.build.outputs.artifact-name }}", path: candidate-download }
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with: { name: "1helm-channel-image-${{ needs.build.outputs.image-digest }}", path: channel-image-download }
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with: { name: "${{ needs.build-macos.outputs.artifact-name }}", path: mac-candidate-download }
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
Expand All @@ -477,6 +557,7 @@ jobs:
- name: Assemble only the retained complete matrix and evidence
env:
HELM_CANDIDATE_DOWNLOAD: candidate-download
HELM_CHANNEL_IMAGE_DOWNLOAD: channel-image-download
HELM_MAC_CANDIDATE_DOWNLOAD: mac-candidate-download
HELM_REHEARSAL_EVIDENCE: rehearsal-download/dress-rehearsal.json
HELM_LINUX_ACCEPTANCE_EVIDENCE: linux-acceptance-download/linux-acceptance.json
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/promote-stable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,10 @@ jobs:
promotion-bundle/1Helm-${{ inputs.version }}-arm64.dmg
promotion-bundle/1Helm-${{ inputs.version }}-mac-arm64.zip
promotion-bundle/1Helm-${{ inputs.version }}-linux-node.tgz
promotion-bundle/1Helm-${{ inputs.version }}-linux-node-offline.tgz
promotion-bundle/1Helm-channel-machine-v1-*.oci.tar
promotion-bundle/channel-image.json
promotion-bundle/channel-image-provenance.json
promotion-bundle/1Helm-${{ inputs.version }}-stable.json
promotion-bundle/1Helm-${{ inputs.version }}-release-notes.md
promotion-bundle/verified-promotion.json
Expand Down
23 changes: 23 additions & 0 deletions config/artifact-budgets.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"schema": 1,
"kind": "1helm-artifact-size-budgets",
"units": "bytes",
"baselines": {
"legacy_linux_complete_tgz": 401045903,
"sealed_oci_image": 203846656,
"legacy_linux_unpacked_node_modules": 447486065
},
"budgets": {
"linux_app_tgz": 220000000,
"linux_offline_tgz": 390000000,
"sealed_oci_image": 220000000,
"linux_unpacked_node_modules": 320000000,
"linux_client_assets": 32000000,
"duplicate_bytes": 50000000
},
"notes": [
"The v0.0.41 complete Linux archive and sealed OCI figures were measured from retained local release outputs.",
"Mac artifacts are intentionally unbaselined until their exact signed bytes are present on a Mac builder.",
"Budgets are regression ceilings, not targets, and do not authorize removal of runtime files."
]
}
32 changes: 32 additions & 0 deletions config/linux-runtime-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"schema": 1,
"kind": "1helm-linux-runtime-package-allowlist",
"description": "Files needed by the ready-to-run Linux host. Generated assets and production dependencies are added by the packager.",
"source": [
"LICENSE",
"NOTICE",
"package.json",
"package-lock.json",
"src/server",
"scripts/1helm-oci-runtime",
"scripts/mnemosyne-bridge.py",
"scripts/ensure-node-pty-helper.cjs",
"site/public/apply-linux-release.sh",
"site/public/install-linux-units.sh",
"site/public/install-oci-runtime.sh",
"site/public/install.sh",
"site/public/uninstall-host.sh",
"site/public/update-host.sh",
"deploy/1helm-oci-runtime-v1.conf",
"container/Containerfile.oci"
],
"built": [
"public",
"desktop/photon-sidecar.bundle.mjs"
],
"production_dependency_excludes": {
"directory_names": [".cache", ".github", "__tests__", "doc", "docs", "example", "examples", "test", "tests"],
"file_suffixes": [".d.ts", ".map"],
"file_names": ["CHANGELOG", "CHANGELOG.md", "README", "README.md"]
}
}
6 changes: 4 additions & 2 deletions docs/GOVERNANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,10 @@ contract as the slice hardens.
- Semantic versioning on `package.json`.
- **Do not** reuse a published version tag for different bits.
- A desktop release requires one unique version and exact commit, changelog, the
complete three-artifact matrix (`1Helm-<version>-arm64.dmg`,
`1Helm-<version>-mac-arm64.zip`, `1Helm-<version>-linux-node.tgz`), and
complete **four-artifact** split desktop matrix (`1Helm-<version>-arm64.dmg`,
`1Helm-<version>-mac-arm64.zip`, online `1Helm-<version>-linux-node.tgz`,
complete `1Helm-<version>-linux-node-offline.tgz`), plus the exact immutable
digest-addressed channel-image manifest, and
clean-install plus prior-to-new update evidence on macOS, Linux, and Windows.
Windows publishes no artifact; its installer is served by the site, not
attached to the release. Partial platform releases under the shared product
Expand Down
6 changes: 6 additions & 0 deletions docs/USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,12 @@ that one fixed operation, but cannot choose an arbitrary URL, command, or target
path. The host updater requires a stable GitHub release and its SHA-256 asset
digest, installs into a versioned directory, switches the current symlink
atomically, restarts, health-checks, and restores the prior release if needed.
The normal online archive omits the large sealed channel image: its exact
digest/architecture/version manifest lets the host reuse already verified bytes
from the shared image store, or fetch and verify them once. For disconnected
recovery, use the complete `linux-node-offline.tgz` bundle; legacy v0.0.41-style
complete archives also remain accepted. Image cleanup is report-only, so
rollback-referenced bytes are retained.
Source/developer deployments report that their host operator owns updates.

Every host update preserves:
Expand Down
84 changes: 84 additions & 0 deletions docs/artifact-size-and-split-delivery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Artifact size and split delivery

Phase 5 separates the large Linux/Windows channel-computer image from ordinary
application releases without relaxing any byte-identity or runtime gate.

## What users download

The normal Linux and Windows/WSL install path downloads
`1Helm-<version>-linux-node.tgz`. That archive is ready to run: it contains the
server, built browser assets, production dependencies, native add-ons, lifecycle
scripts, and an exact channel-image manifest. It does **not** contain the OCI
archive itself.

The installer then resolves the manifest's immutable URL, checks its contract
version and host architecture, downloads the image only when that SHA-256 is not
already retained, verifies byte count and SHA-256, and stores it below
`/var/lib/1helm-oci-v1/shared-images/sha256/<digest>`. A normal application-only
update that references the same digest reuses those bytes. Every retained prior
application release continues to reference its image digest, so rollback does
not depend on a new download.

For a disconnected machine, use
`1Helm-<version>-linux-node-offline.tgz`. It contains the exact same application
tree and the exact image bytes named by its embedded manifest. Copy that one
archive to the machine and pass it to `install.sh`; no channel-image network
fetch is required. Existing v0.0.41-style complete archives remain supported by
the explicit legacy branch.

If the image manifest is absent, malformed, for another architecture, or does
not match the downloaded/embedded bytes, installation stops before the runtime
contract changes. Recovery is to retry online, provide the complete offline
bundle, or reinstall the prior verified complete release. No fallback image is
invented.

## Measured local result

The deterministic v0.0.41 complete Linux artifact baseline is 401,045,903 bytes.
Its embedded sealed OCI archive is 203,846,656 bytes. The Phase 5 local build
produced:

- online Linux application: 149,436,110 bytes;
- complete offline bundle: 350,656,134 bytes;
- shared sealed OCI archive: 203,846,656 bytes;
- packaged production dependencies: 280,851,053 unpacked bytes;
- packaged client assets: 25,497,670 unpacked bytes.

A cold online installation downloads 353,282,766 bytes across the application
and shared image, 47,763,137 bytes (11.91%) less than the v0.0.41 complete
archive. The application artifact itself is 251,609,793 bytes smaller (62.74%);
an application-only update with the unchanged image downloads only that
149,436,110-byte artifact and avoids transferring the 203,846,656-byte image
again. The offline bundle remains complete and is 50,389,769 bytes smaller
(12.56%) than the old archive due to the runtime allowlist and
production-dependency slimming.

Run `npm run artifacts:report` to regenerate the machine-readable JSON and
concise text report. Inputs may be overridden with `--linux-app`,
`--linux-offline`, `--oci`, `--mac-dmg`, `--mac-zip`, `--vendored`, and
`--client`. Missing Mac or Linux outputs are recorded as `missing`, not treated
as zero-byte artifacts. General deterministic baselines and regression ceilings
live in `config/artifact-budgets.json`; reports never record hostnames, machine
identities, or private filesystem paths.

## Packaging and cache auditability

`config/linux-runtime-package.json` is the source allowlist for the Linux
runtime. `npm ci --omit=dev` remains the production dependency authority. The
packager removes only named documentation, test/example/cache directories,
TypeScript declarations, and source maps from the staged production dependency
tree, then requires and fingerprints every native add-on. It does not change the
lockfile or dependencies and never builds on the customer host.

Production dependency cache identity covers the exact lockfile SHA-256, runtime
packaging-manifest SHA-256, Node ABI, Linux architecture, and native builder
image digest. Channel-image cache identity covers architecture, the pinned base
image digest, Containerfile SHA-256, and complete tracked container-context
SHA-256. Candidate manifests state whether each exact cache was reused; the
canonical digest-addressed image manifest stays identical across reused
candidates. Hosted candidate builds use these full keys with no prefix
fallback. A mismatched key or cache manifest is not reusable.

`node scripts/channel-image-gc-report.mjs` reports referenced and unreferenced
digest stores. Phase 5 always emits `action: "retain"` and has no deletion path;
garbage collection is deliberately report-only.
Loading
Loading