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.
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.
| 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 |
The release flow for every packaged repository:
- Pull request: tests, version checks, and for Debian packages a build with lintian.
- Merge to main: a pre-release
v<upstream>+<N>_preand a draftv<upstream>+<N>, both tagging the merged commit. Debian packages are attached and dispatched to the APT unstable channel. - 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, npm, and docs-repo for translated documentation. A tag-only repository uses the npm example's pr.yml and main.yml and has no release.yml.
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:
uses: halos-org/shared-workflows/.github/workflows/<workflow>.yml@v1Only the workflows documented below are part of v1. See Legacy workflows for pr-checks.yml, build-release.yml and publish-stable.yml.
A reusable-workflow call reports its checks as <caller job> / <called job>, 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:
# 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.
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.
Pull request checks for every caller. Jobs:
- tests runs the caller's
.github/actions/run-testsaction. - version-check runs
.github/actions/check-versionsif the caller has it, for example to keepVERSIONequal topackage.json. - version-bump-check calls version-bump-check.yml.
| Input | Type | Default | Description |
|---|---|---|---|
runs-on |
string | ubuntu-latest |
Runner to use for every job |
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/<package>.lintian-overrides.
The workflow has two modes:
- Check mode, from
pr.yml: call with no release inputs. Nothing is uploaded. - Release mode, from
main.yml: passrevisionand every other release input, normally fromrelease-version.yml. The workflow writesdebian/changelogfrom the commit subjects since the last stable tag, builds, renames<package-name>_<debian-version>_all.debto<package-name>_<debian-version>_all+<distro>+<component>.deb, and uploads every.debas the artifact named in theartifactoutput.
Passing some release inputs without revision, or revision without all of them, fails the run.
| 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 |
| Output | Description |
|---|---|
artifact |
Release mode: name of the uploaded package artifact. Empty in check mode. |
Repositories with several packages, architecture-specific packages or a non-standard layout replace the changelog and rename steps with local scripts.
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<upstream>+N and v<upstream>+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.
The run fails when:
- the ref is not the default branch;
VERSIONdoes not hold a version such as1.2.3;- a newer commit is already released, for example when an old main run is rerun.
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.
| Input | Type | Default | Description |
|---|---|---|---|
version-file |
string | VERSION |
Path to the VERSION file |
runs-on |
string | ubuntu-latest |
Runner to use |
| Output | Description |
|---|---|
upstream-version |
Upstream version from the VERSION file, without a v prefix |
revision |
Revision number N for this commit |
debian-version |
<upstream>-<N> |
prerelease-tag |
v<upstream>+<N>_pre |
stable-tag |
v<upstream>+<N> |
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 | <upstream>-<N>, from release-version.yml |
prerelease-tag |
string | required | v<upstream>+<N>_pre, from release-version.yml |
stable-tag |
string | required | v<upstream>+<N>, 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 |
Sends a package-updated repository dispatch to the APT repository, which then fetches the .deb assets from this repository's release.
channel: unstableis called frommain.ymlafterstage-release.yml, with itsprerelease-tag.channel: stableis called fromrelease.ymlonrelease: published. The job skips pre-releases and requires av<upstream>+<N>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 |
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<upstream>+<N>, <upstream> 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: readandid-token: write. - The run needs a GitHub-hosted runner.
- npm adds provenance only for public repositories.
The publish job uses the concurrency group <repository>-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 |
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/<name>/requires the pull request to change that app'smetadata.yamltoo, where theversionbump goes. The check does not read the version itself. - A package-affecting change requires
VERSIONto 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 |
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.
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:
# The called workflow inherits this token. Omit pull-requests: write and the
# run still gates; only the comment is skipped.
permissions:
contents: read
pull-requests: write
jobs:
translation-status:
uses: halos-org/shared-workflows/.github/workflows/translation-status.yml@v1| Input | Type | Default | Description |
|---|---|---|---|
runs-on |
string | 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.tomlpins halos-docs-tools to a tag, anduv.lockis committed. The workflow runsuv sync --locked, so the two must agree.mkdocsandmkdocs-static-i18nare project dependencies. The package brings the checkers, not mkdocs.mkdocs.ymlconfiguresmkdocs-static-i18nwith adocs/<locale>/tree, and leavessite_dirat its default. The anchor check readssite.- The caller grants
pull-requests: writeif it wants the comment.
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.
Making it a gate. The example's status job carries the required check, as described in 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.
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, 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.
Every caller of checks.yml:
-
.github/actions/run-tests/action.yml, a composite action that runs the tests:name: 'Run Tests' description: 'Run all tests' runs: using: 'composite' steps: - name: Run tests run: ./run test shell: bash
-
.github/actions/check-versions/action.yml, optional, for version consistency checks.
Every caller of release-version.yml:
VERSIONholding the upstream version, such as0.2.0, without avprefix.
Debian package repositories also need:
-
.github/actions/build-deb/action.yml, a composite action that writes the.debfiles to the repository root orbuild/:name: 'Build Debian Package' description: 'Build .deb package' runs: using: 'composite' steps: - name: Build run: dpkg-buildpackage -us -uc -b && mv ../*.deb . shell: bash
-
debian/with standard packaging files. In release modebuild-deb.ymloverwritesdebian/changelog. -
Repository secret
APT_REPO_PAT, a token allowed to sendrepository_dispatchto the APT repository.
npm package repositories also need:
package.jsonwhoseversionequalsVERSION.- A trusted publisher for the package on npmjs.com that names this repository and the workflow file
release.yml.
VERSIONholds the upstream version, for example0.2.0.- Git tags are
v<upstream>+<N>for stable releases andv<upstream>+<N>_prefor pre-releases. - The Debian version is
<upstream>-<N>.
Merge to main (VERSION=0.2.0, first time):
→ v0.2.0+1_pre (pre-release)
→ v0.2.0+1 (draft)
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)
Bump VERSION to 0.3.0, merge to main:
→ v0.3.0+1_pre (pre-release)
→ v0.3.0+1 (draft)
Repositories with several packages or a non-standard layout can provide scripts that replace a default step. Each script runs from the repository root.
| Script | Replaces | Called by | Arguments |
|---|---|---|---|
.github/scripts/generate-changelog.sh |
debian/changelog generation |
build-deb.yml, release mode |
--upstream <version> --revision <N> |
.github/scripts/rename-packages.sh |
package rename | build-deb.yml, release mode |
--version <debian-version> --distro <distro> --component <component> |
.github/scripts/generate-release-notes.sh |
release notes | stage-release.yml |
<debian-version> <tag> prerelease|draft; writes release_notes.md |
Example rename-packages.sh for a repository with two packages:
#!/bin/bash
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
mv "${pkg}_${VERSION}_all.deb" "${pkg}_${VERSION}_all+${DISTRO}+${COMPONENT}.deb"
doneA caller of the legacy workflows migrates in one pull request:
- Replace
pr.yml,main.ymlandrelease.ymlwith the example for the repository kind, and carry the repository's values over with the tables below. - A hatlabs repository that called
hatlabs/shared-workflowsnow callshalos-org/shared-workflowsand passesapt-repository: hatlabs/apt.hatlabs.fiexplicitly. - Keep existing local actions and
.github/scripts/overrides; their contracts are unchanged. - When the pull request is green, change branch protection to require
statusinstead of the old check names, and merge. An open pull request gets astatuscheck at its next push or rebase. - Watch the first main run through
apt-publish, or throughstage-releasefor npm and tag-only repositories. The migration pull request never runsmain.yml, becauserelease-version.ymlrefuses other branches. If the run fails, fix forward; a rerun for the same commit resumes the release. - 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.
| 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.
| 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, [email protected]) |
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 |
| 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<upstream>+<N> |
secret APT_REPO_PAT |
secret APT_REPO_PAT on apt-publish.yml |
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.
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.
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.