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
14 changes: 4 additions & 10 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
name: CodeQL

on:
push:
branches: [main]
pull_request:
paths:
- "Sources/**"
- "Engine/**"
- "Package.swift"
- "Package.resolved"
- ".github/workflows/codeql.yml"
schedule:
- cron: "23 8 * * 2"
Expand All @@ -17,15 +12,14 @@ permissions:
contents: read
security-events: write

concurrency:
group: codeql-${{ github.ref }}-${{ matrix.language }}
cancel-in-progress: true

jobs:
analyze:
name: Analyze (${{ matrix.language }})
runs-on: macos-14
timeout-minutes: 55
concurrency:
group: codeql-${{ github.ref }}-${{ matrix.language }}
cancel-in-progress: true
strategy:
fail-fast: false
matrix:
Expand Down
39 changes: 32 additions & 7 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@ name: Quality

on:
pull_request:
push:
branches:
- main
workflow_dispatch:

permissions:
Expand Down Expand Up @@ -70,9 +67,25 @@ jobs:
runner: macos-15-intel
developer_dir: /Applications/Xcode.app/Contents/Developer
lane: compatibility
# Keep the required matrix check names for branch protection, but do not
# reserve two macOS machines just to acknowledge prose/appcast-only work.
runs-on: ${{ needs.changes.outputs.tests == 'true' && matrix.runner || 'ubuntu-latest' }}
# Keep the required matrix check names for branch protection. The complete
# suite runs on Apple silicon for code changes; reserve Intel only for
# engine/compatibility or release-chain changes that can affect that slice.
runs-on: >-
${{
(
(
matrix.lane == 'full' &&
needs.changes.outputs.tests == 'true'
) || (
matrix.lane == 'compatibility' &&
(
needs.changes.outputs.compatibility == 'true' ||
needs.changes.outputs.release_chain == 'true'
)
)
)
&& matrix.runner || 'ubuntu-latest'
}}
timeout-minutes: 35
env:
DEVELOPER_DIR: ${{ matrix.developer_dir }}
Expand All @@ -82,7 +95,19 @@ jobs:
# every surface and enforce geometry/content invariants; reviewed pixel
# hashes remain strict on the macOS family that produced the baseline.
SWAN_SONG_ALLOW_UI_BASELINE_PLATFORM_MISMATCH: "1"
RUN_TESTS: ${{ needs.changes.outputs.tests }}
RUN_TESTS: >-
${{
(
matrix.lane == 'full' &&
needs.changes.outputs.tests == 'true'
) || (
matrix.lane == 'compatibility' &&
(
needs.changes.outputs.compatibility == 'true' ||
needs.changes.outputs.release_chain == 'true'
)
)
}}
steps:
- name: Confirm fast pull-request lane
if: env.RUN_TESTS != 'true'
Expand Down
36 changes: 36 additions & 0 deletions Scripts/selftest-ci-change-classifier.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ set -euo pipefail

script_dir=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)
classifier="$script_dir/classify-ci-changes.sh"
quality_workflow="$script_dir/../.github/workflows/quality.yml"
codeql_workflow="$script_dir/../.github/workflows/codeql.yml"

fail() {
echo "CI workflow policy failed: $1" >&2
exit 1
}

assert_classification() {
local name=$1
Expand Down Expand Up @@ -80,4 +87,33 @@ assert_classification \
'av=true' \
'tests=true')"

grep -Eq '^ pull_request:' "$quality_workflow" ||
fail "Quality must run on pull requests"
grep -Eq '^ workflow_dispatch:' "$quality_workflow" ||
fail "Quality must retain the manual full-validation lane"
if grep -Eq '^ push:' "$quality_workflow"; then
fail "Quality must not repeat protected pull-request checks after a main merge"
fi
grep -Fq "needs.changes.outputs.compatibility == 'true'" "$quality_workflow" ||
fail "the Intel lane must cover compatibility changes"
grep -Fq "needs.changes.outputs.release_chain == 'true'" "$quality_workflow" ||
fail "the Intel lane must cover release-chain changes"

grep -Eq '^ schedule:' "$codeql_workflow" ||
fail "CodeQL must retain its weekly scan"
grep -Eq '^ workflow_dispatch:' "$codeql_workflow" ||
fail "CodeQL must retain explicit dispatch"
if grep -Eq '^ push:' "$codeql_workflow"; then
fail "CodeQL must not scan every protected main merge"
fi
# GitHub expressions are intentionally matched literally.
# shellcheck disable=SC2016
if sed -n '/^concurrency:/,/^jobs:/p' "$codeql_workflow" |
grep -Fq '${{ matrix.language }}'; then
fail "CodeQL cannot use the job matrix from top-level concurrency"
fi
# shellcheck disable=SC2016
grep -Fq 'group: codeql-${{ github.ref }}-${{ matrix.language }}' "$codeql_workflow" ||
fail "CodeQL matrix jobs must cancel only matching in-progress scans"

