From b38b4b3d6093657b964bff3f9ce761a272763c8f Mon Sep 17 00:00:00 2001 From: Chris Oliver Date: Wed, 14 Oct 2026 12:55:04 -0500 Subject: [PATCH 1/2] Build x86_64_linux releases on the self-hosted runners build.yml gains a self_hosted input. When set, x86_64_linux jobs run on [self-hosted, linux, x64], the same labels hatchbox.io's CI uses; arm64_linux always stays on GitHub's hosted arm runner because the self-hosted runners are x64 only. release.yml turns it on by default, with a workflow_dispatch input to fall back to GitHub's runners when needed. tests.yml leaves it off so pull requests, which anyone can open on this public repository, keep running on GitHub's runners. The manylinux container runs as root, so the files it writes into the workspace are root-owned. On a hosted runner that never mattered, but a self-hosted workspace outlives the job and the next checkout's clean would fail on those files. The in-container chown that only ran after a successful build becomes an always() step that hands the whole workspace back to the runner user. --- .github/workflows/build.yml | 29 ++++++++++++++++++++++++++--- .github/workflows/release.yml | 6 ++++++ .github/workflows/tests.yml | 2 ++ 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 95f65e9..814c5cd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,11 +6,23 @@ on: version: required: true type: string + self_hosted: + description: Build x86_64_linux on the self-hosted runners instead of GitHub's + required: false + type: boolean + default: false workflow_call: inputs: version: required: true type: string + # The self-hosted runners are x64 Ubuntu VMs at home, so only x86_64_linux can move + # there; arm64_linux always builds on GitHub's hosted arm runner. Off by default so + # pull requests, which anyone can open on this public repository, never run on them. + self_hosted: + required: false + type: boolean + default: false permissions: contents: read @@ -41,12 +53,14 @@ jobs: - id: matrix env: YJIT: ${{ steps.info.outputs.yjit }} + SELF_HOSTED: ${{ inputs.self_hosted }} run: | # Both variants per target when the series has YJIT, otherwise one build per target # (bin/package names it without the .no_yjit suffix). if [[ "$YJIT" == "true" ]]; then yjits='[true, false]'; else yjits='[false]'; fi - echo "linux=$(jq -c -n --argjson yjits "$yjits" '{include: [ - {runner: "ubuntu-22.04", target: "x86_64_linux"}, + if [[ "$SELF_HOSTED" == "true" ]]; then x86='["self-hosted", "linux", "x64"]'; else x86='"ubuntu-22.04"'; fi + echo "linux=$(jq -c -n --argjson yjits "$yjits" --argjson x86 "$x86" '{include: [ + {runner: $x86, target: "x86_64_linux"}, {runner: "ubuntu-22.04-arm", target: "arm64_linux"} ] | map(. as $t | $yjits[] | $t + {yjit: .})}')" >> "$GITHUB_OUTPUT" @@ -118,9 +132,18 @@ jobs: fi bin/package "$VERSION" --target "$TARGET" "$yjit_arg" --output rubies - chown -R "$(stat -c %u /work):$(stat -c %g /work)" rubies .build .cache || true ' + # The container runs as root, so everything it wrote into the workspace is root-owned. + # On a self-hosted runner the workspace outlives the job, and the next checkout's clean + # would fail on those files, so hand them back to the runner user even after a failed + # build. + - name: Fix workspace ownership + if: always() && steps.target.outputs.container != '' + env: + CONTAINER: ${{ steps.target.outputs.container }} + run: docker run --rm -v "$PWD:/work" "$CONTAINER" chown -R "$(id -u):$(id -g)" /work + - name: Upload portable Ruby uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6de6250..3683213 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,6 +18,11 @@ on: required: false type: boolean default: false + self_hosted: + description: 'Build x86_64_linux on the self-hosted runners (arm64 stays on GitHub)' + required: false + type: boolean + default: true env: # Revision releases of a version to keep after publishing a new one: the current build @@ -119,6 +124,7 @@ jobs: uses: ./.github/workflows/build.yml with: version: ${{ inputs.version }} + self_hosted: ${{ inputs.self_hosted }} combine-hashes: needs: build diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f876740..1b9306c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,6 +29,8 @@ jobs: ruby -c libexec/validate_recipes.rb ruby -c libexec/update_ruby_recipe.rb + # Stays on GitHub's runners (build.yml's self_hosted defaults to false): a pull request + # can come from anyone, and the self-hosted runners are machines at home. build: needs: syntax if: github.event_name == 'pull_request' From a177b98111ae8efc040b15de564dc753e34de3df Mon Sep 17 00:00:00 2001 From: Chris Oliver Date: Wed, 14 Oct 2026 13:19:48 -0500 Subject: [PATCH 2/2] Resolve the manylinux container in the plan job so build jobs need no host Ruby ruby/setup-ruby on a self-hosted runner installs into the shared /opt/hostedtoolcache, and two jobs extracting the same Ruby there at once race each other: a test run on the org runners failed with tar unable to open libruby.so while the other runner was writing it. The only thing the build jobs used Ruby for was reading the target's container out of recipes/targets.yml, which the plan job can do once and carry in the matrix. The job gets an explicit name so the container reference does not end up in it. --- .github/workflows/build.yml | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 814c5cd..9b578c4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -59,12 +59,17 @@ jobs: # (bin/package names it without the .no_yjit suffix). if [[ "$YJIT" == "true" ]]; then yjits='[true, false]'; else yjits='[false]'; fi if [[ "$SELF_HOSTED" == "true" ]]; then x86='["self-hosted", "linux", "x64"]'; else x86='"ubuntu-22.04"'; fi - echo "linux=$(jq -c -n --argjson yjits "$yjits" --argjson x86 "$x86" '{include: [ + # Each target's pinned manylinux container is resolved here so the build jobs need + # no host Ruby: ruby/setup-ruby on self-hosted runners installs into the shared + # /opt/hostedtoolcache, and two jobs extracting the same Ruby there at once race. + containers=$(ruby -ryaml -rjson -e 'puts YAML.load_file("recipes/targets.yml").fetch("targets").transform_values { |t| t.fetch("container") }.to_json') + echo "linux=$(jq -c -n --argjson yjits "$yjits" --argjson x86 "$x86" --argjson containers "$containers" '{include: [ {runner: $x86, target: "x86_64_linux"}, {runner: "ubuntu-22.04-arm", target: "arm64_linux"} - ] | map(. as $t | $yjits[] | $t + {yjit: .})}')" >> "$GITHUB_OUTPUT" + ] | map(. as $t | $yjits[] | $t + {yjit: ., container: $containers[$t.target]})}')" >> "$GITHUB_OUTPUT" build-linux: + name: build-linux (${{ matrix.target }}, yjit=${{ matrix.yjit }}) needs: plan strategy: fail-fast: false @@ -72,24 +77,13 @@ jobs: runs-on: ${{ matrix.runner }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: ruby/setup-ruby@v1 - with: - ruby-version: .ruby-version - - - name: Resolve target container - id: target - env: - TARGET: ${{ matrix.target }} - run: | - container=$(ruby -ryaml -e 'puts YAML.load_file("recipes/targets.yml").fetch("targets").fetch(ENV.fetch("TARGET")).fetch("container")') - echo "container=$container" >> "$GITHUB_OUTPUT" - name: Build portable Ruby in manylinux env: VERSION: ${{ inputs.version }} TARGET: ${{ matrix.target }} YJIT: ${{ matrix.yjit }} - CONTAINER: ${{ steps.target.outputs.container }} + CONTAINER: ${{ matrix.container }} run: | set -euxo pipefail docker pull "$CONTAINER" @@ -139,9 +133,9 @@ jobs: # would fail on those files, so hand them back to the runner user even after a failed # build. - name: Fix workspace ownership - if: always() && steps.target.outputs.container != '' + if: always() env: - CONTAINER: ${{ steps.target.outputs.container }} + CONTAINER: ${{ matrix.container }} run: docker run --rm -v "$PWD:/work" "$CONTAINER" chown -R "$(id -u):$(id -g)" /work - name: Upload portable Ruby