diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4e26cec --- /dev/null +++ b/.dockerignore @@ -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/** diff --git a/.gitea/README.md b/.gitea/README.md index e270f19..9d7402a 100644 --- a/.gitea/README.md +++ b/.gitea/README.md @@ -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 diff --git a/.gitea/workflows/deploy-dev.yml b/.gitea/workflows/deploy-dev.yml deleted file mode 100644 index 5376d39..0000000 --- a/.gitea/workflows/deploy-dev.yml +++ /dev/null @@ -1,4 +0,0 @@ -on: - push: - branches: - - main diff --git a/.gitea/workflows/publish.yml b/.gitea/workflows/publish.yml new file mode 100644 index 0000000..cdba87f --- /dev/null +++ b/.gitea/workflows/publish.yml @@ -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 diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml deleted file mode 100644 index a0f0859..0000000 --- a/.gitea/workflows/release.yml +++ /dev/null @@ -1,4 +0,0 @@ -on: - push: - tags: - - "forever-v*" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c33d639 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/forever/README.md b/forever/README.md index ba1bf5e..72b8800 100644 --- a/forever/README.md +++ b/forever/README.md @@ -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 diff --git a/forever/index.toml b/forever/index.toml index 6b2605c..0871a39 100644 --- a/forever/index.toml +++ b/forever/index.toml @@ -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 diff --git a/forever/mods/screenshot-viewer.pw.toml b/forever/mods/screenshot-viewer.pw.toml new file mode 100644 index 0000000..2395473 --- /dev/null +++ b/forever/mods/screenshot-viewer.pw.toml @@ -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" diff --git a/scripts/build-site.sh b/scripts/build-site.sh new file mode 100755 index 0000000..4fa5441 --- /dev/null +++ b/scripts/build-site.sh @@ -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" diff --git a/sws.toml b/sws.toml new file mode 100644 index 0000000..b631be7 --- /dev/null +++ b/sws.toml @@ -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