diff --git a/CHANGELOG.md b/CHANGELOG.md index f8ce104..6c47c04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,8 @@ - separated desktop-to-Unraid authentication, Docker/Compose capabilities and optional Unraid-to-Gitea access in diagnostics; - preserved adopted DockerMan templates and disabled aggressive recreate/orphan flags by default; - promoted deployment state only after Compose validation and service verification, with atomic manifests, rollback restoration and live lock ownership; -- retained server-side Git and monitor-only modes for explicit use cases. +- retained server-side Git and monitor-only modes for explicit use cases; +- corrected release publication so source, installer, portable executable and SHA-256 sidecars are published together, with a recovery publisher for source-only Gitea releases. ## 0.8.9 - 2026-07-26 diff --git a/PUBLISH-AND-ENABLE-UPDATE.cmd b/PUBLISH-AND-ENABLE-UPDATE.cmd new file mode 100644 index 0000000..5cdebe7 --- /dev/null +++ b/PUBLISH-AND-ENABLE-UPDATE.cmd @@ -0,0 +1,15 @@ +@echo off +setlocal +cd /d "%~dp0" +echo ForgeFlow source and binary release publisher +echo. +powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0Publish-ForgeFlow-Release.ps1" +set "forgeflowExitCode=%ERRORLEVEL%" +echo. +if not "%forgeflowExitCode%"=="0" ( + echo Publication failed. The existing ForgeFlow installation was not modified. +) else ( + echo Publication completed. The older ForgeFlow updater can now install this release. +) +pause +exit /b %forgeflowExitCode% diff --git a/Publish-ForgeFlow-Release.ps1 b/Publish-ForgeFlow-Release.ps1 index 258cf96..888eec8 100644 --- a/Publish-ForgeFlow-Release.ps1 +++ b/Publish-ForgeFlow-Release.ps1 @@ -1,7 +1,9 @@ param( [string]$Remote = "git@gitea.itworx.tech:Jens/ForgeFlow.git", [string]$Branch = "main", - [string]$InstalledSource = "C:\Projects\ForgeFlow" + [string]$InstalledSource = "C:\Projects\ForgeFlow", + [string]$UserDataPath = "", + [switch]$SkipBinaryRelease ) $ErrorActionPreference = "Stop" @@ -58,6 +60,47 @@ try { Write-Host "ForgeFlow $version is available on Gitea at commit $($publishedCommit.Substring(0,7))." -ForegroundColor Green + if (-not $SkipBinaryRelease) { + Write-Host "Building and publishing the matching Windows installer and portable release..." -ForegroundColor Cyan + Push-Location $clone + try { + & cmd.exe /d /s /c "npm ci --no-audit --no-fund" + if ($LASTEXITCODE -ne 0) { throw "npm ci failed in the exact published checkout." } + & cmd.exe /d /s /c "npm run check" + if ($LASTEXITCODE -ne 0) { throw "The exact published checkout failed the release quality gate." } + & cmd.exe /d /s /c "npm run dist:win" + if ($LASTEXITCODE -ne 0) { throw "The Windows release build failed." } + + $expectedAssets = @( + "ForgeFlow-Setup-$version-win-x64.exe", + "ForgeFlow-Setup-$version-win-x64.exe.sha256", + "ForgeFlow-Portable-$version-win-x64.exe", + "ForgeFlow-Portable-$version-win-x64.exe.sha256" + ) + foreach ($assetName in $expectedAssets) { + if (-not (Test-Path -LiteralPath (Join-Path $clone "dist\$assetName"))) { + throw "The Windows build did not produce $assetName." + } + } + + $resolvedUserData = if ($UserDataPath) { $UserDataPath } else { Join-Path $env:APPDATA "forgeflow" } + $previousUserData = $env:FORGEFLOW_USER_DATA + $previousBranch = $env:FORGEFLOW_RELEASE_BRANCH + try { + $env:FORGEFLOW_USER_DATA = $resolvedUserData + $env:FORGEFLOW_RELEASE_BRANCH = $Branch + & cmd.exe /d /s /c "npm run release:binary" + if ($LASTEXITCODE -ne 0) { throw "The Gitea binary release publisher failed." } + } finally { + $env:FORGEFLOW_USER_DATA = $previousUserData + $env:FORGEFLOW_RELEASE_BRANCH = $previousBranch + } + } finally { Pop-Location } + Write-Host "ForgeFlow $version source and Windows release assets are both published." -ForegroundColor Green + } else { + Write-Host "Binary publication was skipped explicitly. Packaged ForgeFlow installations cannot auto-update until the release assets are published." -ForegroundColor Yellow + } + $installedManifestPath = Join-Path $InstalledSource "package.json" if (Test-Path -LiteralPath $installedManifestPath) { try { @@ -123,7 +166,11 @@ try { Write-Host "Installed source was not found at $InstalledSource; publication itself succeeded." -ForegroundColor Yellow } - Write-Host "Open the installed older ForgeFlow and use Settings -> ForgeFlow updates -> Check now." -ForegroundColor Cyan + if ($SkipBinaryRelease) { + Write-Host "Source publication completed. Run Publish-Missing-Binary-Release.ps1 before using the updater from an installed EXE." -ForegroundColor Yellow + } else { + Write-Host "Open the installed older ForgeFlow and use Settings -> ForgeFlow updates -> Check now." -ForegroundColor Cyan + } } finally { Remove-Item -LiteralPath $temp -Recurse -Force -ErrorAction SilentlyContinue diff --git a/Publish-Missing-Binary-Release.ps1 b/Publish-Missing-Binary-Release.ps1 new file mode 100644 index 0000000..53739c6 --- /dev/null +++ b/Publish-Missing-Binary-Release.ps1 @@ -0,0 +1,112 @@ +param( + [string]$Remote = "git@gitea.itworx.tech:Jens/ForgeFlow.git", + [string]$Branch = "main", + [string]$ExpectedVersion = "0.9.1", + [string]$UserDataPath = "" +) + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version Latest +$temp = Join-Path ([IO.Path]::GetTempPath()) ("forgeflow-binary-release-" + [guid]::NewGuid().ToString("N")) +$clone = Join-Path $temp "ForgeFlow" + +function Invoke-CheckedCommand { + param( + [Parameter(Mandatory = $true)][string]$Title, + [Parameter(Mandatory = $true)][scriptblock]$Action + ) + Write-Host "`n$Title" -ForegroundColor Cyan + & $Action + if ($LASTEXITCODE -ne 0) { + throw "$Title failed with exit code $LASTEXITCODE." + } +} + +try { + foreach ($command in @("git", "node", "npm")) { + if (-not (Get-Command $command -ErrorAction SilentlyContinue)) { + throw "Required command '$command' was not found on PATH." + } + } + + New-Item -ItemType Directory -Force -Path $temp | Out-Null + Invoke-CheckedCommand "Cloning the exact published ForgeFlow source..." { + & git clone --branch $Branch --single-branch $Remote $clone + } + + $manifestPath = Join-Path $clone "package.json" + if (-not (Test-Path -LiteralPath $manifestPath)) { + throw "The cloned repository does not contain package.json." + } + $manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json + if ($manifest.name -ne "forgeflow") { + throw "The cloned repository is not ForgeFlow." + } + $version = [string]$manifest.version + if ($ExpectedVersion -and $version -ne $ExpectedVersion) { + throw "Gitea branch '$Branch' contains ForgeFlow $version, not the expected $ExpectedVersion." + } + + $localCommit = (& git -C $clone rev-parse HEAD).Trim() + $remoteLines = @(& git -C $clone ls-remote origin "refs/heads/$Branch") + if ($LASTEXITCODE -ne 0 -or $remoteLines.Count -lt 1) { + throw "Could not verify origin/$Branch." + } + $remoteCommit = ($remoteLines[0] -split "`t")[0].Trim() + if ($localCommit -ne $remoteCommit) { + throw "The temporary checkout is not the current origin/$Branch commit." + } + + Push-Location $clone + try { + Invoke-CheckedCommand "Installing exact dependencies..." { + & cmd.exe /d /s /c "npm ci --no-audit --no-fund" + } + Invoke-CheckedCommand "Running the complete ForgeFlow quality gate..." { + & cmd.exe /d /s /c "npm run check" + } + Invoke-CheckedCommand "Building installer and portable Windows assets..." { + & cmd.exe /d /s /c "npm run dist:win" + } + + $expectedAssets = @( + "ForgeFlow-Setup-$version-win-x64.exe", + "ForgeFlow-Setup-$version-win-x64.exe.sha256", + "ForgeFlow-Portable-$version-win-x64.exe", + "ForgeFlow-Portable-$version-win-x64.exe.sha256" + ) + foreach ($assetName in $expectedAssets) { + $assetPath = Join-Path $clone "dist\$assetName" + if (-not (Test-Path -LiteralPath $assetPath)) { + throw "The build did not produce $assetName." + } + } + + $resolvedUserData = if ($UserDataPath) { $UserDataPath } else { Join-Path $env:APPDATA "forgeflow" } + $configPath = Join-Path $resolvedUserData "forgeflow-config.json" + if (-not (Test-Path -LiteralPath $configPath)) { + throw "ForgeFlow configuration was not found at $configPath. Open ForgeFlow and sign in to Gitea once, then run this script again." + } + + $previousUserData = $env:FORGEFLOW_USER_DATA + $previousBranch = $env:FORGEFLOW_RELEASE_BRANCH + try { + $env:FORGEFLOW_USER_DATA = $resolvedUserData + $env:FORGEFLOW_RELEASE_BRANCH = $Branch + Invoke-CheckedCommand "Creating the Gitea release and uploading all four assets..." { + & cmd.exe /d /s /c "npm run release:binary" + } + } finally { + $env:FORGEFLOW_USER_DATA = $previousUserData + $env:FORGEFLOW_RELEASE_BRANCH = $previousBranch + } + } finally { + Pop-Location + } + + Write-Host "`nForgeFlow $version now has a published binary release for commit $($localCommit.Substring(0,7))." -ForegroundColor Green + Write-Host "Return to ForgeFlow $ExpectedVersion's predecessor and choose Check now -> Download update -> Apply & restart." -ForegroundColor Green +} +finally { + Remove-Item -LiteralPath $temp -Recurse -Force -ErrorAction SilentlyContinue +} diff --git a/README.md b/README.md index 9d2f63b..abab930 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,8 @@ Handige opdrachten: | `npm run doctor` | Controleert de lokale ontwikkelomgeving. | | `npm run acceptance` | Voert de release-acceptatiecontroles uit. | | `npm run dist:win` | Bouwt Windows installer + portable package, schrijft checksums en ruimt oude dist-artifacts op. | +| `.\Publish-ForgeFlow-Release.ps1` | Publiceert broncode én de bijbehorende Windows-release-assets als één gecontroleerde release. | +| `.\Publish-Missing-Binary-Release.ps1` | Herstelt een reeds gepushte versie waarvoor de Gitea binary release ontbreekt. | De belangrijkste onderdelen zijn: diff --git a/SOURCE_MANIFEST.txt b/SOURCE_MANIFEST.txt index 79b42d6..631f4b1 100644 --- a/SOURCE_MANIFEST.txt +++ b/SOURCE_MANIFEST.txt @@ -1,4 +1,4 @@ -ForgeFlow 0.9.0 source manifest +ForgeFlow 0.9.1 source manifest SHA-256 BYTES PATH (The manifest excludes itself, dependencies and generated release artifacts.) 755f4db7d76bfec0963ef051748a82810c0d58acd4ffd823aa6928a5167fceb4 58 .gitignore @@ -12,7 +12,7 @@ ca32a76e708d565c4af659f0f4d2615fc32114c3f75aec1454862a3ed1e72c41 2263 4633990a4b055bb3d00fef915ee29e85be5ee8413f809334728ad9688973c183 3364 build/icon-64.png 25048ed854e8ce8fece115e555c98d25507b002f8019b6ae717b54604c868c50 46223 build/icon.ico 16efd2fca83004f781eae40ae0f706a004ce0bddf338dd087b8adf7eb10c1d84 85704 build/icon.png -e3fd7c60e47b04aab79518bfc80b747bd76b299d20e7caca570abaf10b57cdca 12075 CHANGELOG.md +65a78f6aeb0a3a4e3ef00e33913438b0585259fb3103f43baf4401d5c88c85a8 12252 CHANGELOG.md 21cb96e7afe71b1dc791c818dedd244d92f9a6ed4d9ffbb3022ccb187e1bdf0f 852 docs/ACCEPTANCE.md a17f95d96d3c9fbc69d870874e6fbb7472091adefc454b24f835db1279511d72 8296 docs/ARCHITECTURE.md 30a92bcf5daadb019efa2f82cb820ea302490dd1d68fb772674dc3faccd3e594 2045 docs/DEPLOYMENT_SETUP.md @@ -47,7 +47,8 @@ b516db97a0353babc810c24a87a971d30d72a8021d809e6b833ff7ae0458f442 538 ef049adcfa204908e6dc3a059124b39ba0e2739cc54e38945ce73a57049df0d8 1185 docs/RELEASE_NOTES_0.8.7.md 7eedb25e1aae3b06a04bb9b2f4843bd6af614418737e600b4bdc9161edabd76a 632 docs/RELEASE_NOTES_0.8.8.md 35dcfda990946480d6d55bd2d2e6360c336260bcd05cdb51d92e07a4e8d76945 1046 docs/RELEASE_NOTES_0.8.9.md -0f5d64a752424bb8e2f5aa8a01fa31ea3bce3db8fd70e6b3c2ea29c41dd954f5 2574 docs/RELEASE_NOTES_0.9.0.md +29f7b11fef1e4960ef874f643b2206ca73a310b0b75402bcd3764a01e59db2e0 3114 docs/RELEASE_NOTES_0.9.0.md +ed40e08bac8792f95970bc05e49bce3cc9e288a08d11565a1bd156d787360a3b 720 docs/RELEASE_NOTES_0.9.1.md 2b631b9d6d973bdd70869d84886ff339da351e29e17598970b3b27915674661d 4175 docs/ROADMAP.md 1ccde232c060395d7aedce27e89a7647b77afe28ab71de0a5a3efeded57369d3 140415 docs/screenshots/deploy-confirmation.png b39506254ffa2c73c389fb4795b3a745368bbeb7d8514cc47a636316d6d9a6aa 107166 docs/screenshots/deployment-run.png @@ -62,7 +63,7 @@ c8a5e80bb9fd2d442d2d23d30e6ac1528cf2330e6e19492b7c6799e2d1508b53 112868 b6a178215dab054006aae4944b8ffcbe7f6100691c30f08e221e3a2dbff4cd42 2147 docs/STATUS_ENDPOINT.md 0adfeabb98168a7fc0b02bae8d4af436d3c59459012fb05b2216e02265190128 3139 docs/STITCH_REVIEW.md 4625a10ebd3c749f60b2a7bef6b1716cd05dbc44ccceba0491a1b46bc293c195 4883 docs/TEST_MATRIX.md -b3f317faba63d574ee83924f01b5275c118b5b04f6d9109c7cf9ed18500835d8 3389 docs/UPDATING.md +dbbd9fa96988e7543e98c85da864adaadd3057815f18d20a3b3ccb5c540a169d 4558 docs/UPDATING.md c230b931abf2293d2d44b7a69b94c35f1142c093cc46b88739a0de5cbd6d1896 1532 examples/gitea-actions/deploy.yml 4c792cc9fd57ed36da291300c252a6ef75b08a249cf6f2561e23c4c22522138a 1477 examples/gitea-actions/rollback.yml 1d2cde1bef4882f56006823d2806f6105882fa098a665a303150fdf18ada2004 5705 examples/server/forgeflow-deploy @@ -73,21 +74,23 @@ c230b931abf2293d2d44b7a69b94c35f1142c093cc46b88739a0de5cbd6d1896 1532 4a561ead5ba7cdfaf4efce91842a4308c5f2a77980205879d83835efb8a579db 1067 LICENSE 3b16a087c73b600415394dff8b8e34e7f7519e48fde1cf443007b2e11ca77b27 13123 main.cjs 91a984a89dd57a084b9a2331763cacdb061582fb590f13df379d92c1a77a2ee1 352 OVERLAY-INSTRUCTIONS.md -bf0ccaa096bb7db57eda09c74769ba5f798ccf72654bafe6d04efea6446d0418 130466 package-lock.json -2a6d2668296f9f0e3fea2ef3ce5afebf5b8e6d60fbcbf6939c827e69087dfde7 3980 package.json +7672d16f79afe9273c56ee6e3a579145c20c1f744acb25ead8605df836c5bf34 130466 package-lock.json +0ea7d7a6457ae5e2ca9b90f78a38ad9487957bd92b3449904851d7274f108ac3 4100 package.json 6b66a5aef158de35d5cdd0f205ceeec53b6a375937a67c5e9d00e483999ff840 9898 preload.cjs -b31c43d9355c13b5ae4efc0f3649d8cb8d509b2bb7ebb042ff546b7820fb7de8 8411 Publish-ForgeFlow-Release.ps1 -d929f074b9c24619f0e750753b8721b212ed37cedad44565df9c52754eb71a23 8802 README.md +abe5dd6fd68f2970cd19ef134094907c67219061d8fe9a1a08324c78de4ad437 484 PUBLISH-AND-ENABLE-UPDATE.cmd +f018383f755352ca448e2ebb1e19b1dba412a3eb793d61e64b02953e300754fd 10538 Publish-ForgeFlow-Release.ps1 +688fff7d2c989adb97ebb7fae38962656b70304a0aa5d27433c56adf7f136de0 4196 Publish-Missing-Binary-Release.ps1 +d4b3a2f1174fe056e89e05de9c6478e42cf6758c7279f69f6c2d4bf0aed839ca 9063 README.md 509c7bcff5280349bd9f45ed6151f70372bad7010a9ea582c13e2ccab91fe0cd 6272 scripts/acceptance.mjs 00d57bda5af8c8eda294b72d18b318f024a307b81b0d9205a0821f5240151e31 3814 scripts/apply-binary-update.ps1 f8359a69d20deb2dfe10042d1bec7b12a95e76e58e36bc5f265f073c3111d056 10287 scripts/apply-source-update.ps1 6d46dd6826069d842f20f9f22a99042257db936cdea0bee8d294d2d7ea290126 3893 scripts/doctor.mjs 5e9a2a819522f6a32bbd9d3303263d5e5eaec95898ea2cd5776b221168008d75 1727 scripts/generate-source-manifest.mjs 842436680521311594e798848b050ae4e488d0595f0de57315f6ec081c049fb9 1266 scripts/prune-dist.mjs -74433d8a6b24afe368197a469e2fe0c5050c239d7250b84c2f3f598c304778b0 4736 scripts/publish-binary-release.cjs +403a64db5069595a83006a4e293d7e5ceeaefcfb74e820ed3e899864a0f182d2 6066 scripts/publish-binary-release.cjs 444b397d515d65a7ee59d3088cba869cbb812d2b8cc18fc5d255105e3edb58c2 1468 scripts/serve-demo.mjs 42203f9e0fd4aae517284d387f265cf1b0b180379bc253a092b5c3c5c4caef0a 2992 scripts/validate-installed-connections.cjs -1b269fa25947324120cab42612aaef806c3f72c761eacb4649edd58396a0de6f 12890 scripts/verify.mjs +b9a013440b75db306dee754a8711ae8458c5271d9ddd921589c26f8d97105970 13542 scripts/verify.mjs 0079701b5acbfef07b71a9623613d1940805ccd20649d77e3f34c37e79df7655 735 scripts/write-release-checksums.mjs 619515f524cb89960370ffcbd3fafd3c0e178b95f69c5868b1dd44777f23ec1e 2081 setup-windows.ps1 dd613d04b366f2cd071a1685a414016a5fb008082ed1b4cb8b24b79c100f640a 2412 src/main/audit-service.cjs @@ -98,7 +101,7 @@ c157640e76d558906a9aa9881eda811196623ef1c65fa3467f32f0f84b0ddd0c 15095 a2ef47d5330095b92c2bd22fcc39962091881f9cb60d02e261eb1dd1bd693170 1974 src/main/external-tools-service.cjs 0b7476c2cfe1872601978c20a466c20fe58be35e81b2303e38a753fea62bbc27 32548 src/main/git-service.cjs 857f270a2204b743421eea619a6a88595f749e4c24c1794cb092ace7987d25dc 12553 src/main/git-validator-service.cjs -1c0a1c1b7f20734c646c874e07c89550e601951351c04770874c1ed3496f0833 17837 src/main/gitea-service.cjs +4a58f02cd93e40d5e7880206b279dda75aa1f729f82733e19c554aaaddc1ff35 18436 src/main/gitea-service.cjs 703547b6d8f5837801b953cb4c049e41139343da131088e0cd5bb7382c05e772 48373 src/main/ipc.cjs 62f2c80c8210e19370b8556b1f296cbae50dae6b758a39e209f8fb461691fd4c 4235 src/main/log-redaction.cjs 958595a99fb242c127f475f3d8622bdba4c07b2d658703f69fe3992227a9107e 12909 src/main/preflight-service.cjs @@ -108,7 +111,7 @@ e89b54e7e3174b4b0a1dcd9058d8344e29431f9d16d0e6bb8d11559b691440a0 2508 cc4d5e06119d0315e4240f0776d68aacf72f2f36907f40830fa86e7bb0876490 18250 src/main/server-inventory.cjs afef3841a3948b2121f8fba809aae4ea3da71bd2fda86973ba50200a5b1f89b2 14894 src/main/ssh-service.cjs 51bfc677fcdaec75ac6abe1a55ff531bc081787053b49ee56d8bfffcab666aa4 99239 src/main/unraid-deployment-service.cjs -45e65564e1e8b9db487dc6dda03a752c51130189f23ec3b1260dc62e3621a925 20806 src/main/update-service.cjs +b654a9e45044ad32c61fabe4a6d897288615ec83739b53e3241ff881e32f56bd 21677 src/main/update-service.cjs 704306badd4a1a7080c3d2f4407c8dc1d8ec5f807dd165cd22a37e48d6022435 220714 src/renderer/app.js 16efd2fca83004f781eae40ae0f706a004ce0bddf338dd087b8adf7eb10c1d84 85704 src/renderer/assets/itworx-mark.png 813b8cdeecac43794166f3db9d3c5d2c441e0292f9ab7bd465ba136d6201e95d 82476 src/renderer/assets/itworx-wordmark-dark.png @@ -126,7 +129,7 @@ ede2c95bb045c0005a3931709a0116d9fbcb3faa5f609848a0066c6ba382ca0b 2906 2daa98fd421598bfe5fc9757c9b6f4d82c31d1bfece15829928473581d5d2639 1210 src/shared/tool-invocation.cjs 114f01be8bd54c91b90af82d8e1604e24cc0c5f8e64e63c40cf3f4042623a98e 5402 src/shared/validation.cjs 13b731c38863b1007b0312fd9d89562401b7cce875c952f52429bde74f77a8af 3096 src/shared/zip-writer.cjs -c1b0fd37a6ae74a56750138626fb2bc3523485c1124a0b8ac8ee3ef41089206a 2156 START_HERE.md +f8853dce6fdf360d5df2fbe2b6df3e5687630c807fee5ba8436679b34ec737ea 2436 START_HERE.md 058aeaa5d9bfe377c7e322f213c7871ecc4151b5d08ef790992f4ee28d857658 743 START-FORGEFLOW-OVERLAY.ps1 f5b0ea887fcdeadec78c1ad49b0ec7979723562f5c0b730703acb77a37281ee0 1009 tests/acceptance.test.mjs a4e5947204ff6878e601e32477bc85b53cd0153baf95a161c8935b6e5466c257 1155 tests/audit-service.test.mjs @@ -140,7 +143,7 @@ e7aebcc0d484a6a59d463d5cb26c11b3ad56e28f6535e7c38a0fe166a41565ea 13690 5ea94c6b241a02060d531fad94e449eecd3772eed2137581d4e2babfb09e56db 1239 tests/git-status.test.mjs c98cbe50a783e2a1cfecf9052f558aabe656add6a463899532dd59d743b720b2 3645 tests/git-validator.test.mjs 681ab7bcd02c4dd98d1d8d2092a3521c489d941131e7ffe5903971b940046474 2403 tests/git-workflows.test.mjs -8df055080e7166a52d36a6fdc0bab40c09b054d579c6848fcc076245de1573c4 6797 tests/gitea-actions.test.mjs +8ffadbf02ebbaead210cce636e69b943e0891524b92090534c7e7139a8c202b7 7718 tests/gitea-actions.test.mjs 48bca4711e7c193d19c78a0cb45ea1c83179b3c23640195f66058268e8a11b52 1520 tests/ipc-contract.test.mjs caf98cbd9de9b119dae610ee53fa333a7a11214f34762247452fbb85e8bbf725 2392 tests/log-redaction.test.mjs 96432a97d313f331694900bf0a2c21e38c20eac96d59147977aeed9055a9e3ad 2287 tests/partial-staging.test.mjs @@ -155,7 +158,7 @@ bab853feb0e22aa25af17989baaa632c01efa636533ea67407fecfdd973c7024 627 2571128f0b8e650071df17755baa09c4dfc441af0c20a7a4e9aa445b59e87d11 1654 tests/ssh-service.test.mjs 8a6a8477eb94b85ccef18cddd2640afb0d1eafa679c96bc7de20428d5d69e1be 1794 tests/tool-invocation.test.mjs 936412de29be7fbccb71a32460e222077361090deb1373fd56c904d70a0af718 37436 tests/unraid-deployment.test.mjs -11d6e6329f775617a1ce3657d0454cc97d7bcf3d9759f9a5a623c9d18e13d03e 15118 tests/update-service.test.mjs +4abe7b2fc113c486f35f15c2d629c5b4f24589eada718c4ea58f93551c77d5eb 17777 tests/update-service.test.mjs 9cea5c1d5ba3e0972a0b5c7236cf1f7c5616373e0a39ea4a492ecebf70452e40 948 tests/validation.test.mjs 7ef4d4b9f5f3e6979293b29d571ce0e39f83197f3cade2d999a9cea7bacdd84d 1781 tests/zip-writer.test.mjs 8f36b542736f2933bad8b9464ad7fa37b68196009c81cf702ce3b677cd637dea 767 UPDATE_FROM_0.3.2.md diff --git a/START_HERE.md b/START_HERE.md index 6ae4010..30a6964 100644 --- a/START_HERE.md +++ b/START_HERE.md @@ -1,12 +1,14 @@ -# Start here — ForgeFlow v0.6.0 +# Start here — ForgeFlow v0.9.0 You do **not** need to send anyone your Gitea token, SSH key or server password. All credentials are entered locally in ForgeFlow during setup. Diagnostic logging is designed to exclude them. -## Already running an older source release? +## Already running an older ForgeFlow release? -Publish v0.6.0 with `Publish-ForgeFlow-Release.ps1`, leave the currently installed source folder untouched, and test **Settings → ForgeFlow updates → Check now → Download update → Apply & restart**. Configuration and credentials remain outside the source directory. +Run `Publish-ForgeFlow-Release.ps1` from the validated source. It now publishes the source commit and matching Windows installer/portable assets together. Leave the currently installed older folder or executable untouched, then test **Settings → ForgeFlow updates → Check now → Download update → Apply & restart**. Configuration and credentials remain outside the application directory. + +When version 0.9.0 source was already pushed without a Gitea binary release, run `Publish-Missing-Binary-Release.ps1 -ExpectedVersion 0.9.0` once. Afterwards the existing 0.8.9 updater can install 0.9.0 normally. ## Fast path on Windows diff --git a/docs/RELEASE_NOTES_0.9.0.md b/docs/RELEASE_NOTES_0.9.0.md index 52b0742..3cc20c1 100644 --- a/docs/RELEASE_NOTES_0.9.0.md +++ b/docs/RELEASE_NOTES_0.9.0.md @@ -21,3 +21,10 @@ The server verifies the checksum and archive paths, rejects symlink payloads, pr Adopted workloads retain their existing Compose project, Compose files and service set. ForgeFlow no longer overrides their image or container name in the metadata overlay. Existing DockerMan templates are left untouched; generated templates are managed only for explicitly generated Compose profiles. `--force-recreate` and `--remove-orphans` are opt-in rather than defaults. A deployment lock records the live shell process, and an old lock is removed only when it is sufficiently old and its owner no longer runs. Deployment output truncation now fails explicitly instead of allowing ForgeFlow to interpret an incomplete inventory or command result. +## Release publication correction + +- The standard release publisher now publishes the validated source and matching Windows binary assets as one workflow. +- Added `Publish-Missing-Binary-Release.ps1` to repair a source-only Gitea release without reinstalling ForgeFlow manually. +- Binary publication now derives the repository owner, repository name and branch from ForgeFlow settings instead of hardcoding them. +- Missing-release errors now explain that packaged installations require both Windows executables and their SHA-256 sidecars. + diff --git a/docs/RELEASE_NOTES_0.9.1.md b/docs/RELEASE_NOTES_0.9.1.md new file mode 100644 index 0000000..97c5063 --- /dev/null +++ b/docs/RELEASE_NOTES_0.9.1.md @@ -0,0 +1,10 @@ +# ForgeFlow 0.9.1 + +## Gitea binary updater repair + +- Downloads the actual release attachment through `browser_download_url` instead of treating the Gitea attachment metadata response as an executable. +- Keeps the Gitea token on the configured Gitea origin and follows HTTPS object-storage redirects without leaking credentials. +- Adds regression coverage for attachment metadata lookup, direct browser download URLs and cross-origin redirect safety. +- Improves diagnostics when an older updater receives JSON attachment metadata. + +Because ForgeFlow 0.8.9 and 0.9.0 contain the broken attachment endpoint, upgrading to 0.9.1 requires one manual installer run. in-app updates work normally again after 0.9.1 is installed. diff --git a/docs/UPDATING.md b/docs/UPDATING.md index 540361f..f544567 100644 --- a/docs/UPDATING.md +++ b/docs/UPDATING.md @@ -30,19 +30,29 @@ Update logs and status files are stored beneath ForgeFlow's local user-data `upd ## Packaged Windows updates -ForgeFlow 0.8.2 and newer use authenticated Gitea release assets when running from the installer or portable executable. The updater selects the installer or portable artifact that matches the current installation mode, requires its `.sha256` sidecar, validates the Windows executable header and SHA-256 digest, then verifies the digest again immediately before applying it. An external PowerShell helper waits for ForgeFlow to exit, installs or replaces the executable and restarts it. +ForgeFlow 0.9.1 and newer use authenticated Gitea release assets when running from the installer or portable executable. The updater selects the installer or portable artifact that matches the current installation mode, requires its `.sha256` sidecar, validates the Windows executable header and SHA-256 digest, then verifies the digest again immediately before applying it. An external PowerShell helper waits for ForgeFlow to exit, installs or replaces the executable and restarts it. -Publish a verified binary release after pushing its source commit: +`Publish-ForgeFlow-Release.ps1` now treats source and binaries as one release transaction. By default it pushes the validated source, builds the exact published commit and uploads all four required assets: + +- `ForgeFlow-Setup--win-x64.exe` +- `ForgeFlow-Setup--win-x64.exe.sha256` +- `ForgeFlow-Portable--win-x64.exe` +- `ForgeFlow-Portable--win-x64.exe.sha256` + +Use `-SkipBinaryRelease` only when intentionally publishing source without enabling packaged auto-update. + +When the source was already pushed without a binary release, run the recovery publisher from Windows: ```powershell -npm run dist:win -$env:FORGEFLOW_USER_DATA = "$env:APPDATA\forgeflow" -npm run release:binary +Set-ExecutionPolicy -Scope Process Bypass +.\Publish-Missing-Binary-Release.ps1 -ExpectedVersion 0.9.1 ``` +The recovery script clones the current Gitea branch into a temporary directory, verifies the exact branch commit, runs the complete quality gate, builds both Windows artifacts and creates or repairs the matching Gitea release. It uses the encrypted Gitea token already stored by ForgeFlow. + Every platform build finishes by removing ForgeFlow artifacts for older versions from `dist`. The unpacked application directory and builder diagnostics are kept. -The publisher refuses to upload when local `HEAD` differs from `origin/main`. Users on 0.8.1 or older need one manual 0.8.2 installation because those versions deliberately disabled packaged updates. +The binary publisher refuses to upload when local `HEAD` differs from the configured Gitea branch. ForgeFlow 0.8.9 and 0.9.0 queried Gitea attachment metadata as though it were the executable. Those versions require one manual 0.9.1 installer run. From 0.9.1 onward, the updater follows the release asset browser download URL and in-app updates work normally. ## Publishing a release from Downloads @@ -60,6 +70,6 @@ Set-ExecutionPolicy -Scope Process Bypass .\Publish-ForgeFlow-Release.ps1 ``` -The script installs dependencies, runs the complete quality gate, clones `git@gitea.itworx.tech:Jens/ForgeFlow.git` into a temporary folder, mirrors the validated source without `.git`, `node_modules`, `dist` or release archives, commits it and pushes `main`. It then compares local `HEAD` with `git ls-remote` and fails if Gitea does not report the exact release commit. +The script installs dependencies, runs the complete quality gate, clones `git@gitea.itworx.tech:Jens/ForgeFlow.git` into a temporary folder, mirrors the validated source without `.git`, `node_modules`, `dist` or release archives, commits it and pushes `main`. It then compares local `HEAD` with `git ls-remote`, builds the exact published checkout and uploads the installer, portable executable and both checksums to the matching Gitea release. Publication fails when either the source commit or any required binary asset cannot be verified. Keep the currently installed older ForgeFlow source folder untouched until the built-in updater test is complete. diff --git a/package-lock.json b/package-lock.json index 0a9892b..ef6a5bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "forgeflow", - "version": "0.9.0", + "version": "0.9.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "forgeflow", - "version": "0.9.0", + "version": "0.9.1", "dependencies": { "ssh2": "1.17.0" }, diff --git a/package.json b/package.json index d58947f..419c52b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "forgeflow", - "version": "0.9.0", + "version": "0.9.1", "private": true, "description": "Desktop release cockpit for local Git, Gitea Actions and controlled exact-commit deployments.", "main": "main.cjs", @@ -63,7 +63,9 @@ "docs/RELEASE_NOTES_0.4.5.md", "docs/RELEASE_NOTES_0.5.0.md", "docs/RELEASE_NOTES_0.5.1.md", + "PUBLISH-AND-ENABLE-UPDATE.cmd", "Publish-ForgeFlow-Release.ps1", + "Publish-Missing-Binary-Release.ps1", "docs/RELEASE_NOTES_0.5.2.md", "docs/RELEASE_NOTES_0.5.3.md", "docs/RELEASE_NOTES_0.5.4.md", @@ -83,6 +85,7 @@ "docs/RELEASE_NOTES_0.8.8.md", "docs/RELEASE_NOTES_0.8.9.md", "docs/RELEASE_NOTES_0.9.0.md", + "docs/RELEASE_NOTES_0.9.1.md", "docs/ACCEPTANCE.md" ], "asarUnpack": [ diff --git a/scripts/publish-binary-release.cjs b/scripts/publish-binary-release.cjs index 71996bf..f752d0e 100644 --- a/scripts/publish-binary-release.cjs +++ b/scripts/publish-binary-release.cjs @@ -11,6 +11,14 @@ const configuredUserData = path.join(app.getPath("appData"), "forgeflow"); app.setPath("userData", path.resolve(configuredUserData)); +function safeRepositoryPart(value, label) { + const text = String(value || "").trim(); + if (!/^[a-zA-Z0-9_.-]+$/.test(text)) { + throw new Error(`${label} contains unsupported characters.`); + } + return text; +} + async function api(baseUrl, token, pathname, options = {}) { const response = await fetch(`${baseUrl}/api/v1${pathname}`, { ...options, @@ -28,10 +36,11 @@ async function api(baseUrl, token, pathname, options = {}) { } catch { data = text; } - if (!response.ok) + if (!response.ok) { throw new Error( `Gitea returned HTTP ${response.status}: ${data?.message || text || response.statusText}`, ); + } return data; } @@ -40,16 +49,32 @@ app.whenReady().then(async () => { const manifest = JSON.parse( await fs.readFile(path.join(root, "package.json"), "utf8"), ); - const config = JSON.parse( - await fs.readFile( - path.join(configuredUserData, "forgeflow-config.json"), - "utf8", - ), - ); + const configPath = path.join(configuredUserData, "forgeflow-config.json"); + const config = JSON.parse(await fs.readFile(configPath, "utf8")); + if (!config?.gitea?.encryptedToken) { + throw new Error( + `No encrypted Gitea token was found in ${configPath}. Sign in to Gitea once from ForgeFlow first.`, + ); + } const token = safeStorage.decryptString( Buffer.from(config.gitea.encryptedToken, "base64"), ); - const baseUrl = String(config.gitea.baseUrl).replace(/\/+$/, ""); + const baseUrl = String(config.gitea.baseUrl || "").replace(/\/+$/, ""); + if (!/^https?:\/\//i.test(baseUrl)) { + throw new Error("The configured Gitea base URL is invalid."); + } + const owner = safeRepositoryPart( + process.env.FORGEFLOW_RELEASE_OWNER || config.updates?.owner || "Jens", + "Release repository owner", + ); + const repo = safeRepositoryPart( + process.env.FORGEFLOW_RELEASE_REPO || config.updates?.repo || "ForgeFlow", + "Release repository name", + ); + const branch = safeRepositoryPart( + process.env.FORGEFLOW_RELEASE_BRANCH || config.updates?.branch || "main", + "Release branch", + ); const version = manifest.version; const tag = `v${version}`; const commit = execFileSync("git", ["rev-parse", "HEAD"], { @@ -58,13 +83,16 @@ app.whenReady().then(async () => { }).trim(); const remote = execFileSync( "git", - ["ls-remote", "origin", "refs/heads/main"], + ["ls-remote", "origin", `refs/heads/${branch}`], { cwd: root, encoding: "utf8" }, ) .trim() .split(/\s+/)[0]; - if (commit !== remote) - throw new Error("Local HEAD is not the published origin/main commit."); + if (commit !== remote) { + throw new Error( + `Local HEAD is not the published origin/${branch} commit. Push the exact source before publishing binaries.`, + ); + } const notesPath = path.join(root, "docs", `RELEASE_NOTES_${version}.md`); const body = await fs.readFile(notesPath, "utf8"); let release; @@ -72,22 +100,27 @@ app.whenReady().then(async () => { release = await api( baseUrl, token, - `/repos/Jens/ForgeFlow/releases/tags/${encodeURIComponent(tag)}`, + `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/tags/${encodeURIComponent(tag)}`, ); } catch (error) { if (!/HTTP 404/.test(error.message)) throw error; - release = await api(baseUrl, token, "/repos/Jens/ForgeFlow/releases", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - tag_name: tag, - target_commitish: commit, - name: `ForgeFlow ${version}`, - body, - draft: false, - prerelease: false, - }), - }); + release = await api( + baseUrl, + token, + `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + tag_name: tag, + target_commitish: commit, + name: `ForgeFlow ${version}`, + body, + draft: false, + prerelease: false, + }), + }, + ); } const binaries = [ @@ -115,7 +148,7 @@ app.whenReady().then(async () => { await api( baseUrl, token, - `/repos/Jens/ForgeFlow/releases/${release.id}/assets/${existing.id}`, + `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/${release.id}/assets/${existing.id}`, { method: "DELETE" }, ); } @@ -124,7 +157,7 @@ app.whenReady().then(async () => { const uploaded = await api( baseUrl, token, - `/repos/Jens/ForgeFlow/releases/${release.id}/assets?name=${encodeURIComponent(name)}`, + `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/${release.id}/assets?name=${encodeURIComponent(name)}`, { method: "POST", body: form, @@ -139,7 +172,7 @@ app.whenReady().then(async () => { } } console.log( - `PASS ForgeFlow ${version} binary release published for ${commit.slice(0, 7)}`, + `PASS ForgeFlow ${version} binary release published to ${owner}/${repo} for ${commit.slice(0, 7)}`, ); app.exit(0); } catch (error) { diff --git a/scripts/verify.mjs b/scripts/verify.mjs index 9de49e8..4007897 100644 --- a/scripts/verify.mjs +++ b/scripts/verify.mjs @@ -72,6 +72,7 @@ const required = [ "docs/RELEASE_NOTES_0.8.8.md", "docs/RELEASE_NOTES_0.8.9.md", "docs/RELEASE_NOTES_0.9.0.md", + "docs/RELEASE_NOTES_0.9.1.md", "docs/UPDATING.md", "docs/DIAGNOSTICS.md", "docs/DEPLOYMENT_SETUP.md", @@ -110,9 +111,9 @@ for (const file of required) await access(path.join(root, file)); const packageJson = JSON.parse( await readFile(path.join(root, "package.json"), "utf8"), ); -if (packageJson.version !== "0.9.0") +if (packageJson.version !== "0.9.1") throw new Error( - `Expected package version 0.9.0, got ${packageJson.version}.`, + `Expected package version 0.9.1, got ${packageJson.version}.`, ); const sourceManifest = await readFile( path.join(root, "SOURCE_MANIFEST.txt"), @@ -362,6 +363,24 @@ for (const phrase of [ ]) { if (!release090.includes(phrase)) throw new Error(`0.9.0 release notes are missing: ${phrase}`); } + +const release091 = await readFile(path.join(root, "docs/RELEASE_NOTES_0.9.1.md"), "utf8"); +for (const phrase of [ + "browser_download_url", + "cross-origin", + "manual installer", + "in-app updates", +]) { + if (!release091.includes(phrase)) throw new Error(`0.9.1 release notes are missing: ${phrase}`); +} +const giteaUpdateSource = await readFile(path.join(root, "src/main/gitea-service.cjs"), "utf8"); +for (const phrase of [ + "browser_download_url", + "insecure cross-origin", + "downloadReleaseAsset", +]) { + if (!giteaUpdateSource.includes(phrase)) throw new Error(`0.9.1 updater repair is missing: ${phrase}`); +} const gitSource = await readFile( path.join(root, "src/main/git-service.cjs"), "utf8", diff --git a/src/main/gitea-service.cjs b/src/main/gitea-service.cjs index ea0a159..a020d20 100644 --- a/src/main/gitea-service.cjs +++ b/src/main/gitea-service.cjs @@ -325,13 +325,15 @@ class GiteaService { const token = this.store.getToken(); let target = new URL(url, `${baseUrl}/`); for (let redirects = 0; redirects <= 5; redirects += 1) { - if (target.origin !== base.origin) + const sameOrigin = target.origin === base.origin; + if (!sameOrigin && target.protocol !== "https:") { throw new Error( - "Refusing to send the Gitea token to a different origin.", + "Refusing an insecure cross-origin update download redirect.", ); + } const response = await fetch(target, { headers: { - Authorization: `token ${token}`, + ...(sameOrigin && token ? { Authorization: `token ${token}` } : {}), Accept: "application/octet-stream", }, signal: AbortSignal.timeout(timeout), @@ -360,8 +362,20 @@ class GiteaService { throw new Error("Gitea returned an invalid release ID."); if (!Number.isSafeInteger(numericId) || numericId <= 0) throw new Error("Gitea returned an invalid release asset ID."); - const assetPath = `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/${numericReleaseId}/assets/${numericId}`; - return this.downloadAuthenticated(assetPath, options); + + let downloadUrl = String(options.downloadUrl || "").trim(); + if (!downloadUrl) { + const metadataPath = `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/${numericReleaseId}/assets/${numericId}`; + const metadata = (await this.request(metadataPath)).data; + if (Number(metadata?.id) !== numericId) { + throw new Error("Gitea returned metadata for a different release asset."); + } + downloadUrl = String(metadata?.browser_download_url || "").trim(); + } + if (!downloadUrl) { + throw new Error("Gitea did not provide a release asset download URL."); + } + return this.downloadAuthenticated(downloadUrl, options); } async dispatchWorkflow({ owner, repo, workflowFile, ref, inputs = {} }) { diff --git a/src/main/update-service.cjs b/src/main/update-service.cjs index c5cfa3e..0982ab7 100644 --- a/src/main/update-service.cjs +++ b/src/main/update-service.cjs @@ -279,7 +279,7 @@ class UpdateService { )); if (!release || release.draft || release.prerelease) { const error = new Error( - `ForgeFlow ${update.remoteVersion} has no published binary release yet.`, + `ForgeFlow ${update.remoteVersion} has no published binary release yet. The source branch was updated, but the matching Windows installer/portable assets were not published. Run Publish-Missing-Binary-Release.ps1 from the release source or publish the four required assets in Gitea.`, ); error.code = "BINARY_RELEASE_NOT_FOUND"; throw error; @@ -305,18 +305,29 @@ class UpdateService { update.repo, release.id, asset.id, + { downloadUrl: asset.browser_download_url }, ), this.gitea.downloadReleaseAsset( update.owner, update.repo, release.id, checksumAsset.id, + { downloadUrl: checksumAsset.browser_download_url }, ), ]); if (binary.length < 1_000_000 || binary[0] !== 0x4d || binary[1] !== 0x5a) { - throw new Error( - "The downloaded Windows update is not a valid executable.", + const preview = binary.subarray(0, 200).toString("utf8").trim(); + const looksLikeMetadata = + /^\s*[{[]/.test(preview) || /browser_download_url/i.test(preview); + const error = new Error( + looksLikeMetadata + ? "Gitea returned release-asset metadata instead of the Windows executable. Upgrade ForgeFlow with the 0.9.1 installer once; later in-app updates use the actual browser download URL." + : "The downloaded Windows update is not a valid executable.", ); + error.code = looksLikeMetadata + ? "RELEASE_ASSET_METADATA_RECEIVED" + : "INVALID_WINDOWS_UPDATE"; + throw error; } const expectedSha256 = checksumBytes .toString("utf8") diff --git a/tests/gitea-actions.test.mjs b/tests/gitea-actions.test.mjs index fb5076d..e05e414 100644 --- a/tests/gitea-actions.test.mjs +++ b/tests/gitea-actions.test.mjs @@ -108,18 +108,32 @@ test('creates controlled pull requests and reads branch protection', async () => await assert.rejects(() => service.createPullRequest({ owner: 'owner', repo: 'app', head: 'main', base: 'main', title: 'Invalid' }), /different/); }); -test('downloads release assets through the release-scoped Gitea endpoint', async () => { +test('resolves release attachment metadata before downloading the actual asset', async () => { const service = new GiteaService(makeStore()); + let metadataPath = ''; let requested = ''; + service.request = async (pathname) => { + metadataPath = pathname; + return { + data: { + id: 412, + browser_download_url: 'https://gitea.example.test/attachments/release.exe', + }, + }; + }; service.downloadAuthenticated = async (pathname) => { requested = pathname; return Buffer.from('asset'); }; const asset = await service.downloadReleaseAsset('Jens', 'ForgeFlow', 107, 412); assert.equal(asset.toString(), 'asset'); + assert.equal( + metadataPath, + '/repos/Jens/ForgeFlow/releases/107/assets/412', + ); assert.equal( requested, - '/api/v1/repos/Jens/ForgeFlow/releases/107/assets/412', + 'https://gitea.example.test/attachments/release.exe', ); await assert.rejects( () => service.downloadReleaseAsset('Jens', 'ForgeFlow', null, 412), @@ -127,6 +141,21 @@ test('downloads release assets through the release-scoped Gitea endpoint', async ); }); +test('uses a release-provided browser download URL without requesting metadata again', async () => { + const service = new GiteaService(makeStore()); + service.request = async () => { throw new Error('metadata lookup should not run'); }; + let requested = ''; + service.downloadAuthenticated = async (pathname) => { + requested = pathname; + return Buffer.from('asset'); + }; + const asset = await service.downloadReleaseAsset('Jens', 'ForgeFlow', 107, 412, { + downloadUrl: 'https://gitea.example.test/attachments/direct.exe', + }); + assert.equal(asset.toString(), 'asset'); + assert.equal(requested, 'https://gitea.example.test/attachments/direct.exe'); +}); + test('creates conservative default branch protection rules', async () => { const service = new GiteaService(makeStore()); let request = null; diff --git a/tests/update-service.test.mjs b/tests/update-service.test.mjs index f2468bb..4060184 100644 --- a/tests/update-service.test.mjs +++ b/tests/update-service.test.mjs @@ -242,6 +242,11 @@ test("release publisher verifies Gitea and bootstraps the installed updater serv assert.match(script, /package-lock\.json/); assert.match(script, /non-reproducible update/); assert.match(script, /npm run check/); + assert.match(script, /npm run dist:win/); + assert.match(script, /npm run release:binary/); + assert.match(script, /SkipBinaryRelease/); + assert.match(script, /ForgeFlow-Setup-\$version-win-x64\.exe/); + assert.match(script, /ForgeFlow-Portable-\$version-win-x64\.exe/); assert.match(script, /git ls-remote origin/); assert.match(script, /publishedCommit -ne \$localCommit/); assert.match(script, /scripts\\apply-source-update\.ps1/); @@ -255,6 +260,57 @@ test("release publisher verifies Gitea and bootstraps the installed updater serv assert.doesNotMatch(script, /Copy-Item[^\n]+package\.json/); }); +test("one-click Windows release wrapper invokes the atomic publisher", async () => { + const script = await readFile( + new URL("../PUBLISH-AND-ENABLE-UPDATE.cmd", import.meta.url), + "utf8", + ); + assert.match(script, /ExecutionPolicy Bypass/); + assert.match(script, /Publish-ForgeFlow-Release\.ps1/); + assert.match(script, /older ForgeFlow updater can now install/); + assert.match(script, /exit \/b %forgeflowExitCode%/); +}); + +test("missing binary release recovery script builds the exact Gitea commit and uploads all assets", async () => { + const script = await readFile( + new URL("../Publish-Missing-Binary-Release.ps1", import.meta.url), + "utf8", + ); + assert.match(script.trimStart(), /^param\(/); + assert.match(script, /git clone --branch \$Branch --single-branch/); + assert.match(script, /git -C \$clone ls-remote origin/); + assert.match(script, /npm ci --no-audit --no-fund/); + assert.match(script, /npm run check/); + assert.match(script, /npm run dist:win/); + assert.match(script, /npm run release:binary/); + assert.match(script, /FORGEFLOW_USER_DATA/); + assert.match(script, /ForgeFlow-Setup-\$version-win-x64\.exe/); + assert.match(script, /ForgeFlow-Portable-\$version-win-x64\.exe/); +}); + +test("binary publisher derives repository coordinates from ForgeFlow settings", async () => { + const script = await readFile( + new URL("../scripts/publish-binary-release.cjs", import.meta.url), + "utf8", + ); + assert.match(script, /config\.updates\?\.owner/); + assert.match(script, /config\.updates\?\.repo/); + assert.match(script, /config\.updates\?\.branch/); + assert.match(script, /encodeURIComponent\(owner\)/); + assert.match(script, /encodeURIComponent\(repo\)/); + assert.doesNotMatch(script, /\/repos\/Jens\/ForgeFlow\/releases/); +}); + + +test("packaged updater passes Gitea browser download URLs to the asset downloader", async () => { + const source = await readFile( + new URL("../src/main/update-service.cjs", import.meta.url), + "utf8", + ); + assert.match(source, /downloadUrl: asset\.browser_download_url/); + assert.match(source, /downloadUrl: checksumAsset\.browser_download_url/); + assert.match(source, /RELEASE_ASSET_METADATA_RECEIVED/); +}); test("PowerShell helper replaces an existing launching status with a Windows-safe file API", async () => { const script = await readFile( new URL("../scripts/apply-source-update.ps1", import.meta.url), @@ -354,8 +410,14 @@ test("packaged updater downloads only a published checksum-matched Windows asset ], }; }, - async downloadReleaseAsset(_owner, _repo, releaseId, assetId) { + async downloadReleaseAsset(_owner, _repo, releaseId, assetId, options) { assert.equal(releaseId, 82); + assert.equal( + options.downloadUrl, + assetId === 42 + ? "http://wrong-origin.test/checksum" + : "http://wrong-origin.test/setup", + ); return assetId === 42 ? Buffer.from(`${sha256} ${assetName}\n`) : binary; }, };