Skip to content

release-src: patch→scenario attribution + per-line tip-first staging - #50

Merged
ronaldtse merged 1 commit into
mainfrom
feat/scenario-attribution
Aug 7, 2026
Merged

release-src: patch→scenario attribution + per-line tip-first staging#50
ronaldtse merged 1 commit into
mainfrom
feat/scenario-attribution

Conversation

@ronaldtse

Copy link
Copy Markdown
Contributor

The audit (how the patch tree dependencies actually work)

A release tarball is a function of exactly: the version's upstream source (versions.yml entry), its line's patch set (patches/<line>/), and the shared tooling (tools/, ci/, schema/). The patch set's platform reach follows the PatchSelection suffix convention — the single model of what lands where:

Changed input Feeds Rebuilds
*_msys(_N).patch msys scenario tarballs those rows only
*_musl.patch linux-musl tarball that row only
*_darwin.patch no shipped scenario today (dead in the release flow — verified empirically: the base tarball's configure is unpatched) nothing
base patch every scenario the line's full row set
patch-<line>*.yaml manifest re-scopes any feature the line, all scenarios (fail-closed)
tools/ ci/ schema/ … everything the full catalog (fail-closed)
_pass1/_pass2 marker that msys pass's tarball that pass's row only

Scenario → factory platform (for the follow-up factory fix): linux-gnu tarball → linux-gnu + macos legs; linux-musl → musl legs; msys → windows legs.

Audit finding (flagged, not fixed here): _darwin patches are dead in the release flow — no scenario selects them, and the factory has no darwin patch application either. If macOS runtimes ever need those deltas, the flow must be extended deliberately.

What changes

  • ReleaseDiff#changed_scenarios — the attribution map {line => [[scenario, pass], …]}.
  • BuildPlan — row-level build/copy decisions: an msys-only patch change re-rolls exactly the msys tarballs; POSIX assets carry forward as verified copies.
  • SmokePlan (extracted from tools/smoke_matrix, now rspec'd) — smoke legs only for affected scenarios at the line's newest version.
  • Per-line tip-first staging: release-src.yml becomes plan → per-line release units (_release-line.yml) → publish. Each unit smokes the line tip FIRST and gates only its own builds; copies run independently; a failed line blocks the publish (never back-fills stale copies) but never another line.

Proof (local, per the rule)

  • 110 rspec examples, 0 failures — attribution, row-level build/copy, pass scoping, darwin emptiness, line filters, smoke legs.
  • Live tool verification against the real tags: tools/build_matrix v0.2.16 --lines → the 5 lines; --build --line 3.4 → 40 rows; tools/smoke_matrix v0.2.16 --line 3.3 → the msys leg only.
  • actionlint clean (one pre-existing info-level shellcheck note carried over).
  • Fail-closed holds: v0.2.16's real diff (a shared workflow change) still plans the full catalog.

Comment on lines +26 to +68
name: plan ${{ inputs.line }}
runs-on: ubuntu-latest
outputs:
smoke: ${{ steps.emit.outputs.smoke }}
build: ${{ steps.emit.outputs.build }}
copies: ${{ steps.emit.outputs.copies }}
previous_tag: ${{ steps.emit.outputs.previous_tag }}
smoke_count: ${{ steps.emit.outputs.smoke_count }}
build_count: ${{ steps.emit.outputs.build_count }}
copies_count: ${{ steps.emit.outputs.copies_count }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: fetch tags (previous-release diff base)
run: git fetch --tags --force
- uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
with:
ruby-version: "3.2"
- id: emit
run: |
smoke=$(tools/smoke_matrix "${{ inputs.tag }}" --line "${{ inputs.line }}")
build=$(tools/build_matrix "${{ inputs.tag }}" --build --line "${{ inputs.line }}")
copies=$(tools/build_matrix "${{ inputs.tag }}" --copies --line "${{ inputs.line }}")
{
echo "smoke=$smoke"
echo "build=$build"
echo "copies=$copies"
echo "previous_tag=$(tools/build_matrix "${{ inputs.tag }}" --previous-tag)"
# Leg counts drive the job-level if: guards (an unguarded
# empty matrix materializes no legs and poisons the run
# conclusion — the v0.2.15 lesson, release-src.yml history).
echo "smoke_count=$(jq '.include | length' <<< "$smoke")"
echo "build_count=$(jq '.include | length' <<< "$build")"
echo "copies_count=$(jq '.include | length' <<< "$copies")"
} >> "$GITHUB_OUTPUT"

# The line tip FIRST: configure the tree of the line's newest version
# per affected scenario and compile each patched translation unit
# (objects only — no runtime build, no link, no exts) against the
# vendored stub tebako headers in ci/include. Only the scenarios the
# changed patches FEED get a leg (tools/smoke_matrix).
smoke:
Comment on lines +69 to +94
name: smoke ${{ inputs.line }} (${{ matrix.version }} / ${{ matrix.platform }})
needs: plan
if: needs.plan.outputs.smoke_count != '0'
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.plan.outputs.smoke) }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
with:
ruby-version: "3.2"
- name: scenario toolchain
run: |
sudo apt-get update -qq
case "${{ matrix.platform }}" in
linux-musl) sudo apt-get install -y -qq musl-tools ;;
msys) sudo apt-get install -y -qq gcc-mingw-w64-x86-64 ;;
esac
- name: configure + compile patched translation units
run: tools/compile_smoke "${{ matrix.version }}" "$PWD/build/compile-smoke" --platform "${{ matrix.platform }}"

