Repository navigation
fix: validate Gateway upgrades during release dispatch - #178
Conversation
|
🦞👀 Pull request received. I will update this pull request when review starts. ClawSweeper review completeClawSweeper finished reviewing this revision. The review result is being finalized. |
|
Codex review: needs maintainer review before merge. What this changesThis PR runs installed-upgrade validation for release-candidate dispatches and requires success before release authorization. Example: Dispatch test mode for OpenClaw 2026.10.4.
Review scores
ProductKind: Bug fix · Worth it: Yes · Fix scope: Complete Merge readiness✅ Ready for maintainer review This maintainer-authored PR fixes a release-validation gap still present on main. No actionable patch defects remain; current-head required CI is still running. Priority: P2 Before mergeNone. FindingsNone. Agent review detailsHow this fits togetherThe packaging workflow turns an immutable upstream source selection into architecture packages and bundles, validates installed upgrades, and gates release authorization, signing, and publication. flowchart LR
A[Manual dispatch] --> B[Immutable source selection]
B --> C[Architecture packages and bundles]
C --> D[Installed upgrade matrix]
D --> E[Release authorization]
E --> F[Signing and publication]
E --> G[Authorized Store retention]
Technical reviewBest possible solution: Land the existing-owner dispatch gate after current-head required CI succeeds, retaining same-run upgrade evidence with each release. Do we have a high-confidence way to reproduce the issue? Main's event conditions directly show that manual releases skip installed upgrades, and the supplied old/new DAG execution demonstrates authorization after failed upgrade proof on the original workflow. Is this the best way to solve the issue? The change addresses the orchestration owner directly, reuses immutable source selection and the existing transition matrix, and adds focused behavior tests without creating another release-policy owner. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning medium; reviewed against 19d7cc0aab65. Provenance checked
TestingProof path: shipped entry point. SecurityNone. EvidenceWhat I checked:
Likely related people:
Review metrics
LabelsLabel changes:
Label justifications:
Rating scale6/6 🦀 challenger crab · 5/6 🦞 diamond lobster · 4/6 🐚 platinum hermit · 3/6 🦐 gold shrimp · 2/6 🦪 silver shellfish · 1/6 🧂 unranked krab. Overall follows the weaker of proof and patch quality; ✨ marks media proof (a screenshot, video, or linked artifact) that directly shows the changed behavior. WorkflowClawSweeper edits this one comment on every review. Comment HistoryReview history (1 earlier review cycle)
Reviewed October 15, 2026, 11:42 PM ET / October 16, 2026, 03:42 UTC (Revision 2). |
ClawSweeper authority-boundary proofThe 13 credential-free release-boundary scenarios below passed against the production workflow at This is executed emulator proof, not a live official/store dispatch or a claim that Azure was contacted. A disposable projection reads the real production job Here, signing means that the job containing Azure login was reachable, not that signing occurred. Store means the authorized-retention job was reachable, not an upload. The success-path marker substitutes for release-policy validation; that validator's real fixture suite is separately passing.
Observed output: The real manual test dispatch completed successfully: both architecture packages, both bundles, temporary test signing, all 13 actual installed transitions, and the aggregate gate passed. Every emitted transition JSON was downloaded and verified against candidate Regression confirmed: running the same isolated proof against the original Reproducer used for the credential-free proofThe temporary proof harness below is evidence only; no production policy or test-only release bypass was added. Build the emulator into a disposable directory with [CmdletBinding()]
param(
[string]$RepositoryRoot = (Get-Location).Path,
[string]$WorkflowPath = '',
[string]$ActPath = '/tmp/msix-authorization-proof-tools/act',
[string]$OutputDirectory = '/tmp/msix-release-authorization-proof'
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
if ($WorkflowPath -eq '') {
$WorkflowPath = Join-Path $RepositoryRoot '.github/workflows/gateway-msix.yml'
}
$workflowPath = $WorkflowPath
$workflow = [IO.File]::ReadAllText($workflowPath)
$workflowHash = (Get-FileHash -LiteralPath $workflowPath -Algorithm SHA256).Hash.ToLowerInvariant()
$commit = & git -C $RepositoryRoot rev-parse HEAD
if ($LASTEXITCODE -ne 0) { throw 'Cannot resolve proof commit.' }
# This is execution evidence, not a policy implementation. Copy the production
# DAG and its conditions unchanged; replace external work at the isolation
# boundary. No production step that can request credentials is copied.
$jobs = [ordered]@{}
$jobText = [regex]::Match($workflow, '(?ms)^jobs:\r?\n(?<jobs>.*)\z').Groups['jobs'].Value
foreach ($match in [regex]::Matches(
$jobText,
'(?ms)^ (?<id>[a-z][a-z0-9-]+):\r?\n(?<body>.*?)(?=^ [a-z][a-z0-9-]+:|\z)'
)) {
$jobs[$match.Groups['id'].Value] = $match.Groups['body'].Value
}
if ($jobs.Count -ne 14) { throw "Unexpected workflow job count: $($jobs.Count)." }
$trigger = [regex]::Match($workflow, '(?ms)^on:\r?\n.*?(?=^concurrency:)').Value
if ($trigger -eq '') { throw 'Cannot locate workflow triggers.' }
$encoding = [Text.UTF8Encoding]::new($false)
$null = [IO.Directory]::CreateDirectory($OutputDirectory)
$scenarios = @(
@{ Name='main-official-success'; Mode='official'; Ref='main'; Upgrade='success'; Authorized=$true; Signed=$true; Published=$true; Store=$true; Gate=$true },
@{ Name='main-store-success'; Mode='store'; Ref='main'; Upgrade='success'; Authorized=$true; Store=$true; Gate=$true },
@{ Name='main-official-failed-upgrade'; Mode='official'; Ref='main'; Upgrade='failure' },
@{ Name='main-official-skipped-upgrade'; Mode='official'; Ref='main'; Upgrade='skipped' },
@{ Name='main-store-failed-upgrade'; Mode='store'; Ref='main'; Upgrade='failure' },
@{ Name='main-store-skipped-upgrade'; Mode='store'; Ref='main'; Upgrade='skipped' },
@{ Name='branch-official-success'; Mode='official'; Ref='proof-branch'; Upgrade='success' },
@{ Name='branch-store-success'; Mode='store'; Ref='proof-branch'; Upgrade='success' },
@{ Name='main-test-success'; Mode='test'; Ref='main'; Upgrade='success'; Gate=$true },
@{ Name='branch-test-success'; Mode='test'; Ref='proof-branch'; Upgrade='success'; Gate=$true },
@{ Name='main-unsigned'; Mode='unsigned'; Ref='main'; Upgrade='success'; Versioning='false'; Gate=$true },
@{ Name='pull-request'; Mode='official'; Ref='main'; Event='pull_request'; Upgrade='success'; Gate=$true },
@{ Name='push-main'; Mode='official'; Ref='main'; Event='push'; Upgrade='success'; Versioning='false'; Gate=$true }
)
$results = [Collections.Generic.List[object]]::new()
foreach ($scenario in $scenarios) {
$caseRoot = Join-Path $OutputDirectory $scenario.Name
if (Test-Path -LiteralPath $caseRoot) { throw "Proof output exists: $caseRoot" }
$markers = Join-Path $caseRoot 'markers'
$null = [IO.Directory]::CreateDirectory($markers)
& git -C $caseRoot init --quiet
if ($LASTEXITCODE -ne 0) { throw 'Cannot initialize isolated workflow fixture.' }
& git -C $caseRoot remote add origin https://github.com/fixture/proof.git
if ($LASTEXITCODE -ne 0) { throw 'Cannot record synthetic fixture origin.' }
$eventName = if ($scenario.ContainsKey('Event')) { $scenario.Event } else { 'workflow_dispatch' }
$versioning = if ($scenario.ContainsKey('Versioning')) { $scenario.Versioning } else { 'true' }
$lines = [Collections.Generic.List[string]]::new()
$lines.Add('name: Credential-free production authorization DAG projection')
$lines.Add($trigger.TrimEnd())
$lines.Add('permissions: {}')
$lines.Add('jobs:')
foreach ($id in $jobs.Keys) {
$body = $jobs[$id]
if ($id -eq 'ci-gate') {
# The real aggregate scripts are safe and provide the denied-case
# observable result, including required-but-skipped upgrade proof.
$lines.Add(" ${id}:")
$lines.Add($body.TrimEnd())
$marker = (Join-Path $markers $id).Replace("'", "''")
$lines.Add(' - name: Record successful gate')
$lines.Add(' shell: pwsh')
$lines.Add(" run: '[IO.File]::WriteAllText(''$marker'', ''reached'')'")
continue
}
$lines.Add(" ${id}:")
$condition = [regex]::Match($body, '(?m)^ if: (?<expression>.*)$')
if ($condition.Success) {
$expression = $condition.Groups['expression'].Value.TrimEnd()
if ($id -eq 'test-msix-upgrades' -and $scenario.Upgrade -eq 'skipped') {
# Inject a skipped upstream fixture, not a changed authority gate.
$expression = '${{ false }}'
}
$lines.Add(" if: $expression")
}
$needs = [regex]::Match($body, '(?m)^ needs:\r?\n(?:^ - [^\r\n]+\r?\n)+')
if ($needs.Success) { $lines.Add($needs.Value.TrimEnd()) }
else {
$needs = [regex]::Match($body, '(?m)^ needs: [^\r\n]+')
if ($needs.Success) { $lines.Add($needs.Value) }
}
$lines.Add(' runs-on: ubuntu-latest')
if ($id -eq 'changes') {
$lines.Add(' outputs:')
foreach ($output in @('packaging', 'payload_artifact', 'bundle_build')) {
$lines.Add(" ${output}: 'true'")
}
$lines.Add(" versioning: '$versioning'")
}
$lines.Add(' steps:')
if ($id -eq 'reject-untrusted-official-signing') {
$steps = [regex]::Match($body, '(?ms)^ steps:\r?\n(?<steps>.*)$').Groups['steps'].Value
$lines.RemoveAt($lines.Count - 1)
$lines.Add(' steps:')
$lines.Add($steps.TrimEnd())
continue
}
$marker = (Join-Path $markers $id).Replace("'", "''")
$lines.Add(' - name: Record isolated job effect (no external actions)')
$lines.Add(' shell: pwsh')
$lines.Add(' run: |')
$lines.Add(" [IO.File]::WriteAllText('$marker', 'reached')")
if ($id -eq 'test-msix-upgrades' -and $scenario.Upgrade -eq 'failure') {
$lines.Add(' exit 1')
}
}
$projection = Join-Path $caseRoot 'proof.yml'
$projectionText = ($lines -join "`n") + "`n"
foreach ($forbidden in @('uses:', 'id-token:', 'environment:', 'AZURE_', 'secrets.')) {
if ($projectionText.Contains($forbidden, [StringComparison]::Ordinal)) {
throw "Unsafe projected execution contains $forbidden"
}
}
[IO.File]::WriteAllText($projection, $projectionText, $encoding)
$eventFile = Join-Path $caseRoot 'event.json'
$eventData = @{
ref = "refs/heads/$($scenario.Ref)"
after = $commit.Trim()
deleted = $false
inputs = @{ signing_mode = $scenario.Mode; publish_release = 'true' }
number = 1
pull_request = @{ head = @{ ref = 'proof-branch' }; base = @{ ref = 'main' } }
repository = @{ default_branch = 'main'; full_name = 'fixture/proof' }
}
[IO.File]::WriteAllText($eventFile, ($eventData | ConvertTo-Json -Depth 10), $encoding)
$empty = Join-Path $caseRoot 'empty'
[IO.File]::WriteAllText($empty, '', $encoding)
$processInfo = [Diagnostics.ProcessStartInfo]::new($ActPath)
$processInfo.WorkingDirectory = $caseRoot
$processInfo.UseShellExecute = $false
$processInfo.RedirectStandardOutput = $true
$processInfo.RedirectStandardError = $true
# Keep only OS execution prerequisites. No GitHub/Azure token or agent
# environment reaches the emulator or its marker-only child processes.
$processInfo.Environment.Clear()
foreach ($name in @(
'PATH', 'HOME', 'TMPDIR', 'TEMP', 'TMP', 'SystemRoot', 'COMSPEC',
'DOTNET_ROOT', 'DOTNET_ROOT_ARM64', 'DOTNET_ROOT_X64', 'DOTNET_ROLL_FORWARD'
)) {
$value = [Environment]::GetEnvironmentVariable($name)
if ($null -ne $value) { $processInfo.Environment[$name] = $value }
}
foreach ($argument in @(
$eventName, '-W', $projection, '-e', $eventFile,
'-P', 'ubuntu-latest=-self-hosted', '-P', 'windows-latest=-self-hosted',
'--secret-file', $empty, '--var-file', $empty, '--env-file', $empty,
'--input-file', $empty, '--json', '--concurrent-jobs', '1',
'--container-architecture', 'linux/arm64', '--action-offline-mode',
'--action-cache-path', (Join-Path $caseRoot 'action-cache'), '--no-cache-server'
)) { $processInfo.ArgumentList.Add($argument) }
$process = [Diagnostics.Process]::Start($processInfo)
$stdout = $process.StandardOutput.ReadToEndAsync()
$stderr = $process.StandardError.ReadToEndAsync()
$process.WaitForExit()
$log = $stdout.GetAwaiter().GetResult() + $stderr.GetAwaiter().GetResult()
[IO.File]::WriteAllText((Join-Path $caseRoot 'act.log'), $log, $encoding)
$effects = [ordered]@{
Authorized = 'authorize-signing'
Signed = 'sign-msix'
Published = 'publish-release'
Store = 'retain-store-submission'
Gate = 'ci-gate'
}
$record = [ordered]@{ name = $scenario.Name; exitCode = $process.ExitCode }
foreach ($effect in $effects.Keys) {
$expected = $scenario.ContainsKey($effect) -and $scenario[$effect]
$actual = Test-Path -LiteralPath (Join-Path $markers $effects[$effect])
$record[$effect] = $actual
if ($actual -ne $expected) {
throw "$($scenario.Name): $effect expected=$expected actual=$actual. Inspect $caseRoot/act.log"
}
}
$expectedExit = if ($scenario.ContainsKey('Gate') -and $scenario.Gate) { 0 } else { 1 }
if ($process.ExitCode -ne $expectedExit) {
throw "$($scenario.Name): expected exit $expectedExit, got $($process.ExitCode). Inspect $caseRoot/act.log"
}
$results.Add([pscustomobject]$record)
Write-Host "$($scenario.Name): passed"
}
$report = [ordered]@{
schemaVersion = 1
evidenceKind = 'credential-free act execution of production if/needs with isolated marker effects'
packagingCommit = $commit.Trim()
workflowSha256 = $workflowHash
emulator = 'nektos/act v0.2.89'
scenarios = $results
}
[IO.File]::WriteAllText((Join-Path $OutputDirectory 'report.json'), ($report | ConvertTo-Json -Depth 10) + "`n", $encoding)
Write-Host "All $($results.Count) production-DAG boundary scenarios passed. Evidence: $OutputDirectory"
|
Completed candidate and installed-transition proofHead: Manual release-candidate test dispatch 38841707866 completed successfully, including managed quality/Release tests, NativeAOT execution, every owning PowerShell suite, both architecture packages, both identity-channel bundles, temporary test signing, all 13 installed transitions, and the final aggregate gate. The immutable source snapshot selects OpenClaw
The same real non-main test dispatch left Current-head PR CI 38845312038 is recovering on attempt 2 from a hosted ARM64 The PR body is updated with the completed evidence, exact head, infrastructure failure/recovery, and live release lanes intentionally not run. No merge or release is requested here. @clawsweeper re-review |
|
🦞👀 Re-review progress:
|
What Problem This Solves
Fixes: release dispatches can authorize a Gateway MSIX without installed-upgrade proof for the candidate being shipped.
User Impact
Release owners can run a credential-free
testpreflight, andstore/officialdispatches must pass their own installed-upgrade matrix before authorization. The packaging revision is reset to0for the first package of the new upstream stable version; current stable2026.10.5derivesv2026.10.5-msix.0and package version2026.10.500.0.No installed launcher behavior, package identity, publisher, runtime pin, or immutable upgrade baseline changes. A candidate that cannot upgrade the Store-installed version intentionally stops before release authorization.
Why This Change Was Made
Manual
test,store, andofficialcandidates use the existing immutable source owner and complete upgrade matrix. Test mode captures the same stable release identity constraints without gaining publication or Azure credentials. Push and unsigned dispatches retain development versions and skip installed upgrades.The October 20 update integrates main
403e5ba393857b653e5427bbe9fb80e88d0f5cf1and resolves all three conflicts. It preserves main's reusable trusted/read-only builds, early source-admission checks, cache capability boundaries, and selected-build validation. The dispatch aggregate requirement now extendsAssert-WorkflowJobResults.ps1, rather than restoring the retired inline gate or source-string assertions. Release authorization directly depends on successful upgrade results.Evidence
Current signed head:
801e886a54805f56e3a1554f1a0b275450ccbbac. Independent Codex Auto Review completed against the full candidate relative to main403e5ba, with no actionable P0-P2 findings.Passed locally in PowerShell 7:
scripts/Test-WorkflowSigningConfiguration.ps1: current selected-build contracts and five actual workflow-classifier scenarios; 0.57 s wall time.scripts/Test-WorkflowJobResults.Tests.ps1: existing PR gates plus successful candidate dispatches and rejection of skipped, failed, or cancelled upgrades for all three candidate modes, even with a false classification flag; 0.39 s.scripts/Test-OpenClawSource.Tests.ps1: 75 cases, including test-mode identity, source overrides, offline retry replay, and retained early admission; 1.38 s.scripts/Test-SigningInputs.Tests.ps1.scripts/Test-MSIXReleaseIdentity.Tests.ps1and direct derivation from the updated policy.scripts/Test-WorkflowPackageVersion.Tests.ps1.scripts/Test-MSIXUpgradeMatrix.Tests.ps1.scripts/Test-PackagingRelevance.Tests.ps1.scripts/Test-DocReferences.ps1: zero findings;git diff --check; all ten changed files retain LF.On macOS, the signing and packaging-relevance fixtures initially stopped before their cases because
TEMPwas unset. Their unchanged suites passed with process-localTEMPset to .NET's temporary directory. No product compatibility path or test assertion was changed for that prerequisite.Exact-head PR CI is in progress. Windows, NativeAOT, package composition, and all 13 installed-upgrade scenarios are not covered by the local checks; no current-head installed-upgrade success is claimed. GitHub reports the updated head conflict-free (
MERGEABLE), not merge-ready while CI remains unfinished. Per-suite CI seconds are not yet available.Historical evidence, not current-head qualification
The prior manual test dispatch passed both architectures, bundles, all 13 installed transitions, and the final gate for OpenClaw
2026.10.4at8b58e12. Retained transition details include the authentic Store in-place update and LocalState retention. Credential-free production-DAG evidence covers the earlier workflow's allowed and denied authorization paths. Those receipts do not qualify the new assembled head or the new upstream version.No merge, manual dispatch, official signing, or release publication is part of this update. Real installed recovery/sign-in and MXC-session retention proof remain separate outstanding release work; package-transition CI does not prove those flows.