From e20779e07092db90bdfa5a437d72e2e937f1cb7d Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Tue, 23 Jun 2026 21:38:06 -0700 Subject: [PATCH] Normalize workflow YAML line endings to CRLF per .editorconfig (#446) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six workflow/config files had drifted to LF, but `.editorconfig` mandates CRLF for `*.{yml,yaml}`. This normalizes them to CRLF so the editor/lint config and the files agree. Files: - `.github/dependabot.yml` - `.github/workflows/build-docker-task.yml` - `.github/workflows/merge-bot-pull-request.yml` - `.github/workflows/publish-release.yml` - `.github/workflows/run-codegen-pull-request-task.yml` - `.github/workflows/test-pull-request.yml` The LF→CRLF flip rewrites every line, which is why this is isolated in its own change (per the template's line-ending governance). No content changes; YAML still parses. Other tracked files either already satisfy their `.editorconfig` EOL rule or have no rule (Dockerfiles and s6-overlay init scripts are intentionally left as-is — the Dockerfiles are generated, and the init scripts must stay LF to run in-container). --- .github/dependabot.yml | 194 ++++---- .github/workflows/build-docker-task.yml | 360 +++++++-------- .github/workflows/merge-bot-pull-request.yml | 414 +++++++++--------- .github/workflows/publish-release.yml | 378 ++++++++-------- .../run-codegen-pull-request-task.yml | 178 ++++---- .github/workflows/test-pull-request.yml | 266 +++++------ 6 files changed, 895 insertions(+), 895 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3eb8f80d..4b2306c7 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,97 +1,97 @@ -# 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: - - "*" +# 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/build-docker-task.yml b/.github/workflows/build-docker-task.yml index 478c1342..eadae615 100644 --- a/.github/workflows/build-docker-task.yml +++ b/.github/workflows/build-docker-task.yml @@ -1,180 +1,180 @@ -name: Build Docker image task - - -on: - workflow_call: - inputs: - push: - required: false - type: boolean - default: false - # Smoke mode: build a single Ubuntu + single LSIO variant on amd64 - # only, never push. Used for fast PR feedback in place of the full - # matrix. See test-pull-request.yml. - smoke: - required: false - type: boolean - default: false - # Logical branch whose Matrix.json rows to build/push and whose GHA - # cache scope to use. Decoupled from `ref` so `ref` can be pinned to an - # immutable commit (e.g. the publisher pins main to the versioned SHA) - # while this still selects the right branch's rows. Empty in smoke mode - # builds both branches' rows; the publisher passes main and develop so - # both branches' tags are produced from one scheduled run. Empty in a - # non-smoke build falls back to github.ref_name for the cache scope. - branch: - required: false - type: string - default: '' - # Immutable git ref to check out / version, decoupled from branch/tag - # selection (see `branch`). Empty uses the triggering ref (PR context). - # The publisher pins this to the exact versioned commit so the image's - # embedded version matches the GitHub release tag even if the branch - # advances mid-run. - ref: - required: false - type: string - default: '' - # When false the caller is expected to have built the base images - # already (the publisher builds them once, see publish-release.yml), - # so the internal base build is skipped. - build_base: - required: false - type: boolean - default: true - -jobs: - - get-matrix: - name: Get matrix job - runs-on: ubuntu-latest - outputs: - matrix: ${{ steps.getmatrix.outputs.matrix }} - - steps: - - - name: Checkout step - uses: actions/checkout@v7 - with: - ref: ${{ inputs.ref || github.ref }} - - - name: Load matrix.json step - id: getmatrix - env: - SMOKE: ${{ inputs.smoke }} - BRANCH: ${{ inputs.branch }} - run: | - # $b below is a jq variable (passed via --arg), not a shell - # variable, so it must stay single-quoted / unexpanded. - # shellcheck disable=SC2016 - if [[ "$SMOKE" == "true" ]]; then - # One Ubuntu + one LSIO variant exercises the shared Dockerfile - # build logic and the branch's build args; tags are irrelevant - # since smoke never pushes. Restrict to the PR base branch ($b) - # so a PR onto develop validates the develop rows and a PR onto - # main validates the main rows; empty $b builds both branches. - FILTER='.Images |= (map(select((.Name == "NxMeta" or .Name == "NxMeta-LSIO") and ($b == "" or .Branch == $b))) | unique_by([.Name, .Branch]))' - elif [[ -n "$BRANCH" ]]; then - # Publish: build only the rows targeting the branch being built - # (avoids building the other branch's rows just to discard them). - FILTER='.Images |= map(select(.Branch == $b))' - else - FILTER='.' - fi - echo "matrix=$(jq --arg b "$BRANCH" --compact-output "$FILTER" ./Make/Matrix.json)" >> "$GITHUB_OUTPUT" - - get-version: - name: Get version information job - uses: ./.github/workflows/get-version-task.yml - secrets: inherit - with: - ref: ${{ inputs.ref }} - - build-base: - name: Build base image job - if: ${{ inputs.build_base }} - uses: ./.github/workflows/build-base-images-task.yml - with: - push: ${{ inputs.push }} - platforms: ${{ inputs.smoke && 'linux/amd64' || 'linux/amd64,linux/arm64' }} - ref: ${{ inputs.ref }} - secrets: inherit - - build-docker: - name: Build Docker image job - runs-on: ubuntu-latest - needs: [get-version, get-matrix, build-base] - # always() so the job still runs when build-base is intentionally - # skipped (build_base == false); fail only if a prerequisite failed. - if: >- - ${{ always() - && needs.get-version.result == 'success' - && needs.get-matrix.result == 'success' - && (needs.build-base.result == 'success' || needs.build-base.result == 'skipped') }} - - strategy: - max-parallel: 4 - matrix: - images: ${{ fromJson(needs.get-matrix.outputs.matrix).Images }} - - steps: - - - name: Checkout step - uses: actions/checkout@v7 - with: - ref: ${{ inputs.ref || github.ref }} - - - name: Setup QEMU step - uses: docker/setup-qemu-action@v4 - with: - platforms: ${{ inputs.smoke && 'linux/amd64' || 'linux/amd64,linux/arm64' }} - - - name: Setup Buildx step - uses: docker/setup-buildx-action@v4 - with: - platforms: ${{ inputs.smoke && 'linux/amd64' || 'linux/amd64,linux/arm64' }} - - # Only needed when pushing; skipped for non-publishing builds (e.g. - # PR smoke) so they don't depend on Docker Hub secrets (unavailable - # on forked PRs). The public base image still pulls anonymously. - - name: Login to Docker Hub step - if: ${{ inputs.push }} - uses: docker/login-action@v4 - with: - registry: docker.io - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - - - name: Create tags and args step - id: tagsargs - run: | - TAGS=$(jq -r '.[]' <<< '${{ toJson(matrix.images.Tags) }}') - ARGS=$(jq -r '.[]' <<< '${{ toJson(matrix.images.Args) }}') - { - echo "tags<> "$GITHUB_OUTPUT" - - - name: Docker build and push step - uses: docker/build-push-action@v7 - with: - # Matrix is already filtered to the target branch, so push when asked. - # Smoke callers pass push:false. - push: ${{ inputs.push }} - context: Docker - file: Docker/${{ matrix.images.Name }}.Dockerfile - platforms: ${{ inputs.smoke && 'linux/amd64' || 'linux/amd64,linux/arm64' }} - tags: ${{ steps.tagsargs.outputs.tags }} - build-args: ${{ steps.tagsargs.outputs.args }} - cache-from: | - type=gha,scope=develop-${{ matrix.images.Name }} - type=gha,scope=main-${{ matrix.images.Name }} - ${{ github.event.pull_request && format('type=gha,scope=pr-{0}-{1}', github.event.pull_request.number, matrix.images.Name) || '' }} - cache-to: | - ${{ inputs.push && format('type=gha,mode=min,scope={0}-{1},ignore-error=true', inputs.branch != '' && inputs.branch || github.ref_name, matrix.images.Name) || '' }} - ${{ github.event.pull_request && !github.event.pull_request.head.repo.fork && format('type=gha,mode=min,scope=pr-{0}-{1},ignore-error=true', github.event.pull_request.number, matrix.images.Name) || '' }} +name: Build Docker image task + + +on: + workflow_call: + inputs: + push: + required: false + type: boolean + default: false + # Smoke mode: build a single Ubuntu + single LSIO variant on amd64 + # only, never push. Used for fast PR feedback in place of the full + # matrix. See test-pull-request.yml. + smoke: + required: false + type: boolean + default: false + # Logical branch whose Matrix.json rows to build/push and whose GHA + # cache scope to use. Decoupled from `ref` so `ref` can be pinned to an + # immutable commit (e.g. the publisher pins main to the versioned SHA) + # while this still selects the right branch's rows. Empty in smoke mode + # builds both branches' rows; the publisher passes main and develop so + # both branches' tags are produced from one scheduled run. Empty in a + # non-smoke build falls back to github.ref_name for the cache scope. + branch: + required: false + type: string + default: '' + # Immutable git ref to check out / version, decoupled from branch/tag + # selection (see `branch`). Empty uses the triggering ref (PR context). + # The publisher pins this to the exact versioned commit so the image's + # embedded version matches the GitHub release tag even if the branch + # advances mid-run. + ref: + required: false + type: string + default: '' + # When false the caller is expected to have built the base images + # already (the publisher builds them once, see publish-release.yml), + # so the internal base build is skipped. + build_base: + required: false + type: boolean + default: true + +jobs: + + get-matrix: + name: Get matrix job + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.getmatrix.outputs.matrix }} + + steps: + + - name: Checkout step + uses: actions/checkout@v7 + with: + ref: ${{ inputs.ref || github.ref }} + + - name: Load matrix.json step + id: getmatrix + env: + SMOKE: ${{ inputs.smoke }} + BRANCH: ${{ inputs.branch }} + run: | + # $b below is a jq variable (passed via --arg), not a shell + # variable, so it must stay single-quoted / unexpanded. + # shellcheck disable=SC2016 + if [[ "$SMOKE" == "true" ]]; then + # One Ubuntu + one LSIO variant exercises the shared Dockerfile + # build logic and the branch's build args; tags are irrelevant + # since smoke never pushes. Restrict to the PR base branch ($b) + # so a PR onto develop validates the develop rows and a PR onto + # main validates the main rows; empty $b builds both branches. + FILTER='.Images |= (map(select((.Name == "NxMeta" or .Name == "NxMeta-LSIO") and ($b == "" or .Branch == $b))) | unique_by([.Name, .Branch]))' + elif [[ -n "$BRANCH" ]]; then + # Publish: build only the rows targeting the branch being built + # (avoids building the other branch's rows just to discard them). + FILTER='.Images |= map(select(.Branch == $b))' + else + FILTER='.' + fi + echo "matrix=$(jq --arg b "$BRANCH" --compact-output "$FILTER" ./Make/Matrix.json)" >> "$GITHUB_OUTPUT" + + get-version: + name: Get version information job + uses: ./.github/workflows/get-version-task.yml + secrets: inherit + with: + ref: ${{ inputs.ref }} + + build-base: + name: Build base image job + if: ${{ inputs.build_base }} + uses: ./.github/workflows/build-base-images-task.yml + with: + push: ${{ inputs.push }} + platforms: ${{ inputs.smoke && 'linux/amd64' || 'linux/amd64,linux/arm64' }} + ref: ${{ inputs.ref }} + secrets: inherit + + build-docker: + name: Build Docker image job + runs-on: ubuntu-latest + needs: [get-version, get-matrix, build-base] + # always() so the job still runs when build-base is intentionally + # skipped (build_base == false); fail only if a prerequisite failed. + if: >- + ${{ always() + && needs.get-version.result == 'success' + && needs.get-matrix.result == 'success' + && (needs.build-base.result == 'success' || needs.build-base.result == 'skipped') }} + + strategy: + max-parallel: 4 + matrix: + images: ${{ fromJson(needs.get-matrix.outputs.matrix).Images }} + + steps: + + - name: Checkout step + uses: actions/checkout@v7 + with: + ref: ${{ inputs.ref || github.ref }} + + - name: Setup QEMU step + uses: docker/setup-qemu-action@v4 + with: + platforms: ${{ inputs.smoke && 'linux/amd64' || 'linux/amd64,linux/arm64' }} + + - name: Setup Buildx step + uses: docker/setup-buildx-action@v4 + with: + platforms: ${{ inputs.smoke && 'linux/amd64' || 'linux/amd64,linux/arm64' }} + + # Only needed when pushing; skipped for non-publishing builds (e.g. + # PR smoke) so they don't depend on Docker Hub secrets (unavailable + # on forked PRs). The public base image still pulls anonymously. + - name: Login to Docker Hub step + if: ${{ inputs.push }} + uses: docker/login-action@v4 + with: + registry: docker.io + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + + - name: Create tags and args step + id: tagsargs + run: | + TAGS=$(jq -r '.[]' <<< '${{ toJson(matrix.images.Tags) }}') + ARGS=$(jq -r '.[]' <<< '${{ toJson(matrix.images.Args) }}') + { + echo "tags<> "$GITHUB_OUTPUT" + + - name: Docker build and push step + uses: docker/build-push-action@v7 + with: + # Matrix is already filtered to the target branch, so push when asked. + # Smoke callers pass push:false. + push: ${{ inputs.push }} + context: Docker + file: Docker/${{ matrix.images.Name }}.Dockerfile + platforms: ${{ inputs.smoke && 'linux/amd64' || 'linux/amd64,linux/arm64' }} + tags: ${{ steps.tagsargs.outputs.tags }} + build-args: ${{ steps.tagsargs.outputs.args }} + cache-from: | + type=gha,scope=develop-${{ matrix.images.Name }} + type=gha,scope=main-${{ matrix.images.Name }} + ${{ github.event.pull_request && format('type=gha,scope=pr-{0}-{1}', github.event.pull_request.number, matrix.images.Name) || '' }} + cache-to: | + ${{ inputs.push && format('type=gha,mode=min,scope={0}-{1},ignore-error=true', inputs.branch != '' && inputs.branch || github.ref_name, matrix.images.Name) || '' }} + ${{ github.event.pull_request && !github.event.pull_request.head.repo.fork && format('type=gha,mode=min,scope=pr-{0}-{1},ignore-error=true', github.event.pull_request.number, matrix.images.Name) || '' }} diff --git a/.github/workflows/merge-bot-pull-request.yml b/.github/workflows/merge-bot-pull-request.yml index 3dc8b0c7..17b70f2b 100644 --- a/.github/workflows/merge-bot-pull-request.yml +++ b/.github/workflows/merge-bot-pull-request.yml @@ -1,207 +1,207 @@ -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`). -# Bot PRs are opened under the App token so the `pull_request` event -# fires the auto-merge and PR-test workflows directly; `GITHUB_TOKEN`- -# authored PR opens are blocked from triggering further workflow runs -# by GitHub's recursion guard. 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. (Merges no longer -# trigger publishing at all -- the scheduled `publish-release.yml` is -# the sole publisher -- so no merge-commit workflow needs to fire.) -# 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@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.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 }} +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`). +# Bot PRs are opened under the App token so the `pull_request` event +# fires the auto-merge and PR-test workflows directly; `GITHUB_TOKEN`- +# authored PR opens are blocked from triggering further workflow runs +# by GitHub's recursion guard. 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. (Merges no longer +# trigger publishing at all -- the scheduled `publish-release.yml` is +# the sole publisher -- so no merge-commit workflow needs to fire.) +# 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@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.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/publish-release.yml b/.github/workflows/publish-release.yml index fa6121a7..bdf3b3e6 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -1,189 +1,189 @@ -name: Publish project release action - -# Sole publisher. Images are built and pushed ONLY here: on the weekly -# schedule or via manual dispatch. Merges to main/develop no longer build -# or publish (PRs get a fast smoke build instead, see test-pull-request.yml). -# One run publishes both the main (:latest/:stable) and develop (:develop) -# tags, picking up everything merged since the last run (codegen version -# bumps, Dependabot base-image bumps, code changes). - -on: - workflow_dispatch: - schedule: - - cron: '0 2 * * MON' - -# Single global group (not ref-scoped): this workflow pushes shared tags -# (base images, both branches, readme, badge), so two dispatches on -# different refs must not run concurrently and race on those shared tags. -# cancel-in-progress: false queues the next run instead of cancelling an -# in-flight publish, which could otherwise leave a partially pushed tag set. -concurrency: - group: ${{ github.workflow }} - cancel-in-progress: false - -jobs: - - # Pin the release version to main: this run's GitHub release tracks the - # main publish, so compute it from main even when dispatched from another ref. - get-version: - name: Get version information job - uses: ./.github/workflows/get-version-task.yml - secrets: inherit - with: - ref: main - - # Base images share a single tag (nx-base:ubuntu-noble) across branches, - # so build them once (from the release branch) and let both branch legs - # reuse them via build_base: false. - build-base: - name: Build base image job - uses: ./.github/workflows/build-base-images-task.yml - secrets: inherit - with: - push: true - # Always build the shared nx-base tag from main, even when dispatched - # from another ref, so a develop dispatch can't overwrite main's base. - ref: main - - build-main: - name: Build main images job - needs: [get-version, build-base] - uses: ./.github/workflows/build-docker-task.yml - secrets: inherit - with: - push: true - branch: main - # Pin to the exact commit get-version computed the release version from, - # not the moving `main` ref: this closes the race where a commit landing - # on main mid-run could make the pushed image's embedded version come - # from a newer commit than the GitHub release of the same SemVer2 tag. - ref: ${{ needs.get-version.outputs.GitCommitId }} - build_base: false - - build-develop: - name: Build develop images job - needs: [build-base] - uses: ./.github/workflows/build-docker-task.yml - secrets: inherit - with: - push: true - # Develop only publishes the mutable :develop tag (no versioned GitHub - # release), so the moving ref can't cause a tag/release mismatch; no - # commit pin needed. - branch: develop - ref: develop - build_base: false - - # Gate on both build legs so a release is never published when develop - # is still building or failed. Release metadata/assets track main. - github-release: - name: Publish GitHub release job - needs: [get-version, build-main, build-develop] - runs-on: ubuntu-latest - permissions: - contents: write - - steps: - - # Check out the exact commit NBGV versioned (not the moving `main` ref), - # so the uploaded release files come from the same commit the tag points - # at even if `main` advances mid-run. - - name: Checkout code step - uses: actions/checkout@v7 - with: - ref: ${{ needs.get-version.outputs.GitCommitId }} - - # The weekly schedule re-runs even when main has no new commits, so NBGV - # can produce a SemVer2 that was already released. GitHub release creation - # has no built-in skip-duplicate, and re-publishing an unchanged version - # churns the release (and can fail re-uploading existing assets), so skip - # the release step when a release for this tag already exists — but only - # on the schedule. A `workflow_dispatch` is always allowed through so a - # maintainer can re-run to repair a partially-created release. - - name: Check for existing release step - id: release-exists - env: - GH_TOKEN: ${{ github.token }} - TAG: ${{ needs.get-version.outputs.SemVer2 }} - EVENT: ${{ github.event_name }} - run: | - set -euo pipefail - if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then - echo "exists=true" >> "$GITHUB_OUTPUT" - if [[ "$EVENT" == "workflow_dispatch" ]]; then - echo "Release $TAG already exists; workflow_dispatch will refresh it." - else - echo "Release $TAG already exists; skipping release creation (no-op republish)." - fi - else - echo "exists=false" >> "$GITHUB_OUTPUT" - fi - - - name: Create GitHub release step - # Skip only when the release already exists AND this is a scheduled - # run (the no-op weekly case). A manual `workflow_dispatch` always runs - # so it can repair/refresh an existing release for the same tag. - if: ${{ steps.release-exists.outputs.exists == 'false' || github.event_name == 'workflow_dispatch' }} - uses: softprops/action-gh-release@v3 - with: - generate_release_notes: true - tag_name: ${{ needs.get-version.outputs.SemVer2 }} - # Pin the tag to the exact commit NBGV versioned (main's HEAD at - # version-compute time, since get-version runs with ref: main) rather - # than the moving `main` ref — immutable, and consistent with the - # version/assets even if a commit lands on main mid-run. - target_commitish: ${{ needs.get-version.outputs.GitCommitId }} - # This run's release represents the main publish. - prerelease: false - files: | - LICENSE - README.md - ./Make/Version.json - ./Make/Matrix.json - - docker-readme: - name: Publish docker hub readme job - needs: [build-main, build-develop] - uses: ./.github/workflows/publish-docker-readme-task.yml - secrets: inherit - with: - # Readme content tracks main, consistent with the version/release. - ref: main - - date-badge: - name: Create BYOB date badge job - needs: [build-main, build-develop] - uses: ./.github/workflows/build-datebadge-task.yml - secrets: inherit - permissions: - contents: write - - # Workflow artifacts are an intra-run handoff (durable copies live on the - # GitHub release and Docker Hub), so leaving them accumulates against the small - # account-wide storage quota; delete them once every consumer has read them. - # This publisher always publishes when it runs (schedule/dispatch only), so no - # publish gate is needed beyond always(). - cleanup-artifacts: - name: Delete workflow artifacts job - needs: [build-base, build-main, build-develop, github-release, docker-readme, date-badge] - if: always() - runs-on: ubuntu-latest - permissions: - actions: write - steps: - - name: Delete workflow artifacts step - # continue-on-error: best-effort housekeeping must never red the run, even on an unexpected failure. - continue-on-error: true - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - if ! ids=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --paginate \ - --jq '.artifacts[].id'); then - echo "::warning::Could not list run artifacts; skipping cleanup (storage may not be freed)." - ids="" - fi - for artifact_id in $ids; do - gh api --method DELETE "repos/${{ github.repository }}/actions/artifacts/$artifact_id" \ - || echo "::warning::Failed to delete artifact $artifact_id; continuing." - done +name: Publish project release action + +# Sole publisher. Images are built and pushed ONLY here: on the weekly +# schedule or via manual dispatch. Merges to main/develop no longer build +# or publish (PRs get a fast smoke build instead, see test-pull-request.yml). +# One run publishes both the main (:latest/:stable) and develop (:develop) +# tags, picking up everything merged since the last run (codegen version +# bumps, Dependabot base-image bumps, code changes). + +on: + workflow_dispatch: + schedule: + - cron: '0 2 * * MON' + +# Single global group (not ref-scoped): this workflow pushes shared tags +# (base images, both branches, readme, badge), so two dispatches on +# different refs must not run concurrently and race on those shared tags. +# cancel-in-progress: false queues the next run instead of cancelling an +# in-flight publish, which could otherwise leave a partially pushed tag set. +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +jobs: + + # Pin the release version to main: this run's GitHub release tracks the + # main publish, so compute it from main even when dispatched from another ref. + get-version: + name: Get version information job + uses: ./.github/workflows/get-version-task.yml + secrets: inherit + with: + ref: main + + # Base images share a single tag (nx-base:ubuntu-noble) across branches, + # so build them once (from the release branch) and let both branch legs + # reuse them via build_base: false. + build-base: + name: Build base image job + uses: ./.github/workflows/build-base-images-task.yml + secrets: inherit + with: + push: true + # Always build the shared nx-base tag from main, even when dispatched + # from another ref, so a develop dispatch can't overwrite main's base. + ref: main + + build-main: + name: Build main images job + needs: [get-version, build-base] + uses: ./.github/workflows/build-docker-task.yml + secrets: inherit + with: + push: true + branch: main + # Pin to the exact commit get-version computed the release version from, + # not the moving `main` ref: this closes the race where a commit landing + # on main mid-run could make the pushed image's embedded version come + # from a newer commit than the GitHub release of the same SemVer2 tag. + ref: ${{ needs.get-version.outputs.GitCommitId }} + build_base: false + + build-develop: + name: Build develop images job + needs: [build-base] + uses: ./.github/workflows/build-docker-task.yml + secrets: inherit + with: + push: true + # Develop only publishes the mutable :develop tag (no versioned GitHub + # release), so the moving ref can't cause a tag/release mismatch; no + # commit pin needed. + branch: develop + ref: develop + build_base: false + + # Gate on both build legs so a release is never published when develop + # is still building or failed. Release metadata/assets track main. + github-release: + name: Publish GitHub release job + needs: [get-version, build-main, build-develop] + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + + # Check out the exact commit NBGV versioned (not the moving `main` ref), + # so the uploaded release files come from the same commit the tag points + # at even if `main` advances mid-run. + - name: Checkout code step + uses: actions/checkout@v7 + with: + ref: ${{ needs.get-version.outputs.GitCommitId }} + + # The weekly schedule re-runs even when main has no new commits, so NBGV + # can produce a SemVer2 that was already released. GitHub release creation + # has no built-in skip-duplicate, and re-publishing an unchanged version + # churns the release (and can fail re-uploading existing assets), so skip + # the release step when a release for this tag already exists — but only + # on the schedule. A `workflow_dispatch` is always allowed through so a + # maintainer can re-run to repair a partially-created release. + - name: Check for existing release step + id: release-exists + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ needs.get-version.outputs.SemVer2 }} + EVENT: ${{ github.event_name }} + run: | + set -euo pipefail + if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then + echo "exists=true" >> "$GITHUB_OUTPUT" + if [[ "$EVENT" == "workflow_dispatch" ]]; then + echo "Release $TAG already exists; workflow_dispatch will refresh it." + else + echo "Release $TAG already exists; skipping release creation (no-op republish)." + fi + else + echo "exists=false" >> "$GITHUB_OUTPUT" + fi + + - name: Create GitHub release step + # Skip only when the release already exists AND this is a scheduled + # run (the no-op weekly case). A manual `workflow_dispatch` always runs + # so it can repair/refresh an existing release for the same tag. + if: ${{ steps.release-exists.outputs.exists == 'false' || github.event_name == 'workflow_dispatch' }} + uses: softprops/action-gh-release@v3 + with: + generate_release_notes: true + tag_name: ${{ needs.get-version.outputs.SemVer2 }} + # Pin the tag to the exact commit NBGV versioned (main's HEAD at + # version-compute time, since get-version runs with ref: main) rather + # than the moving `main` ref — immutable, and consistent with the + # version/assets even if a commit lands on main mid-run. + target_commitish: ${{ needs.get-version.outputs.GitCommitId }} + # This run's release represents the main publish. + prerelease: false + files: | + LICENSE + README.md + ./Make/Version.json + ./Make/Matrix.json + + docker-readme: + name: Publish docker hub readme job + needs: [build-main, build-develop] + uses: ./.github/workflows/publish-docker-readme-task.yml + secrets: inherit + with: + # Readme content tracks main, consistent with the version/release. + ref: main + + date-badge: + name: Create BYOB date badge job + needs: [build-main, build-develop] + uses: ./.github/workflows/build-datebadge-task.yml + secrets: inherit + permissions: + contents: write + + # Workflow artifacts are an intra-run handoff (durable copies live on the + # GitHub release and Docker Hub), so leaving them accumulates against the small + # account-wide storage quota; delete them once every consumer has read them. + # This publisher always publishes when it runs (schedule/dispatch only), so no + # publish gate is needed beyond always(). + cleanup-artifacts: + name: Delete workflow artifacts job + needs: [build-base, build-main, build-develop, github-release, docker-readme, date-badge] + if: always() + runs-on: ubuntu-latest + permissions: + actions: write + steps: + - name: Delete workflow artifacts step + # continue-on-error: best-effort housekeeping must never red the run, even on an unexpected failure. + continue-on-error: true + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + if ! ids=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --paginate \ + --jq '.artifacts[].id'); then + echo "::warning::Could not list run artifacts; skipping cleanup (storage may not be freed)." + ids="" + fi + for artifact_id in $ids; do + gh api --method DELETE "repos/${{ github.repository }}/actions/artifacts/$artifact_id" \ + || echo "::warning::Failed to delete artifact $artifact_id; continuing." + done diff --git a/.github/workflows/run-codegen-pull-request-task.yml b/.github/workflows/run-codegen-pull-request-task.yml index 364abee6..1ec666a9 100644 --- a/.github/workflows/run-codegen-pull-request-task.yml +++ b/.github/workflows/run-codegen-pull-request-task.yml @@ -1,89 +1,89 @@ -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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - 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 +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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + 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 359f03b2..a7515b10 100644 --- a/.github/workflows/test-pull-request.yml +++ b/.github/workflows/test-pull-request.yml @@ -1,133 +1,133 @@ -name: Test pull request action - -on: - pull_request: - branches: [ main, develop ] - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - - changes: - name: Detect image changes job - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - outputs: - image: ${{ steps.filter.outputs.image }} - base: ${{ steps.filter.outputs.base }} - steps: - # Checkout so paths-filter can diff via git for non-PR triggers - # (e.g. workflow_dispatch); on pull_request it uses the API. - # fetch-depth: 0 gives the full history those git diffs need. - - name: Checkout step - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Filter changed paths step - id: filter - uses: dorny/paths-filter@v4 - with: - filters: | - image: - - 'Docker/**' - - 'Make/Matrix.json' - - 'Make/Version.json' - base: - - 'Docker/NxBase.Dockerfile' - - 'Docker/NxBase-LSIO.Dockerfile' - - test-release: - name: Test release job - uses: ./.github/workflows/test-release-task.yml - secrets: inherit - - # Fast smoke build in place of the full matrix: only runs when image - # files changed, and builds just NxMeta + NxMeta-LSIO on amd64 (no push). - # See build-docker-task.yml `smoke`. Publishing happens only on the - # scheduled publish-release.yml, so a smoke pass is fast PR feedback, - # not a publish gate. - # - # branch targets the PR base branch so a PR onto develop validates the - # develop image rows and a PR onto main validates the main rows. ref is - # left unset so checkout uses the PR ref being tested. - # - # build_base is only enabled when a base Dockerfile changed: the product - # smoke build pulls the published base from Docker Hub, so building the - # base otherwise is wasted work. When a base Dockerfile does change, build - # it to validate it still compiles (e.g. a Dependabot ubuntu:noble bump). - smoke-build: - name: Smoke build job - # Also gate on test-release: the smoke build builds Docker images, so don't - # spend it when the test job is already failing. A failed test-release - # leaves this job skipped (needs unsatisfied) and the aggregator blocks on - # the test-release failure directly. - needs: [changes, test-release] - if: ${{ needs.changes.outputs.image == 'true' }} - uses: ./.github/workflows/build-docker-task.yml - secrets: inherit - with: - push: false - smoke: true - branch: ${{ github.base_ref }} - build_base: ${{ needs.changes.outputs.base == 'true' }} - - # TODO: Workaround for GitHub Actions not supporting status checks on conditional jobs - # https://github.com/orgs/community/discussions/12395#discussioncomment-12970019 - check-workflow-status: - name: Check pull request workflow status - runs-on: ubuntu-latest - needs: - [ changes, test-release, smoke-build ] - if: always() - steps: - - name: Check workflow results - run: | - exit_on_result() { - # Pass on success or skipped (smoke-build is skipped when no - # image files changed); fail only on failure/cancelled. - if [[ "$2" == "failure" || "$2" == "cancelled" ]]; then - echo "Job '$1' failed or was cancelled." - exit 1 - fi - } - # changes must succeed: if it fails, smoke-build is skipped (not - # run), and treating that skip as a pass would let an - # image-changing PR merge without the smoke build. - exit_on_result "changes" "${{ needs.changes.result }}" - exit_on_result "test-release" "${{ needs.test-release.result }}" - exit_on_result "smoke-build" "${{ needs.smoke-build.result }}" - - # The smoke build runs docker/build-push-action, which can emit a build-record - # artifact, so this terminal cleanup deletes the run's artifacts to keep them - # off the small account-wide storage quota. Independent of - # check-workflow-status so housekeeping never gates the required merge check. - cleanup-artifacts: - name: Delete workflow artifacts job - needs: [smoke-build] - if: always() - runs-on: ubuntu-latest - permissions: - actions: write - steps: - - name: Delete workflow artifacts step - # continue-on-error: best-effort housekeeping must never red the run, even on an unexpected failure. - continue-on-error: true - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - if ! ids=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --paginate \ - --jq '.artifacts[].id'); then - echo "::warning::Could not list run artifacts; skipping cleanup (storage may not be freed)." - ids="" - fi - for artifact_id in $ids; do - gh api --method DELETE "repos/${{ github.repository }}/actions/artifacts/$artifact_id" \ - || echo "::warning::Failed to delete artifact $artifact_id; continuing." - done +name: Test pull request action + +on: + pull_request: + branches: [ main, develop ] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + + changes: + name: Detect image changes job + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + outputs: + image: ${{ steps.filter.outputs.image }} + base: ${{ steps.filter.outputs.base }} + steps: + # Checkout so paths-filter can diff via git for non-PR triggers + # (e.g. workflow_dispatch); on pull_request it uses the API. + # fetch-depth: 0 gives the full history those git diffs need. + - name: Checkout step + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Filter changed paths step + id: filter + uses: dorny/paths-filter@v4 + with: + filters: | + image: + - 'Docker/**' + - 'Make/Matrix.json' + - 'Make/Version.json' + base: + - 'Docker/NxBase.Dockerfile' + - 'Docker/NxBase-LSIO.Dockerfile' + + test-release: + name: Test release job + uses: ./.github/workflows/test-release-task.yml + secrets: inherit + + # Fast smoke build in place of the full matrix: only runs when image + # files changed, and builds just NxMeta + NxMeta-LSIO on amd64 (no push). + # See build-docker-task.yml `smoke`. Publishing happens only on the + # scheduled publish-release.yml, so a smoke pass is fast PR feedback, + # not a publish gate. + # + # branch targets the PR base branch so a PR onto develop validates the + # develop image rows and a PR onto main validates the main rows. ref is + # left unset so checkout uses the PR ref being tested. + # + # build_base is only enabled when a base Dockerfile changed: the product + # smoke build pulls the published base from Docker Hub, so building the + # base otherwise is wasted work. When a base Dockerfile does change, build + # it to validate it still compiles (e.g. a Dependabot ubuntu:noble bump). + smoke-build: + name: Smoke build job + # Also gate on test-release: the smoke build builds Docker images, so don't + # spend it when the test job is already failing. A failed test-release + # leaves this job skipped (needs unsatisfied) and the aggregator blocks on + # the test-release failure directly. + needs: [changes, test-release] + if: ${{ needs.changes.outputs.image == 'true' }} + uses: ./.github/workflows/build-docker-task.yml + secrets: inherit + with: + push: false + smoke: true + branch: ${{ github.base_ref }} + build_base: ${{ needs.changes.outputs.base == 'true' }} + + # TODO: Workaround for GitHub Actions not supporting status checks on conditional jobs + # https://github.com/orgs/community/discussions/12395#discussioncomment-12970019 + check-workflow-status: + name: Check pull request workflow status + runs-on: ubuntu-latest + needs: + [ changes, test-release, smoke-build ] + if: always() + steps: + - name: Check workflow results + run: | + exit_on_result() { + # Pass on success or skipped (smoke-build is skipped when no + # image files changed); fail only on failure/cancelled. + if [[ "$2" == "failure" || "$2" == "cancelled" ]]; then + echo "Job '$1' failed or was cancelled." + exit 1 + fi + } + # changes must succeed: if it fails, smoke-build is skipped (not + # run), and treating that skip as a pass would let an + # image-changing PR merge without the smoke build. + exit_on_result "changes" "${{ needs.changes.result }}" + exit_on_result "test-release" "${{ needs.test-release.result }}" + exit_on_result "smoke-build" "${{ needs.smoke-build.result }}" + + # The smoke build runs docker/build-push-action, which can emit a build-record + # artifact, so this terminal cleanup deletes the run's artifacts to keep them + # off the small account-wide storage quota. Independent of + # check-workflow-status so housekeeping never gates the required merge check. + cleanup-artifacts: + name: Delete workflow artifacts job + needs: [smoke-build] + if: always() + runs-on: ubuntu-latest + permissions: + actions: write + steps: + - name: Delete workflow artifacts step + # continue-on-error: best-effort housekeeping must never red the run, even on an unexpected failure. + continue-on-error: true + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + if ! ids=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --paginate \ + --jq '.artifacts[].id'); then + echo "::warning::Could not list run artifacts; skipping cleanup (storage may not be freed)." + ids="" + fi + for artifact_id in $ids; do + gh api --method DELETE "repos/${{ github.repository }}/actions/artifacts/$artifact_id" \ + || echo "::warning::Failed to delete artifact $artifact_id; continuing." + done