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
101 changes: 101 additions & 0 deletions .github/workflows/get-version-task.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
name: Get version information task

# The fleet's single NBGV run, hosted here once and reached by a leaf or a publisher through a pinned uses:.
# It has no repo-specific line, since version.json and the git history it walks are the only per-repo input.
# Every caller reads the same five outputs plus the derived Prerelease flag.
# The ref input defaults to the caller's own checkout ref.
# A caller may pass a different ref, for example a publisher versioning a branch other than the one it dispatched from.
# IGNORE_GITHUB_REF below keeps NBGV honest even when inputs.ref and github.ref disagree.
# That check costs nothing on the common path where the two already match.
# AssemblyVersion, AssemblyFileVersion and AssemblyInformationalVersion are exposed for a .NET build to stamp.
# A caller with no compiled code, a PyPI or HACS package, simply leaves them unused rather than the task growing a per-ecosystem branch.
# Prerelease is a real derivation two callers duplicated identically, a Python package and a Home Assistant integration, neither versioning a .NET assembly.
# It is hosted here rather than as per-repo logic.
# It reads 'true' whenever SemVer2 carries a prerelease '-' segment, and 'false' on a clean public version.
on:
workflow_call:
inputs:
# Git ref to check out and version.
# Empty falls back to the caller's default checkout ref (`github.ref`).
# The publisher passes an explicit branch so a scheduled run can still compute versions for `develop`.
ref:
required: false
type: string
default: ''
outputs:
SemVer2:
description: "SemVer2-shaped version from NBGV (version.json + git history); the release tag name, no v prefix."
value: ${{ jobs.get-version.outputs.SemVer2 }}
AssemblyVersion:
value: ${{ jobs.get-version.outputs.AssemblyVersion }}
AssemblyFileVersion:
value: ${{ jobs.get-version.outputs.AssemblyFileVersion }}
AssemblyInformationalVersion:
value: ${{ jobs.get-version.outputs.AssemblyInformationalVersion }}
# Full SHA of the commit the version was computed from, used to pin the release tag to the exact built commit.
GitCommitId:
value: ${{ jobs.get-version.outputs.GitCommitId }}
Prerelease:
description: "'true' when SemVer2 carries a prerelease '-' segment, 'false' on a clean public version."
value: ${{ jobs.get-version.outputs.Prerelease }}

jobs:

get-version:
name: Get version information job
runs-on: ubuntu-latest
outputs:
SemVer2: ${{ steps.nbgv.outputs.SemVer2 }}
AssemblyVersion: ${{ steps.nbgv.outputs.AssemblyVersion }}
AssemblyFileVersion: ${{ steps.nbgv.outputs.AssemblyFileVersion }}
AssemblyInformationalVersion: ${{ steps.nbgv.outputs.AssemblyInformationalVersion }}
GitCommitId: ${{ steps.nbgv.outputs.GitCommitId }}
Prerelease: ${{ steps.out.outputs.Prerelease }}

steps:

- name: Setup .NET SDK step
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: 10.x

- name: Checkout code step
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.ref }}
fetch-depth: 0

# The nbgv action is floated on @master, because its tag stream lags master.
# Dependabot tag-tracking would otherwise propose a downgrade.
# Revisit if dotnet/nbgv resumes regular tagged releases.
- name: Run Nerdbank.GitVersioning tool step
id: nbgv
uses: dotnet/nbgv@master
env:
# Version from the checked-out branch rather than the CI ref.
# GITHUB_REF is reserved and a step env can't reliably override it, since the runner re-injects the dispatch ref.
# On a publish dispatched from the default branch, NBGV would therefore classify every leg as the public ref.
# IGNORE_GITHUB_REF makes NBGV ignore GITHUB_REF and use the checked-out branch, which each caller already is.
# The validate-release gate backstops any misclassification.
IGNORE_GITHUB_REF: "true"

