Skip to content

Commit e466249

Browse files
sleidigclaude
andauthored
feat: shared CI workflows, release automation, Renovate presets and org defaults (#1)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 5a7d847 commit e466249

19 files changed

Lines changed: 1338 additions & 0 deletions
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
name: Bug report
3+
about: Report a problem, something that does not work as expected
4+
title: ""
5+
labels: ""
6+
type: "Bug"
7+
assignees: ""
8+
---
9+
10+
<!--
11+
No personal data, credentials, instance names, hostnames or other client-specific details here.
12+
Scrub logs and screenshots before adding them; link to internal sources (e.g. a Sentry issue) instead.
13+
Security vulnerabilities: do not open an issue, see SECURITY.md (report them privately).
14+
-->
15+
16+
17+
What happens, and what you expected to happen instead.
18+
19+
20+
## How to reproduce
21+
22+
<!-- Steps, request or configuration that triggers it.
23+
Version (app version or image tag), relevant configuration, (scrubbed) logs or a link to the error in monitoring. -->
24+
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
name: Feature request
3+
about: Suggest a new functionality
4+
title: ""
5+
labels: ""
6+
type: "New Feature"
7+
assignees: ""
8+
---
9+
10+
<!--
11+
No personal data, credentials, instance names, hostnames or other client-specific details here.
12+
Scrub logs and screenshots before adding them; link to internal sources (e.g. a Sentry issue) instead.
13+
Keep this description short and readable for non-developers. Technical details go into the
14+
implementation plan comment (see the guidelines at the end of this template).
15+
-->
16+
17+
## What
18+
19+
<!-- The intended outcome, as observable behaviour: who can do what afterwards?
20+
e.g. "As an administrator I want to ... in order to ..."
21+
Also state what is explicitly out of scope. -->
22+
23+
## Why
24+
25+
<!-- The problem or use case behind it, and who is affected (users, administrators, operators). -->
26+
27+
## How
28+
29+
<!-- The proposed approach in a few sentences, and alternatives considered.
30+
A small table or list helps to compare options. -->
31+
32+
---
33+
34+
Implementation plan: <!-- link to the plan comment, added once the objective is agreed -->
35+
36+
<!--
37+
IMPLEMENTATION PLAN (not part of this description)
38+
39+
Once the objective above is agreed, post the implementation plan as ONE comment on this issue and
40+
link it above. It is for the developer or agent implementing the change and must be
41+
self-contained: someone who reads only that comment and the code can implement it without the
42+
rest of the discussion. When decisions change, edit that comment in place instead of adding new
43+
ones; change this description only if the objective changes.
44+
45+
## Implementation plan
46+
47+
### Context
48+
The current state, verified against the code: file paths, how it behaves today. Anything assumed
49+
but not verified is marked as such.
50+
51+
### Decisions
52+
Each decision with its reason and the rejected alternative, including how risks are handled.
53+
54+
### Changes
55+
Concrete steps: files, config keys, snippets where precision matters, then the tests and docs to
56+
add or change.
57+
58+
### Open questions
59+
What is still undecided and how it gets settled. Omit when there are none.
60+
-->
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
name: Technical task
3+
about: A technical improvement, e.g. operations, observability, dependencies, CI or infrastructure
4+
title: ""
5+
labels: ""
6+
type: "Task"
7+
assignees: ""
8+
---
9+
10+
<!--
11+
No personal data, credentials, instance names, hostnames or other client-specific details here.
12+
Scrub logs and screenshots before adding them; link to internal sources (e.g. a Sentry issue) instead.
13+
Keep this description short and readable for reviewers. Technical details go into the
14+
implementation plan comment (see the guidelines at the end of this template).
15+
For pure refactorings without behaviour change, set the issue type to "Code Quality / Refactoring".
16+
-->
17+
18+
19+
- [ ] step 1
20+
- [ ] step 2

‎.github/workflows/README.md‎

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Shared GitHub Actions workflows
2+
3+
Reusable workflows for all Aam-Digital repositories, so that each repository keeps only what is specific to it.
4+
Inputs, secrets and outputs are documented in the `workflow_call` section at the top of each file.
5+
6+
| Workflow | What it does | Typical trigger in the caller | Permissions of the calling job | Check name |
7+
| --- | --- | --- | --- | --- |
8+
| [`docker-image.yml`](docker-image.yml) | Builds and pushes a container image (PR, edge, release), each platform on a native runner | `pull_request`, `push`, `release: published` | `contents: read`, `packages: write` | `<job> / build (<platform>)`, `<job> / merge` |
9+
| [`edge-version.yml`](edge-version.yml) | Computes the version of an edge build (`1.4.1-edge.5`) | `push` to the default branch | `contents: read` | `<job> / version` |
10+
| [`release.yml`](release.yml) | Creates the next full release with semantic-release | `push` to the default branch, or `workflow_dispatch` | `contents: read` (+ PAT as secret `token`) | `<job> / release` |
11+
| [`image-cleanup.yml`](image-cleanup.yml) | Deletes the `pr-<n>` image when a PR closes, or the edge images (`tags: "*-edge.*"`) after a full release; then untagged images older than 2 days | `pull_request: closed`, `release: published` | `actions: write`, `packages: write` | `<job> / cleanup` |
12+
| [`pr-title-lint.yml`](pr-title-lint.yml) | Checks that PR titles follow Conventional Commits | `pull_request_target` | `pull-requests: read` | `<job> / Validate PR title` |
13+
| [`actionlint.yml`](actionlint.yml) | Lints the repository's workflows (with shellcheck) | `pull_request`, `push` | `contents: read` | `<job> / actionlint` |
14+
15+
`ci.yml` is this repository's own CI; it is not meant to be called.
16+
17+
## Calling a workflow
18+
19+
```yaml
20+
permissions: {} # nothing at the top level; each job grants what it needs
21+
22+
jobs:
23+
build:
24+
uses: Aam-Digital/.github/.github/workflows/docker-image.yml@v1
25+
permissions:
26+
contents: read
27+
packages: write
28+
with:
29+
images: ghcr.io/aam-digital/my-service
30+
tags: pr-${{ github.event.number }}
31+
platforms: linux/amd64
32+
```
33+
34+
- Always reference **`@v1`**, never a branch.
35+
- The calling job must grant the **permissions** from the table. A called workflow cannot get more, and the run fails
36+
before it starts. `permissions: {}` at the top level keeps `qlty check` (checkov) happy.
37+
- The caller's **secrets and `env` are not passed on**: pass secrets explicitly under `secrets:`, everything else as
38+
inputs.
39+
- **Check names** become `<caller job> / <called job>`. Moving an existing job into a shared workflow renames its
40+
check, so update the required checks of the ruleset when that PR merges. Skipped jobs (e.g. `merge` when `push` is
41+
false) count as passed.
42+
- `pr-title-lint.yml` only works with squash-only merging and the PR title as commit title. Call it on
43+
`pull_request_target` with the comment `# zizmor: ignore[dangerous-triggers]` on that line, otherwise `qlty check`
44+
fails.
45+
- PR images carry only their `pr-<n>` tag (otherwise the cleanup keeps them). PRs from forks build with `push: false`.
46+
47+
Complete examples: [replication-backend](https://github.com/Aam-Digital/replication-backend/tree/master/.github/workflows)
48+
(auto release mode, PR images) and [ndb-core](https://github.com/Aam-Digital/ndb-core/tree/master/.github/workflows)
49+
(manual release mode, two registries, web app built in an earlier job and passed as an artifact).
50+
51+
## Release model
52+
53+
Every push to the default branch builds an **edge** image; full releases come from Conventional Commit PR titles via
54+
[semantic-release](https://semantic-release.gitbook.io/) (plugins in each repository's own `.releaserc*`).
55+
56+
- **Edge build:** `edge-version.yml` computes `<last release, patch + 1>-edge.<commits since>`, e.g. `1.4.1-edge.5`.
57+
The image is tagged with it, but there is no git tag and no GitHub release. Move the `edge` image tag only if
58+
`is-head` is `true`, and give the image job a `concurrency` group with `cancel-in-progress: true`, so that an older
59+
run never moves `edge` back.
60+
- **Full release**, in one of two modes: **auto** (`release.yml` on every push; a `fix`/`feat`/breaking change is
61+
released right away) or **manual** (`release.yml` via `workflow_dispatch`). Releases are created with a PAT, because
62+
releases created with the `GITHUB_TOKEN` trigger no workflows. The `release: published` event builds the image, and
63+
`image-cleanup.yml` then deletes the edge images of the finished cycle.
64+
65+
| Build | Image tags |
66+
| --- | --- |
67+
| push to the default branch | `1.4.1-edge.5`, `edge` |
68+
| full release `1.4.1` | `1.4.1`, `stable`, `latest` |
69+
| pull request #123 | `pr-123` |
70+
71+
Pass `stable: ${{ !github.event.release.prerelease }}` for release images; never derive it from the version string.
72+
73+
## Versioning (`v1`)
74+
75+
There are no releases of this repository. After every push to `main`, CI moves `v1` to the new commit once actionlint
76+
has passed, so a merged PR (Renovate's included) reaches every repository on its next run. Merging here is the release
77+
step: check what a change does to the callers before merging.
78+
79+
A change that breaks callers needs `v2`: set `MAJOR_TAG: v2` in `ci.yml` in the same PR, so that `v1` stays where it
80+
is, then switch the callers one by one. Breaking means:
81+
82+
- removing or renaming an input, output or secret, or changing an input's default or meaning;
83+
- renaming a job id or job `name` (this changes check names);
84+
- requiring more permissions from the caller;
85+
- changing which image tags are produced for the same inputs.
86+
87+
To test a change before merging, point a caller PR at the branch (`@<branch>`), and switch back to `@v1` before
88+
merging it. In an emergency, `v1` can be reset by hand:
89+
`gh api -X PATCH repos/Aam-Digital/.github/git/refs/tags/v1 -f sha=<good commit> -F force=true`.
90+
91+
## Rules for the workflows here
92+
93+
- Keep shell inline; never fetch scripts from this repository at runtime.
94+
- Pass inputs to `run:` scripts via `env:`, never by `${{ }}` interpolation into the script.
95+
- Actions of large trusted providers (`actions/*`, `docker/*`, `github/*`, …) use their major tag; other actions are
96+
pinned to a commit SHA with a `# vX.Y.Z` comment. Renovate (`renovate.json` in the repository root) updates both.
97+
- Steps shared between jobs of one workflow are written once and reused with YAML anchors (`&name` / `*name`).
98+
- Explain *why* in comments in the workflow file, next to the code; keep this README to what callers need.

‎.github/workflows/actionlint.yml‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Lints the calling repository's GitHub Actions workflows with actionlint
2+
# (including shellcheck on `run:` scripts).
3+
name: actionlint
4+
5+
on:
6+
workflow_call:
7+
8+
jobs:
9+
# The job id is part of the check name (`<caller job> / actionlint`). Do not rename it within v1.
10+
actionlint:
11+
runs-on: ubuntu-latest
12+
permissions:
13+
contents: read
14+
steps:
15+
- uses: actions/checkout@v7
16+
with:
17+
persist-credentials: false
18+
19+
- name: actionlint
20+
uses: raven-actions/actionlint@3d39aea434753780c3b3d4a1a31c854b4dbf49d7 # v2.2.0

‎.github/workflows/ci.yml‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# CI of this repository: lints the shared workflows themselves and, on `main`, moves the major tag that consumers
2+
# reference (`@v1`) to the new commit. Every merge to `main` therefore reaches all repositories on their next run.
3+
name: CI
4+
5+
on:
6+
pull_request:
7+
push:
8+
branches:
9+
- main
10+
11+
permissions:
12+
contents: read
13+
14+
env:
15+
# The major tag that follows `main`. For a breaking change (see README.md in .github/workflows), set this to the
16+
# next major (`v2`) in the same PR: the old tag then stays where it is, and consumers switch one by one.
17+
MAJOR_TAG: v1
18+
19+
jobs:
20+
# Check name: `actionlint / actionlint` (required by the ruleset of `main`; do not rename)
21+
actionlint:
22+
uses: ./.github/workflows/actionlint.yml
23+
24+
move-major-tag:
25+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
26+
needs: actionlint
27+
runs-on: ubuntu-latest
28+
permissions:
29+
contents: write # move the tag
30+
# Pushes are handled in order, so that an older run never moves the tag back.
31+
concurrency:
32+
group: move-major-tag
33+
cancel-in-progress: false
34+
steps:
35+
- name: Move ${{ env.MAJOR_TAG }} to this commit
36+
env:
37+
GH_TOKEN: ${{ github.token }}
38+
run: |
39+
if current=$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$MAJOR_TAG" --jq .object.sha 2>/dev/null); then
40+
status=$(gh api "repos/$GITHUB_REPOSITORY/compare/$current...$GITHUB_SHA" --jq .status)
41+
case "$status" in
42+
identical)
43+
echo "$MAJOR_TAG already points to $GITHUB_SHA"
44+
exit 0
45+
;;
46+
ahead) ;;
47+
*)
48+
echo "::warning::$GITHUB_SHA is not ahead of $MAJOR_TAG ($current, compare status '$status'), so the tag is not moved"
49+
exit 0
50+
;;
51+
esac
52+
gh api --silent -X PATCH "repos/$GITHUB_REPOSITORY/git/refs/tags/$MAJOR_TAG" -f sha="$GITHUB_SHA" -F force=true
53+
echo "Moved $MAJOR_TAG from $current to $GITHUB_SHA"
54+
else
55+
gh api --silent -X POST "repos/$GITHUB_REPOSITORY/git/refs" -f ref="refs/tags/$MAJOR_TAG" -f sha="$GITHUB_SHA"
56+
echo "Created $MAJOR_TAG at $GITHUB_SHA"
57+
fi

0 commit comments

Comments
 (0)