Skip to content

Commit 1e3ccf1

Browse files
gitcommit90claude
andauthored
build: split app and channel image delivery (#71)
Reduce normal update downloads by separating immutable OCI image bytes, retain complete offline bundles, and add measured cache-safe artifact budgets. 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 f164905 commit 1e3ccf1

45 files changed

Lines changed: 1874 additions & 425 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/candidate.yml‎

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ jobs:
3535
commit: ${{ steps.identity.outputs.commit }}
3636
version: ${{ steps.identity.outputs.version }}
3737
ci-run-id: ${{ steps.identity.outputs.ci_run_id }}
38+
image-digest: ${{ steps.identity.outputs.image_digest }}
3839
steps:
3940
- name: Check out the exact successful CI commit
4041
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
@@ -76,6 +77,39 @@ jobs:
7677
sudo apt-get update
7778
sudo apt-get install -y podman
7879
80+
- name: Resolve exact OCI and production dependency cache identities
81+
id: packaging-cache
82+
run: |
83+
set -euo pipefail
84+
builder_image=docker.io/library/node:22
85+
podman pull "$builder_image"
86+
builder_digest="$(podman image inspect "$builder_image" --format '{{.Digest}}' | sed 's/^sha256://')"
87+
node_abi="$(podman run --rm "$builder_image" node -p process.versions.modules)"
88+
case "$(uname -m)" in x86_64|amd64) image_arch=amd64; native_arch=x64 ;; aarch64|arm64) image_arch=arm64; native_arch=arm64 ;; *) exit 1 ;; esac
89+
base_digest="$(sed -n 's/^FROM .*@sha256:\([a-f0-9]\{64\}\)$/\1/p' container/Containerfile.oci)"
90+
containerfile_sha="$(sha256sum container/Containerfile.oci | awk '{print $1}')"
91+
context_sha="$(git ls-files -z container | while IFS= read -r -d '' file; do
92+
case "$file" in container/channel-machine.oci.tar|container/channel-machine.oci.sha256|container/channel-machine.oci.json) continue ;; esac
93+
printf '%s\0' "$file"
94+
sha256sum "$file" | awk '{printf "%s\0", $1}'
95+
done | sha256sum | awk '{print $1}')"
96+
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}')"
97+
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}')"
98+
[[ "$builder_digest" =~ ^[a-f0-9]{64}$ && "$node_abi" =~ ^[0-9]+$ && "$oci_key" =~ ^[a-f0-9]{64}$ && "$dependency_key" =~ ^[a-f0-9]{64}$ ]]
99+
printf 'oci_key=%s\ndependency_key=%s\n' "$oci_key" "$dependency_key" >> "$GITHUB_OUTPUT"
100+
101+
- name: Restore only the exact sealed OCI cache
102+
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
103+
with:
104+
path: dist/cache/channel-images
105+
key: 1helm-phase5-channel-image-${{ steps.packaging-cache.outputs.oci_key }}
106+
107+
- name: Restore only the exact production dependency cache
108+
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
109+
with:
110+
path: dist/cache/production-dependencies
111+
key: 1helm-phase5-production-dependencies-${{ steps.packaging-cache.outputs.dependency_key }}
112+
79113
- name: Build sealed OCI image and ready-to-run Linux archive
80114
env:
81115
HELM_CANDIDATE_REPOSITORY: gitcommit90/1Helm
@@ -91,6 +125,17 @@ jobs:
91125
set -euo pipefail
92126
npm run package:channel-image
93127
npm run package:linux
128+
image_name="$(node -p 'require("./container/channel-machine.oci.json").artifact.name')"
129+
cp container/channel-machine.oci.tar "dist/$image_name"
130+
cp container/channel-machine.oci.json "dist/${image_name%.oci.tar}.json"
131+
132+
- name: Measure split artifact composition and enforce regression budgets
133+
run: |
134+
set -euo pipefail
135+
node scripts/artifact-size-report.mjs \
136+
--json dist/artifact-size-report.json \
137+
--text dist/artifact-size-report.txt \
138+
--check
94139
95140
- name: Generate candidate manifest and evidence
96141
id: identity
@@ -101,22 +146,31 @@ jobs:
101146
set -euo pipefail
102147
version="$(node -p 'require("./package.json").version')"
103148
archive="dist/1Helm-${version}-linux-node.tgz"
149+
offline="dist/1Helm-${version}-linux-node-offline.tgz"
150+
split="dist/1Helm-${version}-linux-split.json"
104151
evidence="dist/candidate-evidence"
105152
mkdir -p "$evidence"
106-
HELM_CANDIDATE_ARCHIVE="$archive" HELM_CANDIDATE_MANIFEST="$evidence/candidate.json" \
153+
HELM_CANDIDATE_ARCHIVE="$archive" \
154+
HELM_CANDIDATE_OFFLINE_ARCHIVE="$offline" \
155+
HELM_CANDIDATE_SPLIT_MANIFEST="$split" \
156+
HELM_CANDIDATE_MANIFEST="$evidence/candidate.json" \
107157
node scripts/candidate-manifest.mjs
108158
cp "$archive.sha256" "$evidence/archive.sha256"
109159
sha256sum "$evidence/candidate.json" > "$evidence/manifest.sha256"
110160
printf 'artifact_name=1helm-candidate-%s\n' "$CI_HEAD_SHA" >> "$GITHUB_OUTPUT"
111161
printf 'commit=%s\n' "$CI_HEAD_SHA" >> "$GITHUB_OUTPUT"
112162
printf 'version=%s\n' "$version" >> "$GITHUB_OUTPUT"
113163
printf 'ci_run_id=%s\n' "$HELM_CANDIDATE_CI_RUN_ID" >> "$GITHUB_OUTPUT"
164+
printf 'image_digest=%s\n' "$(node -p 'require("./container/channel-machine.oci.json").sha256')" >> "$GITHUB_OUTPUT"
114165
115166
- name: Attest archive provenance on the hosted builder
116167
id: attest
117168
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3
118169
with:
119-
subject-path: dist/1Helm-*-linux-node.tgz
170+
subject-path: |
171+
dist/1Helm-*-linux-node.tgz
172+
dist/1Helm-*-linux-node-offline.tgz
173+
container/channel-machine.oci.tar
120174
121175
- name: Retain signed provenance bundle
122176
env:
@@ -132,13 +186,29 @@ jobs:
132186
name: ${{ steps.identity.outputs.artifact_name }}
133187
path: |
134188
dist/1Helm-*-linux-node.tgz
189+
dist/1Helm-*-linux-node-offline.tgz
190+
dist/1Helm-*-linux-split.json
191+
dist/artifact-size-report.json
192+
dist/artifact-size-report.txt
193+
container/channel-machine.oci.json
135194
dist/candidate-evidence/candidate.json
136195
dist/candidate-evidence/archive.sha256
137196
dist/candidate-evidence/manifest.sha256
138197
dist/candidate-evidence/provenance.bundle.json
139198
if-no-files-found: error
140199
retention-days: 30
141200

