1457 lines
59 KiB
YAML
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"
|