Skip to content

Commit 70c7fda

Browse files
committed
ci: check for breaking changes
Ticket: DX-2604 This commit enrolls bitgojs into breaking changes checking. TICKET: DX-2604
1 parent 6bcfcb9 commit 70c7fda

File tree

2 files changed

+339
-83
lines changed

2 files changed

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

0 commit comments

Comments
 (0)