Files
Felitendo 90d169130e ci: retry the container registry login
The rolling web build lost a run to ghcr.io answering the login with a TLS
handshake timeout. Nothing was wrong with the code: the app image had already
been built and pushed, and the worker image job failed before it started, on
the one step in the whole workflow that had no way to try again. Both jobs run
on the same self-hosted runner and reach for the registry in the same second,
which is presumably why one of the pair got the timeout and the other did not.

So the login is a plain `docker login` with five attempts and a backoff rather
than an action with no retry in it. The release workflow gets the same
treatment and needs it more: it is dispatched by hand and has already tagged
the release by the time it reaches this step.

Logging in by hand means logging out by hand. The action did that in a post
step, and this runner is self-hosted, so without it the credentials would sit
in the docker config after the job is over.
2026-09-03 07:31:35 +02:00

154 lines
5.8 KiB
YAML

name: Web Release
# Releases the FTStore Admin server (web/). Git tags are namespaced `web-v*` so
# they never collide with the app's `v*` tags, which ios-release.yml and
# app-repo.json own. The Docker images keep the plain `vX.Y` tag.
on:
workflow_dispatch:
inputs:
version:
description: "Version tag (e.g. web-v0.1, web-v1.0.0)"
required: true
type: string
env:
REGISTRY: ghcr.io
IMAGE_PREFIX: ghcr.io/${{ github.repository_owner }}/ftstore-admin
jobs:
release:
name: Build, Push & Release
runs-on: [self-hosted, linux, x64]
permissions:
contents: write
packages: write
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
# The app image compiles zsign from the manager/Zsign submodule, so
# it has to come down with the checkout or the build fails.
submodules: true
- name: Validate version format
id: version
run: |
if [[ ! "${{ inputs.version }}" =~ ^web-v[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then
echo "::error::Version must match web-vX.Y or web-vX.Y.Z (e.g. web-v0.1, web-v1.0.0)"
exit 1
fi
# The image tag drops the namespace prefix: the registry only ever
# holds these images, so ftstore-admin-app:v0.1 reads better than
# ftstore-admin-app:web-v0.1 — and matches the tags already pushed.
IMAGE_TAG="${{ inputs.version }}"
echo "image_tag=${IMAGE_TAG#web-}" >> "$GITHUB_OUTPUT"
- name: Check tag doesn't already exist
run: |
if git rev-parse "${{ inputs.version }}" >/dev/null 2>&1; then
echo "::error::Tag ${{ inputs.version }} already exists"
exit 1
fi
- name: Generate changelog
id: changelog
run: |
# --match keeps the app's `v*` tags out: without it the previous
# iOS release would be picked as the baseline and the changelog would
# span every commit since, most of them unrelated to web/.
PREVIOUS_TAG=$(git describe --tags --abbrev=0 --match 'web-v*' 2>/dev/null || echo "")
if [ -z "$PREVIOUS_TAG" ]; then
COMMITS=$(git log --pretty=format:"- %s (%h)" --no-merges -- web '.github/workflows/web-*.yml')
COMPARE_TEXT="Initial release"
else
COMMITS=$(git log "${PREVIOUS_TAG}..HEAD" --pretty=format:"- %s (%h)" --no-merges -- web '.github/workflows/web-*.yml')
COMPARE_TEXT="**Full changelog**: [\`${PREVIOUS_TAG}...${{ inputs.version }}\`](https://github.com/${{ github.repository }}/compare/${PREVIOUS_TAG}...${{ inputs.version }})"
fi
{
echo "body<<CHANGELOG_EOF"
echo "## What's Changed"
echo ""
echo "$COMMITS"
echo ""
echo "---"
echo ""
echo "### Docker Images"
echo ""
echo "\`\`\`bash"
echo "docker pull ${{ env.IMAGE_PREFIX }}-app:${{ steps.version.outputs.image_tag }}"
echo "docker pull ${{ env.IMAGE_PREFIX }}-worker:${{ steps.version.outputs.image_tag }}"
echo "\`\`\`"
echo ""
echo "$COMPARE_TEXT"
echo "CHANGELOG_EOF"
} >> "$GITHUB_OUTPUT"
- name: Create and push tag
run: |
git tag "${{ inputs.version }}"
git push origin "${{ inputs.version }}"
- uses: docker/setup-buildx-action@v3
# A plain `docker login` with retries rather than `docker/login-action`,
# which has none. ghcr.io answered one of the rolling builds with a TLS
# handshake timeout and took the whole run with it, on a step that costs
# nothing to try again. It matters more here than there: this one is
# dispatched by hand and has already tagged the release by the time it
# gets this far. Five attempts, backing off, then the failure is real.
- name: Log in to GHCR
env:
GHCR_USER: ${{ github.actor }}
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
for attempt in 1 2 3 4 5; do
if printf '%s' "$GHCR_TOKEN" \
| docker login ghcr.io --username "$GHCR_USER" --password-stdin
then
exit 0
fi
echo "ghcr.io login attempt $attempt failed, waiting $((attempt * 5))s"
sleep $((attempt * 5))
done
echo "ghcr.io refused five logins in a row" >&2
exit 1
- name: Build and push app image
uses: docker/build-push-action@v6
with:
# Repo root, not ./web: the image compiles zsign from manager/Zsign.
context: .
file: ./web/Dockerfile
push: true
tags: |
${{ env.IMAGE_PREFIX }}-app:${{ steps.version.outputs.image_tag }}
${{ env.IMAGE_PREFIX }}-app:latest
no-cache: true
- name: Build and push worker image
uses: docker/build-push-action@v6
with:
context: ./web
file: ./web/Dockerfile.worker
push: true
tags: |
${{ env.IMAGE_PREFIX }}-worker:${{ steps.version.outputs.image_tag }}
${{ env.IMAGE_PREFIX }}-worker:latest
no-cache: true
# The runner is self-hosted, so without this the credentials stay in
# ~/.docker/config.json after the job is over. `docker/login-action` did
# this in a post step; logging in by hand means logging out by hand.
- name: Log out of GHCR
if: always()
run: docker logout ghcr.io
- name: Create GitHub Release
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ inputs.version }}
name: ${{ inputs.version }}
body: ${{ steps.changelog.outputs.body }}
generate_release_notes: false