From 661b945c57fd941b906947928ad51a6324672ccb Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Thu, 7 May 2026 17:30:03 -0700 Subject: [PATCH 01/15] Require a fresh Copilot pass before merging any PR (#694) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Require a fresh Copilot pass before merging any PR Document the rule that mergeStateStatus=CLEAN alone is not enough to merge — Copilot must have re-reviewed the latest commit after any thread resolutions or pushes. If Copilot does not auto re-review within a reasonable window, ask before merging; silence is not approval. This was missing from the previous round of process documentation. PR #693 was merged ~3 minutes after I (Claude) replied to Copilot's threads, before Copilot had a chance to post a fresh review on the new commit. The merge happened to be functionally correct but the process was wrong, and it's the kind of small step that hides real regressions in larger PRs. Co-Authored-By: Claude Opus 4.7 (1M context) * Bump develop's minor version after every develop->main merge Document the rule that, immediately after a develop->main merge lands and main's publish workflows complete, the next action is a small isolated PR bumping the minor in version.json on develop. Without it, develop's next NBGV prerelease is numerically lower than the stable that just shipped, producing visibly confusing version numbers in HISTORY.md, --version output, and consumer update prompts. Documentation only; the actual bump for the just-completed PR #693 promotion will land as a separate `bump-version-3.17` PR per the "don't bundle the bump with other work" guidance in this same change. Co-Authored-By: Claude Opus 4.7 (1M context) * Refine Copilot-pass rule per Copilot review Address four issues Copilot raised on the previous commit: - Clarify that review_on_push lives in the copilot_code_review ruleset rule (verifiable via gh api), not in repo source files. - Align the "no issues found" headline with the verification recipe by stating up front that Copilot posts COMMENTED reviews here, so a clean COMMENTED review with zero open threads IS the success state. - Specify committedDate as the exact field to compare submitted_at against, removing ambiguity between authoredDate and committedDate in `gh pr view --json commits` output. - Replace "ask the user" with "ask the maintainer" since this is a repo-wide doc that survives author changes. Co-Authored-By: Claude Opus 4.7 (1M context) * Make the Copilot fresh-review check use commit_id, not timestamps Copilot pointed out (rightly) that comparing submitted_at against committedDate is fragile: the reviews endpoint returns every author's every review, and timestamp drift between client and server can flip the comparison. The robust check is structural — does the latest Copilot review's commit_id equal headRefOid? Recipe rewritten to fetch headRefOid and the last Copilot review's commit_id and compare strings. Also added a pointer to the GitHub UI "Re-request review" flow for cases where Copilot doesn't auto re-review on push (which happens occasionally; observed on this PR). Co-Authored-By: Claude Opus 4.7 (1M context) * Fix two consistency issues from Copilot's round-4 review - AGENTS.md headline said the freshness check was "review submitted after committedDate" but step 2 specifies commit_id == headRefOid. Aligned the headline to also use the SHA-equality wording so the rule is internally consistent end-to-end. - copilot-instructions.md: "develop's next prerelease numbers below main's just-shipped stable" was missing a verb. Now reads "next prerelease version numbers fall below..." Co-Authored-By: Claude Opus 4.7 (1M context) * Bot login consistency and paginated reviews lookup Round 5 Copilot findings: - Use `copilot-pull-request-reviewer[bot]` (the bot login, with "(shown as Copilot in the UI)" gloss) consistently in prose so it matches the jq filter in the verification recipe — copy/paste from the doc now produces a working command. - The reviews endpoint is paginated by default in gh CLI; on PRs with many review entries `last` could pick a stale Copilot review from page 1. Use `--paginate` and a streaming `tail -1` filter so the latest Copilot commit_id is reliably found regardless of review-list length. Co-Authored-By: Claude Opus 4.7 (1M context) * Use / placeholder consistently in API recipes The Merging-a-PR section mixed `` and `/` in adjacent gh api recipes; copy-pasting the bare `` form would fail. Standardised on `/` to match the rest of the doc. Co-Authored-By: Claude Opus 4.7 (1M context) --------- Co-authored-by: Claude Opus 4.7 (1M context) --- .github/copilot-instructions.md | 2 ++ AGENTS.md | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index b6863ac2..873c28b4 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -22,6 +22,8 @@ For full rationale see [`AGENTS.md`](../AGENTS.md). Quick rules: - 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. +- Never merge a PR without a fresh "no issues found" review from `copilot-pull-request-reviewer[bot]` (shown as "Copilot" in the UI) on the latest commit. `mergeStateStatus: CLEAN` is necessary but not sufficient — Copilot's re-review of the latest push is required. If Copilot doesn't auto re-review within ~5 min of the last push, ask before merging. See [`AGENTS.md`](../AGENTS.md#merging-a-pr). +- After a develop → main merge lands and main's publish workflows complete, bump the minor in `version.json` on develop (e.g. `3.16` → `3.17`) via an isolated `bump-version-X.Y` PR. Without it, develop's next prerelease version numbers fall below main's just-shipped stable. - 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 815bd456..e88b60d8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,12 +22,35 @@ For comprehensive coding standards and detailed conventions, refer to [`.github/ 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). +## Merging a PR + +**Never merge a PR without `copilot-pull-request-reviewer[bot]` (shown as "Copilot" in the GitHub UI; the `[bot]` suffix is its actual login) having posted a clean re-review on the latest commit** — defined as a review whose `commit_id` (or GraphQL `commit.oid`) equals the PR's `headRefOid`, with no new unresolved inline threads (Copilot in this repo posts `COMMENTED` reviews, not `APPROVED`, so a clean COMMENTED review with zero open threads is the "no issues found" outcome). `mergeStateStatus: CLEAN` only confirms ruleset gates (thread resolution, status checks, signatures); it does not confirm Copilot has re-evaluated the latest changes. + +After resolving Copilot's threads or pushing fixes: + +1. Wait for Copilot to post a fresh review on the new head commit. The `copilot_code_review` rule on both `develop` and `main` rulesets has `review_on_push: true` configured (verify with `gh api repos///rulesets/ --jq '.rules[] | select(.type=="copilot_code_review")'`), so a re-review normally lands within a few minutes. +2. Verify Copilot's most recent review targets the current head — compare its `commit_id` to `headRefOid`, not timestamps (multiple reviews and authors clutter the list, and timestamp drift is unreliable): + + ```sh + head=$(gh pr view --json headRefOid --jq .headRefOid) + last=$(gh api --paginate repos///pulls//reviews --jq '.[] | select(.user.login == "copilot-pull-request-reviewer[bot]") | .commit_id' | tail -1) + [ "$head" = "$last" ] && echo "fresh" || echo "stale" + ``` + +3. If the fresh review is `COMMENTED` with zero unresolved inline threads (or `APPROVED`), the PR is good to merge. +4. If the fresh review introduces new concerns (inline threads or body-level objections), address them and loop. +5. **If Copilot does not auto re-review within a reasonable window after the latest push (~5 min), do not merge — ask the maintainer.** Silence is not approval. Copilot can be re-prompted manually from the GitHub PR UI ("Re-request review" on the Copilot reviewer entry). + +This applies to every human-authored PR (feature → develop, develop → main). The merge-bot workflow's auto-merge of dependabot bumps is the only exception and is governed separately by the `update-type` filter. + ## 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. +**Immediately after a develop → main merge lands and main's publish workflows complete, bump the minor version in [version.json](version.json) on develop.** Open a small isolated feature PR `bump-version-X.Y` (e.g. `"version": "3.16"` → `"version": "3.17"`), squash into develop, and continue feature work from there. Without this bump, develop's next NBGV-computed prerelease (`3.16.-g{sha}`) is *numerically lower* than the stable that just shipped (`3.16.`), which is visibly confusing in HISTORY.md, `--version` output, and consumer update prompts. Bumping ensures every develop prerelease is `3.17.-g{sha}` — visibly newer than main's `3.16.`. Don't bundle the bump with other work; keep the PR isolated so the version change is unambiguous in git blame. + ## 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. From 52017aa320925115c97b86558d828e0aa567d0bf Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Thu, 7 May 2026 17:42:57 -0700 Subject: [PATCH 02/15] Bump version to 3.17 for next prerelease cycle (#695) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Main just shipped 3.16.7 (PR #693 promotion). Per the rule documented in AGENTS.md "Develop → Main Promotion" section, bump develop's minor so the next prerelease lands at 3.17.X-g{sha}, visibly above main's just-shipped stable rather than below it. Co-authored-by: Claude Opus 4.7 (1M context) --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index a79bea72..746ac17f 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "3.16", + "version": "3.17", "publicReleaseRefSpec": [ "^refs/heads/main$" ], From 35303b77b0dcecf54c6d3cac80b5465586ee3f25 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 05:55:08 +0000 Subject: [PATCH 03/15] Bump actions/create-github-app-token in the actions-deps group (#696) Bumps the actions-deps group with 1 update: [actions/create-github-app-token](https://github.com/actions/create-github-app-token). Updates `actions/create-github-app-token` from 3.1.1 to 3.2.0 - [Release notes](https://github.com/actions/create-github-app-token/releases) - [Changelog](https://github.com/actions/create-github-app-token/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/create-github-app-token/compare/1b10c78c7865c340bc4f6099eb2f838309f1e8c3...bcd2ba49218906704ab6c1aa796996da409d3eb1) --- updated-dependencies: - dependency-name: actions/create-github-app-token dependency-version: 3.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions-deps ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/merge-bot-pull-request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/merge-bot-pull-request.yml b/.github/workflows/merge-bot-pull-request.yml index d2f3f73f..2c58a94a 100644 --- a/.github/workflows/merge-bot-pull-request.yml +++ b/.github/workflows/merge-bot-pull-request.yml @@ -34,7 +34,7 @@ jobs: # 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 + 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 }} From a0eb31e366f9db67086e0393aebb237dc91967ca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 May 2026 05:55:48 +0000 Subject: [PATCH 04/15] Bump the nuget-deps group with 3 updates (#700) Bumps Microsoft.SourceLink.GitHub from 10.0.203 to 10.0.300 Bumps ptr727.LanguageTags from 1.2.29 to 1.2.43 Bumps System.CommandLine from 2.0.7 to 2.0.8 --- updated-dependencies: - dependency-name: Microsoft.SourceLink.GitHub dependency-version: 10.0.300 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-deps - dependency-name: ptr727.LanguageTags dependency-version: 1.2.43 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-deps - dependency-name: System.CommandLine dependency-version: 2.0.8 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-deps ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Directory.Packages.props | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index c8dc0620..dff0955d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,14 +5,14 @@ - - + + - + From da680b8b959d249b6838903cc0bdad58e5fa7945 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 18:34:31 +0000 Subject: [PATCH 05/15] Bump the nuget-deps group with 1 update (#702) Bumps coverlet.collector from 10.0.0 to 10.0.1 --- updated-dependencies: - dependency-name: coverlet.collector dependency-version: 10.0.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-deps ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index dff0955d..92955a7b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,7 +2,7 @@ - + From e0eabdacd074646e0d3d7c2343710233aa787685 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 05:53:05 +0000 Subject: [PATCH 06/15] Bump dotnet/nbgv from 0.5.1 to 0.5.2 in the actions-deps group (#704) Bumps the actions-deps group with 1 update: [dotnet/nbgv](https://github.com/dotnet/nbgv). Updates `dotnet/nbgv` from 0.5.1 to 0.5.2 - [Release notes](https://github.com/dotnet/nbgv/releases) - [Commits](https://github.com/dotnet/nbgv/compare/3cf2d96c2aa00675081b59f401356ac1fb81092f...705dad19ab067f12f4e9eeaa60812e01edef5d25) --- updated-dependencies: - dependency-name: dotnet/nbgv dependency-version: 0.5.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-deps ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/get-version-task.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/get-version-task.yml b/.github/workflows/get-version-task.yml index c8b522cb..465e31df 100644 --- a/.github/workflows/get-version-task.yml +++ b/.github/workflows/get-version-task.yml @@ -38,4 +38,4 @@ jobs: - name: Run Nerdbank.GitVersioning tool step id: nbgv - uses: dotnet/nbgv@3cf2d96c2aa00675081b59f401356ac1fb81092f # v0.5.1 + uses: dotnet/nbgv@705dad19ab067f12f4e9eeaa60812e01edef5d25 # v0.5.2 From e1fd6b96d5e80499df285217e7b01f40c75d5f57 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 05:55:22 +0000 Subject: [PATCH 07/15] Bump docker/build-push-action in the actions-deps group (#706) Bumps the actions-deps group with 1 update: [docker/build-push-action](https://github.com/docker/build-push-action). Updates `docker/build-push-action` from 7.1.0 to 7.2.0 - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/bcafcacb16a39f128d818304e6c9c0c18556b85f...f9f3042f7e2789586610d6e8b85c8f03e5195baf) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-version: 7.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions-deps ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-docker-task.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-docker-task.yml b/.github/workflows/build-docker-task.yml index 922af0fe..cdd70dd5 100644 --- a/.github/workflows/build-docker-task.yml +++ b/.github/workflows/build-docker-task.yml @@ -53,7 +53,7 @@ jobs: password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Docker build and push step - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: . push: ${{ inputs.push }} From 85a86cc3cab3992f32677bfc6bd396b93debc0b4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 07:45:37 +0000 Subject: [PATCH 08/15] Bump the actions-deps group with 2 updates (#708) Bumps the actions-deps group with 2 updates: [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) and [docker/login-action](https://github.com/docker/login-action). Updates `docker/setup-buildx-action` from 4.0.0 to 4.1.0 - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd...d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5) Updates `docker/login-action` from 4.1.0 to 4.2.0 - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/4907a6ddec9925e35a0a9e82d7399ccc52663121...650006c6eb7dba73a995cc03b0b2d7f5ca915bee) --- updated-dependencies: - dependency-name: docker/setup-buildx-action dependency-version: 4.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions-deps - dependency-name: docker/login-action dependency-version: 4.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions-deps ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-docker-task.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-docker-task.yml b/.github/workflows/build-docker-task.yml index cdd70dd5..c2547860 100644 --- a/.github/workflows/build-docker-task.yml +++ b/.github/workflows/build-docker-task.yml @@ -40,14 +40,14 @@ jobs: platforms: linux/amd64,linux/arm64 - name: Setup Buildx step - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.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@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} From 834711f95640b105f8a1538c422b31becb325292 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 May 2026 07:41:47 +0000 Subject: [PATCH 09/15] Bump the nuget-deps group with 2 updates (#710) Bumps Microsoft.NET.Test.Sdk from 18.5.1 to 18.6.0 Bumps ptr727.LanguageTags from 1.2.43 to 1.2.45 --- updated-dependencies: - dependency-name: Microsoft.NET.Test.Sdk dependency-version: 18.6.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-deps - dependency-name: ptr727.LanguageTags dependency-version: 1.2.45 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-deps ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 92955a7b..e1b8ef2c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,9 +4,9 @@ - + - + From f5792db8bb7f860d871c2735c7ec0caafc2d4922 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 05:57:22 +0000 Subject: [PATCH 10/15] Bump the actions-deps group with 2 updates (#712) Bumps the actions-deps group with 2 updates: [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) and [actions/setup-dotnet](https://github.com/actions/setup-dotnet). Updates `docker/setup-qemu-action` from 4.0.0 to 4.1.0 - [Release notes](https://github.com/docker/setup-qemu-action/releases) - [Commits](https://github.com/docker/setup-qemu-action/compare/ce360397dd3f832beb865e1373c09c0e9f86d70a...06116385d9baf250c9f4dcb4858b16962ea869c3) Updates `actions/setup-dotnet` from 5.2.0 to 5.3.0 - [Release notes](https://github.com/actions/setup-dotnet/releases) - [Commits](https://github.com/actions/setup-dotnet/compare/c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7...9a946fdbd5fb07b82b2f5a4466058b876ab72bb2) --- updated-dependencies: - dependency-name: docker/setup-qemu-action dependency-version: 4.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions-deps - dependency-name: actions/setup-dotnet dependency-version: 5.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions-deps ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-docker-task.yml | 2 +- .github/workflows/build-executable-task.yml | 2 +- .github/workflows/get-version-task.yml | 2 +- .github/workflows/test-release-task.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-docker-task.yml b/.github/workflows/build-docker-task.yml index c2547860..f28c1648 100644 --- a/.github/workflows/build-docker-task.yml +++ b/.github/workflows/build-docker-task.yml @@ -35,7 +35,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup QEMU step - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 + uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 with: platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/build-executable-task.yml b/.github/workflows/build-executable-task.yml index d85fd93d..2f8b6205 100644 --- a/.github/workflows/build-executable-task.yml +++ b/.github/workflows/build-executable-task.yml @@ -25,7 +25,7 @@ jobs: steps: - name: Setup .NET SDK step - uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 + uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0 with: dotnet-version: 10.x diff --git a/.github/workflows/get-version-task.yml b/.github/workflows/get-version-task.yml index 465e31df..f6785e18 100644 --- a/.github/workflows/get-version-task.yml +++ b/.github/workflows/get-version-task.yml @@ -27,7 +27,7 @@ jobs: steps: - name: Setup .NET SDK step - uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 + uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0 with: dotnet-version: 10.x diff --git a/.github/workflows/test-release-task.yml b/.github/workflows/test-release-task.yml index 6b5cfae3..c23a1ca4 100644 --- a/.github/workflows/test-release-task.yml +++ b/.github/workflows/test-release-task.yml @@ -13,7 +13,7 @@ jobs: steps: - name: Setup .NET SDK step - uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 + uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0 with: dotnet-version: 10.x From b7ab43728729a8615ef83816709a579f54c9c94e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 May 2026 05:58:48 +0000 Subject: [PATCH 11/15] Bump the nuget-deps group with 1 update (#714) Bumps ptr727.LanguageTags from 1.2.45 to 1.2.47 --- updated-dependencies: - dependency-name: ptr727.LanguageTags dependency-version: 1.2.47 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-deps ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index e1b8ef2c..7368ff3c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,7 +6,7 @@ - + From d792c0147898a2716aeb3a3e8512e3fede12d4d6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2026 18:36:16 +0000 Subject: [PATCH 12/15] Bump the nuget-deps group with 2 updates (#716) Bumps dotnet-outdated-tool from 4.7.1 to 4.8.0 Bumps ptr727.LanguageTags from 1.2.47 to 1.2.49 --- updated-dependencies: - dependency-name: dotnet-outdated-tool dependency-version: 4.8.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: nuget-deps - dependency-name: ptr727.LanguageTags dependency-version: 1.2.49 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-deps ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .config/dotnet-tools.json | 2 +- Directory.Packages.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 2b10359b..c6574309 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -17,7 +17,7 @@ "rollForward": false }, "dotnet-outdated-tool": { - "version": "4.7.1", + "version": "4.8.0", "commands": [ "dotnet-outdated" ], diff --git a/Directory.Packages.props b/Directory.Packages.props index 7368ff3c..f1e75e4a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,7 +6,7 @@ - + From c4db07c7b443adb5e27eae8c8b0a8707bf0d2aee Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 22:44:39 +0000 Subject: [PATCH 13/15] Bump the nuget-deps group with 2 updates (#719) Bumps CliWrap from 3.10.1 to 3.10.2 Bumps ptr727.LanguageTags from 1.2.49 to 1.2.51 --- updated-dependencies: - dependency-name: CliWrap dependency-version: 3.10.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-deps - dependency-name: ptr727.LanguageTags dependency-version: 1.2.51 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nuget-deps ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index f1e75e4a..b17e5330 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,12 +1,12 @@ - + - + From f37f69fd34afbf619da548d5a3260a954262c789 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Wed, 3 Jun 2026 22:33:49 -0700 Subject: [PATCH 14/15] Adopt two-phase CI/CD: PR smoke builds, opt-in publisher (#723) Port the two-phase CI/CD pattern from ProjectTemplate into PlexCleaner (closes #722). PRs run path-gated smoke builds; publish-release.yml becomes the sole publisher (weekly schedule + dispatch build both branches; push publishes only when PUBLISH_ON_MERGE=true). Thread required branch/ref/smoke through every reusable task, branch-scope artifacts + Docker cache, pin releases to GitCommitId, absorb the periodic Docker workflow, and update AGENTS.md/copilot-instructions.md/README. --- .github/copilot-instructions.md | 146 ++++++++++++++++-- .github/workflows/build-datebadge-task.yml | 10 +- .github/workflows/build-docker-task.yml | 86 ++++++++--- .github/workflows/build-dockerreadme-task.yml | 54 +++++++ .github/workflows/build-executable-task.yml | 38 ++++- .github/workflows/build-release-task.yml | 101 +++++++++++- .github/workflows/build-toolversions-task.yml | 74 +++++++++ .github/workflows/get-version-task.yml | 16 ++ .github/workflows/merge-bot-pull-request.yml | 6 +- .../publish-periodic-docker-release.yml | 103 ------------ .github/workflows/publish-release.yml | 111 ++++++++++++- .github/workflows/test-pull-request.yml | 109 ++++++++++++- .github/workflows/test-release-task.yml | 40 ----- AGENTS.md | 39 ++--- README.md | 6 +- 15 files changed, 712 insertions(+), 227 deletions(-) create mode 100644 .github/workflows/build-dockerreadme-task.yml create mode 100644 .github/workflows/build-toolversions-task.yml delete mode 100644 .github/workflows/publish-periodic-docker-release.yml delete mode 100644 .github/workflows/test-release-task.yml diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 873c28b4..df3a6198 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -19,10 +19,10 @@ 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`. +- **Two-phase publishing.** PRs only **smoke-build** changed targets (Docker `linux/amd64`, a 2-runtime executable subset, no push). `publish-release.yml` is the sole publisher: its **weekly schedule + manual dispatch** build/publish **both** branches (develop ⇒ NBGV prereleases `X.Y.Z-g{sha}` tagged `develop`; main ⇒ stable `X.Y.Z` tagged `latest`). Routine merges do **not** publish unless the `PUBLISH_ON_MERGE` repo variable is `true`. - 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. -- Never merge a PR without a fresh "no issues found" review from `copilot-pull-request-reviewer[bot]` (shown as "Copilot" in the UI) on the latest commit. `mergeStateStatus: CLEAN` is necessary but not sufficient — Copilot's re-review of the latest push is required. If Copilot doesn't auto re-review within ~5 min of the last push, ask before merging. See [`AGENTS.md`](../AGENTS.md#merging-a-pr). +- Never merge a PR without a fresh "no issues found" review from `copilot-pull-request-reviewer[bot]` (shown as "Copilot" in the UI) on the latest commit. `mergeStateStatus: CLEAN` is necessary but not sufficient — Copilot's re-review of the latest push is required. Re-request the review **programmatically** after every push via the `requestReviews` GraphQL mutation (don't wait on flaky auto-review-on-push) — see the [GitHub Copilot Review Runbook](#github-copilot-review-runbook) below and [`AGENTS.md`](../AGENTS.md#merging-a-pr). - After a develop → main merge lands and main's publish workflows complete, bump the minor in `version.json` on develop (e.g. `3.16` → `3.17`) via an isolated `bump-version-X.Y` PR. Without it, develop's next prerelease version numbers fall below main's just-shipped stable. - 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. @@ -302,11 +302,13 @@ dotnet husky run ### GitHub Actions -- **publish-release.yml**: Multi-runtime matrix build (win, linux, osx x x64/arm/arm64) -- **publish-periodic-docker-release.yml**: Multi-arch Docker builds (linux/amd64, linux/arm64) -- **test-pull-request.yml**: PR validation -- Version info: `version.json` with Nerdbank.GitVersioning format -- Branches: `main` (stable releases), `develop` (pre-releases) +Two-phase model — reusable `*-task.yml` workflows orchestrated by two entry points: + +- **test-pull-request.yml**: PR validation. `changes` (dorny/paths-filter) → always-on `unit-test` (Husky) + path-gated `smoke-build` (reduced, no-push) → `Check pull request workflow status` aggregator (ruleset-bound name; requires `changes` succeeded). +- **publish-release.yml**: the **sole publisher** (`push` + weekly `schedule` + `workflow_dispatch`). A `setup` job computes the branch list + publish gate; the `publish` matrix builds both branches via `build-release-task.yml` (executable 7-RID matrix + multi-arch Docker `linux/amd64,linux/arm64` + GitHub release), then `tool-versions`, `docker-readme` (main only), `date-badge` (main only). +- Reusable tasks: `build-release-task.yml`, `build-executable-task.yml`, `build-docker-task.yml`, `build-toolversions-task.yml`, `build-dockerreadme-task.yml`, `build-datebadge-task.yml`, `get-version-task.yml`. All thread a required `branch` input (config keys off it, never `github.ref_name`) plus `ref`/`smoke`. +- Version info: `version.json` with Nerdbank.GitVersioning format. `get-version-task.yml` surfaces `SemVer2`, the assembly versions, and `GitCommitId` (used to pin the release `target_commitish`). +- Branches: `main` (stable releases, `latest`), `develop` (pre-releases, `develop`). ### Docker @@ -464,9 +466,131 @@ Check states with `HasFlag()`, combine with `|=` ## Git and Commit Rules -**These rules are absolute — no exceptions:** - -- **Never make git commits.** All commits must be cryptographically signed (SSH/GPG). AI coding agents cannot produce signed commits. Stage changes with `git add` and leave `git commit` to the developer, who must run it in their own environment where signing keys are available. +- **Default to staging, not committing.** Stage changes with `git add` and leave `git commit` to the developer unless explicitly authorized to commit for the current ask ("commit this", "open a PR"). Authorization is scope-bound to that task. +- **All commits must be cryptographically signed (SSH/GPG)** — branch protection rejects unsigned commits. Signing depends on environment config (`commit.gpgsign`, a `user.signingkey`, a loaded agent). If signing isn't configured, **do not commit** — stop at `git add` and surface it. Verify first: `git config --get commit.gpgsign && ssh-add -L`. - **Never force push.** Do not run `git push --force` or `git push --force-with-lease`. Force pushing rewrites shared branch history and is blocked by branch protection rules. - **Never run destructive git commands** (`git reset --hard`, `git checkout .`, `git restore .`, `git clean -f`) without explicit developer instruction. -- **Staging is the limit.** Prepare changes and stage files; the developer handles all commits and pushes. +- **The `develop → main` release merge is maintainer-only.** Drive `feature → develop` PRs end-to-end when authorized (commit, push, Copilot review loop, squash-merge), but never self-merge a release to `main`. + +## GitHub Copilot Review Runbook + +Provider-specific mechanics for driving GitHub Copilot reviews entirely via `gh`/GraphQL — no manual UI clicks. The review-loop *contract* (re-request on every push, verify head-SHA coverage, triage, reply + resolve, escalate when stuck) is in [AGENTS.md → Merging a PR](../AGENTS.md#merging-a-pr); this section is how to make Copilot reliably execute it. + +### Triggering and Polling + +Auto-review on push is configured (the branch ruleset's `copilot_code_review` rule with `review_on_push: true`) but fires inconsistently — treat it as best-effort. After every push, **re-request a review programmatically** via the GraphQL `requestReviews` mutation, passing the Copilot reviewer's bot node id in `botIds`. This drives the loop end-to-end without a maintainer clicking "re-request review" in the UI. + +> **The reviewer login differs by API — this is intentional, not a typo.** In **GraphQL** (`gh api graphql` and `gh pr view --json reviews`, which is GraphQL-backed) the `Bot.login` is `copilot-pull-request-reviewer` — **no `[bot]` suffix**. In the **REST** API (`gh api repos/.../issues|pulls/...`) the same account's `user.login` is `copilot-pull-request-reviewer[bot]` — **with** the suffix. Each query below uses the correct form for its API; match the API, not a single spelling, when adapting them. (The prose elsewhere referring to `copilot-pull-request-reviewer[bot]` is describing the REST/display login.) + +```sh +# 1. PR node id + the Copilot reviewer's bot node id (read from any existing +# Copilot review; the reviewer login is `copilot-pull-request-reviewer`). +PR_NODE=$(gh pr view --json id --jq '.id') +BOT_ID=$(gh api graphql -f query=' +{ + repository(owner: "ptr727", name: "PlexCleaner") { + pullRequest(number: ) { + reviews(first: 50) { nodes { author { __typename login ... on Bot { id } } } } + } + } +}' --jq '[.data.repository.pullRequest.reviews.nodes[] + | select(.author.login == "copilot-pull-request-reviewer") + | .author.id] | first') + +# 2. Re-request a Copilot review on the current head. +gh api graphql -f query=' +mutation($pr: ID!, $bot: ID!) { + requestReviews(input: { pullRequestId: $pr, botIds: [$bot], union: true }) { + pullRequest { id } + } +}' -F pr="$PR_NODE" -F bot="$BOT_ID" +``` + +The bot node id is read from an existing Copilot review, so step 1 needs at least one prior review on the PR — auto-review-on-open normally supplies the first. If none exists yet and auto-review didn't fire, request `Copilot` once through the GitHub PR UI to seed it, then use the mutation for every subsequent re-request. The Copilot reviewer bot's global node id is `BOT_kgDOCnlnWA` (login `copilot-pull-request-reviewer`) if you need to skip discovery. + +**Do NOT post `@Copilot review` as a PR comment.** That triggers the Copilot *coding agent* (`copilot-swe-agent[bot]`), which makes code changes rather than posting a review. + +Known non-working request paths (use the `requestReviews` mutation instead): + +- `POST /requested_reviewers` with `reviewers=[Copilot]` can return 200 but no-op. +- `copilot-pull-request-reviewer` as a requested reviewer slug returns 422. + +### Verify Review Covered Current Head + +Before merging, confirm Copilot reviewed the current PR head SHA. Copilot may respond as a formal review (carries an exact commit SHA) or an issue comment (no SHA). Check both. + +```sh +PR_HEAD=$(gh pr view --json headRefOid --jq '.headRefOid') + +# 1. Formal review — exact SHA match. +gh pr view --json reviews --jq \ + '.reviews[] | select(.author.login=="copilot-pull-request-reviewer") | .commit.oid' \ + | grep -q "$PR_HEAD" && echo "covered via formal review" + +# 2. Issue comment — show the most recent Copilot comment for manual +# confirmation. This is the REST API, so the login carries the `[bot]` suffix. +gh api repos/ptr727/PlexCleaner/issues//comments --jq \ + '[.[] | select(.user.login=="copilot-pull-request-reviewer[bot]")] | last | {created_at, body: .body[:200]}' +``` + +Coverage is confirmed when (1) exits 0. For issue comments (path 2), body content is the only reliable signal — `created_at` is not (commit timestamps can predate the push). Treat path (2) as confirmed only when the comment body explicitly refers to the current changes. + +### Bounded Retry Workflow + +If a review did not run on the current head: + +1. Wait briefly and check head-SHA coverage (above). +1. Re-request via the `requestReviews` mutation; fall back to the GitHub PR UI only if the mutation no-ops. +1. Retry up to two more times (three total). +1. If still missing, mark the review blocked and escalate to the maintainer with what was attempted. + +### Reply and Thread Resolution Workflow + +List unresolved threads (`first: 100` + cursor pagination; if `hasNextPage`, re-run with `after: ""`): + +```sh +gh api graphql -f query=' +{ + repository(owner: "ptr727", name: "PlexCleaner") { + pullRequest(number: ) { + reviewThreads(first: 100) { + nodes { + id isResolved path + comments(first: 1) { nodes { author { login } body } } + } + pageInfo { hasNextPage endCursor } + } + } + } +}' | jq ' + .data.repository.pullRequest.reviewThreads | + (.pageInfo | "hasNextPage=\(.hasNextPage) endCursor=\(.endCursor)"), + (.nodes[] | select(.isResolved == false)) +' +``` + +Reply on a thread, then resolve it: + +```sh +gh api graphql -f query=' +mutation($threadId: ID!, $body: String!) { + addPullRequestReviewThreadReply(input: { pullRequestReviewThreadId: $threadId, body: $body }) { + comment { id } + } +}' -F threadId="PRRT_..." -F body="Fixed in : ." + +gh api graphql -f query=' +mutation($threadId: ID!) { + resolveReviewThread(input: { threadId: $threadId }) { thread { id isResolved } } +}' -F threadId="PRRT_..." +``` + +Issue-level Copilot comments (those in `issues//comments`) have no resolution action — reply if the finding warrants it; no resolution step is possible. + +Reply-body conventions: + +- Accepted bug/style fix: include fixing commit SHA and a one-line summary. +- Declined style comment: cite the rule (AGENTS.md or CODESTYLE) and the existing-tree precedent. +- Declined architecture proposal: one-sentence rationale. + +A PR is mergeable when `mergeStateStatus == CLEAN` and there are 0 unresolved threads on the current head. After the final push, sweep-resolve stale older threads for removed code paths. diff --git a/.github/workflows/build-datebadge-task.yml b/.github/workflows/build-datebadge-task.yml index e6cb9dfc..437879a6 100644 --- a/.github/workflows/build-datebadge-task.yml +++ b/.github/workflows/build-datebadge-task.yml @@ -2,6 +2,14 @@ name: Build BYOB date badge task on: workflow_call: + inputs: + # Logical branch this badge run is for. The badge only updates on + # `main`; the publisher passes the branch explicitly so a scheduled run + # building `develop` doesn't try to write the main badge. Required (no + # `github.ref_name` fallback) so the gate can't silently misfire. + branch: + required: true + type: string jobs: @@ -16,7 +24,7 @@ jobs: run: echo "date=$(date)" >> $GITHUB_OUTPUT - name: Build BYOB date badge step - if: ${{ github.ref_name == 'main' }} + if: ${{ inputs.branch == 'main' }} uses: RubbaBoy/BYOB@24f464284c1fd32028524b59607d417a2e36fee7 # v1.3.0 with: name: lastbuild diff --git a/.github/workflows/build-docker-task.yml b/.github/workflows/build-docker-task.yml index f28c1648..f40677b3 100644 --- a/.github/workflows/build-docker-task.yml +++ b/.github/workflows/build-docker-task.yml @@ -8,6 +8,26 @@ on: required: false type: boolean default: false + # Git ref to check out / version (empty = default checkout ref). + ref: + required: false + type: string + default: '' + # Logical branch driving config and tags (`main` => Release/`latest`, + # anything else => Debug/`develop`). Required (no `github.ref_name` + # fallback): the publisher builds develop from a run whose + # `github.ref_name` is `main`, so a silent fallback would mistag it. + # The orchestrator always passes it explicitly. + branch: + required: true + type: string + # Smoke mode: build `linux/amd64` only (no QEMU/arm64), never push, and + # skip the shared registry `cache-to` so PR builds don't pollute the + # release buildcache. Used for fast PR feedback. + smoke: + required: false + type: boolean + default: false jobs: @@ -15,56 +35,80 @@ jobs: name: Get version information job uses: ./.github/workflows/get-version-task.yml secrets: inherit + with: + ref: ${{ inputs.ref }} build-docker: name: Build Docker image job runs-on: ubuntu-latest needs: [get-version] - strategy: - matrix: - include: - - file: ./Docker/Dockerfile - platforms: linux/amd64,linux/arm64 - tags: | - docker.io/ptr727/plexcleaner:${{ github.ref_name == 'main' && 'latest' || 'develop' }} - docker.io/ptr727/plexcleaner:${{ needs.get-version.outputs.SemVer2 }} steps: - name: Checkout step uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ inputs.ref }} + # QEMU only exists to emulate arm64. Smoke builds are amd64-only, so + # skip it entirely to save the emulation setup cost. - name: Setup QEMU step - uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 + if: ${{ !inputs.smoke }} + uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 with: platforms: linux/amd64,linux/arm64 - name: Setup Buildx step - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 with: - platforms: linux/amd64,linux/arm64 + platforms: ${{ inputs.smoke && 'linux/amd64' || '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 + # Always login to Docker Hub, not just on push, to benefit from higher + # rate limits with a Docker subscription for pulls and cache reads on + # every build (including smoke). This is a CONSCIOUS choice over gating + # login on `inputs.push`: the trade-off is that fork PRs without access + # to the Docker Hub secrets cannot run the Docker smoke build. Acceptable + # because PlexCleaner's contribution flow is same-repo branch PRs + + # Dependabot (App token); a fork-PR Docker smoke path would alternatively + # gate this step on `inputs.push` and accept anonymous-rate-limited reads. - name: Login to Docker Hub step - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Docker build and push step - uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: . push: ${{ inputs.push }} - file: ${{ matrix.file }} - tags: ${{ matrix.tags }} - platforms: ${{ matrix.platforms }} - cache-from: type=registry,ref=docker.io/ptr727/plexcleaner:buildcache - cache-to: type=registry,ref=docker.io/ptr727/plexcleaner:buildcache,mode=max + file: ./Docker/Dockerfile + tags: | + docker.io/ptr727/plexcleaner:${{ inputs.branch == 'main' && 'latest' || 'develop' }} + docker.io/ptr727/plexcleaner:${{ needs.get-version.outputs.SemVer2 }} + platforms: ${{ inputs.smoke && 'linux/amd64' || 'linux/amd64,linux/arm64' }} + # Branch-scoped registry cache. READ both branches' caches — the + # layers are nearly identical (only BUILD_CONFIGURATION differs), so + # a main build can seed from develop's cache and vice versa — but + # WRITE only this branch's own tag, and only when actually pushing. + # Gating the export on `inputs.push` (not just `!smoke`) means a + # non-publishing build never writes the shared registry cache or + # needs Docker Hub write creds — smoke builds (always push=false) are + # covered too. Branch-scoping is what lets the publisher's weekly + # matrix build main and develop concurrently in one run without the + # two legs overwriting a single shared cache (destroying hit rates). + # `mode=max` caches the multi-stage builder layers (the expensive + # `dotnet publish`); `ignore-error=true` keeps a transient cache + # export failure from failing an otherwise-good publish. Registry + # (not gha) cache because the weekly publish cadence would let gha's + # 7-day unused-entry eviction drop branch caches between publishes. + cache-from: | + type=registry,ref=docker.io/ptr727/plexcleaner:buildcache-main + type=registry,ref=docker.io/ptr727/plexcleaner:buildcache-develop + cache-to: ${{ inputs.push && format('type=registry,ref=docker.io/ptr727/plexcleaner:buildcache-{0},mode=max,ignore-error=true', inputs.branch) || '' }} build-args: | LABEL_VERSION=${{ needs.get-version.outputs.SemVer2 }} - BUILD_CONFIGURATION=${{ github.ref_name == 'main' && 'Release' || 'Debug' }} + BUILD_CONFIGURATION=${{ inputs.branch == 'main' && 'Release' || 'Debug' }} BUILD_VERSION=${{ needs.get-version.outputs.AssemblyVersion }} BUILD_FILE_VERSION=${{ needs.get-version.outputs.AssemblyFileVersion }} BUILD_ASSEMBLY_VERSION=${{ needs.get-version.outputs.AssemblyVersion }} diff --git a/.github/workflows/build-dockerreadme-task.yml b/.github/workflows/build-dockerreadme-task.yml new file mode 100644 index 00000000..d17c96f3 --- /dev/null +++ b/.github/workflows/build-dockerreadme-task.yml @@ -0,0 +1,54 @@ +name: Create Docker README.md task + +on: + workflow_call: + inputs: + # Logical branch this run is for. The Docker Hub README is rendered from + # `main` only (it embeds the `latest` image's tool versions); the job + # self-gates so the develop leg of the publisher's branch matrix is a + # no-op. Required (no `github.ref_name` fallback) so the gate can't + # silently misfire. + branch: + required: true + type: string + +jobs: + + docker-readme: + name: Create Docker README.md job + # Render only for main: there is a single Docker Hub README, and the m4 + # template includes the `latest` image's tool versions. + if: ${{ inputs.branch == 'main' }} + runs-on: ubuntu-latest + + steps: + + # Pin to the branch this run is for (always `main` here, via the job + # gate). Without it, checkout uses the triggering ref — a + # `workflow_dispatch` from `develop` would otherwise render the Hub + # README from develop's `Docker/README.m4` instead of main's. + - name: Checkout step + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ inputs.branch }} + + # Download by exact name (not a `versions-*` pattern): the main leg's + # artifact contains a file literally named `latest.ver`, which the m4 + # template references via `include({{latest.ver}})`. + - name: Download version artifacts step + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: versions-main + 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 diff --git a/.github/workflows/build-executable-task.yml b/.github/workflows/build-executable-task.yml index 2f8b6205..e9d36a81 100644 --- a/.github/workflows/build-executable-task.yml +++ b/.github/workflows/build-executable-task.yml @@ -2,6 +2,25 @@ name: Build executable task on: workflow_call: + inputs: + # Git ref to check out / version (empty = default checkout ref). + ref: + required: false + type: string + default: '' + # Logical branch driving build configuration (`main` => Release, else + # Debug). Required (no `github.ref_name` fallback, which would mislabel + # the develop leg of the publisher's matrix); the orchestrator passes it. + branch: + required: true + type: string + # Smoke mode: build a representative runtime subset (linux-x64 + + # win-x64) instead of the full 7-runtime matrix, and skip the zip / + # artifact aggregation. Used for fast PR feedback. + smoke: + required: false + type: boolean + default: false outputs: # Output of the uploaded artifact id artifact-id: @@ -13,6 +32,8 @@ jobs: name: Get version information job uses: ./.github/workflows/get-version-task.yml secrets: inherit + with: + ref: ${{ inputs.ref }} build-executable-matrix: name: Build executable project matrix job @@ -20,7 +41,7 @@ jobs: needs: [get-version] strategy: matrix: - runtime: [ win-x64, linux-x64, linux-musl-x64, linux-arm, linux-arm64, osx-x64, osx-arm64 ] + runtime: ${{ fromJSON(inputs.smoke && '["linux-x64","win-x64"]' || '["win-x64","linux-x64","linux-musl-x64","linux-arm","linux-arm64","osx-x64","osx-arm64"]') }} steps: @@ -31,13 +52,15 @@ jobs: - name: Checkout code step uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ inputs.ref }} - name: Build executable project step run: | dotnet publish ./PlexCleaner/PlexCleaner.csproj \ --runtime ${{ matrix.runtime }} \ -property:PublishDir=${{ runner.temp }}/publish/${{ matrix.runtime }}/ \ - --configuration ${{ github.ref_name == 'main' && 'Release' || 'Debug' }} \ + --configuration ${{ inputs.branch == 'main' && 'Release' || 'Debug' }} \ -property:PublishAot=false \ -property:Version=${{ needs.get-version.outputs.AssemblyVersion }} \ -property:FileVersion=${{ needs.get-version.outputs.AssemblyFileVersion }} \ @@ -48,11 +71,16 @@ jobs: - name: Upload matrix build artifacts step uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: publish-${{ matrix.runtime }} + name: publish-${{ inputs.branch }}-${{ matrix.runtime }} path: ${{ runner.temp }}/publish + # Smoke builds only need the per-runtime compile to succeed (fast PR + # feedback) — the zipped, downloadable artifact is a release concern, so + # skip the aggregation entirely on smoke. The `artifact-id` output is then + # empty, which is fine because the GitHub release job never runs on smoke. upload-build-artifacts: name: Upload matrix build artifacts job + if: ${{ !inputs.smoke }} outputs: artifact-id: ${{ steps.artifact-upload-step.outputs.artifact-id }} runs-on: ubuntu-latest @@ -63,7 +91,7 @@ jobs: - name: Download matrix build artifacts step uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - pattern: publish-* + pattern: publish-${{ inputs.branch }}-* merge-multiple: true path: ${{ runner.temp }}/publish @@ -74,5 +102,5 @@ jobs: id: artifact-upload-step uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: executable-build + name: executable-build-${{ inputs.branch }} path: ${{ runner.temp }}/PlexCleaner.7z diff --git a/.github/workflows/build-release-task.yml b/.github/workflows/build-release-task.yml index e49a381d..9d3a1182 100644 --- a/.github/workflows/build-release-task.yml +++ b/.github/workflows/build-release-task.yml @@ -13,6 +13,38 @@ on: required: false type: boolean default: false + # Git ref to check out / version (empty = default checkout ref). + ref: + required: false + type: string + default: '' + # Logical branch driving config / tags / prerelease for every target. + # Required (no `github.ref_name` fallback): the publisher builds both + # `main` and `develop` from one run whose `github.ref_name` is `main`, + # so a silent fallback would mislabel the develop leg. Every caller + # passes it explicitly; a missing value should fail loudly. + branch: + required: true + type: string + # Smoke mode: reduced, never-published build for fast PR feedback. + # Forwarded to every target; also hard-disables every push below so a + # smoke run can never publish regardless of the publish flags. + smoke: + required: false + type: boolean + default: false + # Per-target presence gates. Default true (build everything). A PR smoke + # run sets these from the paths-filter so only changed targets build; a + # derived project that drops a target deletes its job below and removes + # it from `github-release`'s `needs`. + enable_docker: + required: false + type: boolean + default: true + enable_executable: + required: false + type: boolean + default: true jobs: @@ -20,30 +52,56 @@ jobs: name: Get version information job uses: ./.github/workflows/get-version-task.yml secrets: inherit + with: + ref: ${{ inputs.ref }} build-executable: name: Build executable job + if: ${{ inputs.enable_executable }} + needs: [get-version] uses: ./.github/workflows/build-executable-task.yml secrets: inherit + with: + # Pin to the exact commit get-version resolved (immutable), not the + # possibly-moving branch ref: the publisher passes a branch name, and a + # commit landing mid-run could otherwise build artifacts from a different + # commit than the one the release tag (also GitCommitId) points at. + ref: ${{ needs.get-version.outputs.GitCommitId }} + branch: ${{ inputs.branch }} + smoke: ${{ inputs.smoke }} build-docker: name: Build Docker job + if: ${{ inputs.enable_docker }} + needs: [get-version] uses: ./.github/workflows/build-docker-task.yml secrets: inherit with: - # Conditional push to Docker Hub - push: ${{ inputs.dockerhub }} + # Pin to the resolved commit (see build-executable) so the pushed image + # contents match the SemVer2 tag computed from the same commit. + ref: ${{ needs.get-version.outputs.GitCommitId }} + branch: ${{ inputs.branch }} + smoke: ${{ inputs.smoke }} + # Conditional push to Docker Hub — never on a smoke build. + push: ${{ inputs.dockerhub && !inputs.smoke }} github-release: name: Publish GitHub release job - if: ${{ inputs.github }} + # `&& !inputs.smoke` enforces the "smoke never publishes" guarantee at the + # job level too (matching the push gate above), so a smoke caller that also + # set `github: true` still can't create a release. + if: ${{ inputs.github && !inputs.smoke }} runs-on: ubuntu-latest needs: [get-version, build-executable, build-docker] steps: + # Check out the exact built commit (NBGV `GitCommitId`), so the uploaded + # release files match the tag even if the branch advances mid-run. - name: Checkout code step uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ needs.get-version.outputs.GitCommitId }} - name: Download executable build artifacts step uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 @@ -51,12 +109,45 @@ jobs: artifact-ids: ${{ needs.build-executable.outputs.artifact-id }} path: ./Publish + # The weekly publisher re-runs even when a branch 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 is exactly the churn the two-phase model avoids — so skip the + # release step when a release for this tag already exists. A manual + # `workflow_dispatch` is allowed through so it can repair/refresh a + # partially-created release for the same tag. The Docker mutable tags + # (`latest`/`develop`) and base-image refresh still happen regardless. + - 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` 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). Pin it to NBGV's `GitCommitId` — + # the exact commit the version was computed from — not `github.sha` + # (wrong on the develop leg of the publisher's branch matrix) and not a + # branch name (a moving ref a mid-run commit could advance past). - name: Create GitHub release step + if: ${{ steps.release-exists.outputs.exists == 'false' || github.event_name == 'workflow_dispatch' }} uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 with: tag_name: ${{ needs.get-version.outputs.SemVer2 }} - target_commitish: ${{ github.sha }} - prerelease: ${{ github.ref_name != 'main' }} + target_commitish: ${{ needs.get-version.outputs.GitCommitId }} + prerelease: ${{ inputs.branch != 'main' }} generate_release_notes: true fail_on_unmatched_files: true files: | diff --git a/.github/workflows/build-toolversions-task.yml b/.github/workflows/build-toolversions-task.yml new file mode 100644 index 00000000..091039c7 --- /dev/null +++ b/.github/workflows/build-toolversions-task.yml @@ -0,0 +1,74 @@ +name: Get tool versions task + +on: + workflow_call: + inputs: + # Logical branch this run is for. Selects the pushed image tag + # (`main` => `latest`, else `develop`) and the per-branch versions + # filename so both legs of the publisher's branch matrix can run in one + # run without artifact collisions. Required (no `github.ref_name` + # fallback) so the develop leg of the matrix isn't mistagged. + branch: + required: true + type: string + +jobs: + + tool-versions: + name: Get tool versions job + runs-on: ubuntu-latest + + steps: + + # Authenticate the implicit `docker pull` below (manifest inspect + + # docker-run-action) so reading the just-pushed image isn't subject to + # anonymous Docker Hub rate limits. + - name: Login to Docker Hub step + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + + - name: Get image size step + env: + # `main` => the `latest` tag rendered into the Docker Hub README via + # the `latest.ver` m4 include; `develop` => the `develop` tag and a + # `develop.ver` file kept distinct for the matrix (informational — + # the Docker Hub README is rendered from main only). + TAG: ${{ inputs.branch == 'main' && 'latest' || 'develop' }} + FILE: ${{ inputs.branch == 'main' && 'latest.ver' || 'develop.ver' }} + run: | + set -euo pipefail + mkdir -p "${{ runner.temp }}/versions" + touch "${{ runner.temp }}/versions/$FILE" + echo "Image: docker.io/ptr727/plexcleaner:$TAG" >> "${{ runner.temp }}/versions/$FILE" + echo "Size: $(docker manifest inspect -v "docker.io/ptr727/plexcleaner:$TAG" | jq '.[] | select(.Descriptor.platform.architecture=="amd64") | [.OCIManifest.layers[].size] | add' | numfmt --to=iec)" >> "${{ runner.temp }}/versions/$FILE" + + - name: Write tool versions to file step + uses: addnab/docker-run-action@4f65fabd2431ebc8d299f8e5a018d79a769ae185 # v3 + with: + image: docker.io/ptr727/plexcleaner:${{ inputs.branch == 'main' && 'latest' || 'develop' }} + options: --volume ${{ runner.temp }}/versions:/versions + run: | + FILE="${{ inputs.branch == 'main' && 'latest.ver' || 'develop.ver' }}" + echo OS: $(. /etc/os-release; echo $PRETTY_NAME) >> /versions/$FILE + echo dotNET: $(dotnet --info) >> /versions/$FILE + echo PlexCleaner: $(/PlexCleaner/PlexCleaner --version) >> /versions/$FILE + echo HandBrakeCLI: $(HandBrakeCLI --version) >> /versions/$FILE + echo MediaInfo: $(mediainfo --version) >> /versions/$FILE + echo MkvMerge: $(mkvmerge --version) >> /versions/$FILE + echo FfMpeg: $(ffmpeg -version) >> /versions/$FILE + + - name: Print versions step + env: + FILE: ${{ inputs.branch == 'main' && 'latest.ver' || 'develop.ver' }} + run: cat "${{ runner.temp }}/versions/$FILE" + + - name: Upload version artifacts step + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + # Branch-suffixed so both matrix legs coexist; the inner filename + # stays `latest.ver` for main (the m4 `include({{latest.ver}})` + # token), consumed by build-dockerreadme-task.yml. + name: versions-${{ inputs.branch }} + path: ${{ runner.temp }}/versions/${{ inputs.branch == 'main' && 'latest.ver' || 'develop.ver' }} diff --git a/.github/workflows/get-version-task.yml b/.github/workflows/get-version-task.yml index f6785e18..44d44eca 100644 --- a/.github/workflows/get-version-task.yml +++ b/.github/workflows/get-version-task.yml @@ -2,6 +2,15 @@ name: Get version information task on: workflow_call: + inputs: + # Git ref to check out / version (empty = caller's default checkout ref, + # `github.ref`). The publisher passes an explicit branch so a scheduled + # run — whose `github.ref` is always the default branch — can still + # compute NBGV versions for `develop` too. + ref: + required: false + type: string + default: '' outputs: # Version information outputs SemVer2: @@ -12,6 +21,11 @@ on: value: ${{ jobs.get-version.outputs.AssemblyFileVersion }} AssemblyInformationalVersion: value: ${{ jobs.get-version.outputs.AssemblyInformationalVersion }} + # Full SHA of the commit NBGV computed the version from. Used to pin the + # GitHub release tag to the exact built commit (immutable), rather than a + # moving branch ref. + GitCommitId: + value: ${{ jobs.get-version.outputs.GitCommitId }} jobs: @@ -23,6 +37,7 @@ jobs: AssemblyVersion: ${{ steps.nbgv.outputs.AssemblyVersion }} AssemblyFileVersion: ${{ steps.nbgv.outputs.AssemblyFileVersion }} AssemblyInformationalVersion: ${{ steps.nbgv.outputs.AssemblyInformationalVersion }} + GitCommitId: ${{ steps.nbgv.outputs.GitCommitId }} steps: @@ -34,6 +49,7 @@ jobs: - name: Checkout code step uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: + ref: ${{ inputs.ref }} fetch-depth: 0 - name: Run Nerdbank.GitVersioning tool step diff --git a/.github/workflows/merge-bot-pull-request.yml b/.github/workflows/merge-bot-pull-request.yml index 2c58a94a..6f596abb 100644 --- a/.github/workflows/merge-bot-pull-request.yml +++ b/.github/workflows/merge-bot-pull-request.yml @@ -31,8 +31,10 @@ jobs: # 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. + # publish-release.yml on the merge commit. Under the default two-phase + # model that push is a no-op publish (it only republishes when the + # `PUBLISH_ON_MERGE` repo variable is `true`), but the App token keeps + # that opt-in path — and any future push-triggered workflow — working. id: app-token uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: diff --git a/.github/workflows/publish-periodic-docker-release.yml b/.github/workflows/publish-periodic-docker-release.yml deleted file mode 100644 index 5651069d..00000000 --- a/.github/workflows/publish-periodic-docker-release.yml +++ /dev/null @@ -1,103 +0,0 @@ -name: Publish weekly Docker image to Docker Hub action - -on: - push: - branches: [ main, develop ] - workflow_dispatch: - schedule: - # Run weekly on Mondays at 02:00 UTC - - cron: '0 2 * * MON' - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - - build-docker: - name: Build Docker image job - uses: ./.github/workflows/build-docker-task.yml - secrets: inherit - with: - # Push to registry - push: true - - tool-version-matrix: - name: Get tool versions - runs-on: ubuntu-latest - needs: build-docker - strategy: - matrix: - include: - - tag: ${{ github.ref_name == 'main' && 'latest' || 'develop' }} - file: latest.ver - - 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 - 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 - runs-on: ubuntu-latest - needs: tool-version-matrix - if: ${{ github.ref_name == 'main' }} - - 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 - - date-badge: - name: Create BYOB date badge job - needs: [build-docker] - uses: ./.github/workflows/build-datebadge-task.yml - secrets: inherit - permissions: - contents: write diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index e9390888..2e559e30 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -4,21 +4,120 @@ on: push: branches: [ main, develop ] workflow_dispatch: + schedule: + # Weekly full build/publish of both branches on Mondays at 02:00 UTC. + # This is the guaranteed publisher in the default two-phase model: routine + # merges only smoke-test, and this scheduled run republishes everything + # (also refreshing the Docker base image, e.g. `ubuntu:rolling`). + - cron: '0 2 * * MON' +# Real publishes (schedule, dispatch, or push when PUBLISH_ON_MERGE is set) +# share a single GLOBAL, ref-independent group so they serialize: they push +# both branches' shared Docker tags + caches and create GitHub releases for +# both regardless of the triggering ref, so a ref-scoped group would let a +# scheduled run (ref=main) and a manual dispatch (ref=develop) run concurrently +# and double-push. Non-publishing `push` runs (the two-phase default) get a +# unique per-run group so they don't queue behind — or delay — a real publish; +# they only execute the no-op `setup` job and skip everything else. concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + group: ${{ (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || vars.PUBLISH_ON_MERGE == 'true') && github.workflow || format('{0}-noop-{1}', github.workflow, github.run_id) }} + # Cancelling a publish mid-flight can leave a partially pushed multi-arch tag + # set or a half-created GitHub release. Queue instead of cancel so each + # publish runs to completion. + cancel-in-progress: false jobs: - create-release: + setup: + name: Resolve publish plan job + runs-on: ubuntu-latest + outputs: + branches: ${{ steps.plan.outputs.branches }} + publish: ${{ steps.plan.outputs.publish }} + steps: + - name: Compute publish plan step + id: plan + env: + # Repository variable (Settings -> Actions -> Variables). Unset reads + # as empty string, so the default is the two-phase model. + PUBLISH_ON_MERGE: ${{ vars.PUBLISH_ON_MERGE }} + run: | + set -euo pipefail + case "${{ github.event_name }}" in + push) + branches='["${{ github.ref_name }}"]' + if [[ "${PUBLISH_ON_MERGE:-}" == "true" ]]; then + publish=true + else + publish=false + fi + ;; + *) + # schedule / workflow_dispatch + branches='["main","develop"]' + publish=true + ;; + esac + echo "Event=${{ github.event_name }} branches=$branches publish=$publish" + echo "branches=$branches" >> "$GITHUB_OUTPUT" + echo "publish=$publish" >> "$GITHUB_OUTPUT" + + publish: name: Publish project release job + needs: [setup] + if: ${{ needs.setup.outputs.publish == 'true' }} + strategy: + fail-fast: false + matrix: + branch: ${{ fromJSON(needs.setup.outputs.branches) }} uses: ./.github/workflows/build-release-task.yml secrets: inherit permissions: contents: write with: - # Push to GitHub; Docker image is built (validates Dockerfile) but not pushed - # Docker Hub publishing is handled by publish-periodic-docker-release.yml + ref: ${{ matrix.branch }} + branch: ${{ matrix.branch }} + smoke: false + # Push to GitHub and Docker Hub. github: true - dockerhub: false + dockerhub: true + + tool-versions: + name: Get tool versions job + needs: [setup, publish] + if: ${{ needs.setup.outputs.publish == 'true' }} + strategy: + matrix: + branch: ${{ fromJSON(needs.setup.outputs.branches) }} + uses: ./.github/workflows/build-toolversions-task.yml + secrets: inherit + with: + branch: ${{ matrix.branch }} + + docker-readme: + name: Create Docker README.md job + needs: [setup, tool-versions] + if: ${{ needs.setup.outputs.publish == 'true' }} + strategy: + matrix: + branch: ${{ fromJSON(needs.setup.outputs.branches) }} + uses: ./.github/workflows/build-dockerreadme-task.yml + secrets: inherit + with: + # The task self-gates to `main`; the develop leg is a no-op. + branch: ${{ matrix.branch }} + + date-badge: + name: Create BYOB date badge job + needs: [setup, publish] + if: ${{ needs.setup.outputs.publish == 'true' }} + strategy: + matrix: + branch: ${{ fromJSON(needs.setup.outputs.branches) }} + uses: ./.github/workflows/build-datebadge-task.yml + secrets: inherit + permissions: + contents: write + with: + # The badge task self-gates to `main`; the develop leg is a no-op. + branch: ${{ matrix.branch }} diff --git a/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml index 7322ef08..7de39dde 100644 --- a/.github/workflows/test-pull-request.yml +++ b/.github/workflows/test-pull-request.yml @@ -11,26 +11,123 @@ concurrency: jobs: - test-release: - name: Test release job - uses: ./.github/workflows/test-release-task.yml + changes: + name: Detect changed targets job + runs-on: ubuntu-latest + # `dorny/paths-filter` lists the PR's changed files via the GitHub API + # (this job does not check out the tree), which needs `pull-requests: read`. + # The repo's default GITHUB_TOKEN is restricted, so grant it explicitly. + permissions: + contents: read + pull-requests: read + outputs: + docker: ${{ github.event_name == 'pull_request' && steps.filter.outputs.docker || 'true' }} + executable: ${{ github.event_name == 'pull_request' && steps.filter.outputs.executable || 'true' }} + steps: + # Build-workflow files are intentionally NOT in the filters — a path + # filter can't tell a logic change in a reusable workflow from an action + # version bump, so a workflow-only change isn't smoke-built; the reusable + # workflows are exercised by the next run that uses them (a later code + # PR's smoke build, or the scheduled/publish run). On `workflow_dispatch` + # there is no PR base to diff, so the outputs default to `true`. + - name: Filter changed paths step + id: filter + if: ${{ github.event_name == 'pull_request' }} + uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 + with: + filters: | + shared: &shared + - 'Directory.Build.props' + - 'Directory.Packages.props' + - 'version.json' + - '*.slnx' + docker: + - *shared + - 'Docker/**' + - 'PlexCleaner/**' + executable: + - *shared + - 'PlexCleaner/**' + + unit-test: + name: Run unit tests job + runs-on: ubuntu-latest + + steps: + + - name: Setup .NET SDK step + uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0 + with: + dotnet-version: 10.x + + - name: Checkout code step + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Check code style step + run: | + dotnet tool restore + dotnet husky install + dotnet husky run + + - name: Run unit tests step + run: dotnet test + + smoke-build: + name: Smoke build changed targets job + # Also gate on unit-test: the smoke build includes a Docker image build, so + # don't spend it when unit tests are already failing. A failed unit-test + # leaves this job skipped (needs unsatisfied) and the aggregator blocks on + # the unit-test failure directly. + needs: [changes, unit-test] + if: >- + needs.changes.outputs.docker == 'true' || + needs.changes.outputs.executable == 'true' + uses: ./.github/workflows/build-release-task.yml secrets: inherit + with: + smoke: true + # Do not publish anything from a PR. + github: false + dockerhub: false + # Check out the PR head by SHA (not head_ref): the head SHA is reachable + # in the base repo via refs/pull/N/head even for fork PRs, whereas the + # head_ref branch name does not exist in the base repo for forks and + # would fail checkout. `workflow_dispatch` has no pull_request payload, + # so fall back to the triggering ref. Validate against the base branch's + # configuration (Release for main, Debug for develop). + ref: ${{ github.event.pull_request.head.sha || github.ref_name }} + branch: ${{ github.base_ref || github.ref_name }} + enable_docker: ${{ needs.changes.outputs.docker == 'true' }} + enable_executable: ${{ needs.changes.outputs.executable == 'true' }} # TODO: Workaround for GitHub Actions not supporting status checks on conditional jobs # https://github.com/orgs/community/discussions/12395#discussioncomment-12970019 + # This job's name is bound to the branch ruleset as the required status check + # context — do NOT rename it. check-workflow-status: name: Check pull request workflow status runs-on: ubuntu-latest needs: - [ test-release ] + [ changes, unit-test, smoke-build ] if: always() steps: - - name: Check workflow results + - name: Check workflow results step run: | + set -euo pipefail exit_on_result() { if [[ "$2" == "failure" || "$2" == "cancelled" ]]; then echo "Job '$1' failed or was cancelled." exit 1 fi } - exit_on_result "test-release" "${{ needs.test-release.result }}" + # The paths-filter job MUST succeed: if it failed we don't know which + # targets changed, so a target-changing PR could merge with its smoke + # build silently skipped. Treat anything other than success as a block. + if [[ "${{ needs.changes.result }}" != "success" ]]; then + echo "Job 'changes' did not succeed (${{ needs.changes.result }}); refusing to pass." + exit 1 + fi + # unit-test always runs; smoke-build may be legitimately skipped + # (no target changed) — `skipped` passes, only failure/cancelled blocks. + exit_on_result "unit-test" "${{ needs.unit-test.result }}" + exit_on_result "smoke-build" "${{ needs.smoke-build.result }}" diff --git a/.github/workflows/test-release-task.yml b/.github/workflows/test-release-task.yml deleted file mode 100644 index c23a1ca4..00000000 --- a/.github/workflows/test-release-task.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Test release task - -on: - workflow_call: - workflow_dispatch: - -jobs: - - unit-test: - name: Run unit tests job - runs-on: ubuntu-latest - - steps: - - - name: Setup .NET SDK step - uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0 - with: - dotnet-version: 10.x - - - name: Checkout code step - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Check code style step - run: | - dotnet tool restore - dotnet husky install - dotnet husky run - - - name: Run unit tests step - run: dotnet test - - build-release: - name: Build release without publishing job - needs: [unit-test] - uses: ./.github/workflows/build-release-task.yml - secrets: inherit - with: - # Do not publish - github: false - dockerhub: false diff --git a/AGENTS.md b/AGENTS.md index e88b60d8..42b850f7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,12 +6,11 @@ For comprehensive coding standards and detailed conventions, refer to [`.github/ ## Git and Commit Rules -**These rules are absolute — no exceptions:** - -- **Never make git commits.** AI coding agents cannot produce cryptographically signed commits. All commits must be signed (SSH/GPG) and must be made by the developer. Stage changes with `git add` and leave the commit to the developer. +- **Default to staging, not committing.** Stage changes with `git add` and leave `git commit` to the developer unless the developer has explicitly authorized the agent to commit for the current ask ("commit this", "open a PR", etc.). Authorization is scope-bound — it covers the commits needed for that specific task, not a blanket commit license. +- **All commits must be cryptographically signed (SSH or GPG).** Branch protection enforces this on both branches; unsigned commits are rejected on push. Signing depends on environment configuration (`git config commit.gpgsign true`, a configured `user.signingkey`, and a loaded signing agent). If signing is not configured in the environment, **do not commit** — surface the missing config to the developer and stop at `git add`. Verify before any agent-authored commit (`git config --get commit.gpgsign && ssh-add -L`, or the GPG equivalent). - **Never force push.** Do not run `git push --force` or `git push --force-with-lease` under any circumstances. Force pushing rewrites shared history and can cause data loss. - **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. +- **The `develop → main` release merge is maintainer-only.** An agent may drive `feature → develop` PRs end-to-end (commit, push, review loop, squash-merge) when authorized, but never self-merges a release to `main` — prepare it and hand it off. ## Branches and merging @@ -28,18 +27,11 @@ Repo settings reflect this: `allow_merge_commit=true`, `allow_squash_merge=true` After resolving Copilot's threads or pushing fixes: -1. Wait for Copilot to post a fresh review on the new head commit. The `copilot_code_review` rule on both `develop` and `main` rulesets has `review_on_push: true` configured (verify with `gh api repos///rulesets/ --jq '.rules[] | select(.type=="copilot_code_review")'`), so a re-review normally lands within a few minutes. -2. Verify Copilot's most recent review targets the current head — compare its `commit_id` to `headRefOid`, not timestamps (multiple reviews and authors clutter the list, and timestamp drift is unreliable): - - ```sh - head=$(gh pr view --json headRefOid --jq .headRefOid) - last=$(gh api --paginate repos///pulls//reviews --jq '.[] | select(.user.login == "copilot-pull-request-reviewer[bot]") | .commit_id' | tail -1) - [ "$head" = "$last" ] && echo "fresh" || echo "stale" - ``` - +1. **Re-request a Copilot review programmatically** on the current head via the GraphQL `requestReviews` mutation — auto-review-on-push fires inconsistently, so don't wait on it. The full mechanics (bot node-id discovery, the mutation, head-SHA coverage check, thread reply/resolve, bounded retry) are in the [GitHub Copilot Review Runbook](./.github/copilot-instructions.md#github-copilot-review-runbook). The agent can drive this loop end-to-end without a maintainer clicking "re-request review" in the UI. +2. Verify Copilot's most recent review targets the current head — compare its `commit.oid`/`commit_id` to `headRefOid`, not timestamps (multiple reviews and authors clutter the list, and timestamp drift is unreliable). 3. If the fresh review is `COMMENTED` with zero unresolved inline threads (or `APPROVED`), the PR is good to merge. 4. If the fresh review introduces new concerns (inline threads or body-level objections), address them and loop. -5. **If Copilot does not auto re-review within a reasonable window after the latest push (~5 min), do not merge — ask the maintainer.** Silence is not approval. Copilot can be re-prompted manually from the GitHub PR UI ("Re-request review" on the Copilot reviewer entry). +5. **If Copilot does not re-review within a reasonable window (~5 min) after re-requesting**, retry per the runbook's bounded-retry workflow (up to three total); if still missing, mark the review blocked and escalate to the maintainer. Silence is not approval. This applies to every human-authored PR (feature → develop, develop → main). The merge-bot workflow's auto-merge of dependabot bumps is the only exception and is governed separately by the `update-type` filter. @@ -53,23 +45,22 @@ Under any squash-only setup this would be a recurring pain point: each develop ## 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. +PlexCleaner is a "pull" project: consumers (`docker pull ptr727/plexcleaner:latest`, `docker pull ptr727/plexcleaner:develop`, GitHub Releases) track both branches. It uses a **two-phase model** that decouples merging from publishing: -[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]`: +- **PRs smoke-test only.** [test-pull-request.yml](.github/workflows/test-pull-request.yml) always runs unit tests, then a [`dorny/paths-filter`](.github/workflows/test-pull-request.yml) `changes` job gates a **reduced** build of only the changed targets (Docker `linux/amd64` only, executable on a `linux-x64` + `win-x64` subset), never pushing. Build-workflow files are intentionally not in the path filters — a filter can't tell a logic change from an action-version bump — so a workflow-only change isn't smoke-built; the reusable workflows are exercised by the next run that uses them. There is no CI workflow-lint job; lint workflow edits with `actionlint` locally before pushing. The `changes` job is in the `Check pull request workflow status` aggregator's `needs` and **must succeed** (not just "not fail") — a paths-filter error must never let a target-changing PR merge with its smoke build silently skipped. +- **Merges don't publish by default.** [publish-release.yml](.github/workflows/publish-release.yml) is the **sole publisher**: its **weekly schedule** (Mondays 02:00 UTC) and **manual `workflow_dispatch`** always do the full multi-arch build/publish of **both** `main` and `develop` (a branch matrix in one run). Its `push` trigger publishes only when the **`PUBLISH_ON_MERGE` repository variable** is `true` (opt-in legacy continuous-release). Unset/`false` = two-phase: routine merges to `develop`/`main` only smoke-build, and `:latest`/`:develop` Docker tags + GitHub releases refresh on the weekly run instead of on every merge. -- **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`. +A `setup` job computes the plan: `push` ⇒ the pushed branch with `publish = (vars.PUBLISH_ON_MERGE == 'true')`; `schedule`/`dispatch` ⇒ both branches with `publish = true`. The `publish` job is a `matrix.branch` fan-out over [build-release-task.yml](.github/workflows/build-release-task.yml); `tool-versions`, `docker-readme` (main only), and `date-badge` (main only) run after it. -Branch-aware logic lives in three places: +Branch-aware config keys off the **`branch` input** threaded through every reusable task — **never `github.ref_name`** (the publisher builds `develop` from a run whose `github.ref_name` is `main`, so a fallback would mislabel it). `main` ⇒ Release / `latest` / stable release; anything else ⇒ Debug / `develop` / prerelease. The GitHub release's `target_commitish` is pinned to NBGV's `GitCommitId` (the exact built commit) — not `github.sha` (wrong on the develop leg) and not a branch name (a moving ref); `get-version-task.yml` surfaces `GitCommitId` as an output. The release step is skipped when a release for the computed `SemVer2` tag already exists (no-op weekly republish), except on `workflow_dispatch` (which can refresh a partial release). -- [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' }}`. +**Per-target subsetting (derived projects).** `build-release-task.yml` has per-target `enable_*` gates and self-contained leaf tasks, so a project that drops a target deletes: its `build--task.yml`, the matching job + `github-release` `needs` entry in `build-release-task.yml`, and its path-filter entry in `test-pull-request.yml`. Versioning, badge, tool-versions, Docker README, merge-bot, and Dependabot are target-agnostic. PlexCleaner's targets are the **Docker image** and the **console executable** only (no NuGet/PyPI). -Bot-merged PRs (Dependabot) trigger the publish workflows automatically because the merge-bot uses an App token — see the merge-bot section below. +Bot-merged PRs (Dependabot) still trigger `publish-release.yml` because the merge-bot uses an App token (see the merge-bot section) — under the default two-phase model that push run is a no-op publish unless `PUBLISH_ON_MERGE` is set. ## 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. +[.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: both branches ship from the weekly publisher (and on every merge when `PUBLISH_ON_MERGE` is set), so 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. @@ -86,7 +77,7 @@ When adding a new `uses:` line, resolve the latest release's commit SHA (`gh api [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. +- **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 would silently skip `publish-release.yml` on the merge commit. Under the default two-phase model that push is a no-op publish (it only republishes when `PUBLISH_ON_MERGE` is `true`), but the App token keeps that opt-in path — and any future push-triggered workflow — working. 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.) diff --git a/README.md b/README.md index 4d13a6d5..a50906fc 100644 --- a/README.md +++ b/README.md @@ -206,7 +206,7 @@ Choose an installation method based on your platform and requirements: - See the [Docker README][docker-link] for current distribution and media tool versions. - `ptr727/plexcleaner:latest` is based on [Ubuntu][ubuntu-hub-link] (`ubuntu:rolling`) built from the `main` branch. - `ptr727/plexcleaner:develop` is based on [Ubuntu][ubuntu-hub-link] (`ubuntu:rolling`) built from the `develop` branch. -- Images are updated weekly with the latest upstream updates. +- Docker images and GitHub Releases for both branches are published on a weekly schedule (and on demand), each refreshed with the latest upstream and base-image updates. Pull requests are smoke-tested only and do not publish. - The container has all the prerequisite 3rd party tools pre-installed. **Path Mapping Convention**: All examples use `/data/media` as the host path mapped to `/media` inside the container. Replace `/data/media` with your actual host media location. @@ -990,7 +990,7 @@ Licensed under the [MIT License][license-link]\ [docker-develop-version-shield]: https://img.shields.io/docker/v/ptr727/plexcleaner/develop?label=Docker%20Develop&logo=docker&color=orange [docker-latest-version-shield]: https://img.shields.io/docker/v/ptr727/plexcleaner/latest?label=Docker%20Latest&logo=docker [docker-link]: https://hub.docker.com/r/ptr727/plexcleaner -[docker-status-shield]: https://img.shields.io/github/actions/workflow/status/ptr727/PlexCleaner/publish-periodic-docker-release.yml?logo=github&label=Docker%20Build +[docker-status-shield]: https://img.shields.io/github/actions/workflow/status/ptr727/PlexCleaner/publish-release.yml?event=schedule&logo=github&label=Docker%20Build [github-link]: https://github.com/ptr727/PlexCleaner [plexcleaner-hub-link]: https://hub.docker.com/r/ptr727/plexcleaner [issues-link]: https://github.com/ptr727/PlexCleaner/issues @@ -1000,7 +1000,7 @@ Licensed under the [MIT License][license-link]\ [license-link]: ./LICENSE [license-shield]: https://img.shields.io/github/license/ptr727/PlexCleaner?label=License [pre-release-version-shield]: https://img.shields.io/github/v/release/ptr727/PlexCleaner?include_prereleases&label=GitHub%20Pre-Release&logo=github -[release-status-shield]: https://img.shields.io/github/actions/workflow/status/ptr727/PlexCleaner/publish-release.yml?logo=github&label=Releases%20Build +[release-status-shield]: https://img.shields.io/github/actions/workflow/status/ptr727/PlexCleaner/publish-release.yml?event=schedule&logo=github&label=Releases%20Build [release-version-shield]: https://img.shields.io/github/v/release/ptr727/PlexCleaner?logo=github&label=GitHub%20Release [releases-link]: https://github.com/ptr727/PlexCleaner/releases [ubuntu-hub-link]: https://hub.docker.com/_/ubuntu From b1ad20c39856dca2a4d9cd8218ab15996a229e91 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 05:37:53 +0000 Subject: [PATCH 15/15] Bump actions/checkout in the actions-deps group across 1 directory (#720) Bumps the actions-deps group with 1 update in the / directory: [actions/checkout](https://github.com/actions/checkout). Updates `actions/checkout` from 6.0.2 to 6.0.3 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/de0fac2e4500dabe0009e67214ff5f5447ce83dd...df4cb1c069e1874edd31b4311f1884172cec0e10) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 6.0.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-deps ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-docker-task.yml | 2 +- .github/workflows/build-dockerreadme-task.yml | 2 +- .github/workflows/build-executable-task.yml | 2 +- .github/workflows/build-release-task.yml | 2 +- .github/workflows/get-version-task.yml | 2 +- .github/workflows/test-pull-request.yml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-docker-task.yml b/.github/workflows/build-docker-task.yml index f40677b3..64640089 100644 --- a/.github/workflows/build-docker-task.yml +++ b/.github/workflows/build-docker-task.yml @@ -46,7 +46,7 @@ jobs: steps: - name: Checkout step - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ inputs.ref }} diff --git a/.github/workflows/build-dockerreadme-task.yml b/.github/workflows/build-dockerreadme-task.yml index d17c96f3..a0adf227 100644 --- a/.github/workflows/build-dockerreadme-task.yml +++ b/.github/workflows/build-dockerreadme-task.yml @@ -28,7 +28,7 @@ jobs: # `workflow_dispatch` from `develop` would otherwise render the Hub # README from develop's `Docker/README.m4` instead of main's. - name: Checkout step - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ inputs.branch }} diff --git a/.github/workflows/build-executable-task.yml b/.github/workflows/build-executable-task.yml index e9d36a81..50f5d316 100644 --- a/.github/workflows/build-executable-task.yml +++ b/.github/workflows/build-executable-task.yml @@ -51,7 +51,7 @@ jobs: dotnet-version: 10.x - name: Checkout code step - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ inputs.ref }} diff --git a/.github/workflows/build-release-task.yml b/.github/workflows/build-release-task.yml index 9d3a1182..e79ed82a 100644 --- a/.github/workflows/build-release-task.yml +++ b/.github/workflows/build-release-task.yml @@ -99,7 +99,7 @@ jobs: # Check out the exact built commit (NBGV `GitCommitId`), so the uploaded # release files match the tag even if the branch advances mid-run. - name: Checkout code step - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ needs.get-version.outputs.GitCommitId }} diff --git a/.github/workflows/get-version-task.yml b/.github/workflows/get-version-task.yml index 44d44eca..500f11a5 100644 --- a/.github/workflows/get-version-task.yml +++ b/.github/workflows/get-version-task.yml @@ -47,7 +47,7 @@ jobs: dotnet-version: 10.x - name: Checkout code step - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ inputs.ref }} fetch-depth: 0 diff --git a/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml index 7de39dde..9621fcdb 100644 --- a/.github/workflows/test-pull-request.yml +++ b/.github/workflows/test-pull-request.yml @@ -61,7 +61,7 @@ jobs: dotnet-version: 10.x - name: Checkout code step - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Check code style step run: |