Files
findr-api/.gitea/workflows/ci.yml
T
mars3142 ec7a6d57cb
CI / test (push) Successful in 4m7s
CI / build-and-push (push) Successful in 5m40s
CI / deploy (push) Successful in 6s
Deploy per Portainer-Webhook statt Polling
Nach erfolgreichem Push in findr/infrastructure den Stack-Webhook
aufrufen (PORTAINER_WEBHOOK-Secret), damit der Redeploy sofort
statt auf einen Timer passiert.

Signed-off-by: Peter Siegmund <mars3142@noreply.mars3142.dev>
2026-09-04 10:50:06 +02:00

174 lines
6.8 KiB
YAML

name: CI
# One run per branch or PR. A new commit supersedes the previous run instead of
# leaving it to finish, and a second merge into main cancels the deploy of the
# first one - two merges in a row otherwise build and push two images, of which
# only the last one matters.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
# Only main, so a push to a branch with an open PR does not run the same
# commit twice - branch pushes are already covered by the pull_request event.
push:
branches: [main]
pull_request:
jobs:
# Lint, types and unit tests. A red step stops the ones below it, so a broken
# commit never reaches the registry.
test:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '22'
cache: 'npm'
# The npm 10.9 bundled with node:22 aborts `npm ci` with EBADPLATFORM on
# esbuild's cross-platform optional packages (pulled in transitively via
# tsup). npm 11 handles them.
- name: Upgrade npm
run: npm install -g npm@11
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Typecheck
run: npm run typecheck
- name: Test
run: npm test
build-and-push:
runs-on: ubuntu-latest
timeout-minutes: 20
# Nothing reaches the registry unless lint, types and tests were green.
needs: [test]
# The deploy job has to write the tag into the stack. Passing it on rather
# than letting that job shorten the SHA a second time - two truncations that
# drift apart would pin the stack to an image nobody pushed.
outputs:
tag: ${{ steps.image.outputs.tag }}
steps:
- name: Checkout
uses: actions/checkout@v7
# IMAGE and TAG land in GITHUB_ENV rather than staying shell variables, so
# every step below refers to the same names instead of rebuilding them.
- name: Resolve image name
id: image
run: |
REGISTRY_DOMAIN=$(echo "${{ github.server_url }}" | sed -E 's|https?://||')
echo "REGISTRY_DOMAIN=$REGISTRY_DOMAIN" >> "$GITHUB_ENV"
echo "IMAGE=$REGISTRY_DOMAIN/${{ github.repository }}" >> "$GITHUB_ENV"
# Seven characters, the length git itself prints. Enough to stay
# unique in a repo this size and short enough to read in a compose
# file or a "docker ps" line.
TAG="${GITHUB_SHA::7}"
echo "TAG=$TAG" >> "$GITHUB_ENV"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "Image: $REGISTRY_DOMAIN/${{ github.repository }}:$TAG"
- name: Login to Gitea Registry
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY_DOMAIN }}
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}
# Runs on pull requests too: the test job covers the code, this covers the
# image and the Dockerfile, and both are worth knowing before the merge
# rather than after. The pushes below are what stays behind main.
- name: Build image
run: docker build -t "$IMAGE:$TAG" -t "$IMAGE:latest" .
# An immutable tag: it makes rollbacks possible and shows which commit is
# currently running.
- name: Push commit tag
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
run: docker push "$IMAGE:$TAG"
# Last, so latest never points at a half-finished image.
- name: Push latest tag
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
run: docker push "$IMAGE:latest"
# The stack lives in the infrastructure repo, so a rollout is a commit in that
# repo: it pins the findr-api image to this build's commit. Then the Portainer
# stack webhook is called, which re-pulls the repo and recreates the one
# service whose image changed. Leaving the stack on ":latest" and only
# re-pulling would drag findr-web and postgres along with every api deploy, and
# nothing in Git would say which version is running.
deploy:
runs-on: ubuntu-latest
timeout-minutes: 10
needs: [build-and-push]
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
# The token goes into the remote URL, so it must not be echoed - it can
# write to the whole infrastructure repo. No "set -x" here either.
- name: Pin findr-api image and trigger the stack
env:
INFRA_TOKEN: ${{ secrets.INFRA_TOKEN }}
PORTAINER_WEBHOOK: ${{ secrets.PORTAINER_WEBHOOK }}
TAG: ${{ needs.build-and-push.outputs.tag }}
run: |
set -euo pipefail
REGISTRY_DOMAIN=$(echo "${{ github.server_url }}" | sed -E 's|https?://||')
# An empty output would produce "findr-api:" and pin the stack to a tag
# that cannot exist. Better to stop here than to hand that to Portainer.
test -n "$TAG"
# Not a shallow clone: the push below may have to rebase, which a
# depth-1 clone cannot always do. The repo is a handful of commits.
git clone "https://$INFRA_TOKEN@$REGISTRY_DOMAIN/findr/infrastructure.git" infrastructure
cd infrastructure
git config user.name 'Gitea Actions'
git config user.email 'actions@git.mars3142.dev'
sed -i -E "s|(image: $REGISTRY_DOMAIN/findr/findr-api:).*|\1$TAG|" docker-compose.yml
# A sed that matches nothing still exits 0. Without this check a moved
# or renamed image would leave the previous tag in place and deploy the
# old build under a green job.
grep -q "image: $REGISTRY_DOMAIN/findr/findr-api:$TAG" docker-compose.yml
# Re-running a build writes the same tag again. Nothing to commit then,
# and nothing to deploy either - the stack is already on this image.
if git diff --quiet; then
echo "Stack already pinned to $TAG"
exit 0
fi
git commit -s -am "Deploy findr-api $TAG"
# findr-web writes to the same file, so a push can lose a race.
# Rebasing and retrying beats failing a build whose image is already in
# the registry.
pushed=
for attempt in 1 2 3; do
if git push; then pushed=1; break; fi
git pull --rebase
done
[ -n "$pushed" ] || exit 1
# No polling: tell Portainer to redeploy now. -k because :9443 serves a
# self-signed cert. The URL carries a secret, so it comes from a secret
# and is never printed.
if [ -z "${PORTAINER_WEBHOOK:-}" ]; then
echo "::error::PORTAINER_WEBHOOK secret is not set" && exit 1
fi
curl -fsS -k -X POST "$PORTAINER_WEBHOOK"