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
7 changes: 7 additions & 0 deletions .github/workflows/desktop-nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,13 @@ jobs:
umask 077
printf '%s' "$APPLE_API_KEY_CONTENT" > "$APPLE_API_KEY_PATH"

- name: Use TCP for Apple ticket delivery
if: matrix.platform == 'macos'
# Remove when actions/runner-images#14924 fixes HTTP/3 ticket connection loss.
run: |
echo 'block return out quick proto udp from any to any port 443' | sudo pfctl -f -
sudo pfctl -E

- name: Package the signed and notarized macOS Nightly
if: matrix.platform == 'macos'
env:
Expand Down
13 changes: 13 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,13 @@ jobs:
umask 077
printf '%s' "$APPLE_API_KEY_CONTENT" > "$APPLE_API_KEY_PATH"

- name: Use TCP for Apple ticket delivery
if: matrix.platform == 'macos'
# Remove when actions/runner-images#14924 fixes HTTP/3 ticket connection loss.
run: |
echo 'block return out quick proto udp from any to any port 443' | sudo pfctl -f -
sudo pfctl -E

- name: Package notarized app and signed DMG
if: matrix.platform == 'macos'
env:
Expand Down Expand Up @@ -370,6 +377,12 @@ jobs:
umask 077
printf '%s' "$APPLE_API_KEY_CONTENT" > "$APPLE_API_KEY_PATH"

- name: Use TCP for Apple ticket delivery
# Remove when actions/runner-images#14924 fixes HTTP/3 ticket connection loss.
run: |
echo 'block return out quick proto udp from any to any port 443' | sudo pfctl -f -
sudo pfctl -E

- name: Package signed and notarized CLI and TUI
env:
CSC_LINK: ${{ secrets.CSC_LINK }}
Expand Down
19 changes: 19 additions & 0 deletions scripts/desktop-nightly-workflow-policy.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,25 @@ test('both macOS publication workflows staple through the shared recovery gate a
}
});

