Skip to content

Build and publish

Build and publish #11

Workflow file for this run

name: Build and publish
# One multi-platform build on a single runner. Nothing is compiled — each
# platform's stage downloads the matching RustCFML release binary — so the
# arm64 half runs under QEMU only for `rustcfml --version` and the smoke test.
#
# tag vX.Y.Z[-N] -> :vX.Y.Z-N (if N), :vX.Y.Z, :X.Y, :latest
# push to main -> :edge
# pull request -> build only, no push
# manual -> any engine version, pushed as :edge (or :vX.Y.Z when it is a release)
on:
push:
branches: [main]
tags: ["v*"]
pull_request:
workflow_dispatch:
inputs:
rustcfml_version:
description: "RustCFML release tag to package (e.g. v0.653.3). Defaults to the Dockerfile's ARG."
required: false
permissions:
contents: read
packages: write
env:
IMAGE: ghcr.io/${{ github.repository_owner }}/rustcfml
jobs:
build:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v5
- name: Resolve the engine version
id: ver
run: |
DEFAULT=$(sed -nE 's/^ARG RUSTCFML_VERSION=(v[0-9.]+).*/\1/p' Dockerfile | head -1)
VERSION="${{ github.event.inputs.rustcfml_version }}"
VERSION="${VERSION:-$DEFAULT}"
if [ "${{ github.ref_type }}" = "tag" ]; then
# v0.653.3-2 -> engine v0.653.3, image build 2
BASE="${GITHUB_REF_NAME%-*}"
if [ "$BASE" != "$DEFAULT" ]; then
echo "::error::git tag ${GITHUB_REF_NAME} does not match ARG RUSTCFML_VERSION=${DEFAULT} in the Dockerfile"; exit 1
fi
VERSION="$BASE"
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "image=$(echo '${{ env.IMAGE }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT"
echo "Packaging RustCFML $VERSION"
- name: Compute tags
id: tags
run: |
IMAGE='${{ steps.ver.outputs.image }}'
V='${{ steps.ver.outputs.version }}'
if [ "${{ github.ref_type }}" = "tag" ]; then
MINOR="${V#v}"; MINOR="${MINOR%.*}"
TAGS="$IMAGE:$V,$IMAGE:$MINOR,$IMAGE:latest"
[ "$GITHUB_REF_NAME" != "$V" ] && TAGS="$IMAGE:$GITHUB_REF_NAME,$TAGS"
else
TAGS="$IMAGE:edge"
fi
echo "tags=$TAGS" >> "$GITHUB_OUTPUT"
echo "$TAGS"
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
if: github.event_name != 'pull_request'
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build (and push)
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
build-args: |
RUSTCFML_VERSION=${{ steps.ver.outputs.version }}
tags: ${{ steps.tags.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max
labels: |
org.opencontainers.image.title=RustCFML
org.opencontainers.image.description=RustCFML ${{ steps.ver.outputs.version }} — a CFML engine written in Rust
org.opencontainers.image.version=${{ steps.ver.outputs.version }}
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
org.opencontainers.image.url=https://github.com/RustCFML/RustCFML
org.opencontainers.image.licenses=MIT
- name: Inspect the manifest
if: github.event_name != 'pull_request'
run: docker buildx imagetools inspect "${{ steps.ver.outputs.image }}:${{ github.ref_type == 'tag' && steps.ver.outputs.version || 'edge' }}"
# A published image nobody can pull is not published. GHCR creates a
# container package PRIVATE on first push regardless of how public the
# repository is, and nothing in the push reports that — the build goes
# green, the manifest inspects fine (the runner is authenticated), and the
# `FROM ghcr.io/…` line in our own README fails for every user and for any
# CI that tries to build on it. This step is the check that was missing:
# it asks the registry for an ANONYMOUS pull token and fetches the
# manifest with it, exactly as a stranger would.
- name: Can a stranger pull it?
if: github.event_name != 'pull_request'
run: |
set -uo pipefail
REPO=$(echo "${{ github.repository_owner }}/rustcfml" | tr '[:upper:]' '[:lower:]')
REF='${{ github.ref_type == 'tag' && steps.ver.outputs.version || 'edge' }}'
# A PRIVATE package refuses the anonymous token itself, so a missing
# token is the answer — do not let `set -e` and `curl -f` turn that
# into a bare curl error.
TOKEN=$(curl -sS "https://ghcr.io/token?scope=repository:${REPO}:pull&service=ghcr.io" \
| jq -r '.token // empty' 2>/dev/null || true)
if [ -n "$TOKEN" ]; then
CODE=$(curl -sS -o /dev/null -w '%{http_code}' \
-H "Authorization: Bearer ${TOKEN}" \
-H 'Accept: application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json' \
"https://ghcr.io/v2/${REPO}/manifests/${REF}")
else
CODE="401 — the registry would not even issue an anonymous pull token"
fi
if [ "$CODE" = "200" ]; then
echo "ghcr.io/${REPO}:${REF} is publicly pullable."
exit 0
fi
echo "::warning title=Image is not publicly pullable::Anonymous pull of ghcr.io/${REPO}:${REF} gave ${CODE}. The image is published but nobody can use it."
{
echo "### :warning: \`ghcr.io/${REPO}\` is not publicly pullable"
echo
echo "An anonymous pull gave **${CODE}**. The push succeeded, but every"
echo "\`docker pull\`, every \`FROM\` in a downstream Dockerfile and every CI"
echo "builder — including Fly.io's — gets \`unauthorized\`."
echo
echo "GHCR creates a container package **private on first publish**, whatever the"
echo "repository's visibility, and there is no REST API to change it. Fix it once,"
echo "by hand:"
echo
echo "1. <https://github.com/organizations/${{ github.repository_owner }}/settings/packages> → *Package creation* → tick **Public**."
echo " (Until this is on, the option in step 2 is greyed out and does not say why.)"
echo "2. <https://github.com/orgs/${{ github.repository_owner }}/packages/container/rustcfml/settings> → *Danger Zone* → **Change package visibility → Public**."
echo
echo "It stays public for every later push."
} >> "$GITHUB_STEP_SUMMARY"