Build and push Docker images #657
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: Build and push Docker images | |
on: | |
# Build images daily | |
schedule: | |
- cron: 0 0 * * * | |
push: | |
branches: | |
- master | |
paths: | |
- docker/** | |
pull_request: | |
branches: | |
- master | |
paths: | |
- docker/** | |
workflow_dispatch: | |
inputs: | |
force-rebuild: | |
type: boolean | |
description: Force rebuild (even if commit hash has already been built) | |
default: false | |
env: | |
REGISTRY: ghcr.io | |
IMAGE_NAME_SERVER: ${{ github.repository_owner }}/vmangos-server | |
IMAGE_NAME_DATABASE: ${{ github.repository_owner }}/vmangos-database | |
OCI_ANNOTATION_AUTHORS: ${{ vars.OCI_ANNOTATION_AUTHORS || github.repository_owner }} | |
OCI_ANNOTATION_URL: https://github.com/${{ github.repository }} | |
OCI_ANNOTATION_DOCUMENTATION: https://github.com/${{ github.repository }}/blob/master/README.md | |
OCI_ANNOTATION_SOURCE: https://github.com/${{ github.repository }} | |
OCI_ANNOTATION_VENDOR: ${{ vars.OCI_ANNOTATION_VENDOR || github.repository_owner }} | |
OCI_ANNOTATION_LICENSES: GPL-2.0 | |
OCI_ANNOTATION_SERVER_TITLE: vmangos-deploy - VMaNGOS server image | |
OCI_ANNOTATION_SERVER_DESCRIPTION: VMaNGOS is a server emulator supporting versions 1.6.1 to 1.12.1. | |
OCI_ANNOTATION_SERVER_BASE_NAME: ubuntu:24.04 | |
OCI_ANNOTATION_DATABASE_TITLE: vmangos-deploy - VMaNGOS database image | |
OCI_ANNOTATION_DATABASE_DESCRIPTION: Database for the VMaNGOS server emulator. | |
OCI_ANNOTATION_DATABASE_BASE_NAME: mariadb:11.4 | |
jobs: | |
setup: | |
name: Setup | |
runs-on: ubuntu-24.04 | |
outputs: | |
commit-hash: ${{ steps.latest-commit-hash.outputs.result }} | |
already-built: ${{ steps.already-built.outputs.result }} | |
steps: | |
- name: Get latest commit hash | |
uses: actions/github-script@v7 | |
id: latest-commit-hash | |
with: | |
result-encoding: string | |
retries: 3 | |
script: | | |
const ref = await github.rest.git.getRef({ | |
owner: 'vmangos', | |
repo: 'core', | |
ref: 'heads/development', | |
}) | |
return ref.data.object.sha | |
# We check if the workflow was triggered by a schedule and if today is | |
# Monday, in which case we want to trigger a (re)build in any case (to | |
# keep the Docker images up-to-date). | |
# | |
# We also check if the "force rebuild" input is checked when the workflow | |
# is triggered manually; if yes, we also want to (re)build. | |
- name: Determine if images for commit hash have already been built | |
id: already-built | |
run: | | |
if [[ "${{ github.event_name }}" == "schedule" ]]; then | |
if [[ "$(date +%u)" -eq 1 ]]; then | |
echo "result=false" >> $GITHUB_OUTPUT | |
exit 0 | |
fi | |
fi | |
if [[ "${{ inputs.force-rebuild }}" == "true" ]]; then | |
echo "result=false" >> $GITHUB_OUTPUT | |
exit 0 | |
fi | |
ghcr_token=$(echo ${{ secrets.GITHUB_TOKEN }} | base64) | |
tags=$(skopeo list-tags --registry-token ${ghcr_token} docker://${{ env.REGISTRY }}/${{ env.IMAGE_NAME_DATABASE }} | jq '.Tags[]') | |
if grep -q "${{ steps.latest-commit-hash.outputs.result }}" <<< "${tags}"; then | |
echo "result=true" >> $GITHUB_OUTPUT | |
exit 0 | |
fi | |
echo "result=false" >> $GITHUB_OUTPUT | |
# If the original MariaDB entrypoint script changes, we can't guarantee | |
# that our custom version (which relies on functions defined in the | |
# original) will still work, so we fail the workflow run at this point. | |
- name: Check if MariaDB entrypoint script has been updated | |
id: mariadb-entrypoint-check | |
run: | | |
known_entrypoint=https://raw.githubusercontent.com/MariaDB/mariadb-docker/64252135052f269ed2bb57134ad73537e93b7ab6/11.4/docker-entrypoint.sh | |
latest_entrypoint=https://raw.githubusercontent.com/MariaDB/mariadb-docker/master/11.4/docker-entrypoint.sh | |
curl -o known-entrypoint.sh $known_entrypoint | |
curl -o latest-entrypoint.sh $latest_entrypoint | |
diff known-entrypoint.sh latest-entrypoint.sh | |
build-and-push-server-images: | |
name: Build and push server images | |
needs: setup | |
if: ${{ needs.setup.outputs.already-built != 'true' }} | |
runs-on: ubuntu-24.04 | |
strategy: | |
fail-fast: true | |
matrix: | |
client-version: [5875, 5464, 5302, 5086, 4878, 4695, 4544] | |
permissions: | |
contents: read | |
packages: write | |
steps: | |
- name: Checkout repository | |
uses: actions/checkout@v4 | |
- name: Set up QEMU | |
uses: docker/setup-qemu-action@v3 | |
# Work around QEMU 7.x compilation issues by explicitly using 8.x, | |
# which is not yet the default for the action. | |
# See https://github.com/tonistiigi/binfmt/issues/215 | |
with: | |
image: tonistiigi/binfmt:qemu-v8.1.5 | |
- name: Set up Docker Buildx | |
uses: docker/setup-buildx-action@v3 | |
- name: Log in to container registry | |
uses: docker/login-action@v3 | |
with: | |
registry: ${{ env.REGISTRY }} | |
username: ${{ github.actor }} | |
password: ${{ secrets.GITHUB_TOKEN }} | |
- name: Generate timestamp | |
id: generate-timestamp | |
run: echo "timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_OUTPUT | |
# We tag the newest build for client version 5875 with `latest` since | |
# that can be considered the default. | |
- name: Generate tags | |
uses: actions/github-script@v7 | |
id: generate-tags | |
env: | |
COMMIT_HASH: ${{ needs.setup.outputs.commit-hash }} | |
IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_SERVER }} | |
CLIENT_VERSION: ${{ matrix.client-version }} | |
with: | |
result-encoding: string | |
script: | | |
const tags = [] | |
if (parseInt(process.env.CLIENT_VERSION) === 5875) { | |
tags.push(`${process.env.IMAGE}:latest`) | |
} | |
tags.push(`${process.env.IMAGE}:${process.env.CLIENT_VERSION}`) | |
tags.push(`${process.env.IMAGE}:${process.env.CLIENT_VERSION}-${process.env.COMMIT_HASH}`) | |
return tags.join(',') | |
# See https://github.com/opencontainers/image-spec/blob/main/annotations.md | |
- name: Generate OCI annotations | |
uses: actions/github-script@v7 | |
id: generate-annotations | |
env: | |
COMMIT_HASH: ${{ needs.setup.outputs.commit-hash }} | |
IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_SERVER }} | |
CLIENT_VERSION: ${{ matrix.client-version }} | |
TIMESTAMP: ${{ steps.generate-timestamp.outputs.timestamp }} | |
with: | |
result-encoding: string | |
script: | | |
const annotations = [ | |
{ key: 'created', value: process.env.TIMESTAMP }, | |
{ key: 'authors', envKey: 'OCI_ANNOTATION_AUTHORS' }, | |
{ key: 'url', envKey: 'OCI_ANNOTATION_URL' }, | |
{ key: 'documentation', envKey: 'OCI_ANNOTATION_DOCUMENTATION' }, | |
{ key: 'source', envKey: 'OCI_ANNOTATION_SOURCE' }, | |
{ key: 'version', envKey: 'COMMIT_HASH' }, | |
{ key: 'revision', envKey: 'COMMIT_HASH' }, | |
{ key: 'vendor', envKey: 'OCI_ANNOTATION_VENDOR' }, | |
{ key: 'licenses', envKey: 'OCI_ANNOTATION_LICENSES' }, | |
{ key: 'ref.name', value: `${process.env.IMAGE}:${process.env.CLIENT_VERSION}-${process.env.COMMIT_HASH}` }, | |
{ key: 'title', envKey: 'OCI_ANNOTATION_SERVER_TITLE' }, | |
{ key: 'description', envKey: 'OCI_ANNOTATION_SERVER_DESCRIPTION' }, | |
{ key: 'base.name', envKey: 'OCI_ANNOTATION_SERVER_BASE_NAME' }, | |
].map(({ key, value, envKey }) => { | |
const resolvedValue = value ?? process.env[envKey] ?? '' | |
return [ | |
`manifest:org.opencontainers.image.${key}=${resolvedValue}`, | |
`index:org.opencontainers.image.${key}=${resolvedValue}`, | |
] | |
}) | |
.flat() | |
.join('\n') | |
return annotations | |
# We also add the generated OCI metadata as labels because the GitHub | |
# Container registry does not correctly pick up the OCI annotations in | |
# some cases and prefers to pull displayed data from the image labels | |
# instead (which then results in data from the base images being shown if | |
# we do not provide values for the respective labels ourselves). | |
- name: Generate OCI labels | |
uses: actions/github-script@v7 | |
id: generate-labels | |
env: | |
COMMIT_HASH: ${{ needs.setup.outputs.commit-hash }} | |
IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_SERVER }} | |
CLIENT_VERSION: ${{ matrix.client-version }} | |
TIMESTAMP: ${{ steps.generate-timestamp.outputs.timestamp }} | |
with: | |
result-encoding: string | |
script: | | |
const labels = [ | |
{ key: 'created', value: process.env.TIMESTAMP }, | |
{ key: 'authors', envKey: 'OCI_ANNOTATION_AUTHORS' }, | |
{ key: 'url', envKey: 'OCI_ANNOTATION_URL' }, | |
{ key: 'documentation', envKey: 'OCI_ANNOTATION_DOCUMENTATION' }, | |
{ key: 'source', envKey: 'OCI_ANNOTATION_SOURCE' }, | |
{ key: 'version', envKey: 'COMMIT_HASH' }, | |
{ key: 'revision', envKey: 'COMMIT_HASH' }, | |
{ key: 'vendor', envKey: 'OCI_ANNOTATION_VENDOR' }, | |
{ key: 'licenses', envKey: 'OCI_ANNOTATION_LICENSES' }, | |
{ key: 'ref.name', value: `${process.env.IMAGE}:${process.env.CLIENT_VERSION}-${process.env.COMMIT_HASH}` }, | |
{ key: 'title', envKey: 'OCI_ANNOTATION_SERVER_TITLE' }, | |
{ key: 'description', envKey: 'OCI_ANNOTATION_SERVER_DESCRIPTION' }, | |
{ key: 'base.name', envKey: 'OCI_ANNOTATION_SERVER_BASE_NAME' }, | |
].map(({ key, value, envKey }) => { | |
const resolvedValue = value ?? process.env[envKey] ?? '' | |
return `org.opencontainers.image.${key}=${resolvedValue}` | |
}) | |
.join('\n') | |
return labels | |
- name: Build and push images | |
uses: docker/build-push-action@v6 | |
with: | |
context: . | |
platforms: linux/amd64, linux/arm64 | |
file: ./docker/server/Dockerfile | |
push: ${{ github.event_name != 'pull_request' }} | |
provenance: false | |
build-args: | | |
VMANGOS_REVISION=${{ needs.setup.outputs.commit-hash }} | |
VMANGOS_CLIENT_VERSION=${{ matrix.client-version }} | |
tags: ${{ steps.generate-tags.outputs.result }} | |
annotations: ${{ steps.generate-annotations.outputs.result }} | |
labels: ${{ steps.generate-labels.outputs.result }} | |
# Since the database image builds only take a few minutes (and therefore | |
# always complete before the server image builds), we need to build after the | |
# server images; otherwise, we would push new database images without knowing | |
# if the server image builds will succeed (and if there is, e.g., a | |
# compilation error due to a bug upstream we would end up with a version | |
# mismatch between the latest database images and the latest server images). | |
build-and-push-database-images: | |
name: Build and push database images | |
needs: [setup, build-and-push-server-images] | |
if: ${{ needs.setup.outputs.already-built != 'true' }} | |
runs-on: ubuntu-24.04 | |
permissions: | |
contents: read | |
packages: write | |
steps: | |
- name: Checkout repository | |
uses: actions/checkout@v4 | |
- name: Set up QEMU | |
uses: docker/setup-qemu-action@v3 | |
- name: Set up Docker Buildx | |
uses: docker/setup-buildx-action@v3 | |
- name: Log in to container registry | |
uses: docker/login-action@v3 | |
with: | |
registry: ${{ env.REGISTRY }} | |
username: ${{ github.actor }} | |
password: ${{ secrets.GITHUB_TOKEN }} | |
- name: Generate timestamp | |
id: generate-timestamp | |
run: echo "timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_OUTPUT | |
- name: Generate tags | |
uses: actions/github-script@v7 | |
id: generate-tags | |
env: | |
COMMIT_HASH: ${{ needs.setup.outputs.commit-hash }} | |
IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_DATABASE }} | |
with: | |
result-encoding: string | |
script: | | |
const tags = [] | |
tags.push(`${process.env.IMAGE}:latest`) | |
tags.push(`${process.env.IMAGE}:${process.env.COMMIT_HASH}`) | |
return tags.join(',') | |
# See https://github.com/opencontainers/image-spec/blob/main/annotations.md | |
- name: Generate OCI annotations | |
uses: actions/github-script@v7 | |
id: generate-annotations | |
env: | |
COMMIT_HASH: ${{ needs.setup.outputs.commit-hash }} | |
IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_DATABASE }} | |
TIMESTAMP: ${{ steps.generate-timestamp.outputs.timestamp }} | |
with: | |
result-encoding: string | |
script: | | |
const annotations = [ | |
{ key: 'created', value: process.env.TIMESTAMP }, | |
{ key: 'authors', envKey: 'OCI_ANNOTATION_AUTHORS' }, | |
{ key: 'url', envKey: 'OCI_ANNOTATION_URL' }, | |
{ key: 'documentation', envKey: 'OCI_ANNOTATION_DOCUMENTATION' }, | |
{ key: 'source', envKey: 'OCI_ANNOTATION_SOURCE' }, | |
{ key: 'version', envKey: 'COMMIT_HASH' }, | |
{ key: 'revision', envKey: 'COMMIT_HASH' }, | |
{ key: 'vendor', envKey: 'OCI_ANNOTATION_VENDOR' }, | |
{ key: 'licenses', envKey: 'OCI_ANNOTATION_LICENSES' }, | |
{ key: 'ref.name', value: `${process.env.IMAGE}:${process.env.COMMIT_HASH}` }, | |
{ key: 'title', envKey: 'OCI_ANNOTATION_DATABASE_TITLE' }, | |
{ key: 'description', envKey: 'OCI_ANNOTATION_DATABASE_DESCRIPTION' }, | |
{ key: 'base.name', envKey: 'OCI_ANNOTATION_DATABASE_BASE_NAME' }, | |
].map(({ key, value, envKey }) => { | |
const resolvedValue = value ?? process.env[envKey] ?? '' | |
return [ | |
`manifest:org.opencontainers.image.${key}=${resolvedValue}`, | |
`index:org.opencontainers.image.${key}=${resolvedValue}`, | |
] | |
}) | |
.flat() | |
.join('\n') | |
return annotations | |
# We also add the generated OCI metadata as labels because the GitHub | |
# Container registry does not correctly pick up the OCI annotations in | |
# some cases and prefers to pull displayed data from the image labels | |
# instead (which then results in data from the base images being shown if | |
# we do not provide values for the respective labels ourselves). | |
- name: Generate OCI labels | |
uses: actions/github-script@v7 | |
id: generate-labels | |
env: | |
COMMIT_HASH: ${{ needs.setup.outputs.commit-hash }} | |
IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_DATABASE }} | |
TIMESTAMP: ${{ steps.generate-timestamp.outputs.timestamp }} | |
with: | |
result-encoding: string | |
script: | | |
const labels = [ | |
{ key: 'created', value: process.env.TIMESTAMP }, | |
{ key: 'authors', envKey: 'OCI_ANNOTATION_AUTHORS' }, | |
{ key: 'url', envKey: 'OCI_ANNOTATION_URL' }, | |
{ key: 'documentation', envKey: 'OCI_ANNOTATION_DOCUMENTATION' }, | |
{ key: 'source', envKey: 'OCI_ANNOTATION_SOURCE' }, | |
{ key: 'version', envKey: 'COMMIT_HASH' }, | |
{ key: 'revision', envKey: 'COMMIT_HASH' }, | |
{ key: 'vendor', envKey: 'OCI_ANNOTATION_VENDOR' }, | |
{ key: 'licenses', envKey: 'OCI_ANNOTATION_LICENSES' }, | |
{ key: 'ref.name', value: `${process.env.IMAGE}:${process.env.COMMIT_HASH}` }, | |
{ key: 'title', envKey: 'OCI_ANNOTATION_DATABASE_TITLE' }, | |
{ key: 'description', envKey: 'OCI_ANNOTATION_DATABASE_DESCRIPTION' }, | |
{ key: 'base.name', envKey: 'OCI_ANNOTATION_DATABASE_BASE_NAME' }, | |
].map(({ key, value, envKey }) => { | |
const resolvedValue = value ?? process.env[envKey] ?? '' | |
return `org.opencontainers.image.${key}=${resolvedValue}` | |
}) | |
.join('\n') | |
return labels | |
- name: Build and push images | |
uses: docker/build-push-action@v6 | |
with: | |
context: . | |
platforms: linux/amd64, linux/arm64 | |
file: ./docker/database/Dockerfile | |
push: ${{ github.event_name != 'pull_request' }} | |
provenance: false | |
build-args: | | |
VMANGOS_REVISION=${{ needs.setup.outputs.commit-hash }} | |
tags: ${{ steps.generate-tags.outputs.result }} | |
annotations: ${{ steps.generate-annotations.outputs.result }} | |
labels: ${{ steps.generate-labels.outputs.result }} | |
delete-old-package-versions: | |
name: Delete old package versions | |
needs: [setup, build-and-push-server-images, build-and-push-database-images] | |
if: ${{ needs.setup.outputs.already-built != 'true' && github.event_name != 'pull_request' }} | |
runs-on: ubuntu-24.04 | |
permissions: | |
contents: read | |
packages: write | |
steps: | |
- name: Delete old server package versions | |
uses: actions/delete-package-versions@v5 | |
with: | |
package-name: vmangos-server | |
package-type: container | |
# According to | |
# https://docs.github.com/en/[email protected]/packages/working-with-a-github-packages-registry/working-with-the-npm-registry#limits-for-published-npm-versions | |
# there might at some point be a limit of 1,000 versions per package. | |
# It is not clear if that will only be applied to Enterprise and/or | |
# npm packages, but let's be safe and make sure we don't keep more | |
# than 1,000. | |
# | |
# We have 7 different server versions per build. For each server | |
# version, we have the image index plus two image manifests, making a | |
# total of 21 packages. | |
# | |
# This means, to stay below the assumed limit of 1,000 packages, we | |
# can keep a maximum of 47 builds worth of server images; let's make | |
# that 45 to have a nice and round number: | |
# 21 * 45 = 945 packages | |
min-versions-to-keep: 945 | |
- name: Delete old database package versions | |
uses: actions/delete-package-versions@v5 | |
with: | |
package-name: vmangos-database | |
package-type: container | |
# Above, we have set it up to keep 45 builds worth of server images | |
# (to stay below that supposed 1,000 packages limit). We want to keep | |
# all the matching database images. | |
# | |
# Per database build we have 3 packages in total (the image index | |
# plus two image manifests). | |
# | |
# 3 * 45 = 135 packages | |
min-versions-to-keep: 135 | |
update-revision-badge: | |
name: Update revision badge | |
needs: [setup, build-and-push-server-images, build-and-push-database-images, delete-old-package-versions] | |
if: ${{ needs.setup.outputs.already-built != 'true' && github.event_name != 'pull_request' }} | |
runs-on: ubuntu-24.04 | |
steps: | |
- name: Check if FTP secrets are available | |
run: | | |
if [[ -z "${{ secrets.REVISION_BADGE_FTP_HOST }}" || -z "${{ secrets.REVISION_BADGE_FTP_USERNAME }}" || -z "${{ secrets.REVISION_BADGE_FTP_PASSWORD }}" ]]; then | |
echo "FTP secrets are missing, skipping job." | |
exit 0 | |
fi | |
- name: Generate badge JSON | |
run: | | |
short_hash=$(echo "${{ needs.setup.outputs.commit-hash }}" | cut -c1-7) | |
echo '{ | |
"schemaVersion": 1, | |
"label": "Latest VMaNGOS build", | |
"message": "'$short_hash'", | |
"color": "blue" | |
}' > badge.json | |
- name: Upload badge JSON to web server | |
run: | | |
curl -T badge.json \ | |
--user ${{ secrets.REVISION_BADGE_FTP_USERNAME }}:${{ secrets.REVISION_BADGE_FTP_PASSWORD }} \ | |
ftp://${{ secrets.REVISION_BADGE_FTP_HOST }}/ |