Files
dingtalk-workspace-cli/.github/workflows/ci.yml
T

1457 lines
59 KiB
YAML

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:
checks: read
contents: read
pull-requests: read
outputs:
changelog_only: ${{ steps.classify.outputs.changelog_only }}
changelog_changed: ${{ steps.classify.outputs.changelog_changed }}
docs_only: ${{ steps.classify.outputs.docs_only }}
full_suite: ${{ steps.classify.outputs.full_suite }}
release_sensitive: ${{ steps.classify.outputs.release_sensitive }}
interface_sensitive: ${{ steps.classify.outputs.interface_sensitive }}
mcp_sensitive: ${{ steps.classify.outputs.mcp_sensitive }}
edition_sensitive: ${{ steps.classify.outputs.edition_sensitive }}
platform_sensitive: ${{ steps.classify.outputs.platform_sensitive }}
steps:
- name: Classify revision scope
id: classify
uses: actions/github-script@v7
with:
script: |
let changelogOnly = false;
let changelogChanged = false;
let docsOnly = false;
let fullSuite = context.eventName === 'push';
let releaseSensitive = context.eventName === 'push';
let interfaceSensitive = context.eventName === 'push';
let mcpSensitive = context.eventName === 'push';
let editionSensitive = context.eventName === 'push';
let platformSensitive = context.eventName === 'push';
let files = [];
let fastPathTrust = 'not evaluated';
const isDocsOnly = (filename) =>
typeof filename === 'string' &&
(
filename === 'CHANGELOG.md' ||
filename === 'README.md' ||
filename === 'README_zh.md' ||
filename === 'CONTRIBUTING.md' ||
filename === 'SECURITY.md' ||
filename === 'CODE_OF_CONDUCT.md' ||
filename === 'LICENSE' ||
filename === 'NOTICE' ||
filename === '.github/PULL_REQUEST_TEMPLATE.md' ||
filename.startsWith('.github/ISSUE_TEMPLATE/') ||
filename.startsWith('docs/')
);
const isReleaseSensitive = (filename) =>
typeof filename === 'string' &&
(
filename.startsWith('scripts/release/') ||
filename.startsWith('scripts/install') ||
filename.startsWith('Formula/') ||
filename.startsWith('build/') ||
filename === '.goreleaser.yaml' ||
filename === '.github/workflows/release.yml' ||
filename === '.github/workflows/withdraw-release.yml'
);
const isPlatformSensitive = (filename) =>
typeof filename === 'string' &&
(
filename.startsWith('internal/auth/') ||
filename.startsWith('internal/keychain/') ||
/_(darwin|windows|linux|unix)(?:_[a-z0-9]+)*\.go$/.test(filename) ||
filename === '.github/workflows/ci.yml' ||
filename ===
'scripts/policy/run-platform-coverage-gate.sh'
);
const isNativeGoChange = ({ filename, previous_filename, patch }) => {
const paths = [filename, previous_filename].filter(
(value) => typeof value === 'string' && value !== ''
);
if (paths.some(isPlatformSensitive)) {
return true;
}
if (!paths.some((value) => value.endsWith('.go'))) {
return false;
}
if (typeof patch !== 'string') {
return true;
}
return (
/\/\/go:build[^\n]*(darwin|windows|linux|unix)/.test(patch) ||
/runtime\.GO(OS|ARCH)/.test(patch)
);
};
const isInterfaceSensitive = (filename) =>
typeof filename === 'string' &&
(
filename.startsWith('cmd/') ||
filename.startsWith('internal/app/') ||
filename.startsWith('internal/cli/') ||
filename.startsWith('internal/cobracmd/') ||
filename.startsWith('internal/helpers/') ||
filename.startsWith('internal/i18n/') ||
filename.startsWith('internal/interfacesnapshot/') ||
filename.startsWith('internal/ir/') ||
filename.startsWith('internal/shortcut/') ||
filename.startsWith('pkg/cmdutil/') ||
filename === 'skills_embed.go' ||
filename === 'test/fixtures/cli-interface-baseline.txt' ||
filename.startsWith('skills/')
);
const isMCPSensitive = (filename) =>
typeof filename === 'string' &&
(
filename.startsWith('internal/mcp') ||
filename.startsWith('internal/transport/') ||
filename.startsWith('internal/helpers/') ||
filename.startsWith('internal/app/') ||
filename.startsWith('test/mock_mcp/')
);
const isEditionSensitive = (filename) =>
typeof filename === 'string' &&
(
filename.startsWith('pkg/edition') ||
filename.startsWith('internal/edition') ||
filename === 'go.mod' ||
filename === 'go.sum'
);
const isHighRisk = (filename) =>
typeof filename === 'string' &&
(
isPlatformSensitive(filename) ||
isReleaseSensitive(filename) ||
filename.startsWith('.github/workflows/') ||
filename === '.github/actionlint.yaml' ||
filename.startsWith('scripts/') ||
filename.startsWith('verify/') ||
filename.startsWith('internal/generator/') ||
filename.startsWith('internal/cli/schema') ||
filename.startsWith('internal/interfacesnapshot/') ||
filename.startsWith('internal/app/upgrade') ||
filename.startsWith('internal/transport/') ||
filename.startsWith('internal/recovery/') ||
filename.startsWith('internal/syncdata/') ||
filename.includes('/testdata/') ||
filename.startsWith('testdata/') ||
filename.startsWith('test/fixtures/') ||
filename === 'Makefile' ||
filename === 'go.mod' ||
filename === 'go.sum'
);
const classifyFiles = (complete) => {
const paths = files.flatMap(({ filename, previous_filename }) =>
[filename, previous_filename].filter(
(value) => typeof value === 'string' && value !== ''
)
);
const structuralChange = files.some(
({ status, previous_filename }) =>
status === 'removed' || Boolean(previous_filename)
);
const goPackageShapeChange = files.some(
({ filename, previous_filename, status }) =>
['added', 'removed'].includes(status) &&
[filename, previous_filename].some(
(value) =>
typeof value === 'string' && value.endsWith('.go')
)
);
docsOnly =
complete &&
files.length > 0 &&
paths.length > 0 &&
paths.every(isDocsOnly);
releaseSensitive =
!complete || paths.some(isReleaseSensitive);
platformSensitive =
!complete || files.some(isNativeGoChange);
interfaceSensitive =
!complete ||
paths.some(isInterfaceSensitive) ||
paths.some(isEditionSensitive);
mcpSensitive =
!complete || paths.some(isMCPSensitive);
editionSensitive =
!complete || paths.some(isEditionSensitive);
fullSuite =
!complete ||
(
!docsOnly &&
(
context.eventName === 'push' ||
structuralChange ||
goPackageShapeChange ||
files.some(isNativeGoChange) ||
paths.some(isHighRisk)
)
);
};
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'
);
classifyFiles(true);
fastPathTrust = changelogOnly
? 'exact pull-request revision and synthetic merge policy'
: docsOnly
? 'documentation-only focused admission'
: fullSuite
? 'high-risk full admission'
: 'changed-package focused admission';
} else if (context.eventName === 'push') {
const expectedBefore = context.payload.before;
const expectedAfter = context.payload.after;
const fullCommit = /^[0-9a-f]{40}$/;
const zeroCommit = '0'.repeat(40);
if (
context.ref !== 'refs/heads/main' ||
context.payload.ref !== 'refs/heads/main' ||
context.payload.created !== false ||
context.payload.deleted !== false ||
context.payload.forced !== false ||
!fullCommit.test(expectedBefore || '') ||
!fullCommit.test(expectedAfter || '') ||
expectedBefore === zeroCommit ||
expectedAfter === zeroCommit ||
expectedAfter !== context.sha
) {
fastPathTrust =
'push identity is not a non-forced update of the existing main branch';
} else {
const { data: comparison } =
await github.rest.repos.compareCommitsWithBasehead({
owner: context.repo.owner,
repo: context.repo.repo,
basehead: `${expectedBefore}...${expectedAfter}`,
per_page: 100,
});
files = Array.isArray(comparison.files) ? comparison.files : [];
classifyFiles(files.length < 300);
const linearFromValidatedTip =
comparison.status === 'ahead' &&
comparison.merge_base_commit?.sha === expectedBefore &&
comparison.behind_by === 0 &&
comparison.ahead_by > 0 &&
comparison.total_commits === comparison.ahead_by;
const exactChangelogDiff =
files.length === 1 &&
files[0].filename === 'CHANGELOG.md' &&
files[0].status === 'modified' &&
!files[0].previous_filename;
if (linearFromValidatedTip && exactChangelogDiff) {
const requiredContexts = [
'Lint',
'Test',
'Coverage',
'Policy',
'Edition',
'Interface Integrity',
'AI Behavior',
'CLI Smoke',
'Mock MCP',
];
const runs = await github.paginate(
github.rest.checks.listForRef,
{
owner: context.repo.owner,
repo: context.repo.repo,
ref: expectedBefore,
filter: 'latest',
per_page: 100,
}
);
const latestByName = new Map();
for (const run of runs) {
if (
run.head_sha !== expectedBefore ||
run.app?.slug !== 'github-actions' ||
!requiredContexts.includes(run.name)
) {
continue;
}
const current = latestByName.get(run.name);
if (!current || run.id > current.id) {
latestByName.set(run.name, run);
}
}
const missing = requiredContexts.filter(
(name) => !latestByName.has(name)
);
const nonSuccess = requiredContexts.flatMap((name) => {
const run = latestByName.get(name);
if (!run || run.conclusion === 'success') {
return [];
}
return [
`${name}=${run.conclusion || run.status || 'unknown'}`,
];
});
if (missing.length === 0 && nonSuccess.length === 0) {
changelogOnly = true;
changelogChanged = true;
fastPathTrust =
`exact CHANGELOG-only successor of validated ${expectedBefore}`;
} else {
fastPathTrust =
'predecessor Code Admission is not fully successful; ' +
`missing=${missing.join(',') || 'none'}; ` +
`non-success=${nonSuccess.join(',') || 'none'}`;
}
} else {
fastPathTrust =
'push is not an exact linear CHANGELOG-only successor';
}
}
}
core.setOutput('changelog_only', String(changelogOnly));
core.setOutput('changelog_changed', String(changelogChanged));
core.setOutput('docs_only', String(docsOnly));
core.setOutput('full_suite', String(fullSuite));
core.setOutput('release_sensitive', String(releaseSensitive));
core.setOutput('interface_sensitive', String(interfaceSensitive));
core.setOutput('mcp_sensitive', String(mcpSensitive));
core.setOutput('edition_sensitive', String(editionSensitive));
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(`- Documentation-only: \`${docsOnly}\`\n`)
.addRaw(`- Full suite: \`${fullSuite}\`\n`)
.addRaw(`- Release-sensitive: \`${releaseSensitive}\`\n`)
.addRaw(`- Interface-sensitive: \`${interfaceSensitive}\`\n`)
.addRaw(`- MCP-sensitive: \`${mcpSensitive}\`\n`)
.addRaw(`- Edition-sensitive: \`${editionSensitive}\`\n`)
.addRaw(`- Native-platform risk paths touched: \`${platformSensitive}\`\n`)
.addRaw(`- Changed files: \`${files.length}\`\n`)
.addRaw(`- Fast-path trust: ${fastPathTrust}\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: Record documentation-only fast path
if: steps.classify.outputs.changelog_only != 'true' && steps.classify.outputs.docs_only == 'true'
run: echo "Lint is satisfied by the non-executable documentation scope." >> "$GITHUB_STEP_SUMMARY"
- name: Check out repository
if: steps.classify.outputs.changelog_only != 'true' && steps.classify.outputs.docs_only != 'true'
uses: actions/checkout@v4
- name: Set up Go
if: steps.classify.outputs.changelog_only != 'true' && steps.classify.outputs.docs_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' && steps.classify.outputs.docs_only != 'true'
run: make test-plan
- name: Format Check
if: steps.classify.outputs.changelog_only != 'true' && steps.classify.outputs.docs_only != 'true'
run: make format-check
- name: Go Vet
if: steps.classify.outputs.changelog_only != 'true' && steps.classify.outputs.docs_only != 'true'
run: go vet ./...
- name: Check GitHub Actions workflows
if: steps.classify.outputs.changelog_only != 'true' && steps.classify.outputs.docs_only != 'true'
run: go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.12
test-focused:
name: Test (changed packages)
needs: lint
if: ${{ needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true' && needs.lint.outputs.full_suite != 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Verify authoritative synthetic merge
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 "focused test merge first parent does not match event base" >&2
exit 1
}
test "$(git rev-parse HEAD^2)" = "$PR_HEAD_SHA" || {
echo "focused test merge second parent does not match event head" >&2
exit 1
}
echo "TEST_BASE_REF=$PR_BASE_SHA" >> "$GITHUB_ENV"
echo "TEST_HEAD_REF=$(git rev-parse HEAD)" >> "$GITHUB_ENV"
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Test changed packages and reverse dependencies
shell: bash
env:
DWS_PACKAGE_VERSION: 0.0.0-test
run: |
set -euo pipefail
package_output="$(
./scripts/ci/changed-test-packages.sh \
list "$TEST_BASE_REF" "$TEST_HEAD_REF"
)"
if [ -z "$package_output" ]; then
echo "No buildable Go package is affected by this revision." \
>> "$GITHUB_STEP_SUMMARY"
exit 0
fi
mapfile -t packages <<< "$package_output"
go test -v -race -count=1 -timeout=8m "${packages[@]}"
test-race:
name: "Test (race: ${{ matrix.shard }})"
needs: lint
if: ${{ needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true' && needs.lint.outputs.full_suite == '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 (workflow and release contracts)
needs: lint
if: ${{ needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true' && (needs.lint.outputs.full_suite == 'true' || needs.lint.outputs.release_sensitive == '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' && needs.lint.outputs.docs_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-focused
- 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 }}
DOCS_ONLY: ${{ needs.lint.outputs.docs_only }}
FULL_SUITE: ${{ needs.lint.outputs.full_suite }}
RELEASE_SENSITIVE: ${{ needs.lint.outputs.release_sensitive }}
FOCUSED_RESULT: ${{ needs.test-focused.result }}
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 ] || [ "$DOCS_ONLY" = true ]; then
for shard in \
"changed packages:$FOCUSED_RESULT" \
"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
focused_expected=success
race_expected=skipped
if [ "$FULL_SUITE" = true ]; then
focused_expected=skipped
race_expected=success
fi
release_expected=skipped
if [ "$FULL_SUITE" = true ] || [ "$RELEASE_SENSITIVE" = true ]; then
release_expected=success
fi
for shard in \
"changed packages:$FOCUSED_RESULT:$focused_expected" \
"race shards:$RACE_RESULT:$race_expected" \
"release scripts:$RELEASE_SCRIPTS_RESULT:$release_expected" \
"cross-platform compile:$CROSS_PLATFORM_RESULT:success"
do
name="${shard%%:*}"
remainder="${shard#*:}"
result="${remainder%%:*}"
expected="${remainder#*:}"
printf '%s: %s (expected %s)\n' "$name" "$result" "$expected"
if [ "$result" != "$expected" ]; then
failed=1
fi
done
native_expected=skipped
if [ "$FULL_SUITE" = 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.docs_only != 'true' && needs.lint.outputs.full_suite == '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.docs_only != 'true' && needs.lint.outputs.full_suite == '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' && needs.lint.outputs.docs_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
if: needs.lint.outputs.full_suite == 'true'
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"
echo "COVERAGE_HEAD_REF=$(git rev-parse HEAD)" >> "$GITHUB_ENV"
- name: Build
run: make build
- name: Run current unit tests with coverage
shell: bash
env:
DWS_PACKAGE_VERSION: 0.0.0-test
FULL_SUITE: ${{ needs.lint.outputs.full_suite }}
run: |
set -euo pipefail
if [ "$FULL_SUITE" = true ]; then
go test -count=1 -p 1 \
-coverprofile=coverage.txt \
-covermode=atomic \
./ ./cmd/... ./internal/... ./skills/...
else
changed_output="$(
./scripts/ci/changed-test-packages.sh \
changed "$COVERAGE_BASE_REF" "$COVERAGE_HEAD_REF"
)"
impacted_output="$(
./scripts/ci/changed-test-packages.sh \
list "$COVERAGE_BASE_REF" "$COVERAGE_HEAD_REF"
)"
if [ -z "$changed_output" ] || [ -z "$impacted_output" ]; then
printf 'mode: atomic\n' > coverage.txt
echo "No buildable Go package needs scoped coverage." \
>> "$GITHUB_STEP_SUMMARY"
else
mapfile -t changed_packages <<< "$changed_output"
mapfile -t impacted_packages <<< "$impacted_output"
coverpkg="$(IFS=,; echo "${changed_packages[*]}")"
go test -count=1 -p 1 \
-coverpkg="$coverpkg" \
-coverprofile=coverage.txt \
-covermode=atomic \
"${impacted_packages[@]}"
fi
fi
if [ "$(wc -l < coverage.txt)" -gt 1 ]; then
go tool cover -func=coverage.txt
fi
- 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' && needs.lint.outputs.docs_only != 'true' && needs.lint.outputs.full_suite == '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' && needs.lint.outputs.docs_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
if: needs.lint.outputs.full_suite == 'true'
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
shell: bash
env:
DWS_PACKAGE_VERSION: 0.0.0-test
FULL_SUITE: ${{ needs.lint.outputs.full_suite }}
run: |
set -euo pipefail
changed_output=
impacted_output=
if [ "$FULL_SUITE" != true ]; then
changed_output="$(
./scripts/ci/changed-test-packages.sh \
changed "$COVERAGE_BASE_REF" HEAD
)"
impacted_output="$(
./scripts/ci/changed-test-packages.sh \
list "$COVERAGE_BASE_REF" HEAD
)"
fi
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"
if [ "$FULL_SUITE" = true ]; then
go test -count=1 \
-p 1 \
-coverprofile="$GITHUB_WORKSPACE/coverage-base.txt" \
-covermode=atomic \
./ ./cmd/... ./internal/... ./skills/...
elif [ -z "$changed_output" ] || [ -z "$impacted_output" ]; then
printf 'mode: atomic\n' > "$GITHUB_WORKSPACE/coverage-base.txt"
else
mapfile -t changed_packages <<< "$changed_output"
mapfile -t impacted_packages <<< "$impacted_output"
coverpkg="$(IFS=,; echo "${changed_packages[*]}")"
go test -count=1 \
-p 1 \
-coverpkg="$coverpkg" \
-coverprofile="$GITHUB_WORKSPACE/coverage-base.txt" \
-covermode=atomic \
"${impacted_packages[@]}"
fi
)
- 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 }}
DOCS_ONLY: ${{ needs.lint.outputs.docs_only }}
FULL_SUITE: ${{ needs.lint.outputs.full_suite }}
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
current_expected=success
supporting_expected=skipped
baseline_expected=success
native_expected=skipped
if [ "$CHANGELOG_ONLY" = true ] || [ "$DOCS_ONLY" = true ]; then
current_expected=skipped
baseline_expected=skipped
elif [ "$FULL_SUITE" = true ]; then
supporting_expected=success
fi
if [ "$CHANGELOG_ONLY" != true ] &&
[ "$DOCS_ONLY" != true ] &&
[ "$PLATFORM_SENSITIVE" = true ]; then
native_expected=success
fi
for profile in \
"current:$CURRENT_RESULT:$current_expected" \
"supporting:$SUPPORTING_RESULT:$supporting_expected" \
"baseline:$BASELINE_RESULT:$baseline_expected"
do
name="${profile%%:*}"
remainder="${profile#*:}"
result="${remainder%%:*}"
expected="${remainder#*:}"
printf '%s: %s (expected %s)\n' "$name" "$result" "$expected"
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 (expected %s)\n' \
"$name" "$result" "$native_expected"
if [ "$result" != "$native_expected" ]; then
failed=1
fi
done
test "$failed" -eq 0
- name: Check out repository
if: needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_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' && needs.lint.outputs.docs_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' && needs.lint.outputs.docs_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' && needs.lint.outputs.docs_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' && needs.lint.outputs.docs_only != 'true' && needs.lint.outputs.full_suite == 'true'
uses: actions/download-artifact@v4
with:
name: coverage-supporting-profiles
path: .
- name: Download baseline coverage profile
if: needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true'
uses: actions/download-artifact@v4
with:
name: coverage-baseline-profile
path: .
- name: Enforce coverage gate
if: needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true'
env:
COVERAGE_TARGET: "100"
COVERAGE_ENFORCE_OVERALL: "false"
COVERAGE_OVERALL_TOLERANCE: "0"
run: |
additional_profile=
if [ -f coverage-shortcut.txt ]; then
additional_profile=coverage-shortcut.txt
fi
COVERAGE_ADDITIONAL_DIFF_PROFILE="$additional_profile" \
make coverage-gate BASE_REF="$COVERAGE_BASE_REF"
- name: Generate coverage report
if: needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true'
run: |
if [ "$(wc -l < coverage.txt)" -gt 1 ]; then
go tool cover -html=coverage.txt -o coverage.html
else
echo "No buildable Go package needed a coverage HTML report." \
>> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload coverage artifact
if: needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_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
if-no-files-found: error
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' && needs.lint.outputs.docs_only != 'true' && (needs.lint.outputs.full_suite == 'true' || needs.lint.outputs.interface_sensitive == '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: Validate trusted main CHANGELOG-only push
if: github.event_name == 'push' && needs.lint.outputs.changelog_only == 'true'
env:
PUSH_BEFORE_SHA: ${{ github.event.before }}
PUSH_AFTER_SHA: ${{ github.event.after }}
run: |
set -eu
test "$(git rev-parse HEAD)" = "$PUSH_AFTER_SHA" || {
echo "checked-out push revision does not match event after SHA" >&2
exit 1
}
./scripts/policy/check-changelog-pr.sh \
--fast-path "$PUSH_BEFORE_SHA" "$PUSH_AFTER_SHA"
- name: Record CHANGELOG-only fast path
if: needs.lint.outputs.changelog_only == 'true'
run: |
echo "Only the trusted base-equivalent CHANGELOG validator ran; executable sources are unchanged." \
>> "$GITHUB_STEP_SUMMARY"
- name: Validate scoped policy
if: ${{ needs.lint.outputs.changelog_only != 'true' && (needs.lint.outputs.docs_only == 'true' || (needs.lint.outputs.full_suite != 'true' && needs.lint.outputs.interface_sensitive != 'true')) }}
env:
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="$PR_BASE_SHA"
fi
git rev-parse --verify "${base_ref}^{commit}" >/dev/null
git diff --check "$base_ref" HEAD
./scripts/policy/check-open-source-assets.sh
echo "Policy used the scoped content/source admission path." \
>> "$GITHUB_STEP_SUMMARY"
- name: Build
if: ${{ needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true' && (needs.lint.outputs.full_suite == 'true' || needs.lint.outputs.interface_sensitive == 'true') }}
run: make build
- name: Policy
if: ${{ needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true' && (needs.lint.outputs.full_suite == 'true' || needs.lint.outputs.interface_sensitive == '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' && needs.lint.outputs.docs_only != 'true' && (needs.lint.outputs.full_suite == 'true' || needs.lint.outputs.interface_sensitive == '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' && needs.lint.outputs.docs_only != 'true' && (needs.lint.outputs.full_suite == 'true' || needs.lint.outputs.interface_sensitive == 'true') }}
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Build
if: ${{ needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true' && (needs.lint.outputs.full_suite == 'true' || needs.lint.outputs.interface_sensitive == 'true') }}
run: make build
- name: Resolve authoritative compatibility merge-base
if: ${{ needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true' && (needs.lint.outputs.full_suite == 'true' || needs.lint.outputs.interface_sensitive == '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' && needs.lint.outputs.docs_only != 'true' && (needs.lint.outputs.full_suite == 'true' || needs.lint.outputs.interface_sensitive == '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' && needs.lint.outputs.docs_only != 'true' && (needs.lint.outputs.full_suite == 'true' || needs.lint.outputs.interface_sensitive == 'true') }}
run: make schema-compatibility BASE_REF="$COMPATIBILITY_BASE_REF"
- name: Check skill command references
if: ${{ needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true' && (needs.lint.outputs.full_suite == 'true' || needs.lint.outputs.interface_sensitive == 'true') }}
run: make skill-command-integrity
- name: Record scoped fast path
if: ${{ needs.lint.outputs.changelog_only == 'true' || needs.lint.outputs.docs_only == 'true' || (needs.lint.outputs.full_suite != 'true' && needs.lint.outputs.interface_sensitive != 'true') }}
run: echo "Interface Integrity is unaffected by this classified revision." >> "$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' && needs.lint.outputs.docs_only != 'true' && (needs.lint.outputs.full_suite == 'true' || needs.lint.outputs.interface_sensitive == 'true') }}
uses: actions/checkout@v4
- name: Set up Go
if: ${{ needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true' && (needs.lint.outputs.full_suite == 'true' || needs.lint.outputs.interface_sensitive == 'true') }}
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Build
if: ${{ needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true' && (needs.lint.outputs.full_suite == 'true' || needs.lint.outputs.interface_sensitive == 'true') }}
run: make build
- name: Check public top-level commands
if: ${{ needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true' && (needs.lint.outputs.full_suite == 'true' || needs.lint.outputs.interface_sensitive == 'true') }}
run: make cli-smoke
- name: Record scoped fast path
if: ${{ needs.lint.outputs.changelog_only == 'true' || needs.lint.outputs.docs_only == 'true' || (needs.lint.outputs.full_suite != 'true' && needs.lint.outputs.interface_sensitive != 'true') }}
run: echo "CLI Smoke is unaffected by this classified revision." >> "$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' && needs.lint.outputs.docs_only != 'true' && (needs.lint.outputs.full_suite == 'true' || needs.lint.outputs.mcp_sensitive == 'true') }}
uses: actions/checkout@v4
- name: Set up Go
if: ${{ needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true' && (needs.lint.outputs.full_suite == 'true' || needs.lint.outputs.mcp_sensitive == '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' && needs.lint.outputs.docs_only != 'true' && (needs.lint.outputs.full_suite == 'true' || needs.lint.outputs.mcp_sensitive == 'true') }}
run: make mock-mcp-smoke
- name: Record scoped fast path
if: ${{ needs.lint.outputs.changelog_only == 'true' || needs.lint.outputs.docs_only == 'true' || (needs.lint.outputs.full_suite != 'true' && needs.lint.outputs.mcp_sensitive != 'true') }}
run: echo "Mock MCP is unaffected by this classified revision." >> "$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' && needs.lint.outputs.docs_only != 'true' && (needs.lint.outputs.full_suite == 'true' || needs.lint.outputs.edition_sensitive == 'true') }}
uses: actions/checkout@v4
- name: Set up Go
if: ${{ needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true' && (needs.lint.outputs.full_suite == 'true' || needs.lint.outputs.edition_sensitive == 'true') }}
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Run edition contract tests
if: ${{ needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true' && (needs.lint.outputs.full_suite == 'true' || needs.lint.outputs.edition_sensitive == 'true') }}
run: go test -v -count=1 ./pkg/editiontest/...
- name: Record scoped fast path
if: ${{ needs.lint.outputs.changelog_only == 'true' || needs.lint.outputs.docs_only == 'true' || (needs.lint.outputs.full_suite != 'true' && needs.lint.outputs.edition_sensitive != 'true') }}
run: echo "Edition is unaffected by this classified revision." >> "$GITHUB_STEP_SUMMARY"