Skip to content

Commit 6cdd7f4

Browse files
feat(ci): containerize macOS self-hosted runner
Add macOS container image for GitHub Actions self-hosted runner using jianliang00/container (Apple Virtualization framework fork). The image packages CLT, Homebrew, mise, Node.js 24, Deno 2.5, Task 3.49, Rust nightly-2026-02-09, cargo-tauri, sccache, and the Actions runner binary. Key changes: - docker/macos/Dockerfile: macOS runner image (pre-built tarballs, no network during build) - docker/macos/entrypoint.sh: ephemeral runner lifecycle with signal trapping - scripts/runner.sh: unified CLI (prepare, build, start, stop, status, clean) - Restructure docker/ by OS: linux/Dockerfile, macos/Dockerfile - Update all references to old docker/Dockerfile.linux-amd64 path - Add .hadolint.yaml with trusted registries and project ignores - Add hadolint usage to AGENTS.md Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 962c129 commit 6cdd7f4

File tree

13 files changed

+489
-9
lines changed

13 files changed

+489
-9
lines changed

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ jobs:
135135
uses: useblacksmith/build-push-action@v2
136136
with:
137137
context: .
138-
file: docker/Dockerfile.linux-amd64
138+
file: docker/linux/Dockerfile
139139
target: artifacts
140140
push: false
141141
outputs: type=local,dest=dist

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ jobs:
149149
uses: useblacksmith/build-push-action@v2
150150
with:
151151
context: .
152-
file: docker/Dockerfile.linux-amd64
152+
file: docker/linux/Dockerfile
153153
target: check
154154
push: false
155155

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,11 @@ target
210210
# Contains mutation testing data
211211
**/mutants.out*/
212212

213+
# macOS container build context (large tarballs from `runner.sh prepare`)
214+
docker/macos/*.tar
215+
docker/macos/*.tar.gz
216+
docker/macos/*.pkg
217+
213218
# INCLUDE
214219
!**/.gitkeep
215220
!**/*.example

.hadolint.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
failure-threshold: warning
2+
3+
ignored:
4+
- DL3008 # pin versions in apt-get
5+
- DL3059 # multiple consecutive RUN (intentional in multi-stage)
6+
- DL4006 # pipefail -- macOS container tool does not support SHELL
7+
8+
trustedRegistries:
9+
- docker.io
10+
- ghcr.io

.tool-versions

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ nodejs 24.2.0
77
prek 0.3.2
88
ruff 0.12.11
99
uv 0.9.24
10+
hadolint 2.12.0

AGENTS.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,13 @@ cargo nextest run --workspace # Tests (falls back to cargo t
145145
cargo check --manifest-path src-tauri/Cargo.toml # Fast type check (no binary)
146146
```
147147

148+
**Dockerfiles**:
149+
150+
```bash
151+
hadolint docker/linux/Dockerfile # Lint Linux Dockerfile
152+
hadolint docker/macos/Dockerfile # Lint macOS Dockerfile
153+
```
154+
148155
### When to Run
149156

150157
- **Before committing**: `task lint && task format`
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
id: TASK-312
3+
title: Containerize macOS self-hosted GitHub Actions runner
4+
status: In Progress
5+
assignee: []
6+
created_date: '2026-04-06 07:02'
7+
updated_date: '2026-04-06 07:02'
8+
labels:
9+
- ci
10+
- infrastructure
11+
- macos
12+
dependencies: []
13+
references:
14+
- 'https://github.com/jianliang00/container'
15+
- 'https://github.com/jianliang00/container/releases/tag/0.0.1'
16+
- docker/macos/Dockerfile
17+
- docker/macos/entrypoint.sh
18+
- scripts/runner.sh
19+
priority: medium
20+
---
21+
22+
## Description
23+
24+
<!-- SECTION:DESCRIPTION:BEGIN -->
25+
Containerize the macOS ARM64 self-hosted runner using jianliang00/container (Apple Virtualization framework fork). Replaces bare-metal runner with a reproducible, isolated container image containing all CI dependencies.
26+
27+
The container image packages pre-built tarballs from the host (no network during macOS container builds) including CLT, Homebrew, mise, Node.js, Deno, Task, Rust nightly, cargo tools, and the GitHub Actions runner binary.
28+
<!-- SECTION:DESCRIPTION:END -->
29+
30+
## Acceptance Criteria
31+
<!-- AC:BEGIN -->
32+
- [x] #1 Container image builds successfully with `scripts/runner.sh build`
33+
- [x] #2 All tools verified in smoke test (Rust, Node, Deno, Task, Brew, sccache, gh, CLT)
34+
- [ ] #3 Runner registers with GitHub and picks up jobs
35+
- [ ] #4 Existing workflow labels (macOS, ARM64) work without changes
36+
- [x] #5 Build context prepared from host tools via `scripts/runner.sh prepare`
37+
- [x] #6 Linux Dockerfile relocated to docker/linux/Dockerfile with all references updated
38+
- [x] #7 hadolint passes on both Dockerfiles
39+
<!-- AC:END -->

docker/macos/Dockerfile

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# syntax=docker/dockerfile:1.17.1
2+
# check=skip=all
3+
4+
FROM ghcr.io/jianliang00/macos-base:26.3
5+
6+
# hadolint ignore=DL4006 -- container tool does not support SHELL directive
7+
# No network during build -- all dependencies are COPY'd as pre-built tarballs.
8+
# Run `scripts/runner.sh prepare` on the host to generate the build context.
9+
10+
# Preserve the full default macOS PATH through all ENV directives
11+
ENV BASE_PATH="/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin"
12+
13+
# Xcode Command Line Tools
14+
COPY CLTools.pkg /tmp/CLTools.tar
15+
RUN tar xf /tmp/CLTools.tar -C / \
16+
&& rm /tmp/CLTools.tar \
17+
&& xcode-select --switch /Library/Developer/CommandLineTools \
18+
&& echo "CLT installed at $(xcode-select -p)"
19+
20+
# Homebrew + packages
21+
COPY homebrew.tar /tmp/homebrew.tar
22+
RUN /bin/mkdir -p /opt/homebrew \
23+
&& tar xf /tmp/homebrew.tar -C /opt/homebrew \
24+
&& rm /tmp/homebrew.tar
25+
26+
# mise
27+
COPY mise.tar /tmp/mise.tar
28+
RUN /bin/mkdir -p /Users/Shared/.local/bin \
29+
&& tar xf /tmp/mise.tar -C /Users/Shared/.local/bin \
30+
&& rm /tmp/mise.tar
31+
32+
# Node.js
33+
COPY node.tar /tmp/node.tar
34+
RUN /bin/mkdir -p /opt/node \
35+
&& tar xf /tmp/node.tar -C /opt/node \
36+
&& rm /tmp/node.tar
37+
38+
# Deno
39+
COPY deno.tar /tmp/deno.tar
40+
RUN /bin/mkdir -p /opt/deno/bin \
41+
&& tar xf /tmp/deno.tar -C /opt/deno/bin \
42+
&& rm /tmp/deno.tar
43+
44+
# Task
45+
COPY task.tar /tmp/task.tar
46+
RUN /bin/mkdir -p /opt/task/bin \
47+
&& tar xf /tmp/task.tar -C /opt/task/bin \
48+
&& rm /tmp/task.tar
49+
50+
# Rust toolchain
51+
COPY rust.tar /tmp/rust.tar
52+
RUN /bin/mkdir -p /Users/Shared/.rustup /Users/Shared/.cargo \
53+
&& tar xf /tmp/rust.tar -C /Users/Shared \
54+
&& rm /tmp/rust.tar \
55+
&& cd /Users/Shared/.cargo/bin \
56+
&& for link in $(find . -type l); do \
57+
target=$(readlink "$link"); \
58+
case "$target" in */rustup) ln -sf /Users/Shared/.cargo/bin/rustup "$link" ;; esac; \
59+
done
60+
61+
# Cargo tools (tauri-cli, cargo-binstall)
62+
COPY cargo-tools.tar /tmp/cargo-tools.tar
63+
RUN tar xf /tmp/cargo-tools.tar -C /Users/Shared/.cargo/bin \
64+
&& rm /tmp/cargo-tools.tar
65+
66+
# GitHub Actions runner
67+
COPY actions-runner.tar.gz /tmp/actions-runner.tar.gz
68+
WORKDIR /opt/actions-runner
69+
RUN tar xzf /tmp/actions-runner.tar.gz \
70+
&& rm /tmp/actions-runner.tar.gz
71+
72+
# Set final PATH with all tools
73+
ENV PATH="/Users/Shared/.cargo/bin:/Users/Shared/.local/bin:/Users/Shared/.local/share/mise/shims:/opt/homebrew/bin:/opt/homebrew/sbin:/opt/node/bin:/opt/deno/bin:/opt/task/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin"
74+
ENV RUSTUP_HOME="/Users/Shared/.rustup" \
75+
CARGO_HOME="/Users/Shared/.cargo"
76+
77+
COPY entrypoint.sh /opt/actions-runner/entrypoint.sh
78+
RUN chmod +x /opt/actions-runner/entrypoint.sh
79+
ENTRYPOINT ["/opt/actions-runner/entrypoint.sh"]

