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.
160 lines
5.7 KiB
YAML
160 lines
5.7 KiB
YAML
name: Web Build and Push Docker Images
|
|
# Rolling `dev` / sha images for the FTStore Admin server (web/). Versioned
|
|
# release images are built by web-release.yml, so there is deliberately no tag
|
|
# trigger here: GitHub does not document whether a `paths` filter is evaluated
|
|
# for tag pushes, and a release build must not hinge on undocumented behaviour.
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
paths:
|
|
- "web/**"
|
|
# The app image compiles zsign from here, so a change to it has to
|
|
# rebuild the image even though nothing under web/ moved.
|
|
- "manager/Zsign/**"
|
|
- ".dockerignore"
|
|
- ".github/workflows/web-docker-build.yml"
|
|
pull_request:
|
|
branches: [main]
|
|
paths:
|
|
- "web/**"
|
|
# The app image compiles zsign from here, so a change to it has to
|
|
# rebuild the image even though nothing under web/ moved.
|
|
- "manager/Zsign/**"
|
|
- ".dockerignore"
|
|
- ".github/workflows/web-docker-build.yml"
|
|
|
|
env:
|
|
REGISTRY: ghcr.io
|
|
IMAGE_PREFIX: ghcr.io/${{ github.repository_owner }}/ftstore-admin
|
|
|
|
jobs:
|
|
build-app:
|
|
name: Build App Image
|
|
runs-on: [self-hosted, linux, x64]
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
steps:
|
|
- uses: actions/checkout@v5
|
|
with:
|
|
# manager/Zsign is a submodule, and the zsign stage compiles from it.
|
|
# Without this the directory checks out empty and the build dies on
|
|
# `make -C Zsign/build/linux`. The worker job needs no submodules.
|
|
submodules: true
|
|
|
|
- uses: docker/setup-buildx-action@v3
|
|
|
|
# A plain `docker login` with retries rather than `docker/login-action`,
|
|
# which has none. The two image jobs run on the same self-hosted runner and
|
|
# reach for ghcr.io in the same second, and one of the pair came back with
|
|
# a TLS handshake timeout: a whole build lost on a step that costs nothing
|
|
# to try again. Five attempts, backing off, then the failure is real.
|
|
- name: Log in to GHCR
|
|
if: github.event_name != 'pull_request'
|
|
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: Extract metadata
|
|
id: meta
|
|
uses: docker/metadata-action@v5
|
|
with:
|
|
images: ${{ env.IMAGE_PREFIX }}-app
|
|
tags: |
|
|
type=sha
|
|
type=ref,event=branch
|
|
type=raw,value=dev,enable={{is_default_branch}}
|
|
|
|
- name: Build and push app image
|
|
uses: docker/build-push-action@v6
|
|
with:
|
|
# Repo root, not ./web: the image compiles zsign from manager/Zsign.
|
|
# The root .dockerignore is what keeps that context small.
|
|
context: .
|
|
file: ./web/Dockerfile
|
|
push: ${{ github.event_name != 'pull_request' }}
|
|
tags: ${{ steps.meta.outputs.tags }}
|
|
labels: ${{ steps.meta.outputs.labels }}
|
|
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
|
|
|
|
build-worker:
|
|
name: Build Worker Image
|
|
runs-on: [self-hosted, linux, x64]
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
steps:
|
|
- uses: actions/checkout@v5
|
|
|
|
- uses: docker/setup-buildx-action@v3
|
|
|
|
# A plain `docker login` with retries rather than `docker/login-action`,
|
|
# which has none. The two image jobs run on the same self-hosted runner and
|
|
# reach for ghcr.io in the same second, and one of the pair came back with
|
|
# a TLS handshake timeout: a whole build lost on a step that costs nothing
|
|
# to try again. Five attempts, backing off, then the failure is real.
|
|
- name: Log in to GHCR
|
|
if: github.event_name != 'pull_request'
|
|
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: Extract metadata
|
|
id: meta
|
|
uses: docker/metadata-action@v5
|
|
with:
|
|
images: ${{ env.IMAGE_PREFIX }}-worker
|
|
tags: |
|
|
type=sha
|
|
type=ref,event=branch
|
|
type=raw,value=dev,enable={{is_default_branch}}
|
|
|
|
- name: Build and push worker image
|
|
uses: docker/build-push-action@v6
|
|
with:
|
|
context: ./web
|
|
file: ./web/Dockerfile.worker
|
|
push: ${{ github.event_name != 'pull_request' }}
|
|
tags: ${{ steps.meta.outputs.tags }}
|
|
labels: ${{ steps.meta.outputs.labels }}
|
|
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
|