refactor: migrate to gitea actions

This commit is contained in:
Nils Andreas Svee committed 2026-10-10 14:19:44 +07:00
1 parent 36e69fdd67
commit c4ec9def58
11 files changed
+451 -6

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
.git
.gitea
Jenkinsfile
README.md
**/__pycache__
+78
View File
@@ -0,0 +1,78 @@
name: Build OpenWrt Rust SDK images
on:
workflow_dispatch:
inputs:
targets:
description: 'Comma-separated targets, or all'
default: 'x86/64,mediatek/filogic,octeon/generic'
required: true
type: string
publish:
description: 'Publish the original Docker Hub tags (requires approval)'
default: false
type: boolean
permissions:
contents: read
jobs:
matrix:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.resolve.outputs.matrix }}
steps:
- uses: builtin:checkout
- name: Resolve targets and OpenWrt versions
id: resolve
env:
TARGETS: ${{ inputs.targets }}
run: python3 ci/matrix.py
build:
needs: matrix
runs-on: ubuntu-latest
timeout-minutes: 360
strategy:
fail-fast: true
max-parallel: 1
matrix: ${{ fromJSON(needs.matrix.outputs.matrix) }}
env:
SAFE_TARGET: ${{ matrix.target }}
VERSION: ${{ matrix.version }}
steps:
- uses: builtin:checkout
- name: Prepare persistent cache and bounded builder
run: sh ci/setup-builder.sh
- name: Build SDK image
run: |
docker buildx build --builder openwrt-sdk-rust --load --progress plain \
--network host --allow network.host \
--no-cache-filter configured,sdk \
--build-arg SAFE_TARGET="$SAFE_TARGET" \
--build-arg VERSION="$VERSION" \
--build-arg JOBS=8 \
--build-arg SCCACHE_REDIS_ENDPOINT=redis://127.0.0.1:6379 \
--tag "lochnair/openwrt-sdk-rust:${SAFE_TARGET}-${VERSION}" .
- name: Verify SDK image
run: |
docker run --rm "lochnair/openwrt-sdk-rust:${SAFE_TARGET}-${VERSION}" sh -ec '
sccache --version
grep -qx "CONFIG_RUST_SCCACHE=y" .config
staging_dir/host/bin/rustc -vV
staging_dir/host/bin/cargo --version
'
sh ci/verify-cache.sh "lochnair/openwrt-sdk-rust:${SAFE_TARGET}-${VERSION}"
- name: Publish Docker Hub tag
if: inputs.publish == true
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
shell: bash
run: |
set -euo pipefail
DOCKER_CONFIG=$(mktemp -d)
export DOCKER_CONFIG
trap 'rm -rf "$DOCKER_CONFIG"' EXIT
printf '%s' "$DOCKERHUB_TOKEN" | docker login --username "$DOCKERHUB_USERNAME" --password-stdin
docker push "lochnair/openwrt-sdk-rust:${SAFE_TARGET}-${VERSION}"
+26 -6
View File
@@ -1,13 +1,33 @@
ARG SAFE_TARGET
ARG VERSION
FROM ghcr.io/openwrt/sdk:${SAFE_TARGET}-${VERSION}
FROM ghcr.io/openwrt/sdk:${SAFE_TARGET}-${VERSION} AS tooling
# The SDK runs on amd64 even when compiling for another OpenWrt target.
USER root
RUN curl -fsSL https://github.com/mozilla/sccache/releases/download/v0.18.0/sccache-v0.18.0-x86_64-unknown-linux-musl.tar.gz -o /tmp/sccache.tar.gz && \
echo '45f1447fbe231e3037bde351ef70677dd212216c8d62ae7ca409fecc4d6acc89 /tmp/sccache.tar.gz' | sha256sum -c - && \
tar -xzf /tmp/sccache.tar.gz -C /tmp && \
install -m 0755 /tmp/sccache-v0.18.0-x86_64-unknown-linux-musl/sccache /usr/local/bin/sccache && \
rm -rf /tmp/sccache.tar.gz /tmp/sccache-v0.18.0-x86_64-unknown-linux-musl
USER buildbot
FROM tooling AS configured
ARG SAFE_TARGET
ARG VERSION
ENV SCCACHE_REDIS_KEY_PREFIX=openwrt-rust:${SAFE_TARGET}-${VERSION}: \
CARGO_INCREMENTAL=0
COPY --chmod=0755 ci/build-rust.sh /usr/local/bin/build-rust
RUN ./scripts/feeds update packages && \
./scripts/feeds install rust && \
printf '\nCONFIG_RUST_SCCACHE=y\n' >> .config && \
make defconfig && \
sed -i 's/--set=llvm.download-ci-llvm=true/--set=llvm.download-ci-llvm=false/g' feeds/packages/lang/rust/Makefile; \
{ make package/rust/host/compile -j$(nproc) || \
make package/rust/host/compile -j1 V=sc; } && \
rm -rf build_dir/host/rustc-*/rustc-*/build \
dl/rustc-*.tar.*
grep -qx 'CONFIG_RUST_SCCACHE=y' .config && \
sed -i 's/--set=llvm.download-ci-llvm=true/--set=llvm.download-ci-llvm=false/g' feeds/packages/lang/rust/Makefile && \
sed -i '/--set=llvm.download-ci-llvm=false/a\ --set=build.ccache=sccache \\' feeds/packages/lang/rust/Makefile
FROM configured AS sdk
ARG JOBS=8
# A non-secret endpoint on the runner; do not persist it in the published image.
ARG SCCACHE_REDIS_ENDPOINT
RUN build-rust
+126
View File
@@ -1 +1,127 @@
# openwrt-sdk-rust
Builds the OpenWrt SDK with its Rust host toolchain installed. Gitea Actions
resolves `stable_version` and `oldstable_version` from OpenWrt's `.versions.json`.
The default targets remain `x86/64,mediatek/filogic,octeon/generic`; dispatch with
`targets=all` to use upstream `dump-target-info.pl`, or supply a comma-separated
list. Images still use `ghcr.io/openwrt/sdk:<target-with-dashes>-<version>` and
publish to `lochnair/openwrt-sdk-rust:<target-with-dashes>-<version>` on Docker Hub.
No `latest` tag or additional registry is introduced.
## Runner setup and cutover
The workflow uses `ubuntu-latest` on the existing TheBeast runner, currently
the only runner. Its job image and access to the persistent Docker daemon were
verified; no additional runner, label, capacity change, or restart is required.
`builtin:checkout` uses the existing Runner 5 checkout support.
Enable repository Actions and set `DOCKERHUB_USERNAME` and `DOCKERHUB_TOKEN`
secrets with push permission to `lochnair/openwrt-sdk-rust`. Jenkins' credential
ID is not reusable in Gitea. The workflow is manually dispatched, matching the
parameterized Jenkins job; there are no PR, push, or scheduled publication
triggers. `publish=false` is the safe validation default. Dispatch
`publish=true` only after publication is approved. Login uses a temporary job
directory, and each successfully built and checked image is pushed under its
original tag. Before any push, `ci/verify-cache.sh` uses the installed SDK rustc
in two disposable containers and requires a Redis miss followed by a hit.
Run a non-publishing dispatch for the default six combinations, then an
approved publishing dispatch. Check the pulled image's Rust/Cargo installation
and downstream package build before disabling the old Jenkins job and deleting
`Jenkinsfile`. Do not retire Jenkins before this acceptance run. No Jenkins
plugins or credentials are used by the new workflow.
## Persistent compilation cache
`ci/setup-builder.sh` provisions one Redis service and one reusable BuildKit
builder on that daemon. Redis uses the named volume `openwrt-rust-sccache`, AOF
persistence (`appendfsync everysec`), a 6 GiB LRU cache, an 8 GiB memory limit,
and one CPU. Jobs and build containers can disappear without losing it. A
machine crash can lose up to the most recent second of cache writes; eviction
or lost entries cause recompilation. Do not run `docker compose down -v` or
prune this volume. BuildKit's layer cache is separate and may be garbage
collected without clearing sccache.
The cache joins the existing `gitea-runner-jobs` network and exposes port 6379
only on loopback. BuildKit uses host networking to reach it; package containers
use its Docker DNS name. This is a trusted-runner cache: do not attach untrusted
PR containers or public networks to it. No password or endpoint is baked into
published SDK images. Consumers outside this runner can use their own Redis
endpoint. Each target/OpenWrt version has its own key prefix; sccache also
hashes Rust compiler version and shared libraries, compilation arguments
(including target and flags), source and dependency contents. Different Rust
versions/toolchains therefore miss rather than reuse incompatible objects.
The shared BuildKit worker executes one build step at a time, even across
simultaneous dispatches. It is limited to eight CPUs and 20 GiB RAM with no
swap. `make -j8` keeps compiler parallelism bounded; the original `-j1 V=sc`
diagnostic retry and Rust build-directory/archive cleanup remain. BuildKit
layer storage has a 50 GiB GC budget. Matrix jobs also run one at a time and
stop scheduling on a failure, as Jenkins' sequential stage loop did.
OpenWrt integration was inspected in the SDK's pinned packages feed:
- `rust-values.mk` **unexports `RUSTC_WRAPPER`**. Setting it in the workflow
alone does not enable caching. `CONFIG_RUST_SCCACHE=y` makes OpenWrt inject
`CARGO_INCREMENTAL=0`, `RUSTC_WRAPPER=sccache`, and `SCCACHE_DIR` into host
and package Cargo invocations. The Dockerfile enables and checks that option.
- The base SDK has no sccache binary. We install the pinned 0.18.0 amd64-musl
release and verify its SHA-256 before using it.
- Rust bootstrap wraps rustc itself. We also set `build.ccache=sccache` in
its configure arguments so stage-zero compilation and LLVM use sccache,
retaining `llvm.download-ci-llvm=false`. The inherited wrapper is forwarded
by bootstrap through `RUSTC_WRAPPER_REAL` where applicable.
- sccache is a compiler cache, not a Cargo target-directory cache. Linked
binaries, proc macros, some bootstrap work and incremental compilation are
not cacheable. SDK bootstrap and downstream dependencies can share storage
without necessarily sharing every compiled object. Every actual SDK build
prints statistics on success or failure; configured/compile layers are
rebuilt so stale Docker layer hits do not hide those statistics.
Sources: [OpenWrt Rust configuration](https://github.com/openwrt/packages/blob/openwrt-24.10/lang/rust/rust-values.mk),
[Rust bootstrap](https://github.com/rust-lang/rust/blob/1.90.0/src/bootstrap/src/core/build_steps/compile.rs),
[sccache Redis](https://github.com/mozilla/sccache/blob/v0.18.0/docs/Redis.md),
[Rust cache limitations](https://github.com/mozilla/sccache/blob/v0.18.0/docs/Rust.md).
## Downstream OpenWrt packages
Use the same persistent daemon and target/version image. For example, after
installing the `sqm-autorate-rust` feed/package in the SDK:
```sh
docker run --rm --network gitea-runner-jobs --cpus 8 \
--memory 20g --memory-swap 20g \
-e SCCACHE_REDIS_ENDPOINT=redis://openwrt-rust-cache:6379 \
lochnair/openwrt-sdk-rust:x86-64-24.10.8 sh -ec '
# Install/update the downstream feed here before defconfig.
make defconfig
grep -qx "CONFIG_RUST_SCCACHE=y" .config
sccache --start-server
trap "sccache --show-stats; sccache --stop-server" EXIT
sccache --zero-stats
make package/sqm-autorate-rust/compile -j8 V=sc
'
```
Keep the image's cache prefix and enable `CONFIG_RUST_SCCACHE` after any
downstream configuration replacement. Cache statistics belong to the current
container's sccache server; `--zero-stats` does not erase cached objects.
`sqm-autorate-rust` currently uses cross-rs to publish static binaries, not an
OpenWrt SDK package workflow; that unrelated pipeline is unchanged. The package
Makefile in `openwrt-feeds` already uses OpenWrt's `rust-package.mk`, so an SDK
package job can use this cache without patching the package.
For a local SDK build on the prepared runner:
```sh
sh ci/setup-builder.sh
docker buildx build --builder openwrt-sdk-rust --load --progress plain \
--network host --allow network.host --no-cache-filter configured,sdk \
--build-arg SAFE_TARGET=x86-64 --build-arg VERSION=24.10.8 \
--build-arg JOBS=8 \
--build-arg SCCACHE_REDIS_ENDPOINT=redis://127.0.0.1:6379 \
-t lochnair/openwrt-sdk-rust:x86-64-24.10.8 .
```
See [validation results](ci/VALIDATION.md) for the checks performed and the
remaining cutover acceptance.
+69
View File
@@ -0,0 +1,69 @@
# Migration validation — 2026-10-10
Inspected the original Jenkinsfile, Dockerfile, downstream `sqm-autorate-rust`
workflows and package Makefile, Terraform runner definition, and live TheBeast
runner configuration before changing the repository.
## Passed
- YAML parsing; TOML parsing; `sh -n`; ShellCheck; `git diff --check`.
- actionlint 1.7.12, excluding only its unsupported `builtin:checkout` action
syntax. The workflow uses the existing `ubuntu-latest` label. Gitea's documentation and the
existing downstream workflow confirm built-in checkout support.
- Default matrix resolves six entries for stable 25.12.5 and oldstable 24.10.8.
`targets=all` ran upstream target discovery inside the actual job image and
resolved 194 entries. Empty/invalid targets fail. Every default base-image
tag was read successfully from GHCR.
- Docker Compose configuration accepted; Redis healthy with its named volume,
AOF enabled and successful AOF writes. Service and bounded builder were
provisioned on TheBeast without changing the runner configuration.
- Builder inspected: eight-CPU quota, 20 GiB memory/swap limit, one BuildKit
worker step, 50 GiB layer GC budget. A second disposable job container with
fresh Docker client configuration reused the existing daemon and builder.
- Dockerfile build checks completed. Two expected advisory warnings: mandatory
base-image arguments have no defaults, and Docker's secret-name heuristic
mistakes the public `SCCACHE_REDIS_KEY_PREFIX` for a secret.
- `configured` Dockerfile stage built for x86/64 on both OpenWrt releases.
Pinned feeds select Rust 1.94.0 on 25.12.5 and 1.90.0 on 24.10.8. Both retain
`CONFIG_RUST_SCCACHE=y` through defconfig and contain the explicit bootstrap
`build.ccache=sccache` setting with LLVM CI downloads disabled.
- Actual OpenWrt make-variable expansion injects sccache into host and package
compilation, despite unexporting an inherited wrapper.
- Real Cargo library compilation in Docker BuildKit: one Redis miss. A fresh
runtime container using the Docker-network endpoint: one hit, zero misses.
- Recreated the newly provisioned Redis container using the same named volume;
a fresh compiler container still recorded one hit, zero misses. No existing
infrastructure containers were restarted.
- Same source/cache namespace with GNU versus musl target: first musl build
missed; repetition hit. Rust 1.90.0 versus 1.91.0: first 1.91 build missed;
repetition hit. All probes reported zero cache read/write errors.
- A disposable OpenWrt package used the real `Build/Compile/Cargo` helper,
SDK GCC/musl linker, OpenWrt LTO/optimization flags and a local Rust library
dependency. It built successfully in two separate containers: one dependency
miss, then one hit. The linked executable correctly remained non-cacheable.
This used a prebuilt Rust 1.90 toolchain for the probe, not a freshly compiled
SDK toolchain; it is not a full SDK/source-bootstrap acceptance result.
- Retained `ci/verify-cache.sh` passed its separate-container miss/hit checks
against that prebuilt validation toolchain at the expected SDK rustc path.
- Publication configuration checked: original Docker Hub namespace/tag format,
verification before push, explicit publish input, temporary credential
directory, no automatic publication trigger. No registry login or push was
attempted.
## Remaining acceptance
1. Enable repository Actions and configure the two Docker Hub secrets. No
runner configuration changes are required.
2. Run a non-publishing Gitea dispatch for the full default matrix. Full Rust
source bootstrap, complete SDK images, and live workflow scheduling have
not been verified in this migration session.
3. With explicit publication approval, run the publishing dispatch, pull the
resulting tags and build the downstream OpenWrt package with the new image.
Actual Docker Hub authorization and publication remain unverified.
4. After acceptance, disable the old Jenkins job and remove `Jenkinsfile`.
It is deliberately retained until the replacement passes these checks.
Remote test logs and disposable probe sources are under
`/tmp/openwrt-sdk-rust-migration` in TheBeast's `gitea-runner` instance. The
Redis volume/service and BuildKit builder remain available for the replacement
pipeline; no cache volume or existing infrastructure was deleted.
+13
View File
@@ -0,0 +1,13 @@
#!/bin/sh
set -eu
: "${SCCACHE_REDIS_ENDPOINT:?Set the persistent sccache Redis endpoint}"
: "${JOBS:=8}"
case "$JOBS" in ''|*[!0-9]*|0) echo 'JOBS must be a positive integer' >&2; exit 1 ;; esac
sccache --start-server
trap 'sccache --show-stats; sccache --stop-server' EXIT
sccache --zero-stats
# Preserve Jenkins' serial diagnostic retry. Both attempts use the same cache.
make package/rust/host/compile -j"$JOBS" || make package/rust/host/compile -j1 V=sc
rm -rf build_dir/host/rustc-*/rustc-*/build dl/rustc-*.tar.*
+6
View File
@@ -0,0 +1,6 @@
# Shared by all SDK jobs on this daemon, including simultaneous dispatches.
[worker.oci]
max-parallelism = 1
gc = true
reservedSpace = "10GB"
maxUsedSpace = "50GB"
+40
View File
@@ -0,0 +1,40 @@
services:
cache:
image: redis:7.4.6-alpine
container_name: openwrt-rust-cache
restart: unless-stopped
command:
- redis-server
- --appendonly
- 'yes'
- --appendfsync
- everysec
- --maxmemory
- 6gb
- --maxmemory-policy
- allkeys-lru
- --save
- ''
mem_limit: 8g
memswap_limit: 8g
cpus: 1
ports:
- '127.0.0.1:6379:6379'
volumes:
- cache:/data
networks:
- jobs
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
interval: 5s
timeout: 3s
retries: 12
volumes:
cache:
name: openwrt-rust-sccache
networks:
jobs:
external: true
name: gitea-runner-jobs
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env python3
"""Resolve the same stable/oldstable x target matrix as Jenkins."""
import json
import os
import re
import subprocess
import tempfile
import urllib.request
def resolve(versions, targets):
versions = [versions[k] for k in ("stable_version", "oldstable_version") if versions.get(k)]
if not versions:
raise ValueError("OpenWrt returned no stable or oldstable version")
targets = [t.strip() for t in targets.split(",") if t.strip()]
if not targets:
raise ValueError("No targets selected")
for target in targets:
if not re.fullmatch(r"[a-z0-9_+-]+/[a-z0-9_+-]+", target):
raise ValueError(f"Invalid target: {target!r}")
for version in versions:
if not re.fullmatch(r"[a-zA-Z0-9_.-]+", version):
raise ValueError(f"Invalid version: {version!r}")
return {"include": [{"target": t.replace("/", "-"), "version": v}
for v in versions for t in dict.fromkeys(targets)]}
if __name__ == "__main__":
with urllib.request.urlopen("https://downloads.openwrt.org/.versions.json", timeout=60) as response:
versions = json.load(response)
targets = os.environ.get("TARGETS", "x86/64,mediatek/filogic,octeon/generic")
if targets.strip() == "all":
with tempfile.TemporaryDirectory(prefix="openwrt-targets-") as source:
subprocess.run(["git", "clone", "--depth", "1", "https://github.com/openwrt/openwrt.git", source], check=True)
output = subprocess.check_output(["perl", "scripts/dump-target-info.pl", "targets"], cwd=source, text=True)
targets = ",".join(line.split()[0] for line in output.splitlines() if line.strip())
matrix = json.dumps(resolve(versions, targets), separators=(",", ":"))
print(matrix)
with open(os.environ["GITHUB_OUTPUT"], "a") as output:
output.write(f"matrix={matrix}\n")
+11
View File
@@ -0,0 +1,11 @@
#!/bin/sh
set -eu
docker compose -p openwrt-rust -f ci/cache.compose.yml up -d --wait
if ! docker buildx inspect openwrt-sdk-rust >/dev/null 2>&1; then
docker buildx create --name openwrt-sdk-rust --driver docker-container \
--driver-opt network=host,memory=20g,memory-swap=20g,cpu-period=100000,cpu-quota=800000 \
--buildkitd-config ci/buildkitd.toml \
--buildkitd-flags '--allow-insecure-entitlement network.host' || \
docker buildx inspect openwrt-sdk-rust
fi
docker buildx inspect openwrt-sdk-rust --bootstrap
+37
View File
@@ -0,0 +1,37 @@
#!/bin/sh
# Compile in two disposable containers: the second must read the first's cache.
set -eu
image=${1:?Usage: verify-cache.sh IMAGE}
nonce=$(mktemp -d)
trap 'rmdir "$nonce"' EXIT
prefix="validation:${image##*:}:$(basename "$nonce"):"
for expected in miss hit; do
docker run --rm -i --network gitea-runner-jobs --cpus 2 \
--memory 2g --memory-swap 2g \
-e SCCACHE_REDIS_ENDPOINT=redis://openwrt-rust-cache:6379 \
-e SCCACHE_REDIS_KEY_PREFIX="$prefix" -e EXPECTED="$expected" \
"$image" sh -es <<'PROBE'
mkdir /tmp/sccache-probe
cd /tmp/sccache-probe
printf 'pub fn probe(n: u64) -> u64 { n.wrapping_mul(17) }\n' > probe.rs
sccache --start-server
trap 'sccache --stop-server' EXIT
sccache --zero-stats
sccache /builder/staging_dir/host/bin/rustc --crate-name ci_sccache_probe \
--crate-type rlib --emit link,dep-info --out-dir . probe.rs
sccache --show-stats
sccache --show-stats --stats-format json > stats.json
python3 - <<'PY'
import json
import os
data = json.load(open("stats.json"))
assert data["cache_location"].startswith("redis,"), data
stats = data["stats"]
field = "cache_hits" if os.environ["EXPECTED"] == "hit" else "cache_misses"
assert sum(stats[field]["counts"].values()) == 1, data
assert stats["cache_read_errors"] == stats["cache_write_errors"] == 0, data
print("Verified persistent cache", os.environ["EXPECTED"])
PY
PROBE
done