Skip to content

Commit c8e8e04

Browse files
authored
Merge pull request #175 from BitGo/DX-2592-breaking-changes-linting
ci: move audit-api-spec to separate workflow, check breaking changes
2 parents ab2af84 + 3b14dc9 commit c8e8e04

File tree

2 files changed

+324
-70
lines changed

2 files changed

+324
-70
lines changed
Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
name: Audit API Spec
2+
3+
on:
4+
pull_request:
5+
6+
env:
7+
STATIC_ANALYSIS_BOT_APP_ID: 1819979
8+
jobs:
9+
generate-head-api-spec:
10+
name: Generate head API spec
11+
runs-on: ubuntu-latest
12+
steps:
13+
- name: Checkout PR
14+
uses: actions/checkout@v6
15+
with:
16+
ref: ${{ github.event.pull_request.head.sha }}
17+
18+
- name: Setup Node.js
19+
uses: actions/setup-node@v6
20+
with:
21+
node-version: 22
22+
23+
- name: Cache npm dependencies
24+
id: node-modules-cache
25+
uses: actions/cache@v5
26+
with:
27+
path: "**/node_modules"
28+
key: ${{ runner.os }}-modules-${{ hashFiles('**/package-lock.json') }}
29+
restore-keys: |
30+
${{ runner.os }}-modules-
31+
32+
- name: Install Dependencies
33+
if: steps.node-modules-cache.outputs.cache-hit != 'true'
34+
run: npm ci
35+
36+
- name: Generate API spec
37+
run: |
38+
./node_modules/.bin/openapi-generator \
39+
src/masterBitgoExpress/routers/index.ts \
40+
> generated.json
41+
42+
- name: Remove unknown tags from generated spec
43+
run: |
44+
jq '(.paths[] | .[]? | select(. != null)) |= del(."x-unknown-tags")' generated.json > openapi.json
45+
46+
- name: Convert merged spec to YAML
47+
run: yq -P < openapi.json > openapi-head.yaml
48+
49+
- name: Upload API spec to artifact
50+
uses: actions/upload-artifact@v6
51+
with:
52+
name: openapi-head.yaml
53+
path: openapi-head.yaml
54+
55+
generate-merge-base-api-spec:
56+
name: Generate merge base API spec
57+
runs-on: ubuntu-latest
58+
steps:
59+
- name: Checkout PR
60+
uses: actions/checkout@v6
61+
with:
62+
fetch-depth: 0
63+
64+
- name: Find and checkout merge base
65+
run: |
66+
git fetch origin ${{ github.event.pull_request.base.ref }}
67+
MERGE_BASE=$(git merge-base HEAD origin/${{ github.event.pull_request.base.ref }})
68+
echo "Merge base commit: $MERGE_BASE"
69+
git checkout $MERGE_BASE
70+
71+
- name: Setup Node.js
72+
uses: actions/setup-node@v6
73+
with:
74+
node-version: 22
75+
76+
- name: Cache npm dependencies
77+
id: node-modules-cache
78+
uses: actions/cache@v5
79+
with:
80+
path: "**/node_modules"
81+
key: ${{ runner.os }}-modules-${{ hashFiles('**/package-lock.json') }}
82+
restore-keys: |
83+
${{ runner.os }}-modules-
84+
85+
- name: Install Dependencies
86+
if: steps.node-modules-cache.outputs.cache-hit != 'true'
87+
run: npm ci
88+
89+
- name: Generate API spec
90+
run: |
91+
./node_modules/.bin/openapi-generator \
92+
src/masterBitgoExpress/routers/index.ts \
93+
> generated.json
94+
95+
- name: Remove unknown tags from generated spec
96+
run: |
97+
jq '(.paths[] | .[]? | select(. != null)) |= del(."x-unknown-tags")' generated.json > openapi.json
98+
99+
- name: Convert merged spec to YAML
100+
run: yq -P < openapi.json > openapi-merge-base.yaml
101+
102+
- name: Upload API spec to artifact
103+
uses: actions/upload-artifact@v6
104+
with:
105+
name: openapi-merge-base.yaml
106+
path: openapi-merge-base.yaml
107+
108+
check-specs-identical:
109+
name: Check specs identical
110+
runs-on: ubuntu-latest
111+
needs: [generate-head-api-spec, generate-merge-base-api-spec]
112+
outputs:
113+
specs-identical: ${{ steps.check-specs-identical.outputs.identical }}
114+
steps:
115+
- name: Download head API spec artifact
116+
uses: actions/download-artifact@v7
117+
with:
118+
name: openapi-head.yaml
119+
120+
- name: Download merge base API spec artifact
121+
uses: actions/download-artifact@v7
122+
with:
123+
name: openapi-merge-base.yaml
124+
125+
- name: Check specs identical
126+
id: check-specs-identical
127+
run: |
128+
if diff -q openapi-head.yaml openapi-merge-base.yaml > /dev/null 2>&1; then
129+
echo "identical=true" >> $GITHUB_OUTPUT
130+
echo "✅ Specs are identical - no changes detected, skipping subsequent checks"
131+
else
132+
echo "identical=false" >> $GITHUB_OUTPUT
133+
echo "📝 Specs differ - proceeding with audit checks"
134+
fi
135+
136+
generate-vacuum-reports:
137+
name: Generate vacuum reports for API spec
138+
runs-on: ubuntu-latest
139+
needs: [check-specs-identical]
140+
if: needs.check-specs-identical.outputs.specs-identical != 'true'
141+
steps:
142+
- name: Checkout PR head for ruleset
143+
uses: actions/checkout@v6
144+
with:
145+
ref: ${{ github.event.pull_request.head.sha }}
146+
147+
- name: Download head API spec artifact
148+
uses: actions/download-artifact@v7
149+
with:
150+
name: openapi-head.yaml
151+
152+
- name: Download and install vacuum v0.18.1
153+
run: |
154+
curl -L \
155+
--output vacuum.tar.gz \
156+
--silent \
157+
--show-error \
158+
--fail \
159+
https://github.com/daveshanley/vacuum/releases/download/v0.18.1/vacuum_0.18.1_linux_x86_64.tar.gz
160+
tar -xzf vacuum.tar.gz
161+
chmod u+x vacuum
162+
sudo mv vacuum /usr/local/bin/
163+
vacuum version
164+
165+
- name: Audit head API spec with Vacuum
166+
run: |
167+
vacuum report \
168+
--no-style \
169+
--stdout \
170+
--ruleset ruleset.yaml \
171+
openapi-head.yaml > vacuum-report.json
172+
173+
jq '.resultSet.results // []' vacuum-report.json > vacuum-results.json
174+
175+
ERROR_COUNT=$(jq '[.[] | select(.ruleSeverity == "error")] | length' vacuum-results.json)
176+
WARNING_COUNT=$(jq '[.[] | select(.ruleSeverity == "warn")] | length' vacuum-results.json)
177+
178+
echo "Found $ERROR_COUNT error(s) and $WARNING_COUNT warning(s)"
179+
180+
if [ "$ERROR_COUNT" -gt 0 ]; then
181+
echo "API specification audit failed with $ERROR_COUNT error(s)"
182+
echo ""
183+
echo "Errors:"
184+
jq -r '.[] | select(.ruleSeverity == "error") | " - [\(.ruleId)] \(.message) at \(.path)"' vacuum-results.json
185+
exit 1
186+
else
187+
echo "API specification audit passed!"
188+
fi
189+
190+
check-breaking-changes:
191+
name: Check breaking changes
192+
needs: [check-specs-identical]
193+
if: needs.check-specs-identical.outputs.specs-identical != 'true'
194+
runs-on: ubuntu-latest
195+
steps:
196+
- name: Download head API spec artifact
197+
uses: actions/download-artifact@v7
198+
with:
199+
name: openapi-head.yaml
200+
201+
- name: Download merge base API spec artifact
202+
uses: actions/download-artifact@v7
203+
with:
204+
name: openapi-merge-base.yaml
205+
206+
- name: Create static-analysis config file
207+
run: |
208+
cat <<EOF > .static-analysis.yaml
209+
---
210+
api_version: static-analysis.bitgo/v1alpha1
211+
212+
rules:
213+
- path: rules/openapi/breaking-changes
214+
severity: error
215+
options:
216+
before_spec_path: openapi-merge-base.yaml
217+
after_spec_path: openapi-head.yaml
218+
EOF
219+
- name: Generate GitHub App Token
220+
id: generate-github-app-token
221+
uses: actions/create-github-app-token@v2
222+
with:
223+
app-id: ${{ env.STATIC_ANALYSIS_BOT_APP_ID }}
224+
private-key: ${{ secrets.STATIC_ANALYSIS_BOT_PRIVATE_KEY }}
225+
owner: bitgo
226+
repositories: |
227+
static-analysis
228+
229+
- name: Install BitGo/static-analysis/static-analysis@v1
230+
uses: BitGo/install-github-release-binary@v2
231+
with:
232+
targets: BitGo/static-analysis/static-analysis@v1
233+
token: ${{ steps.generate-github-app-token.outputs.token }}
234+
235+
- name: Check breaking changes
236+
run: |
237+
if ! static-analysis; then
238+
echo "
239+
## ⚠️ Breaking Changes Detected
240+
241+
The OpenAPI spec changes in this PR contain breaking changes that could affect API consumers.
242+
243+
**What to do next:**
244+
1. Review the breaking changes in the workflow run
245+
2. If these breaking changes are intentional and necessary, contact the DevEx team for a manual override
246+
3. If not intentional, please revise your changes to maintain backward compatibility
247+
248+
**Need a manual override?**
249+
Contact the DevEx team to request a manual override.
250+
"
251+
exit 1
252+
fi
253+
254+
manual-linter-override:
255+
name: Linter Override
256+
needs: [check-breaking-changes]
257+
if: always() && needs.check-breaking-changes.result == 'failure'
258+
environment: breaking-changes-override
259+
runs-on: ubuntu-latest
260+
steps:
261+
- name: Override Breaking Changes Check
262+
run: |
263+
echo "⚠️ Manual override requested for breaking changes check"
264+
echo "Breaking changes check failed but was manually approved to proceed"
265+
echo "This override was approved by the reviewer"
266+
267+
api-spec-check:
268+
name: API Spec Check
269+
needs:
270+
[
271+
generate-head-api-spec,
272+
generate-merge-base-api-spec,
273+
check-specs-identical,
274+
generate-vacuum-reports,
275+
check-breaking-changes,
276+
manual-linter-override,
277+
]
278+
runs-on: ubuntu-latest
279+
if: always()
280+
steps:
281+
- name: Check generate-head-api-spec
282+
env:
283+
GENERATE_HEAD_API_SPEC_RESULT: ${{ needs.generate-head-api-spec.result }}
284+
run: |
285+
if [ "$GENERATE_HEAD_API_SPEC_RESULT" != "success" ]; then
286+
echo "❌ generate-head-api-spec: ${{ needs.generate-head-api-spec.result }}"
287+
exit 1
288+
fi
289+
- name: Check generate-merge-base-api-spec
290+
env:
291+
GENERATE_MERGE_BASE_API_SPEC_RESULT: ${{ needs.generate-merge-base-api-spec.result }}
292+
run: |
293+
if [ "$GENERATE_MERGE_BASE_API_SPEC_RESULT" != "success" ]; then
294+
echo "❌ generate-merge-base-api-spec: ${{ needs.generate-merge-base-api-spec.result }}"
295+
exit 1
296+
fi
297+
- name: Check generate-vacuum-reports
298+
env:
299+
GENERATE_VACUUM_REPORTS_RESULT: ${{ needs.generate-vacuum-reports.result }}
300+
SPECS_IDENTICAL: ${{ needs.check-specs-identical.outputs.specs-identical }}
301+
run: |
302+
if [ "$GENERATE_VACUUM_REPORTS_RESULT" = "skipped" ] && [ "$SPECS_IDENTICAL" = "true" ]; then
303+
echo "⏭️ generate-vacuum-reports: skipped (specs are identical)"
304+
elif [ "$GENERATE_VACUUM_REPORTS_RESULT" != "success" ]; then
305+
echo "❌ generate-vacuum-reports: ${{ needs.generate-vacuum-reports.result }}"
306+
exit 1
307+
fi
308+
- name: Check check-breaking-changes
309+
env:
310+
CHECK_BREAKING_CHANGES_RESULT: ${{ needs.check-breaking-changes.result }}
311+
MANUAL_LINTER_OVERRIDE_RESULT: ${{ needs.manual-linter-override.result }}
312+
SPECS_IDENTICAL: ${{ needs.check-specs-identical.outputs.specs-identical }}
313+
run: |
314+
if [ "$CHECK_BREAKING_CHANGES_RESULT" = "skipped" ] && [ "$SPECS_IDENTICAL" = "true" ]; then
315+
echo "⏭️ check-breaking-changes: skipped (specs are identical)"
316+
elif [ "$CHECK_BREAKING_CHANGES_RESULT" != "success" ] && [ "$MANUAL_LINTER_OVERRIDE_RESULT" = "success" ]; then
317+
echo "⚠️ Manual linter override requested for breaking changes check"
318+
echo "Breaking changes check failed but was manually approved to proceed"
319+
elif [ "$CHECK_BREAKING_CHANGES_RESULT" != "success" ]; then
320+
echo "❌ check-breaking-changes: ${{ needs.check-breaking-changes.result }}"
321+
exit 1
322+
fi
323+
- name: Verify API Spec Check Passed
324+
run: echo "✅ All API specification checks passed successfully!"

