Nightly Distribution

What is published

A push to dev builds and tests four distribution artifacts on the amd64 Linux runner. A successful run publishes one immutable Gitea prerelease tagged nightly-YYYYMMDD-<commit>.

TargetRelease fileDistribution boundaryExecution gate
Linux x86-64pit-nightly-linux-x86_64.tar.gzpit with a linker-injected root plus adjacent boot.qop contentNative full suites and CLI tests
Windows x86-64pit-nightly-windows-x86_64.tar.gzpit.exe with a linker-injected Windows root plus adjacent boot.qop contentFull suites under Wine
Web wasm32pit-nightly-web-wasm32.tar.gzStatic HTML, JavaScript, Wasm, and web boot assetsWasm suites under Node
Playdate simulator on Linux x86-64pit-nightly-playdate-simulator.tar.gzA pit.pdx simulator applicationPanic’s Linux simulator under Xvfb

The runtime binary and content bundle have different jobs. Forge links the target root into the binary at construction, so no runtime cart choice or tail repack remains. Linux and Windows archives are self-contained distributions: the tested binary is packaged unchanged beside its boot.qop content bundle.

The Playdate file is explicitly a Linux simulator build, not a device build. There is no macOS artifact until a macOS runner can build and execute it.

Compatibility manifest

Every release has a fifth attachment, compatibility.json. This is the machine-readable answer to “which artifact can run where?” It records, for each archive:

  • target system and architecture;
  • the target recipe and all profile axes (numeric representation, endian, text, record, GC, and Wota width);
  • Mach ABI;
  • how the boot media is delivered;
  • the execution gate that passed;
  • archive size and SHA-256.

The expected matrix lives in .gitea/nightly-targets.json. Each target archive also carries BUILD.txt and an internal SHA256SUMS. The release fan-in rejects a missing archive, an unexpected archive, metadata drift, a payload checksum failure, or a commit mismatch before it contacts Gitea.

Publication transaction

The platform jobs are producers. They test, package, and upload temporary Gitea Actions artifacts. publish-nightly depends on Linux, the hosted profile matrix, Web, Windows, and Playdate simulator jobs, then:

  1. downloads the exact archives made by those jobs;
  2. validates and assembles compatibility.json;
  3. checks that dev still names the candidate commit;
  4. creates an immutable Gitea prerelease as a draft;
  5. uploads all five attachments;
  6. makes the release visible.

If upload fails, the release stays a resumable draft. If a newer push has already advanced dev, the older candidate is treated as superseded and is not published. The 14-day Actions artifacts are staging and diagnostics; the Gitea prerelease is the durable distribution.

No workflow deletes an older release. Establish a retention rule after real artifact sizes and release frequency are visible; cleanup is an operational policy, not something a build should infer while publishing a new artifact.

Gitea 1.25 defaults to five release attachments, which is why checksums are inside each archive and the four archive hashes are carried by the fifth attachment rather than uploaded as a sixth file.

Local reproduction

The local Docker commands use the same amd64 Ubuntu image and committed Linux seed as the dedicated runner. The target seed includes its content stamp, so a clean job can prove it matches the checkout without regenerating it:

scripts/ci_local.sh image
scripts/ci_local.sh vm        # Linux, Web, and Windows VM suites in parallel
scripts/ci_local.sh linux     # full native Linux gate
scripts/ci_local.sh windows   # full Wine gate
scripts/ci_local.sh web       # full Wasm gate

On an Apple Silicon Mac, the default linux/amd64 lanes run through Docker Desktop’s architecture emulation. The focused vm lanes are useful parity checks, but a full lane can run one Pit turn slowly enough to trip the target’s normal slow-turn watchdog. That is a host-speed failure, not evidence that the same target failed natively. Do not relax the distributed runtime’s scheduler policy to make emulation green: the dedicated native-amd64 runner is the authoritative full-suite and release gate.

scripts/package_nightly.sh creates one target archive. scripts/prepare_nightly.py validates all target archives and constructs the release directory. scripts/publish_nightly.sh --dry-run verifies that directory without needing a token or contacting Gitea.

Dedicated Alpine runner setup

The runner host must be amd64 and have enough free Docker storage for the roughly 1.7 GB toolchain image footprint plus build layers and concurrent job containers. Keep the runner private: Docker socket access is effectively root access to the host.

On Alpine, install and enable Docker if it is not already present:

apk add docker git
rc-update add docker default
service docker start

Create a Gitea personal access token with package-write permission, then log the host Docker client into the registry. Do not put the token in this repository or in shell history:

docker login gitea.pockle.world

The repository workflow .gitea/workflows/build-ci-image.yml builds and pushes the image on the image-builder host whenever .gitea/ci.Dockerfile changes. It publishes both an immutable revision tag, derived by scripts/ci_image_tag.sh, and latest. Configure the runner labels to use the host for image builds and latest for ordinary jobs:

runner:
  labels:
    - "image-builder"
    - "pit-ci:docker://gitea.pockle.world/john/pit-ci:latest"
container:
  force_pull: true

force_pull: true ensures a job sees the newest published image. The revision tag remains available for rollback and for reproducing an older run. The runner’s Docker credentials must be able to pull the private registry image; the image-builder job’s registry login is sufficient when both labels use the same host Docker daemon and user.

If act_runner itself runs in Docker, it must mount the host socket at /var/run/docker.sock; if it runs as an Alpine service, its user needs access to the socket. Restart the runner after changing its labels, then confirm in Gitea Administration that it reports pit-ci and the desired capacity.

The release job uses Gitea’s short-lived built-in GITEA_TOKEN; no release PAT is stored. It requests code: read, actions: read, and releases: write in the workflow. Under Repository Settings → Actions → General, confirm that the configured maximum job-token permission allows release writes. Gitea clamps the workflow request to that maximum.

Before the first push, check Gitea’s [repository.release] settings. The defaults in 1.25 allow five files up to 2048 MB each, which matches this release shape. A custom ALLOWED_TYPES, MAX_FILES, or FILE_MAX_SIZE must still allow the four .tar.gz files and compatibility.json.

Server handoff checklist

Before changing the server, collect these facts:

uname -m
cat /etc/alpine-release
getconf _NPROCESSORS_ONLN
grep MemTotal /proc/meminfo
docker version
docker info --format '{{.Architecture}} {{.DockerRootDir}}'
df -h
docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'

Then locate the active act_runner config.yaml and determine whether the runner is an OpenRC service or a Docker container. Those two facts decide the exact restart command; they should be inspected rather than guessed.