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
22 changes: 20 additions & 2 deletions .github/actions/build-executable-default/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ inputs:
description: Path to the console project file.
required: false
default: ./Console/Console.csproj
asset-name:
description: Name of the release archive without its .7z extension, so Widget produces Widget.7z. Empty derives it from the project file's stem, so ./Widget/Widget.csproj also produces Widget.7z. Letters, digits, dot, underscore and hyphen only, given or derived, and anything else fails the step.
required: false
default: ''

runs:
using: composite
Expand Down Expand Up @@ -70,17 +74,31 @@ runs:
-property:PackageVersion="$SEMVER2"
done

# The archive is named for the project unless the caller names it, since a release asset called Console.7z tells a downloader nothing.
- name: Zip build output step
id: zip
if: ${{ inputs.smoke != 'true' }}
shell: bash
run: 7z a -t7z "$RUNNER_TEMP/Console.7z" "$RUNNER_TEMP"/publish/*
env:
PROJECT_FILE: ${{ inputs.project-file }}
ASSET_NAME: ${{ inputs.asset-name }}
run: |
set -Eeuo pipefail
name="${ASSET_NAME:-$(basename -- "$PROJECT_FILE" .csproj)}"
name="${name%.7z}"
if [[ ! "$name" =~ ^[A-Za-z0-9._-]+$ ]]; then
echo "::error::asset-name must be a bare file name of letters, digits, dot, underscore or hyphen"
exit 1
fi
Comment thread
Copilot marked this conversation as resolved.
7z a -t7z "$RUNNER_TEMP/$name.7z" "$RUNNER_TEMP"/publish/*
echo "asset-path=$RUNNER_TEMP/$name.7z" >> "$GITHUB_OUTPUT"

# GitHub-release asset, uploaded under the release-asset-<branch>-<target> pattern the github-release job collects.
- name: Upload release asset step
if: ${{ inputs.smoke != 'true' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-asset-${{ inputs.branch }}-executable
path: ${{ runner.temp }}/Console.7z
path: ${{ steps.zip.outputs.asset-path }}
# Consumed within the run by the github-release job, so minimize artifact storage.
retention-days: 1
7 changes: 7 additions & 0 deletions .github/workflows/build-release-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@ on:
required: false
type: string
default: ./Console/Console.csproj
# The release archive's name without .7z, derived from the project file's stem when empty.
executable_asset_name:
required: false
type: string
default: ''
nuget_project:
required: false
type: string
Expand Down Expand Up @@ -202,6 +207,7 @@ jobs:
ref: ${{ job.workflow_sha }}
path: .hub

# A caller's own hook names its archive itself, so the asset name input reaches only the hub default.
- name: Run caller build-executable hook step
if: ${{ hashFiles('.github/actions/build-executable/action.yml') != '' }}
uses: ./.github/actions/build-executable
Expand All @@ -225,6 +231,7 @@ jobs:
assembly-file-version: ${{ needs.get-version.outputs.AssemblyFileVersion }}
assembly-informational-version: ${{ needs.get-version.outputs.AssemblyInformationalVersion }}
project-file: ${{ inputs.executable_project }}
asset-name: ${{ inputs.executable_asset_name }}

build-nuget:
name: Build NuGet library job
Expand Down
5 changes: 5 additions & 0 deletions catalog/snippets/workflows/publish-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false

# GITHUB_TOKEN gets no scope by default, and each job below grants only what its hub task writes with.
permissions: {}

jobs:

# Single source of the release-gate decision (publish or not, stable or not), reused by every job below.
Expand All @@ -31,6 +34,8 @@ jobs:
needs: [plan]
if: ${{ needs.plan.outputs.publish == 'true' }}
uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@0b07a59d7c65d07d8df275a96deaf2e06cbefd51 # 2.0.352
permissions:
contents: read
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

Expand Down
18 changes: 13 additions & 5 deletions docs/reusable-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,8 @@ Hub: `validate-task.yml` hosts a `lint` job (the fleet doc-lint block, language
- [x] Hub pull request on `develop` with the task, the hub's own hook and default, the manifest contracts, and the catalog snippets left for the release that follows, [#760][pr-760].
- [x] Promoted to `main` in #774 (`0b07a59d`) and released as `2.0.352`, the first tag carrying `validate-task.yml`.
- [ ] Catalog snippets for both stub shapes in [Adopting the Gates][adopting-the-gates] pinned to that release. The no-build shape has one, `catalog/snippets/workflows/test-pull-request.yml`. The release-with-smoke shape still calls its own repo's `build-release-task.yml` by `./` path rather than the hub's, so it carries no catalog-ready pin yet, and this item stays open until it does.
- [x] Hook override path observed on a hub pull request run, [proof run][override-path-run] (runs `./.github/actions/validate`, no hub checkout). Default path awaits a repo with no `validate` hook of its own.
- [ ] PhotoCleaner (pilot, release trigger shape with smoke, the same repo that piloted stage 1)
- [x] Hook override path observed on a hub pull request run, [proof run][override-path-run] (runs `./.github/actions/validate`, no hub checkout). Default path observed on PhotoCleaner's adoption pull request, [pilot smoke run][pilot-smoke-run], where the hub's `validate-default` ran because that repo carries no `validate` hook.
- [x] PhotoCleaner (pilot, release trigger shape with smoke, the same repo that piloted stage 1): ptr727/PhotoCleaner#55 on `develop` (`c80cb29`), promoted in ptr727/PhotoCleaner#56 (`fa91db0`), both on 2026-08-16. `test-pull-request.yml` calls the hub validate task and no repo hook was needed.
- [ ] HomeAutomation-Config (second pilot, operational trigger shape)
- [ ] The remaining repos, one checkbox each added when the pilots close, since the sweep list is every cataloged repo.
- [ ] `reports/workflow-reuse.md` regenerated with `validate-task.yml` at 0 copies (a hub-only file no repo carries) and `test-pull-request.yml` showing callers equal to copies.
Expand Down Expand Up @@ -186,14 +186,15 @@ Hub: `build-release-task.yml` with `build-executable`, `build-nuget`, `build-pyp

- [x] Hub pull request on `develop` in #762.
- [x] Promoted to `main` in #774 (`0b07a59d`) and released as `2.0.352`, the first tag carrying `build-release-task.yml` and `build-docker-task.yml`. The first release attempt, on `82fecef`, ended in `startup_failure` in [run-startup-failure][run-startup-failure] because `build-nuget` and `github-release` declared job-level permissions. #772 fixed it before #774 promoted.
- [ ] PhotoCleaner (pilot, vanilla Docker plus executable)
- [x] PhotoCleaner (pilot, vanilla Docker plus executable): ptr727/PhotoCleaner#55 on `develop` (`c80cb29`), promoted in ptr727/PhotoCleaner#56 (`fa91db0`), five carried task files deleted, every value mapped to a task input (`executable_project`, `docker_image`), no repo hook needed. Its first publish through the task, [pilot publish run][pilot-publish-run], released `1.1.11` with the executable asset attached and the Docker image pushed. That run exposed one regression the pilot exists to find: the hub executable default named the archive `Console.7z` where the repo's own leaf named it `PhotoCleaner.7z`, fixed in the pull request that ticks this item by deriving the name from the project file (an `executable_asset_name` input overrides it), so the next PhotoCleaner release is the proof of the fix.
- [ ] PlexCleaner (second, the same shape)
- [ ] VSCode-Server-DotNetCore (vanilla Docker only)
- [ ] ESPHome-NonRoot (`docker-prepare` hook for the upstream pin)
- [ ] NxWitness (matrix hook and `build-base`)
- [ ] The NuGet, PyPI and remaining release repos, one checkbox each added when the pilots close.
- [ ] A smoke build through `build-release-task.yml` observed on the PhotoCleaner or PlexCleaner pilot's pull request, run URL recorded here.
- [ ] A real publish through `build-release-task.yml` observed on the PhotoCleaner or PlexCleaner pilot, run URL recorded here.
- [x] A smoke build through `build-release-task.yml` observed on the PhotoCleaner pilot's pull request, [pilot smoke run][pilot-smoke-run]: get-version, validate-release, `build-executable`, `docker-prepare` and `build-docker` all through hub defaults, nuget, pypi and the base build skipped.
- [x] A real publish through `build-release-task.yml` observed on the PhotoCleaner pilot, [pilot publish run][pilot-publish-run]: release `1.1.11` on `fa91db0` with `Publish GitHub release job` and `Build Docker image job` both succeeding.
- [ ] Proof: the next PhotoCleaner release names its executable asset `PhotoCleaner.7z`, which the asset-name fix in this repository derives from the project file. Tick with the release.
- [ ] `reports/workflow-reuse.md` regenerated with `build-release-task.yml` and `build-docker-task.yml` at 0 copies (hub-only files) and `publish-release.yml` showing callers equal to copies.

### Stage 5: The Type-Specific Tasks
Expand Down Expand Up @@ -441,6 +442,9 @@ concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false

# GITHUB_TOKEN gets no scope by default, and each job below grants only what its hub task writes with.
permissions: {}

jobs:

# Single source of the release-gate decision (publish or not, stable or not), reused by every job below.
Expand All @@ -458,6 +462,8 @@ jobs:
needs: [plan]
if: ${{ needs.plan.outputs.publish == 'true' }}
uses: ptr727/ProjectTemplate/.github/workflows/validate-task.yml@0b07a59d7c65d07d8df275a96deaf2e06cbefd51 # 2.0.352
permissions:
contents: read
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

Expand Down Expand Up @@ -609,6 +615,8 @@ Four things the hub cannot prove fall to the first downstream adopter. They are
[governance-workflow-yaml-conventions]: ../GOVERNANCE.md#workflow-yaml-conventions
[issue-585]: https://github.com/ptr727/ProjectTemplate/issues/585
[override-path-run]: https://github.com/ptr727/ProjectTemplate/actions/runs/31950332387/job/95172710046
[pilot-publish-run]: https://github.com/ptr727/PhotoCleaner/actions/runs/31977092102
[pilot-smoke-run]: https://github.com/ptr727/PhotoCleaner/actions/runs/31974932749
[pr-760]: https://github.com/ptr727/ProjectTemplate/pull/760
[run-770]: https://github.com/ptr727/ProjectTemplate/actions/runs/31972611554
[run-771]: https://github.com/ptr727/ProjectTemplate/actions/runs/31972622149
Expand Down
33 changes: 16 additions & 17 deletions reports/workflow-reuse.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
# Fleet workflow reuse report

Generated by `python3 spec/workflow_reuse.py --report` at hub `63d244b` - do not hand-edit. Each row reads a repo's ground-truth branch at generation time and compares it against the hub canonical of the same name after line-ending, action-pin, and job-needs normalization, per [`spec/fidelity-model.md`][fidelity-model] "Normalization". Git dates this file. The target model and the migration phases are in [`docs/reusable-workflows.md`][reusable-workflows].
Generated by `python3 spec/workflow_reuse.py --report` at hub `76f15b3` - do not hand-edit. Each row reads a repo's ground-truth branch at generation time and compares it against the hub canonical of the same name after line-ending, action-pin, and job-needs normalization, per [`spec/fidelity-model.md`][fidelity-model] "Normalization". Git dates this file. The target model and the migration phases are in [`docs/reusable-workflows.md`][reusable-workflows].

## Fleet Total

- **109 workflow files, 10,914 lines** across 20 downstream repos, 100 of them named for a hub canonical. No workflow at all in EspDinIoT.
- **3,940 lines (36%) are byte-identical to a hub canonical** after normalization, which is the confirmed duplication. The rest is mostly a per-repo edit of the same canonical rather than independent code.
- **Files reaching a hub reusable workflow or composite action through a pinned `uses:`: 2.** That is the state every carried copy converges to, so this number rises and the two above fall as the migration lands.
- **104 workflow files, 10,358 lines** across 20 downstream repos, 96 of them named for a hub canonical. No workflow at all in EspDinIoT.
- **3,654 lines (35%) are byte-identical to a hub canonical** after normalization, which is the confirmed duplication. The rest is mostly a per-repo edit of the same canonical rather than independent code.
- **Files reaching a hub reusable workflow or composite action through a pinned `uses:`: 3.** That is the state every carried copy converges to, so this number rises and the two above fall as the migration lands.

## Per Workflow

Downstream copies of each hub canonical. A variant is a cluster of copies each at or above 0.85 similarity to the cluster's first member, so the cluster count is how many distinct shapes of one workflow the fleet runs today. Callers are the copies that already reach the hub rather than carrying the job bodies.

| File | Copies | Lines | Identical to hub | Variants | Callers |
| --- | --- | --- | --- | --- | --- |
| `build-release-task.yml` | 10 | 1,934 | 1,042 | 6 | 0 |
| `test-pull-request.yml` | 20 | 1,628 | 493 | 14 | 0 |
| `build-release-task.yml` | 9 | 1,709 | 891 | 6 | 0 |
| `test-pull-request.yml` | 20 | 1,661 | 497 | 14 | 1 |
| `merge-bot-pull-request.yml` | 16 | 1,626 | 203 | 8 | 1 |
| `publish-release.yml` | 17 | 1,341 | 402 | 12 | 0 |
| `validate-task.yml` | 14 | 1,335 | 478 | 12 | 1 |
| `build-docker-task.yml` | 5 | 625 | 334 | 4 | 0 |
| `get-version-task.yml` | 8 | 495 | 391 | 4 | 0 |
| `publish-release.yml` | 17 | 1,374 | 416 | 13 | 1 |
| `validate-task.yml` | 13 | 1,214 | 442 | 11 | 0 |
| `build-docker-task.yml` | 4 | 512 | 266 | 4 | 0 |
| `get-version-task.yml` | 7 | 439 | 342 | 4 | 0 |
| `publish-plan-task.yml` | 3 | 252 | 207 | 1 | 0 |
| `deploy-site-task.yml` | 1 | 191 | 113 | 1 | 0 |
| `run-codegen-pull-request-task.yml` | 2 | 161 | 130 | 1 | 0 |
Expand All @@ -36,7 +36,7 @@ Each variant names the repos whose copies cluster together, so a hub task's inpu
- 2: ESPHome-NonRoot, VSCode-Server-DotNetCore
- 1: KiCadLibrary
- 3: LanguageTags, MediaTools, Utilities
- 2: PhotoCleaner, PlexCleaner
- 1: PlexCleaner
- 1: aiopurpleair
- 1: homeassistant-purpleair
- `test-pull-request.yml`
Expand Down Expand Up @@ -71,7 +71,8 @@ Each variant names the repos whose copies cluster together, so a hub task's inpu
- 1: KiCadLibrary
- 2: LanguageTags, MediaTools
- 1: NxWitness
- 2: PhotoCleaner, PlexCleaner
- 1: PhotoCleaner
- 1: PlexCleaner
- 1: Utilities
- 1: VSCode-Server-DotNetCore
- 1: aiopurpleair
Expand All @@ -85,17 +86,16 @@ Each variant names the repos whose copies cluster together, so a hub task's inpu
- 1: HomeAutomation-Config
- 1: LanguageTags
- 1: NxWitness
- 1: PhotoCleaner
- 1: PlexCleaner
- 1: VSCode-Server-DotNetCore
- 1: aiopurpleair
- `build-docker-task.yml`
- 1: ESPHome-NonRoot
- 1: NxWitness
- 2: PhotoCleaner, PlexCleaner
- 1: PlexCleaner
- 1: VSCode-Server-DotNetCore
- `get-version-task.yml`
- 5: ESPHome-NonRoot, NxWitness, PhotoCleaner, PlexCleaner, VSCode-Server-DotNetCore
- 4: ESPHome-NonRoot, NxWitness, PlexCleaner, VSCode-Server-DotNetCore
- 1: KiCadLibrary
- 1: aiopurpleair
- 1: homeassistant-purpleair
Expand Down Expand Up @@ -130,7 +130,7 @@ Each variant names the repos whose copies cluster together, so a hub task's inpu
| LanguageTags | 7 | 724 | 305 | 0 | - |
| MediaTools | 5 | 530 | 212 | 0 | - |
| NxWitness | 10 | 1,102 | 378 | 0 | `build-base-images-task.yml` |
| PhotoCleaner | 8 | 759 | 372 | 2 | `build-executable-task.yml` |
| PhotoCleaner | 3 | 203 | 86 | 3 | - |
| PlexCleaner | 8 | 805 | 356 | 0 | `build-executable-task.yml` |
| Utilities | 6 | 621 | 294 | 0 | - |
| VSCode-Server-DotNetCore | 8 | 563 | 294 | 0 | - |
Expand All @@ -147,7 +147,6 @@ A workflow no hub canonical names. Each is either genuinely repo-specific, and s
- **ESPHome-NonRoot** `check-upstream-version.yml` (41 lines)
- **KiCadLibrary** `build-datebadge-task.yml` (37 lines)
- **NxWitness** `build-base-images-task.yml` (89 lines)
- **PhotoCleaner** `build-executable-task.yml` (107 lines)
- **PlexCleaner** `build-executable-task.yml` (107 lines)
- **homeassistant-purpleair** `check-ha-version.yml` (316 lines)
- **homeassistant-purpleair** `test-release-task.yml` (248 lines)
Expand Down