# The line's build legs run ONLY when the tip smoke passed (or was
# vacuous — nothing about the line's patches changed). A failed smoke
# blocks this line's builds and nothing else.
build:
Comment on lines +95 to +133
name: build ${{ inputs.line }} (${{ matrix.version }}${{ matrix.suffix }})
needs: [plan, smoke]
if: ${{ needs.plan.outputs.build_count != '0' && (needs.smoke.result == 'success' || needs.smoke.result == 'skipped') }}
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.plan.outputs.build) }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
with:
ruby-version: "3.2"
bundler-cache: true
- name: validate manifests against schema
run: bundle exec tools/validate_manifests
- name: apply patches
run: tools/apply "${{ matrix.version }}" "$PWD/build" --platform "${{ matrix.platform }}" --pass "${{ matrix.pass }}"
- name: package
working-directory: build
run: |
tar -czf "${{ matrix.asset }}" "${{ matrix.tree }}"
sha256sum "${{ matrix.asset }}" > "${{ matrix.asset }}.sha256"
- name: extract-verify
working-directory: build
run: |
mkdir verify
tar -xzf "${{ matrix.asset }}" -C verify
diff -qr "${{ matrix.tree }}" "verify/${{ matrix.tree }}"
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: tfs-ruby-${{ matrix.version }}-src${{ matrix.suffix }}
path: build/${{ matrix.asset }}*
retention-days: 1

# The line's carry-forward copies: sha256-verified against the previous
# release's published SHA256SUMS (tools/copy_asset). Independent of the
# smoke — a copied asset is byte-identical with what the previous
# release published.
copy:
Comment on lines +134 to +152
name: copy ${{ inputs.line }} (${{ matrix.asset }})
needs: plan
if: needs.plan.outputs.copies_count != '0'
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.plan.outputs.copies) }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
with:
ruby-version: "3.2"
- name: download + verify against the previous release's SHA256SUMS
run: tools/copy_asset "${{ needs.plan.outputs.previous_tag }}" "${{ matrix.asset }}" "$PWD/dist"
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: tfs-ruby-${{ matrix.version }}-src${{ matrix.suffix }}
path: dist/${{ matrix.asset }}*
retention-days: 1
Comment on lines +15 to +47
name: plan the per-line release units (diff-aware)
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.emit.outputs.matrix }}
copies: ${{ steps.emit.outputs.copies }}
previous_tag: ${{ steps.emit.outputs.previous_tag }}
lines: ${{ steps.emit.outputs.lines }}
lines_count: ${{ steps.emit.outputs.lines_count }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: fetch tags (previous-release diff base)
run: git fetch --tags --force
- uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
with:
ruby-version: "3.2"
# Fault isolation (tools/build_matrix over Tfs::ReleaseDiff, the same
# previous-tag diff base the compile-smoke gate uses): only versions
# of CHANGED patch lines compile (plus every version on a shared
# tooling change, and versions whose versions.yml entry is new or
# moved); every unchanged version is carried forward from the
# previous release as a sha256-verified copy (the copy job below).
# One release unit per line PRESENT in versions.yml: changed lines
# build (their smoke gates their builds), unchanged lines carry
# their assets forward as sha256-verified copies. The line's own
# unit recomputes its rows from the shared tools (tools/build_matrix
# --line / tools/smoke_matrix --line over Tfs::ReleaseDiff's
# patch→scenario attribution) — no matrix logic lives in YAML.
- id: emit
env:
RELEASE_TAG: ${{ github.event_name == 'push' && github.ref_name || inputs.release_tag }}
run: |
lines=$(tools/build_matrix "$RELEASE_TAG" --lines)
{
echo "matrix=$(tools/build_matrix "$RELEASE_TAG" --build)"
echo "copies=$(tools/build_matrix "$RELEASE_TAG" --copies)"
echo "previous_tag=$(tools/build_matrix "$RELEASE_TAG" --previous-tag)"
echo "lines=$lines"
# The count drives the job-level if: on release (an
# unguarded empty matrix poisons the run conclusion).
echo "lines_count=$(jq 'length' <<< "$lines")"
} >> "$GITHUB_OUTPUT"

changes:
name: map changed patch lines to compile-smoke legs
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.emit.outputs.matrix }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: fetch tags (previous-release diff base)
run: git fetch --tags --force
- uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
with:
ruby-version: "3.2"
- id: emit
run: echo "matrix=$(tools/smoke_matrix "${{ github.event_name == 'push' && github.ref_name || inputs.release_tag }}")" >> "$GITHUB_OUTPUT"

