No description
  • Dockerfile 77.7%
  • Shell 22.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Tim Hårek Andreassen c14b16aa49
All checks were successful
publish / publish (push) Successful in 1m6s
chore: bump mise, alpine, node, python, checkout; move off archived kaniko
- mise 2026.9.3 → 2026.10.13
- alpine 3.22 → 3.24
- node 24 → 26, python 3.14 → 3.15 (musl prebuilds exist for both)
- actions/checkout v5 → v7
- kaniko: gcr.io/kaniko-project (archived 2025-06) →
  ghcr.io/osscontainertools/kaniko:v1.28.6-debug (maintained fork)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-18 10:59:49 +02:00
.forgejo/workflows chore: bump mise, alpine, node, python, checkout; move off archived kaniko 2026-10-18 10:59:49 +02:00
Dockerfile chore: bump mise, alpine, node, python, checkout; move off archived kaniko 2026-10-18 10:59:49 +02:00
mise.sh fix: preserve argv[0] in wrapper so shims keep working 2026-09-11 12:19:44 +02:00
mise.toml chore: bump mise, alpine, node, python, checkout; move off archived kaniko 2026-10-18 10:59:49 +02:00
README.md chore: bump mise, alpine, node, python, checkout; move off archived kaniko 2026-10-18 10:59:49 +02:00

mise-runner

A public Forgejo Actions job image: Alpine 3.22 with mise (v2026.9.3, musl build) and a preinstalled toolchain — node 26, python 3.15 — plus git, curl, bash, and a full C toolchain so mise install can build anything else from source.

GITHUB_TOKEN handling

Forgejo injects its scope-less task token as GITHUB_TOKEN into every job container. mise's token resolution would pick it up and then 401 against api.github.com even for public endpoints, breaking mise install for any repo with a GitHub-hosted tool. The mise on PATH is therefore a wrapper (mise.sh) around the real binary (mise-bin) that unsets GITHUB_TOKEN unless:

  • MISE_GITHUB_TOKEN is set — a real mise token, which mise prefers anyway
  • MISE_KEEP_GITHUB_TOKEN=1 — for mise run/mise x children that call the Forgejo API with the injected token

Workflows on any repo of the same Forgejo instance select it with runs-on: mise-latest and get mise with zero per-job setup:

on: push
jobs:
  check:
    runs-on: mise-latest
    steps:
      - uses: actions/checkout@v5
      - run: mise fmt --check   # tools from your repo's mise.toml

mise install in a job writes to the container's own /opt/mise-cache/data (seeded from the image), which is discarded when the job ends. To carry tools between jobs, cache them explicitly with actions/cache: the runner already serves ACTIONS_CACHE_URL (see below).

      - uses: actions/cache@v4
        with:
          path: /opt/mise-cache/data/installs
          key: mise-${{ runner.os }}-${{ hashFiles('mise.toml', 'mise.lock') }}

Repos pin their own versions with a normal mise.toml; mise install only builds what's missing. System nodejs and bash exist independently of mise, so uses: steps and default step shells work without any mise config.

Who builds this, and where

Only two roles exist, and they happen on different machines:

  1. You, wherever you have Docker — build the image and push it to the Forgejo package registry. This is a one-time step per image update. Your laptop is fine; the target platform is linux/amd64, and Docker on any host (including macOS via Docker Desktop / OrbStack) can build and push that.
  2. The runner's dind daemon, on the server — pulls the published image by tag when a job says runs-on: mise-latest. It never builds anything and needs no access to your local Docker.

So: no, you don't run docker build on the server. Build locally, push, and the server-side runner picks the image up from the registry.

Build & publish

# anywhere with Docker; hailmary is amd64, so that's the platform to build for
docker build --platform linux/amd64 \
  -t src.timharek.no/tim/mise-runner:<version> \
  -t src.timharek.no/tim/mise-runner:latest .
docker login src.timharek.no -u tim   # PAT with package write scope
docker push src.timharek.no/tim/mise-runner:<version>
docker push src.timharek.no/tim/mise-runner:latest

Tag both <version> and latest: latest is what the runner's label points at, but since the runner's force_pull is false, a floating tag alone makes it ambiguous which build a job actually ran.

Then set the package's visibility to public in Forgejo (Packages → mise-runner → manage): the dind daemon pulls anonymously, and a private package fails as a confusing job error rather than an auth prompt.

Where the runner itself is configured

There is no runner daemon in this repo — only the image. The daemon is deployed as a service in the Forgejo Dokploy app on the hailmary host, running jobs under a docker:29-dind sidecar, with its config at /srv/docker/forgejo/runner/config.yml (not managed here). When a new image version is published, the only runner-side change needed is the label pointing at it:

labels:
  - "mise-latest:docker://src.timharek.no/tim/mise-runner:latest"

The same config carries the runner's job cache, which is what backs actions/cache:

cache:
  enabled: true
  dir: /data/cache      # on the /srv/docker/forgejo/runner bind mount
  host: 192.168.16.4    # this container's IP on the compose network
  proxy_port: 4000

host is not optional in practice: jobs run inside the dind sidecar, which resolves no forgejo-runner DNS name and cannot reach the runner on its own network. It can reach the runner's compose-network IP, which is what host pins — auto-detection picks an interface by heuristics and is not worth relying on. The runner then hands each job a per-run ACTIONS_CACHE_URL and proxies to a cache server on dir, with entries scoped per repository. Nothing prunes dir; it grows with every distinct cache key.

Files

  • Dockerfile — alpine:3.22 + mise (musl, verified against the release's SHASUMS256.txt; note mise's asset is x64, not amd64) + baked toolchain.
  • mise.toml — the image's default tool set (node 26, python 3.15).