Skip to content

Commit e48c3d8

Browse files
committed
fix(publication): isolate offline release builds
1 parent 907b820 commit e48c3d8

9 files changed

Lines changed: 2439 additions & 116 deletions

.github/workflows/ci.yml

Lines changed: 40 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ jobs:
226226
copy: [a, b]
227227
env:
228228
PYLON_RELEASE_NODE: 22.23.2
229+
PYLON_RELEASE_MODE: ci
229230
PYLON_RELEASE_NPM: 11.10.1
230231
steps:
231232
- name: Checkout exact source
@@ -240,36 +241,29 @@ jobs:
240241
with:
241242
node-version: ${{ env.PYLON_RELEASE_NODE }}
242243

243-
- name: Install pinned build inputs
244+
- name: Run isolated release sandbox
245+
id: sandbox
246+
shell: /usr/bin/bash --noprofile --norc -euo pipefail {0}
244247
run: |
245-
sudo apt-get update
246-
sudo apt-get install -y libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev
247-
npm install --global "npm@${PYLON_RELEASE_NPM}"
248-
test "$(node --version)" = "v${PYLON_RELEASE_NODE}"
249-
test "$(npm --version)" = "${PYLON_RELEASE_NPM}"
250-
npm ci
251-
252-
- name: Test release contract
253-
run: npm run test:pylon-release
254-
255-
- name: Build offline and pack without network
256-
run: |
257-
sudo env \
258-
"PATH=$PATH" \
259-
"HOME=$HOME" \
260-
"GIT_CONFIG_COUNT=1" \
261-
"GIT_CONFIG_KEY_0=safe.directory" \
262-
"GIT_CONFIG_VALUE_0=$GITHUB_WORKSPACE" \
263-
unshare --net -- npm run release:pylon:pack
264-
265-
- name: Verify artifact contract
266-
run: npm run release:pylon:verify
267-
268-
- name: Upload candidate
248+
node_path="$(node -e 'process.stdout.write(require("node:fs").realpathSync.native(process.execPath))')"
249+
/usr/bin/env -i \
250+
"HOME=/tmp" \
251+
"PATH=${node_path%/*}:/usr/bin:/bin" \
252+
"$node_path" "$GITHUB_WORKSPACE/scripts/run-pylon-release-sandbox.mjs" \
253+
--task pack \
254+
--mode "$PYLON_RELEASE_MODE" \
255+
--host-os "${{ runner.os }}" \
256+
--workspace "$GITHUB_WORKSPACE" \
257+
--github-output "$GITHUB_OUTPUT" \
258+
--run-id "${{ github.run_id }}" \
259+
--run-number "${{ github.run_number }}" \
260+
--publication-policy-revision 3
261+
262+
- name: Upload isolated subjects
269263
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
270264
with:
271265
name: pylon-prime-pack-${{ matrix.copy }}
272-
path: .npm/pylon-release/artifacts
266+
path: ${{ steps.sandbox.outputs.artifact_dir }}
273267
include-hidden-files: true
274268
if-no-files-found: error
275269
retention-days: 3
@@ -301,12 +295,16 @@ jobs:
301295
if: needs.trust.outputs.allowed == 'true'
302296
runs-on: ${{ matrix.os }}
303297
timeout-minutes: 15
298+
permissions:
299+
actions: read
300+
contents: read
304301
strategy:
305302
fail-fast: false
306303
matrix:
307304
os: [ubuntu-24.04, macos-15]
308305
env:
309306
PYLON_RELEASE_NODE: 22.23.2
307+
PYLON_RELEASE_MODE: ci
310308
PYLON_RELEASE_NPM: 11.10.1
311309
steps:
312310
- name: Checkout exact source
@@ -321,17 +319,28 @@ jobs:
321319
with:
322320
node-version: ${{ env.PYLON_RELEASE_NODE }}
323321

