diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 21030f4..3eb8f80 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,23 +1,97 @@ -# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file -version: 2 -updates: - - # main -- package-ecosystem: "nuget" - target-branch: "main" - directory: "/" - schedule: - interval: "daily" - groups: - nuget-deps: - patterns: - - "*" -- package-ecosystem: "github-actions" - target-branch: "main" - directory: "/" - schedule: - interval: "daily" - groups: - actions-deps: - patterns: - - "*" +# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file +# +# Every ecosystem appears **twice**: once with `target-branch: "main"` +# and once with `target-branch: "develop"`. Dependabot will open +# parallel PRs against each branch, so both stay current on +# dependency versions independently of the develop -> main release +# cadence. +# +# Why dual-target and not develop-only: +# - `develop` is the integration branch and ships content forward to +# `main` through merge-commit releases, but the time between releases +# can be long (a feature branch may sit on develop for weeks). +# - Push-distribution channels (e.g. Docker Hub, distros that pull from +# main) consume `main` directly. If `main` only got dependency bumps +# via the next develop -> main release, those channels would ship +# outdated code in the interim. +# - Codegen workflows take the same dual-target shape for the same +# reason - see .github/workflows/run-codegen-pull-request-task.yml. +# +# The merge-bot's `case` statement in +# .github/workflows/merge-bot-pull-request.yml dispatches the merge +# method per base ref (squash on develop, merge on main) so both bases +# auto-merge cleanly. `develop` remains strictly forward-only: there +# are no main -> develop back-merges; each branch absorbs its own +# Dependabot PRs and codegen PRs independently. +# +# Security update PRs (CVE-driven) are opened by Dependabot against +# the repo default branch (`main`) regardless of any `target-branch` +# config - the `case` statement handles them in the same code path. +version: 2 +updates: + + # ----- nuget ----- + + - package-ecosystem: "nuget" + target-branch: "main" + directory: "/" + schedule: + interval: "daily" + groups: + nuget-deps: + patterns: + - "*" + + - package-ecosystem: "nuget" + target-branch: "develop" + directory: "/" + schedule: + interval: "daily" + groups: + nuget-deps: + patterns: + - "*" + + # ----- github-actions ----- + + - package-ecosystem: "github-actions" + target-branch: "main" + directory: "/" + schedule: + interval: "daily" + groups: + actions-deps: + patterns: + - "*" + + - package-ecosystem: "github-actions" + target-branch: "develop" + directory: "/" + schedule: + interval: "daily" + groups: + actions-deps: + patterns: + - "*" + + # ----- docker (Docker/*.Dockerfile) ----- + + - package-ecosystem: "docker" + target-branch: "main" + directory: "/Docker" + schedule: + interval: "daily" + groups: + docker-deps: + patterns: + - "*" + + - package-ecosystem: "docker" + target-branch: "develop" + directory: "/Docker" + schedule: + interval: "daily" + groups: + docker-deps: + patterns: + - "*" diff --git a/.github/workflows/merge-bot-pull-request.yml b/.github/workflows/merge-bot-pull-request.yml index 36b61cd..e006257 100644 --- a/.github/workflows/merge-bot-pull-request.yml +++ b/.github/workflows/merge-bot-pull-request.yml @@ -1,50 +1,205 @@ -name: Merge bot pull request action - -on: - pull_request: - types: [opened, reopened, synchronize] - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - - merge-dependabot: - name: Merge dependabot pull request job - runs-on: ubuntu-latest - if: github.actor == 'dependabot[bot]' && github.event.pull_request.head.repo.full_name == github.repository - permissions: - contents: write - pull-requests: write - - steps: - - - name: Get dependabot metadata step - id: metadata - uses: dependabot/fetch-metadata@v2 - with: - github-token: "${{ secrets.GITHUB_TOKEN }}" - - - name: Merge pull request step - if: steps.metadata.outputs.update-type != 'version-update:semver-major' - run: gh pr merge --auto --squash "$PR_URL" - env: - PR_URL: ${{github.event.pull_request.html_url}} - GH_TOKEN: ${{secrets.GITHUB_TOKEN}} - - merge-codegen: - name: Merge codegen pull request job - runs-on: ubuntu-latest - if: github.actor == 'ptr727' && github.event.pull_request.user.login == 'ptr727' && github.event.pull_request.head.ref == 'codegen' && github.event.pull_request.base.ref == 'main' && github.event.pull_request.head.repo.full_name == github.repository - permissions: - contents: write - pull-requests: write - - steps: - - - name: Merge pull request step - run: gh pr merge --auto --squash "$PR_URL" - env: - PR_URL: ${{github.event.pull_request.html_url}} - GH_TOKEN: ${{secrets.GITHUB_TOKEN}} +name: Merge bot pull request action + +# Three-job model: +# 1. `merge-dependabot` / `merge-codegen` run on `opened` and `reopened` +# events only. They enable auto-merge via `gh pr merge --auto` once +# per PR. Restricting to open/reopen (skipping `synchronize`) is what +# makes step 3 below stick — if these jobs re-ran on every +# `synchronize`, they'd undo a maintainer-triggered disable. +# 2. The merge method (`--squash` vs `--merge`) is dispatched by a +# `case` statement on `pull_request.base.ref` so the form matches +# each branch's ruleset (develop = squash-only, main = merge-only). +# Both Dependabot and codegen open parallel PRs against both +# branches; Dependabot security updates always target `main` and +# flow through the same code path. +# 3. `disable-auto-merge-on-maintainer-push` runs on `synchronize` +# events against bot-authored PRs when the event actor is NOT the +# same bot — i.e. a maintainer pushed commits to a bot PR. It +# calls `gh pr merge --disable-auto` so the maintainer's commits +# don't auto-merge along with the bot's content. The maintainer +# re-enables auto-merge manually (UI or `gh pr merge --auto`) +# when ready. +# +# Token strategy: +# Every job uses an App token (`actions/create-github-app-token`). +# The resulting push is committed by the App, which fires downstream +# workflows on develop and main. `GITHUB_TOKEN`-authored pushes are +# blocked from triggering further workflow runs by GitHub's recursion +# guard, which would silently skip `publish-release.yml` on the merge +# commit. The App-token path also removes the close/reopen dance +# previously used by codegen PRs created under `GITHUB_TOKEN` to nudge +# the auto-merge workflow. The disable job needs an App token too: +# even though the event actor is a maintainer, the workflow context +# on a Dependabot PR runs with Dependabot's restricted secrets +# regardless of actor, so plain `GITHUB_TOKEN` would be read-only. + +on: + pull_request: + types: [opened, reopened, synchronize] + +# `cancel-in-progress: false` is load-bearing. The three-job model +# (enable on opened/reopened, disable on maintainer-triggered +# synchronize) relies on those events running to completion in arrival +# order. With cancel-in-progress: true, a fast follow-up synchronize +# (e.g. a Dependabot rebase right after PR open) would cancel the +# in-flight `opened` run before it reached `gh pr merge --auto`, and +# the new synchronize run skips the enable jobs (opened/reopened +# filter), leaving auto-merge never enabled. Queueing instead of +# cancelling makes the final state deterministic: opened enables, +# then any subsequent synchronize disables (if maintainer) or no-ops +# (if bot). Action-aware grouping has its own race (opened finishing +# after a maintainer synchronize would re-enable auto-merge), so we +# keep a single group and just disable cancellation. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + + merge-dependabot: + name: Merge dependabot pull request job + runs-on: ubuntu-latest + # Restrict to Dependabot PRs that originate from this repository, not + # a fork. Only runs on `opened` / `reopened` events so the auto-merge + # enable happens once per PR; the `disable-auto-merge-on-maintainer-push` + # job below is what disables auto-merge when a maintainer pushes to a + # Dependabot branch. Skipping `synchronize` here is what keeps that + # disable sticky. + if: >- + (github.event.action == 'opened' || github.event.action == 'reopened') && + github.event.pull_request.user.login == 'dependabot[bot]' && + github.event.pull_request.head.repo.full_name == github.repository + permissions: + contents: write + pull-requests: write + + steps: + + - name: Generate GitHub App token step + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ secrets.CODEGEN_APP_CLIENT_ID }} + private-key: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }} + + - name: Get dependabot metadata step + id: metadata + uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a # v2.5.0 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + + # Skip semver-major NuGet bumps: majors can build cleanly but break + # runtime behavior, so they should land via human review. Other + # ecosystems' majors (github-actions, docker) are usually safe and merge. + - name: Merge pull request step + if: >- + (steps.metadata.outputs.package-ecosystem != 'nuget') || + (steps.metadata.outputs.update-type != 'version-update:semver-major') + run: | + set -euo pipefail + case "${{ github.event.pull_request.base.ref }}" in + develop) method=--squash ;; + main) method=--merge ;; + *) + echo "::error::Unsupported base branch: ${{ github.event.pull_request.base.ref }}" + exit 1 + ;; + esac + gh pr merge --auto "$method" "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} + + merge-codegen: + name: Merge codegen pull request job + runs-on: ubuntu-latest + # Restrict to codegen PRs that originate from the App in this + # repository. Codegen runs in a matrix over `main` and `develop`, + # so two head refs are valid: `codegen-main` (always targets `main`) + # and `codegen-develop` (always targets `develop`). The head/base + # pairing is enforced strictly so a misconfigured workflow can't, + # for example, sneak a `codegen-develop` branch into `main`. + # Only runs on `opened` / `reopened` events so the auto-merge enable + # happens once per PR; the `disable-auto-merge-on-maintainer-push` + # job below is what disables auto-merge when a maintainer pushes to a + # codegen branch. Skipping `synchronize` here is what keeps that + # disable sticky. + if: >- + (github.event.action == 'opened' || github.event.action == 'reopened') && + github.event.pull_request.user.login == 'ptr727-codegen[bot]' && + github.event.pull_request.head.repo.full_name == github.repository && + ( + (github.event.pull_request.head.ref == 'codegen-main' && github.event.pull_request.base.ref == 'main') || + (github.event.pull_request.head.ref == 'codegen-develop' && github.event.pull_request.base.ref == 'develop') + ) + permissions: + contents: write + pull-requests: write + + steps: + + - name: Generate GitHub App token step + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ secrets.CODEGEN_APP_CLIENT_ID }} + private-key: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }} + + - name: Merge pull request step + run: | + set -euo pipefail + case "${{ github.event.pull_request.base.ref }}" in + develop) method=--squash ;; + main) method=--merge ;; + *) + echo "::error::Unsupported base branch: ${{ github.event.pull_request.base.ref }}" + exit 1 + ;; + esac + gh pr merge --auto "$method" "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} + + disable-auto-merge-on-maintainer-push: + name: Disable auto-merge on maintainer push job + runs-on: ubuntu-latest + # Fires on `synchronize` events against bot-authored PRs (Dependabot + # or codegen) when the event actor is NOT the same bot — i.e. a + # maintainer pushed commits to the bot's branch. Disables auto-merge + # so the maintainer's commits don't auto-merge along with the bot's + # content. The maintainer re-enables auto-merge manually when ready + # (UI button, or `gh pr merge --auto `). + # + # `gh pr merge --disable-auto` is idempotent — calling it on a PR + # that already has auto-merge disabled is a no-op. + if: >- + github.event.action == 'synchronize' && + github.event.pull_request.head.repo.full_name == github.repository && + ( + github.event.pull_request.user.login == 'dependabot[bot]' || + github.event.pull_request.user.login == 'ptr727-codegen[bot]' + ) && + github.actor != github.event.pull_request.user.login + permissions: + pull-requests: write + + steps: + + - name: Generate GitHub App token step + # App token rather than GITHUB_TOKEN: on a Dependabot PR the + # workflow context runs with Dependabot's restricted secrets + # regardless of who triggered the event (GitHub gates by PR + # origin, not by event actor), and the restricted GITHUB_TOKEN + # is read-only. Same App token pattern as the other merge jobs. + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ secrets.CODEGEN_APP_CLIENT_ID }} + private-key: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }} + + - name: Disable auto-merge step + run: gh pr merge --disable-auto "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} diff --git a/.github/workflows/run-codegen-pull-request-task.yml b/.github/workflows/run-codegen-pull-request-task.yml index 12a50e0..4e01f79 100644 --- a/.github/workflows/run-codegen-pull-request-task.yml +++ b/.github/workflows/run-codegen-pull-request-task.yml @@ -1,49 +1,89 @@ -name: Run codegen and pull request task - -on: - workflow_call: - secrets: - WORKFLOW_PAT: - required: true - -jobs: - - codegen: - name: Run codegen and pull request job - runs-on: ubuntu-latest - - steps: - - - name: Setup .NET SDK step - uses: actions/setup-dotnet@v5 - with: - dotnet-version: 10.x - - - name: Checkout code step - uses: actions/checkout@v6 - with: - ref: main - - - name: Run codegen step - run: | - dotnet run --project ./CreateMatrix/CreateMatrix.csproj -- \ - matrix --versionpath=./Make/Version.json --matrixpath=./Make/Matrix.json --updateversion - - - name: Format code step - run: | - dotnet tool restore - dotnet husky install - dotnet csharpier format --log-level=debug . - git status - - - name: Create pull request step - uses: peter-evans/create-pull-request@v8 - with: - token: ${{ secrets.WORKFLOW_PAT }} - base: main - branch: codegen - title: 'Update codegen files' - body: 'This PR updates the codegen files.' - commit-message: 'Update codegen files' - delete-branch: true - sign-commits: true +name: Run codegen and pull request task + +# Runs codegen against `main` and `develop` in parallel via a matrix, +# opens a PR against each base (`codegen-main` branch -> main, +# `codegen-develop` branch -> develop). The merge-bot auto-merges +# either PR independently. This keeps both branches current on +# generated content (Nx product matrix, version data) without either +# branch falling behind the other and without main -> develop +# back-merges. + +on: + workflow_call: + secrets: + # GitHub App credentials to generate an installation token. + # The App-token-driven PR open fires `pull_request` workflow + # events directly. `GITHUB_TOKEN`-driven PR opens do not (GitHub's + # recursion guard), which previously required a close/reopen dance + # under a PAT to nudge the auto-merge workflow. + CODEGEN_APP_CLIENT_ID: + required: true + CODEGEN_APP_PRIVATE_KEY: + required: true + +jobs: + + codegen: + name: Run ${{ matrix.target.ref }} codegen and pull request job + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + strategy: + # Each branch gets its own parallel codegen run + PR. If one + # branch's PR fails (CI, conflicts, etc.) the other is unaffected. + fail-fast: false + matrix: + target: + - ref: main + branch: codegen-main + - ref: develop + branch: codegen-develop + + steps: + + - name: Generate GitHub App token step + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ secrets.CODEGEN_APP_CLIENT_ID }} + private-key: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }} + + - name: Setup .NET SDK step + uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 + with: + dotnet-version: 10.x + + - name: Checkout code step + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ matrix.target.ref }} + token: ${{ steps.app-token.outputs.token }} + + - name: Run codegen step + run: | + set -euo pipefail + dotnet run --project ./CreateMatrix/CreateMatrix.csproj -- \ + matrix --versionpath=./Make/Version.json --matrixpath=./Make/Matrix.json --updateversion + + - name: Format code step + run: | + set -euo pipefail + dotnet tool restore + dotnet husky install + dotnet csharpier format --log-level=debug . + git status + + - name: Create pull request step + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + id: cpr + with: + # App token: triggers pull_request workflow events directly, creates verified commits as the app + token: ${{ steps.app-token.outputs.token }} + base: ${{ matrix.target.ref }} + branch: ${{ matrix.target.branch }} + title: 'Update codegen files' + body: 'This PR updates the codegen files.' + commit-message: 'Update codegen files' + delete-branch: true + sign-commits: true diff --git a/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml index 8bbf5e0..05f4fa2 100644 --- a/.github/workflows/test-pull-request.yml +++ b/.github/workflows/test-pull-request.yml @@ -2,7 +2,7 @@ name: Test pull request action on: pull_request: - branches: [ main, develop, codegen ] + branches: [ main, develop ] workflow_dispatch: concurrency: