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
8 changes: 8 additions & 0 deletions .github/workflows/publish-release.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,11 +11,19 @@ concurrency:

jobs:

# The same reusable gate the PR runs - a dispatch cannot release a ref that fails validation.
validate:
name: Validate sources job
uses: ./.github/workflows/validate-task.yml
permissions:
contents: read

# Publish the dispatched branch (main => release, develop => prerelease): NBGV computes the tag from the ref, then
# a GitHub release is created (tag + auto source archive + README + LICENSE). Source-only repo - no build targets.
publish:
name: Publish project release job
runs-on: ubuntu-latest
needs: [ validate ]
permissions:
contents: write

Expand Down
50 changes: 7 additions & 43 deletions .github/workflows/test-pull-request.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,61 +11,25 @@ concurrency:

jobs:

# Source-only repo: CI is lint-only. Markdown, spelling, workflow YAML, and the registry/spec JSON are validated with
# the same configs the editor extensions and CLI use (linter parity). There is no build or unit test.
lint:
name: Lint sources job
runs-on: ubuntu-latest
# The reusable validation gate (validate-task.yml) - the same job publish-release runs before a release.
validate:
name: Validate sources job
uses: ./.github/workflows/validate-task.yml
permissions:
contents: read

steps:

- name: Checkout code step
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

# Doc linters run as pinned action wrappers. editorconfig-checker's action is install-only, so it runs via Docker.
- name: Lint Markdown step
uses: DavidAnson/markdownlint-cli2-action@8de2aa07cae85fd17c0b35642db70cf5495f1d25 # v24.0.0
with:
globs: '**/*.md'

# cspell gate = README + HISTORY only; all-*.md would mean endlessly padding cspell.json for technical terms
# (broad live spell-check is the editor extension's job). See CODESTYLE.md "Markdown and Spelling".
- name: Spell check step
uses: streetsidesoftware/cspell-action@de2a73e963e7443969755b648a1008f77033c5b2 # v8.4.0
with:
files: |
README.md
HISTORY.md
incremental_files_only: false

- name: Lint workflows step
uses: raven-actions/actionlint@3d39aea434753780c3b3d4a1a31c854b4dbf49d7 # v2.2.0

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

- name: Validate registry and spec step
run: |
set -euo pipefail
for f in registry/*.json spec/*.json repo-config/*.json; do
jq empty "$f"
done
python3 spec/validate.py

# GitHub Actions does not support required status checks on conditional jobs, so a single always-run aggregator gates
# the merge. Its name is the ruleset-bound required status-check context - rename it and the ruleset context together.
check-workflow-status:
name: Check pull request workflow status job
runs-on: ubuntu-latest
needs: [ lint ]
needs: [ validate ]
if: always()
steps:
- name: Check workflow results step
run: |
set -euo pipefail
if [[ "${{ needs.lint.result }}" != "success" ]]; then
echo "Job 'lint' did not succeed (${{ needs.lint.result }}); refusing to pass."
if [[ "${{ needs.validate.result }}" != "success" ]]; then
echo "Job 'validate' did not succeed (${{ needs.validate.result }}); refusing to pass."
exit 1
fi
50 changes: 50 additions & 0 deletions .github/workflows/validate-task.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
name: Validate task

# The single validation gate - reused by test-pull-request (feeding the required check) and publish-release.

on:
workflow_call:

jobs:

# Source-only repo: lint-only validation, using the same configs the editor and CLI use (linter parity); no build or tests.
lint:
name: Lint sources job
runs-on: ubuntu-latest
permissions:
contents: read

steps:

- name: Checkout code step
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

# Doc linters run as pinned action wrappers. editorconfig-checker's action is install-only, so it runs via Docker.
- name: Lint Markdown step
uses: DavidAnson/markdownlint-cli2-action@8de2aa07cae85fd17c0b35642db70cf5495f1d25 # v24.0.0
with:
globs: '**/*.md'

