From 6e87435b56eeab9dcfb992cef3a47237a2dee1f1 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Thu, 7 May 2026 12:51:39 -0700 Subject: [PATCH 1/5] Align dependabot, merge-bot, and release flow for both branches - dependabot.yml: also target develop so both auto-publishing branches stay on the same baseline. - merge-bot-pull-request.yml: branch-aware merge method (squash for develop, merge for main) and App-token auth so post-merge pushes trigger publish-release and publish-periodic-docker-release on develop instead of being skipped by GitHub's recursion guard. - build-release-task.yml: set target_commitish=github.sha so release tags land on the triggering commit, not the repo default branch; fail_on_unmatched_files; pin softprops/action-gh-release to v3.0.0. - AGENTS.md: document the feature->develop->main flow, develop->main promotion via merge commit, dual-publish release flow, dependabot rationale, and merge-bot design. - copilot-instructions.md: terse summary section pointing at AGENTS.md. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/copilot-instructions.md | 11 ++++ .github/dependabot.yml | 56 +++++++++++++------- .github/workflows/build-release-task.yml | 16 +++++- .github/workflows/merge-bot-pull-request.yml | 51 ++++++++++++++---- AGENTS.md | 44 +++++++++++++++ 5 files changed, 149 insertions(+), 29 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 30935153..2adc7193 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -13,6 +13,17 @@ PlexCleaner is a .NET 10.0 CLI utility that optimizes media files for Direct Pla The tool orchestrates external media processing tools (FFmpeg, HandBrake, MkvToolNix, MediaInfo, 7-Zip) via CLI wrappers. +## Branching, Releases, and Bot Behavior + +For full rationale see [`AGENTS.md`](../AGENTS.md). Quick rules: + +- `feature → develop → main`. PRs only. +- Develop accepts **squash merges only**; main accepts **merge commits only**. Don't suggest rebase-merge — it's disabled at the repo level. +- Both branches **auto-publish on push**: develop produces NBGV prereleases (`X.Y.Z-g{sha}`) tagged `develop` on Docker Hub; main produces stable releases (`X.Y.Z`) tagged `latest`. +- Dependabot targets **both** `main` and `develop` with the same ecosystems; major NuGet bumps gate on human review, everything else auto-merges via App-token-driven merge-bot. +- Don't recommend `git push --force` or `--force-with-lease`; both rulesets enforce `non_fast_forward`. +- `version.json`'s `publicReleaseRefSpec` is `^refs/heads/main$` — bumping the base `version` field is the only manual versioning action. + ## Documentation User-facing documentation is organized as follows: diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5d47fa7e..152efecb 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,21 +3,41 @@ version: 2 updates: # main -- package-ecosystem: "nuget" - target-branch: "main" - directory: "/" - schedule: - interval: "daily" - groups: - nuget-deps: - patterns: - - "*" -- package-ecosystem: "github-actions" - target-branch: "main" - directory: "/" - schedule: - interval: "daily" - groups: - actions-deps: - patterns: - - "*" + - package-ecosystem: "nuget" + target-branch: "main" + directory: "/" + schedule: + interval: "daily" + groups: + nuget-deps: + patterns: + - "*" + - package-ecosystem: "github-actions" + target-branch: "main" + directory: "/" + schedule: + interval: "daily" + groups: + actions-deps: + patterns: + - "*" + + # develop + - package-ecosystem: "nuget" + target-branch: "develop" + directory: "/" + schedule: + interval: "daily" + groups: + nuget-deps: + patterns: + - "*" + - package-ecosystem: "github-actions" + target-branch: "develop" + directory: "/" + schedule: + interval: "daily" + groups: + actions-deps: + patterns: + - "*" diff --git a/.github/workflows/build-release-task.yml b/.github/workflows/build-release-task.yml index 6ae18c07..94e4588c 100644 --- a/.github/workflows/build-release-task.yml +++ b/.github/workflows/build-release-task.yml @@ -52,11 +52,23 @@ jobs: path: ./Publish - name: Create GitHub release step - uses: softprops/action-gh-release@v3 + # softprops creates the underlying tag from `tag_name` if it does not + # already exist, so the workflow drives the entire release lifecycle: + # NBGV computes the version, this step creates the tag + release with + # auto-generated notes, and the artifacts are attached in one shot. + # `target_commitish` MUST be set explicitly: softprops doesn't pass a + # default through, and GitHub's REST API then defaults the new tag to + # the repository's default branch (main). On `push: develop` runs the + # tag would land on main's tip instead of the develop commit that + # built the artifact, leaving "Browse files" and `git checkout ` + # pointing at unrelated code. + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 with: - generate_release_notes: true tag_name: ${{ needs.get-version.outputs.SemVer2 }} + target_commitish: ${{ github.sha }} prerelease: ${{ github.ref_name != 'main' }} + generate_release_notes: true + fail_on_unmatched_files: true files: | LICENSE README.md diff --git a/.github/workflows/merge-bot-pull-request.yml b/.github/workflows/merge-bot-pull-request.yml index a5656d24..d2f3f73f 100644 --- a/.github/workflows/merge-bot-pull-request.yml +++ b/.github/workflows/merge-bot-pull-request.yml @@ -1,6 +1,6 @@ name: Merge bot pull request action -on: +"on": pull_request: types: [opened, reopened, synchronize] @@ -13,9 +13,12 @@ jobs: merge-dependabot: name: Merge dependabot pull request job runs-on: ubuntu-latest - # To prevent abuse, the PR must come from Dependabot and the PR must originate from this repository. + # Restrict to dependabot PRs that originate from this repository, not a + # fork. Check the PR author rather than the event actor so maintainer + # repair commits on Dependabot branches can still auto-merge after CI + # passes. if: >- - github.actor == 'dependabot[bot]' && + github.event.pull_request.user.login == 'dependabot[bot]' && github.event.pull_request.head.repo.full_name == github.repository permissions: contents: write @@ -23,19 +26,49 @@ jobs: steps: + - name: Generate GitHub App token step + # Use an App token (not GITHUB_TOKEN) so the resulting merge push is + # committed by the App and fires downstream workflows on develop/main. + # Pushes from GITHUB_TOKEN are blocked from triggering further workflow + # runs by GitHub's recursion guard, which would silently skip + # publish-release.yml and publish-periodic-docker-release.yml on the + # merge commit and prevent develop's auto-prerelease/Docker rebuild. + id: app-token + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 + 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@v3 + uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0 with: github-token: "${{ secrets.GITHUB_TOKEN }}" - # Merge any non-NuGet update, e.g. GitHub Actions often updates v1 to v2. - # Merge NuGet only for non-major updates, e.g. major updates may build but break functionality. + # Skip semver-major NuGet bumps: they often build cleanly but break + # runtime behavior, so they should land via human review. GitHub Actions + # majors auto-merge because the workflow execution itself validates them. + # + # Merge method must match the base branch's ruleset: + # develop -> squash only (linear history) + # main -> merge commits only (preserves develop ancestry) + # A mismatch fails enablePullRequestAutoMerge with + # "Merge method ... is not allowed on this repository". - name: Merge pull request step if: >- (steps.metadata.outputs.package-ecosystem != 'nuget') || (steps.metadata.outputs.update-type != 'version-update:semver-major') - run: gh pr merge --auto --squash "$PR_URL" + 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: ${{secrets.GITHUB_TOKEN}} + PR_URL: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} diff --git a/AGENTS.md b/AGENTS.md index 99126a5c..b0458480 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,6 +13,50 @@ For comprehensive coding standards and detailed conventions, refer to [`.github/ - **Never run destructive git commands** (`git reset --hard`, `git checkout .`, `git restore .`, `git clean -f`) without explicit developer instruction. - **Staging is the limit.** Prepare and stage file changes; the developer runs `git commit` in their own environment where signing keys are available. +## Branches and merging + +- Pipeline is `feature → develop → main`. Both branches are protected by branch rulesets; everything lands via PR. +- **Feature → develop PRs squash-merge** (single commit on develop, PR title becomes the commit message; never rebase-merge). +- **Develop → main PRs merge-commit** (one merge commit on main per release, develop's tip becomes a second parent and stays in main's ancestry — see [Develop → Main Promotion](#develop--main-promotion)). +- Open feature PRs against `develop`. `develop → main` is how stable releases are cut. + +Repo settings reflect this: `allow_merge_commit=true`, `allow_squash_merge=true`, `allow_rebase_merge=false`, `allow_auto_merge=true`. The `develop` ruleset enforces `allowed_merge_methods=["squash"]` and `required_linear_history`. The `main` ruleset enforces `allowed_merge_methods=["merge"]` and intentionally omits linear-history (the develop → main merge commit is non-linear by design). + +## Develop → Main Promotion + +Use the **"Create a merge commit"** option on develop → main PRs. Repo rulesets are split: PRs into `develop` are squash-only (linear history); PRs into `main` are merge-commit only. Clicking "Create a merge commit" on a develop → main PR produces a merge commit on main whose second parent is develop's tip — so develop becomes a real ancestor of main, and the *next* develop → main PR has a clean merge base (no recurring conflicts, no behind-base churn). + +Under any squash-only setup this would be a recurring pain point: each develop → main squash drops develop's ancestry and forces a per-cycle admin-bypass merge commit on develop to resync. With merge-commit on main, that resync is unnecessary — main's history shows one merge commit per release (a feature, not a defect: each promotion is visible as a single auditable node), and develop stays linear. + +## Release flow + +PlexCleaner is a "pull" project: consumers (`docker pull ptr727/plexcleaner:latest`, `docker pull ptr727/plexcleaner:develop`, GitHub Releases) track both branches. **Both `main` and `develop` auto-publish on every push** — there is no manual `workflow_dispatch` gate. + +[publish-release.yml](.github/workflows/publish-release.yml) drives both prereleases and stable releases off the same [build-release-task.yml](.github/workflows/build-release-task.yml). It triggers on `push: [main, develop]`: + +- **Push to `develop`** — automatic prerelease. Merging any PR into `develop` (feature, bug fix, dependabot) calls [get-version-task.yml](.github/workflows/get-version-task.yml) for an NBGV-computed version like `3.16.42-g1a2b3c4` (because develop does not match `publicReleaseRefSpec` in [version.json](version.json)) and creates a GitHub Release with `prerelease: true`. The Docker image is tagged `develop` by [publish-periodic-docker-release.yml](.github/workflows/publish-periodic-docker-release.yml). +- **Push to `main`** — automatic stable release. NBGV produces a clean version like `3.16.42` and creates a GitHub Release with `prerelease: false`. The Docker image is tagged `latest`. + +Branch-aware logic lives in three places: + +- [build-release-task.yml](.github/workflows/build-release-task.yml) — `prerelease: ${{ github.ref_name != 'main' }}` and `target_commitish: ${{ github.sha }}` (the latter is critical: without it, softprops creates the tag against the repo's default branch, mis-tagging develop builds onto main's tip). +- [publish-periodic-docker-release.yml](.github/workflows/publish-periodic-docker-release.yml) — `tag: ${{ github.ref_name == 'main' && 'latest' || 'develop' }}`. + +Bot-merged PRs (Dependabot) trigger the publish workflows automatically because the merge-bot uses an App token — see the merge-bot section below. + +## Dependabot + +[.github/dependabot.yml](.github/dependabot.yml) targets **both `main` and `develop`** with two ecosystems each (`nuget`, `github-actions`), grouped per ecosystem, daily. The duplication is intentional: because both branches auto-publish, develop must not drift from main's dependency baseline. A NuGet major bump landing on develop should land on main on the next promotion cycle, not weeks later. + +Major NuGet bumps are not auto-merged by [merge-bot-pull-request.yml](.github/workflows/merge-bot-pull-request.yml) — they require human review. Major GitHub Actions bumps are auto-merged because the workflow execution itself is the validation surface. + +## Merge bot + +[merge-bot-pull-request.yml](.github/workflows/merge-bot-pull-request.yml) auto-merges Dependabot PRs. Two key design choices: + +- **Branch-aware merge method**: the script picks `--squash` for PRs targeting develop and `--merge` for PRs targeting main, matching each ruleset's `allowed_merge_methods`. An unknown base branch is a hard error. +- **App token, not GITHUB_TOKEN**: the merge step uses a token minted by `actions/create-github-app-token` from `CODEGEN_APP_CLIENT_ID` / `CODEGEN_APP_PRIVATE_KEY` secrets. Pushes authored by `GITHUB_TOKEN` are blocked from triggering downstream workflows by GitHub's recursion guard; without the App token, a Dependabot merge to develop would silently skip `publish-release.yml` and `publish-periodic-docker-release.yml`, leaving the develop Docker tag and prerelease stale. + ## Key Requirements for All Projects Derived from This Template ### Build & Quality Standards From 11203fe94964b24683bdc84ca2735f717fc73bc8 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Thu, 7 May 2026 12:58:32 -0700 Subject: [PATCH 2/5] Pin GitHub Actions to commit SHAs across all workflows Replace floating major-version refs (@v5/@v6/@v7/@v8/@v3/@v4/@v1) and the @master ref on dotnet/nbgv with full commit SHAs plus version comments, matching the pattern already used for create-github-app-token, fetch-metadata, and softprops/action-gh-release. Pins make supply-chain attacks via tag mutation impossible and let dependabot bump them mechanically. Also drops the verbose target_commitish rationale comment from build-release-task.yml; the field is self-explanatory in this repo's context. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/build-datebadge-task.yml | 2 +- .github/workflows/build-docker-task.yml | 10 +++++----- .github/workflows/build-executable-task.yml | 10 +++++----- .github/workflows/build-release-task.yml | 14 ++------------ .github/workflows/get-version-task.yml | 6 +++--- .../workflows/publish-periodic-docker-release.yml | 10 +++++----- .github/workflows/test-release-task.yml | 4 ++-- 7 files changed, 23 insertions(+), 33 deletions(-) diff --git a/.github/workflows/build-datebadge-task.yml b/.github/workflows/build-datebadge-task.yml index fc3355a6..e6cb9dfc 100644 --- a/.github/workflows/build-datebadge-task.yml +++ b/.github/workflows/build-datebadge-task.yml @@ -17,7 +17,7 @@ jobs: - name: Build BYOB date badge step if: ${{ github.ref_name == 'main' }} - uses: RubbaBoy/BYOB@v1 + uses: RubbaBoy/BYOB@24f464284c1fd32028524b59607d417a2e36fee7 # v1.3.0 with: name: lastbuild label: "Last Build" diff --git a/.github/workflows/build-docker-task.yml b/.github/workflows/build-docker-task.yml index cc9e7b68..922af0fe 100644 --- a/.github/workflows/build-docker-task.yml +++ b/.github/workflows/build-docker-task.yml @@ -32,28 +32,28 @@ jobs: steps: - name: Checkout step - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup QEMU step - uses: docker/setup-qemu-action@v4 + uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 with: platforms: linux/amd64,linux/arm64 - name: Setup Buildx step - uses: docker/setup-buildx-action@v4 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 with: platforms: linux/amd64,linux/arm64 # Always login to Docker Hub, not just on push, to benefit from # higher rate limits with a Docker subscription for pulls and cache - name: Login to Docker Hub step - uses: docker/login-action@v4 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Docker build and push step - uses: docker/build-push-action@v7 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 with: context: . push: ${{ inputs.push }} diff --git a/.github/workflows/build-executable-task.yml b/.github/workflows/build-executable-task.yml index 06679136..d85fd93d 100644 --- a/.github/workflows/build-executable-task.yml +++ b/.github/workflows/build-executable-task.yml @@ -25,12 +25,12 @@ jobs: steps: - name: Setup .NET SDK step - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 with: dotnet-version: 10.x - name: Checkout code step - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Build executable project step run: | @@ -46,7 +46,7 @@ jobs: -property:PackageVersion=${{ needs.get-version.outputs.SemVer2 }} - name: Upload matrix build artifacts step - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: publish-${{ matrix.runtime }} path: ${{ runner.temp }}/publish @@ -61,7 +61,7 @@ jobs: steps: - name: Download matrix build artifacts step - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: publish-* merge-multiple: true @@ -72,7 +72,7 @@ jobs: - name: Upload build artifacts step id: artifact-upload-step - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: executable-build path: ${{ runner.temp }}/PlexCleaner.7z diff --git a/.github/workflows/build-release-task.yml b/.github/workflows/build-release-task.yml index 94e4588c..e49a381d 100644 --- a/.github/workflows/build-release-task.yml +++ b/.github/workflows/build-release-task.yml @@ -43,25 +43,15 @@ jobs: steps: - name: Checkout code step - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Download executable build artifacts step - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: artifact-ids: ${{ needs.build-executable.outputs.artifact-id }} path: ./Publish - name: Create GitHub release step - # softprops creates the underlying tag from `tag_name` if it does not - # already exist, so the workflow drives the entire release lifecycle: - # NBGV computes the version, this step creates the tag + release with - # auto-generated notes, and the artifacts are attached in one shot. - # `target_commitish` MUST be set explicitly: softprops doesn't pass a - # default through, and GitHub's REST API then defaults the new tag to - # the repository's default branch (main). On `push: develop` runs the - # tag would land on main's tip instead of the develop commit that - # built the artifact, leaving "Browse files" and `git checkout ` - # pointing at unrelated code. uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 with: tag_name: ${{ needs.get-version.outputs.SemVer2 }} diff --git a/.github/workflows/get-version-task.yml b/.github/workflows/get-version-task.yml index 2c52cb21..c8b522cb 100644 --- a/.github/workflows/get-version-task.yml +++ b/.github/workflows/get-version-task.yml @@ -27,15 +27,15 @@ jobs: steps: - name: Setup .NET SDK step - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 with: dotnet-version: 10.x - name: Checkout code step - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Run Nerdbank.GitVersioning tool step id: nbgv - uses: dotnet/nbgv@master + uses: dotnet/nbgv@3cf2d96c2aa00675081b59f401356ac1fb81092f # v0.5.1 diff --git a/.github/workflows/publish-periodic-docker-release.yml b/.github/workflows/publish-periodic-docker-release.yml index f09ca5f2..f9aad9da 100644 --- a/.github/workflows/publish-periodic-docker-release.yml +++ b/.github/workflows/publish-periodic-docker-release.yml @@ -42,7 +42,7 @@ jobs: echo Size: $(docker manifest inspect -v docker.io/ptr727/plexcleaner:${{ matrix.tag }} | jq '.[] | select(.Descriptor.platform.architecture=="amd64") | [.OCIManifest.layers[].size] | add' | numfmt --to=iec) >> ${{ runner.temp }}/versions/${{ matrix.file }} - name: Write tool versions to file step - uses: addnab/docker-run-action@v3 + uses: addnab/docker-run-action@4f65fabd2431ebc8d299f8e5a018d79a769ae185 # v3 with: image: docker.io/ptr727/plexcleaner:${{ matrix.tag }} options: --volume ${{ runner.temp }}/versions:/versions @@ -59,7 +59,7 @@ jobs: run: cat ${{ runner.temp }}/versions/${{ matrix.file }} - name: Upload version artifacts step - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: versions-${{ matrix.file }} path: ${{ runner.temp }}/versions/${{ matrix.file }} @@ -73,10 +73,10 @@ jobs: steps: - name: Checkout step - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Download version artifacts step - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: versions-* merge-multiple: true @@ -86,7 +86,7 @@ jobs: run: m4 --include=${{ runner.temp }}/versions ./Docker/README.m4 > ${{ runner.temp }}/README.md - name: Update Docker Hub README.md step - uses: peter-evans/dockerhub-description@v5 + uses: peter-evans/dockerhub-description@1b9a80c056b620d92cedb9d9b5a223409c68ddfa # v5.0.0 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_PASSWORD }} diff --git a/.github/workflows/test-release-task.yml b/.github/workflows/test-release-task.yml index 4340986f..6b5cfae3 100644 --- a/.github/workflows/test-release-task.yml +++ b/.github/workflows/test-release-task.yml @@ -13,12 +13,12 @@ jobs: steps: - name: Setup .NET SDK step - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 with: dotnet-version: 10.x - name: Checkout code step - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Check code style step run: | From b6a54ebb53f1887e1f0133a0cc03ea9c92b3a2a1 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Thu, 7 May 2026 13:00:17 -0700 Subject: [PATCH 3/5] Document GitHub Actions SHA-pinning policy The previous commit pinned every third-party action to a commit SHA but didn't capture the rule itself. Add a "GitHub Actions pinning" section to AGENTS.md explaining what to pin, why floating tags are unsafe, and how to resolve a SHA when adding a new action. Add a one-line summary to copilot-instructions.md so AI agents don't reintroduce floating refs in suggestions. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/copilot-instructions.md | 1 + AGENTS.md | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 2adc7193..b6863ac2 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -21,6 +21,7 @@ For full rationale see [`AGENTS.md`](../AGENTS.md). Quick rules: - Develop accepts **squash merges only**; main accepts **merge commits only**. Don't suggest rebase-merge — it's disabled at the repo level. - Both branches **auto-publish on push**: develop produces NBGV prereleases (`X.Y.Z-g{sha}`) tagged `develop` on Docker Hub; main produces stable releases (`X.Y.Z`) tagged `latest`. - Dependabot targets **both** `main` and `develop` with the same ecosystems; major NuGet bumps gate on human review, everything else auto-merges via App-token-driven merge-bot. +- Every third-party GitHub Action is pinned to a full commit SHA with a `# vX.Y.Z` comment. Don't introduce `@v6` / `@main` / `@master` floating refs. - Don't recommend `git push --force` or `--force-with-lease`; both rulesets enforce `non_fast_forward`. - `version.json`'s `publicReleaseRefSpec` is `^refs/heads/main$` — bumping the base `version` field is the only manual versioning action. diff --git a/AGENTS.md b/AGENTS.md index b0458480..030df834 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -50,6 +50,14 @@ Bot-merged PRs (Dependabot) trigger the publish workflows automatically because Major NuGet bumps are not auto-merged by [merge-bot-pull-request.yml](.github/workflows/merge-bot-pull-request.yml) — they require human review. Major GitHub Actions bumps are auto-merged because the workflow execution itself is the validation surface. +## GitHub Actions pinning + +Every third-party action in `.github/workflows/*.yml` is pinned to a full commit SHA with a `# vX.Y.Z` trailing comment, e.g. `uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2`. Floating tags (`@v6`, `@main`, `@master`) are not used. Local reusable workflows (`./.github/workflows/*.yml`) are referenced by path and don't need pinning. + +**Why:** Floating tags can be silently re-pointed by the action's owner (or by a compromised account) to malicious code; a SHA pin is immutable. The version comment keeps the file readable and lets dependabot rewrite both the SHA and the comment together when bumping. + +When adding a new `uses:` line, resolve the latest release's commit SHA (`gh api repos///releases/latest`) and include the version comment. Don't ship a floating tag and "pin it later". + ## Merge bot [merge-bot-pull-request.yml](.github/workflows/merge-bot-pull-request.yml) auto-merges Dependabot PRs. Two key design choices: From 38daccc4d9140800ae9f218ecfefdd28e0dc6093 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Thu, 7 May 2026 13:11:13 -0700 Subject: [PATCH 4/5] Re-indent publish-periodic-docker-release.yml step lists Every other workflow file in the repo uses the over-indented YAML style where list items under `steps:` are at parent_indent+2 (e.g., `steps:` at column 4, `- name:` at column 6). This file alone used the compact style with both at the same column. Both are valid YAML and GitHub Actions accepts either, but consistency matters more than syntactic flexibility. Bring this file in line with the other six. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../publish-periodic-docker-release.yml | 98 +++++++++---------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/.github/workflows/publish-periodic-docker-release.yml b/.github/workflows/publish-periodic-docker-release.yml index f9aad9da..5651069d 100644 --- a/.github/workflows/publish-periodic-docker-release.yml +++ b/.github/workflows/publish-periodic-docker-release.yml @@ -34,35 +34,35 @@ jobs: steps: - - name: Get image size step - run: | - mkdir -p ${{ runner.temp }}/versions - touch ${{ runner.temp }}/versions/${{ matrix.file }} - echo Image: docker.io/ptr727/plexcleaner:${{ matrix.tag }} >> ${{ runner.temp }}/versions/${{ matrix.file }} - echo Size: $(docker manifest inspect -v docker.io/ptr727/plexcleaner:${{ matrix.tag }} | jq '.[] | select(.Descriptor.platform.architecture=="amd64") | [.OCIManifest.layers[].size] | add' | numfmt --to=iec) >> ${{ runner.temp }}/versions/${{ matrix.file }} - - - name: Write tool versions to file step - uses: addnab/docker-run-action@4f65fabd2431ebc8d299f8e5a018d79a769ae185 # v3 - with: - image: docker.io/ptr727/plexcleaner:${{ matrix.tag }} - options: --volume ${{ runner.temp }}/versions:/versions + - name: Get image size step run: | - echo OS: $(. /etc/os-release; echo $PRETTY_NAME) >> /versions/${{ matrix.file }} - echo dotNET: $(dotnet --info) >> /versions/${{ matrix.file }} - echo PlexCleaner: $(/PlexCleaner/PlexCleaner --version) >> /versions/${{ matrix.file }} - echo HandBrakeCLI: $(HandBrakeCLI --version) >> /versions/${{ matrix.file }} - echo MediaInfo: $(mediainfo --version) >> /versions/${{ matrix.file }} - echo MkvMerge: $(mkvmerge --version) >> /versions/${{ matrix.file }} - echo FfMpeg: $(ffmpeg -version) >> /versions/${{ matrix.file }} - - - name: Print versions step - run: cat ${{ runner.temp }}/versions/${{ matrix.file }} - - - name: Upload version artifacts step - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: versions-${{ matrix.file }} - path: ${{ runner.temp }}/versions/${{ matrix.file }} + mkdir -p ${{ runner.temp }}/versions + touch ${{ runner.temp }}/versions/${{ matrix.file }} + echo Image: docker.io/ptr727/plexcleaner:${{ matrix.tag }} >> ${{ runner.temp }}/versions/${{ matrix.file }} + echo Size: $(docker manifest inspect -v docker.io/ptr727/plexcleaner:${{ matrix.tag }} | jq '.[] | select(.Descriptor.platform.architecture=="amd64") | [.OCIManifest.layers[].size] | add' | numfmt --to=iec) >> ${{ runner.temp }}/versions/${{ matrix.file }} + + - name: Write tool versions to file step + uses: addnab/docker-run-action@4f65fabd2431ebc8d299f8e5a018d79a769ae185 # v3 + with: + image: docker.io/ptr727/plexcleaner:${{ matrix.tag }} + options: --volume ${{ runner.temp }}/versions:/versions + run: | + echo OS: $(. /etc/os-release; echo $PRETTY_NAME) >> /versions/${{ matrix.file }} + echo dotNET: $(dotnet --info) >> /versions/${{ matrix.file }} + echo PlexCleaner: $(/PlexCleaner/PlexCleaner --version) >> /versions/${{ matrix.file }} + echo HandBrakeCLI: $(HandBrakeCLI --version) >> /versions/${{ matrix.file }} + echo MediaInfo: $(mediainfo --version) >> /versions/${{ matrix.file }} + echo MkvMerge: $(mkvmerge --version) >> /versions/${{ matrix.file }} + echo FfMpeg: $(ffmpeg -version) >> /versions/${{ matrix.file }} + + - name: Print versions step + run: cat ${{ runner.temp }}/versions/${{ matrix.file }} + + - name: Upload version artifacts step + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: versions-${{ matrix.file }} + path: ${{ runner.temp }}/versions/${{ matrix.file }} update-readme: name: Create Docker README.md job @@ -72,27 +72,27 @@ jobs: steps: - - name: Checkout step - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Download version artifacts step - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: versions-* - merge-multiple: true - path: ${{ runner.temp }}/versions - - - name: Create README.md from README.m4 step - run: m4 --include=${{ runner.temp }}/versions ./Docker/README.m4 > ${{ runner.temp }}/README.md - - - name: Update Docker Hub README.md step - uses: peter-evans/dockerhub-description@1b9a80c056b620d92cedb9d9b5a223409c68ddfa # v5.0.0 - with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_PASSWORD }} - repository: ptr727/plexcleaner - short-description: ${{ github.event.repository.description }} - readme-filepath: ${{ runner.temp }}/README.md + - name: Checkout step + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Download version artifacts step + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: versions-* + merge-multiple: true + path: ${{ runner.temp }}/versions + + - name: Create README.md from README.m4 step + run: m4 --include=${{ runner.temp }}/versions ./Docker/README.m4 > ${{ runner.temp }}/README.md + + - name: Update Docker Hub README.md step + uses: peter-evans/dockerhub-description@1b9a80c056b620d92cedb9d9b5a223409c68ddfa # v5.0.0 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_PASSWORD }} + repository: ptr727/plexcleaner + short-description: ${{ github.event.repository.description }} + readme-filepath: ${{ runner.temp }}/README.md date-badge: name: Create BYOB date badge job From 78b8273c4c05f1da97458a8a7a850cc4544e3a88 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Thu, 7 May 2026 13:26:17 -0700 Subject: [PATCH 5/5] Document Dependabot secret duplication and tag-comment convention MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two clarifications surfaced by Copilot review on PR #692: 1. The merge-bot uses an App token minted from CODEGEN_APP_* secrets. For Dependabot-authored pull_request events, GitHub only exposes secrets from the Dependabot namespace (Settings → Secrets → Dependabot), not the regular Actions namespace. The secrets must exist in both, or the App-token step gets empty inputs at runtime. This is non-obvious and worth calling out explicitly so future maintainers don't strip the Dependabot duplicate as redundant. 2. The SHA-pinning comment convention is "match the upstream release tag" — usually # vX.Y.Z, but # v3 (or # master) when upstream only publishes major-only / branch tags. Don't fabricate a semver suffix; use what gh api repos///releases/latest returns as tag_name. Co-Authored-By: Claude Opus 4.7 (1M context) --- AGENTS.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 030df834..815bd456 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,11 +52,11 @@ Major NuGet bumps are not auto-merged by [merge-bot-pull-request.yml](.github/wo ## GitHub Actions pinning -Every third-party action in `.github/workflows/*.yml` is pinned to a full commit SHA with a `# vX.Y.Z` trailing comment, e.g. `uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2`. Floating tags (`@v6`, `@main`, `@master`) are not used. Local reusable workflows (`./.github/workflows/*.yml`) are referenced by path and don't need pinning. +Every third-party action in `.github/workflows/*.yml` is pinned to a full commit SHA with a trailing comment matching the upstream release tag, e.g. `uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2`. The comment is whatever tag the action's repo actually publishes — typically `# vX.Y.Z`, but use `# v3` if upstream only publishes major-only tags (e.g. `addnab/docker-run-action`) and `# master` if the action ships only a moving branch (rare). Floating refs without a SHA (`@v6`, `@main`, `@master`) are never used. Local reusable workflows (`./.github/workflows/*.yml`) are referenced by path and don't need pinning. -**Why:** Floating tags can be silently re-pointed by the action's owner (or by a compromised account) to malicious code; a SHA pin is immutable. The version comment keeps the file readable and lets dependabot rewrite both the SHA and the comment together when bumping. +**Why:** Floating tags can be silently re-pointed by the action's owner (or by a compromised account) to malicious code; a SHA pin is immutable. Matching the comment to upstream's actual release tag (rather than fabricating one) lets dependabot rewrite both the SHA and the comment together when bumping. -When adding a new `uses:` line, resolve the latest release's commit SHA (`gh api repos///releases/latest`) and include the version comment. Don't ship a floating tag and "pin it later". +When adding a new `uses:` line, resolve the latest release's commit SHA (`gh api repos///releases/latest`) and copy its `tag_name` into the comment verbatim. Don't ship a floating tag and "pin it later". ## Merge bot @@ -65,6 +65,8 @@ When adding a new `uses:` line, resolve the latest release's commit SHA (`gh api - **Branch-aware merge method**: the script picks `--squash` for PRs targeting develop and `--merge` for PRs targeting main, matching each ruleset's `allowed_merge_methods`. An unknown base branch is a hard error. - **App token, not GITHUB_TOKEN**: the merge step uses a token minted by `actions/create-github-app-token` from `CODEGEN_APP_CLIENT_ID` / `CODEGEN_APP_PRIVATE_KEY` secrets. Pushes authored by `GITHUB_TOKEN` are blocked from triggering downstream workflows by GitHub's recursion guard; without the App token, a Dependabot merge to develop would silently skip `publish-release.yml` and `publish-periodic-docker-release.yml`, leaving the develop Docker tag and prerelease stale. +The App secrets (`CODEGEN_APP_CLIENT_ID`, `CODEGEN_APP_PRIVATE_KEY`) must exist in **both** secret namespaces: Settings → Secrets and variables → **Actions**, and Settings → Secrets and variables → **Dependabot**. Since Sept 2021, GitHub injects only the Dependabot-namespace secrets when a Dependabot-authored `pull_request` event fires; the regular Actions namespace is not visible to that run. Without the Dependabot duplicate the App-token step gets empty inputs and merge-bot silently fails to auto-merge. (The trigger remains `pull_request`, not `pull_request_target` — the merge-bot doesn't check out PR code, but `pull_request` plus duplicated secrets is the simpler, less-permissive setup.) + ## Key Requirements for All Projects Derived from This Template ### Build & Quality Standards