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