docker/macos/entrypoint.sh

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
5+
export HOME="${HOME:-/Users/Shared}"
6+
7+
# PATH setup -- mirrors .github/actions/setup-tauri-build/action.yml
8+
export PATH="$HOME/.cargo/bin:$HOME/.local/share/mise/shims:/opt/homebrew/bin:/opt/homebrew/sbin:$PATH"
9+
10+
# Enable sccache for Rust builds (CI taskfile disables it; override here)
11+
export RUSTC_WRAPPER=sccache
12+
13+
# Required environment variables
14+
: "${GITHUB_REPO_URL:?GITHUB_REPO_URL is required}"
15+
: "${RUNNER_TOKEN:?RUNNER_TOKEN is required}"
16+
17+
RUNNER_NAME="${RUNNER_NAME:-mt-macos-container}"
18+
RUNNER_LABELS="${RUNNER_LABELS:-macOS,ARM64}"
19+
RUNNER_WORKDIR="${RUNNER_WORKDIR:-_work}"
20+
21+
cleanup() {
22+
echo "Caught signal, removing runner..."
23+
./config.sh remove --token "${RUNNER_TOKEN}" 2>/dev/null || true
24+
exit 0
25+
}
26+
trap cleanup SIGTERM SIGINT
27+
28+
echo "Configuring runner '${RUNNER_NAME}' with labels '${RUNNER_LABELS}'..."
29+
./config.sh \
30+
--url "${GITHUB_REPO_URL}" \
31+
--token "${RUNNER_TOKEN}" \
32+
--name "${RUNNER_NAME}" \
33+
--labels "${RUNNER_LABELS}" \
34+
--work "${RUNNER_WORKDIR}" \
35+
--ephemeral \
36+
--unattended \
37+
--replace
38+
39+
echo "Starting runner..."
40+
./run.sh &
41+
wait $!

0 commit comments

Comments
 (0)