diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4d7e6bbb4..b11772e67 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1158,17 +1158,19 @@ jobs: -MsixBundleResult $env:MSIX_BUNDLE_RESULT "CI Gate passed $validatedMode validation." >> $env:GITHUB_STEP_SUMMARY - release: - needs: [change-classification, metadata, reserve-msix-version, build-x64, build-arm64, build-msix-bundle, ci-gate] - if: startsWith(github.ref, 'refs/tags/v') && needs.ci-gate.result == 'success' && needs.change-classification.outputs.full == 'true' && needs.metadata.result == 'success' && needs.reserve-msix-version.result == 'success' && needs.build-x64.result == 'success' && needs.build-arm64.result == 'success' && needs.build-msix-bundle.result == 'success' && !cancelled() + # Sign and package after the early release-critical suites pass so this work can + # overlap the long UI and E2E tail. Publication remains gated by ci-gate below. + prepare-release-assets: + needs: [change-classification, metadata, reserve-msix-version, proof-pool-contracts, core-tests, tray-tests, build-x64, build-arm64] + if: startsWith(github.ref, 'refs/tags/v') && needs.change-classification.outputs.full == 'true' && needs.metadata.result == 'success' && needs.reserve-msix-version.result == 'success' && needs.proof-pool-contracts.result == 'success' && needs.core-tests.result == 'success' && needs.tray-tests.result == 'success' && needs.build-x64.result == 'success' && needs.build-arm64.result == 'success' && !cancelled() runs-on: windows-latest environment: release-signing permissions: actions: read - contents: write + contents: read id-token: write concurrency: - group: openclaw-windows-node-release + group: openclaw-windows-node-release-preparation cancel-in-progress: false steps: @@ -1352,6 +1354,39 @@ jobs: timestamp-rfc3161: http://timestamp.acs.microsoft.com timestamp-digest: SHA256 + - name: Upload prepared release assets + uses: actions/upload-artifact@v7 + with: + name: openclaw-release-assets + path: | + Output/OpenClawCompanion-Setup-x64.exe + Output/OpenClawCompanion-Setup-arm64.exe + OpenClawTray-${{ needs.metadata.outputs.semVer }}-win-x64.zip + OpenClawTray-${{ needs.metadata.outputs.semVer }}-win-arm64.zip + compression-level: 0 + if-no-files-found: error + + release: + needs: [change-classification, metadata, reserve-msix-version, prepare-release-assets, build-msix-bundle, ci-gate] + if: startsWith(github.ref, 'refs/tags/v') && needs.ci-gate.result == 'success' && needs.change-classification.outputs.full == 'true' && needs.metadata.result == 'success' && needs.reserve-msix-version.result == 'success' && needs.prepare-release-assets.result == 'success' && needs.build-msix-bundle.result == 'success' && !cancelled() + runs-on: windows-latest + environment: release-signing + permissions: + actions: read + contents: write + concurrency: + group: openclaw-windows-node-release + cancel-in-progress: false + + steps: + - uses: actions/checkout@v7 + + - name: Download prepared release assets + uses: actions/download-artifact@v8 + with: + name: openclaw-release-assets + path: . + - name: Revalidate stable correction release ordering if: needs.metadata.outputs.isStableCorrection == 'true' shell: pwsh diff --git a/scripts/test-ci-workflow-contract.ps1 b/scripts/test-ci-workflow-contract.ps1 index 295082466..a1cdb57da 100644 --- a/scripts/test-ci-workflow-contract.ps1 +++ b/scripts/test-ci-workflow-contract.ps1 @@ -818,10 +818,28 @@ foreach ($token in @( Assert-Contains -Text $ciGateJob -Expected $token -Message "Stable CI Gate is missing '$token'." } +$releasePreparationJob = Get-JobBlock "prepare-release-assets" +foreach ($token in @( + "needs: [change-classification, metadata, reserve-msix-version, proof-pool-contracts, core-tests, tray-tests, build-x64, build-arm64]", + "needs.reserve-msix-version.result == 'success'", + "needs.proof-pool-contracts.result == 'success'", + "needs.core-tests.result == 'success'", + "needs.tray-tests.result == 'success'", + "group: openclaw-windows-node-release-preparation", + "name: Upload prepared release assets", + "name: openclaw-release-assets", + "compression-level: 0", + "if-no-files-found: error" + )) { + Assert-Contains -Text $releasePreparationJob -Expected $token -Message "Release preparation is missing '$token'." +} +Assert-NotContains -Text $releasePreparationJob -Unexpected "ci-gate" -Message "Release preparation must overlap the long CI tail." + $releaseJob = Get-JobBlock "release" foreach ($token in @( - "needs: [change-classification, metadata, reserve-msix-version, build-x64, build-arm64, build-msix-bundle, ci-gate]", + "needs: [change-classification, metadata, reserve-msix-version, prepare-release-assets, build-msix-bundle, ci-gate]", "needs.reserve-msix-version.result == 'success'", + "needs.prepare-release-assets.result == 'success'", "needs.build-msix-bundle.result == 'success'", "needs.ci-gate.result == 'success'", "needs.metadata.outputs.semVer", @@ -830,6 +848,9 @@ foreach ($token in @( )) { Assert-Contains -Text $releaseJob -Expected $token -Message "Tag release is missing '$token'." } +$preparedAssetsDownload = Get-StepBlock -Text $releaseJob -Name 'Download prepared release assets' +Assert-Contains -Text $preparedAssetsDownload -Expected 'name: openclaw-release-assets' -Message 'Publication must consume the prepared signed assets.' +Assert-Contains -Text $preparedAssetsDownload -Expected 'path: .' -Message 'Prepared assets must retain the release action paths.' $msixX64Download = Get-StepBlock -Text $releaseJob -Name 'Download x64 signed Dev MSIX release artifact' $msixArm64Download = Get-StepBlock -Text $releaseJob -Name 'Download ARM64 signed Dev MSIX release artifact' $msixTrust = Get-StepBlock -Text $releaseJob -Name 'Trust signed Dev MSIX certificates for validation' diff --git a/tests/OpenClaw.Tray.Tests/ReleaseSigningWorkflowTests.cs b/tests/OpenClaw.Tray.Tests/ReleaseSigningWorkflowTests.cs index 52a2f4258..5d0f7ad7c 100644 --- a/tests/OpenClaw.Tray.Tests/ReleaseSigningWorkflowTests.cs +++ b/tests/OpenClaw.Tray.Tests/ReleaseSigningWorkflowTests.cs @@ -129,7 +129,8 @@ public void ReleaseWorkflow_PublishesSignedDevMsixForEveryTag() Assert.Contains("MSIX_BUNDLE_RESULT: ${{ needs.build-msix-bundle.result }}", workflow); Assert.Contains(@".\scripts\Build-StoreMsixBundle.ps1", workflow); Assert.Contains("name: openclaw-msix-store-unsigned-bundle", workflow); - Assert.Contains("needs: [change-classification, metadata, reserve-msix-version, build-x64, build-arm64, build-msix-bundle, ci-gate]", workflow); + Assert.Contains("needs: [change-classification, metadata, reserve-msix-version, prepare-release-assets, build-msix-bundle, ci-gate]", workflow); + Assert.Contains("needs.prepare-release-assets.result == 'success'", workflow); Assert.Contains("needs.build-msix-bundle.result == 'success'", workflow); Assert.DoesNotContain("Download win-x64 MSIX artifact", workflow); Assert.DoesNotContain("Download win-arm64 MSIX artifact", workflow); @@ -155,6 +156,32 @@ public void ReleaseWorkflow_PublishesSignedDevMsixForEveryTag() Assert.DoesNotContain("OpenClaw.msixbundle", releaseStep); } + [Fact] + public void ReleaseWorkflow_PreparesSignedAssetsBeforeTheFullCiGate() + { + var workflow = File.ReadAllText(Path.Combine(TestRepositoryPaths.GetRepositoryRoot(), ".github", "workflows", "ci.yml")); + var preparationStart = workflow.IndexOf(" prepare-release-assets:", StringComparison.Ordinal); + var releaseStart = workflow.IndexOf(" release:", StringComparison.Ordinal); + + Assert.True(preparationStart >= 0, "Could not find release preparation job."); + Assert.True(releaseStart > preparationStart, "Release publication must follow asset preparation."); + + var preparationJob = workflow[preparationStart..releaseStart]; + var releaseJob = workflow[releaseStart..]; + Assert.Contains("needs: [change-classification, metadata, reserve-msix-version, proof-pool-contracts, core-tests, tray-tests, build-x64, build-arm64]", preparationJob); + Assert.DoesNotContain("ci-gate", preparationJob); + Assert.Contains("name: Sign Installers", preparationJob); + Assert.Contains("name: Upload prepared release assets", preparationJob); + Assert.Contains("name: openclaw-release-assets", preparationJob); + + Assert.Contains("needs: [change-classification, metadata, reserve-msix-version, prepare-release-assets, build-msix-bundle, ci-gate]", releaseJob); + Assert.Contains("needs.prepare-release-assets.result == 'success'", releaseJob); + Assert.Contains("name: Download prepared release assets", releaseJob); + Assert.Contains("name: openclaw-release-assets", releaseJob); + Assert.Contains("name: Create Release", releaseJob); + Assert.DoesNotContain("name: Sign Installers", releaseJob); + } + private static string ExtractReleaseStep(string workflow) { var start = workflow.IndexOf(" - name: Create Release", StringComparison.Ordinal);