Publish Packwiz site as immutable image
Publish Packwiz site / publish (push) Failing after 20s

This commit is contained in:
2026-09-04 12:04:31 +00:00
parent 033211c830
commit 4749e800a3
11 changed files with 299 additions and 8 deletions
+8
View File
@@ -0,0 +1,8 @@
# The image receives only the generated site and its runtime configuration.
# In particular, never send the repository's .git directory to Docker.
**
!Dockerfile
!sws.toml
!.build/
!.build/site/
!.build/site/**
+22
View File
@@ -43,6 +43,10 @@ https://mc.ahmza.com/playground/dev/pack.toml
mc-modpacks/
├── .gitea/
│ └── workflows/
├── scripts/
│ └── build-site.sh
├── Dockerfile
├── sws.toml
├── forever/
│ ├── pack.toml
│ ├── index.toml
@@ -168,6 +172,24 @@ Packwiz is responsible for modpack contents and side filtering.
Pterodactyl is responsible for Java, server process management and resources.
Kubernetes/SWS provides static hosting for the published Packwiz repositories.
The publishing workflow runs [`scripts/build-site.sh`](../scripts/build-site.sh),
which exports committed Git revisions into the documented URL layout, then
builds the multi-architecture image
`tea.ahmza.com/ahmza/mcpacks-site`.
The workflow finally commits that image's immutable OCI digest to
`apps/mcpacks/deployment.yaml` in the `ahmza/k8s-starter-pack` repository.
Argo CD notices that commit and performs the rollout; the workflow does not
hold Kubernetes credentials or change the cluster directly.
The Gitea Actions repository secret `GITEATOKEN` must be able to:
- push packages owned by `ahmza`
- read and write `ahmza/k8s-starter-pack`
The `latest` image tag exists only to bootstrap the first Argo CD sync and for
human inspection. Normal rollouts use the exact content digest recorded in the
GitOps manifest, making the running version auditable and rollback-friendly.
## Rollback
-4
View File
@@ -1,4 +0,0 @@
on:
push:
branches:
- main
+137
View File
@@ -0,0 +1,137 @@
name: Publish Packwiz site
on:
push:
branches:
- main
tags:
- "forever-v*"
# A branch push and release tag can happen close together. Serializing them
# prevents two jobs from racing to update the GitOps deployment manifest.
concurrency:
group: mcpacks-publish
cancel-in-progress: false
env:
REGISTRY: tea.ahmza.com
IMAGE_NAME: ahmza/mcpacks-site
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Check out all pack history
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Fetch current main and release tags
# A tag-triggered checkout points HEAD at that tag. Fetching main
# explicitly ensures /dev still contains today's development branch.
run: git fetch origin main:refs/remotes/origin/main --tags --force
- name: Assemble static Packwiz site
env:
EVENT_REF: ${{ gitea.ref }}
EVENT_SHA: ${{ gitea.sha }}
run: |
chmod +x scripts/build-site.sh
# A branch job must publish exactly the commit that triggered it,
# even if a newer main commit arrives while this job is queued. A
# tag job uses current main for /dev while exporting the tag itself
# to /releases in scripts/build-site.sh.
case "$EVENT_REF" in
refs/tags/*) dev_ref=origin/main ;;
*) dev_ref=$EVENT_SHA ;;
esac
./scripts/build-site.sh "$dev_ref"
test -s .build/site/forever/dev/pack.toml
- name: Verify tagged release output
if: startsWith(gitea.ref, 'refs/tags/')
env:
RELEASE_TAG: ${{ gitea.ref_name }}
run: |
version=${RELEASE_TAG#forever-}
test -s ".build/site/forever/releases/$version/pack.toml"
test -s .build/site/forever/stable/pack.toml
- name: Log in to Gitea Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ gitea.repository_owner }}
password: ${{ secrets.GITEATOKEN }}
# The SWS runtime image already publishes amd64 and arm64 variants and
# this Dockerfile has no target-architecture RUN commands, so Buildx can
# create both variants without slow QEMU emulation.
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push immutable site image
id: build
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
build-args: |
VCS_REF=${{ gitea.sha }}
# latest bootstraps the first deployment and is convenient for
# inspection. Kubernetes runs the immutable manifest digest emitted
# by this step, never this mutable tag.
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max
- name: Check out cluster GitOps repository
uses: actions/checkout@v4
with:
repository: ahmza/k8s-starter-pack
ref: main
token: ${{ secrets.GITEATOKEN }}
path: _gitops
- name: Record the deployed image in Git
env:
# A digest identifies the actual multi-architecture image contents.
# This changes for a release build even when its tag points at the
# same Git commit previously built by the main-branch workflow.
IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
run: |
python3 - "$IMAGE" <<'PY'
from pathlib import Path
import sys
manifest = Path("_gitops/apps/mcpacks/deployment.yaml")
image = sys.argv[1]
prefix = " image: "
repository = "tea.ahmza.com/ahmza/mcpacks-site"
lines = manifest.read_text().splitlines()
matches = [
i for i, line in enumerate(lines)
if line.startswith(prefix + repository)
]
if len(matches) != 1:
raise SystemExit(f"expected one mcpacks image line, found {len(matches)}")
lines[matches[0]] = f"{prefix}{image}"
manifest.write_text("\n".join(lines) + "\n")
PY
cd _gitops
if git diff --quiet -- apps/mcpacks/deployment.yaml; then
echo "GitOps manifest already references $IMAGE"
exit 0
fi
git config user.name "gitea-actions[bot]"
git config user.email "gitea-actions[bot]@tea.ahmza.com"
git add apps/mcpacks/deployment.yaml
git commit -m "Deploy mcpacks ${GITEA_SHA}"
git push origin HEAD:main
-4
View File
@@ -1,4 +0,0 @@
on:
push:
tags:
- "forever-v*"
+19
View File
@@ -0,0 +1,19 @@
# syntax=docker/dockerfile:1
# scripts/build-site.sh prepares .build/site before this image is built. The
# runtime contains neither Git nor a shell-based synchronizer: published files
# are immutable and can only change by building a new, traceable image.
# Pin both the tested SWS release and its multi-architecture manifest digest so
# rebuilding an old pack revision cannot silently pick up a different runtime.
FROM ghcr.io/static-web-server/static-web-server:2.44.0@sha256:2c1a7c3e0feaea5859307403b74e1c575f3ec1499094fc077344173d11abaae2
ARG VCS_REF=unknown
LABEL org.opencontainers.image.source="https://tea.ahmza.com/ahmza/mcpacks" \
org.opencontainers.image.revision="$VCS_REF" \
org.opencontainers.image.description="Static Packwiz repositories for AGS Minecraft servers"
COPY sws.toml /etc/sws.toml
COPY .build/site /public
ENV SERVER_CONFIG_FILE=/etc/sws.toml
EXPOSE 80/tcp
CMD ["--config-file", "/etc/sws.toml"]
+4
View File
@@ -129,3 +129,7 @@ Before introducing or removing a content mod, consider:
The objective is not zero world dependencies; Create and other content mods deliberately become part of the world.
The objective is to keep those dependencies intentional.
## Mods I would like to add
- https://modrinth.com/mod/better-stats - No NeoForge 1.21.1 support
+4
View File
@@ -247,6 +247,10 @@ metafile = true
file = "mods/resourceful-lib.pw.toml"
metafile = true
[[files]]
file = "mods/screenshot-viewer.pw.toml"
metafile = true
[[files]]
file = "mods/searchables.pw.toml"
metafile = true
+13
View File
@@ -0,0 +1,13 @@
name = "Screenshot Viewer"
filename = "screenshot_viewer-1.3.4-neoforge-mc1.21.1.jar"
side = "client"
[download]
url = "https://cdn.modrinth.com/data/laNoi025/versions/efjk3jeu/screenshot_viewer-1.3.4-neoforge-mc1.21.1.jar"
hash-format = "sha512"
hash = "3aa3959a518a0a5eeede72ba699d9b1974486736b8c0c6c5a624ca286f4f73dce5cea80a3e207b7e96dfe22a65a803aa5e240beb535f343459757cc474836029"
[update]
[update.modrinth]
mod-id = "laNoi025"
version = "efjk3jeu"
+66
View File
@@ -0,0 +1,66 @@
#!/bin/sh
# Assemble the exact URL layout documented in .gitea/README.md from committed
# Git revisions. Using git archive, rather than copying the working tree, keeps
# uncommitted experiments and ignored pack exports out of published images.
set -eu
repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
cd "$repo_root"
dev_ref=${1:-HEAD}
build_root="$repo_root/.build"
site_root="$build_root/site"
work_root="$build_root/work"
rm -rf "$site_root" "$work_root"
mkdir -p "$site_root/forever/releases" "$work_root"
export_pack() {
revision=$1
pack=$2
destination=$3
scratch="$work_root/export"
rm -rf "$scratch"
mkdir -p "$scratch" "$(dirname -- "$destination")"
if ! git cat-file -e "$revision:$pack/pack.toml" 2>/dev/null; then
return 1
fi
git archive "$revision" "$pack" | tar -x -C "$scratch"
mv "$scratch/$pack" "$destination"
}
# main is always the development source, including when this script runs for a
# release-tag event whose checked-out commit is intentionally older than main.
export_pack "$dev_ref" forever "$site_root/forever/dev"
# Playground is documented but intentionally incomplete today. Start serving
# it automatically as soon as a committed playground/pack.toml exists.
if export_pack "$dev_ref" playground "$site_root/playground/dev"; then
echo "Published playground development pack from $dev_ref"
else
echo "Skipping playground: $dev_ref has no playground/pack.toml" >&2
fi
# Each forever-vX.Y.Z tag becomes an immutable URL. Version-aware Git sorting
# also identifies the newest release to copy to the stable channel.
release_tags=$(git tag --list 'forever-v*' --sort=-version:refname \
| grep -E '^forever-v[0-9]+\.[0-9]+\.[0-9]+$' || true)
for tag in $release_tags; do
version=${tag#forever-}
export_pack "$tag" forever "$site_root/forever/releases/$version"
echo "Published Forever release $version"
done
latest_tag=$(printf '%s\n' "$release_tags" | sed -n '/./{p;q;}')
if [ -n "$latest_tag" ]; then
export_pack "$latest_tag" forever "$site_root/forever/stable"
echo "Promoted ${latest_tag#forever-} to Forever stable"
else
echo "No forever-vX.Y.Z tag exists; /forever/stable is not published yet" >&2
fi
rm -rf "$work_root"
+26
View File
@@ -0,0 +1,26 @@
[general]
# Listen on the Pod network. Kubernetes' Service and ingress-nginx provide the
# stable internal address and public TLS endpoint in front of this process.
host = "0.0.0.0"
port = 80
root = "/public"
log-level = "info"
# Packwiz clients need files, not browsable directory indexes or SPA fallback
# behavior. A missing pack/release must remain a real 404 rather than silently
# returning an unrelated document.
directory-listing = false
# ingress-nginx handles response compression. Disabling it here avoids doing
# the same CPU work twice for large TOML indexes.
compression = false
compression-static = false
# Development and stable paths can change, so do not emit SWS's long-lived
# static-asset cache policy. Release paths remain immutable by convention.
cache-control-headers = false
# Kubernetes probes this endpoint without depending on any particular pack
# having been published yet.
health = true