No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Hugues KADI fde217784b
Some checks failed
rust-all.yaml / feat: a feature build and extra container images (push) Failing after 0s
Check / check (push) Successful in 4s
feat: a feature build and extra container images
A crate can carry a feature that is off by default, and nothing here ever
built it: the suite and clippy ran the default set, and the one container
job built the one Containerfile. Code behind such a feature reached the
registry having never been compiled in CI.

`features` adds a second test job with that set on. The default run stays
as it is, since what gets published has to keep working on its own, and
running both catches a file that only compiles with the feature on as
well as one that only compiles with it off. `system-packages` installs
what such a build needs from apt, for a dependency that compiles C on a
runner image without the tools.

`container-variants` builds more images from the same source and tag,
each with its own Containerfile and a suffix on both tags, so a variant
publishes `<version>-git` and `latest-git` instead of overwriting the
plain ones. The release and both build jobs now also wait on the feature
run, so a broken feature stops the publish.

Every input defaults to off, so a caller that does not set them runs
exactly as before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 22:29:36 +02:00
.forgejo/workflows feat: a feature build and extra container images 2026-09-19 22:29:36 +02:00
actions feat: a feature build and extra container images 2026-09-19 22:29:36 +02:00
scripts fix: drop permissions blocks Forgejo ignores, name the refused token 2026-09-18 06:47:50 +02:00
.gitignore feat: shared Rust CI actions and reusable workflows 2026-09-14 18:24:06 +02:00
AGENTS.md fix: publish from the run that tagged, a job's tag starts none 2026-09-18 18:03:07 +02:00
LICENSE feat: shared Rust CI actions and reusable workflows 2026-09-14 18:24:06 +02:00
README.md fix: match a squash merge's subject in rust-release 2026-09-19 14:06:11 +02:00

ci

CI

Shared Forgejo Actions for git.attssystem.fr: how Rust crates are tested, audited, published and containerised here, written once.

A repository needs one file and no jobs of its own:

# .github/workflows/ci.yaml
name: CI

on:
  push:
    branches: [main]
    tags: ["*"]
  pull_request:
  schedule:
    - cron: "17 4 * * *"
  workflow_dispatch:

jobs:
  rust:
    uses: attssystem/ci/.forgejo/workflows/rust-all.yaml@main
    with:
      event-name: ${{ github.event_name }}
    secrets: inherit

rust-all picks what to run from the trigger that reached it — via event-name, since a called workflow's own github.event_name is workflow_call, not the trigger that reached the caller:

Trigger Runs
push to a branch, pull request fmt, clippy, the suite
push to the default branch the same, then the release: tag from Conventional Commits, or a pull request that bumps toward one — and, having tagged, cargo publish and the container in that same run
push of a tag by hand fmt, clippy, the suite, then cargo publish and the container
schedule, or by hand cargo audit, and cargo upgrade, each with its own pull request