324-
- name: Install pinned npm
325-
run: npm install --global "npm@${{ env.PYLON_RELEASE_NPM }}"
326-
327322
- name: Download reproducible pack
328323
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
329324
with:
330325
name: pylon-prime-pack-a
331326
path: .npm/pylon-release/artifacts
332327

333-
- name: Verify temporary-prefix runtime
334-
run: npm run release:pylon:smoke
328+
- name: Run isolated artifact smoke
329+
shell: /usr/bin/bash --noprofile --norc -euo pipefail {0}
330+
run: |
331+
node_path="$(node -e 'process.stdout.write(require("node:fs").realpathSync.native(process.execPath))')"
332+
/usr/bin/env -i \
333+
"HOME=/tmp" \
334+
"PATH=${node_path%/*}:/usr/bin:/bin" \
335+
"$node_path" "$GITHUB_WORKSPACE/scripts/run-pylon-release-sandbox.mjs" \
336+
--task smoke \
337+
--mode "$PYLON_RELEASE_MODE" \
338+
--host-os "${{ runner.os }}" \
339+
--workspace "$GITHUB_WORKSPACE" \
340+
--artifact-dir "$GITHUB_WORKSPACE/.npm/pylon-release/artifacts" \
341+
--run-id "${{ github.run_id }}" \
342+
--run-number "${{ github.run_number }}" \
343+
--publication-policy-revision 3
335344
336345
build-check-test:
337346
name: build-check-test

.github/workflows/pylon-preview-release.yml

Lines changed: 42 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,16 @@ jobs:
5050
needs: admission
5151
runs-on: ubuntu-24.04
5252
timeout-minutes: 20
53+
env:
54+
PYLON_RELEASE_MODE: preview
5355
permissions:
5456
contents: read
5557
strategy:
5658
fail-fast: false
5759
matrix:
5860
copy: [a, b]
5961
steps:
60-
- name: Checkout exact pushed source
62+
- name: Checkout exact source
6163
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
6264
with:
6365
ref: ${{ github.sha }}
@@ -69,41 +71,29 @@ jobs:
6971
with:
7072
node-version: ${{ env.PYLON_RELEASE_NODE }}
7173

72-
- name: Install pinned build inputs
74+
- name: Run isolated release sandbox
75+
id: sandbox
76+
shell: /usr/bin/bash --noprofile --norc -euo pipefail {0}
7377
run: |
74-
sudo apt-get update
75-
sudo apt-get install -y libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev
76-
npm install --global "npm@${PYLON_RELEASE_NPM}"
77-
test "$(node --version)" = "v${PYLON_RELEASE_NODE}"
78-
test "$(npm --version)" = "${PYLON_RELEASE_NPM}"
79-
npm ci
78+
node_path="$(node -e 'process.stdout.write(require("node:fs").realpathSync.native(process.execPath))')"
79+
/usr/bin/env -i \
80+
"HOME=/tmp" \
81+
"PATH=${node_path%/*}:/usr/bin:/bin" \
82+
"$node_path" "$GITHUB_WORKSPACE/scripts/run-pylon-release-sandbox.mjs" \
83+
--task pack \
84+
--mode "$PYLON_RELEASE_MODE" \
85+
--host-os "${{ runner.os }}" \
86+
--workspace "$GITHUB_WORKSPACE" \
87+
--github-output "$GITHUB_OUTPUT" \
88+
--run-id "${{ github.run_id }}" \
89+
--run-number "${{ github.run_number }}" \
90+
--publication-policy-revision 3
8091
81-
- name: Test publication contract
82-
run: |
83-
npm run test:pylon-release
84-
npm run test:pylon-publication
85-
86-
- name: Build and pack without network
87-
run: |
88-
sudo env \
89-
"PATH=$PATH" \
90-
"HOME=$HOME" \
91-
"GIT_CONFIG_COUNT=1" \
92-
"GIT_CONFIG_KEY_0=safe.directory" \
93-
"GIT_CONFIG_VALUE_0=$GITHUB_WORKSPACE" \
94-
unshare --net -- npm run release:pylon:pack
95-
96-
- name: Verify and prepare six exact subjects
97-
run: |
98-
npm run release:pylon:verify
99-
npm run release:pylon:preview -- --publication-policy-revision 2
100-
npm run release:pylon:verify-preview
101-
102-
- name: Upload isolated preview subjects
92+
- name: Upload isolated subjects
10393
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
10494
with:
10595
name: pylon-preview-pack-${{ matrix.copy }}
106-
path: .npm/pylon-release/artifacts
96+
path: ${{ steps.sandbox.outputs.artifact_dir }}
10797
include-hidden-files: true
10898
if-no-files-found: error
10999
retention-days: 3
@@ -183,6 +173,8 @@ jobs:
183173
needs: [pack, reproducibility]
184174
runs-on: ${{ matrix.os }}
185175
timeout-minutes: 15
176+
env:
177+
PYLON_RELEASE_MODE: preview
186178
permissions:
187179
actions: read
188180
contents: read
@@ -191,7 +183,7 @@ jobs:
191183
matrix:
192184
os: [ubuntu-24.04, macos-15]
193185
steps:
194-
- name: Checkout exact pushed source
186+
- name: Checkout exact source
195187
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
196188
with:
197189
ref: ${{ github.sha }}
@@ -203,9 +195,6 @@ jobs:
203195
with:
204196
node-version: ${{ env.PYLON_RELEASE_NODE }}
205197

