-
Notifications
You must be signed in to change notification settings - Fork 13
190 lines (173 loc) · 8.19 KB
/
Copy pathrelease-core.yml
File metadata and controls
190 lines (173 loc) · 8.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# release-core -- the release lane's brain, ALWAYS executed from main
# (the trampoline calls it @main; the recovery dispatch runs main's YAML by
# construction). THIN ORCHESTRATOR (design D3): every refusal / lint /
# predicate decision lives in tools/release/*.ps1 executed from the MAIN
# checkout (checkout#2) -- the self-check below guards the JUDGE, not just the
# evidence. The run logs workflow_ref/workflow_sha as evidence (not a gate --
# a gate cannot guard itself; residual D7(a)).
name: release-core
on:
workflow_call:
inputs:
tag-name:
required: true
type: string
tag-sha:
description: caller's sha; EVIDENCE only -- the judge resolves the tag itself
required: false
type: string
default: ''
workflow_dispatch:
inputs:
tag:
description: existing v* tag to (re)run (no-run recovery / refusal drills)
required: true
type: string
concurrency:
group: release # full serialization; a queued twin lands on ALREADY_PUBLISHED
cancel-in-progress: false
permissions:
contents: write # Release API only (D7); the v* tag ruleset blocks robot tags independently
jobs:
judge:
name: Judge (refuse-to-publish predicate)
runs-on: windows-latest
outputs:
verdict: ${{ steps.judge.outputs.verdict }}
tag-name: ${{ steps.resolve.outputs.tag-name }}
tag-sha: ${{ steps.resolve.outputs.tag-sha }}
steps:
- name: Checkout main (checkout#2 -- the judge's own tree)
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: main
fetch-depth: 0
- name: Self-check -- judge tree is origin/main HEAD
shell: pwsh
run: |
git fetch origin main
git fetch origin '+refs/tags/*:refs/tags/*'
$head = (git rev-parse HEAD).Trim()
$main = (git rev-parse origin/main).Trim()
Write-Host "judge HEAD = $head"
Write-Host "origin/main = $main"
Write-Host "workflow_ref = ${{ github.workflow_ref }} (evidence)"
Write-Host "workflow_sha = ${{ github.workflow_sha }} (evidence)"
if ($head -ne $main) { throw "judge checkout is not origin/main HEAD (main moved mid-run?) -- re-run" }
- name: Resolve tag
id: resolve
shell: pwsh
run: |
$tag = if ('${{ github.event_name }}' -eq 'workflow_dispatch') { '${{ inputs.tag }}' } else { '${{ inputs.tag-name }}' }
if (-not $tag) { throw 'no tag input' }
$sha = (git rev-list -n1 $tag).Trim()
if (-not $sha) { throw "tag '$tag' not found on origin" }
Write-Host "tag $tag -> $sha (caller evidence: '${{ inputs.tag-sha }}')"
"tag-name=$tag" | Out-File -Append $env:GITHUB_OUTPUT
"tag-sha=$sha" | Out-File -Append $env:GITHUB_OUTPUT
- name: Tag-grammar fixture selftest
shell: pwsh
run: pwsh -File tools/release/tag_regex_selftest.ps1
- name: Ledger lint (ENFORCING on the release lane)
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
run: pwsh -File tools/release/ledger_lint.ps1
- name: Judge -- full verdict vector (no short-circuit)
id: judge
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
run: |
pwsh -File tools/release/judge.ps1 -TagName '${{ steps.resolve.outputs.tag-name }}' -TagShaEvidence '${{ steps.resolve.outputs.tag-sha }}'
$code = $LASTEXITCODE
# Explicit exits: without them the step ends with the child's
# $LASTEXITCODE (10) and the no-op verdict turned the job RED --
# caught live by the drill matrix's double-dispatch drill.
switch ($code) {
0 { 'verdict=publish' | Out-File -Append $env:GITHUB_OUTPUT; exit 0 }
10 { 'verdict=already_published' | Out-File -Append $env:GITHUB_OUTPUT; exit 0 }
default { exit 1 }
}
- name: Fingerprint gate (early refusal, pre-build)
if: steps.judge.outputs.verdict == 'publish'
shell: pwsh
run: pwsh -File tools/release/fingerprint.ps1 -Mode check
build:
name: Cacheless build of the tagged source
needs: judge
if: needs.judge.outputs.verdict == 'publish'
# Remote@main form on purpose (R22) -- same rule as the callers of this file.
uses: VOTV-MP/Multivoid/.github/workflows/build-core.yml@main
with:
ref: ${{ needs.judge.outputs.tag-sha }}
source-sha: ${{ needs.judge.outputs.tag-sha }}
use-cache: false
publish:
name: Draft-first publish + read-back
needs: [judge, build]
if: needs.judge.outputs.verdict == 'publish'
runs-on: windows-latest
steps:
# THE CONTENT AND THE IDENTITY COME FROM THE TAG; THE PREDICATES COME FROM
# MAIN. This job used to check out `main` for both, and that is a
# CONTRADICTION with the ritual it serves: RELEASE.md steps 1-3 tag, then
# bump kProtocolVersion N -> N+1 on main, then push both together -- so by
# the time this job runs, main ALWAYS reads N+1 while the tag declares N.
# publish.ps1's leg 3 compares the two and throws. It could not have been
# noticed earlier: leg 3 landed 2026-08-28 (`d693609b`), after the last
# release (b133-dev, 2026-07-31), so the b150 run was the first ever to
# reach it -- and it refused with `'0.9.0n b151' != '0.9.0n b150'`.
#
# Taking the content from the tag is also what makes the zip honest: the
# icon, the store README, the legal files and the paks now come from the
# commit being released rather than from whatever main has become.
- name: Checkout the TAGGED tree (release content + identity)
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ needs.judge.outputs.tag-sha }}
# Only the *.ps1 predicates are overlaid, and only from main. A tag can be
# created by anyone who can push one; the refuse-to-publish logic must not
# be readable from it (RELEASE.md: "executed from main HEAD -- editing them
# is a human-only act"). Everything else under tools/release is CONTENT that
# describes this release -- the notes file, the store README, the ledger,
# the fingerprint -- and stays at the tag.
- name: Overlay the release predicates from main
shell: pwsh
run: |
git fetch --depth=1 origin main
git checkout FETCH_HEAD -- 'tools/release/*.ps1'
Write-Host 'predicates overlaid from main:'
git --no-pager diff --name-only HEAD -- tools/release | ForEach-Object { " $_" }
Write-Host 'content kept at the tag:'
Get-ChildItem tools/release -Exclude '*.ps1' -Name | ForEach-Object { " tools/release/$_" }
- name: Download build artifact
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: ${{ needs.build.outputs.artifact-name }}
path: _release_artifact
- name: Publish (draft -> assets -> sha256 verify -> flip -> read-back)
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
run: |
pwsh -File tools/release/publish.ps1 `
-TagName '${{ needs.judge.outputs.tag-name }}' `
-TagSha '${{ needs.judge.outputs.tag-sha }}' `
-ArtifactDir _release_artifact
Get-Content _release_artifact/fingerprint-dump.json | Out-Host
- name: Summary
shell: pwsh
run: |
"## Release ${{ needs.judge.outputs.tag-name }}" | Out-File -Append $env:GITHUB_STEP_SUMMARY
"source: ${{ needs.judge.outputs.tag-sha }}" | Out-File -Append $env:GITHUB_STEP_SUMMARY
"Ritual: verify the release page, then append the 'published' ledger row (see publish log; STABLE also: master env + verify_latest.ps1)." | Out-File -Append $env:GITHUB_STEP_SUMMARY
already-published:
name: Completion no-op
needs: judge
if: needs.judge.outputs.verdict == 'already_published'
runs-on: windows-latest
steps:
- name: ALREADY_PUBLISHED
shell: pwsh
run: Write-Host 'ALREADY_PUBLISHED -- live release on the triggering tag with matching source; nothing to do (assets untouched).'