201+
- name: Retain immutable digest-addressed channel image candidate
202+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
203+
with:
204+
name: 1helm-channel-image-${{ steps.identity.outputs.image_digest }}
205+
path: |
206+
dist/1Helm-channel-machine-v1-*.oci.tar
207+
dist/1Helm-channel-machine-v1-*.json
208+
container/channel-machine.oci.sha256
209+
if-no-files-found: error
210+
retention-days: 90
211+
142212
build-macos:
143213
name: Build signed notarized exact Mac candidate
144214
if: >-
@@ -259,6 +329,9 @@ jobs:
259329
archive="$(find candidate-download -maxdepth 1 -type f -name '1Helm-*-linux-node.tgz' -print -quit)"
260330
test -n "$archive"
261331
install -m 0600 "$archive" /var/lib/1helm-candidate/inbox/candidate.tgz
332+
offline="$(find candidate-download -maxdepth 1 -type f -name '1Helm-*-linux-node-offline.tgz' -print -quit)"
333+
test -n "$offline"
334+
install -m 0600 "$offline" /var/lib/1helm-candidate/inbox/candidate-offline.tgz
262335
install -m 0600 candidate-download/candidate-evidence/candidate.json /var/lib/1helm-candidate/inbox/candidate.json
263336
install -m 0600 candidate-download/candidate-evidence/provenance.bundle.json /var/lib/1helm-candidate/inbox/provenance.bundle.json
264337
sudo -n /usr/local/sbin/1helm-candidate-install
@@ -318,7 +391,9 @@ jobs:
318391
test "${{ github.event.workflow_run.event }}" = push
319392
test "${{ github.event.workflow_run.head_repository.full_name }}" = "$GITHUB_REPOSITORY"
320393
export HELM_CANDIDATE_ARCHIVE="$(find candidate-download -maxdepth 1 -type f -name '1Helm-*-linux-node.tgz' -print -quit)"
394+
export HELM_CANDIDATE_OFFLINE_ARCHIVE="$(find candidate-download -maxdepth 1 -type f -name '1Helm-*-linux-node-offline.tgz' -print -quit)"
321395
test -n "$HELM_CANDIDATE_ARCHIVE"
396+
test -n "$HELM_CANDIDATE_OFFLINE_ARCHIVE"
322397
bash ops/platform-acceptance/linux.sh
323398
324399
- name: Upload exact Linux acceptance evidence
@@ -431,6 +506,9 @@ jobs:
431506
$archive = Get-ChildItem candidate-download -Filter '1Helm-*-linux-node.tgz' | Select-Object -First 1 -ExpandProperty FullName
432507
if (-not $archive) { throw 'Exact Linux candidate archive is missing.' }
433508
$env:HELM_CANDIDATE_ARCHIVE = $archive
509+
$offline = Get-ChildItem candidate-download -Filter '1Helm-*-linux-node-offline.tgz' | Select-Object -First 1 -ExpandProperty FullName
510+
if (-not $offline) { throw 'Exact Linux offline candidate archive is missing.' }
511+
$env:HELM_CANDIDATE_OFFLINE_ARCHIVE = $offline
434512
& .\ops\platform-acceptance\windows.ps1
435513
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
436514
@@ -463,6 +541,8 @@ jobs:
463541

