## Summary - consolidate vet, tests, lint, smoke, and cross-build into one pull-request-only job - keep binary releases strictly v-tag-only with release-only write permission - retain wrapper synchronization only for canonical wrapper changes on main - pin actions and enforce exact trigger contracts ## Verification - nix develop --command make check - trigger mutation contract - 28-workflow cross-repository actionlint audit Generated with Claude CodeReviewed-on: #47 Co-authored-by: vickydotbat <vickydotbat@tutamail.com>
85 lines
3.5 KiB
Bash
85 lines
3.5 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
contract_script="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
|
|
default_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
repo_root="${WORKFLOW_ROOT:-$default_root}"
|
|
cd "$repo_root"
|
|
|
|
ci=.gitea/workflows/ci.yml
|
|
release=.gitea/workflows/build-binaries.yml
|
|
sync=.gitea/workflows/sync-wrappers.yml
|
|
checkout='actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683'
|
|
|
|
assert_exact_events() {
|
|
local workflow="$1" expected="$2" actual
|
|
actual="$(awk '
|
|
/^on:$/ { in_on = 1 }
|
|
in_on && /^$/ { next }
|
|
in_on && $0 != "on:" && $0 !~ /^[[:space:]]/ { exit }
|
|
in_on { print }
|
|
' "$workflow")"
|
|
[[ "$actual" == "$expected" ]] || {
|
|
echo "workflow-contract: unexpected event scope in $workflow" >&2
|
|
return 1
|
|
}
|
|
}
|
|
|
|
[[ -f "$ci" ]] || { echo "missing consolidated CI workflow" >&2; exit 1; }
|
|
[[ ! -e .gitea/workflows/test.yml ]] || { echo "legacy test workflow still present" >&2; exit 1; }
|
|
|
|
assert_exact_events "$ci" $'on:\n pull_request:'
|
|
grep -Fqx 'permissions: read-all' "$ci"
|
|
grep -Fqx ' timeout-minutes: 60' "$ci"
|
|
[[ "$(grep -Fc "$checkout" "$ci")" -eq 1 ]]
|
|
grep -Fqx ' fetch-depth: 0' "$ci"
|
|
grep -Fq 'go vet ./...' "$ci"
|
|
grep -Fq 'go test ./...' "$ci"
|
|
grep -Fq 'shellcheck scripts/*.sh' "$ci"
|
|
grep -Fq 'yamllint .gitea' "$ci"
|
|
grep -Fq 'make smoke' "$ci"
|
|
grep -Fq 'for target in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64 windows/arm64; do' "$ci"
|
|
|
|
assert_exact_events "$release" $'on:\n push:\n tags: [\'v*\']'
|
|
grep -Fqx ' code: read' "$release"
|
|
grep -Fqx ' releases: write' "$release"
|
|
grep -Fqx ' timeout-minutes: 30' "$release"
|
|
[[ "$(grep -Fc "$checkout" "$release")" -eq 1 ]]
|
|
grep -Fq 'secrets.GITEA_TOKEN' "$release"
|
|
if grep -Fq 'secrets.GITHUB_TOKEN' "$release"; then
|
|
echo "workflow-contract: release uses the GitHub token alias" >&2
|
|
exit 1
|
|
fi
|
|
|
|
grep -Fqx 'permissions: read-all' "$sync"
|
|
grep -Fqx ' timeout-minutes: 30' "$sync"
|
|
[[ "$(grep -Fc "$checkout" "$sync")" -eq 1 ]]
|
|
assert_exact_events "$sync" $'on:\n push:\n branches: [main]\n paths:\n - \'wrappers/crucible.sh\'\n - \'wrappers/crucible.ps1\''
|
|
|
|
expect_mutation_rejected() {
|
|
local name="$1" workflow="$2" expression="$3" tmp
|
|
tmp="$(mktemp -d)"
|
|
mkdir -p "$tmp/.gitea"
|
|
cp -R "$default_root/.gitea/workflows" "$tmp/.gitea/workflows"
|
|
sed -i "$expression" "$tmp/$workflow"
|
|
if WORKFLOW_ROOT="$tmp" WORKFLOW_CONTRACT_MUTATION=1 bash "$contract_script" >/dev/null 2>&1; then
|
|
echo "workflow-contract: accepted forbidden mutation: $name" >&2
|
|
rm -rf "$tmp"
|
|
return 1
|
|
fi
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
if [[ "${WORKFLOW_CONTRACT_MUTATION:-0}" != 1 ]]; then
|
|
mutations_ok=0
|
|
expect_mutation_rejected 'CI workflow_dispatch event' "$ci" '/^ pull_request:$/a\ workflow_dispatch:' || mutations_ok=1
|
|
expect_mutation_rejected 'release develop branch' "$release" "/^ tags:/a\\ branches: [develop]" || mutations_ok=1
|
|
expect_mutation_rejected 'release schedule event' "$release" "/^ tags:/a\\ schedule:\n - cron: '0 0 * * *'" || mutations_ok=1
|
|
expect_mutation_rejected 'release extra tag pattern' "$release" "s/tags: \['v\*'\]/tags: ['v*', 'release-*']/" || mutations_ok=1
|
|
expect_mutation_rejected 'wrapper sync pull_request event' "$sync" '/^ push:$/i\ pull_request:' || mutations_ok=1
|
|
expect_mutation_rejected 'wrapper sync broad path' "$sync" "/^ - 'wrappers\/crucible.ps1'$/a\\ - 'wrappers/**'" || mutations_ok=1
|
|
(( mutations_ok == 0 )) || exit 1
|
|
fi
|
|
|
|
echo "workflow-contract: CI is PR-only; release and wrapper sync retain their narrow triggers"
|