From 3a25affcdda93b7d27f2494e56753d93306bf678 Mon Sep 17 00:00:00 2001 From: "ptr727-codegen[bot]" <275599072+ptr727-codegen[bot]@users.noreply.github.com> Date: Sat, 4 Jul 2026 06:05:04 +0000 Subject: [PATCH 1/3] Update upstream versions: device_builder to 1.0.28 (#115) Co-authored-by: ptr727-codegen[bot] <275599072+ptr727-codegen[bot]@users.noreply.github.com> --- upstream-version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/upstream-version.json b/upstream-version.json index b588350..43eac83 100644 --- a/upstream-version.json +++ b/upstream-version.json @@ -1,4 +1,4 @@ { - "device_builder": "1.0.27", + "device_builder": "1.0.28", "esphome": "2026.6.4" } From fe68ecea0280e94f4051ea4fdb28629b7231e7a0 Mon Sep 17 00:00:00 2001 From: "ptr727-codegen[bot]" <275599072+ptr727-codegen[bot]@users.noreply.github.com> Date: Sun, 5 Jul 2026 06:19:33 +0000 Subject: [PATCH 2/3] Update upstream versions: device_builder to 1.0.29 (#117) Co-authored-by: ptr727-codegen[bot] <275599072+ptr727-codegen[bot]@users.noreply.github.com> --- upstream-version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/upstream-version.json b/upstream-version.json index 43eac83..b4a3042 100644 --- a/upstream-version.json +++ b/upstream-version.json @@ -1,4 +1,4 @@ { - "device_builder": "1.0.28", + "device_builder": "1.0.29", "esphome": "2026.6.4" } From 7314357bb0d63863180609ba3b0d143ca4ed9cf8 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sun, 5 Jul 2026 13:42:59 -0700 Subject: [PATCH 3/3] Add editorconfig CRLF default and renormalize workflow YAML (#119) * Add the global editorconfig CRLF default Set end_of_line = crlf on the [*] block so every file type has a defined ending, matching the fleet template. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Pieter Viljoen * Renormalize workflow YAML to CRLF per .editorconfig Five workflow YAML files were committed LF, violating the .editorconfig [*.{yml,yaml}] crlf rule (git .gitattributes * -text does not renormalize). Convert them to CRLF; EOL-only change, no content difference (verified with git diff --ignore-cr-at-eol). Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Pieter Viljoen --------- Signed-off-by: Pieter Viljoen Co-authored-by: Claude Opus 4.8 (1M context) --- .editorconfig | 1 + .github/workflows/build-docker-task.yml | 242 ++++++++++---------- .github/workflows/build-release-task.yml | 270 +++++++++++------------ .github/workflows/publish-release.yml | 96 ++++---- .github/workflows/test-pull-request.yml | 126 +++++------ .github/workflows/validate-task.yml | 90 ++++---- 6 files changed, 413 insertions(+), 412 deletions(-) diff --git a/.editorconfig b/.editorconfig index 22e58dd..4c36804 100644 --- a/.editorconfig +++ b/.editorconfig @@ -16,6 +16,7 @@ root = true # Defaults [*] charset = utf-8 +end_of_line = crlf indent_size = 4 indent_style = space insert_final_newline = true diff --git a/.github/workflows/build-docker-task.yml b/.github/workflows/build-docker-task.yml index 91b665a..f5f6e01 100644 --- a/.github/workflows/build-docker-task.yml +++ b/.github/workflows/build-docker-task.yml @@ -1,121 +1,121 @@ -name: Build Docker image task - -# Builds the multi-arch image (linux/amd64,arm64) and, on a main publish, pushes it plus the Docker Hub overview. -# Branch drives tags: main => latest + the pinned esphome-version tag, else develop, plus the `:SemVer2` tag. Smoke -# builds amd64 only and never pushes; registry buildcache is branch-scoped. The orchestrator passes `branch` explicitly -# (the publisher builds one branch per run - the trigger ref - and smoke passes github.ref_name) and the threaded -# `semver2` from its single NBGV run, so this task never re-runs NBGV on the detached commit. -on: - workflow_call: - inputs: - # Push the built image and the Docker Hub overview. - push: - required: false - type: boolean - default: false - # Git ref to check out (empty = default checkout ref). - ref: - required: false - type: string - default: '' - # Logical branch: main => latest + pinned version tag, else develop. Required (see header). - branch: - required: true - type: string - # Smoke: build linux/amd64 only, never push, skip the shared cache-to. Fast PR feedback. - smoke: - required: false - type: boolean - default: false - # Version from the orchestrator's single NBGV run (threaded). This task does not - # re-run NBGV on the detached commit, which could classify the branch differently. - semver2: - required: true - type: string - -jobs: - - build-docker: - name: Build Docker image job - runs-on: ubuntu-latest - - steps: - - - name: Checkout step - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ inputs.ref }} - - # Pinned ESPHome + device-builder versions from the committed state file; the ESPHome version drives the - # image tag, both become build-args. - - name: Get pinned versions step - id: esphome - run: | - set -euo pipefail - version="$(jq -r .esphome upstream-version.json)" - if [[ -z "$version" || "$version" == "null" ]]; then - echo "::error::Could not read .esphome from upstream-version.json" - exit 1 - fi - device_builder="$(jq -r .device_builder upstream-version.json)" - if [[ -z "$device_builder" || "$device_builder" == "null" ]]; then - echo "::error::Could not read .device_builder from upstream-version.json" - exit 1 - fi - echo "version=$version" >> "$GITHUB_OUTPUT" - echo "device_builder=$device_builder" >> "$GITHUB_OUTPUT" - - # QEMU only emulates arm64; smoke is amd64-only, so skip it to save setup cost. - - name: Setup QEMU step - if: ${{ !inputs.smoke }} - uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 - with: - platforms: linux/amd64,linux/arm64 - - - name: Setup Buildx step - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - with: - platforms: ${{ inputs.smoke && 'linux/amd64' || 'linux/amd64,linux/arm64' }} - - # Always login (even on smoke) for higher pull/cache-read rate limits; the credentials are in both the - # Actions and Dependabot secret stores so a Dependabot push CI run can log in too. Forks cannot push here. - - name: Login to Docker Hub step - uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0 - with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - - - name: Docker build and push step - uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 - with: - # Context is Docker/ (the Dockerfile only copies entrypoint/). - context: ./Docker - push: ${{ inputs.push }} - file: ./Docker/Dockerfile - # main: latest + the pinned esphome-version tag; develop: develop only (the middle line is empty off - # main, which build-push-action ignores). Every image also carries the `:SemVer2` release tag. - tags: | - docker.io/ptr727/esphome-nonroot:${{ inputs.branch == 'main' && 'latest' || 'develop' }} - ${{ inputs.branch == 'main' && format('docker.io/ptr727/esphome-nonroot:{0}', steps.esphome.outputs.version) || '' }} - docker.io/ptr727/esphome-nonroot:${{ inputs.semver2 }} - platforms: ${{ inputs.smoke && 'linux/amd64' || 'linux/amd64,linux/arm64' }} - # Read both branches' caches (near-identical layers), write only this branch's tag and only when pushing. - # mode=max caches the builder publish; ignore-error keeps a cache hiccup from failing a publish. - cache-from: | - type=registry,ref=docker.io/ptr727/esphome-nonroot:buildcache-main - type=registry,ref=docker.io/ptr727/esphome-nonroot:buildcache-develop - cache-to: ${{ inputs.push && format('type=registry,ref=docker.io/ptr727/esphome-nonroot:buildcache-{0},mode=max,ignore-error=true', inputs.branch) || '' }} - build-args: | - LABEL_VERSION=${{ inputs.semver2 }} - ESPHOME_VERSION=${{ steps.esphome.outputs.version }} - DEVICE_BUILDER_VERSION=${{ steps.esphome.outputs.device_builder }} - - # Push the trimmed Docker Hub overview from Docker/README.md, main only (one overview, no per-branch context). - - name: Update Docker Hub description step - if: ${{ inputs.push && inputs.branch == 'main' }} - uses: peter-evans/dockerhub-description@1b9a80c056b620d92cedb9d9b5a223409c68ddfa # v5.0.0 - with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - repository: ptr727/esphome-nonroot - readme-filepath: ./Docker/README.md +name: Build Docker image task + +# Builds the multi-arch image (linux/amd64,arm64) and, on a main publish, pushes it plus the Docker Hub overview. +# Branch drives tags: main => latest + the pinned esphome-version tag, else develop, plus the `:SemVer2` tag. Smoke +# builds amd64 only and never pushes; registry buildcache is branch-scoped. The orchestrator passes `branch` explicitly +# (the publisher builds one branch per run - the trigger ref - and smoke passes github.ref_name) and the threaded +# `semver2` from its single NBGV run, so this task never re-runs NBGV on the detached commit. +on: + workflow_call: + inputs: + # Push the built image and the Docker Hub overview. + push: + required: false + type: boolean + default: false + # Git ref to check out (empty = default checkout ref). + ref: + required: false + type: string + default: '' + # Logical branch: main => latest + pinned version tag, else develop. Required (see header). + branch: + required: true + type: string + # Smoke: build linux/amd64 only, never push, skip the shared cache-to. Fast PR feedback. + smoke: + required: false + type: boolean + default: false + # Version from the orchestrator's single NBGV run (threaded). This task does not + # re-run NBGV on the detached commit, which could classify the branch differently. + semver2: + required: true + type: string + +jobs: + + build-docker: + name: Build Docker image job + runs-on: ubuntu-latest + + steps: + + - name: Checkout step + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ inputs.ref }} + + # Pinned ESPHome + device-builder versions from the committed state file; the ESPHome version drives the + # image tag, both become build-args. + - name: Get pinned versions step + id: esphome + run: | + set -euo pipefail + version="$(jq -r .esphome upstream-version.json)" + if [[ -z "$version" || "$version" == "null" ]]; then + echo "::error::Could not read .esphome from upstream-version.json" + exit 1 + fi + device_builder="$(jq -r .device_builder upstream-version.json)" + if [[ -z "$device_builder" || "$device_builder" == "null" ]]; then + echo "::error::Could not read .device_builder from upstream-version.json" + exit 1 + fi + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "device_builder=$device_builder" >> "$GITHUB_OUTPUT" + + # QEMU only emulates arm64; smoke is amd64-only, so skip it to save setup cost. + - name: Setup QEMU step + if: ${{ !inputs.smoke }} + uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 + with: + platforms: linux/amd64,linux/arm64 + + - name: Setup Buildx step + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 + with: + platforms: ${{ inputs.smoke && 'linux/amd64' || 'linux/amd64,linux/arm64' }} + + # Always login (even on smoke) for higher pull/cache-read rate limits; the credentials are in both the + # Actions and Dependabot secret stores so a Dependabot push CI run can log in too. Forks cannot push here. + - name: Login to Docker Hub step + uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + + - name: Docker build and push step + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 + with: + # Context is Docker/ (the Dockerfile only copies entrypoint/). + context: ./Docker + push: ${{ inputs.push }} + file: ./Docker/Dockerfile + # main: latest + the pinned esphome-version tag; develop: develop only (the middle line is empty off + # main, which build-push-action ignores). Every image also carries the `:SemVer2` release tag. + tags: | + docker.io/ptr727/esphome-nonroot:${{ inputs.branch == 'main' && 'latest' || 'develop' }} + ${{ inputs.branch == 'main' && format('docker.io/ptr727/esphome-nonroot:{0}', steps.esphome.outputs.version) || '' }} + docker.io/ptr727/esphome-nonroot:${{ inputs.semver2 }} + platforms: ${{ inputs.smoke && 'linux/amd64' || 'linux/amd64,linux/arm64' }} + # Read both branches' caches (near-identical layers), write only this branch's tag and only when pushing. + # mode=max caches the builder publish; ignore-error keeps a cache hiccup from failing a publish. + cache-from: | + type=registry,ref=docker.io/ptr727/esphome-nonroot:buildcache-main + type=registry,ref=docker.io/ptr727/esphome-nonroot:buildcache-develop + cache-to: ${{ inputs.push && format('type=registry,ref=docker.io/ptr727/esphome-nonroot:buildcache-{0},mode=max,ignore-error=true', inputs.branch) || '' }} + build-args: | + LABEL_VERSION=${{ inputs.semver2 }} + ESPHOME_VERSION=${{ steps.esphome.outputs.version }} + DEVICE_BUILDER_VERSION=${{ steps.esphome.outputs.device_builder }} + + # Push the trimmed Docker Hub overview from Docker/README.md, main only (one overview, no per-branch context). + - name: Update Docker Hub description step + if: ${{ inputs.push && inputs.branch == 'main' }} + uses: peter-evans/dockerhub-description@1b9a80c056b620d92cedb9d9b5a223409c68ddfa # v5.0.0 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + repository: ptr727/esphome-nonroot + readme-filepath: ./Docker/README.md diff --git a/.github/workflows/build-release-task.yml b/.github/workflows/build-release-task.yml index c5be481..3568ae8 100644 --- a/.github/workflows/build-release-task.yml +++ b/.github/workflows/build-release-task.yml @@ -1,135 +1,135 @@ -name: Build project release task - -# Orchestrate one branch's release: validate, version once (get-version), build the Docker image, then create the -# GitHub release. github/dockerhub gate the two publish targets; smoke builds the image, publishes nothing. -on: - workflow_call: - inputs: - github: - required: false - type: boolean - default: false - dockerhub: - required: false - type: boolean - default: false - # Git ref to check out / version (empty = default checkout ref). - ref: - required: false - type: string - default: '' - # Drives tags/prerelease: main => latest/non-prerelease, else develop. Required (see header). - branch: - required: true - type: string - # Smoke: reduced, never-published build for PR feedback; hard-disables every push below. - smoke: - required: false - type: boolean - default: false - -jobs: - - # Validate the branch being published (lint), gating the build/publish below. Skipped on smoke: the PR runs its - # own validate job, so a smoke build does not double-validate. - validate: - name: Validate job - if: ${{ !inputs.smoke }} - uses: ./.github/workflows/validate-task.yml - with: - ref: ${{ inputs.ref }} - - get-version: - name: Get version information job - uses: ./.github/workflows/get-version-task.yml - secrets: inherit - with: - ref: ${{ inputs.ref }} - - # Build only when validation passed (success) or was skipped (smoke); never when it failed. - build-docker: - name: Build Docker job - needs: [get-version, validate] - if: ${{ !cancelled() && needs.get-version.result == 'success' && (needs.validate.result == 'success' || needs.validate.result == 'skipped') }} - uses: ./.github/workflows/build-docker-task.yml - secrets: inherit - with: - # Pin to the resolved commit so the image matches the release tag, and thread the single NBGV version. - ref: ${{ needs.get-version.outputs.GitCommitId }} - branch: ${{ inputs.branch }} - smoke: ${{ inputs.smoke }} - semver2: ${{ needs.get-version.outputs.SemVer2 }} - # Push to Docker Hub, never on a smoke build. - push: ${{ inputs.dockerhub && !inputs.smoke }} - - github-release: - name: Publish GitHub release job - # !smoke enforces "smoke never publishes" even if a smoke caller set github: true. - if: ${{ inputs.github && !inputs.smoke }} - runs-on: ubuntu-latest - needs: [get-version, build-docker] - - steps: - - # Check out the exact built commit so the uploaded release files match the tag even if the branch advances mid-run. - - name: Checkout code step - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ needs.get-version.outputs.GitCommitId }} - - # Backstop (main only): refuse to publish if the public version carries a prerelease '-' (NBGV mis-versioning). - # Strip '+buildmetadata' first; only a '-' in the core segment marks a prerelease. - - name: Verify public release version step - if: ${{ inputs.branch == 'main' }} - env: - SEMVER2: ${{ needs.get-version.outputs.SemVer2 }} - run: | - set -euo pipefail - CORE_AND_PRE="${SEMVER2%%+*}" # drop +buildmetadata; a '-' here is the genuine prerelease separator - if [[ "$CORE_AND_PRE" == *-* ]]; then - echo "::error::Public (main) release version '$SEMVER2' carries a prerelease suffix; refusing to publish." - exit 1 - fi - - # Collect any release-asset--* artifacts. The Docker target pushes directly and uploads none, so this - # matches nothing (which succeeds) and the release carries just the tag + LICENSE/README. - - name: Download release asset artifacts step - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: release-asset-${{ inputs.branch }}-* - merge-multiple: true - path: ./Publish - - # Weekly re-runs may hit an already-released version; skip release-create when the tag exists (no-op republish). - - name: Check for existing release step - id: release-exists - env: - GH_TOKEN: ${{ github.token }} - TAG: ${{ needs.get-version.outputs.SemVer2 }} - run: | - set -euo pipefail - if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then - echo "exists=true" >> "$GITHUB_OUTPUT" - if [[ "${{ github.event_name }}" == "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 - - # target_commitish pins the tag to the exact built commit (GitCommitId), not the moving branch ref. - # The Docker-only target ships no asset, so fail_on_unmatched_files is omitted (the glob matches zero files). - - name: Create GitHub release step - if: ${{ steps.release-exists.outputs.exists == 'false' || github.event_name == 'workflow_dispatch' }} - uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1 - with: - generate_release_notes: true - tag_name: ${{ needs.get-version.outputs.SemVer2 }} - target_commitish: ${{ needs.get-version.outputs.GitCommitId }} - prerelease: ${{ inputs.branch != 'main' }} - files: | - LICENSE - README.md - ./Publish/* +name: Build project release task + +# Orchestrate one branch's release: validate, version once (get-version), build the Docker image, then create the +# GitHub release. github/dockerhub gate the two publish targets; smoke builds the image, publishes nothing. +on: + workflow_call: + inputs: + github: + required: false + type: boolean + default: false + dockerhub: + required: false + type: boolean + default: false + # Git ref to check out / version (empty = default checkout ref). + ref: + required: false + type: string + default: '' + # Drives tags/prerelease: main => latest/non-prerelease, else develop. Required (see header). + branch: + required: true + type: string + # Smoke: reduced, never-published build for PR feedback; hard-disables every push below. + smoke: + required: false + type: boolean + default: false + +jobs: + + # Validate the branch being published (lint), gating the build/publish below. Skipped on smoke: the PR runs its + # own validate job, so a smoke build does not double-validate. + validate: + name: Validate job + if: ${{ !inputs.smoke }} + uses: ./.github/workflows/validate-task.yml + with: + ref: ${{ inputs.ref }} + + get-version: + name: Get version information job + uses: ./.github/workflows/get-version-task.yml + secrets: inherit + with: + ref: ${{ inputs.ref }} + + # Build only when validation passed (success) or was skipped (smoke); never when it failed. + build-docker: + name: Build Docker job + needs: [get-version, validate] + if: ${{ !cancelled() && needs.get-version.result == 'success' && (needs.validate.result == 'success' || needs.validate.result == 'skipped') }} + uses: ./.github/workflows/build-docker-task.yml + secrets: inherit + with: + # Pin to the resolved commit so the image matches the release tag, and thread the single NBGV version. + ref: ${{ needs.get-version.outputs.GitCommitId }} + branch: ${{ inputs.branch }} + smoke: ${{ inputs.smoke }} + semver2: ${{ needs.get-version.outputs.SemVer2 }} + # Push to Docker Hub, never on a smoke build. + push: ${{ inputs.dockerhub && !inputs.smoke }} + + github-release: + name: Publish GitHub release job + # !smoke enforces "smoke never publishes" even if a smoke caller set github: true. + if: ${{ inputs.github && !inputs.smoke }} + runs-on: ubuntu-latest + needs: [get-version, build-docker] + + steps: + + # Check out the exact built commit so the uploaded release files match the tag even if the branch advances mid-run. + - name: Checkout code step + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ needs.get-version.outputs.GitCommitId }} + + # Backstop (main only): refuse to publish if the public version carries a prerelease '-' (NBGV mis-versioning). + # Strip '+buildmetadata' first; only a '-' in the core segment marks a prerelease. + - name: Verify public release version step + if: ${{ inputs.branch == 'main' }} + env: + SEMVER2: ${{ needs.get-version.outputs.SemVer2 }} + run: | + set -euo pipefail + CORE_AND_PRE="${SEMVER2%%+*}" # drop +buildmetadata; a '-' here is the genuine prerelease separator + if [[ "$CORE_AND_PRE" == *-* ]]; then + echo "::error::Public (main) release version '$SEMVER2' carries a prerelease suffix; refusing to publish." + exit 1 + fi + + # Collect any release-asset--* artifacts. The Docker target pushes directly and uploads none, so this + # matches nothing (which succeeds) and the release carries just the tag + LICENSE/README. + - name: Download release asset artifacts step + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: release-asset-${{ inputs.branch }}-* + merge-multiple: true + path: ./Publish + + # Weekly re-runs may hit an already-released version; skip release-create when the tag exists (no-op republish). + - name: Check for existing release step + id: release-exists + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ needs.get-version.outputs.SemVer2 }} + run: | + set -euo pipefail + if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then + echo "exists=true" >> "$GITHUB_OUTPUT" + if [[ "${{ github.event_name }}" == "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 + + # target_commitish pins the tag to the exact built commit (GitCommitId), not the moving branch ref. + # The Docker-only target ships no asset, so fail_on_unmatched_files is omitted (the glob matches zero files). + - name: Create GitHub release step + if: ${{ steps.release-exists.outputs.exists == 'false' || github.event_name == 'workflow_dispatch' }} + uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1 + with: + generate_release_notes: true + tag_name: ${{ needs.get-version.outputs.SemVer2 }} + target_commitish: ${{ needs.get-version.outputs.GitCommitId }} + prerelease: ${{ inputs.branch != 'main' }} + files: | + LICENSE + README.md + ./Publish/* diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index c1742e7..1a2f7a3 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -1,48 +1,48 @@ -name: Publish project release action - -# Triggered-Docker publisher, one run = one branch (the trigger ref). The weekly schedule rebuilds `main` only -# (multi-arch image + GitHub release; refreshes the `latest`/version tags and the python:3.14-slim base for CVEs). -# A path-scoped push publishes `main` immediately when the daily upstream-check commits a new `upstream-version.json` -# pin. A manual dispatch publishes the branch it is started from (main => latest, develop => prerelease/`develop`). -# Building only the trigger branch keeps github.ref aligned with the branch being versioned, so NBGV classifies it -# with no matrix and no cross-branch ref leak. -# -# Ordinary code merges do NOT publish: only a pin change (push, branch-filtered to main) or a schedule/dispatch -# does. develop's daily pin update is sync-only (the push trigger is main-only). CI/validation runs separately on -# push (test-pull-request). -on: - workflow_dispatch: - schedule: - # Weekly rebuild/publish of main (Mon 02:00 UTC) to pick up base-image updates. - - cron: '0 2 * * MON' - # Publish main immediately when the upstream pin changes; the daily upstream-check commits the new version here. - push: - branches: [main] - paths: [upstream-version.json] - -# Global, ref-independent group so two publishes never overlap (a schedule + a dispatch + a pin push, or back-to-back -# dispatches). cancel-in-progress: false so a publish is never cancelled mid-release. -concurrency: - group: ${{ github.workflow }} - cancel-in-progress: false - -jobs: - - # Publish the trigger branch in full (Docker image + GitHub release). The schedule and the pin push both run on - # main; a dispatch runs on whatever branch it is started from. build-release-task validates, versions, and tags - # that one branch; the no-op guard skips release creation when the version is unchanged while the Docker push - # still refreshes the base image. - publish: - name: Publish project release job - # Only the long-lived branches publish; a stray dispatch from a feature branch is a no-op. - if: ${{ github.ref_name == 'main' || github.ref_name == 'develop' }} - uses: ./.github/workflows/build-release-task.yml - secrets: inherit - permissions: - contents: write - with: - ref: ${{ github.ref_name }} - branch: ${{ github.ref_name }} - smoke: false - github: true - dockerhub: true +name: Publish project release action + +# Triggered-Docker publisher, one run = one branch (the trigger ref). The weekly schedule rebuilds `main` only +# (multi-arch image + GitHub release; refreshes the `latest`/version tags and the python:3.14-slim base for CVEs). +# A path-scoped push publishes `main` immediately when the daily upstream-check commits a new `upstream-version.json` +# pin. A manual dispatch publishes the branch it is started from (main => latest, develop => prerelease/`develop`). +# Building only the trigger branch keeps github.ref aligned with the branch being versioned, so NBGV classifies it +# with no matrix and no cross-branch ref leak. +# +# Ordinary code merges do NOT publish: only a pin change (push, branch-filtered to main) or a schedule/dispatch +# does. develop's daily pin update is sync-only (the push trigger is main-only). CI/validation runs separately on +# push (test-pull-request). +on: + workflow_dispatch: + schedule: + # Weekly rebuild/publish of main (Mon 02:00 UTC) to pick up base-image updates. + - cron: '0 2 * * MON' + # Publish main immediately when the upstream pin changes; the daily upstream-check commits the new version here. + push: + branches: [main] + paths: [upstream-version.json] + +# Global, ref-independent group so two publishes never overlap (a schedule + a dispatch + a pin push, or back-to-back +# dispatches). cancel-in-progress: false so a publish is never cancelled mid-release. +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +jobs: + + # Publish the trigger branch in full (Docker image + GitHub release). The schedule and the pin push both run on + # main; a dispatch runs on whatever branch it is started from. build-release-task validates, versions, and tags + # that one branch; the no-op guard skips release creation when the version is unchanged while the Docker push + # still refreshes the base image. + publish: + name: Publish project release job + # Only the long-lived branches publish; a stray dispatch from a feature branch is a no-op. + if: ${{ github.ref_name == 'main' || github.ref_name == 'develop' }} + uses: ./.github/workflows/build-release-task.yml + secrets: inherit + permissions: + contents: write + with: + ref: ${{ github.ref_name }} + branch: ${{ github.ref_name }} + smoke: false + github: true + dockerhub: true diff --git a/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml index c3eacfa..a911c4d 100644 --- a/.github/workflows/test-pull-request.yml +++ b/.github/workflows/test-pull-request.yml @@ -1,63 +1,63 @@ -name: Test pull request action - -# CI for every branch. Runs on push so the reusable tasks resolve from the pushed head: a PR that edits a workflow -# tests its own copy. validate (lint) and smoke-build run on every push with no paths filter, so a reusable-workflow -# change is always exercised. The aggregator below is the ruleset required status check, produced here on the head -# SHA. CI never publishes (the publisher runs on schedule/dispatch/pin-push). -# -# There is deliberately no pull_request trigger: a fork PR cannot push to this repo, so it produces no run and -# cannot satisfy the required check. A maintainer lands such a contribution on an in-repo branch (which pushes, and -# so validates) before merging. This is the documented exception (see WORKFLOW.md). -on: - # All branches, but not tags: release tags must not re-run CI. - push: - branches: ['**'] - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - - # `!github.event.deleted` skips a branch-deletion push (github.sha is all-zeros, so checkout/build fail). The same - # lint gate the publisher runs, so the PR gate and the publish gate are identical. - validate: - name: Validate job - if: ${{ !github.event.deleted }} - uses: ./.github/workflows/validate-task.yml - - # Build and pack the Docker target (amd64-only, no push) to prove it ships, publishing nothing. Smoke seeds from - # the branch-scoped cache, so it stays fast. - smoke-build: - name: Smoke build job - if: ${{ !github.event.deleted }} - uses: ./.github/workflows/build-release-task.yml - secrets: inherit - with: - smoke: true - # Never publish from CI. - github: false - dockerhub: false - branch: ${{ github.ref_name }} - - # Single required status check. Its name is the ruleset-bound context in repo-config/ruleset-*.json. Do not rename - # it without updating those in lockstep. Must succeed, not merely not-fail. Skips on a branch deletion (when - # validate/smoke-build also skip), so the deletion push does not fail this required check. - check-workflow-status: - name: Check pull request workflow status job - runs-on: ubuntu-latest - needs: [validate, smoke-build] - if: ${{ always() && !github.event.deleted }} - steps: - - name: Check workflow results step - run: | - set -euo pipefail - for result in "validate:${{ needs.validate.result }}" "smoke-build:${{ needs.smoke-build.result }}"; do - name="${result%%:*}" - value="${result#*:}" - if [[ "$value" != "success" ]]; then - echo "::error::Job '$name' did not succeed ($value)." - exit 1 - fi - done +name: Test pull request action + +# CI for every branch. Runs on push so the reusable tasks resolve from the pushed head: a PR that edits a workflow +# tests its own copy. validate (lint) and smoke-build run on every push with no paths filter, so a reusable-workflow +# change is always exercised. The aggregator below is the ruleset required status check, produced here on the head +# SHA. CI never publishes (the publisher runs on schedule/dispatch/pin-push). +# +# There is deliberately no pull_request trigger: a fork PR cannot push to this repo, so it produces no run and +# cannot satisfy the required check. A maintainer lands such a contribution on an in-repo branch (which pushes, and +# so validates) before merging. This is the documented exception (see WORKFLOW.md). +on: + # All branches, but not tags: release tags must not re-run CI. + push: + branches: ['**'] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + + # `!github.event.deleted` skips a branch-deletion push (github.sha is all-zeros, so checkout/build fail). The same + # lint gate the publisher runs, so the PR gate and the publish gate are identical. + validate: + name: Validate job + if: ${{ !github.event.deleted }} + uses: ./.github/workflows/validate-task.yml + + # Build and pack the Docker target (amd64-only, no push) to prove it ships, publishing nothing. Smoke seeds from + # the branch-scoped cache, so it stays fast. + smoke-build: + name: Smoke build job + if: ${{ !github.event.deleted }} + uses: ./.github/workflows/build-release-task.yml + secrets: inherit + with: + smoke: true + # Never publish from CI. + github: false + dockerhub: false + branch: ${{ github.ref_name }} + + # Single required status check. Its name is the ruleset-bound context in repo-config/ruleset-*.json. Do not rename + # it without updating those in lockstep. Must succeed, not merely not-fail. Skips on a branch deletion (when + # validate/smoke-build also skip), so the deletion push does not fail this required check. + check-workflow-status: + name: Check pull request workflow status job + runs-on: ubuntu-latest + needs: [validate, smoke-build] + if: ${{ always() && !github.event.deleted }} + steps: + - name: Check workflow results step + run: | + set -euo pipefail + for result in "validate:${{ needs.validate.result }}" "smoke-build:${{ needs.smoke-build.result }}"; do + name="${result%%:*}" + value="${result#*:}" + if [[ "$value" != "success" ]]; then + echo "::error::Job '$name' did not succeed ($value)." + exit 1 + fi + done diff --git a/.github/workflows/validate-task.yml b/.github/workflows/validate-task.yml index 954fd1c..be549a9 100644 --- a/.github/workflows/validate-task.yml +++ b/.github/workflows/validate-task.yml @@ -1,45 +1,45 @@ -name: Validate task - -# The single validation gate: the full lint set (the editor's checks, enforced in CI). This is a Docker-only repo -# with no language source tree, so there is no unit-test or compiler step - lint is the whole gate. Reused by -# test-pull-request (produces the required check) and the publisher, so the CI and publish gates are identical. -on: - workflow_call: - inputs: - # Git ref to validate (empty = default checkout ref). The publisher passes its trigger branch so the publish - # run validates the branch it publishes. - ref: - required: false - type: string - default: '' - -jobs: - - # The editor's checks, enforced in CI from the same config files. - lint: - name: Lint job - runs-on: ubuntu-latest - - steps: - - - name: Checkout code step - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ inputs.ref }} - - - name: Lint markdown step - uses: DavidAnson/markdownlint-cli2-action@ded1f9488f68a970bc66ea5619e13e9b52e601cd # v23.2.0 - with: - globs: '**/*.md' - - # Spell-check the user-facing docs; word list + exclusions live in cspell.json (shared with the editor). - - name: Spell check step - uses: streetsidesoftware/cspell-action@de2a73e963e7443969755b648a1008f77033c5b2 # v8.4.0 - with: - files: | - README.md - HISTORY.md - incremental_files_only: false - - - name: Lint workflows step - uses: raven-actions/actionlint@3d39aea434753780c3b3d4a1a31c854b4dbf49d7 # v2.2.0 +name: Validate task + +# The single validation gate: the full lint set (the editor's checks, enforced in CI). This is a Docker-only repo +# with no language source tree, so there is no unit-test or compiler step - lint is the whole gate. Reused by +# test-pull-request (produces the required check) and the publisher, so the CI and publish gates are identical. +on: + workflow_call: + inputs: + # Git ref to validate (empty = default checkout ref). The publisher passes its trigger branch so the publish + # run validates the branch it publishes. + ref: + required: false + type: string + default: '' + +jobs: + + # The editor's checks, enforced in CI from the same config files. + lint: + name: Lint job + runs-on: ubuntu-latest + + steps: + + - name: Checkout code step + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ inputs.ref }} + + - name: Lint markdown step + uses: DavidAnson/markdownlint-cli2-action@ded1f9488f68a970bc66ea5619e13e9b52e601cd # v23.2.0 + with: + globs: '**/*.md' + + # Spell-check the user-facing docs; word list + exclusions live in cspell.json (shared with the editor). + - name: Spell check step + uses: streetsidesoftware/cspell-action@de2a73e963e7443969755b648a1008f77033c5b2 # v8.4.0 + with: + files: | + README.md + HISTORY.md + incremental_files_only: false + + - name: Lint workflows step + uses: raven-actions/actionlint@3d39aea434753780c3b3d4a1a31c854b4dbf49d7 # v2.2.0