Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
324 changes: 324 additions & 0 deletions .github/workflows/audit-api-spec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,324 @@
name: Audit API Spec

on:
pull_request:

env:
STATIC_ANALYSIS_BOT_APP_ID: 1819979
jobs:
generate-head-api-spec:
name: Generate head API spec
runs-on: ubuntu-latest
steps:
- name: Checkout PR
uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.sha }}

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 22

- name: Cache npm dependencies
id: node-modules-cache
uses: actions/cache@v5
with:
path: "**/node_modules"
key: ${{ runner.os }}-modules-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-modules-

- name: Install Dependencies
if: steps.node-modules-cache.outputs.cache-hit != 'true'
run: npm ci

- name: Generate API spec
run: |
./node_modules/.bin/openapi-generator \
src/masterBitgoExpress/routers/index.ts \
> generated.json

- name: Remove unknown tags from generated spec
run: |
jq '(.paths[] | .[]? | select(. != null)) |= del(."x-unknown-tags")' generated.json > openapi.json

- name: Convert merged spec to YAML
run: yq -P < openapi.json > openapi-head.yaml

- name: Upload API spec to artifact
uses: actions/upload-artifact@v6
with:
name: openapi-head.yaml
path: openapi-head.yaml

generate-merge-base-api-spec:
name: Generate merge base API spec
runs-on: ubuntu-latest
steps:
- name: Checkout PR
uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Find and checkout merge base
run: |
git fetch origin ${{ github.event.pull_request.base.ref }}
MERGE_BASE=$(git merge-base HEAD origin/${{ github.event.pull_request.base.ref }})
echo "Merge base commit: $MERGE_BASE"
git checkout $MERGE_BASE

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 22

- name: Cache npm dependencies
id: node-modules-cache
uses: actions/cache@v5
with:
path: "**/node_modules"
key: ${{ runner.os }}-modules-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-modules-

- name: Install Dependencies
if: steps.node-modules-cache.outputs.cache-hit != 'true'
run: npm ci

- name: Generate API spec
run: |
./node_modules/.bin/openapi-generator \
src/masterBitgoExpress/routers/index.ts \
> generated.json

- name: Remove unknown tags from generated spec
run: |
jq '(.paths[] | .[]? | select(. != null)) |= del(."x-unknown-tags")' generated.json > openapi.json

- name: Convert merged spec to YAML
run: yq -P < openapi.json > openapi-merge-base.yaml

- name: Upload API spec to artifact
uses: actions/upload-artifact@v6
with:
name: openapi-merge-base.yaml
path: openapi-merge-base.yaml

check-specs-identical:
name: Check specs identical
runs-on: ubuntu-latest
needs: [generate-head-api-spec, generate-merge-base-api-spec]
outputs:
specs-identical: ${{ steps.check-specs-identical.outputs.identical }}
steps:
- name: Download head API spec artifact
uses: actions/download-artifact@v7
with:
name: openapi-head.yaml

- name: Download merge base API spec artifact
uses: actions/download-artifact@v7
with:
name: openapi-merge-base.yaml

- name: Check specs identical
id: check-specs-identical
run: |
if diff -q openapi-head.yaml openapi-merge-base.yaml > /dev/null 2>&1; then
echo "identical=true" >> $GITHUB_OUTPUT
echo "✅ Specs are identical - no changes detected, skipping subsequent checks"
else
echo "identical=false" >> $GITHUB_OUTPUT
echo "📝 Specs differ - proceeding with audit checks"
fi

generate-vacuum-reports:
name: Generate vacuum reports for API spec
runs-on: ubuntu-latest
needs: [check-specs-identical]
if: needs.check-specs-identical.outputs.specs-identical != 'true'
steps:
- name: Checkout PR head for ruleset
uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.sha }}

- name: Download head API spec artifact
uses: actions/download-artifact@v7
with:
name: openapi-head.yaml

- name: Download and install vacuum v0.18.1
run: |
curl -L \
--output vacuum.tar.gz \
--silent \
--show-error \
--fail \
https://github.com/daveshanley/vacuum/releases/download/v0.18.1/vacuum_0.18.1_linux_x86_64.tar.gz
tar -xzf vacuum.tar.gz
chmod u+x vacuum
sudo mv vacuum /usr/local/bin/
vacuum version

