Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
6e6dbbd
Make agent docs self-contained without external-repo pointers (#866)
ptr727 Jul 19, 2026
83aafd9
Publish runtime metrics and byte-weighted progress via System.Diagnos…
ptr727 Jul 19, 2026
33c05ca
Add per-file partial-credit progress to the runtime metrics (#868)
ptr727 Jul 19, 2026
acd795c
Switch runtime metrics to operation-weighted progress (#869)
ptr727 Jul 19, 2026
63c22bc
Bump DavidAnson/markdownlint-cli2-action in the actions-deps group (#…
dependabot[bot] Jul 20, 2026
c6ef6cd
Bump the nuget-deps group with 2 updates (#872)
dependabot[bot] Jul 20, 2026
8deea9b
Bump the nuget-deps group with 2 updates (#876)
dependabot[bot] Jul 21, 2026
2a2c3c4
Bump actions/checkout from 7.0.0 to 7.0.1 in the actions-deps group (…
dependabot[bot] Jul 21, 2026
3a68155
Bump astral-sh/setup-uv from 8.3.2 to 9.0.0 in the actions-deps group…
dependabot[bot] Jul 22, 2026
086cb8b
Realign agent governance, configs, and workflows with the canonical
ptr727 Jul 23, 2026
ba3b01a
Re-vendor the release task with the validate-release entry gate
ptr727 Jul 24, 2026
f91af07
Refresh AGENTS.md against the current canonical structure
ptr727 Jul 24, 2026
4d194a7
Bump docker/login-action from 4.4.0 to 4.5.0 in the actions-deps grou…
dependabot[bot] Jul 24, 2026
d5fb147
Bump the nuget-deps group with 1 update (#887)
dependabot[bot] Jul 24, 2026
55ff24f
Byte-match AGENTS.md to the canonical section-fidelity model
ptr727 Jul 24, 2026
2a57a76
Re-vendor the genericized AGENTS.md verbatim sections
ptr727 Jul 24, 2026
39687f9
Converge governance docs: Durable Knowledge section and review guidance
ptr727 Jul 25, 2026
ca71183
Fix metrics doc drift and counters.sh shell prologue
ptr727 Jul 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ Summarized for VS Code's generators; the full rules, rationale, and examples are
- US English, title case with lowercase short bind words; no vague titles, no `Co-Authored-By:` unless asked, no release-bump magnitude (NBGV handles versioning). Dependabot's `Bump X from Y to Z` titles are fine.
- develop PRs squash-merge (`gh pr merge --squash`), main PRs merge-commit (`--merge`); a mismatched flag is rejected by branch protection.

## Reviewing Carried Fleet Content

Several of this repository's governance files are carried from a shared template and kept in sync across a fleet of sibling repositories - among them `AGENTS.md`, `CODESTYLE.md`, `WORKFLOW.md`, this file, and the `repo-config/` rulesets. Most of `AGENTS.md` is universal fleet law - every section that states a rule, as opposed to the two that describe this repository's own directory tree and devcontainer, is byte-locked and verified by an automated byte-for-byte match against the template canonical, not by line-by-line review.

Two constraints follow when reviewing that content.

- **A reference inside byte-locked text to a path or section this repository does not carry is intentional, not a broken link.** Universal rule text names shared infrastructure - a fleet registry, a reusable config snippet, the other workflow model's ruleset payload - that a given repository legitimately may not contain. Editing the text to "fix" such a reference would break the fleet audit that governs it, so the reference is correct as written. Do not report it as a dead link, a missing file, or a broken cross-reference.
- **A genuine substantive defect is still worth raising.** Byte-locked is not unreviewable. A self-contradiction, a factual error, or a real typo in the canonical prose is a valid finding - note that the fix lands at the template and re-vendors to every repository, rather than proposing a local edit the audit would reject.

## GitHub Copilot Review Runbook

> This runbook implements the [AGENTS.md "PR Review Etiquette"](../AGENTS.md#pr-review-etiquette) review-loop contract for GitHub Copilot. Without it in-repo, an agent has no pointer to the reliable Copilot mechanics and falls back to known-broken paths (the no-op `POST /requested_reviewers`, the wrong bot-login filter). In the API snippets below, `<N>` is the PR number.
Expand Down Expand Up @@ -153,4 +162,4 @@ After the final push, sweep-resolve stale older threads for removed code paths.

Read [AGENTS.md](../AGENTS.md) for this repo's conventions and [../ARCHITECTURE.md](../ARCHITECTURE.md) for PlexCleaner's architecture, processing pipeline, and design patterns. For code-style rules, [`CODESTYLE.md`](../CODESTYLE.md) is authoritative. Don't restate any of these files' rules in commit bodies or PR descriptions - keep those focused on the change itself.

**In a derived repo:** if you find a discrepancy that should be fixed in the template itself (this file or AGENTS.md is out of date, a rule is missing, something bit this repo and would bite the next), open an issue upstream in [`ptr727/ProjectTemplate`](https://github.com/ptr727/ProjectTemplate) rather than only fixing it locally - see the template's [AGENTS.md "Staying in Sync and Reporting Drift Upstream"](https://github.com/ptr727/ProjectTemplate/blob/main/AGENTS.md#staying-in-sync-and-reporting-drift-upstream).
**Shared conventions:** if you find a discrepancy that looks like it should be fixed in the shared conventions rather than only locally (this file or AGENTS.md is out of date, a rule is missing, something bit this repo and would bite the next), raise it with the maintainer rather than only patching it here.
4 changes: 2 additions & 2 deletions .github/workflows/build-docker-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ jobs:
steps:

- name: Checkout step
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.ref }}

Expand All @@ -72,7 +72,7 @@ jobs:
# Always login (even on smoke) for higher pull/cache-read rate limits; the credentials are in both the
# Actions and Dependabot secret stores so a Dependabot push CI run can log in too. Forks cannot push here.
- name: Login to Docker Hub step
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
uses: docker/login-action@06fb636fac595d6fb4b28a5dfcb21a6f5091859c # v4.5.0
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/build-executable-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,13 @@ jobs:
dotnet-version: 10.x

- name: Checkout code step
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.ref }}

- name: Build executable project step
run: |
set -Eeuo pipefail
dotnet publish ./PlexCleaner/PlexCleaner.csproj \
--runtime ${{ matrix.runtime }} \
-property:PublishDir=${{ runner.temp }}/publish/${{ matrix.runtime }}/ \
Expand Down
125 changes: 96 additions & 29 deletions .github/workflows/build-release-task.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
name: Build project release task

# Orchestrate one branch's release: version once (get-version), build the executable 7z and the Docker image, then
# create the GitHub release. github/dockerhub gate the two publish targets; smoke builds everything, publishes nothing.
# Orchestrate one branch's release: version once (get-version), gate on branch<->version consistency
# (validate-release), build the executable 7z and the Docker image, then create the GitHub release.
# github/dockerhub gate the two publish targets; smoke builds everything, publishes nothing.
on:
workflow_call:
inputs:
Expand All @@ -27,6 +28,12 @@ on:
required: false
type: boolean
default: false
# Set false for a repo that produces no release-asset-* files (e.g. Docker-only): the release is then just the
# tag + source zip + README + LICENSE; the artifact download is skipped and the unmatched-files guard relaxes.
expect_release_assets:
required: false
type: boolean
default: true

jobs:

Expand All @@ -48,11 +55,43 @@ jobs:
with:
ref: ${{ inputs.ref }}

# Entry gate: validate branch<->version consistency once, before the build jobs, so an NBGV mis-classification fails
# fast instead of after building and publishing. main must be a public release (no prerelease '-'); every other branch
# must carry a prerelease '-' (guards a develop leg being classified public and published as stable). Strip
# '+buildmetadata' first; a '-' there is legitimate, only a '-' in the core/prerelease segment marks a prerelease.
validate-release:
name: Validate release version job
needs: [get-version]
runs-on: ubuntu-latest
steps:
- name: Validate branch and version consistency step
env:
SEMVER2: ${{ needs.get-version.outputs.SemVer2 }}
BRANCH: ${{ inputs.branch }}
SMOKE: ${{ inputs.smoke }}
run: |
set -Eeuo pipefail
# Smoke builds never publish and always version as prerelease (detached PR HEAD), which would trip the main arm.
if [[ "$SMOKE" == "true" ]]; then
echo "Smoke build; skipping release version validation."
exit 0
fi
CORE_AND_PRE="${SEMVER2%%+*}"
if [[ "$BRANCH" == "main" ]]; then
if [[ "$CORE_AND_PRE" == *-* ]]; then
echo "::error::Public (main) release version '$SEMVER2' carries a prerelease suffix; refusing to publish."
exit 1
fi
elif [[ "$CORE_AND_PRE" != *-* ]]; then
echo "::error::Prerelease ($BRANCH) version '$SEMVER2' has no prerelease suffix (NBGV classified it public); refusing to publish."
exit 1
fi

# Build only when validation passed (success) or was skipped (smoke); never when it failed.
build-executable:
name: Build executable job
needs: [get-version, validate]
if: ${{ !cancelled() && needs.get-version.result == 'success' && (needs.validate.result == 'success' || needs.validate.result == 'skipped') }}
needs: [get-version, validate, validate-release]
if: ${{ !cancelled() && needs.get-version.result == 'success' && needs.validate-release.result == 'success' && (needs.validate.result == 'success' || needs.validate.result == 'skipped') }}
uses: ./.github/workflows/build-executable-task.yml
secrets: inherit
with:
Expand All @@ -65,10 +104,15 @@ jobs:
assembly_file_version: ${{ needs.get-version.outputs.AssemblyFileVersion }}
assembly_informational_version: ${{ needs.get-version.outputs.AssemblyInformationalVersion }}

# Docker is the terminal registry push, so it must never push on a partial run. It needs every other build and
# guards with `!failure() && !cancelled()`: a *failed* build skips docker (no build, no push), while a *skipped*
# build - validate on a smoke run - does not, so docker still builds on smoke. Plain `needs` cannot express this
# (a skipped need skips the dependent). The github-release job reaches the same intent more simply because it
# only runs on a publish (`!inputs.smoke`), where nothing is skipped.
build-docker:
name: Build Docker job
needs: [get-version, validate]
if: ${{ !cancelled() && needs.get-version.result == 'success' && (needs.validate.result == 'success' || needs.validate.result == 'skipped') }}
needs: [get-version, validate, validate-release, build-executable]
if: ${{ !failure() && !cancelled() }}
Comment thread
ptr727 marked this conversation as resolved.
uses: ./.github/workflows/build-docker-task.yml
secrets: inherit
with:
Expand All @@ -85,49 +129,42 @@ jobs:

github-release:
name: Publish GitHub release job
# !smoke enforces "smoke never publishes" even if a smoke caller set github: true.
# `!inputs.smoke` enforces "smoke never publishes" at the job level, 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]
needs: [get-version, validate-release, build-executable, build-docker]

steps:

# Check out the exact built commit so the uploaded release files match the tag even if the branch advances mid-run.
- name: Checkout code step
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.get-version.outputs.GitCommitId }}

# Backstop (main only): refuse to publish if the public version carries a prerelease '-' (NBGV mis-versioning).
# Strip '+buildmetadata' first; only a '-' in the core segment marks a prerelease.
- name: Verify public release version step
if: ${{ inputs.branch == 'main' }}
env:
SEMVER2: ${{ needs.get-version.outputs.SemVer2 }}
run: |
set -euo pipefail
CORE_AND_PRE="${SEMVER2%%+*}" # drop +buildmetadata; a '-' here is the genuine prerelease separator
if [[ "$CORE_AND_PRE" == *-* ]]; then
echo "::error::Public (main) release version '$SEMVER2' carries a prerelease suffix; refusing to publish."
exit 1
fi

# Collect the executable build's release-asset-<branch>-* artifacts (the PlexCleaner.7z) by pattern.
# Collect assets by the `release-asset-<branch>-*` pattern so this step is target-agnostic: subset releases by
# deleting the target, not `enable_*: false` (a skipped `needs` job would skip this release job too). The release
# step guards `fail_on_unmatched_files: true`, so at least one `release-asset-*` must match; a repo that drops
# every file-producing target (e.g. a Docker-only repo, whose release carries only source zip + README + LICENSE)
# relaxes that guard.
- name: Download release asset artifacts step
if: ${{ inputs.expect_release_assets }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: release-asset-${{ inputs.branch }}-*
merge-multiple: true
path: ./Publish

# Weekly re-runs may hit an already-released version; skip release-create when the tag exists (no-op republish).
# The weekly publisher re-runs even with no new commits, so the version may already be released. Skip the release
# step when a release for this tag already exists to avoid a no-op republish.
- name: Check for existing release step
id: release-exists
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.get-version.outputs.SemVer2 }}
run: |
set -euo pipefail
set -Eeuo 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
Expand All @@ -139,8 +176,13 @@ jobs:
echo "exists=false" >> "$GITHUB_OUTPUT"
fi

# target_commitish pins the tag to the exact built commit (GitCommitId), not the moving branch ref.
# fail_on_unmatched_files catches a missing or misnamed PlexCleaner.7z.
# `target_commitish` must be set explicitly: otherwise GitHub's REST API tags the release on the default branch.
# Pin it to `GitCommitId` so the tag is on the exact built commit, consistent with the SemVer2 tag and artifacts.
# Skip when the release already exists, but always let a manual `workflow_dispatch` through to refresh it.
# Every release (any branch, any target) is a tag on the built commit plus the auto-attached source zip, README,
# and LICENSE; targets amend it by uploading `release-asset-*` files (binaries/packages) or pushing elsewhere
# (image/registry). `fail_on_unmatched_files: true` fails loudly if a promised `release-asset-*` is missing or
# misnamed; a no-file-target repo relaxes it (see download step).
- name: Create GitHub release step
if: ${{ steps.release-exists.outputs.exists == 'false' || github.event_name == 'workflow_dispatch' }}
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
Expand All @@ -149,8 +191,33 @@ jobs:
tag_name: ${{ needs.get-version.outputs.SemVer2 }}
target_commitish: ${{ needs.get-version.outputs.GitCommitId }}
prerelease: ${{ inputs.branch != 'main' }}
fail_on_unmatched_files: true
fail_on_unmatched_files: ${{ inputs.expect_release_assets }}
files: |
LICENSE
README.md
./Publish/*

# Surgical cleanup at the point of consumption: the release-asset-<branch>-* transfer artifacts now have durable
# copies on the release, so delete them by exact pattern to free the storage quota - scoped to this branch's
# assets, leaving diagnostics and any other artifacts. Gated to the same condition as the create step so it only
# deletes when a release was actually created/refreshed this run; on a skipped create (existing tag, no new
# commits) the fresh artifacts stay for the run, reaped by the retention-days: 1 backstop. Needs the caller to
# grant `actions: write` (publish-release's publish job does).
- name: Delete consumed release asset artifacts step
if: ${{ inputs.expect_release_assets && (steps.release-exists.outputs.exists == 'false' || github.event_name == 'workflow_dispatch') }}
# Best-effort: the release is already published, so a listing/delete hiccup must never red the job; the
# retention-days: 1 backstop reaps anything missed. Deletes every matching id (a rerun can upload duplicates).
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: |
set -Eeuo pipefail
if ! ids=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/${{ github.run_id }}/artifacts" --paginate \
--jq ".artifacts[] | select(.name | startswith(\"release-asset-${{ inputs.branch }}-\")) | .id"); then
echo "::warning::Could not list run artifacts; retention-days backstop will reap them."
ids=""
fi
for id in $ids; do
gh api --method DELETE "repos/$GITHUB_REPOSITORY/actions/artifacts/$id" \
|| echo "::warning::Failed to delete artifact $id; retention-days backstop will reap it."
done
Comment thread
ptr727 marked this conversation as resolved.
8 changes: 4 additions & 4 deletions .github/workflows/get-version-task.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
name: Get version information task

# Run NBGV once and expose the version outputs. The publisher passes its trigger branch as ref so the run
# versions the branch it publishes; github.ref matches that branch (one branch per run), so NBGV classifies it
# correctly without overriding github.ref.
# Run NBGV once and expose the version outputs. The publisher passes the exact commit it publishes as ref, and
# github.ref still names the branch that commit belongs to (one branch per run), so NBGV classifies it correctly
# without overriding github.ref.
on:
workflow_call:
inputs:
Expand Down Expand Up @@ -44,7 +44,7 @@ jobs:
dotnet-version: 10.x

- name: Checkout code step
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.ref }}
fetch-depth: 0
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/merge-bot-pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
# Auto-merge every tier, semver-major included: the required checks are the gate, not the bump magnitude.
- name: Merge pull request step
run: |
set -euo pipefail
set -Eeuo pipefail
case "${{ github.event.pull_request.base.ref }}" in
develop) method=--squash ;;
main) method=--merge ;;
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,11 @@ jobs:
secrets: inherit
permissions:
contents: write
# The release job deletes the release-asset-* transfer artifacts once they are attached to the release.
actions: write
with:
ref: ${{ github.ref_name }}
# Pin the exact dispatch/schedule-time commit: a push landing mid-run must not be released unvalidated.
ref: ${{ github.sha }}
branch: ${{ github.ref_name }}
smoke: false
github: true
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test-pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ jobs:
steps:
- name: Check workflow results step
run: |
set -euo pipefail
set -Eeuo pipefail
for result in "validate:${{ needs.validate.result }}" "smoke-build:${{ needs.smoke-build.result }}"; do
name="${result%%:*}"
value="${result#*:}"
Expand Down
Loading