464542
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
465543
with: { name: "${{ needs.build.outputs.artifact-name }}", path: candidate-download }
544+
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
545+
with: { name: "1helm-channel-image-${{ needs.build.outputs.image-digest }}", path: channel-image-download }
466546
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
467547
with: { name: "${{ needs.build-macos.outputs.artifact-name }}", path: mac-candidate-download }
468548
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
@@ -477,6 +557,7 @@ jobs:
477557
- name: Assemble only the retained complete matrix and evidence
478558
env:
479559
HELM_CANDIDATE_DOWNLOAD: candidate-download
560+
HELM_CHANNEL_IMAGE_DOWNLOAD: channel-image-download
480561
HELM_MAC_CANDIDATE_DOWNLOAD: mac-candidate-download
481562
HELM_REHEARSAL_EVIDENCE: rehearsal-download/dress-rehearsal.json
482563
HELM_LINUX_ACCEPTANCE_EVIDENCE: linux-acceptance-download/linux-acceptance.json

‎.github/workflows/promote-stable.yml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,10 @@ jobs:
166166
promotion-bundle/1Helm-${{ inputs.version }}-arm64.dmg
167167
promotion-bundle/1Helm-${{ inputs.version }}-mac-arm64.zip
168168
promotion-bundle/1Helm-${{ inputs.version }}-linux-node.tgz
169+
promotion-bundle/1Helm-${{ inputs.version }}-linux-node-offline.tgz
170+
promotion-bundle/1Helm-channel-machine-v1-*.oci.tar
171+
promotion-bundle/channel-image.json
172+
promotion-bundle/channel-image-provenance.json
169173
promotion-bundle/1Helm-${{ inputs.version }}-stable.json
170174
promotion-bundle/1Helm-${{ inputs.version }}-release-notes.md
171175
promotion-bundle/verified-promotion.json

‎config/artifact-budgets.json‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"schema": 1,
3+
"kind": "1helm-artifact-size-budgets",
4+
"units": "bytes",
5+
"baselines": {
6+
"legacy_linux_complete_tgz": 401045903,
7+
"sealed_oci_image": 203846656,
8+
"legacy_linux_unpacked_node_modules": 447486065
9+
},
10+
"budgets": {
11+
"linux_app_tgz": 220000000,
12+
"linux_offline_tgz": 390000000,
13+
"sealed_oci_image": 220000000,
14+
"linux_unpacked_node_modules": 320000000,
15+
"linux_client_assets": 32000000,
16+
"duplicate_bytes": 50000000
17+
},
18+
"notes": [
19+
"The v0.0.41 complete Linux archive and sealed OCI figures were measured from retained local release outputs.",
20+
"Mac artifacts are intentionally unbaselined until their exact signed bytes are present on a Mac builder.",
21+
"Budgets are regression ceilings, not targets, and do not authorize removal of runtime files."
22+
]
23+
}