# NBGV's PrereleaseVersion output only carries version.json's `-tag` segment, empty for a bare SemVer base.
# The auto-appended -g{sha} segment lives in SemVer2 but is not exposed there.
# Detect prerelease by testing SemVer2 for any `-` instead, the same test validate-release uses.
# Strip '+buildmetadata' first, since SemVer2 allows a `-` there and it carries no prerelease meaning.
# That reads true for `1.0.5-g1a2b3c4`, a prerelease branch, and false for `1.0.5` or `1.0.5+abc-def`, both public.
# This also keeps a manual workflow_dispatch from the wrong branch honest about its prerelease status.
- name: Compose workflow outputs step
id: out
env:
SEMVER2: ${{ steps.nbgv.outputs.SemVer2 }}
run: |
set -Eeuo pipefail
CORE_AND_PRE="${SEMVER2%%+*}"
if [[ "$CORE_AND_PRE" == *-* ]]; then
PRERELEASE=true
else
PRERELEASE=false
fi
echo "Prerelease=$PRERELEASE" >> "$GITHUB_OUTPUT"
echo "Version: $SEMVER2 (prerelease: $PRERELEASE)"
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: Publish plan task

# Single source of truth for the release-gate decision, reused by every publish-release.yml job.
# Single source of truth for the release-gate decision, hosted here once and reused by every publish-release job.
# The policy therefore lives here rather than scattered across job `if:` conditions.
# A human PR merge never auto-publishes.
# A release is a deliberate dispatch, a bot (Dependabot/codegen) code-merge to main, or the Docker weekly schedule.
Expand Down
3 changes: 3 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,9 @@ One pull request per stage moving a standard workflow out of every repo and into
- **Checked** - `develop` at `7c67328` on 2026-08-15, where 5 of 8 `get-version-task.yml` copies are identical and all 3 `publish-plan-task.yml` copies are.
- **Open** - Nothing.
- **Settled** - PlexCleaner carries no `plan` job, so its next scheduled run ships a Dependabot bump, and the hub-hosted plan job is the fix rather than a per-repo copy.
- **Settled** - Of the 3 `get-version-task.yml` copies that differ, KiCadLibrary only documents the same design the canonical already carries, and aiopurpleair and homeassistant-purpleair each add a `Prerelease` output derived from `SemVer2` with identical logic. The hub task hosts `Prerelease` as a sixth output rather than leaving it as per-repo logic, and drops homeassistant-purpleair's `Tag` alias since it is a bare copy of `SemVer2` with no derivation of its own.
- **Settled** - All 3 `publish-plan-task.yml` copies are a strict subset of the canonical, missing the `-E` in `set -Eeuo pipefail` and the `::warning::` branch for an unrecognized actor pushing to `main` (D8.4). The hub task carries the canonical as is.
- **Settled** - Neither task is nested inside the other or inside a future `build-release-task.yml`, so a caller that only needs the version, not the whole release orchestrator, reaches `get-version-task.yml` directly. `build-release-task.yml` (stage 4) inlines the get-version and validate-release jobs rather than nesting a sibling hub task, per the no-`./`-nesting rule.

- **Host the release chain: `build-release-task.yml` with `build-<target>` hooks, `publish-release-task.yml`, and the Docker core.** The orchestration is generic and the target list is per repo, which the hooks express without a per-repo copy of the orchestrator.
- **Blocked by** - The pure functions, since the release task calls both.
Expand Down
6 changes: 3 additions & 3 deletions catalog/snippets/workflows/README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# Workflow snippets

The reusable build/publish workflow tasks a code-shipping repo runs. They are **inert reference here**: this repo is source-only and keeps just the orchestrator set (`test-pull-request`, `publish-release`, `validate-task`, `merge-bot-pull-request`) in `.github/workflows/`, plus the hub-hosted reusable tasks a downstream repo reaches rather than carries (`merge-bot-task`, per [`docs/reusable-workflows.md`][reusable-workflows]). Each file below is the canonical implementation of one or more `WORKFLOW.md` guarantees. The audit asserts a downstream repo's own Actions satisfy those guarantees, not that they match these bytes.
The reusable build/publish workflow tasks a code-shipping repo runs. They are **inert reference here**: this repo is source-only and keeps just the orchestrator set (`test-pull-request`, `publish-release`, `validate-task`, `merge-bot-pull-request`) in `.github/workflows/`, plus the hub-hosted reusable tasks a downstream repo reaches rather than carries (`merge-bot-task`, `get-version-task`, `publish-plan-task`, per [`docs/reusable-workflows.md`][reusable-workflows]). Each row below names the canonical implementation of one or more `WORKFLOW.md` guarantees, whether the file lives in this directory or is hub-hosted and reached by pin. The audit asserts a downstream repo's own Actions satisfy those guarantees, not that they match these bytes.