Requirements

  • This repository stays public. Composite actions are fetched by the runner with a token scoped to the calling repository, so a private ci would break them for private consumers.

  • CI_BOT_TOKEN, as an organisation secret on internal and attssystem. The only secret — nothing reads a separate CI_PACKAGE_TOKEN any more — so one token covers the lot. What each scope is for:

    Scope What needs it
    write:repository the ci/* branch pushes, the release's tag, and the pull request API — repository is the scope pull requests live under
    write:issue assigning those pull requests
    read:organization the membership list an organisation's assignment is built from
    write:package cargo publish and the container push
    read:user optional; lets a refusal say who the forge takes the token for

    Scopes are a ceiling, not access: the token's user also has to be able to see the repository, through a team with write on Code, Issues and Pull Requests. A bot outside internal is refused a private repository's pull requests however wide its scopes — the 404 below looks the same either way.

Inputs

All optional.

Input Default
event-name "" ${{ github.event_name }}; without it, audit and upgrade never run
working-directory . space separated; a repository of independent crates names each
test true fmt, clippy and the suite, and the gate before a publish
network-tests false also the #[ignore] tests, reporting rather than blocking
readme-summary false keep an ![tests](...) badge in README.md current
publish true cargo publish on a tag, or on the release that made one
container true build and push the Containerfile, likewise
audit true cargo audit on a schedule
upgrade true cargo upgrade against Cargo.toml on a schedule
release true tag from Conventional Commits on a push to the default branch, or a pull request that bumps toward one
assign true assign the audit's, the upgrade's and the release's pull requests
toolchain stable
registry-owner this repository's owner
manifest Cargo.toml where the container tag's version is read
containerfile Containerfile
runner ubuntu-latest

A repository with no Containerfile sets container: false; one whose source is not yet clean sets test: false until it is.

Why the pull requests need a token of their own

The run's own token pushes a branch and is then refused the pull request API on a private repository: Forgejo answers 404, Can't read pulls. The branch is there, nothing opens, and the step summary carries the compare link instead.

A permissions: block in the workflow does not change that. Forgejo ignores one and warns on every run that it did — "use Authorized Integrations to grant capabilities to this job instead" — so what a job may do comes from the token it is handed, or from an Authorized Integration granted in the forge's own settings. Hence CI_BOT_TOKEN, which is also the reason a pull request it opens runs its own CI, unlike one the run's token opened.

When something is still refused, the error names which token was used and who the forge takes it for, which is usually the whole answer: a bot that is not a member of the organisation sees none of its private repositories.

The audit

cargo audit runs on the schedule, and for anything found asks cargo for a newer version of the named crate. If that clears the advisory, the lockfile change is pushed to a rolling ci/cargo-audit branch and a pull request opens — a later run force-pushes the same branch rather than opening another. What a bump cannot fix is reported and left for a decision: a newer major, a different crate, or an entry in .cargo/audit.toml with the reason.

The job is red whenever an advisory was found, fixed or not. The default branch still carries it until the pull request is merged.

The upgrade

cargo upgrade also runs on the schedule, against every Cargo.toml, including across a semver-incompatible major. Anything it bumps is pushed to a rolling ci/cargo-upgrade branch — a separate branch from the audit's, since they can run the same night — and a pull request opens, force-pushed by later runs rather than duplicated. Unlike the audit, an available upgrade is not a failing job: nothing there is broken, only newer.

The release

On every push to the default branch, rust-release looks at what landed since the last release tag — vX.Y.Z or bare X.Y.Z, whichever style the repository's tags already use; the next one copies it, so this does not referee a repository into a style it does not already use. A feat:, a fix:, a perf:, or anything marked breaking (!:, or a BREAKING CHANGE: footer) is release-worthy; anything else — chore:, docs:, and so on — is not. If it is, every Cargo.toml in working-directory is bumped to the version that implies (major, minor or patch — pre-1.0, a breaking change bumps the minor instead, the digit Cargo itself already treats as the breaking one) and pushed to a rolling ci/release branch, opening or force-pushing the pull request that carries it, the same way the audit's and the upgrade's do.

Merging that pull request is what tags: the next push to the default branch finds the bump commit's chore(release): X.Y.Z subject, tags that commit and pushes it. Squash, merge or rebase — a squash merge's trailing (#4), which the forge appends to the pull request's title, is matched too, so how a repository merges does not decide whether its releases tag. cargo publish and the container then run in that same run, checking the new tag out — because a tag a job pushes starts no run of its own. The forge accepts the tag and nothing follows it, which is how a release came to stop dead after Tagged v1.2.2; waiting for that tag event is waiting for something the forge never sends. A tag someone pushes by hand does reach the forge as a push, and still runs both the ordinary way.

Unlike this repository's own scripts/release.sh, nothing moves a floating major tag; a leaf application has no uses: ...@v1 follower to keep it for.

Every directory moves to the same version: one release, not one per crate. A repository of crates versioned independently of each other is not what this bumps toward — call rust-release directly with one working-directory for that instead of enabling it through rust-all.

Nothing here runs cargo: only Cargo.toml's version and, best effort, a crate's own entry in its Cargo.lock are ever touched. Before a repository's first tag, cut one by hand — nothing here invents a starting point.

Assignment

All three pull requests are assigned by default: to the owner, for a repository owned by a user, or to every member, for one owned by an organisation. /orgs/{owner} 404ing is what tells the two apart, so no configuration names which kind a repository is. Assignment is a follow-up call after the pull request exists, so it also refreshes a rolling branch's already-open pull request — useful when an organisation's membership has changed since it was opened. assign: false turns it off.

The actions underneath

rust-all is a wrapper. The logic is in six composite actions, which a repository with steps of its own can call directly, sharing one checkout:

    steps:
      - uses: actions/checkout@v4
      - uses: https://git.attssystem.fr/attssystem/ci/actions/rust-test@main
      - run: ./scripts/something-only-this-repo-does.sh
      - uses: https://git.attssystem.fr/attssystem/ci/actions/rust-build-native@main
        with:
          token: ${{ secrets.CI_BOT_TOKEN }}

The URL must be absolute: a bare attssystem/ci/... resolves against the instance's DEFAULT_ACTIONS_URL.

Action Inputs beyond those above
rust-test components, test-command, args, clippy-args, lint
rust-build-native token, args
rust-build-container token, username, image, version, context, build-args, latest
rust-audit token, fallback-token, audit-args, branch, base, assign
rust-upgrade token, fallback-token, upgrade-args, branch, base, assign
rust-release token, fallback-token, branch, base, assign; outputs version, the tag it pushed or empty

Clippy runs --no-deps: this repository's source, not the crates it pulls in.

Badges

A repository that sets readme-summary: true also gets its test counts in the README. Put the line anywhere and CI keeps it current on every push to the default branch:

![tests](https://img.shields.io/badge/tests-not%20yet%20run-lightgrey)

The counts go into the URL rather than being fetched from it. shields.io cannot read a private repository, so an endpoint badge would render for ci, fltl and rust-common and break everywhere else; baking the value into the path is what Forgejo's own status badge does, and is why that one works. The update is a commit marked [skip ci], so it does not start the run that would push again.

Forgejo serves one badge per workflow file, and honours branch and event:

[![CI](https://git.attssystem.fr/OWNER/REPO/actions/workflows/ci.yaml/badge.svg?event=push)](https://git.attssystem.fr/OWNER/REPO/actions)
[![Audit](https://git.attssystem.fr/OWNER/REPO/actions/workflows/ci.yaml/badge.svg?event=schedule)](https://git.attssystem.fr/OWNER/REPO/actions)

A repository has one workflow now, so an unfiltered badge reports whichever run came last — a nightly advisory would read as a broken build. ?event= is what keeps the two apart. A private repository's badge renders only for someone who can already see the repository.

Pinning

@main moves. @v1 follows the major, @v1.2.0 does not move at all. Workflows call the actions by URL and a uses: ref cannot be an expression, so scripts/release.sh v1.2.0 rewrites those refs to the tag on a commit of its own before tagging — a pinned caller gets a pinned workflow and action.