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