SCF API v2026.1 — automated pipeline with 249 framework crosswalks #12
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Update SCF | ||
| on: | ||
| schedule: | ||
| # Weekly check: Monday 9:00 UTC | ||
| - cron: "0 9 * * 1" | ||
| workflow_dispatch: | ||
| inputs: | ||
| tag: | ||
| description: "SCF release tag to update to (leave empty for latest)" | ||
| required: false | ||
| type: string | ||
| force: | ||
| description: "Force update even if version matches" | ||
| required: false | ||
| type: boolean | ||
| default: false | ||
| permissions: | ||
| contents: write | ||
| pull-requests: write | ||
| jobs: | ||
| check-and-update: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v4 | ||
| - name: Setup Node.js | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: 20 | ||
| cache: npm | ||
| - name: Install dependencies | ||
| run: npm ci | ||
| - name: Determine target version | ||
| id: version | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| INPUT_TAG: ${{ inputs.tag }} | ||
| run: | | ||
| if [ -n "$INPUT_TAG" ]; then | ||
| NEW_TAG="$INPUT_TAG" | ||
| else | ||
| NEW_TAG=$(gh api repos/securecontrolsframework/securecontrolsframework/releases/latest --jq '.tag_name') | ||
| fi | ||
| CURRENT=$(cat .scf-version) | ||
| echo "new_tag=$NEW_TAG" >> "$GITHUB_OUTPUT" | ||
| echo "current=$CURRENT" >> "$GITHUB_OUTPUT" | ||
| echo "Current: $CURRENT → Target: $NEW_TAG" | ||
| - name: Check if update needed | ||
| id: check | ||
| env: | ||
| NEW_TAG: ${{ steps.version.outputs.new_tag }} | ||
| CURRENT: ${{ steps.version.outputs.current }} | ||
| FORCE: ${{ inputs.force }} | ||
| run: | | ||
| if [ "$NEW_TAG" = "$CURRENT" ] && [ "$FORCE" != "true" ]; then | ||
| echo "skip=true" >> "$GITHUB_OUTPUT" | ||
| echo "Already at version $CURRENT, skipping." | ||
| else | ||
| echo "skip=false" >> "$GITHUB_OUTPUT" | ||
| fi | ||
| - name: Parse SCF Excel | ||
| if: steps.check.outputs.skip != 'true' | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| SCF_TAG: ${{ steps.version.outputs.new_tag }} | ||
| run: node scripts/parse-scf-excel.mjs --tag "$SCF_TAG" | ||
| - name: Build static API | ||
| if: steps.check.outputs.skip != 'true' | ||
| run: npm run build | ||
| - name: Generate change summary | ||
| if: steps.check.outputs.skip != 'true' | ||
| id: summary | ||
| env: | ||
| NEW_TAG: ${{ steps.version.outputs.new_tag }} | ||
| CURRENT: ${{ steps.version.outputs.current }} | ||
| run: | | ||
| CONTROLS=$(node -e "console.log(require('./docs/api/summary.json').total_controls)") | ||
| FAMILIES=$(node -e "console.log(require('./docs/api/summary.json').total_families)") | ||
| FRAMEWORKS=$(node -e "console.log(require('./docs/api/summary.json').crosswalk_frameworks.length)") | ||
| cat > /tmp/pr-body.md <<EOF | ||
| ## SCF Update: ${CURRENT} → ${NEW_TAG} | ||
| ### Stats | ||
| - **Controls:** ${CONTROLS} | ||
| - **Families:** ${FAMILIES} | ||
| - **Framework crosswalks:** ${FRAMEWORKS} | ||
| ### Source | ||
| - [SCF ${NEW_TAG} Release](https://github.com/securecontrolsframework/securecontrolsframework/releases/tag/${NEW_TAG}) | ||
| ### Generated by | ||
| Automated workflow — [update-scf.yml](.github/workflows/update-scf.yml) | ||
| EOF | ||
| - name: Remove old data files | ||
| if: steps.check.outputs.skip != 'true' | ||
| run: | | ||
| NEW_SLUG=$(cat .scf-version | tr '.' '-') | ||
| # Remove any scf-*.json that isn't the current version or crosswalks | ||
| for f in data/scf-*.json; do | ||
| base=$(basename "$f") | ||
| if [ "$base" != "scf-${NEW_SLUG}.json" ] && [ "$base" != "scf-crosswalks.json" ]; then | ||
| echo "Removing old file: $f" | ||
| rm "$f" | ||
| fi | ||
| done | ||
| # Remove defunct individual crosswalk files | ||
| for f in data/hipaa-scf-crosswalk.json data/gdpr-scf-crosswalk.json data/ccpa-scf-crosswalk.json data/nis2-scf-crosswalk.json; do | ||
| [ -f "$f" ] && echo "Removing: $f" && rm "$f" | ||
| done | ||
| - name: Create Pull Request | ||
| if: steps.check.outputs.skip != 'true' | ||
| uses: peter-evans/create-pull-request@v7 | ||
| with: | ||
| token: ${{ secrets.GITHUB_TOKEN }} | ||
| branch: update-scf/${{ steps.version.outputs.new_tag }} | ||
| title: "Update SCF to ${{ steps.version.outputs.new_tag }}" | ||
| body-path: /tmp/pr-body.md | ||
| commit-message: "Update SCF to ${{ steps.version.outputs.new_tag }}" | ||
| labels: automated,scf-update | ||
| delete-branch: true | ||