# cspell gate = README + HISTORY only; all-*.md would mean endlessly padding cspell.json for technical terms
# (broad live spell-check is the editor extension's job). See CODESTYLE.md "Markdown and Spelling".
- name: Spell check step
uses: streetsidesoftware/cspell-action@de2a73e963e7443969755b648a1008f77033c5b2 # v8.4.0
with:
files: |
README.md
HISTORY.md
incremental_files_only: false

- name: Lint workflows step
uses: raven-actions/actionlint@3d39aea434753780c3b3d4a1a31c854b4dbf49d7 # v2.2.0

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

- name: Validate registry and spec step
run: |
set -euo pipefail
for f in registry/*.json spec/*.json repo-config/*.json; do
jq empty "$f"
done
python3 spec/validate.py
2 changes: 1 addition & 1 deletion WORKFLOW.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -274,7 +274,7 @@ Each type maps the *applicable* S-scenarios onto its targets; the differences ar
- **PyPI library.** The leaf builds + uploads `pypilibrary-build-<branch>`; a **separate** `publish-pypi` job (with `environment: pypi`, `id-token: write`, `actions: write`) does the OIDC Trusted-Publishing upload with `skip-existing: true`, then **consume-then-deletes** the build artifact - **unconditionally on consume**, so on S9 it is deleted even though the `release-asset-*` delete is skipped. The version is `AssemblyFileVersion` with `.dev0` appended on `develop` only, and must stay `--pre`-selectable and sorted above the default release. PyPI contributes no `release-asset-*`; a PyPI-only repo sets `expect_release_assets: false` at the caller. Test: S7 default leg publishes a release, non-default a `.dev0`; S9 is a `skip-existing` no-op; 5C inspects the `dist/*` filenames and the compute-version log.
- **Docker image.** The leaf pushes the default branch multi-arch (amd64+arm64) and any other branch `amd64`-only, with a per-branch registry buildcache (`buildcache-<branch>`; a multi-image repo adds a per-image tag) (`cache-to` only the built branch and only on push, `cache-from` both branches); no `release-asset-*`, so a Docker-only repo's caller passes `expect_release_assets: false`; the readme (`peter-evans/dockerhub-description`, `DOCKER_HUB_ACCESS_TOKEN`) and date-badge jobs run **only** when the default branch publishes; the docker-readme task validates `repositories` XOR `manifest`+`manifest-jq` and a multi-image repo derives its publish matrix from the manifest. Docker **always re-pushes** the image, independently of a skipped release-create (S9). A **wrapper** repo tracks an upstream release: the upstream tracker writes a `name -> version` state file and the merge-bot auto-merges the bump PR (S11), and the leaf MUST read that file for the immutable tag instead of `SemVer2` (the template ships the tracker but not this consumer wiring). Test: S7 default leg pushes `latest` + the version tag and updates readme/badge; non-default pushes the develop tag (amd64 only); S9 still re-pushes; S11 ships the bumped upstream version next publish. 5C Docker probe needs `DOCKER_HUB_*` secrets and same-repo (not fork) runs.
- **Data / asset library.** A single new leaf: validate -> zip -> upload `release-asset-<branch>-library` (`retention-days: 1`, upload gated `!smoke` - mirror the nugetlibrary leaf's shape). Because the template has no such leaf, you **add a target** (D6.4): a new `enable_library` input + `build-library` job + `github-release` `needs:` entry in the release task, and a `library` paths-filter entry + `changes` output + `smoke-build` enable-forward in the PR workflow (without it, D1.1 never smoke-builds the library). Keep `expect_release_assets: true` (it has a file target, unlike Docker). The .NET `unit-test` job is replaced by a type-appropriate validator with the aggregator **and** `smoke-build` both re-pointed to it (D1.2/D1.5); `version.json` + the NBGV `get-version` step are retained (they own the tag). Test: S1 smoke runs validate+zip and uploads nothing; S7 attaches the zip, prerelease on the non-default leg; S9 on a *scheduled* re-run release-create + asset-delete skip (the existing zip is untouched, no registry push), while a `workflow_dispatch` re-run **refreshes** the release and re-runs the asset-delete (the asset is re-uploaded then re-deleted). N/A: the nuget/pypi/docker/executable 5A addenda and their scenario clauses.
- **Source-only / no build.** No package/image leaf: remove all four `build-*` jobs and their `github-release` `needs:` entries (leaving `get-version -> validate-release -> github-release`, which fires on `github && !smoke`), and the caller passes `expect_release_assets: false` so the release is tag + source zip + README + LICENSE with no asset download. With no target the paths-filter matches nothing, so `smoke-build` is **structurally always skipped** - validation is carried solely by the (replaced, non-.NET) validation job that the aggregator and `smoke-build`'s own `needs:` must both point at (D1.2; or drop the never-running `smoke-build` job). NBGV and `version.json` are still retained (they own the tag). Applicable scenarios: S1 (validation only), S5/S6 (publish gating), S7 (tag-only release), S8 (dispatch guard), S9 (no-op republish), S10 (classification gate). N/A: S2-S4 (assume a smoke-built target), the artifact-lifecycle and registry clauses of S7/S9, the D5/D6 artifact items, and all per-type 5A addenda - recorded N/A, not failed.
- **Source-only / no build.** No package/image leaf: remove all four `build-*` jobs and their `github-release` `needs:` entries (leaving `get-version -> validate-release -> github-release`, which fires on `github && !smoke`), and the caller passes `expect_release_assets: false` so the release is tag + source zip + README + LICENSE with no asset download. With no target the paths-filter matches nothing, so `smoke-build` is **structurally always skipped** - validation is carried solely by the (replaced, non-.NET) validation job that the aggregator and `smoke-build`'s own `needs:` must both point at (D1.2; or drop the never-running `smoke-build` job). NBGV and `version.json` are still retained (they own the tag). A standalone dispatch-only publisher gates its release job on the repo's reusable validation task (`needs:` the same `workflow_call` job the PR workflow runs), so a dispatch cannot release a ref that fails validation. Applicable scenarios: S1 (validation only), S5/S6 (publish gating), S7 (tag-only release), S8 (dispatch guard), S9 (no-op republish), S10 (classification gate). N/A: S2-S4 (assume a smoke-built target), the artifact-lifecycle and registry clauses of S7/S9, the D5/D6 artifact items, and all per-type 5A addenda - recorded N/A, not failed.
- **Operational (workflow model, not a build target).** A `workflowModel: operational` repo layers a direct-commit `develop` onto the **source-only** release shape (above). Two workflows: (1) a **lint/validation** PR workflow feeding the required `Check pull request workflow status job` - the generic linters (editorconfig/EOL, markdownlint, cspell, actionlint) plus a domain validator (Home Assistant `hass --script check_config`, `esphome config`, a firmware build), **no unit tests**; its triggers differ from the `release` template - `push` to `develop` (advisory feedback on direct commits) plus `pull_request` to `main` (the enforced promotion gate) plus `workflow_dispatch`. (2) the standard **source-only publisher** on `workflow_dispatch` only (`releaseTrigger: dispatch-only`): NBGV + `version.json` own the tag, and a manual dispatch cuts a GitHub release (tag + source zip + README + LICENSE, `expect_release_assets: false`). Applicable scenarios: S1 (validation) on the promotion PR, plus the source-only release set - S7 (tag-only release), S8 (dispatch guard), S9 (no-op republish), S10 (classification). N/A: the auto-publish paths (S5/S6 bot-push and schedule - operational repos have neither) and every build/registry scenario. See the branch-model note in Section 3 and [AGENTS.md "Branching Model"][agents-branching-model].

<!-- Workflow -->
Expand Down