From cc886936ed5054dbb98dbe3225e70c8066ecf409 Mon Sep 17 00:00:00 2001 From: lilleman Date: Sat, 4 Jul 2026 17:08:31 +0200 Subject: [PATCH] CI: sync release images to Docker Hub on vX.Y.Z tag push --- .gitea/workflows/release.yml | 23 +++++++++++++++++++++-- AGENTS.md | 5 +++++ README.md | 12 ++++++++++-- todo.md | 2 +- 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 0ee8764..4468563 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -24,6 +24,25 @@ jobs: docker tag "$REPO:$COMMIT" "$REPO:$TAG" docker push "$REPO:$TAG" done - - name: Log out of the registry + - name: Sync the release tags to Docker Hub + env: + DOCKERHUB_REPO: docker.io/${{ github.repository }} + DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} + DOCKERHUB_USER: ${{ vars.DOCKERHUB_USER }} + GIT_TAG: ${{ github.ref_name }} + REPO: gitea.larvit.se/${{ github.repository }} + run: | + COMMIT=$(git rev-parse 'HEAD^{commit}') + VERSION=${GIT_TAG#v} + [ -n "$DOCKERHUB_USER" ] && [ -n "$DOCKERHUB_TOKEN" ] \ + || { echo "Set the DOCKERHUB_USER variable + DOCKERHUB_TOKEN secret (README -> CI/CD)"; exit 1; } + printf '%s' "$DOCKERHUB_TOKEN" | docker login docker.io -u "$DOCKERHUB_USER" --password-stdin + for TAG in "$VERSION" "${VERSION%.*}" "${VERSION%%.*}" latest; do + docker tag "$REPO:$COMMIT" "$DOCKERHUB_REPO:$TAG" + docker push "$DOCKERHUB_REPO:$TAG" + done + - name: Log out of the registries if: always() - run: docker logout gitea.larvit.se + run: | + docker logout gitea.larvit.se + docker logout docker.io diff --git a/AGENTS.md b/AGENTS.md index a0e97c4..4f6d95c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -86,6 +86,11 @@ them. Revisit only if the stated reason stops holding. `tsconfig.include` and resolve the host surface via `#`-imports, so each example typechecks in place *and* copies across unchanged. Never commit real plugins/config into the root mount dirs (`plugins/`, `config/`) — they ship empty (`.gitkeep`, git-ignored otherwise). +- **CI docker logins share the runner host's Docker config.** The act_runner is host-mode, so + `docker login`/`logout` in the workflows mutate one shared `~/.docker/config.json`: + concurrent jobs can race (one job's logout can 401 another's push — recover by re-running), + and tokens sit in that file between login and logout. Accepted for a single-maintainer + cadence; serialize with a workflow `concurrency` group if it ever bites. ## Docker only — no host tooling diff --git a/README.md b/README.md index 9d0fb42..87fabd0 100644 --- a/README.md +++ b/README.md @@ -1174,7 +1174,7 @@ Gitea Actions (`.gitea/workflows/`) runs the pipeline; the test job runs | Workflow | Trigger | Does | | --- | --- | --- | | `ci.yml` | push, any branch except `main` | the full gate (`bash ci.sh`), then build + push the app image | -| `release.yml` | push of a `vX.Y.Z` tag | re-tag that commit's image as `X.Y.Z`, `X.Y`, `X`, `latest` | +| `release.yml` | push of a `vX.Y.Z` tag | re-tag that commit's image as `X.Y.Z`, `X.Y`, `X`, `latest`; sync those tags to Docker Hub | | `mirror.yml` | push to `main` or any tag, or manual | force-push `main` + tags to the [GitHub mirror](https://github.com/larvit/plainpages) | | `registry-cleanup.yml` | nightly cron, or manual | delete registry images that are neither release-tagged nor a branch head | @@ -1216,7 +1216,15 @@ workflow protects. `release.yml`, which pulls that commit's hash image from the registry and re-tags it as `1.2.3`, `1.2`, `1`, and `latest` — nothing is rebuilt, the released image is byte-identical to the gated one. It fails loud if no hash image exists: release tags must point at a commit -that went through the gate (in practice, any `main` commit). +that went through the gate (in practice, any `main` commit). The same four tags are then +synced to [Docker Hub](https://hub.docker.com/r/larvit/plainpages) (`larvit/plainpages`) — +releases only, no hash tags. One-time setup: create the public `larvit/plainpages` +repository on Docker Hub, generate a read/write access token **scoped to that repository** +(an organization access token, or a token on a dedicated single-purpose account — an +account-wide PAT can push to every repo under the account), and store the account name as +the Actions **variable** `DOCKERHUB_USER` and the token as the Actions **secret** +`DOCKERHUB_TOKEN`. Until they exist, a release run fails at the Docker Hub step — after the +Gitea re-tag has succeeded — so set them, then re-run the workflow. **GitHub mirror** — [github.com/larvit/plainpages](https://github.com/larvit/plainpages) is a read-only mirror; after every merge, `mirror.yml` force-pushes `main` and all tags there, diff --git a/todo.md b/todo.md index a14cfd9..c8eba62 100644 --- a/todo.md +++ b/todo.md @@ -8,7 +8,7 @@ - [x] CI/CD - Sync up to github after every successful merge to main, URL: git@github.com:larvit/plainpages.git - also note the true home top of the README. Force push to github, it should only ever be a mirror of the gitea.larvit.se repository. (`.gitea/workflows/mirror.yml` force-pushes main + tags over HTTPS with a dedicated account's PAT in the `MIRROR_GITHUB_TOKEN` secret; setup documented in README → CI/CD.) - [x] CI/CD - Build docker images as part of the requirements to be able to merge to main. Push them with the git commit hash as docker tag. Push to container registry at Gitea. (`ci.yml` builds + pushes `gitea.larvit.se/larvit/plainpages:` after a green gate — with ff-only merges that is the main commit's image; auth via the `DOCKER_REGISTRY_USER` variable + `DOCKER_REGISTRY_TOKEN` secret, retention via an org cleanup rule; documented in README → CI/CD.) - [x] CI/CD - Re-tag docker images from git hash to semver when a semver git tag is pushed. (`release.yml` on a `vX.Y.Z` tag pulls the commit-hash image and re-tags it `X.Y.Z`/`X.Y`/`X`/`latest`, failing loud if the gated image is missing; tag pushes also trigger the GitHub mirror; documented in README → CI/CD.) -- [ ] CI/CD - Sync docker images to docker hub after each re-tag to git tags. +- [x] CI/CD - Sync docker images to docker hub after each re-tag to git tags. (`release.yml` pushes the same `X.Y.Z`/`X.Y`/`X`/`latest` tags to `docker.io/larvit/plainpages` after the Gitea re-tag — releases only, no hash tags; auth via the `DOCKERHUB_USER` variable + `DOCKERHUB_TOKEN` secret; documented in README → CI/CD.) - [ ] CI/CD - Setup renovate bot. - [ ] Add an e2e test for the admin plugin's OAuth2-clients (Hydra) screen. The full-flow e2e suite runs without Hydra (compose.full.yml), so /admin/clients register/detail/delete is only unit-covered (src/http/app.test.ts); wire Hydra into an e2e stack and drive the screen in the browser. - [ ] Build and publish docker image as CI/CD.