Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions .github/workflows/publish-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ on:
tags:
- "v*"
workflow_dispatch:
inputs:
candidate_only:
description: Publish only a commit-addressed Classic review candidate
required: true
type: boolean
default: false

permissions:
contents: read
Expand Down Expand Up @@ -48,6 +54,37 @@ jobs:
echo EOF
} >> "${GITHUB_OUTPUT}"

- name: Select Classic image tags
id: classic-metadata
env:
IMAGE_NAME: classic-build
run: |
image="ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/${IMAGE_NAME}"
if [[ "${{ inputs.candidate_only }}" == true ]]; then
tags="${image}:candidate-sha-${GITHUB_SHA}"
else
tags="${image}:latest
${image}:ubuntu-26.04
${image}:sha-${GITHUB_SHA}"
fi

if [[ "${{ inputs.candidate_only }}" != true \
&& "${GITHUB_REF_TYPE}" == tag ]]; then
if [[ ! "${GITHUB_REF_NAME}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Invalid image release tag: ${GITHUB_REF_NAME}" >&2
exit 1
fi
tags="${tags}
${image}:${GITHUB_REF_NAME#v}"
fi

{
echo "image=${image}"
echo 'tags<<EOF'
echo "${tags}"
echo EOF
} >> "${GITHUB_OUTPUT}"

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4

Expand All @@ -58,7 +95,22 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

# Prove both Linux-owned image contracts before moving any release tag.
- name: Build and smoke Classic validation target
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
with:
context: .
file: linux/Dockerfile
target: classic-validation
platforms: linux/amd64
outputs: type=cacheonly
cache-from: |
type=registry,ref=ghcr.io/atrinik/classic-build:ubuntu-26.04
type=gha,scope=classic-build-image
cache-to: type=gha,mode=max,scope=classic-build-image,ignore-error=true

- name: Build and publish Linux image
if: ${{ !inputs.candidate_only }}
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
with:
context: .
Expand All @@ -76,3 +128,25 @@ jobs:
cache-to: |
type=inline
type=gha,mode=max,scope=linux-build-image,ignore-error=true

- name: Build and publish Classic image
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
with:
context: .
file: linux/Dockerfile
target: classic-final
platforms: linux/amd64
pull: true
push: true
tags: ${{ steps.classic-metadata.outputs.tags }}
labels: |
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}
sbom: true
provenance: mode=max
cache-from: |
type=registry,ref=ghcr.io/atrinik/classic-build:ubuntu-26.04
type=gha,scope=classic-build-image
cache-to: |
type=inline
type=gha,mode=max,scope=classic-build-image,ignore-error=true
119 changes: 104 additions & 15 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ jobs:
name: Select changed images
runs-on: ubuntu-26.04
outputs:
classic: ${{ steps.changes.outputs.classic }}
linux: ${{ steps.changes.outputs.linux }}
windows: ${{ steps.changes.outputs.windows }}
steps:
Expand All @@ -29,6 +30,7 @@ jobs:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
classic=false
linux=false
windows=false
while IFS= read -r path; do
Expand All @@ -37,31 +39,53 @@ jobs:
.github/workflows/* | linux/* | \
audio-toolchain.json | audio-toolchain.spdx.json | \
toolchains.json | tools/audio/* | \
tools/build-sdl3-mixer.sh | tools/validate-toolchains.sh)
tools/build-sdl3-mixer.sh | \
tools/require-image-checks.sh | \
tools/test-require-image-checks.sh | \
tools/validate-toolchains.sh)
linux=true
;;
esac
case "${path}" in
.dockerignore | .github/workflows/publish-linux.yml | \
.github/workflows/validate.yml | linux/* | \
audio-toolchain.json | audio-toolchain.spdx.json | \
classic-packages.lock | classic-toolchain.json | \
tools/audio/* | tools/build-sdl3-mixer.sh | \
tools/require-image-checks.sh | \
tools/test-require-image-checks.sh | \
tools/validate-classic-check.sh | \
tools/validate-classic-image.sh)
classic=true
;;
esac
case "${path}" in
.dockerignore | .github/workflows/measure-classic-check.yml | \
.github/workflows/publish-windows.yml | \
.github/workflows/validate.yml | windows/* | \
audio-toolchain.json | audio-toolchain.spdx.json | \
tools/audio/* | \
tools/build-sdl3-mixer.sh | \
tools/measure-classic-check-images.sh | \
tools/require-image-checks.sh | \
tools/smoke-classic-check.sh | \
tools/test-require-image-checks.sh | \
tools/validate-toolchains.sh | \
tools/verify-classic-check-package.py | \
tools/verify-pe-imports.sh)
windows=true
;;
esac
done < <(git diff --name-only "${BASE_SHA}" "${HEAD_SHA}")
done < <(git diff --no-renames --name-only "${BASE_SHA}" "${HEAD_SHA}")
{
echo "classic=${classic}"
echo "linux=${linux}"
echo "windows=${windows}"
} >> "${GITHUB_OUTPUT}"

- name: Test required-check aggregation
run: tools/test-require-image-checks.sh

linux:
name: Linux image
needs: changes
Expand Down Expand Up @@ -225,33 +249,98 @@ jobs:
}
}

classic:
name: Classic CI image
needs: changes
if: needs.changes.outputs.classic == 'true'
runs-on: ubuntu-26.04
timeout-minutes: 60
steps:
- name: Check out image sources
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Read pinned Classic validation revision
id: consumer
run: echo "commit=$(jq -r '.consumer_validation.commit' classic-toolchain.json)" >> "${GITHUB_OUTPUT}"

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4

- name: Build and smoke Classic validation target
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
with:
context: .
file: linux/Dockerfile
target: classic-validation
platforms: linux/amd64
outputs: type=cacheonly
cache-from: type=gha,scope=classic-build-image
cache-to: type=gha,mode=max,scope=classic-build-image,ignore-error=true

- name: Load slim Classic image
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
with:
context: .
file: linux/Dockerfile
target: classic-final
platforms: linux/amd64
load: true
tags: atrinik-classic-build:validation
cache-from: type=gha,scope=classic-build-image

- name: Check out pinned Classic sources
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: atrinik/classic
ref: ${{ steps.consumer.outputs.commit }}
path: classic

- name: Run Classic client and server checks as the runner user
run: |
install -d -m 1777 "${RUNNER_TEMP}/classic-ccache"
docker run --rm \
--user "$(id -u):$(id -g)" \
--env CCACHE_DIR=/cache/ccache \
--env HOME=/tmp/classic-home \
--volume "${GITHUB_WORKSPACE}:/image-source:ro" \
--volume "${GITHUB_WORKSPACE}/classic:/workspace" \
--volume "${RUNNER_TEMP}/classic-ccache:/cache/ccache" \
--workdir /workspace \
atrinik-classic-build:validation \
/image-source/tools/validate-classic-check.sh /workspace

required:
name: Required checks
needs:
- changes
- classic
- linux
- windows
- windows-native
if: always()
permissions: {}
permissions:
contents: read
runs-on: ubuntu-26.04
steps:
- name: Check out required-check helper
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Require successful applicable validations
env:
CHANGES_RESULT: ${{ needs.changes.result }}
CLASSIC_SELECTED: ${{ needs.changes.outputs.classic }}
CLASSIC_RESULT: ${{ needs.classic.result }}
LINUX_SELECTED: ${{ needs.changes.outputs.linux }}
LINUX_RESULT: ${{ needs.linux.result }}
WINDOWS_SELECTED: ${{ needs.changes.outputs.windows }}
WINDOWS_RESULT: ${{ needs.windows.result }}
WINDOWS_NATIVE_RESULT: ${{ needs.windows-native.result }}
run: |
if [[ "${CHANGES_RESULT}" != success ]]; then
echo "Change selection concluded ${CHANGES_RESULT}." >&2
exit 1
fi

for result in "${LINUX_RESULT}" "${WINDOWS_RESULT}" \
"${WINDOWS_NATIVE_RESULT}"; do
if [[ "${result}" != success && "${result}" != skipped ]]; then
echo "Image validation concluded ${result}." >&2
exit 1
fi
done
tools/require-image-checks.sh \
"${CHANGES_RESULT}" \
"${CLASSIC_SELECTED}" "${CLASSIC_RESULT}" \
"${LINUX_SELECTED}" "${LINUX_RESULT}" \
"${WINDOWS_SELECTED}" "${WINDOWS_RESULT}" \
"${WINDOWS_NATIVE_RESULT}"
22 changes: 18 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,31 @@
- Treat Dockerfile inputs, `.dockerignore`, cache scopes, build arguments,
published tags, and workflow path filters as one contract. If a relevant file
changes, the required aggregate validation must still run.
- Every semantic release publishes the Linux image plus the general and
task-focused Windows images with their supported tags. Keep the
- `classic-final` is the slim Classic Check target. Keep its Ubuntu snapshot,
direct package lock, tool inventory, non-root ccache mount, Classic validation
revision, smoke/SBOM checks, and published tags synchronized. Do not make it
inherit the broad replacement/development toolchain.
- Keep a stable numeric runner UID when restoring a Classic ccache directory;
the mode-1777 mount root supports non-root initialization but does not make
ccache's owner-writable nested directories reusable across different UIDs.
- A Linux `candidate_only` dispatch is the pre-merge Classic review path. It
must publish only `classic-build:candidate-sha-<commit>` after validation and
must never move a rolling, platform, or version tag.
- Every semantic release publishes the broad Linux, slim Linux Classic,
general Windows, and task-focused Windows Classic images with their
supported tags. The Linux publisher owns `linux-build` and `classic-build`;
the Windows publisher owns both `windows-build` variants. Keep the
`classic-check` target branched from the expensive shared MXE foundation
before general-image Python/worldmaker additions, preserve the general
Windows image as the default Dockerfile result, validate immutable SHA
candidates before promoting rolling/version aliases, and recover partial
alias promotion by rerunning the failed job from the same workflow run. Do
not create manual release tags as a substitute for semantic-release.
- Validate Dockerfiles with `docker build --check`. Build and smoke-test each
affected image; note that a cold Windows/MXE build is expensive and may rely
on CI cache for final verification.
affected image, including `classic-validation` before `classic-final` and the
Windows `classic-check` target plus native bundle; note that a cold
Windows/MXE build is expensive and may rely on CI cache for final
verification.
- Workflow changes also require actionlint and Atrinik GitHub-governance review
for permissions, pinned actions, check names, and ruleset compatibility.
Never expose private-package permissions or images to `pull_request` code;
Expand Down
7 changes: 4 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

Use a Conventional Commits pull-request title and run the image checks
documented in `README.md`. Keep image inputs reproducible and avoid embedding
credentials or host-specific state. Every squash merge releases the Linux,
general Windows, and task-focused Classic Check images, so changes must leave
both publishing workflows valid even when only one Dockerfile is edited.
credentials or host-specific state. Every squash merge releases the broad
Linux, slim Linux Classic, general Windows, and task-focused Windows Classic
Check images, so changes must leave both publishing workflows valid even when
only one Dockerfile is edited.
Loading