Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
39 changes: 25 additions & 14 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -13,49 +13,60 @@
# Root config
root = true

# Defaults
# Defaults: CRLF is the default, and only the LF exceptions below are declared.
# Most LF pins are shared with `.gitattributes`, which git enforces: `*.sh`, the husky pre-commit shebang, and Dockerfiles.
# The workflow-YAML pin (`.github/workflows/*`) is `.editorconfig`-only.
# For that one, git stays passive (`* -text`) and CI (editorconfig-checker) enforces LF.
# Keep the `[*]` `end_of_line = crlf` default, which the Windows-GUI and WSL-engine workflow requires because Windows tooling misbehaves on LF.
# Every uncovered file type relies on that default too.
[*]
charset = utf-8
end_of_line = crlf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

# Markdown files
[*.md]
end_of_line = crlf
trim_trailing_whitespace = false

# Xml files
[*.{xml,csproj,props,targets}]
end_of_line = crlf
indent_size = 2

# Yaml files
[*.{yml,yaml}]
end_of_line = crlf
indent_size = 2

# Workflow YAML is LF: Dependabot and Actions rewrite it with LF, so declaring LF keeps it consistent instead of
# mixed. git still leaves endings alone (`* -text`). This and CI (editorconfig-checker) enforce it. Other YAML is CRLF.
# Workflow YAML is LF, because Dependabot and Actions rewrite it with LF, so declaring LF keeps it consistent instead of mixed.
# Endings are still left alone by git (`* -text`), and this file plus CI (editorconfig-checker) enforce it.
# Other YAML stays CRLF.
[.github/workflows/*.{yml,yaml}]
end_of_line = lf

# Json files
[*.{json,jsonc}]
end_of_line = crlf

# Linux scripts
[*.sh]
end_of_line = lf

# Windows scripts
[*.{cmd,bat,ps1}]
end_of_line = crlf
# Husky.Net ships an extensionless pre-commit hook with a `/bin/sh` shebang, which matches no extension rule.
# Pin it by path, so a CRLF cannot break the hook's execution.
# `.gitattributes` carries the matching pin.
[.husky/pre-commit]
end_of_line = lf

# Dockerfiles are LF, because CRLF breaks RUN heredocs and line continuations.
[{Dockerfile,*.Dockerfile}]
end_of_line = lf

# .NET-only below, covering C# and ReSharper style.
# Everything above is the line-ending governance every derived repo carries.

# C# files
[*.cs]
end_of_line = crlf
# Suppressions follow CODESTYLE.md "Analyzer Diagnostics and Suppressions".
# Prefer a [SuppressMessage] attribute, or the owning project's .editorconfig.
# Relax a rule repo-wide here only when it applies to every project, never for a brownfield batch.
dotnet_diagnostic.IDE0055.severity = none
csharp_indent_block_contents = true
csharp_indent_braces = false
Expand Down
28 changes: 19 additions & 9 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
# Default: do not normalize line endings (`* -text`); .editorconfig end_of_line rules guide what the editor writes.
# The exception pins below are git enforcement - they force LF for execution-sensitive classes regardless of editor.
* -text

# Scripts and extensionless executables must stay LF - a CRLF shebang breaks execution.
*.sh text eol=lf

# Husky.Net ships an extensionless pre-commit hook with a /bin/sh shebang; pin it to LF so a CRLF cannot break execution.
.husky/pre-commit text eol=lf
# Default: git does not normalize line endings (`* -text`), and .editorconfig end_of_line rules guide what the editor writes.
# The exception pins below are git's own enforcement, forcing LF for execution-sensitive classes regardless of editor.
# Configure with: git config --global core.autocrlf false
# Renormalize with: git add --renormalize .
# Inspect with: git ls-files --eol
* -text

# Exception: scripts must stay LF regardless of the `* -text` default, because a CRLF shebang breaks execution.
# `.editorconfig` covers `*.sh`, but an extensionless executable matches no extension rule.
# Pin those here, so git enforces LF on checkout and on `--renormalize`.
*.sh text eol=lf

# Husky.Net ships an extensionless pre-commit hook with a `/bin/sh` shebang, so pin it to LF.
# A CRLF shebang would break the hook's execution.
.husky/pre-commit text eol=lf

# Dockerfiles must be LF, because a CRLF breaks RUN heredocs and line continuations.
Dockerfile text eol=lf
*.Dockerfile text eol=lf
196 changes: 137 additions & 59 deletions .github/copilot-instructions.md

Large diffs are not rendered by default.

10 changes: 8 additions & 2 deletions .github/workflows/build-release-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,16 @@ jobs:
assembly_file_version: ${{ needs.get-version.outputs.AssemblyFileVersion }}
assembly_informational_version: ${{ needs.get-version.outputs.AssemblyInformationalVersion }}

# The terminal publish target, so it needs every other build (D4.5).
# The image builds from source and consumes no executable artifact, so nothing else forces the ordering.
# Without it a failed build-executable skips github-release while the image still pushes and `latest` moves.
# That is a partial publish: an image with no release.
# `!failure()` is what blocks it, since a failed upstream build must stop the push where a skipped one must not.
# `validate` is skipped on smoke, which is the skip that must still build.
build-docker:
name: Build Docker job
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') }}
needs: [get-version, validate, validate-release, build-executable]
if: ${{ !failure() && !cancelled() && needs.get-version.result == 'success' && needs.validate-release.result == 'success' && (needs.validate.result == 'success' || needs.validate.result == 'skipped') }}
uses: ./.github/workflows/build-docker-task.yml
secrets: inherit
with:
Expand Down
95 changes: 95 additions & 0 deletions .github/workflows/merge-bot-pull-request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
name: Merge bot pull request action

# Auto-merges in-repo bot pull requests: enable on opened/reopened, disable on a maintainer push.
# - Merge method by base: develop = squash, main = merge.
# - App token, not GITHUB_TOKEN: it fires downstream workflows and can write on a Dependabot PR.
# - pull_request_target, not pull_request: jobs hold the App key.
# The workflow and action SHAs then resolve from the trusted base, not the pull request head.
# No job checks out pull request code, since each runs gh pr merge by URL.
#
# The fleet canonical also carries merge-codegen and merge-upstream-version jobs.
# This repository runs neither codegen nor an upstream-version tracker, so both are N/A here.
# They are left out rather than carried as conditions that can never match.
on:
pull_request_target:
types: [opened, reopened, synchronize]

# Concurrency keys on the pull request number rather than on github.ref.
# Under pull_request_target that ref is the base branch, serializing every bot pull request.
# Cancelling is off, so a follow-up synchronize cannot kill an opened run mid-enable.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: false

jobs:

merge-dependabot:
name: Merge dependabot pull request job
runs-on: ubuntu-latest
# Dependabot pull requests from this repository, not forks.
# Only on opened or reopened, so the disable job stays sticky.
if: >-
(github.event.action == 'opened' || github.event.action == 'reopened') &&
github.event.pull_request.user.login == 'dependabot[bot]' &&
github.event.pull_request.head.repo.full_name == github.repository
permissions:
contents: write
pull-requests: write

steps:

- name: Generate GitHub App token step
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ secrets.CODEGEN_APP_CLIENT_ID }}
private-key: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }}

# Auto-merge every tier, semver-major included.
# The required checks are the gate, not the size of the bump.
- name: Merge pull request step
run: |
set -Eeuo pipefail
case "${{ github.event.pull_request.base.ref }}" in
develop) method=--squash ;;
main) method=--merge ;;
*)
echo "::error::Unsupported base branch: ${{ github.event.pull_request.base.ref }}"
exit 1
;;
esac
gh pr merge --auto "$method" "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}

disable-auto-merge-on-maintainer-push:
name: Disable auto-merge on maintainer push job
runs-on: ubuntu-latest
# Fires when a maintainer pushes to a bot's branch, meaning synchronize with a non-bot actor.
# Disables auto-merge so the maintainer's commits do not merge with the bot's.
# They re-enable it by hand.
# The disable call is idempotent.
if: >-
github.event.action == 'synchronize' &&
github.event.pull_request.head.repo.full_name == github.repository &&
github.event.pull_request.user.login == 'dependabot[bot]' &&
github.actor != github.event.pull_request.user.login
permissions:
pull-requests: write

steps:

- name: Generate GitHub App token step
# App token, because a Dependabot pull request's GITHUB_TOKEN is read-only whoever triggered it.
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ secrets.CODEGEN_APP_CLIENT_ID }}
private-key: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }}

- name: Disable auto-merge step
run: gh pr merge --disable-auto "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
33 changes: 33 additions & 0 deletions .github/workflows/validate-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@ jobs:

- name: Checkout code step
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
# The prose gate diffs against the base branch, so it needs that branch's history.
# Only the runs that reach the gate pay for it, since a publish run skips the gate.
# A full clone there is time and network a release does not need.
# The condition matches the gate's own below, so the two cannot drift apart silently.
#
# The depths are quoted, and must stay quoted.
# An unquoted 0 is falsy, so `cond && 0 || 1` collapses to 1 whenever cond is true.
# Every run would then shallow-clone, leaving the gate no base to diff against.
fetch-depth: ${{ (github.event_name == 'push' && github.ref_name != 'main') && '0' || '1' }}

- name: Check C# formatting step
run: |
Expand Down Expand Up @@ -86,3 +96,26 @@ jobs:

- name: Check EditorConfig step
run: docker run --rm -v "$PWD":/check --workdir /check mstruebing/editorconfig-checker:latest

# The fleet prose rules live in the hub, so this repo consumes them rather than vendoring them.
# The rules are read from hub develop, so a rule change is exercised here before promotion.
# Only lines a change touches are reported, so the existing backlog blocks nothing.
#
# The base is the branch this one merges into.
# The gate runs on push, so there is no pull_request event to read a base from.
# Runs on main are skipped, since it only receives promotion merges already gated on develop.
#
# A publish run reaches this workflow too, through build-release-task, and is skipped.
# Its content was already gated when it was pushed.
# Re-reading it would diff the whole unpromoted delta and could fail a release on prose.
- name: Check prose step
if: ${{ github.event_name == 'push' && github.ref_name != 'main' }}
# A SHA pin, containing the prose gate from ptr727/ProjectTemplate#520.
# It was hub develop when pinned, and says nothing about where develop is now.
# A pin that claims to be a branch goes stale the moment that branch moves.
# It carries no release tag yet, so Dependabot cannot compare it and will not bump it.
# Repoint it by hand at the next hub release.
# Dependabot tracks it like any other action pin from then on.
uses: ptr727/ProjectTemplate/.github/actions/prose-gate@cbd5eb3c9e079b640d5461400d89b390de2f0780 # hub develop
with:
base: origin/${{ github.ref_name == 'develop' && 'main' || 'develop' }}
39 changes: 39 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,45 @@
"console": "internalConsole",
"stopAtEntry": false
},
{
"name": "Verify",
"type": "coreclr",
"request": "launch",
"preLaunchTask": ".NET Build",
"program": "${workspaceFolder}/PhotoCleaner/bin/Debug/net10.0/PhotoCleaner.dll",
"args": [
"verify",
"--path=/data/media/PhotoCleaner/Originals",
"--db=/data/media/PhotoCleaner/Verify.db",
"--rehash=false",
"--reprocess=true",
"--threads=4",
"--loglevel=verbose",
"--logfile=/data/media/PhotoCleaner/Verify.log",
"--logclear=true"
],
"cwd": "${workspaceFolder}/PhotoCleaner/bin/Debug/net10.0",
"console": "internalConsole",
"stopAtEntry": false
},
{
"name": "Verify No Database",
"type": "coreclr",
"request": "launch",
"preLaunchTask": ".NET Build",
"program": "${workspaceFolder}/PhotoCleaner/bin/Debug/net10.0/PhotoCleaner.dll",
"args": [
"verify",
"--path=/data/media/PhotoCleaner/Originals",
"--threads=4",
"--loglevel=verbose",
"--logfile=/data/media/PhotoCleaner/Verify.log",
"--logclear=true"
],
"cwd": "${workspaceFolder}/PhotoCleaner/bin/Debug/net10.0",
"console": "internalConsole",
"stopAtEntry": false
},
{
"name": "Undo",
"type": "coreclr",
Expand Down
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ An agent session is billed on the context it carries, not the work it does. Ever
### Session Scope

- **One deliverable, one session.** A session covers one branch and one deliverable, and ends when that work merges. A multi-step task is one deliverable and stays in one session. Two unrelated tasks are two sessions even when they run back to back.
- **End a session at any of these, without being asked:** the branch changes, the pull request merges, the next task is unrelated to the last, or a third review round opens on the same pull request.
- **End a session at any of these, without being asked:** the branch changes, the pull request merges, or the next task is unrelated to the last. A review round is none of them. A loop still producing findings is the deliverable in progress, and a round count is not a reason to leave one open.
- **Hand off in a file, never in context.** Close a session by writing at most 2 KB to a scratch file: branch, pull request link, what is done, the next command. A summary held in context is re-billed until the session ends, and a summary on disk is read once by whoever needs it.
- **Re-derive state, do not carry it.** "This session already has the context" is the signal to split, not to continue. Context that has gone stale is worse than absent, because a file read hundreds of requests ago no longer describes the file.
- **Compaction is a fallback, not the strategy.** It restarts context from a floor and climbs again, where a fresh session starts from zero.
Expand Down Expand Up @@ -45,6 +45,7 @@ If a rule you were given does not cover what you find, stop and report it. Do no
```

- **Wait in a background process, not in a poll loop.** A review or CI wait is a sequence of near-identical requests, each billed for whatever context it happens to carry. Run the wait as one backgrounded command that returns when the condition is met.
- **A wait separates three outcomes, and says which one it reached.** The condition was met, it has not been met yet, and the wait cannot reach it at all are three different results, and a backgrounded wait that emits nothing renders all three identically. Run the command once in the foreground and read its output before backgrounding it, because a wait is only as good as the command inside it, and an unsupported flag on the installed tool version exits non-zero with an empty stdout that every naive test reads as "nothing yet". Never let a fallback stand in for a failed command, since `|| echo '[]'`, `|| true`, and `2>/dev/null` convert an error into that same reading, which is the suppression the write-safety rules already forbid on a mutation. Make the wait emit on failure as loudly as on success, so silence means "still running" and nothing else, and bound it, so a condition that is never coming ends in a report rather than in another wait.

## Where the Rules Live

Expand All @@ -55,6 +56,7 @@ Every rule below is a level-two section of [`GOVERNANCE.md`](./GOVERNANCE.md). R
| Why the rules are shaped this way | `Foundational Principles` |
| Recording a durable lesson or updating governance | `Durable Knowledge and Self-Improvement` |
| Any push, API mutation, comment, label, or merge | `Repository Boundaries and Write Safety` |
| Quoting data into a comment, commit, test, or doc | `Representative Data in Agent-Authored Text` |
| Committing, signing, rebasing, force-pushing | `Git and Commit Rules` |
| Branch choice, promotion, keeping branches in sync | `Branching Model` |
| Releasing, version bumps, publishing | `Release Model` |
Expand Down
4 changes: 2 additions & 2 deletions AUDIT.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ The audit is read-only: it diffs live state against the committed baseline and r

## Scope

This is a release-model repo: the self-audit covers the `main` and `develop` rulesets, general repository settings, and secret names. Code-project conformance (analyzers, tests, coverage, publish workflows) is CI's job and the fleet hub's fleet-wide audit's, not this self-audit's - see [GOVERNANCE.md "Branching Model"][governance-branching-model] for the model this baseline encodes.
This is a release-model repo: the self-audit covers the `main` and `develop` rulesets, general repository settings, and secret names. Code-project conformance (analyzers, tests, coverage, publish workflows) is CI's job and the fleet hub's fleet-wide audit's, not this self-audit's. See [GOVERNANCE.md "Branching Model"][governance-branching-model] for the model this baseline encodes.

## General Settings

Expand Down Expand Up @@ -35,7 +35,7 @@ for b in develop main; do
done
```

The result must be exactly two rulesets named `develop` and `main` - a missing ruleset or a divergent payload is a **defect**; a duplicate or stray ruleset is a **drift finding**.
The result must be exactly two rulesets named `develop` and `main`. A missing ruleset or a divergent payload is a **defect**, and a duplicate or stray ruleset is a **drift finding**.

## Secrets

Expand Down
Loading
Loading