echo "PASS CI change classifier"
11 changes: 7 additions & 4 deletions docs/RELEASE_PROCESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,13 @@ not routine release metadata.
local automation launcher to disable Keychain lookup.
4. Run the engine, Swift, app runtime, UI, translation, architecture, payload,
embedded-Sparkle-framework, and personally owned acceptance lanes
appropriate to the release. After building the app, verify the feed/key,
signed-feed and pre-extraction requirements, privacy defaults, build-number
policy, pinned framework, updater helper, XPC services, bundle identities,
symbolic-link structure, and absence of game/firmware-like payloads:
appropriate to the release. First, manually dispatch the **Quality**
workflow on `main` and require its complete Apple-silicon, Intel, and
universal release-preflight lanes to pass. After building the app, verify
the feed/key, signed-feed and pre-extraction requirements, privacy defaults,
build-number policy, pinned framework, updater helper, XPC services, bundle
identities, symbolic-link structure, and absence of game/firmware-like
payloads:

```sh
./Scripts/check-sparkle-configuration.sh .build/app/SwanSong.app
Expand Down
38 changes: 24 additions & 14 deletions docs/wiki/Build-and-Test.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,18 +153,20 @@ the separate live-engine invocation is mandatory for release evidence.
## CI lanes

Application pull requests run the complete Swift/XCTest and UI snapshot suite
once on the macOS 14 Apple-silicon runner. A macOS 15 Intel runner simultaneously
compiles the native engine/library compatibility target and verifies its x86_64
Mach-O identity. The hosted Intel image does not reliably permit ad-hoc
standalone Swift executables, so runtime proof stays on Apple silicon. The
Apple-silicon suite enables published Homebrew production enforcement in that
same test process instead of rebuilding the test target for a duplicate pass.
once on the macOS 14 Apple-silicon runner. Engine, dependency, packaging, and
release-chain changes also reserve a macOS 15 Intel runner to compile the native
engine/library compatibility target and verify its x86_64 Mach-O identity.
Ordinary app and documentation changes do not reserve an Intel Mac. The hosted
Intel image does not reliably permit ad-hoc standalone Swift executables, so
runtime proof stays on Apple silicon. The Apple-silicon suite enables published
Homebrew production enforcement in that same test process instead of rebuilding
the test target for a duplicate pass.

Prose-only and signed-appcast-only pull requests keep the same branch-protected
test names but finish after the deterministic change-classifier self-test. They
do not download the SDK or rebuild the unchanged app on two Macs. Any source,
test, script, package, workflow, or configuration change restores the full
lanes automatically; pushes to `main` and manual runs are always full.
test, script, package, workflow, or configuration change restores the affected
lanes automatically.

The required release-preflight check is impact-aware. It finishes immediately
when a change cannot affect the packaged app or live engine. Packaging, updater,
Expand All @@ -179,14 +181,22 @@ release-sensitive path there whenever a new packaging or live-runtime boundary
is introduced. The branch-protected `Release preflight` check must remain
present even when its expensive work is unnecessary.

Pushes to `main` and manual workflow runs retain the complete XCTest suite on
the Intel runner, release-chain tamper and rollback tests, bundled SDK
Protected merges do not repeat the same three macOS jobs after their exact
content has passed the required pull-request checks. A manually dispatched
Quality run remains the explicit complete lane: it retains the full XCTest
suite on Intel, release-chain tamper and rollback tests, bundled SDK
materialization, the 360-frame compatibility matrix, guarded Translation Lab
automation, the 60-second CI soak, and complete universal app inspection
including every Intel slice. The full release standard therefore stays intact;
only the feedback loop used while developing a change becomes selective. UI
snapshots remain part of the complete XCTest suite and are not repeated in the
separate release-preflight job.
including every Intel slice. Run that lane when preparing a release or when a
maintainer needs fresh whole-tree evidence. UI snapshots remain part of the
complete XCTest suite and are not repeated in the separate release-preflight
job.

CodeQL runs on its weekly schedule and on explicit dispatch. Editing the CodeQL
workflow itself also exercises both language jobs in the pull request so an
invalid workflow cannot land silently. Dependency review remains attached to
pull requests, while sanitizers and fuzzing stay on their separate weekly
schedule.

The shared SwiftPM wrapper disables login-keychain credential lookup in CI and
uses only `Package.resolved`. Set `SWAN_SWIFTPM_DISABLE_KEYCHAIN=1` for the same
Expand Down
6 changes: 4 additions & 2 deletions docs/wiki/Release-Gates.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ network boundaries.
2. Confirm bundle identifier `com.regionallyfamous.swansong`, minimum macOS 14,
universal `arm64` + `x86_64` policy, pinned Sparkle dependency, production
feed URL, tracked public update key, and disabled system profiling.
3. Start from a clean tree and create the exact signed tag `vX.Y.Z`.
4. Recreate the pinned, patched, firmware-free ares source from its lock file.
3. Manually dispatch the **Quality** workflow on `main` and require the complete
Apple-silicon, Intel, and universal release-preflight lanes to pass.
4. Start from a clean tree and create the exact signed tag `vX.Y.Z`.
5. Recreate the pinned, patched, firmware-free ares source from its lock file.
Materialize the exact Sparkle source from its separate lock and include its
license, package manifest, and public header without Git metadata.

Expand Down
Loading