From 0ae5c29024e060cb3a5da3400fbd532241edbc4f Mon Sep 17 00:00:00 2001 From: Jordan Ritter Date: Thu, 14 May 2026 17:40:38 -0700 Subject: [PATCH 1/3] =?UTF-8?q?ci:=20harden=20existing=20workflows=20?= =?UTF-8?q?=E2=80=94=20SHA=20pins,=20permissions,=20shell=20injection,=20p?= =?UTF-8?q?ersist-credentials?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/check.yml | 62 +++++++----- .github/workflows/deploy.yml | 190 ++++++++++++++++++++--------------- 2 files changed, 145 insertions(+), 107 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 4d4e68534..613e0d031 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -1,4 +1,4 @@ -name: ✅ Check +name: "✅ Check" on: push: branches: @@ -15,30 +15,34 @@ concurrency: cancel-in-progress: true permissions: - contents: write - checks: write + contents: read jobs: lint: - name: 🔬 ESLint + name: "\U0001F52C ESLint" runs-on: ubuntu-latest timeout-minutes: 10 if: ${{ !github.event.pull_request.draft || github.event_name == 'push' }} + permissions: + contents: write + checks: write steps: - - name: 📥 Checkout repo - uses: actions/checkout@v4 + - name: "\U0001F4E5 Checkout repo" + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false - - name: ⚙️ Setup node - uses: actions/setup-node@v4 + - name: "⚙️ Setup node" + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 20 cache: npm - - name: 📥 Download deps + - name: "\U0001F4E5 Download deps" run: npm ci - - name: 🔬 Lint - uses: wearerequired/lint-action@v2 + - name: "\U0001F52C Lint" + uses: wearerequired/lint-action@548d8a7c4b04d3553d32ed5b6e91eb171e10e7bb # v2 with: eslint: true eslint_args: "**/*.{mjs,ts,tsx}" @@ -46,50 +50,54 @@ jobs: prettier_args: "--config ./.prettierrc.json \"**/*.{js,json,ts,tsx,css,md,html}\"" test: - name: 🔎 Test + name: "\U0001F50E Test" runs-on: ubuntu-latest timeout-minutes: 10 if: ${{ !github.event.pull_request.draft || github.event_name == 'push' }} steps: - - name: 📥 Checkout repo - uses: actions/checkout@v4 + - name: "\U0001F4E5 Checkout repo" + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false - - name: ⚙️ Setup node - uses: actions/setup-node@v4 + - name: "⚙️ Setup node" + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 20 cache: npm - - name: 📥 Download deps + - name: "\U0001F4E5 Download deps" run: npm ci - - name: 🎭 Install Playwright browsers + - name: "\U0001F3AD Install Playwright browsers" run: npx playwright install chromium chromium-headless-shell - - name: 🔎 Unit Tests + - name: "\U0001F50E Unit Tests" run: CI=1 npm test -- --run - - name: 🔎 Integration Tests + - name: "\U0001F50E Integration Tests" run: CI=1 npm run test:integration -- --run build: - name: 🔨 Build + name: "\U0001F528 Build" timeout-minutes: 10 runs-on: ubuntu-latest if: ${{ !github.event.pull_request.draft || github.event_name == 'push' }} steps: - - name: 📥 Checkout repo - uses: actions/checkout@v4 + - name: "\U0001F4E5 Checkout repo" + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false - - name: ⚙️ Setup node - uses: actions/setup-node@v4 + - name: "⚙️ Setup node" + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 20 cache: npm - - name: 📥 Download deps + - name: "\U0001F4E5 Download deps" run: npm ci - - name: 🔨 Build + - name: "\U0001F528 Build" run: npm run build diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f65446220..67d3aa9c6 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,4 +1,4 @@ -name: 🚀 Deploy +name: "\U0001F680 Deploy" on: push: branches: @@ -9,37 +9,38 @@ concurrency: cancel-in-progress: true permissions: - pages: write - contents: write + contents: read jobs: # ─── Documentation ─────────────────────────────────────────────── build-doc: - name: 🔨 Build (doc) + name: "\U0001F528 Build (doc)" runs-on: ubuntu-latest timeout-minutes: 5 steps: - - name: 📥 Checkout repo - uses: actions/checkout@v4 + - name: "\U0001F4E5 Checkout repo" + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false # No cache — deploy builds must be hermetic - - name: ⚙️ Setup node - uses: actions/setup-node@v4 + - name: "⚙️ Setup node" + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 22 - - name: 📥 Download deps + - name: "\U0001F4E5 Download deps" run: make doc-install - - name: 🔨 Build Registry + - name: "\U0001F528 Build Registry" run: make registry-build - - name: 🔨 Build Documentation + - name: "\U0001F528 Build Documentation" run: make doc-build - - name: 📤 Upload doc artifacts - uses: actions/upload-artifact@v4 + - name: "\U0001F4E4 Upload doc artifacts" + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: doc-artifacts path: | @@ -47,35 +48,42 @@ jobs: public/r deploy-doc: - name: 🚀 Deploy (doc) + name: "\U0001F680 Deploy (doc)" runs-on: ubuntu-latest timeout-minutes: 5 needs: build-doc + permissions: + contents: write + pages: write steps: - - name: 📥 Checkout repo - uses: actions/checkout@v4 + - name: "\U0001F4E5 Checkout repo" + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false - - name: ⚙️ Setup git + - name: "⚙️ Setup git" run: | - git remote set-url origin https://git:${{ secrets.GITHUB_TOKEN }}@github.com/${GITHUB_REPOSITORY} + git remote set-url origin "https://git:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}" git config --global user.email support+actions@github.com git config --global user.name github-actions-bot + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: 📥 Download doc artifacts - uses: actions/download-artifact@v4 + - name: "\U0001F4E5 Download doc artifacts" + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: name: doc-artifacts - - name: 📡 Deploy Registry + - name: "\U0001F4E1 Deploy Registry" run: cd public/r && npx gh-pages -b gh-pages -d ./ -s atomic-crm.json -e r -m "Deploy registry" --remove r - - name: 📡 Deploy Documentation + - name: "\U0001F4E1 Deploy Documentation" run: cd doc && npx gh-pages -b gh-pages -d dist -e doc -m "Deploy docs" --remove doc # ─── Demo ──────────────────────────────────────────────────────── build-demo: - name: 🔨 Build (demo) + name: "\U0001F528 Build (demo)" runs-on: ubuntu-latest timeout-minutes: 10 @@ -84,68 +92,79 @@ jobs: VITE_INBOUND_EMAIL: ${{ vars.VITE_INBOUND_EMAIL }} steps: - - name: 📥 Checkout repo - uses: actions/checkout@v4 + - name: "\U0001F4E5 Checkout repo" + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false # No cache — deploy builds must be hermetic - - name: ⚙️ Setup node - uses: actions/setup-node@v4 + - name: "⚙️ Setup node" + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 22 - - name: 📥 Download deps + - name: "\U0001F4E5 Download deps" run: npm ci - - name: 🔨 Build + - name: "\U0001F528 Build" run: npm run build:demo - - name: 📤 Upload demo artifacts - uses: actions/upload-artifact@v4 + - name: "\U0001F4E4 Upload demo artifacts" + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: demo-artifacts path: dist deploy-demo: - name: 🚀 Deploy (demo) + name: "\U0001F680 Deploy (demo)" if: ${{ vars.DEMO_DEPLOY_REPOSITORY != '' }} runs-on: ubuntu-latest timeout-minutes: 5 needs: build-demo + permissions: + contents: write + pages: write env: PRODUCTION_REMOTE: https://git:${{ secrets.DEPLOY_TOKEN || secrets.GITHUB_TOKEN }}@github.com/${{ vars.DEMO_DEPLOY_REPOSITORY }} steps: - - name: 📥 Checkout repo - uses: actions/checkout@v4 + - name: "\U0001F4E5 Checkout repo" + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false - - name: ⚙️ Setup node - uses: actions/setup-node@v4 + - name: "⚙️ Setup node" + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 22 - - name: ⚙️ Setup git + - name: "⚙️ Setup git" run: | - echo Deploying to pages to ${{ vars.DEMO_DEPLOY_REPOSITORY }} - git remote set-url origin https://git:${{ secrets.DEPLOY_TOKEN || secrets.GITHUB_TOKEN }}@github.com/${{ vars.DEMO_DEPLOY_REPOSITORY }} - git remote add production $PRODUCTION_REMOTE + echo "Deploying to pages to $DEMO_DEPLOY_REPOSITORY" + git remote set-url origin "https://git:${DEPLOY_TOKEN}@github.com/${DEMO_DEPLOY_REPOSITORY}" + git remote add production "$PRODUCTION_REMOTE" git config --global user.email support+actions@github.com git config --global user.name github-actions-bot + env: + DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN || secrets.GITHUB_TOKEN }} + DEMO_DEPLOY_REPOSITORY: ${{ vars.DEMO_DEPLOY_REPOSITORY }} - - name: 📥 Download demo artifacts - uses: actions/download-artifact@v4 + - name: "\U0001F4E5 Download demo artifacts" + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: name: demo-artifacts path: dist - - name: 📡 Deploy GitHub pages - run: npx gh-pages --remote production -d dist -b ${{ vars.DEPLOY_BRANCH || 'gh-pages' }} + - name: "\U0001F4E1 Deploy GitHub pages" + run: npx gh-pages --remote production -d dist -b "${DEPLOY_BRANCH:-gh-pages}" env: GITHUB_TOKEN: ${{ secrets.DEPLOY_TOKEN || secrets.GITHUB_TOKEN }} + DEPLOY_BRANCH: ${{ vars.DEPLOY_BRANCH }} # ─── Supabase ──────────────────────────────────────────────────── build-supabase: - name: 🔨 Build (supabase) + name: "\U0001F528 Build (supabase)" runs-on: ubuntu-latest timeout-minutes: 10 @@ -158,33 +177,38 @@ jobs: VITE_INBOUND_EMAIL: ${{ vars.VITE_INBOUND_EMAIL }} steps: - - name: 📥 Checkout repo - uses: actions/checkout@v4 + - name: "\U0001F4E5 Checkout repo" + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false # No cache — deploy builds must be hermetic - - name: ⚙️ Setup node - uses: actions/setup-node@v4 + - name: "⚙️ Setup node" + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 22 - - name: 📥 Download deps + - name: "\U0001F4E5 Download deps" run: npm ci - - name: 🔨 Build + - name: "\U0001F528 Build" run: npm run build - - name: 📤 Upload supabase artifacts - uses: actions/upload-artifact@v4 + - name: "\U0001F4E4 Upload supabase artifacts" + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: supabase-artifacts path: dist deploy-supabase: - name: 🚀 Deploy (supabase) + name: "\U0001F680 Deploy (supabase)" if: ${{ vars.DEPLOY_REPOSITORY != '' }} runs-on: ubuntu-latest timeout-minutes: 10 needs: build-supabase + permissions: + contents: write + pages: write env: SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }} @@ -196,50 +220,55 @@ jobs: VITE_IS_DEMO: ${{ vars.VITE_IS_DEMO }} VITE_INBOUND_EMAIL: ${{ vars.VITE_INBOUND_EMAIL }} PRODUCTION_REMOTE: https://git:${{ secrets.DEPLOY_TOKEN || secrets.GITHUB_TOKEN }}@github.com/${{ vars.DEPLOY_REPOSITORY }} - POSTMARK_WEBHOOK_USER: ${{secrets.POSTMARK_WEBHOOK_USER}} - POSTMARK_WEBHOOK_PASSWORD: ${{secrets.POSTMARK_WEBHOOK_PASSWORD}} - POSTMARK_WEBHOOK_AUTHORIZED_IPS: ${{secrets.POSTMARK_WEBHOOK_AUTHORIZED_IPS}} - VITE_GOOGLE_WORKPLACE_DOMAIN: ${{secrets.VITE_GOOGLE_WORKPLACE_DOMAIN}} + POSTMARK_WEBHOOK_USER: ${{ secrets.POSTMARK_WEBHOOK_USER }} + POSTMARK_WEBHOOK_PASSWORD: ${{ secrets.POSTMARK_WEBHOOK_PASSWORD }} + POSTMARK_WEBHOOK_AUTHORIZED_IPS: ${{ secrets.POSTMARK_WEBHOOK_AUTHORIZED_IPS }} + VITE_GOOGLE_WORKPLACE_DOMAIN: ${{ secrets.VITE_GOOGLE_WORKPLACE_DOMAIN }} steps: - - name: 📥 Checkout repo - uses: actions/checkout@v4 + - name: "\U0001F4E5 Checkout repo" + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false - - name: ⚙️ Setup git + - name: "⚙️ Setup git" run: | - echo Deploying to pages to ${{ vars.DEPLOY_REPOSITORY }} - git remote set-url origin https://git:${{ secrets.DEPLOY_TOKEN || secrets.GITHUB_TOKEN }}@github.com/${{ vars.DEPLOY_REPOSITORY }} - git remote add production $PRODUCTION_REMOTE + echo "Deploying to pages to $DEPLOY_REPOSITORY" + git remote set-url origin "https://git:${DEPLOY_TOKEN}@github.com/${DEPLOY_REPOSITORY}" + git remote add production "$PRODUCTION_REMOTE" git config --global user.email support+actions@github.com git config --global user.name github-actions-bot + env: + DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN || secrets.GITHUB_TOKEN }} + DEPLOY_REPOSITORY: ${{ vars.DEPLOY_REPOSITORY }} - - name: ⚙️ Setup supabase - uses: supabase/setup-cli@v1 + - name: "⚙️ Setup supabase" + uses: supabase/setup-cli@b60b5899c73b63a2d2d651b1e90db8d4c9392f51 # v1.6.0 - - name: 📥 Download supabase artifacts - uses: actions/download-artifact@v4 + - name: "\U0001F4E5 Download supabase artifacts" + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: name: supabase-artifacts path: dist - if: ${{ env.IS_SUPABASE_CONFIGURED }} - name: 🔗 Supabase Link + name: "\U0001F517 Supabase Link" run: npx supabase link --project-ref $SUPABASE_PROJECT_ID - if: ${{ env.IS_SUPABASE_CONFIGURED }} - name: 📡 Push supabase migrations + name: "\U0001F4E1 Push supabase migrations" run: npx supabase db push - if: ${{ env.IS_SUPABASE_CONFIGURED && env.POSTMARK_WEBHOOK_USER && env.POSTMARK_WEBHOOK_PASSWORD && env.POSTMARK_WEBHOOK_AUTHORIZED_IPS }} - name: 📡 Push supabase postmark webhook user secret + name: "\U0001F4E1 Push supabase postmark webhook user secret" run: | - npx supabase secrets set POSTMARK_WEBHOOK_USER=${{ env.POSTMARK_WEBHOOK_USER }} - npx supabase secrets set POSTMARK_WEBHOOK_PASSWORD=${{ env.POSTMARK_WEBHOOK_PASSWORD }} - npx supabase secrets set POSTMARK_WEBHOOK_AUTHORIZED_IPS=${{ env.POSTMARK_WEBHOOK_AUTHORIZED_IPS }} - npx supabase secrets set VITE_INBOUND_EMAIL=${{ env.VITE_INBOUND_EMAIL }} + npx supabase secrets set "POSTMARK_WEBHOOK_USER=$POSTMARK_WEBHOOK_USER" + npx supabase secrets set "POSTMARK_WEBHOOK_PASSWORD=$POSTMARK_WEBHOOK_PASSWORD" + npx supabase secrets set "POSTMARK_WEBHOOK_AUTHORIZED_IPS=$POSTMARK_WEBHOOK_AUTHORIZED_IPS" + npx supabase secrets set "VITE_INBOUND_EMAIL=$VITE_INBOUND_EMAIL" - if: ${{ env.IS_SUPABASE_CONFIGURED }} - name: 📡 Deploy supabase functions + name: "\U0001F4E1 Deploy supabase functions" run: npx supabase functions deploy - if: ${{ !env.SUPABASE_ACCESS_TOKEN }} @@ -267,18 +296,19 @@ jobs: run: echo ':warning:POSTMARK_WEBHOOK_USER secret is missing' >> $GITHUB_STEP_SUMMARY - if: ${{ !env.POSTMARK_WEBHOOK_PASSWORD }} - name: Check POSTMARK_WEBHOOK_USER secret + name: Check POSTMARK_WEBHOOK_PASSWORD secret run: echo ':warning:POSTMARK_WEBHOOK_PASSWORD secret is missing' >> $GITHUB_STEP_SUMMARY - if: ${{ !env.POSTMARK_WEBHOOK_AUTHORIZED_IPS }} - name: Check POSTMARK_WEBHOOK_USER secret + name: Check POSTMARK_WEBHOOK_AUTHORIZED_IPS secret run: echo ':warning:POSTMARK_WEBHOOK_AUTHORIZED_IPS secret is missing' >> $GITHUB_STEP_SUMMARY - if: ${{ !env.IS_SUPABASE_CONFIGURED }} name: Supabase deployment skipped run: echo ':warning:Supabase deployment skipped' >> $GITHUB_STEP_SUMMARY - - name: 📡 Deploy GitHub pages - run: npx gh-pages --remote production -d dist -b ${{ vars.DEPLOY_BRANCH || 'gh-pages' }} + - name: "\U0001F4E1 Deploy GitHub pages" + run: npx gh-pages --remote production -d dist -b "${DEPLOY_BRANCH:-gh-pages}" env: GITHUB_TOKEN: ${{ secrets.DEPLOY_TOKEN || secrets.GITHUB_TOKEN }} + DEPLOY_BRANCH: ${{ vars.DEPLOY_BRANCH }} From 53c51982e8b5d9c8c1afa855950b00643d37833d Mon Sep 17 00:00:00 2001 From: Jordan Ritter Date: Thu, 14 May 2026 17:40:38 -0700 Subject: [PATCH 2/3] ci: add zizmor static analysis for workflow security --- .github/workflows/security_zizmor.yml | 47 +++++++++++++++++++++++++++ .github/zizmor.yml | 9 +++++ 2 files changed, 56 insertions(+) create mode 100644 .github/workflows/security_zizmor.yml create mode 100644 .github/zizmor.yml diff --git a/.github/workflows/security_zizmor.yml b/.github/workflows/security_zizmor.yml new file mode 100644 index 000000000..1474636c0 --- /dev/null +++ b/.github/workflows/security_zizmor.yml @@ -0,0 +1,47 @@ +name: security / zizmor + +on: + push: + branches: [main, master] + paths: + - ".github/workflows/**" + - ".github/actions/**" + - ".github/zizmor.yml" + - ".github/dependabot.yml" + pull_request: + paths: + - ".github/workflows/**" + - ".github/actions/**" + - ".github/zizmor.yml" + - ".github/dependabot.yml" + schedule: + - cron: "0 9 * * 1" + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event_name == 'schedule' && github.run_id || github.ref }} + cancel-in-progress: ${{ github.event_name != 'schedule' }} + +permissions: + contents: read + +jobs: + zizmor: + name: Static analysis (zizmor) + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false + + - name: Run zizmor + uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3 + with: + min-severity: medium + advanced-security: false + annotations: true + config: .github/zizmor.yml diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 000000000..118b4ac90 --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,9 @@ +rules: + dangerous-triggers: + ignore: + # Dependabot auto-merge: uses pull_request_target for write token. + # Does NOT checkout PR code. Actor-gated to dependabot[bot]. + - dependabot-auto-merge.yml + # Dependabot major analysis: uses pull_request_target for PR comments. + # Does NOT checkout PR code. Actor-gated to dependabot[bot]. + - dependabot-major-analysis.yml From d5018d515b5014c5eb830a649cb0c423c60c0219 Mon Sep 17 00:00:00 2001 From: Jordan Ritter Date: Thu, 14 May 2026 17:40:38 -0700 Subject: [PATCH 3/3] ci: add Dependabot daily with auto-merge and major-version analysis --- .github/dependabot.yml | 32 ++++ .github/workflows/dependabot-auto-merge.yml | 32 ++++ .../workflows/dependabot-major-analysis.yml | 144 ++++++++++++++++++ 3 files changed, 208 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/dependabot-auto-merge.yml create mode 100644 .github/workflows/dependabot-major-analysis.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..e392e17ef --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,32 @@ +version: 2 + +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + groups: + minor-and-patch: + patterns: + - "*" + update-types: + - "minor" + - "patch" + # Workaround for dependabot/dependabot-core#14202: without an explicit + # major group, major updates matching the minor-and-patch pattern are + # silently suppressed. Remove this group when #14202 is fixed to get + # individual (ungrouped) PRs per major bump instead. + major: + patterns: + - "*" + update-types: + - "major" + labels: + - "dependencies" + - "github-actions" + commit-message: + prefix: "ci" + include: "scope" + open-pull-requests-limit: 10 + cooldown: + default-days: 1 diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 000000000..218916149 --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,32 @@ +name: Dependabot Auto-Merge (Minor/Patch) + +on: + pull_request_target: + types: [opened, synchronize] + +permissions: + contents: write + pull-requests: write + +jobs: + auto-merge: + runs-on: ubuntu-latest + if: github.event.pull_request.user.login == 'dependabot[bot]' + steps: + - name: Fetch Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a # v2.5.0 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Auto-approve and merge minor/patch github-actions updates + if: >- + steps.metadata.outputs.package-ecosystem == 'github_actions' && + (steps.metadata.outputs.update-type == 'version-update:semver-minor' || + steps.metadata.outputs.update-type == 'version-update:semver-patch') + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_URL: ${{ github.event.pull_request.html_url }} + run: | + gh pr review "$PR_URL" --approve + gh pr merge "$PR_URL" --auto --merge diff --git a/.github/workflows/dependabot-major-analysis.yml b/.github/workflows/dependabot-major-analysis.yml new file mode 100644 index 000000000..8e1719287 --- /dev/null +++ b/.github/workflows/dependabot-major-analysis.yml @@ -0,0 +1,144 @@ +name: Dependabot Major Version Analysis + +on: + pull_request_target: + types: [opened] + +permissions: + contents: read + pull-requests: write + +jobs: + analyze-major: + runs-on: ubuntu-latest + if: github.event.pull_request.user.login == 'dependabot[bot]' + steps: + - name: Fetch Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a # v2.5.0 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Analyze major version bump + if: >- + steps.metadata.outputs.package-ecosystem == 'github_actions' && + steps.metadata.outputs.update-type == 'version-update:semver-major' + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + env: + DEP_NAME: ${{ steps.metadata.outputs.dependency-names }} + PREV_VERSION: ${{ steps.metadata.outputs.previous-version }} + NEW_VERSION: ${{ steps.metadata.outputs.new-version }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const depName = process.env.DEP_NAME; + const prevVersion = process.env.PREV_VERSION; + const newVersion = process.env.NEW_VERSION; + const parts = depName.split('/'); + const owner = parts[0]; + const repo = parts[1]; + const repoSlug = `${owner}/${repo}`; + + let releases = []; + try { + const { data } = await github.rest.repos.listReleases({ owner, repo, per_page: 50 }); + releases = data; + } catch (err) { + core.warning(`Could not fetch releases for ${repoSlug}: ${err.message}`); + } + + const prevMajor = parseInt(prevVersion.replace(/^v/, ''), 10); + const newMajor = parseInt(newVersion.replace(/^v/, ''), 10); + + const relevantReleases = releases.filter(r => { + const major = parseInt(r.tag_name.replace(/^v/, ''), 10); + return major > prevMajor && major <= newMajor; + }); + + let releaseNotesSummary = ''; + let breakingChanges = ''; + + if (relevantReleases.length === 0) { + releaseNotesSummary = '_No releases found between these versions._'; + breakingChanges = `_Unable to determine breaking changes automatically. Please review the [full changelog](https://github.com/${repoSlug}/releases)._`; + } else { + for (const release of relevantReleases.slice(0, 10)) { + const body = release.body || '_No release notes._'; + releaseNotesSummary += `### ${release.tag_name}${release.name && release.name !== release.tag_name ? ' — ' + release.name : ''}\n\n`; + releaseNotesSummary += body.substring(0, 2000); + if (body.length > 2000) releaseNotesSummary += '\n\n_...truncated_'; + releaseNotesSummary += '\n\n---\n\n'; + const lines = body.split('\n'); + for (const line of lines) { + if (/breaking|BREAKING|removed|deprecated|incompatible|migration/i.test(line)) { + breakingChanges += `- ${line.trim()}\n`; + } + } + } + } + + if (!breakingChanges) { + breakingChanges = '_No explicit breaking changes detected in release notes. Manual review recommended._'; + } + + let commentBody = `## :warning: Major Version Update — Manual Review Required + + | Field | Value | + |-------|-------| + | **Action** | [\`${depName}\`](https://github.com/${repoSlug}) | + | **Previous** | \`v${prevVersion}\` | + | **New** | \`v${newVersion}\` | + | **Type** | Major (\`v${prevMajor}\` → \`v${newMajor}\`) | + + ### Breaking Changes + + ${breakingChanges} + + ### Release Notes (v${prevMajor + 1} → v${newMajor}) + + ${releaseNotesSummary} + + ### Next Steps + + 1. Review breaking changes above + 2. Check if workflow inputs/outputs changed + 3. Verify compatibility with your CI/CD configuration + + > Full changelog: https://github.com/${repoSlug}/releases + + --- + _Generated automatically for Dependabot major version PRs._`.replace(/^ /gm, ''); + + if (commentBody.length > 64000) { + commentBody = commentBody.substring(0, 63900) + '\n\n_...comment truncated due to size limit._'; + } + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + body: commentBody, + }); + + try { + const labelsToAdd = ['major-update', 'needs-review']; + for (const label of labelsToAdd) { + try { + await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label }); + } catch { + const colors = { 'major-update': 'B60205', 'needs-review': 'FBCA04' }; + await github.rest.issues.createLabel({ + owner: context.repo.owner, repo: context.repo.repo, + name: label, color: colors[label] || 'EDEDED', + }); + } + } + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + labels: labelsToAdd, + }); + } catch (err) { + core.warning(`Could not add labels: ${err.message}`); + }