test('macOS publication jobs disable the broken HTTP/3 route before any notarization', async () => {
for (const name of ['desktop-nightly.yml', 'release.yml']) {
const workflow = await readWorkflow(name);
for (const job of [workflow.jobs.desktop, workflow.jobs['cli-macos-arm64']].filter(Boolean)) {
const setup = job.steps.findIndex(
(step) => step.name === 'Use TCP for Apple ticket delivery',
);
const packaging = job.steps.findIndex((step) =>
/npm run package:(macos|cli:macos)/u.test(step.run ?? ''),
);
assert.ok(setup >= 0 && setup < packaging, name);
const step = job.steps[setup];
assert.equal(step.if, job.strategy ? "matrix.platform == 'macos'" : undefined);
assert.match(step.run, /block return out quick proto udp from any to any port 443/u);
assert.match(step.run, /sudo pfctl -f -\nsudo pfctl -E/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
9 changes: 7 additions & 2 deletions scripts/staple-macos-artifact.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,15 @@ const execute = promisify(execFile);

export async function stapleMacosArtifact(
path,
{ run = execute, wait = sleep, report = (text) => process.stderr.write(text) } = {},
{
validateOnly = false,
run = execute,
wait = sleep,
report = (text) => process.stderr.write(text),
} = {},
) {
const options = { encoding: 'utf8', timeout: 120_000 };
for (const action of ['staple', 'validate']) {
for (const action of validateOnly ? ['validate'] : ['staple', 'validate']) {
for (let attempt = 1; attempt <= 3; attempt += 1) {
try {
const result = await run('xcrun', ['stapler', action, path], options);
Expand Down
6 changes: 5 additions & 1 deletion scripts/verify-macos-dmg.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { crc32, inflateSync } from 'node:zlib';
import { FILESYSTEM_WORKER_PROTOCOL_VERSION } from '../packages/runtime/dist/filesystem-worker/protocol.js';
import { readProductManifestIdentity } from './product-release-identity.mjs';
import { assertPackagedUpdateConfiguration } from './desktop-update-contract.mjs';
import { stapleMacosArtifact } from './staple-macos-artifact.mjs';
import {
resolveDesktopBuildVersion,
resolveDesktopReleaseTarget,
Expand Down Expand Up @@ -355,7 +356,10 @@ export async function verifyPackagedMacApp(
assertSingleArchitecture(executableArchitectures.stdout, 'Maka executable', expectedArch);
await run('codesign', ['--verify', '--deep', '--strict', '--verbose=2', appPath]);
await run('spctl', ['--assess', '--type', 'execute', '--verbose=4', appPath]);
await run('xcrun', ['stapler', 'validate', appPath]);
await stapleMacosArtifact(appPath, {
validateOnly: true,
run: (command, args, options) => run(command, args, { timeoutMs: options.timeout }),
});

const ptyProbe = makePtyProbe(
'/bin/echo',
Expand Down
13 changes: 9 additions & 4 deletions scripts/verify-packaged-app.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,15 @@ export function runCommand(command, args, options = {}) {
return;
}
reject(
new Error(
`${command} ${args.join(' ')} failed with ${
signal ? `signal ${signal}` : `exit code ${code}`
}\n${stderr.trim()}`,
Object.assign(
new Error(
`${command} ${args.join(' ')} failed with ${
signal ? `signal ${signal}` : `exit code ${code}`
}` +
`${stdout.trim() ? `\nstdout: ${stdout.trim()}` : ''}` +
`${stderr.trim() ? `\nstderr: ${stderr.trim()}` : ''}`,
),
{ code, signal, stdout, stderr },
),
);
});
Expand Down
41 changes: 36 additions & 5 deletions scripts/verify-packaged-app.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,31 @@ import {
asarLookupPath,
assertPackagedDependencyClosure,
assertPackagedResources,
runCommand,
stopChild,
} from './verify-packaged-app.mjs';

const { version: productVersion } = JSON.parse(
await readFile(new URL('../package.json', import.meta.url), 'utf8'),
);

test('runCommand preserves the exit code and both output streams on failure', async () => {
await assert.rejects(
runCommand(process.execPath, [
'-e',
"require('node:fs').writeSync(1, 'CloudKit connection lost'); require('node:fs').writeSync(2, 'stderr detail'); process.exit(68)",
]),
(error) => {
assert.equal(error.code, 68);
assert.equal(error.stdout, 'CloudKit connection lost');
assert.equal(error.stderr, 'stderr detail');
assert.match(error.message, /CloudKit connection lost/u);
assert.match(error.message, /stderr detail/u);
return true;
},
);
});

test('stopChild waits for a process that ignores SIGTERM to exit before returning', {
skip: process.platform === 'win32',
}, async (t) => {
Expand Down Expand Up @@ -478,7 +496,7 @@ const options = {
collectPackagedAllowlist: () => allowlistOf(PTY_PACKAGES),
};

test('accepts the Intel Mach-O architecture for an x64 package', async () => {
test('verifies an Intel macOS package after a transient ticket validation failure', async () => {
const { verifyPackagedMacApp } = await import('./verify-macos-dmg.mjs');
const asarPackages = await Promise.all(
PTY_PACKAGES.map(async (name) => {
Expand Down Expand Up @@ -508,6 +526,7 @@ test('accepts the Intel Mach-O architecture for an x64 package', async () => {
const app = join(dirname(resources), 'Maka.app');
await mkdir(join(app, 'Contents'), { recursive: true });
await rename(resources, join(app, 'Contents', 'Resources'));
const verificationSteps = [];
// The archive and update configuration are real; macOS command output and
// app launches are the system boundaries this portable test substitutes.
await verifyPackagedMacApp(app, {
Expand All @@ -516,7 +535,9 @@ test('accepts the Intel Mach-O architecture for an x64 package', async () => {
environment: { MAKA_DESKTOP_NIGHTLY_VERSION: version },
requirePath: async () => {},
smokeFilesystemWorker: async () => {},
smokeRenderer: async () => {},
smokeRenderer: async () => {
verificationSteps.push('renderer');
},
run: async (command, args) => {
if (command === 'plutil') {
const values = {
Expand All @@ -528,14 +549,24 @@ test('accepts the Intel Mach-O architecture for an x64 package', async () => {
return { stdout: `${values[args[1]]}\n` };
}
if (command === 'lipo') return { stdout: 'x86_64\n' };
if (
['codesign', 'spctl', 'xcrun', join(app, 'Contents', 'MacOS', 'Maka')].includes(command)
) {
if (command === 'xcrun') {
verificationSteps.push(args[1]);
assert.deepEqual(args, ['stapler', 'validate', app]);
if (verificationSteps.length === 1) {
return runCommand(process.execPath, [
'-e',
"require('node:fs').writeSync(1, 'Error Domain=NSURLErrorDomain Code=-1005 connection lost'); process.exit(68)",
]);
}
return { stdout: '' };
}
if (['codesign', 'spctl', join(app, 'Contents', 'MacOS', 'Maka')].includes(command)) {
return { stdout: '' };
}
throw new Error(`Unexpected command: ${command}`);
},
});
assert.deepEqual(verificationSteps, ['validate', 'validate', 'renderer']);
});

describe('assertRenderableAppIcon', () => {
Expand Down