Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/desktop-nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
2 changes: 1 addition & 1 deletion docs/astryx-surface-file-inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions scripts/ci-test-plan.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
15 changes: 15 additions & 0 deletions scripts/desktop-nightly-workflow-policy.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
56 changes: 56 additions & 0 deletions scripts/staple-macos-artifact.mjs
Original file line number Diff line number Diff line change
@@ -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]);
}
110 changes: 110 additions & 0 deletions scripts/staple-macos-artifact.test.mjs
Original file line number Diff line number Diff line change
@@ -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']);
});
Loading