‎config/linux-runtime-package.json‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"schema": 1,
3+
"kind": "1helm-linux-runtime-package-allowlist",
4+
"description": "Files needed by the ready-to-run Linux host. Generated assets and production dependencies are added by the packager.",
5+
"source": [
6+
"LICENSE",
7+
"NOTICE",
8+
"package.json",
9+
"package-lock.json",
10+
"src/server",
11+
"scripts/1helm-oci-runtime",
12+
"scripts/mnemosyne-bridge.py",
13+
"scripts/ensure-node-pty-helper.cjs",
14+
"site/public/apply-linux-release.sh",
15+
"site/public/install-linux-units.sh",
16+
"site/public/install-oci-runtime.sh",
17+
"site/public/install.sh",
18+
"site/public/uninstall-host.sh",
19+
"site/public/update-host.sh",
20+
"deploy/1helm-oci-runtime-v1.conf",
21+
"container/Containerfile.oci"
22+
],
23+
"built": [
24+
"public",
25+
"desktop/photon-sidecar.bundle.mjs"
26+
],
27+
"production_dependency_excludes": {
28+
"directory_names": [".cache", ".github", "__tests__", "doc", "docs", "example", "examples", "test", "tests"],
29+
"file_suffixes": [".d.ts", ".map"],
30+
"file_names": ["CHANGELOG", "CHANGELOG.md", "README", "README.md"]
31+
}
32+
}

‎docs/GOVERNANCE.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,10 @@ contract as the slice hardens.
7474
- Semantic versioning on `package.json`.
7575
- **Do not** reuse a published version tag for different bits.
7676
- A desktop release requires one unique version and exact commit, changelog, the
77-
complete three-artifact matrix (`1Helm-<version>-arm64.dmg`,
78-
`1Helm-<version>-mac-arm64.zip`, `1Helm-<version>-linux-node.tgz`), and
77+
complete **four-artifact** split desktop matrix (`1Helm-<version>-arm64.dmg`,
78+
`1Helm-<version>-mac-arm64.zip`, online `1Helm-<version>-linux-node.tgz`,
79+
complete `1Helm-<version>-linux-node-offline.tgz`), plus the exact immutable
80+
digest-addressed channel-image manifest, and
7981
clean-install plus prior-to-new update evidence on macOS, Linux, and Windows.
8082
Windows publishes no artifact; its installer is served by the site, not
8183
attached to the release. Partial platform releases under the shared product

‎docs/USER_GUIDE.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,12 @@ that one fixed operation, but cannot choose an arbitrary URL, command, or target
384384
path. The host updater requires a stable GitHub release and its SHA-256 asset
385385
digest, installs into a versioned directory, switches the current symlink
386386
atomically, restarts, health-checks, and restores the prior release if needed.
387+
The normal online archive omits the large sealed channel image: its exact
388+
digest/architecture/version manifest lets the host reuse already verified bytes
389+
from the shared image store, or fetch and verify them once. For disconnected
390+
recovery, use the complete `linux-node-offline.tgz` bundle; legacy v0.0.41-style
391+
complete archives also remain accepted. Image cleanup is report-only, so
392+
rollback-referenced bytes are retained.
387393
Source/developer deployments report that their host operator owns updates.
388394

