The CI setup action and the zizmor, DCO, changeset, release pull request, release and update workflows every zeabur-oss repository calls, the shared Renovate preset and the shared agent skills. A repository keeps what is its own in its CI workflow and scripts; what is here only checks out, installs Nix, caches and uploads.
A repository's .github/workflows/ci.yml lists its jobs. Each one checks out, runs
actions/ci-setup and then nix develop -c scripts/ci/<job>.sh (nix develop .#<shell> for
another dev shell), so a job runs the same way locally. Each scripts/ci/<job>.sh is plain bash
that starts with set -euo pipefail and cd "$(dirname "$0")/../..".
actions/ci-setupinstalls Nix. Withrust-cache: trueit also caches~/.cargoandtarget/per job with Swatinem/rust-cache, keyed on the dev shell'srustc. Only runs onmainsave it; a cache saved on a pull request or merge queue ref can't be read from any other ref. The scheduled run keeps the caches of the merge-queue-only jobs current.- Jobs that need a minute or less of work share a runner:
lint.shruns the formatters, linters and other short checks one after another. - Slow jobs run only in the merge queue, on a schedule, a manual run or a tag, with
if: github.event_name != 'pull_request' && !(github.event_name == 'push' && github.ref_type == 'branch'). - The job
ok(namedci / ok, the check rulesets require) needs every other job, runsif: always()and fails when one of them failed or was cancelled. zizmor.ymlruns zizmor on the caller's.github.
One entrypoint per stage, bun scripts/release.ts <stage>, run in the dev shell of the commit being released (so the dev shell has Bun):
| Stage | Arguments | Does |
|---|---|---|
check |
<version> |
The version is the manifests'. The workflow then checks the CHANGELOG.md section. |
build |
<version> <os> <arch> <dist> |
Builds one platform into <dist> and smokes what it built. |
sign |
<version> <os> <arch> <dist> |
Only with signing-environment, on macOS: signs <dist>'s build with the Developer ID and notarizes it; leaves it as it is without the APPLE_* secrets. |
publish |
<version> <artifacts> <published> |
Pushes by digest and signs; writes <published>/subjects.json, [{"name": ..., "digest": "sha256:..."}] ([] when nothing is pushed). |
finish |
<version> <os> <arch> <artifacts> <dist> |
Only with finish: true: per platform, what needs the published digests, such as an installer. |
assets |
<version> <artifacts> <assets> |
Copies the release's files into <assets>. The workflow adds the signed SHA256SUMS and the notes. |
verify |
<version> <os> <arch> <downloaded> |
Checks the draft's images and charts. The workflow has checked the assets and SHA256SUMS first. |
Inputs of the shared release.yml: name, docs (the documentation the notes link to), version, platforms (JSON array of
{os, arch, runner}, also the platforms a draft is verified on), finish with finish-platforms
(default platforms), runner, signing-environment and telemetry-key. build may leave logs in $CI_ARTIFACTS; they
are uploaded when it fails.
signing-environment names an environment whose deployment branches are main and v* tags,
holding APPLE_CERTIFICATE_P12 (the Developer ID Application certificate and key, base64),
APPLE_CERTIFICATE_PASSWORD, and an App Store Connect API key for notarization: APPLE_API_KEY_P8
(the file's text), APPLE_API_KEY_ID and APPLE_API_ISSUER_ID. The caller passes the five by
name. macOS builds of main and tags run in it; others build unsigned.
<artifacts> holds one directory per earlier artifact: candidate-<os>-<arch>, published and
finish-<os>-<arch>.
zeabur-oss-release pushes images and charts by digest, signs, writes and
signs SHA256SUMS, verifies a release and copies the release notes from CHANGELOG.md. A
repository depends on it as @zeabur-oss/release in its package.json, pinned in its
bun.lock, and imports it in its scripts/release.ts stages; the workflow runs notes,
checksums and verify-assets itself, from the same package.
zeabur-oss-unsafe-report <package>..., the second command of @zeabur-oss/release, lists the shipped
dependencies of those packages that have unsafe code or link native code. It writes
target/unsafe-report.md and fails on any that the repository's
supply-chain/unsafe-allowlist.toml does not approve, and on any entry there whose crate and kind
the shipped graph doesn't need: a crate that comes back needs a new approval. A Rust repository runs
node_modules/.bin/zeabur-oss-unsafe-report from its scripts/ci/lint.sh; Cargo comes from the caller's
dev shell.
A repository has a knope.toml with a prepare-release workflow: its PrepareRelease step turns
the change files in .changeset/ into the next ## X.Y.Z (YYYY-MM-DD) section of CHANGELOG.md
and sets the version in the file versioned_files names; a Command step with
variables = { "$version" = "Version" } may set it in the other manifests. changeset.yml checks
every pull request: a feat, fix or perf title, or any with !, needs a change file or the
label changelog: none, and each change file must name a package of knope.toml with major,
minor, patch or a type of the package's extra_changelog_sections (such as security for a
### Security section, which knope releases as a patch) and start with a # summary. release-pr.yml, run by hand, runs knope,
bun scripts/release.ts check (unless check: false), and opens chore(release): vX.Y.Z into
the branch it ran on (main or a maintenance branch maint/vX.Y) as zeabur-oss-bot, from
release/next, one at a time; its inputs are version,
prerelease (a label such as rc), check and runner. On main or maint/vX.Y, the merged
release pull request is tagged by the bot, whose tag starts the repository's release.
maint/vX.Y takes only vX.Y.* tags. Both run zeabur-oss-release from the caller's bun.lock.
The repository needs the label changelog: none, and a tag ruleset that lets zeabur-oss-bot create
v* tags while nobody may move or delete them.
actions/bot-pr opens or updates a pull request with the working tree's changes as
zeabur-oss-bot, whose pull requests start CI. Pass client-id: ${{ vars.ZEABUR_OSS_BOT_CLIENT_ID }},
private-key, branch, title and optionally body, add-paths and workflows: "true" when the
changes touch .github/workflows (the bot's Workflows write permission); the job needs no write
permission of its own.
update.yml opens one pull request, update/zeabur-oss, as zeabur-oss-bot. It moves every pin of
this repository to one commit: the flake input in flake.lock (its locked revision is the commit
for the rest), each uses: under .github, and @zeabur-oss/release in package.json with
bun install. Then it copies each skill in .claude/skills that skills/ also has, by
directory name, again from that commit. Nothing is dispatched on a push to main; a
caller runs it on Monday 02:30 in Asia/Taipei, or by hand with workflow_dispatch when a change is
urgent. Renovate leaves these pins alone. This repository and each caller need the bot installed,
ZEABUR_OSS_BOT_PRIVATE_KEY and ZEABUR_OSS_BOT_CLIENT_ID visible to them, and Workflows write for
the bot, since most pins are under .github/workflows.
default.json leaves third-party updates to Renovate, daily between 03:00 and 06:59 in
Asia/Taipei, one pull request per update. Lock file maintenance runs in the same hours on Monday.
updateNotScheduled is false, so updates to existing branches wait for the schedule too. Per the
Renovate documentation, a vulnerability-fix pull request ignores schedule and prHourlyLimit.
A repository's own groups and package rules override these.
Third-party releases wait seven days (minimumReleaseAge); our own, @zeabur-oss/* on npm and
zeabur-oss/* tags and releases, move as soon as they are published. A repository that pins the
rancher/k3s test image or documents INSTALL_K3S_VERSION also extends
github>zeabur-oss/.github:k3s and enables custom.regex: k3s.json moves both with K3s's
stable channel, without the wait.
Keyless signatures and attestations name the workflow that ran, which is this repository's
release.yml, not the caller's. Verify a release of zeabur-oss/<repo> with:
cosign verify <image>@<digest> \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp '^https://github\.com/zeabur-oss/\.github/\.github/workflows/release\.yml@' \
--certificate-github-workflow-repository zeabur-oss/<repo> \
--certificate-github-workflow-ref refs/tags/<version>
gh attestation verify <file> --repo zeabur-oss/<repo> \
--signer-workflow zeabur-oss/.github/.github/workflows/release.ymlThe verify stage gets these values in RELEASE_CERT_IDENTITY_REGEXP, RELEASE_SIGNER_WORKFLOW,
RELEASE_SOURCE_REPOSITORY and RELEASE_SOURCE_REF.
If the verify job fails on a draft, fix the check and run it again from a checkout, in the dev
shell, after bun install:
export RELEASE_CERT_IDENTITY_REGEXP='^https://github\.com/zeabur-oss/\.github/\.github/workflows/release\.yml@' \
RELEASE_SIGNER_WORKFLOW=zeabur-oss/.github/.github/workflows/release.yml \
RELEASE_SOURCE_REPOSITORY=zeabur-oss/<repo> RELEASE_SOURCE_REF=refs/tags/<version>
gh release download <version> --repo zeabur-oss/<repo> --dir downloaded
node_modules/.bin/zeabur-oss-release verify-assets downloaded
bun scripts/release.ts verify <version> linux arm64 downloadedskills/ holds the agent skills every zeabur-oss repository shares, in the
Agent Skills format. To use one, copy its directory into the
repository's .claude/skills/; the Update workflow keeps the copy current. A repository's own
skill keeps its commands, paths and terms, has a name not used here, and points to the shared skill
for the rest.
| Skill | Use it to |
|---|---|
zeabur-writing |
Write or review docs, interface strings and translations in English, zh-TW and zh-CN, with a Vale style that checks them |
zeabur-oss-workflow |
Branch, commit with a DCO sign-off, open and squash-merge pull requests, release, file issues |
zeabur-oss-dependencies |
Add or update a Rust crate or Bun package under the shared supply-chain policy |
zeabur-oss-spec-work |
Implement security- or protocol-relevant behaviour from the specification, with citations and adversarial tests |
zeabur-oss-cluster-tests |
Run and write Kubernetes integration and end-to-end tests on a disposable cluster in a VM |
zeabur-oss-ci |
Understand the shared CI and release workflows and investigate a failed run |
zeabur-oss-rust |
Follow the conventions every zeabur-oss Rust workspace shares |
zeabur-oss-standards |
Review a change against the shared engineering standards |
no-ai-flavour |
Keep code comments, copy and visuals free of the generic AI-made feel |
story-writing |
Write or revise a blog post, launch note, technical article or other long-form piece |
zeabur-writing ships a Vale style in
skills/zeabur-writing/vale. To check a repository's text with it,
copy vale.ini to .vale.ini at the repository root with
StylesPath = .claude/skills/zeabur-writing/vale, and put Vale in the dev shell. .mdx files
also need mdx2vast, which nixpkgs does not have; this flake builds it:
packages = [ pkgs.vale inputs.oss.packages.${system}.mdx2vast ];AGENTS.md covers how to write a skill and change the Vale style.
Jobs run on the newest Ubuntu, ubuntu-26.04-arm. A job that only runs a short script without Nix
(a check of the pull request, a summary) runs on ubuntu-slim. ubuntu-26.04 and macOS are only
for work that needs that platform, such as an amd64 build or e2e run.
Pin this repository by commit; the Update workflow updates the pins.
.github/workflows/ci.yml:
name: CI
on:
push:
branches: [main]
pull_request:
merge_group:
schedule:
- cron: "0 16 * * *" # every job on main, which also keeps main's caches warm
workflow_dispatch:
permissions:
contents: read
# A new push to a pull request stops its older run; a push to main does not stop the schedule.
concurrency:
group: ci-${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
jobs:
lint:
runs-on: ubuntu-26.04-arm
steps:
- uses: actions/checkout@<sha> # v7.0.1
with:
persist-credentials: false
- uses: zeabur-oss/.github/actions/ci-setup@<sha> # main
with:
rust-cache: true
- run: nix develop -c scripts/ci/lint.sh
kubernetes:
if: github.event_name != 'pull_request' && !(github.event_name == 'push' && github.ref_type == 'branch')
runs-on: ubuntu-26.04-arm
steps:
- uses: actions/checkout@<sha> # v7.0.1
with:
persist-credentials: false
- uses: zeabur-oss/.github/actions/ci-setup@<sha> # main
with:
rust-cache: true
- run: nix develop -c scripts/ci/kubernetes.sh
zizmor:
uses: zeabur-oss/.github/.github/workflows/zizmor.yml@<sha> # main
ok:
name: ci / ok
if: always()
needs: [lint, kubernetes, zizmor]
runs-on: ubuntu-slim
steps:
- if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
run: exit 1
dco:
uses: zeabur-oss/.github/.github/workflows/dco.yml@<sha> # main.github/workflows/release.yml:
name: Release
on:
push:
tags: ["v[0-9]*"] # the shared workflow checks the rest (SemVer)
workflow_dispatch:
inputs:
version:
description: "Version to rehearse (v0.1.1); a rehearsal never publishes"
required: true
permissions:
contents: read
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
release:
uses: zeabur-oss/.github/.github/workflows/release.yml@<sha> # main
permissions:
contents: write
packages: write
id-token: write
attestations: write
with:
name: Zeabur Example
version: ${{ inputs.version }}.github/workflows/changeset.yml:
name: Changeset
on:
pull_request:
types: [opened, edited, synchronize, reopened, labeled, unlabeled]
permissions:
contents: read
jobs:
changeset:
uses: zeabur-oss/.github/.github/workflows/changeset.yml@<sha> # main.github/workflows/release-pr.yml:
name: Release pull request
on:
workflow_dispatch:
inputs:
version:
description: "Version instead of the one the change files give (v0.2.0); empty for theirs"
default: ""
prerelease:
description: "Pre-release label (rc, beta) for vX.Y.Z-<label>.N; empty for a normal release"
default: ""
push:
branches: [main, "maint/*"]
permissions:
contents: read
concurrency:
group: release-pr-${{ github.ref }}
cancel-in-progress: false
jobs:
release-pr:
uses: zeabur-oss/.github/.github/workflows/release-pr.yml@<sha> # main
with:
version: ${{ inputs.version }}
prerelease: ${{ inputs.prerelease }}
secrets:
private-key: ${{ secrets.ZEABUR_OSS_BOT_PRIVATE_KEY }}.github/workflows/update.yml, Monday 02:30 in Asia/Taipei:
name: Update
on:
schedule:
- cron: "30 18 * * 0"
workflow_dispatch:
permissions:
contents: read
jobs:
update:
uses: zeabur-oss/.github/.github/workflows/update.yml@<sha> # main
secrets:
private-key: ${{ secrets.ZEABUR_OSS_BOT_PRIVATE_KEY }}renovate.json: {"extends": ["github>zeabur-oss/.github"], ...} plus the repository's
enabledManagers and package rules.
- Every pull request that users notice adds a change file (
knope document-change). - Run the repository's Release pull request workflow, with
prerelease: rcfor a release candidate. Check the pull request it opens; editCHANGELOG.mdon its branch if needed. - Optionally rehearse: run the repository's Release workflow by hand with the version.
- Merge the release pull request. zeabur-oss-bot tags the merge commit, and the tag's workflow leaves a draft release.
- Check the draft and publish it. Releases are immutable once published.
A tag is a SemVer 2.0.0 version with a v and no build
metadata. vX.Y.Z, v0.* included, is a normal release. A tag with a pre-release part
(v0.1.2-rc.0) is a GitHub pre-release, never marked latest. knope gives a pre-release its own
CHANGELOG.md section with every change since the last normal release and keeps the change files,
so the normal release lists them all again. Before 1.0, knope makes a major change a minor
release and a minor change a patch.
When main already has changes for the next minor release, release fixes for the current one from
a maintenance branch:
- Create
maint/vX.Yfrom the tagvX.Y.0, protected by a branch ruleset forrefs/heads/maint/*withmain's rules but no merge queue, which GitHub doesn't allow on a pattern. Maintenance branches don't start withrelease/, which holds the release pull requests' branches. - Merge each fix into
mainfirst. Then open a pull request that cherry-picks it, with its change file, intomaint/vX.Y. - Run the Release pull request workflow on
maint/vX.Y, once no other release pull request is open: both userelease/next. The tag must be onmainor on themaint/vX.Yits version names; otherwise the release stops before building. - When publishing the draft of an older line, clear Set as latest if a newer minor release is out.
- Open a pull request into
mainthat adds the release'sCHANGELOG.mdsection and deletes the change files it used, so the next release frommaindoes not list them again.
See CONTRIBUTING.md: signed-off commits (DCO) and Conventional Commits.