From 43e01bd7d6fbe540e878c14fe0955255d45300ff Mon Sep 17 00:00:00 2001 From: Matti Airas Date: Thu, 17 Sep 2026 16:38:50 +0300 Subject: [PATCH 1/3] docs(examples): keep only v1 callers Remove the cockpit-apt example of the legacy workflows and pin the docs-repo example to @v1. Co-Authored-By: Claude Opus 5 (1M context) --- .coderabbit.yaml | 9 +++------ .../cockpit-apt/.github/workflows/main.yml | 19 ------------------- examples/cockpit-apt/.github/workflows/pr.yml | 12 ------------ .../cockpit-apt/.github/workflows/release.yml | 15 --------------- .../.github/workflows/translation-status.yml | 9 ++++----- 5 files changed, 7 insertions(+), 57 deletions(-) delete mode 100644 examples/cockpit-apt/.github/workflows/main.yml delete mode 100644 examples/cockpit-apt/.github/workflows/pr.yml delete mode 100644 examples/cockpit-apt/.github/workflows/release.yml diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 14f07fc..1493a58 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -84,12 +84,9 @@ reviews: - path: "examples/**" instructions: | - Examples are copied into calling repositories. `examples/deb` and - `examples/npm` show the v1 interface: flag a call to a legacy workflow - or an `@main` pin there, and a `pr.yml` whose `status` job does not - need every other job. `examples/cockpit-apt` shows the legacy - workflows and `examples/docs-repo` still pins `@main` until the v1 - documentation work in issue 54 replaces them; do not flag either. + Examples are copied into calling repositories. Flag a call to a + legacy workflow, a pin other than `@v1`, and a `status` job that does + not need every other job in its file. knowledge_base: code_guidelines: diff --git a/examples/cockpit-apt/.github/workflows/main.yml b/examples/cockpit-apt/.github/workflows/main.yml deleted file mode 100644 index c72ed03..0000000 --- a/examples/cockpit-apt/.github/workflows/main.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: Main Branch CI/CD - -on: - push: - branches: [main] - -jobs: - build-release: - uses: halos-org/shared-workflows/.github/workflows/build-release.yml@main - with: - package-name: cockpit-apt - package-description: 'Modern Cockpit interface for APT package management on Debian' - # These use defaults, but shown for clarity: - # apt-distro: 'trixie' - # apt-component: 'main' - # test-action: './.github/actions/run-tests' - # build-action: './.github/actions/build-deb' - secrets: - APT_REPO_PAT: ${{ secrets.APT_REPO_PAT }} diff --git a/examples/cockpit-apt/.github/workflows/pr.yml b/examples/cockpit-apt/.github/workflows/pr.yml deleted file mode 100644 index 1d1114a..0000000 --- a/examples/cockpit-apt/.github/workflows/pr.yml +++ /dev/null @@ -1,12 +0,0 @@ -name: Pull Request Checks - -on: - pull_request: - branches: [main] - -jobs: - checks: - uses: halos-org/shared-workflows/.github/workflows/pr-checks.yml@main - # Optional: override defaults - # with: - # test-action: './.github/actions/run-tests' diff --git a/examples/cockpit-apt/.github/workflows/release.yml b/examples/cockpit-apt/.github/workflows/release.yml deleted file mode 100644 index 398aaab..0000000 --- a/examples/cockpit-apt/.github/workflows/release.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: Release Published - -on: - release: - types: [published] - -jobs: - publish: - uses: halos-org/shared-workflows/.github/workflows/publish-stable.yml@main - # Optional: override defaults - # with: - # apt-distro: 'trixie' - # apt-component: 'main' - secrets: - APT_REPO_PAT: ${{ secrets.APT_REPO_PAT }} diff --git a/examples/docs-repo/.github/workflows/translation-status.yml b/examples/docs-repo/.github/workflows/translation-status.yml index 45843b7..70b49b6 100644 --- a/examples/docs-repo/.github/workflows/translation-status.yml +++ b/examples/docs-repo/.github/workflows/translation-status.yml @@ -25,17 +25,16 @@ concurrency: jobs: translation-status: - uses: halos-org/shared-workflows/.github/workflows/translation-status.yml@main + uses: halos-org/shared-workflows/.github/workflows/translation-status.yml@v1 # A reusable-workflow call reports as ` / `, so the # check name is not the caller's to choose and changes if the called job is # ever renamed. Carrying the required context on a plain job of your own - # decouples branch protection from both. Drop this job if you would rather - # require `translation-status / translation-status` directly. + # decouples branch protection from both. status: needs: translation-status - # always(), or a failed gate leaves this skipped, and a skipped required - # check never reports either. GitHub waits on a required context forever. + # always(), or a failed gate skips this job, and GitHub counts a skipped + # required check as passing. if: always() permissions: {} runs-on: ubuntu-latest From 5f6cdaeb5cd2766f8fbbdb6518b7159d1b25d0c1 Mon Sep 17 00:00:00 2001 From: Matti Airas Date: Thu, 17 Sep 2026 16:38:50 +0300 Subject: [PATCH 2/3] docs(readme): document the v1 interface Describe each v1 workflow with input tables taken from its workflow_call, the caller-owned status check, repository requirements, and a migration guide from the legacy workflows. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 589 +++++++++++++++++++++++++++++------------------------- 1 file changed, 314 insertions(+), 275 deletions(-) diff --git a/README.md b/README.md index 2d2b28d..a713220 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,28 @@ # Hat Labs Shared Workflows -Reusable GitHub Actions workflows for two classes of Hat Labs repository: those -that build Debian packages, and documentation sites. +Reusable GitHub Actions workflows for halos-org and hatlabs repositories: pull request checks, Debian package builds, GitHub releases, APT and npm publication, container image update checks, and translated documentation gates. ## Overview -Most of these workflows implement a standardized release process for Debian -packages: +A caller composes the workflows its repository needs. A Debian package repository calls `build-deb.yml` and `apt-publish.yml`; an npm package repository calls `publish-npm.yml`; neither calls the other's. No workflow builds a package unless the caller asks for it. -1. **PR** → Run tests -2. **Merge to main** → Build .deb, create pre-release, dispatch to APT unstable -3. **Publish release** → Dispatch to APT stable +| Caller file | Debian package | npm package | Tag-only | Debian and npm | +|---|---|---|---|---| +| `pr.yml` | `checks`, `build-deb`, `status` | `checks`, `status` | `checks`, `status` | `checks`, `build-deb`, `status` | +| `main.yml` | tests, `release-version` → `build-deb` → `stage-release` → `apt-publish` (unstable) | tests, `release-version` → `stage-release` | tests, `release-version` → `stage-release` | tests, `release-version` → `build-deb` → `stage-release` → `apt-publish` (unstable) | +| `release.yml` | `apt-publish` (stable) | `publish-npm` | none | `apt-publish` (stable), `publish-npm` | -`translation-status.yml` is the exception. It serves translated documentation -repositories and has nothing to do with packaging. +The release flow for every packaged repository: + +1. **Pull request:** tests, version checks, and for Debian packages a build with lintian. +2. **Merge to main:** a pre-release `v+_pre` and a draft `v+`, both tagging the merged commit. Debian packages are attached and dispatched to the APT unstable channel. +3. **Publish the draft:** Debian packages are dispatched to the APT stable channel, and npm packages are published to npm. + +Copy the caller files from `examples/`: [deb](examples/deb/.github/workflows), [npm](examples/npm/.github/workflows), and [docs-repo](examples/docs-repo/.github/workflows) for translated documentation. A tag-only repository uses the npm example's `pr.yml` and `main.yml` and has no `release.yml`. ## Versioning -Releases are tagged `vX.Y.Z`. Each full release also moves the major tag `vX` -to the newest `vX.*.*` release; release candidates (`vX.Y.Z-rc.N`) do not. -Breaking changes to workflow names, inputs, secrets, outputs, or required local -actions ship as a new major. +Releases are tagged `vX.Y.Z`. Each full release also moves the major tag `vX` to the newest `vX.*.*` release; release candidates (`vX.Y.Z-rc.N`) do not. Breaking changes to any of these ship as a new major: workflow names, inputs, secrets and outputs; the required local actions and the arguments passed to local script overrides; the permissions a calling job must grant; the `concurrency` requirement on `main.yml`; and the `VERSION` and tag formats. Pin the major tag: @@ -28,144 +30,202 @@ Pin the major tag: uses: halos-org/shared-workflows/.github/workflows/.yml@v1 ``` -The examples below still reference `@main` until the v1 workflows are released -([issue 49](https://github.com/halos-org/shared-workflows/issues/49)). +Only the workflows documented below are part of `v1`. See [Legacy workflows](#legacy-workflows) for `pr-checks.yml`, `build-release.yml` and `publish-stable.yml`. + +## Required status check + +A reusable-workflow call reports its checks as ` / `, so a check name required by branch protection would change whenever a job in this repository is renamed. Every caller's `pr.yml` therefore ends with a `status` job of its own, and branch protection requires `status` only: + +```yaml + # Branch protection requires only this job. List every other job in needs, + # including jobs defined in this file. + status: + needs: [checks, build-deb] + if: always() + runs-on: ubuntu-latest + permissions: {} + steps: + - name: Require every job to succeed + env: + NEEDS: ${{ toJSON(needs) }} + run: jq -e 'all(.[]; .result == "success")' <<< "$NEEDS" +``` + +`if: always()` makes the job run when a dependency fails. Without it, a failed dependency skips `status`, and GitHub counts a skipped required check as passing, so the pull request could merge. The `jq` filter fails on `skipped` too. If a caller makes one of its jobs conditional, it has to allow `skipped` for that job. ## Workflows -### pr-checks.yml +A called workflow cannot get more token permissions than its calling job grants. Every workflow here needs at least `contents: read`; sections below name the extra permissions a workflow needs. -Runs tests and lintian checks on pull requests. +### checks.yml -```yaml -# .github/workflows/pr.yml -name: Pull Request Checks +Pull request checks for every caller. Jobs: -on: - pull_request: - branches: [main] +- **tests** runs the caller's `.github/actions/run-tests` action. +- **version-check** runs `.github/actions/check-versions` if the caller has it, for example to keep `VERSION` equal to `package.json`. +- **version-bump-check** calls [version-bump-check.yml](#version-bump-checkyml). -jobs: - checks: - uses: halos-org/shared-workflows/.github/workflows/pr-checks.yml@main -``` +| Input | Type | Default | Description | +|---|---|---|---| +| `runs-on` | string | `ubuntu-latest` | Runner to use for every job | -**Inputs:** -| Input | Default | Description | -|-------|---------|-------------| -| `runs-on` | `ubuntu-latest` | Runner to use for tests | -| `skip-lintian` | `false` | Skip lintian checks | +### build-deb.yml -**Jobs:** -1. **tests**: Runs `.github/actions/run-tests/action.yml` -2. **version-check**: Runs `.github/actions/check-versions/action.yml` (if exists) -3. **lintian**: Builds package and runs lintian (if `.github/actions/build-deb/action.yml` exists) +Builds Debian packages with the caller's `.github/actions/build-deb` action and runs lintian on every `.deb` in the repository root or `build/`. The run fails when the build produces no package, and when lintian reports an error or a warning. To suppress a lintian tag, add `debian/.lintian-overrides`. -**Version Checks:** -- Automatically runs if `.github/actions/check-versions/action.yml` exists -- Use to verify VERSION file stays in sync with language-specific version files -- Each repo implements its own version checking logic +The workflow has two modes: -**Lintian Checks:** -- Automatically runs if `.github/actions/build-deb/action.yml` exists -- Fails on errors and warnings -- To suppress specific tags, create `debian/.lintian-overrides` -- Set `skip-lintian: true` to disable +- **Check mode**, from `pr.yml`: call with no release inputs. Nothing is uploaded. +- **Release mode**, from `main.yml`: pass `revision` and every other release input, normally from `release-version.yml`. The workflow writes `debian/changelog` from the commit subjects since the last stable tag, builds, renames `__all.deb` to `__all++.deb`, and uploads every `.deb` as the artifact named in the `artifact` output. -**Required local action**: `.github/actions/run-tests/action.yml` +Passing some release inputs without `revision`, or `revision` without all of them, fails the run. -**Optional local actions**: -- `.github/actions/build-deb/action.yml` (enables lintian checks) -- `.github/actions/check-versions/action.yml` (enables version consistency checks) +| Input | Type | Default | Description | +|---|---|---|---| +| `runs-on` | string | `ubuntu-latest` | Runner to use | +| `lintian` | boolean | `true` | Run lintian on the built packages | +| `revision` | string | `''` | Release mode: revision N. Leave empty for check mode. | +| `upstream-version` | string | `''` | Release mode: upstream version | +| `package-name` | string | `''` | Release mode: Debian package name used in the changelog and default rename | +| `apt-distro` | string | `''` | Release mode: APT distribution for the package name suffix (e.g. `trixie`, `any`) | +| `apt-component` | string | `''` | Release mode: APT component for the package name suffix (e.g. `main`, `hatlabs`) | +| `maintainer-name` | string | `''` | Release mode: maintainer name for `debian/changelog` | +| `maintainer-email` | string | `''` | Release mode: maintainer email for `debian/changelog` | -### build-release.yml +| Output | Description | +|---|---| +| `artifact` | Release mode: name of the uploaded package artifact. Empty in check mode. | -Main branch CI/CD: test, build, release, dispatch. +Repositories with several packages, architecture-specific packages or a non-standard layout replace the changelog and rename steps with [local scripts](#local-script-overrides). -```yaml -# .github/workflows/main.yml -name: Main Branch CI/CD +### release-version.yml -on: - push: - branches: [main] +Reads the upstream version from `VERSION` and computes the revision N for the commit being released. N is one more than the highest N over existing `v+N` and `v+N_pre` tags, or 1 when none exist. When the commit already carries a `_pre` tag, the run reuses that N, so a rerun completes the same release. -jobs: - build-release: - uses: halos-org/shared-workflows/.github/workflows/build-release.yml@main - with: - package-name: my-package - package-description: 'Description for release notes' - secrets: - APT_REPO_PAT: ${{ secrets.APT_REPO_PAT }} -``` +The run fails when: -**Inputs:** -| Input | Default | Description | -|-------|---------|-------------| -| `package-name` | *required* | Debian package name | -| `package-description` | `Debian package` | Short description | -| `apt-distro` | `trixie` | APT distribution | -| `apt-component` | `main` | APT component | -| `apt-repository` | `hatlabs/apt.hatlabs.fi` | APT repo to dispatch to | -| `version-file` | `VERSION` | Path to version file | -| `maintainer-name` | `Hat Labs` | Changelog maintainer | -| `maintainer-email` | `info@hatlabs.fi` | Changelog email | -| `skip-tests` | `false` | Skip test job | - -**Required local actions** (hardcoded paths): -- `.github/actions/run-tests/action.yml` - Test action -- `.github/actions/build-deb/action.yml` - Build action - -**Optional local script overrides** (for multi-package or custom repos): -- `.github/scripts/generate-changelog.sh` - Custom changelog generation -- `.github/scripts/rename-packages.sh` - Custom package renaming -- `.github/scripts/generate-release-notes.sh` - Custom release notes - -If these scripts exist, they will be called instead of the default inlined logic. -See [Local Script Overrides](#local-script-overrides) for details. - -**Secrets:** -| Secret | Description | -|--------|-------------| -| `APT_REPO_PAT` | PAT for dispatching to APT repository | +- the ref is not the default branch; +- `VERSION` does not hold a version such as `1.2.3`; +- a newer commit is already released, for example when an old main run is rerun. -### publish-stable.yml +The caller's `main.yml` must set a `concurrency` group with `cancel-in-progress: false`. Otherwise two concurrent runs compute the same N, and `stage-release.yml` fails the second. -Handles stable release publishing. +| Input | Type | Default | Description | +|---|---|---|---| +| `version-file` | string | `VERSION` | Path to the VERSION file | +| `runs-on` | string | `ubuntu-latest` | Runner to use | -```yaml -# .github/workflows/release.yml -name: Release Published +| Output | Description | +|---|---| +| `upstream-version` | Upstream version from the VERSION file, without a `v` prefix | +| `revision` | Revision number N for this commit | +| `debian-version` | `-` | +| `prerelease-tag` | `v+_pre` | +| `stable-tag` | `v+` | -on: - release: - types: [published] +### stage-release.yml -jobs: - publish: - uses: halos-org/shared-workflows/.github/workflows/publish-stable.yml@main - secrets: - APT_REPO_PAT: ${{ secrets.APT_REPO_PAT }} -``` +Creates `prerelease-tag` as a published pre-release and `stable-tag` as a draft, both tagging the commit the run built, and deletes every other draft release in the repository. Publishing the draft starts the caller's `release.yml`. + +With `artifact`, the packages from `build-deb.yml` are attached to both releases, and the run fails if the artifact holds no `.deb`. Without it, the releases carry notes only. + +A rerun for the same commit keeps a complete existing release and replaces an incomplete one. The run fails when a release with a higher version already exists, or when a release with these tags belongs to another commit. + +The release notes list the commit subjects since the last stable release. When packages are attached and `apt-repository` is set, the notes add installation instructions that link to the APT host. The host is the repository name, so `halos-org/apt.halos.fi` links to `https://apt.halos.fi`. + +The calling job needs `permissions: contents: write`. + +| Input | Type | Default | Description | +|---|---|---|---| +| `package-name` | string | required | Package name shown in the release notes | +| `package-description` | string | `''` | Short description for the stable release notes | +| `debian-version` | string | required | `-`, from `release-version.yml` | +| `prerelease-tag` | string | required | `v+_pre`, from `release-version.yml` | +| `stable-tag` | string | required | `v+`, from `release-version.yml` | +| `artifact` | string | `''` | Artifact with `.deb` packages to attach, from `build-deb.yml`. Empty for notes-only releases. | +| `apt-repository` | string | `''` | APT repository (owner/name, named after its host) mentioned in the notes when packages are attached | +| `apt-distro` | string | `''` | APT distribution mentioned in the notes | +| `apt-component` | string | `''` | APT component mentioned in the notes | +| `runs-on` | string | `ubuntu-latest` | Runner to use | + +### apt-publish.yml + +Sends a `package-updated` repository dispatch to the APT repository, which then fetches the `.deb` assets from this repository's release. + +- `channel: unstable` is called from `main.yml` after `stage-release.yml`, with its `prerelease-tag`. +- `channel: stable` is called from `release.yml` on `release: published`. The job skips pre-releases and requires a `v+` tag. + +On both channels the run fails when the release has no assets, so a notes-only release is never dispatched. There is no default APT repository: hatlabs callers pass `hatlabs/apt.hatlabs.fi`, halos-org callers pass `halos-org/apt.halos.fi`. + +| Input | Type | Default | Description | +|---|---|---|---| +| `channel` | string | required | `unstable` or `stable` | +| `apt-repository` | string | required | APT repository to dispatch to (e.g. `halos-org/apt.halos.fi`) | +| `apt-distro` | string | required | APT distribution (e.g. `trixie`, `any`) | +| `apt-component` | string | required | APT component (e.g. `main`, `hatlabs`) | +| `prerelease-tag` | string | `''` | Unstable channel: the pre-release tag from `release-version.yml` | +| `runs-on` | string | `ubuntu-latest` | Runner to use | + +| Secret | Required | Description | +|---|---|---| +| `APT_REPO_PAT` | yes | Token allowed to send `repository_dispatch` to `apt-repository` | + +### publish-npm.yml + +Publishes the npm package for a published stable release. Call it from `release.yml` on `release: published`; pre-releases are skipped. + +Before building, the run requires that the release commit is on the default branch, the tag is `v+`, `` equals `VERSION` at that commit, and `VERSION` equals the `version` in `package.json`. When that version is already on npm, the run skips with a warning. + +The build job installs dependencies with `--ignore-scripts` (`npm ci` when `package-lock.json` exists), runs the package's `prepublishOnly` script, and packs the tarball. A separate publish job with `id-token: write` publishes that tarball with `--ignore-scripts`, so no dependency code runs next to the publish credentials. + +Publication uses npm trusted publishing: + +- The package's trusted publisher on npmjs.com names the caller's workflow file, `release.yml`, not this one. +- The calling job grants `permissions: contents: read` and `id-token: write`. +- The run needs a GitHub-hosted runner. +- npm adds provenance only for public repositories. + +The publish job uses the concurrency group `-publish-npm-job`. A caller must not use that group name, or the run deadlocks. + +| Input | Type | Default | Description | +|---|---|---|---| +| `version-file` | string | `VERSION` | Path to the VERSION file | +| `runs-on` | string | `ubuntu-latest` | GitHub-hosted runner to use; npm trusted publishing does not support self-hosted runners | + +### version-bump-check.yml -**Inputs:** -| Input | Default | Description | -|-------|---------|-------------| -| `apt-distro` | `trixie` | APT distribution | -| `apt-component` | `main` | APT component | -| `apt-repository` | `hatlabs/apt.hatlabs.fi` | APT repo to dispatch to | -| `version-pattern` | `^v([0-9]+\.[0-9]+\.[0-9]+)\+([0-9]+)$` | Tag validation regex | +Called by `checks.yml`; a caller can also call it on its own. It requires a version bump when a pull request changes files that end up in a package: + +- A change under `apps//` requires the pull request to change that app's `metadata.yaml` too, where the `version` bump goes. The check does not read the version itself. +- A package-affecting change requires `VERSION` to differ from the latest stable release, once per release cycle. + +Documentation, tests, CI configuration, tooling and lockfiles do not count as package-affecting. The workflow file holds the exact list. + +| Input | Type | Default | Description | +|---|---|---|---| +| `runs-on` | string | `ubuntu-latest` | Runner to use | + +### check-image-updates.yml + +For container app repositories. Finds container images in compose files that have a newer version, and opens or updates a pull request with the new versions. The scripts it runs come from the same revision of this repository as the workflow. + +| Input | Type | Default | Description | +|---|---|---|---| +| `compose-pattern` | string | required | Glob for compose files (e.g., `docker-compose.yml`) | +| `branch-name` | string | `chore/update-container-images` | PR branch name | +| `dry-run` | boolean | `false` | Log updates without creating a PR | + +| Secret | Required | Description | +|---|---|---| +| `token` | no | PAT or App token for PR creation (to trigger CI on the PR). Falls back to `github.token` if not provided, but PRs from `github.token` will not trigger other workflows. | + +The calling job needs `permissions: contents: write` and `pull-requests: write`. ### translation-status.yml -For translated documentation repositories, not Debian packages. Reports which -translations are behind their English source, posts that report as a pull -request comment, builds the site, checks its anchors, and fails the run when any -translation is stale, missing, unstamped or orphaned. +For translated documentation repositories, not packages. Reports which translations are behind their English source, posts that report as a pull request comment, builds the site, checks its anchors, and fails the run when any translation is stale, missing, unstamped or orphaned. -Copy the caller from `examples/docs-repo/.github/workflows/translation-status.yml`. -The stanza that selects this workflow is: +Copy the caller from `examples/docs-repo/.github/workflows/translation-status.yml`. The stanza that selects this workflow is: ```yaml # The called workflow inherits this token. Omit pull-requests: write and the @@ -176,200 +236,179 @@ permissions: jobs: translation-status: - uses: halos-org/shared-workflows/.github/workflows/translation-status.yml@main + uses: halos-org/shared-workflows/.github/workflows/translation-status.yml@v1 ``` -**Inputs:** -| Input | Default | Description | -|-------|---------|-------------| -| `runs-on` | `ubuntu-latest` | Runner to use | - -**Requirements** — none of these is validated, so getting one wrong shows up as -a failing step rather than a clear message: -- `pyproject.toml` pins [halos-docs-tools](https://github.com/halos-org/docs-tools) - to a tag, and `uv.lock` is committed. The workflow runs `uv sync --locked`, so - the two must agree. -- `mkdocs` and `mkdocs-static-i18n` are project dependencies. The package brings - the checkers, not mkdocs. -- `mkdocs.yml` configures `mkdocs-static-i18n` with a `docs//` tree, and - leaves `site_dir` at its default — the anchor check reads `site`. -- The caller grants `pull-requests: write` if it wants the comment. +| Input | Type | Default | Description | +|---|---|---|---| +| `runs-on` | string | `ubuntu-latest` | Runner to use | -**What it enforces.** Every translation carries the git blob hash of the English -page it was written against. The checker compares hashes; it cannot read the -translated text. A commit that only rewrites the stamp therefore turns the gate -green and makes that page's staleness permanently invisible — and the bot -comment prints the hash needed to do it, because that is also what an honest -update needs. Catching a stamp-only diff is a reviewer's job. - -**Making it a gate.** Nothing here makes the check required; that is a branch -protection rule in the consuming repository. The check is named ` / -translation-status`, which is a name a caller cannot choose freely: a -reusable-workflow call always reports as ` / `. A -repository that already requires some other context, or that would rather not -tie its protection to a job name in this repository, can carry the name on a -job of its own that depends on this one — see the caller example. Two more -things to know before enabling it: the example deliberately carries no `paths` -filter, because a required check that never runs on a PR touching none of the -filtered paths leaves that PR unmergeable forever; -and the gate reads the repository as it stood when the run started, so two -independently green PRs can merge into a stale `main`. Require branches to be up -to date before merging, or use a merge queue — which needs a `merge_group` -trigger the example does not have. - -The checkers come from the package, so the same commands run on a laptop before -you push. Glossaries and per-language rules stay in the documentation -repository. - -This workflow builds and runs pull request code, including code from forks, so -callers should leave it on GitHub-hosted runners and must not switch the trigger -to `pull_request_target`. - -A repository without translations has no use for this workflow — the status -checker needs the i18n configuration to know what to compare. Such a repository -consumes the package directly from its own build job instead, for example to run -`check-anchors` on the built site. - -`hatlabs` documentation repositories call this copy rather than the one in -`hatlabs/shared-workflows`. The one-org-per-copy rule exists to keep the APT -inputs straight, and this workflow has none; a single copy is deliberate. - -## Repository Requirements (Debian package workflows) - -Each repository using `pr-checks.yml`, `build-release.yml` or -`publish-stable.yml` must have the following. A documentation repository calling -`translation-status.yml` needs none of them — its requirements are listed in -that workflow's section above. - -### 1. VERSION file +**Requirements.** None of these is validated, so getting one wrong shows up as a failing step rather than a clear message: -``` -0.2.0 -``` +- `pyproject.toml` pins [halos-docs-tools](https://github.com/halos-org/docs-tools) to a tag, and `uv.lock` is committed. The workflow runs `uv sync --locked`, so the two must agree. +- `mkdocs` and `mkdocs-static-i18n` are project dependencies. The package brings the checkers, not mkdocs. +- `mkdocs.yml` configures `mkdocs-static-i18n` with a `docs//` tree, and leaves `site_dir` at its default. The anchor check reads `site`. +- The caller grants `pull-requests: write` if it wants the comment. -Plain version number, no `v` prefix. +**What it enforces.** Every translation carries the git blob hash of the English page it was written against. The checker compares hashes; it cannot read the translated text. A commit that only rewrites the stamp therefore turns the gate green and makes that page's staleness permanently invisible. The bot comment prints the hash needed to do it, because an honest update needs the same hash. Catching a stamp-only diff is a reviewer's job. -### 2. Test action (`.github/actions/run-tests/action.yml`) +**Making it a gate.** The example's `status` job carries the required check, as described in [Required status check](#required-status-check). The example deliberately carries no `paths` filter, because a required check that never runs on a PR touching none of the filtered paths leaves that PR unmergeable forever. The gate reads the repository as it stood when the run started, so two independently green PRs can merge into a stale `main`. Require branches to be up to date before merging, or use a merge queue, which needs a `merge_group` trigger the example does not have. -```yaml -name: 'Run Tests' -description: 'Run all tests' -runs: - using: 'composite' - steps: - - name: Run tests - run: ./run test - shell: bash -``` +The checkers come from the package, so the same commands run on a laptop before you push. Glossaries and per-language rules stay in the documentation repository. -### 3. Build action (`.github/actions/build-deb/action.yml`) +This workflow builds and runs pull request code, including code from forks, so callers should leave it on GitHub-hosted runners and must not switch the trigger to `pull_request_target`. -```yaml -name: 'Build Debian Package' -description: 'Build .deb package' -runs: - using: 'composite' - steps: - - name: Build - run: dpkg-buildpackage -us -uc -b - shell: bash -``` +A repository without translations has no use for this workflow, because the status checker needs the i18n configuration to know what to compare. Such a repository consumes the package directly from its own build job instead, for example to run `check-anchors` on the built site. -### 4. debian/ directory +## Repository requirements -Standard Debian packaging files. The `debian/changelog` will be auto-generated. +Every caller of `checks.yml`: -### 5. Repository secret: `APT_REPO_PAT` +- **`.github/actions/run-tests/action.yml`**, a composite action that runs the tests: -Personal Access Token with permission to trigger repository dispatch on the APT repository. + ```yaml + name: 'Run Tests' + description: 'Run all tests' + runs: + using: 'composite' + steps: + - name: Run tests + run: ./run test + shell: bash + ``` -## Version Management +- **`.github/actions/check-versions/action.yml`**, optional, for version consistency checks. -- **VERSION file**: Contains upstream version (e.g., `0.2.0`) -- **Git tags**: Auto-generated as `v{version}+{N}` or `v{version}+{N}_pre` -- **Revision (N)**: Auto-incremented based on existing tags +Every caller of `release-version.yml`: -### Version Progression Example +- **`VERSION`** holding the upstream version, such as `0.2.0`, without a `v` prefix. -``` -Push to main (VERSION=0.2.0, first time): - → Creates v0.2.0+1_pre (pre-release) - → Creates v0.2.0+1 (draft) +Debian package repositories also need: -Push to main again (same VERSION): - → Creates v0.2.0+2_pre (pre-release) - → Creates v0.2.0+2 (draft) +- **`.github/actions/build-deb/action.yml`**, a composite action that writes the `.deb` files to the repository root or `build/`: -Bump VERSION to 0.3.0, push to main: - → Creates v0.3.0+1_pre (pre-release) - → Creates v0.3.0+1 (draft) -``` + ```yaml + name: 'Build Debian Package' + description: 'Build .deb package' + runs: + using: 'composite' + steps: + - name: Build + run: dpkg-buildpackage -us -uc -b && mv ../*.deb . + shell: bash + ``` -## Migration Guide +- **`debian/`** with standard packaging files. In release mode `build-deb.yml` overwrites `debian/changelog`. +- **Repository secret `APT_REPO_PAT`**, a token allowed to send `repository_dispatch` to the APT repository. -To migrate an existing repository: +npm package repositories also need: -1. **Create local actions** if not present: - - `.github/actions/run-tests/action.yml` - - `.github/actions/build-deb/action.yml` +- **`package.json`** whose `version` equals `VERSION`. +- **A trusted publisher** for the package on npmjs.com that names this repository and the workflow file `release.yml`. -2. **Replace workflow files**: - ```bash - # Backup existing workflows - mv .github/workflows/pr.yml .github/workflows/pr.yml.bak - mv .github/workflows/main.yml .github/workflows/main.yml.bak - mv .github/workflows/release.yml .github/workflows/release.yml.bak - ``` +## Version management -3. **Copy caller templates** from `examples/` and customize. +- **`VERSION`** holds the upstream version, for example `0.2.0`. +- **Git tags** are `v+` for stable releases and `v+_pre` for pre-releases. +- **The Debian version** is `-`. -4. **Scripts**: For simple single-package repos, you can remove old scripts (now inlined). - For multi-package repos, keep the scripts - they'll be used as overrides. +``` +Merge to main (VERSION=0.2.0, first time): + → v0.2.0+1_pre (pre-release) + → v0.2.0+1 (draft) -5. **Test** with a PR before merging. +Merge to main again (same VERSION): + → v0.2.0+2_pre (pre-release) + → v0.2.0+2 (draft; replaces the v0.2.0+1 draft) -## Local Script Overrides +Bump VERSION to 0.3.0, merge to main: + → v0.3.0+1_pre (pre-release) + → v0.3.0+1 (draft) +``` -For repos with non-standard structures (e.g., multiple packages, subdirectories), provide local scripts that the shared workflow will call instead of the default inlined logic. +## Local script overrides -### generate-changelog.sh +Repositories with several packages or a non-standard layout can provide scripts that replace a default step. Each script runs from the repository root. -Called with: `--upstream --revision ` +| Script | Replaces | Called by | Arguments | +|---|---|---|---| +| `.github/scripts/generate-changelog.sh` | `debian/changelog` generation | `build-deb.yml`, release mode | `--upstream --revision ` | +| `.github/scripts/rename-packages.sh` | package rename | `build-deb.yml`, release mode | `--version --distro --component ` | +| `.github/scripts/generate-release-notes.sh` | release notes | `stage-release.yml` | ` prerelease\|draft`; writes `release_notes.md` | + +Example `rename-packages.sh` for a repository with two packages: -Example for multi-package repo: ```bash #!/bin/bash -# Generate changelogs for multiple packages +set -euo pipefail +while [ $# -gt 0 ]; do + case $1 in + --version) VERSION=$2 ;; + --distro) DISTRO=$2 ;; + --component) COMPONENT=$2 ;; + esac + shift 2 +done for pkg in halos halos-marine; do - cat > ${pkg}/debian/changelog < $(date -R) -EOF + mv "${pkg}_${VERSION}_all.deb" "${pkg}_${VERSION}_all+${DISTRO}+${COMPONENT}.deb" done ``` -### rename-packages.sh +## Migrating from `@main` -Called with: `--version --distro --component ` +A caller of the legacy workflows migrates in one pull request: -Example: -```bash -#!/bin/bash -# Rename multiple packages -for pkg in halos halos-marine; do - OLD="${pkg}_${VERSION}_all.deb" - NEW="${pkg}_${VERSION}_all+${DISTRO}+${COMPONENT}.deb" - [ -f "$OLD" ] && mv "$OLD" "$NEW" -done -``` +1. Replace `pr.yml`, `main.yml` and `release.yml` with the example for the repository kind, and carry the repository's values over with the tables below. +2. A hatlabs repository that called `hatlabs/shared-workflows` now calls `halos-org/shared-workflows` and passes `apt-repository: hatlabs/apt.hatlabs.fi` explicitly. +3. Keep existing local actions and `.github/scripts/` overrides; their contracts are unchanged. +4. When the pull request is green, change branch protection to require `status` instead of the old check names, and merge. An open pull request gets a `status` check at its next push or rebase. +5. Watch the first main run through `apt-publish`, or through `stage-release` for npm and tag-only repositories. The migration pull request never runs `main.yml`, because `release-version.yml` refuses other branches. If the run fails, fix forward; a rerun for the same commit resumes the release. +6. Check the first published draft's APT stable dispatch or npm publish; that run is the first of the new `release.yml`. + +The workflow files do not end up in a package, so the migration needs no `VERSION` bump. + +### pr-checks.yml + +| Legacy input | v1 | +|---|---| +| `runs-on` | `runs-on` on `checks.yml` and `build-deb.yml`. The legacy lintian job ignored it and ran on `ubuntu-latest`. | +| `skip-lintian: true` | do not call `build-deb.yml`; the legacy job skipped the whole PR build. For a PR build without lintian, call it with `lintian: false`. | +| no `.github/actions/build-deb` (lintian skipped itself) | do not call `build-deb.yml` | + +Legacy lintian checked only `*.deb` in the repository root. `build-deb.yml` also checks `build/`, so a repository that builds there is linted for the first time; fix or override the findings in the migration pull request. + +### build-release.yml + +| Legacy input | v1 | +|---|---| +| `package-name` | `package-name` on `build-deb.yml` and `stage-release.yml` | +| `package-description` (default `Debian package`) | `package-description` on `stage-release.yml` (default empty) | +| `apt-distro`, `apt-component` | same inputs on `build-deb.yml`, `stage-release.yml` and `apt-publish.yml` | +| `apt-repository` (default `halos-org/apt.halos.fi`) | `apt-repository` on `stage-release.yml` and `apt-publish.yml`; required on `apt-publish.yml` | +| `version-file` | `version-file` on `release-version.yml`, and on `publish-npm.yml` for npm packages | +| `maintainer-name`, `maintainer-email` (defaults `Hat Labs`, `info@hatlabs.fi`) | same inputs on `build-deb.yml`, with no defaults. A caller that relied on the legacy defaults now passes values. | +| `runs-on` | `runs-on` on each called workflow | +| `skip-tests: true` | omit the `tests` job and the `needs: tests` in `main.yml` | +| `build-deb: false` | do not call `build-deb.yml` or `apt-publish.yml` | +| `run-lintian: false` | `lintian: false` on `build-deb.yml` | +| secret `APT_REPO_PAT` | secret `APT_REPO_PAT` on `apt-publish.yml` | + +### publish-stable.yml + +| Legacy input | v1 | +|---|---| +| `apt-distro`, `apt-component`, `apt-repository` | same inputs on `apt-publish.yml` with `channel: stable` | +| `version-pattern` | removed; the tag must be `v+` | +| secret `APT_REPO_PAT` | secret `APT_REPO_PAT` on `apt-publish.yml` | + +### Repository-local npm release workflows -### generate-release-notes.sh +An npm repository with its own `release.yml` replaces its jobs with a call to `publish-npm.yml`, as in the npm example. The file name stays `release.yml`, so the package's trusted publisher needs no change. The workflow reads the package name from `package.json`. -Called with: ` ` +### Other workflows -Where `release-type` is `prerelease` or `draft`. Must write to `release_notes.md`. +Callers of `version-bump-check.yml`, `check-image-updates.yml` and `translation-status.yml` change the ref from `@main` to `@v1`. Callers of `hatlabs/shared-workflows` also change the owner to `halos-org`. -## Examples +## Legacy workflows -See `examples/` directory for complete caller workflow examples. +`pr-checks.yml`, `build-release.yml` and `publish-stable.yml` serve callers that still reference `@main`. They are frozen and are not part of the `v1` interface. They are deleted once the last `@main` caller has migrated, which is the one exception to shipping breaking changes as a new major. Progress: [issue 49](https://github.com/halos-org/shared-workflows/issues/49). From 2ebfb66a0487a2d3c34cecf928d0d4ce213e297e Mon Sep 17 00:00:00 2001 From: Matti Airas Date: Thu, 17 Sep 2026 16:44:24 +0300 Subject: [PATCH 3/3] docs(agents): list every caller-facing contract Script override arguments, required permissions, and the VERSION and tag formats also break callers when changed. Co-Authored-By: Claude Opus 5 (1M context) --- AGENTS.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 60f9472..1a5430a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,7 +14,11 @@ A caller depends on: - workflow filenames under `.github/workflows/` - `workflow_call` inputs, secrets, and outputs -- the repo-local actions a caller must provide (`.github/actions/run-tests`, `build-deb`, `check-versions`) +- the repo-local actions a caller provides: `.github/actions/run-tests`, `build-deb` for Debian packages, and the optional `check-versions` +- the arguments passed to optional `.github/scripts/` overrides, and the files they must write +- the permissions a calling job must grant +- the `concurrency` group with `cancel-in-progress: false` that a caller's `main.yml` must set, because `release-version.yml` computes the revision from existing tags +- the `VERSION` file and the `v+` and `v+_pre` tag formats Changing any of these incompatibly is a breaking change and ships as a new major version.