206-
- name: Install pinned npm
207-
run: npm install --global "npm@${{ env.PYLON_RELEASE_NPM }}"
208-
209198
- name: Verify workflow artifact provenance
210199
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
211200
with:
@@ -235,11 +224,22 @@ jobs:
235224
name: pylon-preview-pack-a
236225
path: .npm/pylon-release/artifacts
237226

238-
- name: Verify and install the exact preview bytes
227+
- name: Run isolated artifact smoke
228+
shell: /usr/bin/bash --noprofile --norc -euo pipefail {0}
239229
run: |
240-
npm run release:pylon:verify-preview
241-
node -e "require('node:fs').unlinkSync('.npm/pylon-release/artifacts/pylon-preview-channel-v1.json')"
242-
npm run release:pylon:smoke
230+
node_path="$(node -e 'process.stdout.write(require("node:fs").realpathSync.native(process.execPath))')"
231+
/usr/bin/env -i \
232+
"HOME=/tmp" \
233+
"PATH=${node_path%/*}:/usr/bin:/bin" \
234+
"$node_path" "$GITHUB_WORKSPACE/scripts/run-pylon-release-sandbox.mjs" \
235+
--task smoke \
236+
--mode "$PYLON_RELEASE_MODE" \
237+
--host-os "${{ runner.os }}" \
238+
--workspace "$GITHUB_WORKSPACE" \
239+
--artifact-dir "$GITHUB_WORKSPACE/.npm/pylon-release/artifacts" \
240+
--run-id "${{ github.run_id }}" \
241+
--run-number "${{ github.run_number }}" \
242+
--publication-policy-revision 3
243243
244244
stage-draft:
245245
name: Stage exact preview draft
@@ -281,7 +281,7 @@ jobs:
281281
release.source?.commit !== context.sha || release.source?.tree !== preview.build?.source?.tree ||
282282
release.build?.id !== tag || preview.build?.tag !== tag ||
283283
preview.build?.releaseManifest?.sha256 !== crypto.createHash("sha256").update(releaseBytes).digest("hex") ||
284-
preview.publicationPolicyRevision !== 2 || preview.sequenceEpoch !== 1 ||
284+
preview.publicationPolicyRevision !== 3 || preview.sequenceEpoch !== 1 ||
285285
preview.sequence !== Number(process.env.GITHUB_RUN_NUMBER) || preview.workflowRunId !== process.env.GITHUB_RUN_ID
286286
) throw new Error("Preview tag plan is not bound to the exact source and workflow sequence.");
287287
core.setOutput("tag", tag);
@@ -469,7 +469,7 @@ jobs:
469469
if (
470470
release.source?.commit !== context.sha || release.source?.tree !== preview.build?.source?.tree ||
471471
release.build?.id !== tag || preview.build?.tag !== tag || preview.build?.releaseManifest?.sha256 !== sha256(releaseBytes) ||
472-
preview.publicationPolicyRevision !== 2 || preview.sequenceEpoch !== 1 ||
472+
preview.publicationPolicyRevision !== 3 || preview.sequenceEpoch !== 1 ||
473473
preview.sequence !== Number(process.env.GITHUB_RUN_NUMBER) || preview.workflowRunId !== process.env.GITHUB_RUN_ID
474474
) throw new Error("Preview draft manifests do not bind the exact source and workflow sequence.");
475475
const expectedNames = [...release.assets.map((asset) => asset.file), "pylon-prime-agent-release-v1.json", "pylon-preview-channel-v1.json"].sort();
@@ -795,7 +795,7 @@ jobs:
795795
previewManifest.build.source.commit !== sourceSha ||
796796
previewManifest.build.source.tree !== releaseManifest.source.tree ||
797797
previewManifest.build.releaseManifest.sha256 !== sha256(releaseBytes) ||
798-
previewManifest.publicationPolicyRevision !== 2 || previewManifest.sequenceEpoch !== 1 ||
798+
previewManifest.publicationPolicyRevision !== 3 || previewManifest.sequenceEpoch !== 1 ||
799799
previewManifest.sequence !== Number(process.env.GITHUB_RUN_NUMBER) || previewManifest.workflowRunId !== process.env.GITHUB_RUN_ID
800800
) {
801801
throw new Error("Downloaded preview metadata is not bound to this exact push and workflow sequence.");

.github/workflows/pylon-stable-release.yml

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,8 @@ jobs:
332332
needs: [admission, verify-preview]
333333
runs-on: ${{ matrix.os }}
334334
timeout-minutes: 15
335+
env:
336+
PYLON_RELEASE_MODE: historical
335337
permissions:
336338
actions: read
337339
contents: read
@@ -340,7 +342,7 @@ jobs:
340342
matrix:
341343
os: [ubuntu-24.04, macos-15]
342344
steps:
343-
- name: Checkout current protected install policy
345+
- name: Checkout exact source
344346
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
345347
with:
346348
ref: ${{ github.sha }}
@@ -352,9 +354,6 @@ jobs:
352354
with:
353355
node-version: ${{ env.PYLON_RELEASE_NODE }}
354356

355-
- name: Install pinned npm
356-
run: npm install --global "npm@${{ env.PYLON_RELEASE_NPM }}"
357-
358357
- name: Verify workflow artifact provenance
359358
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
360359
with:
@@ -382,10 +381,22 @@ jobs:
382381
name: verified-stable-preview
383382
path: .npm/pylon-release/artifacts
384383

385-
- name: Install the same exact preview bytes
384+
- name: Run isolated artifact smoke
385+
shell: /usr/bin/bash --noprofile --norc -euo pipefail {0}
386386
run: |
387-
npm run release:pylon:verify-preview -- --historical
388-
npm run release:pylon:smoke -- --historical
387+
node_path="$(node -e 'process.stdout.write(require("node:fs").realpathSync.native(process.execPath))')"
388+
/usr/bin/env -i \
389+
"HOME=/tmp" \
390+
"PATH=${node_path%/*}:/usr/bin:/bin" \
391+
"$node_path" "$GITHUB_WORKSPACE/scripts/run-pylon-release-sandbox.mjs" \
392+
--task smoke \
393+
--mode "$PYLON_RELEASE_MODE" \
394+
--host-os "${{ runner.os }}" \
395+
--workspace "$GITHUB_WORKSPACE" \
396+
--artifact-dir "$GITHUB_WORKSPACE/.npm/pylon-release/artifacts" \
397+
--run-id "${{ github.run_id }}" \
398+
--run-number "${{ github.run_number }}" \
399+
--publication-policy-revision 3
389400
390401
prepare:
391402
name: Resolve exact stable transaction
@@ -473,7 +484,7 @@ jobs:
473484
--operation "$OPERATION"
474485
--policy-sha "${{ github.sha }}"
475486
--policy-tree "$policy_tree"
476-
--publication-policy-revision 2
487+
--publication-policy-revision 3
477488
)
478489
if [ "$OPERATION" = withdraw ]; then
479490
args+=(--revoke-tag "$REVOKE_STABLE_TAG" --reason "$REASON")
@@ -557,7 +568,7 @@ jobs:
557568
if (
558569
manifest.schemaVersion !== 1 || manifest.channel !== "stable" ||
559570
manifest.repository !== "https://github.com/pylon-code/prime-agent" ||
560-
manifest.promotion?.publicationPolicyRevision !== 2 ||
571+
manifest.promotion?.publicationPolicyRevision !== 3 ||
561572
!/^pylon-stable-[0-9]{6}-g[0-9a-f]{12}-r[1-9][0-9]*$/.test(manifest.tag)
562573
) throw new Error("Stable manifest identity is malformed.");
563574
NODE
@@ -663,8 +674,8 @@ jobs:
663674
if (
664675
!parsed || !preview || Number(parsed[1]) !== manifest.sequence || parsed[2] !== manifest.build.source?.commit?.slice(0, 12) ||
665676
Number(parsed[3]) !== manifest.build.recipeRevision || preview[1] !== parsed[2] || Number(preview[2]) !== manifest.build.recipeRevision ||
666-
manifest.build.previewTag !== manifest.build.id || ![1, 2].includes(manifest.build.publicationPolicyRevision) ||
667-
manifest.promotion?.policyCommit !== context.sha || manifest.promotion?.publicationPolicyRevision !== 2
677+
manifest.build.previewTag !== manifest.build.id || ![1, 2, 3].includes(manifest.build.publicationPolicyRevision) ||
678+
manifest.promotion?.policyCommit !== context.sha || manifest.promotion?.publicationPolicyRevision !== 3
668679
) throw new Error("Stable draft identity is malformed or not signed by this policy commit.");
669680
const name = `Pylon Prime stable ${tag}`;
670681
const encoded = bytes.toString("base64");
@@ -840,10 +851,10 @@ jobs:
840851
Number(previewMatch[2]) !== manifest.build?.recipeRevision || manifest.build.previewTag !== manifest.build.id ||
841852
manifest.build.previewSequence?.sequenceEpoch !== 1 || !Number.isSafeInteger(manifest.build.previewSequence?.sequence) ||
842853
manifest.build.previewSequence.sequence < 1 || !/^[1-9][0-9]*$/.test(manifest.build.previewSequence?.workflowRunId ?? "") ||
843-
manifest.build.previewTag !== process.env.PREVIEW_TAG || ![1, 2].includes(manifest.build.publicationPolicyRevision) ||
854+
manifest.build.previewTag !== process.env.PREVIEW_TAG || ![1, 2, 3].includes(manifest.build.publicationPolicyRevision) ||
844855
manifest.promotion?.kind !== operation ||
845-
(mode === "normal" ? manifest.promotion?.publicationPolicyRevision !== 2 :
846-
![1, 2].includes(manifest.promotion?.publicationPolicyRevision)) ||
856+
(mode === "normal" ? manifest.promotion?.publicationPolicyRevision !== 3 :
857+
![1, 2, 3].includes(manifest.promotion?.publicationPolicyRevision)) ||
847858
manifest.promotion?.policyCommit !== process.env.POLICY_SHA || manifest.promotion?.policyTree !== process.env.POLICY_TREE
848859
) throw new Error("Stable manifest, preview recipe, operator request, or policy identity differs.");
849860
if (operation === "withdraw") {

0 commit comments

Comments
 (0)