- name: Audit head API spec with Vacuum
run: |
vacuum report \
--no-style \
--stdout \
--ruleset ruleset.yaml \
openapi-head.yaml > vacuum-report.json

jq '.resultSet.results // []' vacuum-report.json > vacuum-results.json

ERROR_COUNT=$(jq '[.[] | select(.ruleSeverity == "error")] | length' vacuum-results.json)
WARNING_COUNT=$(jq '[.[] | select(.ruleSeverity == "warn")] | length' vacuum-results.json)

echo "Found $ERROR_COUNT error(s) and $WARNING_COUNT warning(s)"

if [ "$ERROR_COUNT" -gt 0 ]; then
echo "API specification audit failed with $ERROR_COUNT error(s)"
echo ""
echo "Errors:"
jq -r '.[] | select(.ruleSeverity == "error") | " - [\(.ruleId)] \(.message) at \(.path)"' vacuum-results.json
exit 1
else
echo "API specification audit passed!"
fi

check-breaking-changes:
name: Check breaking changes
needs: [check-specs-identical]
if: needs.check-specs-identical.outputs.specs-identical != 'true'
runs-on: ubuntu-latest
steps:
- name: Download head API spec artifact
uses: actions/download-artifact@v7
with:
name: openapi-head.yaml

- name: Download merge base API spec artifact
uses: actions/download-artifact@v7
with:
name: openapi-merge-base.yaml

- name: Create static-analysis config file
run: |
cat <<EOF > .static-analysis.yaml
---
api_version: static-analysis.bitgo/v1alpha1

rules:
- path: rules/openapi/breaking-changes
severity: error
options:
before_spec_path: openapi-merge-base.yaml
after_spec_path: openapi-head.yaml
EOF
- name: Generate GitHub App Token
id: generate-github-app-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ env.STATIC_ANALYSIS_BOT_APP_ID }}
private-key: ${{ secrets.STATIC_ANALYSIS_BOT_PRIVATE_KEY }}
owner: bitgo
repositories: |
static-analysis

- name: Install BitGo/static-analysis/static-analysis@v1
uses: BitGo/install-github-release-binary@v2
with:
targets: BitGo/static-analysis/static-analysis@v1
token: ${{ steps.generate-github-app-token.outputs.token }}

- name: Check breaking changes
run: |
if ! static-analysis; then
echo "
## ⚠️ Breaking Changes Detected

The OpenAPI spec changes in this PR contain breaking changes that could affect API consumers.

**What to do next:**
1. Review the breaking changes in the workflow run
2. If these breaking changes are intentional and necessary, contact the DevEx team for a manual override
3. If not intentional, please revise your changes to maintain backward compatibility

**Need a manual override?**
Contact the DevEx team to request a manual override.
"
exit 1
fi

manual-linter-override:
name: Linter Override
needs: [check-breaking-changes]
if: always() && needs.check-breaking-changes.result == 'failure'
environment: breaking-changes-override
runs-on: ubuntu-latest
steps:
- name: Override Breaking Changes Check
run: |
echo "⚠️ Manual override requested for breaking changes check"
echo "Breaking changes check failed but was manually approved to proceed"
echo "This override was approved by the reviewer"

