Reviewed-on: #18
gitea-pipelines
Reusable Gitea Actions workflows and the central Renovate configuration for the repos on scm.cabillot.eu.
This repo is public so the Actions runner can clone it for reusable workflow calls. No secret is stored here — they come from org-level secrets on the calling repo's org.
Contents
| Path | Role |
|---|---|
config.json |
Global Renovate config (placeholders only, resolved at runtime) |
.gitea/workflows/renovate.yaml |
Reusable Renovate runner (workflow_call) |
.gitea/workflows/validate.yaml |
Reusable renovate-config-validator check for a consumer's renovate.json |
.gitea/workflows/gitea-release.yaml |
Reusable "create the Gitea release for a tag" (fills the Release Notes of Renovate bump PRs) |
.gitea/workflows/release-auto.yaml |
Reusable "tag the next version of the calling repo — only when the commits ship something" (conventional-commit gate) |
.gitea/workflows/release.yaml |
Releases this repo: next vX.Y.Z tag + Gitea release |
scripts/gitea-release.sh |
Implementation behind gitea-release.yaml (also usable locally) |
scripts/check-custom-managers.py |
Fails when a custom-manager template references a field Renovate does not provide |
scripts/smoke-regex-manager.sh |
Extraction smoke test: runs Renovate in local mode on a fixture repo and requires a perso/gitea-pipelines update |
Pinning: always @vX.Y.Z, never @main
A consumer references a workflow at a version tag:
uses: perso/gitea-pipelines/.gitea/workflows/renovate.yaml@vX.Y.Z
@main is a moving target: one push and every project using the pipeline
changes at the same instant, with no review and no way to roll one project
back on its own. Pinning inverts that: a pipeline change reaches a project only
through a Renovate bump PR that its owner merges.
Renovate owns those refs. The github-actions manager parses
.gitea/workflows/*.yaml and resolves owner/repo/.gitea/workflows/x.yaml@ref
against Gitea's tag list (gitea-tags datasource, the instance endpoint from
config.json). No custom manager is needed.
What a bump PR looks like:
- title
ci(pipelines): update perso/gitea-pipelines to vX.Y.Z— the auto-merge cron only matches explicitchore(deps)titles, so it never merges a pipelines bump on its own; - one PR per repo, both refs (
renovate.yamlandtag.yaml) in it; - its "Release Notes" section is the body of that version's Gitea release, i.e. the actual commits — not a bare compare link;
- a major bump always lands in its own PR (
gitea-pipelines (major)), so the migration is reviewed separately from a routine bump; - no release-age soak on these refs: the global
minimumReleaseAgeis overridden to0 daysforperso/gitea-pipelines— the tag is published deliberately by the owner on this very repo, not by an upstream project, so waiting two days only hides a change that is already known. The review gate is the PR itself, which is still merged by hand.
What counts as major / minor / patch
| Bump | Changes |
|---|---|
| major | an input is removed or renamed, a default changes meaning, a job disappears, a new secret becomes required, the runtime behaviour of an existing call changes |
| minor | new optional input, new job, new workflow file |
| patch | fixes, docs, comments, internal refactors with no interface change |
The # pipelines-version marker
renovate.yaml and gitea-release.yaml read files from this repo at runtime
(the Renovate config, the release script). Those reads are pinned to the same
tag as the workflow itself:
with:
repository: perso/gitea-pipelines
ref: vX.Y.Z # pipelines-version
Without that pin a consumer would run a workflow frozen at a tag while the
config it executes kept sliding with main — half the pipeline would still be
unversioned. The marker is written by the release job; never edit that line
by hand (the release job refuses to publish if a marker is missing or if it
finds fewer than two of them).
There is deliberately no moving vN alias tag: it would reproduce @main
with extra steps.
Releasing a new version
Actions → Release → Run workflow, with bump = patch | minor | major
(dry_run prints the computed version and the stamped refs without changing
anything).
The job:
- computes the next version from the highest existing
vX.Y.Ztag (the first release isv1.0.0, whatever the input says); - stamps
# pipelines-versionrefs; - commits
chore(release): vX.Y.Z, tags it, and pushes the tag first, thenmain— if the branch push fails, the published version still exists andmainstays on the previous one, which is the safe side; - creates the Gitea release on that tag (same script the image repos use), so the Renovate bump PR carries a readable changelog.
Adopting Renovate in a repo
Add .gitea/workflows/renovate.yaml to the repo:
name: Renovate
on:
schedule:
- cron: '0 0 */2 * *'
push:
branches:
- main
workflow_dispatch:
jobs:
renovate:
uses: perso/gitea-pipelines/.gitea/workflows/renovate.yaml@vX.Y.Z
secrets: inherit
Then Renovate scans every 2 days, re-runs on every push to the default branch (to rebase stale PRs), and can be dispatched manually.
Per-repo overrides
Add a renovate.json at the root of the repo; it is merged with the global
config. To have it validated on every PR, call the reusable check:
jobs:
renovate-config:
uses: perso/gitea-pipelines/.gitea/workflows/validate.yaml@vX.Y.Z
Required secrets
Set as org-level secrets in the Gitea org of the consumer repo:
| Secret | Purpose |
|---|---|
RENOVATE_TOKEN |
Gitea API token for the renovate bot |
RENOVATE_GITHUB_TOKEN |
GitHub token for GitHub-hosted dependencies |
RENOVATE_SSH_KEY |
SSH private key for git operations (optional) |
SA_TOKEN_ACTION_PUSH_TAGS |
Token used by gitea-release.yaml to push tags and create releases (already org-wide for the image repos) |
A called reusable workflow receives no secret unless the caller adds
secrets: inherit — Gitea does not inherit them, not even same-repo.
Release notes for image repos (jcabillot/*)
Renovate copies a dependency's Gitea release body into the bump PR under "Release Notes". An image repo that only pushes git tags produces an empty section with a bare "Compare Source" link.
gitea-release.yaml fixes that at the source: called from the image repo's
tag.yaml, after the image push and before the Renovate dispatch (Renovate
reads the release while it builds the PR body):
release:
needs: [build-push]
uses: perso/gitea-pipelines/.gitea/workflows/gitea-release.yaml@vX.Y.Z
with:
image: jcabillot/<repo>
secrets: inherit
trigger-renovate:
needs: [build-push, release]
The body is the commit list since the previous tag, grouped by conventional
type (features / fixes / refactor & perf / deps / CI & chores / docs), with
links to each commit, the full diff, and the image reference.
scripts/gitea-release.sh is idempotent: re-running for an existing tag
updates that release instead of creating a duplicate.
Backfilling an existing tag needs no change in the image repo: dispatch the
Gitea Release workflow with repository=<org>/<repo>, tag=<tag> and
image=jcabillot/<repo> — or run the script locally with GITEA_URL,
GITEA_TOKEN, GITEA_REPO, TAG and IMAGE.
Deciding whether to release at all (release-auto.yaml)
The tag is the single trigger of an image repo's chain: tag.yaml → build-push
→ Gitea release → Renovate dispatch. Tagging a ci:-only change therefore
rebuilds an identical image, publishes a release and opens a bump PR for it.
release-auto.yaml moves the decision in front of the tag. It reads the commits
since the highest reachable vX.Y.Z tag of the calling repo and, in auto
mode:
| Commits since the last tag | Result |
|---|---|
<type>! or a BREAKING CHANGE: footer |
major |
feat |
minor |
fix, perf, revert |
patch |
chore(deps), build(deps) |
patch (the shipped image does change) |
ci:, chore:, docs:, test:, build:, refactor:, style:, merge commits |
no release — green, no tag created |
fix(ci): counts as CI-only, and chore(deps-dev) does not ship, so neither
triggers a release. A repo with no vX.Y.Z tag yet gets v0.0.1 when the first
dispatch finds a releasable commit (a ci-only history still skips it — force with
bump). An explicit bump: patch|minor|major forces a release whatever the
commits say — the escape hatch for a change whose commit message understates it.
Consumer workflow, one per image repo:
name: Release
on:
workflow_dispatch:
inputs:
bump:
description: "auto | patch | minor | major"
type: choice
options: [auto, patch, minor, major]
default: auto
dry_run:
description: "Print the decision, create no tag"
type: boolean
default: false
jobs:
release:
uses: perso/gitea-pipelines/.gitea/workflows/release-auto.yaml@vX.Y.Z
with:
bump: ${{ inputs.bump }}
dry_run: ${{ inputs.dry_run }}
secrets: inherit
Then Actions → Release → Run workflow on the image repo. dry_run prints the
computed version and the reason without touching a ref. The tag it creates fires
the repo's own tag.yaml, which builds, releases and dispatches Renovate —
release-auto.yaml creates the ref and nothing else. It is idempotent: a second
dispatch on an already released version does not fail.
Why the two config checks exist
renovate-config-validator checks the schema, not the behaviour: both failures
this repo has shipped were schema-valid.
- A template variable that is not a dependency field.
validMatchFieldsis a closed list (depName,packageName,currentValue,currentDigest,datasource,versioning,extractVersion,registryUrl,depType,indentation). A named capture outside it —subpathinuses: owner/repo/path@vX.Y.Z— exists in the match but not on the dependency, so{{subpath}}compiles to an empty string. The rewrite produceduses: perso/gitea-pipelines@v1.1.0, the regex no longer matched, and Renovate aborted withNo dependencies found in file ... after autoreplace/update failure. Guarded byscripts/check-custom-managers.py. - An over-escaped
managerFilePatterns. One JSON round-trip too many turned\\into\\\\: the pattern then required a literal backslash, matched no file, and the dependency vanished from every consumer silently. Guarded byscripts/smoke-regex-manager.sh, which runs Renovate against a fixture repo.
Both run on every PR, in the same renovate/renovate image the consumers use.