.github/workflows/pull_request.yaml

Lines changed: 0 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -41,73 +41,3 @@ jobs:
4141
VCS_REF=${{ github.sha }}
4242
cache-from: type=gha
4343
cache-to: type=gha,mode=max
44-
45-
audit-api-spec:
46-
name: Audit API Spec
47-
runs-on: ubuntu-latest
48-
49-
steps:
50-
- name: Checkout PR
51-
uses: actions/checkout@v4
52-
with:
53-
ref: ${{ github.event.pull_request.head.sha }}
54-
55-
- name: Setup Node.js
56-
uses: actions/setup-node@v4
57-
with:
58-
node-version: 22
59-
- name: Cache npm dependencies
60-
id: node-modules-cache
61-
uses: actions/cache@v4
62-
with:
63-
path: '**/node_modules'
64-
key: ${{ runner.os }}-modules-${{ hashFiles('**/package-lock.json') }}
65-
restore-keys: |
66-
${{ runner.os }}-modules-
67-
- name: Install Dependencies
68-
if: steps.node-modules-cache.outputs.cache-hit != 'true'
69-
run: npm ci
70-
71-
- name: Download and install vacuum v0.18.1
72-
run: |
73-
curl -L \
74-
--output vacuum.tar.gz \
75-
--silent \
76-
--show-error \
77-
--fail \
78-
https://github.com/daveshanley/vacuum/releases/download/v0.18.1/vacuum_0.18.1_linux_x86_64.tar.gz
79-
tar -xzf vacuum.tar.gz
80-
chmod u+x vacuum
81-
sudo mv vacuum /usr/local/bin/
82-
vacuum version
83-
84-
- name: Generate API spec
85-
run: |
86-
./node_modules/.bin/openapi-generator \
87-
src/masterBitgoExpress/routers/index.ts \
88-
> api-generated.json
89-
90-
- name: Audit with Vacuum
91-
run: |
92-
vacuum report \
93-
--no-style \
94-
--stdout \
95-
--ruleset ruleset.yaml \
96-
api-generated.json > vacuum-report.json
97-
98-
jq '.resultSet.results // []' vacuum-report.json > vacuum-results.json
99-
100-
ERROR_COUNT=$(jq '[.[] | select(.ruleSeverity == "error")] | length' vacuum-results.json)
101-
WARNING_COUNT=$(jq '[.[] | select(.ruleSeverity == "warn")] | length' vacuum-results.json)
102-
103-
echo "Found $ERROR_COUNT error(s) and $WARNING_COUNT warning(s)"
104-
105-
if [ "$ERROR_COUNT" -gt 0 ]; then
106-
echo "API specification audit failed with $ERROR_COUNT error(s)"
107-
echo ""
108-
echo "Errors:"
109-
jq -r '.[] | select(.ruleSeverity == "error") | " - [\(.ruleId)] \(.message) at \(.path)"' vacuum-results.json
110-
exit 1
111-
else
112-
echo "API specification audit passed!"
113-
fi

0 commit comments

Comments
 (0)