| File | Role | WORKFLOW.md guarantees |
| --- | --- | --- |
| `merge-bot-pull-request.yml` | Caller stub for the hub-hosted merge-bot task, pinned to a hub release, the shape every repo carries | D8.1, D8.3, D8.4 |
| `build-release-task.yml` | Multi-target release orchestrator: get-version, validate-release, github-release plus per-target build jobs | D3, D4, D5, D6 |
| `get-version-task.yml` | NBGV version/tag computation (reusable) | D3 |
| `publish-plan-task.yml` | Single-source release-gate decision (publish? stable?) reused by every publish-release job | D4 |
| `.github/workflows/get-version-task.yml` | Hub-hosted and reached by pin from a leaf or publisher rather than carried, with no caller-stub snippet since it is called as a job inside a larger stub, per [`docs/reusable-workflows.md`][reusable-workflows] | D3 |
| `.github/workflows/publish-plan-task.yml` | Hub-hosted and reached by pin from every publish-release job rather than carried, with no caller-stub snippet since it is called as a job inside a larger stub, per [`docs/reusable-workflows.md`][reusable-workflows] | D4 |
| `build-executable-task.yml` | Console/executable per-runtime publish, aggregate to one release asset | D5, D6, and section 6 Console walkthrough |
| `build-nugetlibrary-task.yml` | Build + `dotnet nuget push` (OIDC), upload release asset | D3.4, D4.4, D6, and section 6 NuGet walkthrough |
| `build-pypilibrary-task.yml` | Build PyPI package, with publishing split to an OIDC job | D3.4, D4, D7.2, and section 6 PyPI walkthrough |
Expand Down
63 changes: 0 additions & 63 deletions catalog/snippets/workflows/get-version-task.yml

This file was deleted.

