From 9d8ab57b217cee586d8608c43671970d81f80b4c Mon Sep 17 00:00:00 2001 From: Nathan Calvas Date: Thu, 8 Oct 2026 04:46:06 -0400 Subject: [PATCH 1/4] ci: make rust-setup safe to rerun on a self-hosted runner The protoc includes are copied rather than moved, since mv fails once /usr/local/include/google exists from an earlier job. On self-hosted runners mold is installed without replacing the system ld, which would otherwise stay in place for every later job on the host. --- .github/actions/rust-setup/action.yaml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/actions/rust-setup/action.yaml b/.github/actions/rust-setup/action.yaml index 4b7f0edec..1a9525565 100644 --- a/.github/actions/rust-setup/action.yaml +++ b/.github/actions/rust-setup/action.yaml @@ -27,12 +27,20 @@ runs: wget -q https://github.com/protocolbuffers/protobuf/releases/download/v21.8/protoc-21.8-linux-x86_64.zip unzip -q protoc*.zip sudo mv bin/protoc /usr/local/bin - sudo mv include/google /usr/local/include + # cp, not mv: a self-hosted runner keeps /usr/local/include/google from + # the previous job, and mv refuses to replace a non-empty directory. + sudo cp -r include/google /usr/local/include/ rm -rf protoc*.zip bin include readme.txt + # make-default replaces the system /usr/bin/ld with mold. That is fine on a + # throwaway GitHub-hosted VM, but a self-hosted runner keeps it for every + # later job, including other repos' builds, so there mold is installed + # without taking over ld. - name: Setup mold linker if: inputs.mold == 'true' uses: rui314/setup-mold@v1 + with: + make-default: ${{ runner.environment == 'github-hosted' }} - name: Install toolchain uses: dtolnay/rust-toolchain@master From 9448b64262980cdaceb43926df437370e0c9287a Mon Sep 17 00:00:00 2001 From: Nathan Calvas Date: Thu, 8 Oct 2026 04:46:06 -0400 Subject: [PATCH 2/4] ci(rust): free disk space only on GitHub-hosted runners The step strips the hosted image. On a persistent self-hosted runner it would uninstall toolchains and prune Docker images other jobs use. --- .github/workflows/ci-rust.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci-rust.yaml b/.github/workflows/ci-rust.yaml index 4bb1f14d5..4465406db 100644 --- a/.github/workflows/ci-rust.yaml +++ b/.github/workflows/ci-rust.yaml @@ -59,7 +59,11 @@ jobs: METEROID_TEST_PG_URL: postgres://postgres:postgres@127.0.0.1:5432/postgres steps: - uses: actions/checkout@v7 + # Strips the GitHub-hosted image to make room. On a self-hosted runner it + # would uninstall toolchains and prune Docker images that later jobs + # rely on, and those runners have the disk to spare. - name: Free disk space + if: runner.environment == 'github-hosted' run: | df -h sudo apt-get remove -y '^llvm-.*' From 0ba2148d30acfa772844ec8a3210bfd3f8389222 Mon Sep 17 00:00:00 2001 From: Nathan Calvas Date: Thu, 8 Oct 2026 04:46:06 -0400 Subject: [PATCH 3/4] ci(docker): keep image digests under runner.temp /tmp outlives the job on a self-hosted runner, so digests from earlier builds would end up in the manifest list. runner.temp is emptied after every job. --- .github/workflows/docker.yaml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 8985be029..2cea2d022 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -113,17 +113,20 @@ jobs: provenance: false outputs: type=image,name=${{ env.IMAGE_PREFIX }}/${{ matrix.image_type.name }},push-by-digest=true,name-canonical=true,push=true,store=true + # Under runner.temp, which the runner empties after every job. A fixed + # /tmp path survives on a self-hosted runner, and the manifest job would + # then pick up digests left by earlier builds. - name: Export digest run: | - mkdir -p /tmp/digests/${{ matrix.image_type.name }} + mkdir -p ${{ runner.temp }}/digests/${{ matrix.image_type.name }} digest="${{ steps.build.outputs.digest }}" - touch "/tmp/digests/${{ matrix.image_type.name }}/${digest#sha256:}" + touch "${{ runner.temp }}/digests/${{ matrix.image_type.name }}/${digest#sha256:}" - name: Upload digest uses: actions/upload-artifact@v7 with: name: digests-${{ matrix.image_type.name }}-${{ env.PLATFORM_PAIR }} - path: /tmp/digests/${{ matrix.image_type.name }}/* + path: ${{ runner.temp }}/digests/${{ matrix.image_type.name }}/* retention-days: 1 manifest: @@ -140,7 +143,7 @@ jobs: - name: Download digests uses: actions/download-artifact@v8 with: - path: /tmp/digests + path: ${{ runner.temp }}/digests pattern: digests-${{ matrix.image_type }}-* merge-multiple: true @@ -174,7 +177,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Create and push manifest list - working-directory: /tmp/digests + working-directory: ${{ runner.temp }}/digests run: | tags=() while IFS= read -r tag; do @@ -190,7 +193,7 @@ jobs: - name: Push tip tag if: github.ref == 'refs/heads/main' - working-directory: /tmp/digests + working-directory: ${{ runner.temp }}/digests run: | images=() for digest in *; do From 6b758b72675c246c8698ece45e1aa49d24bc0a77 Mon Sep 17 00:00:00 2001 From: Nathan Calvas Date: Thu, 8 Oct 2026 04:47:57 -0400 Subject: [PATCH 4/4] ci: run CI on the self-hosted triton runners Pushes, dispatches and same-repo pull requests run on the triton group. Pull requests from forks stay on GitHub-hosted runners since the repo is public. The arm64 Docker builds stay on GitHub-hosted, as there are no arm64 self-hosted runners. The Helm chart, preview and Claude workflows are unchanged. --- .github/workflows/audit-rust.yaml | 4 +++- .github/workflows/ci-rust.yaml | 12 +++++++++--- .github/workflows/ci-ui.yaml | 4 +++- .github/workflows/docker.yaml | 9 +++++++-- .github/workflows/openapi-rust.yaml | 4 +++- .github/workflows/release-artifact.yaml | 11 +++++++---- .github/workflows/release-drafter.yml | 8 ++++++-- 7 files changed, 38 insertions(+), 14 deletions(-) diff --git a/.github/workflows/audit-rust.yaml b/.github/workflows/audit-rust.yaml index 6dab67552..450ee18df 100644 --- a/.github/workflows/audit-rust.yaml +++ b/.github/workflows/audit-rust.yaml @@ -25,7 +25,9 @@ concurrency: jobs: audit: - runs-on: ubuntu-latest + # Pull requests from forks stay on GitHub-hosted runners: the repo is + # public, and self-hosted runners must not run untrusted code. + runs-on: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && fromJSON('{"group":"triton","labels":["self-hosted","APP"]}') || 'ubuntu-latest' }} steps: - name: Checkout code diff --git a/.github/workflows/ci-rust.yaml b/.github/workflows/ci-rust.yaml index 4465406db..fc0d63df8 100644 --- a/.github/workflows/ci-rust.yaml +++ b/.github/workflows/ci-rust.yaml @@ -39,7 +39,9 @@ env: jobs: check: name: Test - runs-on: ubuntu-latest + # Pull requests from forks stay on GitHub-hosted runners: the repo is + # public, and self-hosted runners must not run untrusted code. + runs-on: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && fromJSON('{"group":"triton","labels":["self-hosted","APP"]}') || 'ubuntu-latest' }} timeout-minutes: 45 services: postgres: @@ -96,7 +98,9 @@ jobs: fmt: name: Rustfmt - runs-on: ubuntu-latest + # Pull requests from forks stay on GitHub-hosted runners: the repo is + # public, and self-hosted runners must not run untrusted code. + runs-on: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && fromJSON('{"group":"triton","labels":["self-hosted","APP"]}') || 'ubuntu-latest' }} steps: - uses: actions/checkout@v7 - uses: ./.github/actions/rust-setup @@ -109,7 +113,9 @@ jobs: clippy: name: Clippy - runs-on: ubuntu-latest + # Pull requests from forks stay on GitHub-hosted runners: the repo is + # public, and self-hosted runners must not run untrusted code. + runs-on: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && fromJSON('{"group":"triton","labels":["self-hosted","APP"]}') || 'ubuntu-latest' }} steps: - uses: actions/checkout@v7 - uses: ./.github/actions/rust-setup diff --git a/.github/workflows/ci-ui.yaml b/.github/workflows/ci-ui.yaml index e75cf8333..a490dcb97 100644 --- a/.github/workflows/ci-ui.yaml +++ b/.github/workflows/ci-ui.yaml @@ -18,7 +18,9 @@ concurrency: jobs: lint: - runs-on: ubuntu-latest + # Pull requests from forks stay on GitHub-hosted runners: the repo is + # public, and self-hosted runners must not run untrusted code. + runs-on: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && fromJSON('{"group":"triton","labels":["self-hosted","APP"]}') || 'ubuntu-latest' }} name: Lint env: TURBO_TELEMETRY_DISABLED: true diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 2cea2d022..784449341 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -44,12 +44,15 @@ jobs: proto_arch: x86_64 grpc_health_probe_arch: amd64 prefix: amd64 - runs_on: ubuntu-latest + runs_on: + group: triton + labels: [self-hosted, APP] - platform: linux/arm64 mold_arch: aarch64 proto_arch: aarch_64 grpc_health_probe_arch: arm64 prefix: arm64 + # GitHub-hosted: there are no arm64 self-hosted runners. runs_on: ubuntu-24.04-arm runs-on: ${{ matrix.runs_on }} steps: @@ -138,7 +141,9 @@ jobs: - meteroid-scheduler - metering-api - meteroid-web - runs-on: ubuntu-latest + runs-on: + group: triton + labels: [self-hosted, APP] steps: - name: Download digests uses: actions/download-artifact@v8 diff --git a/.github/workflows/openapi-rust.yaml b/.github/workflows/openapi-rust.yaml index bca178b7e..227e3a719 100644 --- a/.github/workflows/openapi-rust.yaml +++ b/.github/workflows/openapi-rust.yaml @@ -29,7 +29,9 @@ env: jobs: openapi: name: Open API - runs-on: ubuntu-latest + # Pull requests from forks stay on GitHub-hosted runners: the repo is + # public, and self-hosted runners must not run untrusted code. + runs-on: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && fromJSON('{"group":"triton","labels":["self-hosted","APP"]}') || 'ubuntu-latest' }} steps: - uses: actions/checkout@v7 - uses: ./.github/actions/rust-setup diff --git a/.github/workflows/release-artifact.yaml b/.github/workflows/release-artifact.yaml index d51db69fd..9ecb7ed32 100644 --- a/.github/workflows/release-artifact.yaml +++ b/.github/workflows/release-artifact.yaml @@ -27,10 +27,13 @@ concurrency: jobs: build: name: Build deploy artifact - # Pinned, not ubuntu-latest. The deploy targets run Ubuntu 24.04 and these - # binaries are dynamically linked; a runner on a newer glibc would produce - # binaries that will not load there. - runs-on: ubuntu-24.04 + # The deploy targets run Ubuntu 24.04 and these binaries are dynamically + # linked; a runner on a newer glibc would produce binaries that will not + # load there. The triton runners are Ubuntu 24.04 too: move this back to + # a pinned ubuntu-24.04 image if that ever stops being true. + runs-on: + group: triton + labels: [self-hosted, APP] timeout-minutes: 60 steps: diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index d0047508e..4f1d36325 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -21,7 +21,9 @@ jobs: permissions: pull-requests: read name: Validate PR title - runs-on: ubuntu-latest + # Pull requests from forks stay on GitHub-hosted runners: the repo is + # public, and self-hosted runners must not run untrusted code. + runs-on: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && fromJSON('{"group":"triton","labels":["self-hosted","APP"]}') || 'ubuntu-latest' }} if: github.event_name == 'pull_request' steps: - uses: amannn/action-semantic-pull-request@v6 @@ -48,7 +50,9 @@ jobs: # write permission is required for autolabeler # otherwise, read permission is required at least pull-requests: write - runs-on: ubuntu-latest + # Pull requests from forks stay on GitHub-hosted runners: the repo is + # public, and self-hosted runners must not run untrusted code. + runs-on: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && fromJSON('{"group":"triton","labels":["self-hosted","APP"]}') || 'ubuntu-latest' }} steps: # (Optional) GitHub Enterprise requires GHE_HOST variable set #- name: Set GHE_HOST