name: CI on: push: branches: - main pull_request: permissions: contents: read concurrency: group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: lint: name: Lint runs-on: ubuntu-latest timeout-minutes: 10 permissions: contents: read pull-requests: read outputs: changelog_only: ${{ steps.classify.outputs.changelog_only }} changelog_changed: ${{ steps.classify.outputs.changelog_changed }} platform_sensitive: ${{ steps.classify.outputs.platform_sensitive }} steps: - name: Classify pull request scope id: classify uses: actions/github-script@v7 with: script: | let changelogOnly = false; let changelogChanged = false; let platformSensitive = context.eventName === 'push'; let files = []; if (context.eventName === 'pull_request') { const expectedHead = context.payload.pull_request.head.sha; const expectedBase = context.payload.pull_request.base.sha; const assertCurrentRevision = async (phase) => { const { data: pull } = await github.rest.pulls.get({ owner: context.repo.owner, repo: context.repo.repo, pull_number: context.issue.number, }); if (pull.head.sha !== expectedHead || pull.base.sha !== expectedBase) { throw new Error( `Pull request revision changed during ${phase}: ` + `expected base/head ${expectedBase}/${expectedHead}, ` + `got ${pull.base.sha}/${pull.head.sha}` ); } return pull; }; const before = await assertCurrentRevision('pre-classification'); files = await github.paginate(github.rest.pulls.listFiles, { owner: context.repo.owner, repo: context.repo.repo, pull_number: context.issue.number, per_page: 100, }); const after = await assertCurrentRevision('post-classification'); if ( before.changed_files !== files.length || after.changed_files !== files.length ) { throw new Error( `Pull request file list is incomplete: API reports ` + `${after.changed_files} changed files, pagination returned ${files.length}` ); } changelogOnly = files.length === 1 && files[0].filename === 'CHANGELOG.md' && files[0].status === 'modified' && !files[0].previous_filename; changelogChanged = files.some( ({ filename, previous_filename }) => filename === 'CHANGELOG.md' || previous_filename === 'CHANGELOG.md' ); const isPlatformSensitive = (filename) => typeof filename === 'string' && ( filename.startsWith('internal/auth/') || filename.startsWith('internal/keychain/') || /_(darwin|windows|linux|unix)\.go$/.test(filename) || filename.startsWith('scripts/release/') || filename.startsWith('scripts/install') || filename.startsWith('Formula/') || filename.startsWith('build/npm/') || filename === '.goreleaser.yaml' || filename === '.github/workflows/release.yml' ); platformSensitive = files.some( ({ filename, previous_filename }) => isPlatformSensitive(filename) || isPlatformSensitive(previous_filename) ); } core.setOutput('changelog_only', String(changelogOnly)); core.setOutput('changelog_changed', String(changelogChanged)); core.setOutput('platform_sensitive', String(platformSensitive)); await core.summary .addHeading('Code Admission scope') .addRaw(`- Event: \`${context.eventName}\`\n`) .addRaw(`- Exact modified CHANGELOG only: \`${changelogOnly}\`\n`) .addRaw(`- CHANGELOG touched: \`${changelogChanged}\`\n`) .addRaw(`- Native-platform risk paths touched: \`${platformSensitive}\`\n`) .addRaw(`- Changed files: \`${files.length}\`\n`) .write(); - name: Record CHANGELOG-only fast path if: steps.classify.outputs.changelog_only == 'true' run: echo "Lint is satisfied by the trusted CHANGELOG-only Policy path." >> "$GITHUB_STEP_SUMMARY" - name: Check out repository if: steps.classify.outputs.changelog_only != 'true' uses: actions/checkout@v4 - name: Set up Go if: steps.classify.outputs.changelog_only != 'true' uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Verify Test Package Plan if: steps.classify.outputs.changelog_only != 'true' run: make test-plan - name: Format Check if: steps.classify.outputs.changelog_only != 'true' run: make format-check - name: Go Vet if: steps.classify.outputs.changelog_only != 'true' run: go vet ./... - name: Check GitHub Actions workflows if: steps.classify.outputs.changelog_only != 'true' run: go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 test-race: name: "Test (race: ${{ matrix.shard }})" needs: lint if: ${{ needs.lint.outputs.changelog_only != 'true' }} runs-on: ubuntu-latest timeout-minutes: 15 strategy: fail-fast: false matrix: shard: - app - generators - helpers - remaining steps: - name: Check out repository uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Build if: ${{ matrix.shard == 'remaining' }} run: make build - name: Test shard with Race Detection shell: bash env: DWS_PACKAGE_VERSION: 0.0.0-test TEST_SHARD: ${{ matrix.shard }} run: | set -euo pipefail package_output="$(./scripts/ci/test-packages.sh list "$TEST_SHARD")" test -n "$package_output" mapfile -t packages <<< "$package_output" test "${#packages[@]}" -gt 0 go test -v -race -count=1 -timeout=10m "${packages[@]}" test-release-scripts: name: Test (release scripts) needs: lint if: ${{ needs.lint.outputs.changelog_only != 'true' }} runs-on: ubuntu-latest timeout-minutes: 15 steps: - name: Check out repository uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Install archive tooling run: sudo apt-get update && sudo apt-get install -y zip unzip - name: Test release scripts shell: bash env: DWS_PACKAGE_VERSION: 0.0.0-test run: | set -euo pipefail package_output="$(./scripts/ci/test-packages.sh list release-scripts)" test -n "$package_output" mapfile -t packages <<< "$package_output" test "${#packages[@]}" -gt 0 go test -v -count=1 -timeout=10m "${packages[@]}" test-cross-platform: name: Test (cross-platform compile) needs: lint if: ${{ needs.lint.outputs.changelog_only != 'true' }} runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Check out repository uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Compile supported operating systems shell: bash run: | set -eu for target in darwin/amd64 darwin/arm64 windows/amd64 windows/arm64; do goos="${target%/*}" goarch="${target#*/}" output="$RUNNER_TEMP/dws-${goos}-${goarch}" if [ "$goos" = windows ]; then output="${output}.exe" fi printf 'compile %s/%s\n' "$goos" "$goarch" CGO_ENABLED=0 GOOS="$goos" GOARCH="$goarch" \ go build -o "$output" ./cmd done test: name: Test needs: - lint - test-race - test-release-scripts - test-cross-platform - test-darwin - test-windows if: ${{ always() && needs.lint.result == 'success' }} runs-on: ubuntu-latest timeout-minutes: 5 permissions: {} steps: - name: Verify test shards env: CHANGELOG_ONLY: ${{ needs.lint.outputs.changelog_only }} PLATFORM_SENSITIVE: ${{ needs.lint.outputs.platform_sensitive }} RACE_RESULT: ${{ needs.test-race.result }} RELEASE_SCRIPTS_RESULT: ${{ needs.test-release-scripts.result }} CROSS_PLATFORM_RESULT: ${{ needs.test-cross-platform.result }} DARWIN_RESULT: ${{ needs.test-darwin.result }} WINDOWS_RESULT: ${{ needs.test-windows.result }} run: | failed=0 if [ "$CHANGELOG_ONLY" = true ]; then for shard in \ "race shards:$RACE_RESULT" \ "release scripts:$RELEASE_SCRIPTS_RESULT" \ "cross-platform compile:$CROSS_PLATFORM_RESULT" \ "macOS native:$DARWIN_RESULT" \ "Windows native:$WINDOWS_RESULT" do name="${shard%%:*}" result="${shard#*:}" printf '%s: %s\n' "$name" "$result" if [ "$result" != skipped ]; then failed=1 fi done test "$failed" -eq 0 exit fi for shard in \ "race shards:$RACE_RESULT" \ "release scripts:$RELEASE_SCRIPTS_RESULT" \ "cross-platform compile:$CROSS_PLATFORM_RESULT" do name="${shard%%:*}" result="${shard#*:}" printf '%s: %s\n' "$name" "$result" if [ "$result" != "success" ]; then failed=1 fi done native_expected=skipped if [ "$PLATFORM_SENSITIVE" = true ]; then native_expected=success fi for native in \ "macOS native:$DARWIN_RESULT" \ "Windows native:$WINDOWS_RESULT" do name="${native%%:*}" result="${native#*:}" printf '%s: %s\n' "$name" "$result" if [ "$result" != "$native_expected" ]; then failed=1 fi done test "$failed" -eq 0 test-darwin: name: Test (macOS auth/keychain) needs: lint if: ${{ needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.platform_sensitive == 'true' }} runs-on: macos-latest timeout-minutes: 15 steps: - name: Check out repository uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Test macOS auth and Keychain paths with Race Detection run: go test -v -race -count=1 -timeout=10m ./internal/keychain ./internal/auth ./internal/app test-windows: name: Test (Windows) needs: lint if: ${{ needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.platform_sensitive == 'true' }} runs-on: windows-latest timeout-minutes: 15 steps: - name: Check out repository uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Build Windows CLI run: go build -o dws.exe ./cmd - name: Test Windows auth and DPAPI packages run: go test -v -count=1 -timeout=10m ./internal/keychain ./internal/auth - name: Test Windows auth migration and portable auth diagnostics run: go test -v -count=1 -timeout=5m ./internal/app -run '^Test(CrossPlatformCoverage)?Auth(MigrateKeychain|StatusDiagnosticReportsCiphertextKeyMismatch|ExportRejectsWindowsDPAPIBackend|ImportRejectsWindowsDPAPIBackend)' coverage-darwin: name: Coverage (macOS) needs: lint if: ${{ needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.platform_sensitive == 'true' }} runs-on: macos-latest timeout-minutes: 20 steps: - name: Check out repository uses: actions/checkout@v4 with: fetch-depth: 0 ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} - name: Set up Go uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Resolve authoritative coverage base env: PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} PUSH_BEFORE_SHA: ${{ github.event.before }} run: | set -eu base_ref="$PUSH_BEFORE_SHA" if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then base_ref="$(git merge-base "$PR_HEAD_SHA" "$PR_BASE_SHA")" fi if [ -z "$base_ref" ] || [ "$base_ref" = "0000000000000000000000000000000000000000" ]; then base_ref="$(git rev-parse HEAD^)" fi git rev-parse --verify "${base_ref}^{commit}" >/dev/null echo "COVERAGE_BASE_REF=$base_ref" >> "$GITHUB_ENV" - name: Run and enforce macOS changed-code coverage run: make coverage-gate-platform BASE_REF="$COVERAGE_BASE_REF" PROFILE=coverage-darwin.txt - name: Upload macOS coverage artifact uses: actions/upload-artifact@v4 with: name: coverage-darwin path: coverage-darwin.txt coverage-windows: name: Coverage (Windows) needs: lint if: ${{ needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.platform_sensitive == 'true' }} runs-on: windows-latest timeout-minutes: 20 steps: - name: Check out repository uses: actions/checkout@v4 with: fetch-depth: 0 ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} - name: Set up Go uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Resolve authoritative coverage base shell: bash env: PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} PUSH_BEFORE_SHA: ${{ github.event.before }} run: | set -eu base_ref="$PUSH_BEFORE_SHA" if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then base_ref="$(git merge-base "$PR_HEAD_SHA" "$PR_BASE_SHA")" fi if [ -z "$base_ref" ] || [ "$base_ref" = "0000000000000000000000000000000000000000" ]; then base_ref="$(git rev-parse HEAD^)" fi git rev-parse --verify "${base_ref}^{commit}" >/dev/null echo "COVERAGE_BASE_REF=$base_ref" >> "$GITHUB_ENV" - name: Run and enforce Windows changed-code coverage shell: bash run: ./scripts/policy/run-platform-coverage-gate.sh --base-ref "$COVERAGE_BASE_REF" --profile coverage-windows.txt - name: Upload Windows coverage artifact uses: actions/upload-artifact@v4 with: name: coverage-windows path: coverage-windows.txt coverage-current: name: Coverage (current) needs: lint if: ${{ needs.lint.outputs.changelog_only != 'true' }} runs-on: ubuntu-latest timeout-minutes: 20 steps: - name: Check out repository uses: actions/checkout@v4 with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} - name: Set up Go uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Install archive tooling run: sudo apt-get update && sudo apt-get install -y zip unzip - name: Build run: make build - name: Run current unit tests with coverage run: | go test -count=1 -p 1 -coverprofile=coverage.txt -covermode=atomic ./ ./cmd/... ./internal/... ./skills/... go tool cover -func=coverage.txt - name: Upload current coverage profile uses: actions/upload-artifact@v4 with: name: coverage-current-profile path: coverage.txt retention-days: 1 coverage-supporting: name: Coverage (supporting) needs: lint if: ${{ needs.lint.outputs.changelog_only != 'true' }} runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Check out repository uses: actions/checkout@v4 with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} - name: Set up Go uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Install archive tooling run: sudo apt-get update && sudo apt-get install -y zip unzip - name: Run policy and shortcut coverage run: | go test -count=1 -coverprofile=coverage-policy.txt -covermode=atomic ./pkg/... ./scripts/policy/... go test -count=1 \ -run '^(TestAllShortcuts|TestCrossPlatformCoverage)' \ -coverpkg=./internal/app,./internal/helpers,./internal/shortcut/... \ -coverprofile=coverage-shortcut.txt \ -covermode=atomic \ ./internal/app ./internal/helpers ./internal/shortcut/... - name: Upload supporting coverage profiles uses: actions/upload-artifact@v4 with: name: coverage-supporting-profiles path: | coverage-policy.txt coverage-shortcut.txt retention-days: 1 coverage-baseline: name: Coverage (baseline) needs: lint if: ${{ needs.lint.outputs.changelog_only != 'true' }} runs-on: ubuntu-latest timeout-minutes: 20 steps: - name: Check out repository uses: actions/checkout@v4 with: fetch-depth: 0 ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} - name: Set up Go uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Install archive tooling run: sudo apt-get update && sudo apt-get install -y zip unzip - name: Resolve authoritative coverage base env: PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} PUSH_BEFORE_SHA: ${{ github.event.before }} run: | set -eu base_ref="$PUSH_BEFORE_SHA" if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then base_ref="$(git merge-base "$PR_HEAD_SHA" "$PR_BASE_SHA")" fi if [ -z "$base_ref" ] || [ "$base_ref" = "0000000000000000000000000000000000000000" ]; then base_ref="$(git rev-parse HEAD^)" fi git rev-parse --verify "${base_ref}^{commit}" >/dev/null echo "COVERAGE_BASE_REF=$base_ref" >> "$GITHUB_ENV" - name: Run baseline unit tests with coverage run: | set -eu base_worktree="$(mktemp -d "${RUNNER_TEMP}/dws-coverage-base.XXXXXX")" rmdir "$base_worktree" cleanup() { git worktree remove --force "$base_worktree" >/dev/null 2>&1 || true } trap cleanup EXIT git worktree add --detach "$base_worktree" "$COVERAGE_BASE_REF" ( cd "$base_worktree" go test -count=1 \ -p 1 \ -coverprofile="$GITHUB_WORKSPACE/coverage-base.txt" \ -covermode=atomic \ ./ ./cmd/... ./internal/... ./skills/... ) - name: Upload baseline coverage profile uses: actions/upload-artifact@v4 with: name: coverage-baseline-profile path: coverage-base.txt retention-days: 1 coverage: name: Coverage needs: - lint - coverage-current - coverage-supporting - coverage-baseline - coverage-darwin - coverage-windows if: ${{ always() && needs.lint.result == 'success' }} runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Verify coverage profile jobs env: CHANGELOG_ONLY: ${{ needs.lint.outputs.changelog_only }} PLATFORM_SENSITIVE: ${{ needs.lint.outputs.platform_sensitive }} CURRENT_RESULT: ${{ needs.coverage-current.result }} SUPPORTING_RESULT: ${{ needs.coverage-supporting.result }} BASELINE_RESULT: ${{ needs.coverage-baseline.result }} DARWIN_RESULT: ${{ needs.coverage-darwin.result }} WINDOWS_RESULT: ${{ needs.coverage-windows.result }} run: | failed=0 expected=success native_expected=skipped if [ "$CHANGELOG_ONLY" = true ]; then expected=skipped elif [ "$PLATFORM_SENSITIVE" = true ]; then native_expected=success fi for profile in \ "current:$CURRENT_RESULT" \ "supporting:$SUPPORTING_RESULT" \ "baseline:$BASELINE_RESULT" do name="${profile%%:*}" result="${profile#*:}" printf '%s: %s\n' "$name" "$result" if [ "$result" != "$expected" ]; then failed=1 fi done for native in \ "macOS native:$DARWIN_RESULT" \ "Windows native:$WINDOWS_RESULT" do name="${native%%:*}" result="${native#*:}" printf '%s: %s\n' "$name" "$result" if [ "$CHANGELOG_ONLY" = true ]; then native_expected=skipped fi if [ "$result" != "$native_expected" ]; then failed=1 fi done test "$failed" -eq 0 - name: Check out repository if: needs.lint.outputs.changelog_only != 'true' uses: actions/checkout@v4 with: fetch-depth: 0 ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} - name: Set up Go if: needs.lint.outputs.changelog_only != 'true' uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Resolve authoritative coverage base if: needs.lint.outputs.changelog_only != 'true' env: PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} PUSH_BEFORE_SHA: ${{ github.event.before }} run: | set -eu base_ref="$PUSH_BEFORE_SHA" if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then base_ref="$(git merge-base "$PR_HEAD_SHA" "$PR_BASE_SHA")" fi if [ -z "$base_ref" ] || [ "$base_ref" = "0000000000000000000000000000000000000000" ]; then base_ref="$(git rev-parse HEAD^)" fi git rev-parse --verify "${base_ref}^{commit}" >/dev/null echo "COVERAGE_BASE_REF=$base_ref" >> "$GITHUB_ENV" - name: Download current coverage profile if: needs.lint.outputs.changelog_only != 'true' uses: actions/download-artifact@v4 with: name: coverage-current-profile path: . - name: Download supporting coverage profiles if: needs.lint.outputs.changelog_only != 'true' uses: actions/download-artifact@v4 with: name: coverage-supporting-profiles path: . - name: Download baseline coverage profile if: needs.lint.outputs.changelog_only != 'true' uses: actions/download-artifact@v4 with: name: coverage-baseline-profile path: . - name: Enforce coverage gate if: needs.lint.outputs.changelog_only != 'true' env: COVERAGE_TARGET: "100" COVERAGE_ENFORCE_OVERALL: "false" COVERAGE_OVERALL_TOLERANCE: "0" run: COVERAGE_ADDITIONAL_DIFF_PROFILE=coverage-shortcut.txt make coverage-gate BASE_REF="$COVERAGE_BASE_REF" - name: Generate coverage report if: needs.lint.outputs.changelog_only != 'true' run: go tool cover -html=coverage.txt -o coverage.html - name: Upload coverage artifact if: needs.lint.outputs.changelog_only != 'true' uses: actions/upload-artifact@v4 with: name: coverage-report path: | coverage.txt coverage-base.txt coverage-policy.txt coverage-shortcut.txt coverage.html policy: name: Policy needs: lint runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Check out repository uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Go if: needs.lint.outputs.changelog_only != 'true' uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Verify pull request merge revision if: github.event_name == 'pull_request' env: PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | set -eu test "$(git rev-parse HEAD^1)" = "$PR_BASE_SHA" || { echo "checked-out merge first parent does not match event base" >&2 exit 1 } test "$(git rev-parse HEAD^2)" = "$PR_HEAD_SHA" || { echo "checked-out merge second parent does not match event head" >&2 exit 1 } - name: Validate changed CHANGELOG content if: github.event_name == 'pull_request' env: CLASSIFIED_CHANGELOG_CHANGED: ${{ needs.lint.outputs.changelog_changed }} CHANGELOG_ONLY: ${{ needs.lint.outputs.changelog_only }} PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} run: | set -eu merge_changelog_changed=false if git diff --no-ext-diff --find-renames --name-status \ "$PR_BASE_SHA" HEAD | awk -F '\t' ' { for (field = 2; field <= NF; field++) { if ($field == "CHANGELOG.md") found = 1 } } END { exit !found } ' then merge_changelog_changed=true fi test "$merge_changelog_changed" = "$CLASSIFIED_CHANGELOG_CHANGED" || { echo "Files API and synthetic merge tree disagree on CHANGELOG scope" >&2 exit 1 } if [ "$merge_changelog_changed" != true ]; then exit 0 fi mode=--content-only if [ "$CHANGELOG_ONLY" = true ]; then mode=--fast-path fi ./scripts/policy/check-changelog-pr.sh \ "$mode" "$PR_BASE_SHA" HEAD - name: Record CHANGELOG-only fast path if: needs.lint.outputs.changelog_only == 'true' run: | echo "Only the base-equivalent CHANGELOG validator ran; full Policy resumes on main." \ >> "$GITHUB_STEP_SUMMARY" - name: Build if: needs.lint.outputs.changelog_only != 'true' run: make build - name: Policy if: needs.lint.outputs.changelog_only != 'true' run: make policy interface-integrity: name: Interface Integrity needs: lint runs-on: ubuntu-latest timeout-minutes: 15 steps: - name: Check out repository if: needs.lint.outputs.changelog_only != 'true' uses: actions/checkout@v4 with: fetch-depth: 0 ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} - name: Set up Go if: needs.lint.outputs.changelog_only != 'true' uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Build if: needs.lint.outputs.changelog_only != 'true' run: make build - name: Resolve authoritative compatibility merge-base if: needs.lint.outputs.changelog_only != 'true' env: PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} PUSH_BEFORE_SHA: ${{ github.event.before }} run: | set -eu base_ref="$PUSH_BEFORE_SHA" if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then base_ref="$(git merge-base "$PR_HEAD_SHA" "$PR_BASE_SHA")" fi if [ -z "$base_ref" ] || [ "$base_ref" = "0000000000000000000000000000000000000000" ]; then base_ref="$(git rev-parse HEAD^)" fi git rev-parse --verify "${base_ref}^{commit}" >/dev/null stable_ref="$(git tag --merged "$base_ref" --list 'v[0-9]*' --sort=-version:refname | awk 'index($0, "-") == 0 { print; exit }')" if [ -z "$stable_ref" ]; then echo "No stable release tag is reachable from compatibility base $base_ref" >&2 exit 1 fi git rev-parse --verify "${stable_ref}^{commit}" >/dev/null echo "COMPATIBILITY_BASE_REF=$base_ref" >> "$GITHUB_ENV" echo "COMPATIBILITY_STABLE_REF=$stable_ref" >> "$GITHUB_ENV" - name: Check historical commands and help compatibility if: needs.lint.outputs.changelog_only != 'true' run: | make authoritative-interface-integrity \ BASE_REF="$COMPATIBILITY_BASE_REF" if [ "$(git rev-parse "${COMPATIBILITY_BASE_REF}^{commit}")" != "$(git rev-parse "${COMPATIBILITY_STABLE_REF}^{commit}")" ]; then make authoritative-interface-integrity \ BASE_REF="$COMPATIBILITY_STABLE_REF" fi - name: Check complete Schema compatibility if: needs.lint.outputs.changelog_only != 'true' run: make schema-compatibility BASE_REF="$COMPATIBILITY_BASE_REF" - name: Check skill command references if: needs.lint.outputs.changelog_only != 'true' run: make skill-command-integrity - name: Record CHANGELOG-only fast path if: needs.lint.outputs.changelog_only == 'true' run: echo "Interface Integrity is unaffected by an exact CHANGELOG-only diff." >> "$GITHUB_STEP_SUMMARY" cli-smoke: name: CLI Smoke needs: lint runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Check out repository if: needs.lint.outputs.changelog_only != 'true' uses: actions/checkout@v4 - name: Set up Go if: needs.lint.outputs.changelog_only != 'true' uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Build if: needs.lint.outputs.changelog_only != 'true' run: make build - name: Check public top-level commands if: needs.lint.outputs.changelog_only != 'true' run: make cli-smoke - name: Record CHANGELOG-only fast path if: needs.lint.outputs.changelog_only == 'true' run: echo "CLI Smoke is unaffected by an exact CHANGELOG-only diff." >> "$GITHUB_STEP_SUMMARY" mock-mcp-smoke: name: Mock MCP needs: lint runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Check out repository if: needs.lint.outputs.changelog_only != 'true' uses: actions/checkout@v4 - name: Set up Go if: needs.lint.outputs.changelog_only != 'true' uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Check HTTP and stdio MCP transport if: needs.lint.outputs.changelog_only != 'true' run: make mock-mcp-smoke - name: Record CHANGELOG-only fast path if: needs.lint.outputs.changelog_only == 'true' run: echo "Mock MCP is unaffected by an exact CHANGELOG-only diff." >> "$GITHUB_STEP_SUMMARY" edition-tests: name: Edition needs: lint runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Check out repository if: needs.lint.outputs.changelog_only != 'true' uses: actions/checkout@v4 - name: Set up Go if: needs.lint.outputs.changelog_only != 'true' uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Run edition contract tests if: needs.lint.outputs.changelog_only != 'true' run: go test -v -count=1 ./pkg/editiontest/... - name: Record CHANGELOG-only fast path if: needs.lint.outputs.changelog_only == 'true' run: echo "Edition is unaffected by an exact CHANGELOG-only diff." >> "$GITHUB_STEP_SUMMARY"