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.
154 lines
5.8 KiB
YAML
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
|