Skip to content

publish

publish #3

Workflow file for this run

# 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" ]