| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .forgejo/workflows | ||
| .gitignore | ||
| README.md | ||
Pipeline Templates
Reusable Forgejo Actions workflows. CI builds and scans images; Argo CD deploys them from each repo's .argo/ directory, so there are no deploy workflows here.
Usage
Reference workflows by full URL. Forgejo resolves bare owner/repo against its default actions host, not this instance.
jobs:
image:
uses: https://code.levior.io/levior/pipeline-templates/.forgejo/workflows/build-image.yml@main
with:
images: ./Dockerfile|api|.
secrets: inherit
The team's REGISTRY_USERNAME and REGISTRY_PASSWORD org secrets are picked up through secrets: inherit, and images go to reg.levior.io/<owner>/<name>.
Both workflows run on the docker runner label and need a runner with access to a Docker daemon.
build-image.yml
One build job per image. Pushes <registry>/<owner>/<name>:<commit sha> and, by default, :<branch>. Layer cache lives in the registry as <image>:buildcache. Every image carries the OCI labels org.opencontainers.image.source (the repo URL) and org.opencontainers.image.revision (the commit), so an image leads back to its code.
The image name is the full name under the owner, so a repo with several images names them itself, e.g. api and api-frontend.
| Input | Default | Description |
|---|---|---|
images |
required | One image per line as dockerfile|name|context |
registry |
reg.levior.io |
Registry hostname |
registry_username |
REGISTRY_USERNAME secret |
Registry username |
push |
true |
Push the image. false only builds it and refreshes the :buildcache layer cache, e.g. on pull requests, so the build after the merge reuses the PR's layers |
push_branch_tag |
true |
Also push the sanitized branch name as a tag |
artifact |
'' |
Artifact from an earlier job of the same run to download into the checkout before the build, for images that package something built outside Docker (upload it with https://code.forgejo.org/forgejo/upload-artifact@v4) |
artifact_path |
. |
Where the artifact goes, relative to the checkout |
build_args |
'' |
Newline-separated KEY=VALUE build args |
secret_files |
'' |
Newline-separated id=path buildx secret mounts, path relative to the repo |
generated_secrets |
'' |
Newline-separated buildx secret ids that get a random value per run |
npm_scope |
'' |
Scope served by a private npm registry. When set, the build gets an .npmrc as the npmrc secret |
npm_registry |
Forgejo npm registry for levior |
Registry serving npm_scope |
require_postgres |
false |
Start a postgres service and pass its connection string as the DATABASE_URL build secret over host networking |
postgres_version |
17 |
|
postgres_database / postgres_username / postgres_password |
app |
|
source_url |
https://code.levior.io |
Public forge address for the org.opencontainers.image.source label; the runner's github.server_url is Forgejo's in-cluster address |
scan |
false |
Scan the image with Trivy before it is pushed, from the same build. Findings at or above scan_severity fail the job and nothing is pushed. Use this instead of a separate scan-image.yml job, which builds the image a second time |
scan_severity |
CRITICAL,HIGH |
Severities that fail the scan |
scan_ignore_unfixed |
true |
Ignore vulnerabilities without an upstream fix |
scan_ignorefile |
.trivyignore.yaml |
Trivy ignore file in the repo, used when it exists. Give every entry an expired_at: after that date it stops applying and the scan fails again, so the exception gets looked at |
| Secret | Description |
|---|---|
registry_password |
Registry password. Defaults to the REGISTRY_PASSWORD secret |
build_secrets |
Newline-separated id=value buildx secrets |
npm_token |
Token for npm_registry. Defaults to the run token |
composer_token |
Token that can read private repositories for Composer packages pulled from git. Merged into the composer_auth secret |
Output tag is the full commit SHA. Argo CD's build environment exposes the same value as ARGOCD_APP_REVISION, which is why the tag is not shortened.
scan-image.yml
Trivy scan, one job per image, same images format. Logs in to the same registry with the same credentials, builds the image locally without pushing and fails on findings at or above severity.
Prefer build-image.yml with scan: true: it scans the image it is about to push, so the image is built once instead of twice. This workflow stays for repos that scan without building with build-image.yml.
| Input | Default | Description |
|---|---|---|
images |
required | One image per line as dockerfile|name|context |
registry |
reg.levior.io |
Registry hostname |
registry_username |
REGISTRY_USERNAME secret |
Registry username |
severity |
CRITICAL,HIGH |
Severities that fail the job |
ignore_unfixed |
true |
Ignore findings without an upstream fix |
build_args |
'' |
Build args for the local build |
digger.yml
Runs Digger in backendless mode on pull requests: tofu plan when a PR is opened or updated, tofu apply on a digger apply comment, and digger unlock when the PR closes. Results land as PR comments and digger/plan / digger/apply commit statuses. The repo needs a digger.yml at its root; PR-level locks are off since there is no DynamoDB or GCS here.
on:
pull_request:
types: [opened, synchronize, reopened, closed]
issue_comment:
types: [created]
# One apply at a time, queued behind any other apply in the repo (use the
# same group as a workflow that applies on push to main). Other runs are
# grouped per pull request.
concurrency:
group: ${{ (github.event_name == 'issue_comment' && startsWith(github.event.comment.body, 'digger apply')) && 'tofu-apply' || format('digger-{0}', github.event.pull_request.number || github.event.issue.number) }}
cancel-in-progress: false
jobs:
digger:
if: github.event_name != 'issue_comment' || startsWith(github.event.comment.body, 'digger')
runs-on: ubuntu-latest
uses: https://code.levior.io/levior/pipeline-templates/.forgejo/workflows/digger.yml@main
secrets: inherit
Forgejo needs runs-on on the calling job, unlike GitHub Actions; without it the run stays in "waiting".
digger apply refuses a branch that is behind its base: Digger applies the branch as it is, so a branch that misses commits from main would undo them (levior/infra #63 undid #62 that way). The run comments the missing commits and fails; update the branch, wait for the new plan and comment digger apply again. Forgejo only honours concurrency at the workflow level of the calling workflow, not inside this one, so the caller sets it (above): without it two applies started together could both pass the check before either merges. It also holds an apply lock: once the required checks are green, before Digger applies, the PR gets the label digger:applied (created if missing), and a digger apply on any other PR is refused, with a comment, while an open PR carries it. The label comes off when the PR closes; closed without merging, the run comments that main must be applied again, because the applied changes are still live (levior/infra #73 undid the half-applied #72 before this lock existed). A stuck label can be removed by hand. Every repo secret named TF_VAR_* or AWS_* is exported to the OpenTofu run. Forgejo stores secret names in upper case, so a TF_VAR_ secret is exported with its suffix lower-cased (TF_VAR_STATE_PASSPHRASE becomes TF_VAR_state_passphrase), which matches snake_case variable names. A PVE_SSH_KEY secret, when present, is loaded into an ssh-agent for providers that need SSH. The run token (GITHUB_TOKEN) is what Digger uses to comment and set statuses. A DIGGER_FORGEJO_TOKEN secret, when present, replaces it: Digger then comments, sets statuses and, with auto_merge: true in digger.yml, merges as that user, which is what a protected branch with a merge whitelist needs. The token needs write:repository and write:issue.
| Input | Default | Description |
|---|---|---|
digger_version |
latest |
Release tag of levior/digger to run |
opentofu_version |
1.12.6 |
OpenTofu version to install |
Exceptions with a deadline
A finding that can't be fixed yet (a dependency only a framework BOM can move) goes into the repo's .trivyignore.yaml with a statement and an expired_at, at most two weeks out:
vulnerabilities:
- id: CVE-2024-57699
statement: json-smart 2.5.0 via kafka-oauth, pinned by the Quarkus BOM; drop when the BOM has 2.5.2
expired_at: 2026-11-11
On that date the entry stops applying, the next build fails on the finding, and someone has to look again.
CI
ci.yml checks every workflow here with forgejo-runner validate (Forgejo's own schema, at the runners' version), fails on a secrets: block under workflow_call (the schema allows it, Forgejo doesn't). ci / validate (pull_request) is required on main.