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
80 changes: 43 additions & 37 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1535,7 +1535,7 @@ jobs:
startsWith(github.ref, 'refs/tags/') ||
(github.event_name == 'workflow_dispatch' &&
inputs.release_artifacts == 'full')
needs: [release-credentials, rust-check-shared, rust-check-macos, rust-test, rust-test-servo, rust-windows, rust-deny, sdk, ui, e2e, web-assets, python, python-generated]
needs: [release-credentials, web-assets]
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -1763,6 +1763,45 @@ jobs:
--app "$app" --dmg "$dmg" --provenance "${dmg}.notarization.json" \
--target '${{ matrix.rust-target }}' --team-id "$APPLE_TEAM_ID"

- name: Assemble signed macOS distribution
if: runner.os == 'macOS'
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
APPLE_API_KEY_CONTENT: ${{ secrets.APPLE_API_KEY_CONTENT }}
run: |
# The sidecars use the host profile; Tauri builds the app with an
# explicit target. Package those exact binaries without recompiling.
bin_dir="${RUNNER_TEMP}/macos-release-binaries"
mkdir -p "${bin_dir}"
install -m755 "${CARGO_TARGET_DIR}/release/hypercolor-daemon" "${bin_dir}/hypercolor-daemon"
install -m755 "${CARGO_TARGET_DIR}/release/hypercolor" "${bin_dir}/hypercolor"
install -m755 "${CARGO_TARGET_DIR}/${{ matrix.rust-target }}/release/hypercolor-app" "${bin_dir}/hypercolor-app"
bash scripts/with-macos-signing.sh scripts/dist.sh --ci --skip-docs \
--web-assets web-assets --target '${{ matrix.rust-target }}' \
--bin-dir "${bin_dir}" --version '${{ steps.version.outputs.version }}'
platform="macos-arm64"
if [[ '${{ matrix.cask_arch }}' == 'x86_64' ]]; then platform="macos-amd64"; fi
dist_name="hypercolor-${{ steps.version.outputs.version }}-${platform}"
./scripts/sign-macos-artifacts.sh verify-standalone \
--directory "dist/${dist_name}" \
--target '${{ matrix.rust-target }}' --team-id "$APPLE_TEAM_ID"
(cd dist && shasum -a 256 "${dist_name}.tar.gz" > "${dist_name}.tar.gz.sha256")

- name: Upload macOS release tarball
if: runner.os == 'macOS'
uses: actions/upload-artifact@v7
with:
name: hypercolor-tarball-${{ steps.version.outputs.version }}-${{ matrix.target }}
path: |
dist/hypercolor-*.tar.gz
dist/hypercolor-*.tar.gz.sha256
if-no-files-found: error

- name: Upload native release bundle
uses: actions/upload-artifact@v7
with:
Expand Down Expand Up @@ -1893,7 +1932,7 @@ jobs:
startsWith(github.ref, 'refs/tags/') ||
(github.event_name == 'workflow_dispatch' &&
inputs.release_artifacts == 'full')
needs: [release-credentials, rust-check-shared, rust-check-macos, rust-test, rust-test-servo, rust-windows, rust-deny, sdk, ui, e2e, web-assets, python, python-generated]
needs: [release-credentials, web-assets]
strategy:
fail-fast: false
matrix:
Expand All @@ -1904,29 +1943,13 @@ jobs:
- target: linux-arm64
os: ubuntu-24.04-arm
rust-target: aarch64-unknown-linux-gnu
- target: macos-arm64
os: macos-26
rust-target: aarch64-apple-darwin
- target: macos-amd64
os: macos-26-intel
rust-target: x86_64-apple-darwin
# A full workflow_dispatch on the default branch warms these release
# shapes. Tags restore that trusted cache; they never publish cache state.
timeout-minutes: 120
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7

- name: Qualify macOS runner and SDK
if: runner.os == 'macOS'
run: |
sudo xcode-select -s "/Applications/Xcode_${XCODE_VERSION}.app/Contents/Developer"
test "$(xcrun --show-sdk-version | cut -d. -f1)" = "26"

- name: Install NASM for Intel macOS
if: matrix.target == 'macos-amd64'
run: brew install nasm

