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>
174 lines
6.8 KiB
YAML
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"
|