The fourth leg of build-and-test.yml, in the shape of the Android one: an image workflow that builds docker/windows and pushes it tagged by the directory's tree id, and a job inside that image that lints the Windows target — the only place the cfg(windows) branches are ever compiled by CI — builds, runs the smoke tests docs/windows.md §6 specifies, packages, installs and uninstalls under Wine, and uploads the installer. Every step was run by hand in the same container first. The spec's open list closes with this: the four §3.2 items, the licence page, and the leg. What remains is what Wine cannot show, and §10 now lists it as the first real Windows run's checklist.
171 lines
7.7 KiB
YAML
171 lines
7.7 KiB
YAML
name: '🐳 Windows image'
|
|
|
|
# Builds and pushes gitea.tourolle.paris/dtourolle/darkroom-windows, the job
|
|
# container for the Windows leg of build-and-test.yml.
|
|
#
|
|
# The same shape as android-image.yml, for the same reason that one exists:
|
|
# an image that lives only on a developer's laptop is a job that dies at
|
|
# `docker pull`. Built from docker/windows, tagged by that directory's tree
|
|
# id, skipped when the registry already has it.
|
|
#
|
|
# Called by build-and-test.yml on every push, and runnable by hand via
|
|
# workflow_dispatch. It is cheap when nothing changed — see the guard below.
|
|
on:
|
|
workflow_call:
|
|
inputs:
|
|
force:
|
|
description: 'Rebuild even if the registry already has this image ("true"/"false")'
|
|
type: string
|
|
default: 'false'
|
|
workflow_dispatch:
|
|
inputs:
|
|
force:
|
|
description: 'Rebuild even if the registry already has this image ("true"/"false")'
|
|
type: string
|
|
default: 'false'
|
|
|
|
# Gitea's act_runner mangles boolean workflow inputs passed through an
|
|
# expression — they arrive as false regardless of what was sent. Every input
|
|
# here is a string compared with == 'true', as in KPN's docker.yaml.
|
|
|
|
env:
|
|
IMAGE: gitea.tourolle.paris/dtourolle/darkroom-windows
|
|
|
|
jobs:
|
|
build:
|
|
runs-on: linux/amd64
|
|
name: Build and push
|
|
# Deliberately NOT in a container: this job needs the host Docker daemon to
|
|
# build an image, and the host's cached ~/.docker/config.json to push it.
|
|
# That is also why there is no `docker login` step — the runner host was
|
|
# authenticated to the registry during setup.
|
|
|
|
steps:
|
|
# The host has no Node, so the JS-based actions/checkout cannot run here.
|
|
# A minimal shallow fetch with plain git gets the same tree.
|
|
- name: Checkout
|
|
run: |
|
|
set -e
|
|
git init -q .
|
|
git remote add origin "${{ github.server_url }}/${{ github.repository }}.git"
|
|
git -c http.extraheader="AUTHORIZATION: basic $(printf '%s' '${{ github.actor }}:${{ github.token }}' | base64 -w0)" \
|
|
fetch --depth 1 origin "${{ github.sha }}"
|
|
git checkout -q FETCH_HEAD
|
|
|
|
# The image is tagged by the content of docker/windows, not by the commit
|
|
# that happened to touch it. `git rev-parse HEAD:<dir>` is the tree object
|
|
# id — it changes when and only when a file in that directory changes, so
|
|
# an unrelated push reuses the existing image and a Dockerfile edit can
|
|
# never silently keep serving a stale `latest`.
|
|
#
|
|
# Using the commit sha instead would rebuild 2.5 GB on every push; using a
|
|
# paths-filter action would need a container that has Node, and the only
|
|
# one this repo would reach for is the very image being built.
|
|
- name: Resolve image tag
|
|
id: tag
|
|
run: |
|
|
set -e
|
|
TREE=$(git rev-parse HEAD:docker/windows)
|
|
echo "tree=$TREE" >> "$GITHUB_OUTPUT"
|
|
echo "docker/windows tree: $TREE"
|
|
|
|
# Skip the build when the registry already holds this exact content. This
|
|
# is what keeps the job a few seconds long on a normal push, and what
|
|
# makes it self-healing: if the tag is missing for any reason, including
|
|
# the image having never been pushed at all, it gets built here.
|
|
#
|
|
# The probe is curl against the registry API, NOT `docker manifest
|
|
# inspect`. The latter exits 1 on this registry even for tags that are
|
|
# demonstrably present — jellytau-builder:latest answers HTTP 200 to the
|
|
# API while `docker manifest inspect` reports "manifest unknown" for it.
|
|
# Trusting that would have rebuilt 7 GB on every single push.
|
|
#
|
|
# A HEAD request also gives the digest for free, which is how the repoint
|
|
# decision below is made without pulling any layers.
|
|
- name: Query registry
|
|
id: check
|
|
env:
|
|
# The runner's own credentials, so this does not depend on how the
|
|
# host's ~/.docker/config.json happens to be set up.
|
|
REG_USER: ${{ github.actor }}
|
|
REG_PASS: ${{ github.token }}
|
|
TREE: ${{ steps.tag.outputs.tree }}
|
|
run: |
|
|
set -eu
|
|
ACCEPT='application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.v2+json,application/vnd.oci.image.manifest.v1+json,application/vnd.docker.distribution.manifest.list.v2+json'
|
|
API="https://gitea.tourolle.paris/v2/dtourolle/darkroom-windows/manifests"
|
|
|
|
# Prints "<http-status> <digest-or-empty>" for a tag.
|
|
probe() {
|
|
curl -sI -u "$REG_USER:$REG_PASS" -H "Accept: $ACCEPT" "$API/$1" \
|
|
| tr -d '\r' \
|
|
| awk 'BEGIN{s="000";d=""} /^HTTP/{s=$2} tolower($1)=="docker-content-digest:"{d=$2} END{print s, d}'
|
|
}
|
|
|
|
read -r TREE_STATUS TREE_DIGEST <<EOF
|
|
$(probe "$TREE")
|
|
EOF
|
|
read -r LATEST_STATUS LATEST_DIGEST <<EOF
|
|
$(probe latest)
|
|
EOF
|
|
|
|
echo "tag $TREE -> HTTP $TREE_STATUS ${TREE_DIGEST:-(no digest)}"
|
|
echo "tag latest -> HTTP $LATEST_STATUS ${LATEST_DIGEST:-(no digest)}"
|
|
|
|
# Build unless the registry definitively confirms this content is
|
|
# already there. An auth failure or an unreachable registry lands
|
|
# here too, and rebuilding needlessly is the safe direction to fail —
|
|
# skipping a build that was needed is what breaks the Windows job.
|
|
if [ "${{ inputs.force }}" = "true" ]; then
|
|
echo "forced rebuild requested"
|
|
echo "build=true" >> "$GITHUB_OUTPUT"
|
|
echo "repoint=false" >> "$GITHUB_OUTPUT"
|
|
elif [ "$TREE_STATUS" != "200" ]; then
|
|
echo "registry does not have this content — building"
|
|
echo "build=true" >> "$GITHUB_OUTPUT"
|
|
echo "repoint=false" >> "$GITHUB_OUTPUT"
|
|
elif [ -n "$TREE_DIGEST" ] && [ "$TREE_DIGEST" = "$LATEST_DIGEST" ]; then
|
|
echo "registry is already correct — nothing to do"
|
|
echo "build=false" >> "$GITHUB_OUTPUT"
|
|
echo "repoint=false" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "content is present but latest points elsewhere — repointing"
|
|
echo "build=false" >> "$GITHUB_OUTPUT"
|
|
echo "repoint=true" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
# Context is docker/windows, matching the README's build command. The
|
|
# Dockerfile COPYs nothing from the repo, so it needs no wider context —
|
|
# and a narrow context keeps the daemon from tarring up the whole tree,
|
|
# target/ included.
|
|
- name: Build
|
|
if: ${{ steps.check.outputs.build == 'true' }}
|
|
run: |
|
|
set -e
|
|
docker build \
|
|
-t "$IMAGE:${{ steps.tag.outputs.tree }}" \
|
|
-t "$IMAGE:latest" \
|
|
docker/windows
|
|
|
|
# Both tags are pushed: the tree tag is what the guard above looks for on
|
|
# the next run, and `latest` is what build-and-test.yml pulls.
|
|
- name: Push
|
|
if: ${{ steps.check.outputs.build == 'true' }}
|
|
run: |
|
|
set -e
|
|
docker push "$IMAGE:${{ steps.tag.outputs.tree }}"
|
|
docker push "$IMAGE:latest"
|
|
|
|
# A cache hit on the tree tag says nothing about where `latest` points — a
|
|
# reverted Dockerfile or a build from another branch can leave it on
|
|
# different content. This runs only when the digests above actually
|
|
# disagree, so the common case costs nothing; the layers are already in
|
|
# the registry, so the push that follows uploads a manifest, not 2.5 GB.
|
|
- name: Repoint latest
|
|
if: ${{ steps.check.outputs.repoint == 'true' }}
|
|
run: |
|
|
set -e
|
|
docker pull "$IMAGE:${{ steps.tag.outputs.tree }}"
|
|
docker tag "$IMAGE:${{ steps.tag.outputs.tree }}" "$IMAGE:latest"
|
|
docker push "$IMAGE:latest"
|