389395
Every host update preserves:
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Artifact size and split delivery
2+
3+
Phase 5 separates the large Linux/Windows channel-computer image from ordinary
4+
application releases without relaxing any byte-identity or runtime gate.
5+
6+
## What users download
7+
8+
The normal Linux and Windows/WSL install path downloads
9+
`1Helm-<version>-linux-node.tgz`. That archive is ready to run: it contains the
10+
server, built browser assets, production dependencies, native add-ons, lifecycle
11+
scripts, and an exact channel-image manifest. It does **not** contain the OCI
12+
archive itself.
13+
14+
The installer then resolves the manifest's immutable URL, checks its contract
15+
version and host architecture, downloads the image only when that SHA-256 is not
16+
already retained, verifies byte count and SHA-256, and stores it below
17+
`/var/lib/1helm-oci-v1/shared-images/sha256/<digest>`. A normal application-only
18+
update that references the same digest reuses those bytes. Every retained prior
19+
application release continues to reference its image digest, so rollback does
20+
not depend on a new download.
21+
22+
For a disconnected machine, use
23+
`1Helm-<version>-linux-node-offline.tgz`. It contains the exact same application
24+
tree and the exact image bytes named by its embedded manifest. Copy that one
25+
archive to the machine and pass it to `install.sh`; no channel-image network
26+
fetch is required. Existing v0.0.41-style complete archives remain supported by
27+
the explicit legacy branch.
28+
29+
If the image manifest is absent, malformed, for another architecture, or does
30+
not match the downloaded/embedded bytes, installation stops before the runtime
31+
contract changes. Recovery is to retry online, provide the complete offline
32+
bundle, or reinstall the prior verified complete release. No fallback image is
33+
invented.
34+
35+
## Measured local result
36+
37+
The deterministic v0.0.41 complete Linux artifact baseline is 401,045,903 bytes.
38+
Its embedded sealed OCI archive is 203,846,656 bytes. The Phase 5 local build
39+
produced:
40+
41+
- online Linux application: 149,436,110 bytes;
42+
- complete offline bundle: 350,656,134 bytes;
43+
- shared sealed OCI archive: 203,846,656 bytes;
44+
- packaged production dependencies: 280,851,053 unpacked bytes;
45+
- packaged client assets: 25,497,670 unpacked bytes.
46+
47+
A cold online installation downloads 353,282,766 bytes across the application
48+
and shared image, 47,763,137 bytes (11.91%) less than the v0.0.41 complete
49+
archive. The application artifact itself is 251,609,793 bytes smaller (62.74%);
50+
an application-only update with the unchanged image downloads only that
51+
149,436,110-byte artifact and avoids transferring the 203,846,656-byte image
52+
again. The offline bundle remains complete and is 50,389,769 bytes smaller
53+
(12.56%) than the old archive due to the runtime allowlist and
54+
production-dependency slimming.
55+
56+
Run `npm run artifacts:report` to regenerate the machine-readable JSON and
57+
concise text report. Inputs may be overridden with `--linux-app`,
58+
`--linux-offline`, `--oci`, `--mac-dmg`, `--mac-zip`, `--vendored`, and
59+
`--client`. Missing Mac or Linux outputs are recorded as `missing`, not treated
60+
as zero-byte artifacts. General deterministic baselines and regression ceilings
61+
live in `config/artifact-budgets.json`; reports never record hostnames, machine
62+
identities, or private filesystem paths.
63+
64+
## Packaging and cache auditability
65+
66+
`config/linux-runtime-package.json` is the source allowlist for the Linux
67+
runtime. `npm ci --omit=dev` remains the production dependency authority. The
68+
packager removes only named documentation, test/example/cache directories,
69+
TypeScript declarations, and source maps from the staged production dependency
70+
tree, then requires and fingerprints every native add-on. It does not change the
71+
lockfile or dependencies and never builds on the customer host.
72+
73+
Production dependency cache identity covers the exact lockfile SHA-256, runtime
74+
packaging-manifest SHA-256, Node ABI, Linux architecture, and native builder
75+
image digest. Channel-image cache identity covers architecture, the pinned base
76+
image digest, Containerfile SHA-256, and complete tracked container-context
77+
SHA-256. Candidate manifests state whether each exact cache was reused; the
78+
canonical digest-addressed image manifest stays identical across reused
79+
candidates. Hosted candidate builds use these full keys with no prefix
80+
fallback. A mismatched key or cache manifest is not reusable.
81+
82+
`node scripts/channel-image-gc-report.mjs` reports referenced and unreferenced
83+
digest stores. Phase 5 always emits `action: "retain"` and has no deletion path;
84+
garbage collection is deliberately report-only.

0 commit comments

Comments
 (0)