diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 460c414..9688e5e 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -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" @@ -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: diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index a6048ce..34c09bd 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -2,9 +2,6 @@ name: Quality on: pull_request: - push: - branches: - - main workflow_dispatch: permissions: @@ -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 }} @@ -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' diff --git a/Scripts/selftest-ci-change-classifier.sh b/Scripts/selftest-ci-change-classifier.sh index e6ab524..39c8fd7 100755 --- a/Scripts/selftest-ci-change-classifier.sh +++ b/Scripts/selftest-ci-change-classifier.sh @@ -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 @@ -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" diff --git a/docs/RELEASE_PROCESS.md b/docs/RELEASE_PROCESS.md index dd32a40..e7e2121 100644 --- a/docs/RELEASE_PROCESS.md +++ b/docs/RELEASE_PROCESS.md @@ -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 diff --git a/docs/wiki/Build-and-Test.md b/docs/wiki/Build-and-Test.md index c965e32..564494f 100644 --- a/docs/wiki/Build-and-Test.md +++ b/docs/wiki/Build-and-Test.md @@ -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, @@ -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 diff --git a/docs/wiki/Release-Gates.md b/docs/wiki/Release-Gates.md index 46a9c5a..2191e94 100644 --- a/docs/wiki/Release-Gates.md +++ b/docs/wiki/Release-Gates.md @@ -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.