publish #142
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Copyright (c) 2025-2026 [Ribose Inc](https://www.ribose.com). | |
| # All rights reserved. | |
| # This file is a part of tamatebako | |
| # | |
| # Redistribution and use in source and binary forms, with or without | |
| # modification, are permitted provided that the following conditions | |
| # are met: | |
| # 1. Redistributions of source code must retain the above copyright | |
| # notice, this list of conditions and the following disclaimer. | |
| # 2. Redistributions in binary form must reproduce the above copyright | |
| # notice, this list of conditions and the following disclaimer in the | |
| # documentation and/or other materials provided with the distribution. | |
| # | |
| # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| # ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED | |
| # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR | |
| # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS | |
| # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | |
| # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | |
| # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | |
| # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | |
| # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | |
| # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | |
| # POSSIBILITY OF SUCH DAMAGE. | |
| # The release coordinator — the explicit, operator-facing entry points: | |
| # | |
| # workflow_dispatch: the three first-class slice shapes — | |
| # one ruby on every platform (ruby_filter=<version>, platform=all) | |
| # one platform, every ruby (ruby_filter=full|tidy|catalog, platform=<one>) | |
| # one ruby on one platform (ruby_filter=<version>, platform=<one>) | |
| # plus the era baseline (ruby_filter=catalog, platform=all). | |
| # repository_dispatch "tebako release": a new tamatebako/ruby source pin | |
| # landed — compute_matrix.rb diffs the two pins' SHA256SUMS and only | |
| # the versions whose tarballs moved rebuild (a 4.0-only source release | |
| # rebuilds 4.0.x), on every platform, and publishes. | |
| # | |
| # Every leg fans out to _build-platform.yml to BUILD, and — spec 13 §2a's | |
| # de-rendezvous — to PUBLISH and SIGN in-leg: the leg that | |
| # built a package uploads its write-once names (payload + .sha256 sidecars | |
| # + .manifest.json shard) and signs every served name, all inside the leg | |
| # that holds the fresh bytes. No shared mutable file exists anymore (the | |
| # monolithic manifest.json / SHA256SUMS.txt are gone — each shard IS its | |
| # release-index entry, and the monoliths derive consumer-side), so N legs publish | |
| # concurrently with zero rendezvous. What remains here is the ONE release | |
| # job per coordinator run: it AUDITS the whole matrix against the release | |
| # (read-only) and renders + publishes the tpkg-registry.yaml mirror by bot | |
| # PR (spec 04 §2) — the two duties that genuinely need the full matrix. | |
| name: publish | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| ruby_filter: | |
| description: 'Rubies: "full" (line tips), "tidy", "catalog" (every published version), or comma-separated versions' | |
| type: string | |
| default: 'full' | |
| platform: | |
| description: 'Platform slice: "all" or one of windows | linux-gnu | linux-musl | macos' | |
| type: string | |
| default: 'all' | |
| arch_filter: | |
| description: 'Optional arch slice within each platform (x86_64 | arm64)' | |
| type: string | |
| default: 'all' | |
| publish: | |
| description: 'Publish this run''s packages to the release' | |
| type: boolean | |
| default: true | |
| force_rebuild: | |
| description: 'Re-upload even unchanged assets' | |
| type: boolean | |
| default: false | |
| audit: | |
| description: 'Audit only: verify the release against the expected matrix, no builds/uploads' | |
| type: boolean | |
| default: false | |
| repository_dispatch: | |
| types: ['tebako release'] | |
| jobs: | |
| windows: | |
| # repository_dispatch always fans to every platform (a source tarball | |
| # change reaches every scenario of the changed versions). | |
| if: ${{ github.event_name == 'repository_dispatch' || inputs.platform == 'all' || inputs.platform == 'windows' }} | |
| uses: ./.github/workflows/_build-platform.yml | |
| # Spec 34 §3/§8.2: the called workflow's build job declares | |
| # id-token:write (the windows-signing OIDC boundary) — a reusable | |
| # workflow can never elevate past the caller's grant, so the grant | |
| # flows from here. contents/packages restate the called workflow's | |
| # existing needs (a permissions block is the complete set, not an | |
| # addition). | |
| permissions: | |
| contents: write | |
| packages: write | |
| id-token: write | |
| with: | |
| platform: windows | |
| ruby_filter: ${{ inputs.ruby_filter || '' }} | |
| arch_filter: ${{ inputs.arch_filter || '' }} | |
| audit: ${{ inputs.audit || false }} | |
| publish: ${{ github.event_name == 'repository_dispatch' || inputs.publish }} | |
| force_rebuild: ${{ inputs.force_rebuild || false }} | |
| secrets: inherit | |
| linux-gnu: | |
| if: ${{ github.event_name == 'repository_dispatch' || inputs.platform == 'all' || inputs.platform == 'linux-gnu' }} | |
| uses: ./.github/workflows/_build-platform.yml | |
| # Spec 34 §3/§8.2: the called workflow's build job declares | |
| # id-token:write (the windows-signing OIDC boundary) — a reusable | |
| # workflow can never elevate past the caller's grant, so the grant | |
| # flows from here. contents/packages restate the called workflow's | |
| # existing needs (a permissions block is the complete set, not an | |
| # addition). | |
| permissions: | |
| contents: write | |
| packages: write | |
| id-token: write | |
| with: | |
| platform: linux-gnu | |
| ruby_filter: ${{ inputs.ruby_filter || '' }} | |
| arch_filter: ${{ inputs.arch_filter || '' }} | |
| audit: ${{ inputs.audit || false }} | |
| publish: ${{ github.event_name == 'repository_dispatch' || inputs.publish }} | |
| force_rebuild: ${{ inputs.force_rebuild || false }} | |
| secrets: inherit | |
| linux-musl: | |
| if: ${{ github.event_name == 'repository_dispatch' || inputs.platform == 'all' || inputs.platform == 'linux-musl' }} | |
| uses: ./.github/workflows/_build-platform.yml | |
| # Spec 34 §3/§8.2: the called workflow's build job declares | |
| # id-token:write (the windows-signing OIDC boundary) — a reusable | |
| # workflow can never elevate past the caller's grant, so the grant | |
| # flows from here. contents/packages restate the called workflow's | |
| # existing needs (a permissions block is the complete set, not an | |
| # addition). | |
| permissions: | |
| contents: write | |
| packages: write | |
| id-token: write | |
| with: | |
| platform: linux-musl | |
| ruby_filter: ${{ inputs.ruby_filter || '' }} | |
| arch_filter: ${{ inputs.arch_filter || '' }} | |
| audit: ${{ inputs.audit || false }} | |
| publish: ${{ github.event_name == 'repository_dispatch' || inputs.publish }} | |
| force_rebuild: ${{ inputs.force_rebuild || false }} | |
| secrets: inherit | |
| macos: | |
| if: ${{ github.event_name == 'repository_dispatch' || inputs.platform == 'all' || inputs.platform == 'macos' }} | |
| uses: ./.github/workflows/_build-platform.yml | |
| # Spec 34 §3/§8.2: the called workflow's build job declares | |
| # id-token:write (the windows-signing OIDC boundary) — a reusable | |
| # workflow can never elevate past the caller's grant, so the grant | |
| # flows from here. contents/packages restate the called workflow's | |
| # existing needs (a permissions block is the complete set, not an | |
| # addition). | |
| permissions: | |
| contents: write | |
| packages: write | |
| id-token: write | |
| with: | |
| platform: macos | |
| ruby_filter: ${{ inputs.ruby_filter || '' }} | |
| arch_filter: ${{ inputs.arch_filter || '' }} | |
| audit: ${{ inputs.audit || false }} | |
| publish: ${{ github.event_name == 'repository_dispatch' || inputs.publish }} | |
| force_rebuild: ${{ inputs.force_rebuild || false }} | |
| secrets: inherit | |
| # The ONE release job per coordinator run. Spec 13 §2a's de-rendezvous | |
| # moved the publish into the legs (write-once per-leg names — no shared | |
| # release-asset mutation remains); what serializes here is only the two | |
| # duties that need the whole matrix: the AUDIT (every platform's | |
| # expected names verified against the release, read-only) and the | |
| # registry render + bot PR (git arbitrates the PR — the concurrency | |
| # group is politeness now, not correctness). | |
| release: | |
| name: Audit the release + publish the registry | |
| needs: [windows, linux-gnu, linux-musl, macos] | |
| runs-on: ubuntu-latest | |
| # The legs publish; this job only reads the release and pushes one | |
| # registry branch. The 2026-08-09 hostage lesson still bounds it — | |
| # but there is no 2.5 GB download here anymore, so the bound drops | |
| # from the platform-publish 150 to 30. | |
| timeout-minutes: 30 | |
| concurrency: | |
| group: publish-runtime-packages | |
| cancel-in-progress: false | |
| # Run when the run asked to publish (dispatch input or a source | |
| # repository_dispatch) and at least one platform actually built. | |
| if: ${{ always() && !cancelled() && (github.event_name == 'repository_dispatch' || inputs.publish || inputs.audit) && (needs.windows.result == 'success' || needs.linux-gnu.result == 'success' || needs.linux-musl.result == 'success' || needs.macos.result == 'success') }} | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.TEBAKO_CI_PAT_TOKEN || secrets.GITHUB_TOKEN }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| # The registry bot PR pushes the publish branch — that needs the | |
| # PAT, not the runner's default (read-only) token. | |
| token: ${{ secrets.TEBAKO_CI_PAT_TOKEN || secrets.GITHUB_TOKEN }} | |
| fetch-depth: 1 | |
| submodules: true | |
| - name: Setup Ruby gems | |
| uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0 | |
| with: | |
| ruby-version: '3.3' | |
| bundler-cache: true | |
| # The audit, per platform that built: the legs already published | |
| # their packages in-leg, so this job never downloads a byte — it | |
| # verifies each platform's expected names (payloads, .sha256 | |
| # sidecars, shards, and — on signing-enabled lines — every .asc, | |
| # spec 09 §5) against the release's own listing, read-only. | |
| - name: Audit the release, per platform | |
| env: | |
| AUDIT_ONLY: "true" | |
| TEBAKO_RELEASE_SIGNING_ENABLED: ${{ vars.TEBAKO_RELEASE_SIGNING_ENABLED }} | |
| WIN_RUN: ${{ needs.windows.outputs.run }} | |
| WIN_VER: ${{ needs.windows.outputs.tebako-version }} | |
| WIN_ENV: ${{ needs.windows.outputs.env-matrix }} | |
| WIN_RUBY: ${{ needs.windows.outputs.ruby-matrix }} | |
| GNU_RUN: ${{ needs.linux-gnu.outputs.run }} | |
| GNU_VER: ${{ needs.linux-gnu.outputs.tebako-version }} | |
| GNU_ENV: ${{ needs.linux-gnu.outputs.env-matrix }} | |
| GNU_RUBY: ${{ needs.linux-gnu.outputs.ruby-matrix }} | |
| MUSL_RUN: ${{ needs.linux-musl.outputs.run }} | |
| MUSL_VER: ${{ needs.linux-musl.outputs.tebako-version }} | |
| MUSL_ENV: ${{ needs.linux-musl.outputs.env-matrix }} | |
| MUSL_RUBY: ${{ needs.linux-musl.outputs.ruby-matrix }} | |
| MAC_RUN: ${{ needs.macos.outputs.run }} | |
| MAC_VER: ${{ needs.macos.outputs.tebako-version }} | |
| MAC_ENV: ${{ needs.macos.outputs.env-matrix }} | |
| MAC_RUBY: ${{ needs.macos.outputs.ruby-matrix }} | |
| run: | | |
| set -uo pipefail | |
| # Audit every platform that built green; THEN fail loudly naming | |
| # any selected platform that did not — a green platform's audit | |
| # always runs, and a coverage gap never goes unreported. | |
| audit_failed="" | |
| audit_platform() { | |
| local run="$1" ver="$2" envm="$3" rubym="$4" name="$5" | |
| if [ "$run" != "true" ]; then | |
| echo "--- $name did not build this run; skipping its audit" | |
| return 0 | |
| fi | |
| echo "=== auditing $name (tebako $ver)" | |
| TEBAKO_VERSION="$ver" EXPECTED_ENV_MATRIX="$envm" EXPECTED_RUBY_MATRIX="$rubym" \ | |
| ./scripts/upload_release.rb || audit_failed="$audit_failed $name" | |
| } | |
| audit_platform "$WIN_RUN" "$WIN_VER" "$WIN_ENV" "$WIN_RUBY" windows | |
| audit_platform "$GNU_RUN" "$GNU_VER" "$GNU_ENV" "$GNU_RUBY" linux-gnu | |
| audit_platform "$MUSL_RUN" "$MUSL_VER" "$MUSL_ENV" "$MUSL_RUBY" linux-musl | |
| audit_platform "$MAC_RUN" "$MAC_VER" "$MAC_ENV" "$MAC_RUBY" macos | |
| failed="" | |
| mark() { # $1=selected? $2=result $3=name | |
| if [ "$1" = "true" ] && [ "$2" != "success" ] && [ "$2" != "skipped" ]; then | |
| failed="$failed $3" | |
| fi | |
| } | |
| mark "${{ github.event_name == 'repository_dispatch' || inputs.platform == 'all' || inputs.platform == 'windows' }}" "${{ needs.windows.result }}" windows | |
| mark "${{ github.event_name == 'repository_dispatch' || inputs.platform == 'all' || inputs.platform == 'linux-gnu' }}" "${{ needs.linux-gnu.result }}" linux-gnu | |
| mark "${{ github.event_name == 'repository_dispatch' || inputs.platform == 'all' || inputs.platform == 'linux-musl' }}" "${{ needs.linux-musl.result }}" linux-musl | |
| mark "${{ github.event_name == 'repository_dispatch' || inputs.platform == 'all' || inputs.platform == 'macos' }}" "${{ needs.macos.result }}" macos | |
| if [ -n "$failed" ]; then | |
| echo "::error::these platforms were selected but did not build green — their packages did not land:$failed" | |
| fi | |
| if [ -n "$audit_failed" ]; then | |
| echo "::error::these platforms' audits FAILED — the release is incomplete:$audit_failed" | |
| fi | |
| [ -z "$failed" ] && [ -z "$audit_failed" ] | |
| # The registry mirror (spec 04 §2): rendered from the release's own | |
| # shards — the write-once per-leg metadata units, ground truth — | |
| # never from a job's local knowledge. One tebako version per | |
| # coordinator run, so the first platform that built names it. | |
| - name: Render the registry entries | |
| if: ${{ !inputs.audit }} | |
| env: | |
| TEBAKO_VERSION: ${{ needs.windows.outputs.tebako-version || needs.linux-gnu.outputs.tebako-version || needs.linux-musl.outputs.tebako-version || needs.macos.outputs.tebako-version }} | |
| run: ./tools/registry_update.rb | |
| # main is branch-protected: the registry lands by bot PR + | |
| # auto-merge, never by direct push. Idempotent across re-runs of the | |
| # same version (an unchanged registry exits before the branch). | |
| - name: Publish the registry via pull request | |
| if: ${{ !inputs.audit }} | |
| env: | |
| GH_TOKEN: ${{ secrets.TEBAKO_CI_PAT_TOKEN || secrets.GITHUB_TOKEN }} | |
| TEBAKO_VERSION: ${{ needs.windows.outputs.tebako-version || needs.linux-gnu.outputs.tebako-version || needs.linux-musl.outputs.tebako-version || needs.macos.outputs.tebako-version }} | |
| run: | | |
| set -euo pipefail | |
| cp tpkg-registry.yaml "$RUNNER_TEMP/registry.yaml" | |
| # main can move the registry mid-publish (a mirror PR landing | |
| # while the matrix builds): the in-tree render would abort the | |
| # branch checkout below. The temp copy is the payload — discard | |
| # the dirty tree before branching, restore after. | |
| git checkout -- tpkg-registry.yaml | |
| git config user.name "tebako-ci" | |
| git config user.email "tebako@ribose.com" | |
| git fetch origin main | |
| branch="publish/v${TEBAKO_VERSION}-registry" | |
| git checkout -b "$branch" origin/main | |
| cp "$RUNNER_TEMP/registry.yaml" tpkg-registry.yaml | |
| if git diff --quiet origin/main -- tpkg-registry.yaml; then | |
| echo "registry unchanged (or the entries already landed on main)" | |
| exit 0 | |
| fi | |
| git add tpkg-registry.yaml | |
| git commit -m "publish v${TEBAKO_VERSION}: registry entries" | |
| # force-with-lease on the bot branch: a re-publish re-creates it. | |
| git push --force-with-lease origin "$branch" | |
| if ! gh pr view "$branch" >/dev/null 2>&1; then | |
| body="$RUNNER_TEMP/registry-pr-body.md" | |
| cat > "$body" <<EOF | |
| Automated registry entries for v${TEBAKO_VERSION} — rendered from the | |
| release's per-package .manifest.json shards by tools/registry_update.rb | |
| (spec 13 §2a's audit+registry job; spec 04 §2's mirror). main is | |
| protected, so the registry lands by PR. | |
| EOF | |
| gh pr create --base main --head "$branch" \ | |
| --title "publish v${TEBAKO_VERSION}: registry entries" \ | |
| --body-file "$body" | |
| fi | |
| gh pr merge --auto --squash "$branch" || gh pr merge --squash "$branch" | |
| - name: Ping the versions catalog | |
| if: ${{ always() && success() }} | |
| continue-on-error: true | |
| # Refreshes www.tebako.org/versions within minutes of a publish | |
| # (versions plan 05; the daily cron stays the backstop). The org | |
| # TEBAKO_CI_PAT_TOKEN must carry actions:write on | |
| # tamatebako/versions for the dispatch — until proven, a failed | |
| # ping never blocks a release. | |
| run: gh api repos/tamatebako/versions/dispatches -f event_type=runtime-published | |
| env: | |
| GH_TOKEN: ${{ secrets.TEBAKO_CI_PAT_TOKEN || secrets.GITHUB_TOKEN }} |