release-src: patch→scenario attribution + per-line tip-first staging - #50
Merged
Conversation
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
force-pushed
the
feat/scenario-attribution
branch
from
August 7, 2026 00:57
1871a48 to
a87d206
Compare
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:*_msys(_N).patch*_musl.patch*_darwin.patchconfigureis unpatched)patch-<line>*.yamlmanifesttools/ ci/ schema/ …_pass1/_pass2markerScenario → 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):
_darwinpatches 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 fromtools/smoke_matrix, now rspec'd) — smoke legs only for affected scenarios at the line's newest version.release-src.ymlbecomes 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)
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.actionlintclean (one pre-existing info-level shellcheck note carried over).