diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 8dd02581..0f6c6159 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -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 diff --git a/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml index 0342e8b3..dc4c1c20 100644 --- a/.github/workflows/test-pull-request.yml +++ b/.github/workflows/test-pull-request.yml @@ -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 diff --git a/.github/workflows/validate-task.yml b/.github/workflows/validate-task.yml new file mode 100644 index 00000000..e66644d3 --- /dev/null +++ b/.github/workflows/validate-task.yml @@ -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 diff --git a/WORKFLOW.md b/WORKFLOW.md index 37d97ce1..e3f2f43e 100644 --- a/WORKFLOW.md +++ b/WORKFLOW.md @@ -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-`; 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-`; 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--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].