- name: Report runner capacity
shell: bash
run: |
Expand Down Expand Up @@ -2030,24 +2053,6 @@ jobs:
--version "${{ steps.version.outputs.version }}"
test -f "dist/${{ steps.version.outputs.dist_name }}.tar.gz"

- name: Assemble signed macOS distribution
if: runner.os == 'macOS'
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
APPLE_API_KEY_CONTENT: ${{ secrets.APPLE_API_KEY_CONTENT }}
run: |
bash scripts/with-macos-signing.sh scripts/dist.sh --ci --skip-docs \
--web-assets web-assets --target '${{ matrix.target }}' \
--version '${{ steps.version.outputs.version }}'
./scripts/sign-macos-artifacts.sh verify-standalone \
--directory 'dist/${{ steps.version.outputs.dist_name }}' \
--target '${{ matrix.rust-target }}' --team-id "$APPLE_TEAM_ID"

# Memory exhaustion can take down the VM before this step runs.
- name: Report runner capacity after failure
if: failure()
Expand Down Expand Up @@ -2124,11 +2129,12 @@ jobs:
# Accepts workflow_dispatch as well as push: release.yml pushes tags
# with GITHUB_TOKEN (which never fires `on: push`) and then dispatches
# this workflow on the tag ref.
# Compilation runs alongside validation; only publication waits for both.
create-release:
if: >-
(github.event_name == 'push' || github.event_name == 'workflow_dispatch') &&
startsWith(github.ref, 'refs/tags/')
needs: [build-release, build-native-app, python, python-generated]
needs: [build-release, build-native-app, release-credentials, rust-check-shared, rust-check-macos, rust-test, rust-test-servo, rust-windows, rust-deny, sdk, ui, e2e, web-assets, python, python-generated]
runs-on: ubuntu-latest
permissions:
contents: write
Expand Down
21 changes: 15 additions & 6 deletions scripts/tests/macos-ci-coverage.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,21 @@ test('cache ownership and restored target directories agree across lanes', () =>
assert.equal(condition('Save Rust build caches'), 'always()');
});