api-spec-check:
name: API Spec Check
needs:
[
generate-head-api-spec,
generate-merge-base-api-spec,
check-specs-identical,
generate-vacuum-reports,
check-breaking-changes,
manual-linter-override,
]
runs-on: ubuntu-latest
if: always()
steps:
- name: Check generate-head-api-spec
env:
GENERATE_HEAD_API_SPEC_RESULT: ${{ needs.generate-head-api-spec.result }}
run: |
if [ "$GENERATE_HEAD_API_SPEC_RESULT" != "success" ]; then
echo "❌ generate-head-api-spec: ${{ needs.generate-head-api-spec.result }}"
exit 1
fi
- name: Check generate-merge-base-api-spec
env:
GENERATE_MERGE_BASE_API_SPEC_RESULT: ${{ needs.generate-merge-base-api-spec.result }}
run: |
if [ "$GENERATE_MERGE_BASE_API_SPEC_RESULT" != "success" ]; then
echo "❌ generate-merge-base-api-spec: ${{ needs.generate-merge-base-api-spec.result }}"
exit 1
fi
- name: Check generate-vacuum-reports
env:
GENERATE_VACUUM_REPORTS_RESULT: ${{ needs.generate-vacuum-reports.result }}
SPECS_IDENTICAL: ${{ needs.check-specs-identical.outputs.specs-identical }}
run: |
if [ "$GENERATE_VACUUM_REPORTS_RESULT" = "skipped" ] && [ "$SPECS_IDENTICAL" = "true" ]; then
echo "⏭️ generate-vacuum-reports: skipped (specs are identical)"
elif [ "$GENERATE_VACUUM_REPORTS_RESULT" != "success" ]; then
echo "❌ generate-vacuum-reports: ${{ needs.generate-vacuum-reports.result }}"
exit 1
fi
- name: Check check-breaking-changes
env:
CHECK_BREAKING_CHANGES_RESULT: ${{ needs.check-breaking-changes.result }}
MANUAL_LINTER_OVERRIDE_RESULT: ${{ needs.manual-linter-override.result }}
SPECS_IDENTICAL: ${{ needs.check-specs-identical.outputs.specs-identical }}
run: |
if [ "$CHECK_BREAKING_CHANGES_RESULT" = "skipped" ] && [ "$SPECS_IDENTICAL" = "true" ]; then
echo "⏭️ check-breaking-changes: skipped (specs are identical)"
elif [ "$CHECK_BREAKING_CHANGES_RESULT" != "success" ] && [ "$MANUAL_LINTER_OVERRIDE_RESULT" = "success" ]; then
echo "⚠️ Manual linter override requested for breaking changes check"
echo "Breaking changes check failed but was manually approved to proceed"
elif [ "$CHECK_BREAKING_CHANGES_RESULT" != "success" ]; then
echo "❌ check-breaking-changes: ${{ needs.check-breaking-changes.result }}"
exit 1
fi
- name: Verify API Spec Check Passed
run: echo "✅ All API specification checks passed successfully!"
70 changes: 0 additions & 70 deletions .github/workflows/pull_request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,73 +41,3 @@ jobs:
VCS_REF=${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max

audit-api-spec:
name: Audit API Spec
runs-on: ubuntu-latest

steps:
- name: Checkout PR
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Cache npm dependencies
id: node-modules-cache
uses: actions/cache@v4
with:
path: '**/node_modules'
key: ${{ runner.os }}-modules-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-modules-
- name: Install Dependencies
if: steps.node-modules-cache.outputs.cache-hit != 'true'
run: npm ci

- name: Download and install vacuum v0.18.1
run: |
curl -L \
--output vacuum.tar.gz \
--silent \
--show-error \
--fail \
https://github.com/daveshanley/vacuum/releases/download/v0.18.1/vacuum_0.18.1_linux_x86_64.tar.gz
tar -xzf vacuum.tar.gz
chmod u+x vacuum
sudo mv vacuum /usr/local/bin/
vacuum version

- name: Generate API spec
run: |
./node_modules/.bin/openapi-generator \
src/masterBitgoExpress/routers/index.ts \
> api-generated.json

- name: Audit with Vacuum
run: |
vacuum report \
--no-style \
--stdout \
--ruleset ruleset.yaml \
api-generated.json > vacuum-report.json

jq '.resultSet.results // []' vacuum-report.json > vacuum-results.json

ERROR_COUNT=$(jq '[.[] | select(.ruleSeverity == "error")] | length' vacuum-results.json)
WARNING_COUNT=$(jq '[.[] | select(.ruleSeverity == "warn")] | length' vacuum-results.json)

echo "Found $ERROR_COUNT error(s) and $WARNING_COUNT warning(s)"

if [ "$ERROR_COUNT" -gt 0 ]; then
echo "API specification audit failed with $ERROR_COUNT error(s)"
echo ""
echo "Errors:"
jq -r '.[] | select(.ruleSeverity == "error") | " - [\(.ruleId)] \(.message) at \(.path)"' vacuum-results.json
exit 1
else
echo "API specification audit passed!"
fi