name: CI on: push: branches: - main pull_request: permissions: contents: read jobs: lint: name: Lint 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: Format Check run: | unformatted="$(find cmd internal test scripts/policy -name '*.go' -print0 | xargs -0r gofmt -l)" test -z "$unformatted" || (printf '%s\n' "$unformatted" && exit 1) - name: Go Vet run: go vet ./... # golangci-lint temporarily disabled: v1.64.8 built with Go 1.24 is incompatible with Go 1.25 # - name: golangci-lint # uses: golangci/golangci-lint-action@v6 # with: # version: v1.64.8 # args: ./... test: name: Test runs-on: ubuntu-latest timeout-minutes: 20 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: Build run: make build - name: Test with Race Detection # The registry-first final-delivery gate validates all public commands # and the complete generated Catalog under the race detector. Keep the # package timeout aligned with the macOS race job so the Linux runner's # five-minute default does not expire while that gate is still making # progress. run: go test -v -race -count=1 -timeout=10m ./cmd/... ./internal/... - name: Test release scripts run: go test -v -count=1 -timeout=5m ./test/scripts test-darwin: name: Test (macOS auth/keychain) 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) 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 diagnostics run: go test -v -count=1 -timeout=5m ./internal/app -run '^TestAuth(MigrateKeychain|StatusDiagnosticReportsCiphertextKeyMismatch)' coverage-darwin: name: Coverage (macOS) 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) 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: name: Coverage runs-on: ubuntu-latest timeout-minutes: 30 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: Build run: make build - 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 current and baseline unit tests with coverage run: | set -eu go test -count=1 -p 1 -coverprofile=coverage.txt -covermode=atomic ./ ./cmd/... ./internal/... ./skills/... 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/... 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/... ) go tool cover -func=coverage.txt - name: Enforce coverage gate env: COVERAGE_TARGET: "80" COVERAGE_ENFORCE_OVERALL: "false" run: COVERAGE_ADDITIONAL_PROFILE=coverage-shortcut.txt make coverage-gate BASE_REF="$COVERAGE_BASE_REF" - name: Generate coverage report run: go tool cover -html=coverage.txt -o coverage.html - name: Upload coverage artifact 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 Check 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: Build run: make build - name: Policy run: make policy interface-integrity: name: Interface Integrity runs-on: ubuntu-latest timeout-minutes: 15 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: Build run: make build - name: Resolve authoritative compatibility merge-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 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 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 run: make schema-compatibility BASE_REF="$COMPATIBILITY_BASE_REF" - name: Check skill command references run: make skill-command-integrity cli-smoke: name: CLI Smoke 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: Build run: make build - name: Check public top-level commands run: make cli-smoke mock-mcp-smoke: name: Mock MCP Smoke 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: Check HTTP and stdio MCP transport run: make mock-mcp-smoke edition-tests: name: Edition Contract Tests 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: Run edition contract tests run: go test -v -count=1 ./pkg/editiontest/... ci-gate: name: CI Gate needs: - lint - test - test-darwin - test-windows - coverage - coverage-darwin - coverage-windows - policy - interface-integrity - cli-smoke - mock-mcp-smoke - edition-tests if: ${{ always() }} runs-on: ubuntu-latest timeout-minutes: 5 permissions: {} steps: - name: Verify required checks env: LINT_RESULT: ${{ needs.lint.result }} TEST_RESULT: ${{ needs.test.result }} TEST_DARWIN_RESULT: ${{ needs.test-darwin.result }} TEST_WINDOWS_RESULT: ${{ needs.test-windows.result }} COVERAGE_RESULT: ${{ needs.coverage.result }} COVERAGE_DARWIN_RESULT: ${{ needs.coverage-darwin.result }} COVERAGE_WINDOWS_RESULT: ${{ needs.coverage-windows.result }} POLICY_RESULT: ${{ needs.policy.result }} INTERFACE_INTEGRITY_RESULT: ${{ needs.interface-integrity.result }} CLI_SMOKE_RESULT: ${{ needs.cli-smoke.result }} MOCK_MCP_SMOKE_RESULT: ${{ needs.mock-mcp-smoke.result }} EDITION_TESTS_RESULT: ${{ needs.edition-tests.result }} run: | failed=0 for check in \ "Lint:$LINT_RESULT" \ "Test:$TEST_RESULT" \ "Test (macOS auth/keychain):$TEST_DARWIN_RESULT" \ "Test (Windows):$TEST_WINDOWS_RESULT" \ "Coverage:$COVERAGE_RESULT" \ "Coverage (macOS):$COVERAGE_DARWIN_RESULT" \ "Coverage (Windows):$COVERAGE_WINDOWS_RESULT" \ "Policy Check:$POLICY_RESULT" \ "Interface Integrity:$INTERFACE_INTEGRITY_RESULT" \ "CLI Smoke:$CLI_SMOKE_RESULT" \ "Mock MCP Smoke:$MOCK_MCP_SMOKE_RESULT" \ "Edition Contract Tests:$EDITION_TESTS_RESULT" do name="${check%%:*}" result="${check#*:}" printf '%s: %s\n' "$name" "$result" if [ "$result" != "success" ]; then failed=1 fi done test "$failed" -eq 0 notify-downstream: name: Notify Wukong Overlay needs: [ci-gate] runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' && github.event_name == 'push' permissions: {} steps: - name: Trigger downstream CI run: | # Trigger internal GitLab CI pipeline via webhook. # WUKONG_TRIGGER_TOKEN is a repository secret. if [ -n "${{ secrets.WUKONG_TRIGGER_TOKEN }}" ]; then curl --fail --silent --show-error \ -X POST \ -F "token=${{ secrets.WUKONG_TRIGGER_TOKEN }}" \ -F "ref=main" \ -F "variables[UPSTREAM_SHA]=${{ github.sha }}" \ "${{ secrets.WUKONG_TRIGGER_URL }}" echo "Downstream CI triggered." else echo "No WUKONG_TRIGGER_TOKEN configured, skipping downstream notification." fi