You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by as...@apache.org on 2026/10/14 07:42:05 UTC
(maka) branch main updated: fix(release): recover lost macOS ticket connections (#6097)
This is an automated email from the ASF dual-hosted git repository.
Astro-Han pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/maka.git
The following commit(s) were added to refs/heads/main by this push:
new fb51f07b7f fix(release): recover lost macOS ticket connections (#6097)
fb51f07b7f is described below
commit fb51f07b7fca2e4244682fa98d41f34d1af8fd2b
Author: AstroHan <le...@outlook.com>
AuthorDate: Wed Oct 14 18:42:00 2026 +1100
fix(release): recover lost macOS ticket connections (#6097)
Recover confirmed CloudKit ticket connection loss after accepted macOS notarization. Nightly and formal Desktop publication share a bounded stapling gate that retries only exit 68 with NSURLErrorDomain Code=-1005, and requires successful ticket validation. Other failures remain fatal. Refresh the stale generated Astryx inventory summary required by CI.
Generated-by: Codex
---
.github/workflows/desktop-nightly.yml | 2 +-
.github/workflows/release.yml | 3 +-
package.json | 2 +-
scripts/ci-test-plan.mjs | 2 +
scripts/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(-)
diff --git a/.github/workflows/desktop-nightly.yml b/.github/workflows/desktop-nightly.yml
index 12538c08cb..4cef95d6dd 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 6ab7bb3f58..ac65ddfd2e 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 880d79d851..1d7a14b9d0 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- [...]
+ "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.tes [...]
"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 94b1633c1a..b0827f8a9c 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 dd5e067f2e..b5ed8e0f9b 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 0000000000..561e8eb5a0
--- /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 <artifact>');
+ 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 0000000000..8d051d5f33
--- /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']);
+});
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org