test('both release builders require the macOS matrix and Windows tests', () => {
test('publication retains every validation gate while compilation overlaps it', () => {
const body = id => workflow.match(new RegExp(`^ ${id}:\\n([\\s\\S]*?)(?=^ [a-z][\\w-]*:)`, 'm'))?.[1];
const needs = id => body(id)?.match(/^ needs: \[(.+)\]$/m)?.[1].split(', ');
for (const id of ['build-release', 'build-native-app']) {
const body = workflow.match(new RegExp(`^ ${id}:\\n([\\s\\S]*?)(?=^ [a-z][\\w-]*:)`, 'm'))?.[1];
assert.ok(body, `missing release builder: ${id}`);
const needs = body.match(/^ needs: \[(.+)\]$/m)?.[1].split(', ').map((value) => value.trim());
assert.ok(needs?.includes('rust-check-macos'), `${id} must wait for both lanes on both architectures`);
assert.ok(needs?.includes('rust-windows'), `${id} must wait for Windows tests before producing release artifacts`);
assert.deepEqual(needs(id), ['release-credentials', 'web-assets']);
}
const validation = [
'release-credentials', 'rust-check-shared', 'rust-check-macos', 'rust-test',
'rust-test-servo', 'rust-windows', 'rust-deny', 'sdk', 'ui', 'e2e',
'web-assets', 'python', 'python-generated',
];
assert.deepEqual(needs('create-release'), ['build-release', 'build-native-app', ...validation]);
// Preserve GitHub's implicit success() gate: failed or skipped validation
// must never become publishable through always() or !cancelled().
const condition = body('create-release').split(' needs:')[0];
assert.doesNotMatch(condition, /always\(|cancelled\(|failure\(/);
assert.match(condition, /startsWith\(github.ref, 'refs\/tags\/'\)/);
});
71 changes: 70 additions & 1 deletion scripts/tests/macos-release.test.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync } from 'node:fs';
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { tmpdir } from 'node:os';
import path from 'node:path';
Expand Down Expand Up @@ -125,3 +125,72 @@ test('Homebrew checksum step supplies every value consumed by its renderer', ()
rmSync(dir, { recursive: true, force: true });
}
});

test('macOS tarballs package the native job binaries for both architectures', () => {
const workflow = readFileSync(new URL('../../.github/workflows/ci.yml', import.meta.url), 'utf8');
const native = workflow.match(/^ build-native-app:\n([\s\S]*?)(?=^ [a-z][\w-]*:)/m)[1];
const tarballs = workflow.match(/^ build-release:\n([\s\S]*?)(?=^ [a-z][\w-]*:)/m)[1];
assert.doesNotMatch(tarballs, /target: macos-/);
const step = native.match(/ - name: Assemble signed macOS distribution\n([\s\S]*?)(?= - name:)/)[1];
assert.match(step, /if: runner.os == 'macOS'/);
const script = step.split(' run: |\n')[1].replace(/^ /gm, '');
assert.doesNotMatch(script, /cargo build|cargo tauri/);
assert.match(native, /name: hypercolor-tarball-\$\{\{ steps.version.outputs.version \}\}-\$\{\{ matrix.target \}\}/);
assert.match(native, /dist\/hypercolor-\*\.tar.gz\n/);
for (const [arch, target, platform] of [
['arm64', 'aarch64-apple-darwin', 'macos-arm64'],
['x86_64', 'x86_64-apple-darwin', 'macos-amd64'],
]) {
const dir = mkdtempSync(path.join(tmpdir(), 'macos-prebuilt-release-'));
try {
mkdirSync(path.join(dir, 'scripts'));
mkdirSync(path.join(dir, 'target/release'), { recursive: true });
mkdirSync(path.join(dir, `target/${target}/release`), { recursive: true });
for (const binary of ['hypercolor-daemon', 'hypercolor']) {
writeFileSync(path.join(dir, 'target/release', binary), binary, { mode: 0o755 });
}
writeFileSync(path.join(dir, `target/${target}/release/hypercolor-app`), 'hypercolor-app', { mode: 0o755 });
writeFileSync(path.join(dir, 'scripts/with-macos-signing.sh'), '#!/bin/bash\nexec "$@"\n');
// Replace the platform signing transport; execute the actual workflow's
// path selection, staging, package invocation and checksum generation.
writeFileSync(path.join(dir, 'scripts/dist.sh'), `#!/bin/bash
set -euo pipefail
bin_dir=''
while (( $# )); do
case "$1" in
--bin-dir) bin_dir="$2"; shift ;;
--target) test "$2" = '${target}'; shift ;;
--version) test "$2" = '0.5.2'; shift ;;
--web-assets) shift ;;
esac
shift
done
for binary in hypercolor-daemon hypercolor hypercolor-app; do
test -x "$bin_dir/$binary"
test "$(cat "$bin_dir/$binary")" = "$binary"
done
mkdir -p dist/hypercolor-0.5.2-${platform}
printf 'archive-fixture' > dist/hypercolor-0.5.2-${platform}.tar.gz
`, { mode: 0o755 });
writeFileSync(path.join(dir, 'scripts/sign-macos-artifacts.sh'), `#!/bin/bash
set -euo pipefail
test "$1" = verify-standalone
test "$3" = dist/hypercolor-0.5.2-${platform}
test "$5" = '${target}'
test "$7" = fixture-team
`, { mode: 0o755 });
const run = script.replaceAll('${{ matrix.rust-target }}', target)
.replaceAll('${{ matrix.cask_arch }}', arch)
.replaceAll('${{ steps.version.outputs.version }}', '0.5.2');
const result = spawnSync('bash', ['-e', '-c', run], {
cwd: dir, encoding: 'utf8',
env: { ...environment, RUNNER_TEMP: dir, CARGO_TARGET_DIR: path.join(dir, 'target'), APPLE_TEAM_ID: 'fixture-team' },
});
assert.equal(result.status, 0, result.stderr);
const checksum = readFileSync(path.join(dir, `dist/hypercolor-0.5.2-${platform}.tar.gz.sha256`), 'utf8');
assert.equal(checksum.trim(), `${createHash('sha256').update('archive-fixture').digest('hex')} hypercolor-0.5.2-${platform}.tar.gz`);
} finally {
rmSync(dir, { recursive: true, force: true });
}
}
});
Loading