# Publish gate (roadmap 17.0; the v0.2.8 lesson: a patch release shipped
# apply-clean but uncompilable and broke every linux runtime leg). For
# every line whose patch set changed in the tag (tools/smoke_matrix diffs
# the tag against the previous release tag), configure the tree and
# compile each patched translation unit of the line's NEWEST version per
# shipped scenario -- a representative per-line leg, not per-version:
# patches are line-wide, so per-version legs would multiply configure
# runs for no extra signal. The compile is the cheapest that catches a
# broken shim: ./configure + make <obj> per patched .c (objects only --
# no runtime build, no link, no exts), against the vendored stub tebako
# headers in ci/include. msys legs compile at pass 2 (the pass split
# selects GNUmakefile variants; no patched .c differs between passes).
# With no changed patch set the matrix is empty and the gate is vacuous;
# publish's if: below treats that (skipped) as pass, failure as block.
compile-smoke:
name: compile-smoke ${{ matrix.version }} (${{ matrix.platform }})
needs: changes
# The per-line release units: each line's tip smoke runs FIRST and
# gates only its own line's builds; copies run independently. A line
# that fails blocks the publish (its changed assets must never be
# back-filled with stale copies) but never another line's work.
release:
THE ATTRIBUTION MODEL (the audit): a release asset is a function of
exactly these inputs — the version's upstream tarball (versions.yml),
its line's patch set (patches/<line>/), the shared tooling. The patch
set's platform reach follows the PatchSelection suffix convention, the
single model of what lands where:

  *_msys(_N).patch          -> the msys scenario tarballs only
  *_musl.patch              -> linux-musl only
  *_darwin.patch            -> NO shipped scenario (dead in the release
                               flow today — a darwin-only change re-rolls
                               nothing; if a darwin scenario ever ships,
                               extend SCENARIO_BUILDS and the map together)
  base patches              -> every scenario (never narrowed by
                               msys/musl shadowing — fail closed)
  patch-<line>*.yaml        -> every scenario of the line (re-scopes any
                               feature — fail closed)
  tools/ ci/ schema/ etc.   -> everything (unchanged)
  a _pass1/_pass2 marker    -> only that msys pass's tarball

ReleaseDiff#changed_scenarios carries the map; BuildPlan decides
per (version x scenario-build) row, so an msys-only patch change
re-rolls exactly the msys tarballs and carries the POSIX assets forward
as verified copies; SmokePlan (extracted from tools/smoke_matrix,
rspec'd) smokes only the affected scenarios at the line's newest
version.

THE STAGING: release-src.yml becomes plan -> per-line release units
(_release-line.yml, one call per line) -> publish. Each unit smokes the
line tip FIRST and gates only its own line's builds on it; copies run
independently; a failed line blocks the publish (its changed assets
must never be back-filled with stale copies) but never another line's
work. The matrix logic stays in the shared tools (--line filters); the
YAML carries none.

Specs: 110 examples, 0 failures (scenario attribution, row-level
build/copy, pass scoping, darwin emptiness, line filters, smoke legs).
Live-verified: v0.2.16's real diff (a shared workflow change) still
plans the full catalog — fail-closed holds.
@ronaldtse
ronaldtse force-pushed the feat/scenario-attribution branch from 1871a48 to a87d206 Compare August 7, 2026 00:57
@ronaldtse
ronaldtse merged commit 2136335 into main Aug 7, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants