From d1b0391668e5b5ae24e59dd53d7f74b8ce704d82 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Wed, 14 Oct 2026 16:07:13 +1100 Subject: [PATCH 1/2] fix(release): recover lost macOS ticket connections Generated-by: Codex --- .github/workflows/desktop-nightly.yml | 2 +- .github/workflows/release.yml | 3 +- package.json | 2 +- scripts/ci-test-plan.mjs | 2 + .../desktop-nightly-workflow-policy.test.mjs | 15 +++ scripts/staple-macos-artifact.mjs | 56 +++++++++ scripts/staple-macos-artifact.test.mjs | 110 ++++++++++++++++++ 7 files changed, 186 insertions(+), 4 deletions(-) create mode 100644 scripts/staple-macos-artifact.mjs create mode 100644 scripts/staple-macos-artifact.test.mjs diff --git a/.github/workflows/desktop-nightly.yml b/.github/workflows/desktop-nightly.yml index 12538c08cb2..4cef95d6dd5 100644 --- a/.github/workflows/desktop-nightly.yml +++ b/.github/workflows/desktop-nightly.yml @@ -174,7 +174,7 @@ jobs: --key-id "$APPLE_API_KEY_ID" \ --issuer "$APPLE_API_ISSUER" \ --wait - xcrun stapler staple "$DMG_PATH" + node scripts/staple-macos-artifact.mjs "$DMG_PATH" npm run verify:macos -- "${{ matrix.arch }}" - name: Package the Windows Nightly diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6ab7bb3f58f..ac65ddfd2e3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -191,10 +191,9 @@ jobs: --key-id "$APPLE_API_KEY_ID" \ --issuer "$APPLE_API_ISSUER" \ --wait - xcrun stapler staple "$DMG_PATH" + node scripts/staple-macos-artifact.mjs "$DMG_PATH" codesign --verify --verbose=4 "$DMG_PATH" - xcrun stapler validate "$DMG_PATH" spctl --assess \ --type open \ --context context:primary-signature \ diff --git a/package.json b/package.json index 880d79d851f..1d7a14b9d08 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "check:runtime-host-peer-dependencies": "node scripts/generate-runtime-host-peer-dependencies.mjs --check", "generate:runtime-host-peer-notices": "node scripts/generate-runtime-host-peer-notices.mjs", "check:runtime-host-peer-notices": "node scripts/generate-runtime-host-peer-notices.mjs --check", - "check:release": "npm run check:stale && npm run check:third-party-notices && npm run check:cli-third-party-notices && npm run check:model-metadata && npm run check:product-release-identity && npm run check:asf-npm && node --test scripts/product-nightly.test.mjs scripts/desktop-release-targets.test.mjs scripts/verify-linux-harness.test.mjs scripts/desktop-nightly.test.mjs scripts/desktop-nightly-stage.test.mjs scripts/desktop-nightly-release.test.mjs scripts/desktop-nightly-workflow-policy.test.mjs scripts/audit-shipped-dependencies.test.mjs scripts/product-release.test.mjs scripts/product-release-authority.test.mjs scripts/release-cli-file-policy.test.mjs scripts/release-cli-artifact-policy.test.mjs scripts/release-cli-eval-support.test.mjs scripts/release-cli-publication.test.mjs scripts/release-cli-runtime-host-diagnostics.test.mjs scripts/qualify-released-cli-state-root.test.mjs scripts/release-cli-workflow-policy.test.mjs scripts/verify-packaged-app.test.mjs scripts/macos-update-archive.test.mjs scripts/third-party-closure.test.mjs scripts/generate-third-party-notices.test.mjs scripts/source-legal-inventory.test.mjs scripts/sync-model-metadata.test.mjs scripts/prepare-windows-upgrade-baseline.test.mjs scripts/windows-package-source-closure.test.mjs", + "check:release": "npm run check:stale && npm run check:third-party-notices && npm run check:cli-third-party-notices && npm run check:model-metadata && npm run check:product-release-identity && npm run check:asf-npm && node --test scripts/staple-macos-artifact.test.mjs scripts/product-nightly.test.mjs scripts/desktop-release-targets.test.mjs scripts/verify-linux-harness.test.mjs scripts/desktop-nightly.test.mjs scripts/desktop-nightly-stage.test.mjs scripts/desktop-nightly-release.test.mjs scripts/desktop-nightly-workflow-policy.test.mjs scripts/audit-shipped-dependencies.test.mjs scripts/product-release.test.mjs scripts/product-release-authority.test.mjs scripts/release-cli-file-policy.test.mjs scripts/release-cli-artifact-policy.test.mjs scripts/release-cli-eval-support.test.mjs scripts/release-cli-publication.test.mjs scripts/release-cli-runtime-host-diagnostics.test.mjs scripts/qualify-released-cli-state-root.test.mjs scripts/release-cli-workflow-policy.test.mjs scripts/verify-packaged-app.test.mjs scripts/macos-update-archive.test.mjs scripts/third-party-closure.test.mjs scripts/generate-third-party-notices.test.mjs scripts/source-legal-inventory.test.mjs scripts/sync-model-metadata.test.mjs scripts/prepare-windows-upgrade-baseline.test.mjs scripts/windows-package-source-closure.test.mjs", "package:macos-arm64": "node scripts/package-macos.mjs arm64", "package:macos-x64": "node scripts/package-macos.mjs x64", "verify:macos": "node scripts/verify-macos-dmg.mjs", diff --git a/scripts/ci-test-plan.mjs b/scripts/ci-test-plan.mjs index 94b1633c1a6..b0827f8a9c9 100644 --- a/scripts/ci-test-plan.mjs +++ b/scripts/ci-test-plan.mjs @@ -59,6 +59,8 @@ const RELEASE_CONTRACT_FILES = new Set([ 'scripts/audit-shipped-dependencies.mjs', 'scripts/audit-shipped-dependencies.test.mjs', 'scripts/package-macos.mjs', + 'scripts/staple-macos-artifact.mjs', + 'scripts/staple-macos-artifact.test.mjs', 'scripts/package-macos-autoupdate-next.mjs', 'scripts/package-macos-arm64-cli.mjs', 'scripts/package-linux.mjs', diff --git a/scripts/desktop-nightly-workflow-policy.test.mjs b/scripts/desktop-nightly-workflow-policy.test.mjs index dd5e067f2eb..b5ed8e0f9b4 100644 --- a/scripts/desktop-nightly-workflow-policy.test.mjs +++ b/scripts/desktop-nightly-workflow-policy.test.mjs @@ -206,6 +206,21 @@ test('the Nightly Linux verification runs under a virtual display', async () => } }); +test('both macOS publication workflows staple through the shared recovery gate after notarization', async () => { + for (const name of ['desktop-nightly.yml', 'release.yml']) { + const workflow = await readWorkflow(name); + const step = workflow.jobs.desktop.steps.find((entry) => + entry.run?.includes('notarytool submit'), + ); + assert.ok(step, name); + assert.match( + step.run, + /notarytool submit[\s\S]*--wait[\s\S]*node scripts\/staple-macos-artifact\.mjs "\$DMG_PATH"/u, + ); + assert.doesNotMatch(step.run, /xcrun stapler/u); + } +}); + test('Desktop Nightly has no Apache Nightlies transport or compatibility state', async () => { const workflow = await readWorkflow('desktop-nightly.yml'); assert.equal(workflow.jobs.publish.environment, 'nightly'); diff --git a/scripts/staple-macos-artifact.mjs b/scripts/staple-macos-artifact.mjs new file mode 100644 index 00000000000..561e8eb5a08 --- /dev/null +++ b/scripts/staple-macos-artifact.mjs @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { execFile } from 'node:child_process'; +import { setTimeout as sleep } from 'node:timers/promises'; +import { pathToFileURL } from 'node:url'; +import { promisify } from 'node:util'; + +const execute = promisify(execFile); + +export async function stapleMacosArtifact( + path, + { run = execute, wait = sleep, report = (text) => process.stderr.write(text) } = {}, +) { + const options = { encoding: 'utf8', timeout: 120_000 }; + for (let attempt = 1; attempt <= 3; attempt += 1) { + try { + const result = await run('xcrun', ['stapler', 'staple', path], options); + report(`${result.stdout ?? ''}${result.stderr ?? ''}`); + break; + } catch (error) { + const output = `${error.stdout ?? ''}${error.stderr ?? ''}`; + report(output); + // Exit 68 also covers invalid tickets; retry only the observed transport failure. + if (error.code !== 68 || !/NSURLErrorDomain Code=-1005\b/u.test(output) || attempt === 3) { + throw error; + } + const delay = attempt * 10_000; + report(`[stapler] ticket connection lost on attempt ${attempt}/3; retrying in ${delay}ms\n`); + await wait(delay); + } + } + const validated = await run('xcrun', ['stapler', 'validate', path], options); + report(`${validated.stdout ?? ''}${validated.stderr ?? ''}`); +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + if (process.argv.length !== 3) throw new Error('Usage: staple-macos-artifact.mjs '); + await stapleMacosArtifact(process.argv[2]); +} diff --git a/scripts/staple-macos-artifact.test.mjs b/scripts/staple-macos-artifact.test.mjs new file mode 100644 index 00000000000..8d051d5f330 --- /dev/null +++ b/scripts/staple-macos-artifact.test.mjs @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { stapleMacosArtifact } from './staple-macos-artifact.mjs'; + +const networkFailure = () => + Object.assign(new Error('stapler failed'), { + code: 68, + stdout: + 'Error Domain=NSURLErrorDomain Code=-1005 "The network connection was lost."\nThe staple and validate action failed! Error 68.\n', + stderr: '', + }); + +test('recovers a lost ticket connection and validates the same artifact', async () => { + const calls = []; + const delays = []; + const output = []; + const path = '/release/Maka snapshot.dmg'; + await stapleMacosArtifact(path, { + run: async (command, args) => { + calls.push([command, ...args]); + if (calls.length === 1) throw networkFailure(); + return { stdout: 'The action worked!\n', stderr: '' }; + }, + wait: async (delay) => delays.push(delay), + report: (text) => output.push(text), + }); + assert.deepEqual(calls, [ + ['xcrun', 'stapler', 'staple', path], + ['xcrun', 'stapler', 'staple', path], + ['xcrun', 'stapler', 'validate', path], + ]); + assert.deepEqual(delays, [10_000]); + assert.match(output.join(''), /The network connection was lost/u); +}); + +test('fails after three lost connections without validating an unstapled artifact', async () => { + let attempts = 0; + const delays = []; + const failure = networkFailure(); + await assert.rejects( + stapleMacosArtifact('/release/Maka.dmg', { + run: async (_command, args) => { + assert.equal(args[1], 'staple'); + attempts += 1; + throw failure; + }, + wait: async (delay) => delays.push(delay), + report: () => {}, + }), + (error) => error === failure, + ); + assert.equal(attempts, 3); + assert.deepEqual(delays, [10_000, 20_000]); +}); + +test('does not retry other stapling failures or a rejected validation', async () => { + for (const failure of [ + Object.assign(new Error('ticket missing'), { code: 68, stdout: 'Record not found' }), + Object.assign(networkFailure(), { code: 65 }), + ]) { + let attempts = 0; + await assert.rejects( + stapleMacosArtifact('/release/Maka.dmg', { + run: async () => { + attempts += 1; + throw failure; + }, + wait: async () => assert.fail('permanent failures must not be retried'), + report: () => {}, + }), + (error) => error === failure, + ); + assert.equal(attempts, 1); + } + + const calls = []; + const failure = networkFailure(); + await assert.rejects( + stapleMacosArtifact('/release/Maka.dmg', { + run: async (_command, args) => { + calls.push(args[1]); + if (args[1] === 'validate') throw failure; + return { stdout: '', stderr: '' }; + }, + wait: async () => assert.fail('a failed validation must not be retried'), + report: () => {}, + }), + (error) => error === failure, + ); + assert.deepEqual(calls, ['staple', 'validate']); +}); From 2449dc637fa2c73a7a0fb98175f0a96e097a092d Mon Sep 17 00:00:00 2001 From: AstroHan Date: Wed, 14 Oct 2026 16:12:24 +1100 Subject: [PATCH 2/2] chore(ci): refresh Astryx inventory totals Generated-by: Codex --- docs/astryx-surface-file-inventory.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/astryx-surface-file-inventory.md b/docs/astryx-surface-file-inventory.md index bd4faec3552..d43b381a994 100644 --- a/docs/astryx-surface-file-inventory.md +++ b/docs/astryx-surface-file-inventory.md @@ -6,7 +6,7 @@ Generated against `@astryxdesign/core@0.6.3` (195 component exports). Wiki bar: Design Conventions · API Use-the-System · Theming · Container Padding. -**Totals:** 334 files — blocker 0, reimplementation 0, polish 4, aligned 330. +**Totals:** 335 files — blocker 0, reimplementation 0, polish 4, aligned 331. ## Exclusions (explicit)