Repository navigation
252 lines (228 loc) · 9.33 KB
/
Copy pathpr.yaml
File metadata and controls
252 lines (228 loc) · 9.33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# Pull Request validation workflow
# Runs unit tests + lint on PRs. E2E runs separately (e2e.yaml) on
# release tags and manual dispatch. Too slow and flaky to run per PR.
#
# This workflow triggers on every PR to main so the PR Gate job always
# reports a status. The expensive jobs (Unit Tests, Lint) are gated on a
# path filter and skip when only docs / non-code files changed. The Docs
# Build job is gated on a docs path filter and builds the Astro docs site so
# a broken build blocks the merge; it skips when no docs changed. The PR Gate
# job aggregates their results and is the single check to require in branch
# protection: it passes when each gated job succeeds or is skipped, and
# fails when any gated job fails or is cancelled.
name: PR Validation
on:
pull_request:
branches: [main]
permissions:
contents: read
jobs:
changes:
name: Detect changes
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
outputs:
code: ${{ steps.filter.outputs.code }}
docs: ${{ steps.filter.outputs.docs }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- id: filter
uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
with:
filters: |
code:
- '!**.md'
- '!docs/**'
- '!LICENSE'
- '!.gitignore'
- '!.editorconfig'
- '!.gitattributes'
- '!.github/ISSUE_TEMPLATE/**'
- '!.github/PULL_REQUEST_TEMPLATE.md'
docs:
- 'docs/**'
unit-tests:
name: Unit Tests
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
# Release tags must be visible so the DefaultCLIVersion release-lag
# guard runs instead of skipping on a tag-less shallow checkout, and
# the history since the last release tag must be complete so the
# CHANGELOG [Unreleased] freshness guard runs instead of skipping on
# a shallow checkout.
fetch-tags: true
fetch-depth: 0
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version-file: go.mod
cache: true
# actionlint must be on PATH so the emitted-workflow enforcement guard
# (TestActionlint_FeatureMatrix and the census-driven sweep) genuinely runs
# instead of skipping. Pinned to the same SHA the workflow-lint job uses so
# the guard and the repo's own workflow lint agree on the actionlint version.
- name: Install actionlint
run: go install github.com/rhysd/actionlint/cmd/actionlint@914e7df21a07ef503a81201c76d2b11c789d3fca # v1.7.12
- name: Run unit tests
run: |
go list ./... | grep -v /e2e \
| xargs go test -v -race -coverprofile=coverage.out
- name: Check coverage
run: |
go tool cover -func=coverage.out
COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | tr -d '%')
echo "Total coverage: ${COVERAGE}%"
- name: Check coverage threshold
run: |
COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | tr -d '%')
echo "Total coverage: ${COVERAGE}%"
awk -v cov="$COVERAGE" 'BEGIN {
if (cov + 0 < 80) {
printf "Coverage %s%% is below the 80%% threshold\n", cov
exit 1
}
printf "Coverage %s%% meets the 80%% threshold\n", cov
}'
lint:
name: Lint
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version-file: go.mod
cache: true
- name: Run golangci-lint
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0
with:
version: v2.10.1
workflow-drift:
name: Workflow Drift Check
runs-on: ubuntu-latest
# contents:read only. This job runs on pull_request, so a fork PR gets a
# read-only token and no secrets. It cannot post a PR comment itself; it
# captures the drift result as an artifact that the drift-comment workflow
# (workflow_run, base-repo context) consumes with a write token.
permissions:
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
# Full history so the reconcile detector can diff base..head to find
# the changed governed source files a pin bump would land in.
fetch-depth: 0
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version-file: go.mod
cache: true
- name: Build cascade CLI
run: go build -o /tmp/cascade ./cmd/cascade
- name: Check for workflow drift
run: |
set +e
/tmp/cascade verify --own-repo --config .github/manifest.yaml > drift-report.txt 2>&1
echo $? > drift-exit.txt
set -e
cat drift-report.txt
- name: Upload drift result
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: drift-result
path: |
drift-report.txt
drift-exit.txt
retention-days: 1
# List the PR's changed governed source files so the reconcile detector
# scans them for a moved pin. cascade's own governed sources are the
# hand-written workflows AND the composite actions, so diff over both.
# The pull_request SHAs are attacker-influenceable on a fork PR, so they
# are routed through env and referenced as quoted shell variables.
- name: List changed governed source files
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
git diff --name-only "$BASE_SHA" "$HEAD_SHA" \
-- .github/workflows/ .github/actions/ > changed-files.txt
# Read-only reconcile detector. It records whether a governed action pin
# moved in a source file so the pin-reconcile companion (workflow_run,
# base-repo context) can adopt it back into the manifest. This step is
# advisory: it never fails the PR gate, and the companion recomputes the
# adoption authoritatively before it writes anything.
- name: Detect governed pin drift for reconcile
run: |
set +e
args=()
while IFS= read -r f; do
[ -n "$f" ] && args+=(--changed-file "$f")
done < changed-files.txt
/tmp/cascade reconcile --check --check-output pin-reconcile-result.json "${args[@]}"
echo "reconcile check exit: $?"
set -e
- name: Upload reconcile result
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: pin-reconcile-result
path: pin-reconcile-result.json
if-no-files-found: ignore
retention-days: 1
- name: Fail on drift
run: |
CODE=$(cat drift-exit.txt)
if [ "$CODE" != "0" ]; then
echo "Workflows are out of sync with the manifest."
echo "Run: cascade generate-workflow --own-repo --config .github/manifest.yaml --force"
echo "Then commit the regenerated workflows."
fi
exit "$CODE"
docs-build:
name: Docs Build
needs: changes
if: needs.changes.outputs.docs == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
defaults:
run:
working-directory: docs
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22"
cache: npm
cache-dependency-path: docs/package-lock.json
- name: Install dependencies
run: npm ci
- name: Build site
run: npm run build
pr-gate:
name: PR Gate
needs: [unit-tests, lint, workflow-drift, docs-build]
if: ${{ always() }}
runs-on: ubuntu-latest
steps:
- name: Verify gated jobs succeeded or were skipped
run: |
if [ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" = "true" ]; then
echo "A required check failed or was cancelled."
echo " unit-tests: ${{ needs.unit-tests.result }}"
echo " lint: ${{ needs.lint.result }}"
echo " workflow-drift: ${{ needs.workflow-drift.result }}"
echo " docs-build: ${{ needs.docs-build.result }}"
exit 1
fi
echo "PR Gate passed (jobs succeeded or were skipped)."
echo " unit-tests: ${{ needs.unit-tests.result }}"
echo " lint: ${{ needs.lint.result }}"
echo " workflow-drift: ${{ needs.workflow-drift.result }}"
echo " docs-build: ${{ needs.docs-build.result }}"