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
1 change: 1 addition & 0 deletions .github/workflows/release-swift-cli.yml
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,7 @@ jobs:
PREPARE_SCRIPT: ${{ inputs.prepare-script }}
run: |
set -euo pipefail
git config --global --add safe.directory "$GITHUB_WORKSPACE"
apt-get update
apt-get install -y --no-install-recommends python3 file
if [[ -n "$GENERATE_VERSION_SCRIPT" ]]; then "$GENERATE_VERSION_SCRIPT"; fi
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

## 1.11.8 - 2026-10-18

- Trust only the exact Swift CLI Linux container checkout before dependency preparation so Git ownership checks do not block the release lockfile check.

## 1.11.7 - 2026-10-18

- Move all macOS release builds, signing, CI, and independent verification to the GA `xcode-27` runners; verify x86_64 with Rosetta, preserve caller deployment targets and legacy v1 runner inputs, and keep Sparkle's three attestations distinct by verifier identity.
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ release-build proof.

Set `dry_run: true` to rehearse the exact current protected main commit, even when the requested version already has a historical tag. Validation, signing, notarization, and both write-free verifiers run from retained Actions artifacts; tag creation, draft creation, publication, Homebrew handoff, and closeout are skipped. Apple still receives the candidate ZIP for notarization. Existing callers retain `dry_run: false`; dispatch callers should default their user-facing input to true.

The Linux container explicitly trusts only its exact `GITHUB_WORKSPACE` checkout for Git operations before dependency preparation. The `Package.resolved` drift check remains required; unrelated repositories are not trusted.

`include-helper: false` removes the companion dylib from build requirements, signing, the signing manifest, and both verifiers. `include-linux: false` skips the Linux build/download and omits its archive from the inventory and checksums. Both default to true for existing imsg callers. Set `require-resource-bundles: false` for CLIs without SwiftPM resources. Disabled helper/Linux names and identifiers may be empty.

`macos-runner` and `verify-arm64-runner` default to `xcode-27`; `xcode-27-xlarge` is available for callers that need larger runners. Existing v1 macOS labels remain accepted as aliases for `xcode-27`. `minimum-macos-version` defaults to 14 and sets `MACOSX_DEPLOYMENT_TARGET` for the build; keep Swift package platforms and any explicit build-script targets aligned with it. Both verifiers run on macOS 27: arm64 natively and x86_64 explicitly under Rosetta. This proves execution on macOS 27, not runtime compatibility on the minimum supported OS. Empty `prepare-script`, `generate-version-script`, and `entitlements-file` disable those optional steps. An empty `version-file` requires `metadata-check-script`, a tracked credential-free executable called with `vX.Y.Z` during Linux validation; it must validate the caller's version metadata. `macos-build-script` must emit an executable into `OUTPUT_DIR`; a thin adapter may call an existing local universal builder.
Expand Down
60 changes: 60 additions & 0 deletions scripts/test-swift-linux-git-ownership.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/env node
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { mkdtempSync, mkdirSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { loadWorkflow, workflowStep } from './workflow-source.cjs';

const workflow = loadWorkflow('release-swift-cli.yml');
const prepare = workflowStep(workflow, 'build-linux', 'name', 'Resolve and patch SwiftPM dependencies').run;
const root = realpathSync(mkdtempSync(join(tmpdir(), 'swift-git-owner-')));
try {
const workspace = join(root, 'checkout with spaces');
const unrelated = join(root, 'unrelated');
const home = join(root, 'home');
const tools = join(root, 'tools');
for (const dir of [workspace, unrelated, home, tools]) mkdirSync(dir);
const env = Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.startsWith('GIT_')));
Object.assign(env, {
HOME: home, XDG_CONFIG_HOME: home, GIT_CONFIG_NOSYSTEM: '1',
GIT_CONFIG_GLOBAL: join(home, '.gitconfig'), GITHUB_WORKSPACE: workspace,
PATH: `${tools}:${process.env.PATH}`,
});
const run = (command, args, cwd = workspace, runEnv = env) =>
spawnSync(command, args, { cwd, env: runEnv, encoding: 'utf8' });
const success = result => assert.equal(result.status, 0, result.stdout + result.stderr);
for (const cwd of [workspace, unrelated]) {
success(run('git', ['init', '-q'], cwd));
writeFileSync(join(cwd, 'Package.resolved'), '{"pins":[]}\n');
success(run('git', ['add', 'Package.resolved'], cwd));
success(run('git', ['-c', 'user.name=Fixture', '-c', 'user.email=fixture@example.invalid',
'-c', 'commit.gpgsign=false', 'commit', '-qm', 'fixture'], cwd));
}
env.GIT_TEST_ASSUME_DIFFERENT_OWNER = '1';
const before = run('git', ['diff', '--exit-code', '--', 'Package.resolved']);
assert.equal(before.status, 129, 'fixture must reproduce the failing release diff');
assert.match(run('git', ['status', '--short']).stderr, /dubious ownership/);

// Execute the production shell without installing packages or resolving real dependencies.
for (const tool of ['apt-get', 'swift']) {
writeFileSync(join(tools, tool), '#!/bin/bash\nexit 0\n', { mode: 0o755 });
}
const caller = join(tools, 'caller');
writeFileSync(caller, '#!/bin/bash\nset -eu\ngit diff --exit-code -- Package.resolved\n', { mode: 0o755 });
env.GENERATE_VERSION_SCRIPT = caller;
env.PREPARE_SCRIPT = caller;
success(run('bash', ['-c', prepare]));
assert.deepEqual(run('git', ['config', '--global', '--get-all', 'safe.directory']).stdout.trim().split('\n'),
[workspace], 'trust must name only the exact checkout');
success(run('git', ['diff', '--exit-code', '--', 'Package.resolved']));
writeFileSync(join(workspace, 'Package.resolved'), '{"pins":["changed"]}\n');
assert.equal(run('git', ['diff', '--exit-code', '--', 'Package.resolved']).status, 1,
'lockfile drift must still fail');
const other = run('git', ['diff', '--exit-code', '--', 'Package.resolved'], unrelated);
assert.notEqual(other.status, 0, 'unrelated checkouts must remain untrusted');
assert.match(run('git', ['status', '--short'], unrelated).stderr, /dubious ownership/);
} finally {
rmSync(root, { recursive: true, force: true });
}
console.log('Swift Linux checkout ownership regression passed');
1 change: 1 addition & 0 deletions scripts/validate-workflows.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ node scripts/test-checksum-collisions.mjs
node scripts/test-build-artifact-matrix.mjs
node scripts/test-split-host-build.mjs
node scripts/test-swift-cli-workflow.mjs
node scripts/test-swift-linux-git-ownership.mjs
node scripts/test-swift-cli-options.mjs
node scripts/test-swift-keychain-lifecycle.mjs
node scripts/test-swift-draft-isolation.mjs
Expand Down
Loading