43 changes: 41 additions & 2 deletions docs/reusable-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ The design for moving the fleet's standard GitHub Actions workflows out of every
- [Stage 4: The Release Chain and the Docker Core](#stage-4-the-release-chain-and-the-docker-core)
- [Stage 5: The Type-Specific Tasks](#stage-5-the-type-specific-tasks)
- [Adopting the Merge-Bot](#adopting-the-merge-bot)
- [Adopting the Pure Functions](#adopting-the-pure-functions)
- [What a Pilot Proves](#what-a-pilot-proves)
- [Open Decisions](#open-decisions)

Expand Down Expand Up @@ -161,9 +162,19 @@ Hub: `validate-task.yml` hosts the per-type doc-lint block once and calls the `v

Hub: `get-version-task.yml` and `publish-plan-task.yml` hosted, and the downstream copies deleted on adoption. PlexCleaner gains the `plan` job D4.1 requires by adopting rather than by a copy.

- [ ] Hub pull request on `develop`.
- [x] Hub pull request on `develop`, #759.
- [ ] Promoted and released, tag recorded here.
- [ ] Adoption, one checkbox per carrier added when the hub pull request merges: today `get-version-task.yml` has 8 carriers and `publish-plan-task.yml` 3.
- [ ] Catalog snippet for a caller stub, after the release: a caller of either task is a job inside a repo's own `publish-release.yml` or a future `build-release-task.yml` rather than a standalone top-level workflow, so today's honest answer is no snippet, only the `with:`/`uses:` lines in [Adopting the Pure Functions](#adopting-the-pure-functions). Revisit if an adopting repo's shape argues otherwise.
- [ ] Adoption, one checkbox per carrier added when the hub pull request merges: today `get-version-task.yml` has 8 carriers and `publish-plan-task.yml` 3, with ESPHome-NonRoot and NxWitness carrying both.
- [ ] ESPHome-NonRoot (`get-version-task.yml` and `publish-plan-task.yml`)
- [ ] NxWitness (`get-version-task.yml` and `publish-plan-task.yml`)
- [ ] PhotoCleaner (`get-version-task.yml`)
- [ ] PlexCleaner (`get-version-task.yml`, and gains the `plan` job D4.1 requires by adopting `publish-plan-task.yml` rather than by a copy)
- [ ] VSCode-Server-DotNetCore (`get-version-task.yml`)
- [ ] KiCadLibrary (`get-version-task.yml`)
- [ ] aiopurpleair (`get-version-task.yml`)
- [ ] homeassistant-purpleair (`get-version-task.yml`)
- [ ] Utilities (`publish-plan-task.yml`)
- [ ] `reports/workflow-reuse.md` regenerated with `get-version-task.yml` and `publish-plan-task.yml` at 0 copies, since both are hub-only files no repo carries.

### Stage 4: The Release Chain and the Docker Core
Expand Down Expand Up @@ -235,6 +246,34 @@ The task's inputs are `app-login` (default `ptr727-codegen[bot]`), `rules` (a JS

Two copies today filter Dependabot by ecosystem and semver tier before merging. [WORKFLOW.md D8.1][workflow-d8] says every Dependabot tier auto-merges and the required checks are the gate, so those two repos drop the filter on adoption unless the [Open Decisions][open-decisions] below settle otherwise.

## Adopting the Pure Functions

Neither `get-version-task.yml` nor `publish-plan-task.yml` has a caller-stub snippet of its own, since a caller reaching either one is a job inside a repo's own `publish-release.yml` or a future `build-release-task.yml`, not a standalone top-level workflow. A repo whose publisher reads NBGV's version outputs directly, without carrying the whole release orchestrator, reaches `get-version-task.yml` by pin in place of its own copy:

```yaml
get-version:
name: Get version information job
uses: ptr727/ProjectTemplate/.github/workflows/get-version-task.yml@<hub-main-commit-sha> # <release-tag>
with:
ref: ${{ github.ref }}
# Outputs: SemVer2, AssemblyVersion, AssemblyFileVersion, AssemblyInformationalVersion, GitCommitId, Prerelease.
```

A repo whose publisher needs the release-gate decision reaches `publish-plan-task.yml` by pin the same way:

```yaml
plan:
name: Plan release job
uses: ptr727/ProjectTemplate/.github/workflows/publish-plan-task.yml@<hub-main-commit-sha> # <release-tag>
with:
event_name: ${{ github.event_name }}
actor: ${{ github.actor }}
ref_name: ${{ github.ref_name }}
# Outputs: publish, stable.
```

`build-release-task.yml` (stage 4) does not call `get-version-task.yml` this way. It inlines the get-version and validate-release jobs rather than nesting a sibling hub task, per the design's no-`./`-nesting rule: a local `uses:` inside a called workflow would resolve against the top-level caller's repository, not the hub. Only a caller that reads the version outputs on their own, without the rest of the release orchestrator, reaches `get-version-task.yml` directly, and a downstream `publish-release.yml` stub that does exactly that is the shape shown above.

## What a Pilot Proves

The hub's own stub proves most of the mechanics on the first Dependabot pull request after the task lands on `develop`. That run shows the callee reading the caller's `github.event.*` under `pull_request_target`. It shows an explicit `secrets:` map reaching the callee and the App token minting inside one. It shows `permissions: {}` at the caller not failing the callee at startup, and `--squash` running on `develop`. A Dependabot pull request against `main` after promotion proves `--merge`, and a maintainer push to a bot branch proves the disable job. A hub feature branch cannot test itself, since under `pull_request_target` the callee resolves from the base branch, so the proof follows the merge rather than preceding it.
Expand Down
8 changes: 7 additions & 1 deletion scripts/prose_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,13 @@ def path_candidate(token: str, in_span: bool = True) -> str | None:
# That is the gate with the least room to fix it, and a ruleset bypass is the only local remedy.
# Held as a literal because the prose-gate action fetches this one file with no hub tree beside it.
# The `retire` dispositions in `spec/divergences.json` are the source, and a hub test asserts this.
HUB_HOSTED = frozenset({"repo-config/configure.sh"})
HUB_HOSTED = frozenset(
{
"repo-config/configure.sh",
".github/workflows/get-version-task.yml",
".github/workflows/publish-plan-task.yml",
}
)


@functools.cache
Expand Down
Loading