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:` 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 " " 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 < 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"