From 26c68c5bffc65872d1bef116ae61566e81a02dea Mon Sep 17 00:00:00 2001 From: Jeremy Harrington Date: Sat, 10 Oct 2026 08:01:46 -0700 Subject: [PATCH 1/3] ci: route trusted Linux jobs to the openctl runner --- .github/workflows/ci.yaml | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0c29c83..b3f8330 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -6,6 +6,12 @@ on: pull_request: branches: [main] +permissions: + contents: read + +# Only main pushes and same-repository PRs may use the homelab runner. +# Fork PRs remain GitHub-hosted, even after their workflow runs are approved. + env: # Modules declare `go 1.25.0`; setup-go auto-downloads that toolchain # if it's newer than this baseline, but golangci-lint is compiled @@ -16,16 +22,25 @@ env: jobs: build: name: Build - runs-on: ubuntu-latest + runs-on: ${{ fromJSON((github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && '["self-hosted","Linux","X64","openctl-linux"]' || '["ubuntu-latest"]') }} steps: - name: Checkout code uses: actions/checkout@v4 + with: + persist-credentials: false - name: Set up Go uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: "24" + cache: npm + cache-dependency-path: ui/package-lock.json + - name: Download dependencies run: make deps @@ -34,10 +49,12 @@ jobs: test: name: Test - runs-on: ubuntu-latest + runs-on: ${{ fromJSON((github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && '["self-hosted","Linux","X64","openctl-linux"]' || '["ubuntu-latest"]') }} steps: - name: Checkout code uses: actions/checkout@v4 + with: + persist-credentials: false - name: Set up Go uses: actions/setup-go@v5 @@ -59,10 +76,12 @@ jobs: lint: name: Lint - runs-on: ubuntu-latest + runs-on: ${{ fromJSON((github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && '["self-hosted","Linux","X64","openctl-linux"]' || '["ubuntu-latest"]') }} steps: - name: Checkout code uses: actions/checkout@v4 + with: + persist-credentials: false - name: Set up Go uses: actions/setup-go@v5 @@ -92,10 +111,12 @@ jobs: vet: name: Vet & Staticcheck - runs-on: ubuntu-latest + runs-on: ${{ fromJSON((github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && '["self-hosted","Linux","X64","openctl-linux"]' || '["ubuntu-latest"]') }} steps: - name: Checkout code uses: actions/checkout@v4 + with: + persist-credentials: false - name: Set up Go uses: actions/setup-go@v5 @@ -134,10 +155,12 @@ jobs: format: name: Format Check - runs-on: ubuntu-latest + runs-on: ${{ fromJSON((github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && '["self-hosted","Linux","X64","openctl-linux"]' || '["ubuntu-latest"]') }} steps: - name: Checkout code uses: actions/checkout@v4 + with: + persist-credentials: false - name: Set up Go uses: actions/setup-go@v5 @@ -154,10 +177,12 @@ jobs: modernize: name: Modernize Check - runs-on: ubuntu-latest + runs-on: ${{ fromJSON((github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && '["self-hosted","Linux","X64","openctl-linux"]' || '["ubuntu-latest"]') }} steps: - name: Checkout code uses: actions/checkout@v4 + with: + persist-credentials: false - name: Set up Go uses: actions/setup-go@v5 From 9e7a6fdfd01b6c798958134490bf3208e287bcaa Mon Sep 17 00:00:00 2001 From: Jeremy Harrington Date: Sat, 10 Oct 2026 08:26:53 -0700 Subject: [PATCH 2/3] docs: explain trusted and fork CI runner routing --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index daabc3c..1b53641 100644 --- a/README.md +++ b/README.md @@ -743,6 +743,19 @@ make lint make modernize-check ``` +### CI runners + +All six Linux jobs in `.github/workflows/ci.yaml` use the repository's +self-hosted runner labeled `openctl-linux` for pushes to `main` and +same-repository pull requests. Fork pull requests use GitHub-hosted +`ubuntu-latest` runners instead, even when their workflow runs are approved. +Do not use `pull_request_target` to run fork code on the homelab runner. + +CI installs Go 1.25, and the UI build explicitly installs Node 24. +Workflow tokens have read-only repository contents access, and checkouts do +not persist credentials. With one registered self-hosted runner, trusted CI +jobs execute sequentially and queue while that runner is busy or offline. + ## Project Layout ```text From 1ab533494d18e9dac0856b1207b3e14652e5b042 Mon Sep 17 00:00:00 2001 From: Jeremy Harrington Date: Sat, 10 Oct 2026 08:28:21 -0700 Subject: [PATCH 3/3] docs: clarify public self-hosted runner approval boundary --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 1b53641..6d1790a 100644 --- a/README.md +++ b/README.md @@ -750,6 +750,9 @@ self-hosted runner labeled `openctl-linux` for pushes to `main` and same-repository pull requests. Fork pull requests use GitHub-hosted `ubuntu-latest` runners instead, even when their workflow runs are approved. Do not use `pull_request_target` to run fork code on the homelab runner. +Keep repository Actions settings at `all_external_contributors` approval. +The routing expression is not a security boundary: PRs can modify workflow +YAML, so review workflow changes before approving an external contributor's run. CI installs Go 1.25, and the UI build explicitly installs Node 24. Workflow tokens have read-only repository contents access, and checkouts do