492 lines
16 KiB
YAML
492 lines
16 KiB
YAML
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
|