publish #3
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) 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 (tebako-runtime-ruby's publish.yml grammar). | |
| # The explicit, operator-facing entry point — the three first-class slice | |
| # shapes: | |
| # one python on every platform (python_filter=<version>, platform=all) | |
| # one platform, every python (python_filter=full|tidy|catalog, platform=<one>) | |
| # one python on one platform (python_filter=<version>, platform=<one>) | |
| # plus the era baseline (python_filter=catalog, platform=all). | |
| # | |
| # Every slice fans out to _build-platform.yml to BUILD. Publishing lives | |
| # ONLY here, in the single `release` job that runs once per coordinator | |
| # run after every platform build: the release is one shared resource, so | |
| # publishing must serialize — and serializing per-platform publish jobs | |
| # through a shared concurrency group displaced (cancelled) queued | |
| # publishes (the 2026-08-09 lesson). One release job per run means a run | |
| # publishes every platform it built, never fewer. Each per-platform | |
| # invocation writes ONLY names that platform owns (its payload assets + | |
| # each package's .sha256 sidecars and .manifest.json shard — issue 139's | |
| # per-asset metadata, no merge path); the shared monolithic conveniences | |
| # (manifest.json / SHA256SUMS.txt) and the release notes are derived from | |
| # the release's own shards + asset listing by the ONE finalize invocation | |
| # that runs after every platform landed. | |
| # | |
| # Deliberate deviations from the ruby coordinator: | |
| # - publish defaults to FALSE (ruby: true). This factory has no release | |
| # yet; the first publish is an explicit operator act. | |
| # - No repository_dispatch trigger yet: the python source factory does | |
| # not dispatch "tebako release" here (the ruby chain's pin-moved | |
| # rebuilds). The trigger arrives when tamatebako/python wires it. | |
| # - The release job authenticates with the runner GITHUB_TOKEN, not the | |
| # ruby grammar's `TEBAKO_CI_PAT_TOKEN || GITHUB_TOKEN` preference: | |
| # the org PAT's fine-grained repository selection predates this repo | |
| # and 404s every API call from here (the first publish died at | |
| # create_release, run 33999688520). The runner token's contents:write | |
| # suffices — no downstream cascade listens to this factory's releases | |
| # yet. The PAT preference returns WITH the cascade wiring, and the | |
| # PAT's repo selection must cover this repo by then | |
| # (tebako-runtime-python#5 — owner action). | |
| name: publish | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| python_filter: | |
| description: 'Pythons: "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: false | |
| force_rebuild: | |
| description: 'Re-upload even unchanged assets (the one exception to per-name byte-immutability; the ruby coordinator declares this input but never wires it — here it reaches upload_release.rb as FORCE_REBUILD)' | |
| type: boolean | |
| default: false | |
| audit: | |
| description: 'Audit only: verify the release against the expected matrix, no builds/uploads' | |
| type: boolean | |
| default: false | |
| permissions: | |
| contents: write | |
| jobs: | |
| windows: | |
| if: ${{ inputs.platform == 'all' || inputs.platform == 'windows' }} | |
| uses: ./.github/workflows/_build-platform.yml | |
| with: | |
| platform: windows | |
| python_filter: ${{ inputs.python_filter }} | |
| arch_filter: ${{ inputs.arch_filter }} | |
| audit: ${{ inputs.audit }} | |
| secrets: inherit | |
| linux-gnu: | |
| if: ${{ inputs.platform == 'all' || inputs.platform == 'linux-gnu' }} | |
| uses: ./.github/workflows/_build-platform.yml | |
| with: | |
| platform: linux-gnu | |
| python_filter: ${{ inputs.python_filter }} | |
| arch_filter: ${{ inputs.arch_filter }} | |
| audit: ${{ inputs.audit }} | |
| secrets: inherit | |
| linux-musl: | |
| if: ${{ inputs.platform == 'all' || inputs.platform == 'linux-musl' }} | |
| uses: ./.github/workflows/_build-platform.yml | |
| with: | |
| platform: linux-musl | |
| python_filter: ${{ inputs.python_filter }} | |
| arch_filter: ${{ inputs.arch_filter }} | |
| audit: ${{ inputs.audit }} | |
| secrets: inherit | |
| macos: | |
| if: ${{ inputs.platform == 'all' || inputs.platform == 'macos' }} | |
| uses: ./.github/workflows/_build-platform.yml | |
| with: | |
| platform: macos | |
| python_filter: ${{ inputs.python_filter }} | |
| arch_filter: ${{ inputs.arch_filter }} | |
| audit: ${{ inputs.audit }} | |
| secrets: inherit | |
| # The ONE release job per coordinator run (the serialization rationale | |
| # is the header's). A runner that dies mid-publish never reports, and | |
| # the job-level group (cancel-in-progress: false) then holds every | |
| # later publish hostage for the cap — bound it. | |
| release: | |
| name: Publish the runtime packages | |
| needs: [windows, linux-gnu, linux-musl, macos] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| concurrency: | |
| group: publish-runtime-packages | |
| cancel-in-progress: false | |
| # Run when the run asked to publish or audit and at least one | |
| # platform actually built (an audit builds nothing but still computes | |
| # every selected platform's expectations). | |
| if: ${{ always() && !cancelled() && (inputs.publish || inputs.audit) && (needs.windows.result == 'success' || needs.linux-gnu.result == 'success' || needs.linux-musl.result == 'success' || needs.macos.result == 'success') }} | |
| env: | |
| # Runner GITHUB_TOKEN, not the org PAT preference — see the header's | |
| # deviation note and tebako-runtime-python#5. | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| # Every platform's packages land in one workspace; the idempotent | |
| # skip keeps unchanged assets from re-uploading, and each platform | |
| # invocation writes only its own packages' metadata (sidecars + | |
| # shards — issue 139), so platforms never touch a shared file. | |
| - name: Download the runtime packages | |
| if: ${{ !inputs.audit }} | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| pattern: runtime-packages-* | |
| path: runtime-packages | |
| merge-multiple: true | |
| - name: Setup Ruby gems | |
| uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0 | |
| with: | |
| ruby-version: '3.3' | |
| bundler-cache: true | |
| # One upload_release.rb per platform that built, sequential inside | |
| # this job. Each platform's completeness gate reads its own matrix. | |
| # The finalize pass runs LAST: it derives the monolithic | |
| # conveniences (manifest.json / SHA256SUMS.txt) and the release | |
| # notes from the release's own shards + asset listing — the one | |
| # place the shared files are (re)written, from ground truth only, | |
| # never from a job's local knowledge (issue 139). | |
| - name: Update the release, per platform | |
| env: | |
| AUDIT_ONLY: ${{ inputs.audit }} | |
| FORCE_REBUILD: ${{ inputs.force_rebuild }} | |
| WIN_RUN: ${{ needs.windows.outputs.run }} | |
| WIN_VER: ${{ needs.windows.outputs.tebako_version }} | |
| WIN_ENV: ${{ needs.windows.outputs.env_matrix }} | |
| WIN_PYTHON: ${{ needs.windows.outputs.python_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_PYTHON: ${{ needs.linux-gnu.outputs.python_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_PYTHON: ${{ needs.linux-musl.outputs.python_matrix }} | |
| MAC_RUN: ${{ needs.macos.outputs.run }} | |
| MAC_VER: ${{ needs.macos.outputs.tebako_version }} | |
| MAC_ENV: ${{ needs.macos.outputs.env_matrix }} | |
| MAC_PYTHON: ${{ needs.macos.outputs.python_matrix }} | |
| run: | | |
| set -uo pipefail | |
| # Publish every platform that built green; THEN fail loudly naming | |
| # any selected platform that did not — a green platform's packages | |
| # always land, and a coverage gap never goes unreported. | |
| upload_failed="" | |
| publish_platform() { | |
| local run="$1" ver="$2" envm="$3" pym="$4" name="$5" | |
| if [ "$run" != "true" ]; then | |
| echo "--- $name did not build this run; skipping its merge" | |
| return 0 | |
| fi | |
| echo "=== publishing $name (tebako $ver)" | |
| TEBAKO_VERSION="$ver" EXPECTED_ENV_MATRIX="$envm" EXPECTED_PYTHON_MATRIX="$pym" \ | |
| ./scripts/upload_release.rb || upload_failed="$upload_failed $name" | |
| } | |
| publish_platform "$WIN_RUN" "$WIN_VER" "$WIN_ENV" "$WIN_PYTHON" windows | |
| publish_platform "$GNU_RUN" "$GNU_VER" "$GNU_ENV" "$GNU_PYTHON" linux-gnu | |
| publish_platform "$MUSL_RUN" "$MUSL_VER" "$MUSL_ENV" "$MUSL_PYTHON" linux-musl | |
| publish_platform "$MAC_RUN" "$MAC_VER" "$MAC_ENV" "$MAC_PYTHON" macos | |
| # The finalize pass (skipped on audit-only runs, which mutate | |
| # nothing): one tebako version per coordinator run, so the first | |
| # platform that built names it. | |
| if [ "${AUDIT_ONLY:-false}" != "true" ]; then | |
| FIN_VER="${WIN_VER:-${GNU_VER:-${MUSL_VER:-${MAC_VER:-}}}}" | |
| if [ -n "$FIN_VER" ]; then | |
| echo "=== finalizing (tebako $FIN_VER)" | |
| TEBAKO_VERSION="$FIN_VER" FINALIZE_ONLY=true ./scripts/upload_release.rb || upload_failed="$upload_failed finalize" | |
| fi | |
| fi | |
| failed="" | |
| mark() { # $1=selected? $2=result $3=name | |
| if [ "$1" = "true" ] && [ "$2" != "success" ] && [ "$2" != "skipped" ]; then | |
| failed="$failed $3" | |
| fi | |
| } | |
| mark "${{ inputs.platform == 'all' || inputs.platform == 'windows' }}" "${{ needs.windows.result }}" windows | |
| mark "${{ inputs.platform == 'all' || inputs.platform == 'linux-gnu' }}" "${{ needs.linux-gnu.result }}" linux-gnu | |
| mark "${{ inputs.platform == 'all' || inputs.platform == 'linux-musl' }}" "${{ needs.linux-musl.result }}" linux-musl | |
| mark "${{ 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 "$upload_failed" ]; then | |
| echo "::error::these platforms' upload_release.rb failed:$upload_failed" | |
| fi | |
| [ -z "$failed" ] && [ -z "$upload_failed" ] |