diff --git a/CHANGELOG.md b/CHANGELOG.md index e0304c4..132f0d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.8.8 - 2026-07-26 + +- fixed binary update downloads on Gitea servers that require release-scoped attachment routes; +- sends both the immutable release ID and attachment ID when downloading update assets; +- preserves strict same-origin token handling and SHA-256 verification. + ## 0.8.7 - 2026-07-26 - automatically inventories running workloads across configured Unraid servers; diff --git a/SOURCE_MANIFEST.txt b/SOURCE_MANIFEST.txt index 649e17b..88013d2 100644 --- a/SOURCE_MANIFEST.txt +++ b/SOURCE_MANIFEST.txt @@ -1,4 +1,4 @@ -ForgeFlow 0.8.7 source manifest +ForgeFlow 0.8.8 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 -feac8256bec2762533c6dd76a49fceaeafb4935528e406158598e8ba55500a6e 10277 CHANGELOG.md +b5ebb381645c9a7e9de693ff30c563819b70182931057727dc6e48db551adf9f 10557 CHANGELOG.md 21cb96e7afe71b1dc791c818dedd244d92f9a6ed4d9ffbb3022ccb187e1bdf0f 852 docs/ACCEPTANCE.md a17f95d96d3c9fbc69d870874e6fbb7472091adefc454b24f835db1279511d72 8296 docs/ARCHITECTURE.md 30a92bcf5daadb019efa2f82cb820ea302490dd1d68fb772674dc3faccd3e594 2045 docs/DEPLOYMENT_SETUP.md @@ -45,6 +45,7 @@ c2802fa5dbff392c846b82b55e84a8bfb8e1625546fd1eba39f7129318bece96 654 b516db97a0353babc810c24a87a971d30d72a8021d809e6b833ff7ae0458f442 538 docs/RELEASE_NOTES_0.8.5.md 838d196f3fbbfeee8df375a0502107f56b6df28babca30aa09ef1c7aa5196d09 738 docs/RELEASE_NOTES_0.8.6.md ef049adcfa204908e6dc3a059124b39ba0e2739cc54e38945ce73a57049df0d8 1185 docs/RELEASE_NOTES_0.8.7.md +7eedb25e1aae3b06a04bb9b2f4843bd6af614418737e600b4bdc9161edabd76a 632 docs/RELEASE_NOTES_0.8.8.md 2b631b9d6d973bdd70869d84886ff339da351e29e17598970b3b27915674661d 4175 docs/ROADMAP.md 1ccde232c060395d7aedce27e89a7647b77afe28ab71de0a5a3efeded57369d3 140415 docs/screenshots/deploy-confirmation.png b39506254ffa2c73c389fb4795b3a745368bbeb7d8514cc47a636316d6d9a6aa 107166 docs/screenshots/deployment-run.png @@ -68,8 +69,8 @@ c230b931abf2293d2d44b7a69b94c35f1142c093cc46b88739a0de5cbd6d1896 1532 4a561ead5ba7cdfaf4efce91842a4308c5f2a77980205879d83835efb8a579db 1067 LICENSE 0d9f0ac837b4d3b39476d9bcbb57fb3e12d6b7084bb1f7435401e416cff1854f 12920 main.cjs 91a984a89dd57a084b9a2331763cacdb061582fb590f13df379d92c1a77a2ee1 352 OVERLAY-INSTRUCTIONS.md -5610ef8f25cc47b5296c57edb29d5e1b605de4113b4b0128fa451b9ec16bba2c 130466 package-lock.json -faae3af578a40306472494f5816855bf9422425b6daf39732fe04cbad55675ae 3869 package.json +dc26585558f060c74a11bc7033cf1f22c626e91668a6349a06a928114ecf417d 130466 package-lock.json +e3d60b3adde674e5aa80cc7776db3efbc0ced66d49e89490cb9fbb6786c0db70 3906 package.json 45eea5fe4fd58cc0e96497295223e81a4f3acc3778bf546d4c7c96af03da4d26 9496 preload.cjs b31c43d9355c13b5ae4efc0f3649d8cb8d509b2bb7ebb042ff546b7820fb7de8 8411 Publish-ForgeFlow-Release.ps1 a6d32a742412b7836606be00f17be0465f1b6f55d3911f6c73a14029787ba206 14037 README.md @@ -82,7 +83,7 @@ f8359a69d20deb2dfe10042d1bec7b12a95e76e58e36bc5f265f073c3111d056 10287 74433d8a6b24afe368197a469e2fe0c5050c239d7250b84c2f3f598c304778b0 4736 scripts/publish-binary-release.cjs 444b397d515d65a7ee59d3088cba869cbb812d2b8cc18fc5d255105e3edb58c2 1468 scripts/serve-demo.mjs 42203f9e0fd4aae517284d387f265cf1b0b180379bc253a092b5c3c5c4caef0a 2992 scripts/validate-installed-connections.cjs -e77edf02fc9b9afb398ae46767e634f47259e58288ce9a18bc696b028234b581 12341 scripts/verify.mjs +1a0931346c89d2d49b736a25aa58c5d29fbdf4acd593ea3614dba904ec462843 12374 scripts/verify.mjs 0079701b5acbfef07b71a9623613d1940805ccd20649d77e3f34c37e79df7655 735 scripts/write-release-checksums.mjs 619515f524cb89960370ffcbd3fafd3c0e178b95f69c5868b1dd44777f23ec1e 2081 setup-windows.ps1 dd613d04b366f2cd071a1685a414016a5fb008082ed1b4cb8b24b79c100f640a 2412 src/main/audit-service.cjs @@ -92,7 +93,7 @@ a381848a296c28f6d14093c96f722967acf9c994ffb867d54dd92bf5ada2729b 23648 c157640e76d558906a9aa9881eda811196623ef1c65fa3467f32f0f84b0ddd0c 15095 src/main/diagnostics-service.cjs a2ef47d5330095b92c2bd22fcc39962091881f9cb60d02e261eb1dd1bd693170 1974 src/main/external-tools-service.cjs 0b7476c2cfe1872601978c20a466c20fe58be35e81b2303e38a753fea62bbc27 32548 src/main/git-service.cjs -75f25fa8ee520b03b0bbe4c4cea439853202d318547343cda49a19c6ab868901 16984 src/main/gitea-service.cjs +6ee0c645521c20043708595d1a9dd0fe7642fc300be1306effd7e1142eebcfb7 17201 src/main/gitea-service.cjs 13f6521bb82aa5a3399d6839ea7563459b19969c1e5af7525e4116cf771282e9 46526 src/main/ipc.cjs 62f2c80c8210e19370b8556b1f296cbae50dae6b758a39e209f8fb461691fd4c 4235 src/main/log-redaction.cjs 958595a99fb242c127f475f3d8622bdba4c07b2d658703f69fe3992227a9107e 12909 src/main/preflight-service.cjs @@ -101,14 +102,14 @@ e89b54e7e3174b4b0a1dcd9058d8344e29431f9d16d0e6bb8d11559b691440a0 2508 17e2a53f61cd7faba461b9f332967143087eaac95b72001462292976278ca305 7782 src/main/repository-service.cjs b31a63bf8cb1807b3e838e2bf8a0e742738f119d13de8ca9f42e471f072217d3 8328 src/main/ssh-service.cjs f4e221edace12e2120ec439e0f91274de110872c5bfbe968f2d7935eeb7193c4 78544 src/main/unraid-deployment-service.cjs -27b9e15dd6530bc7bfab51414430eaef169e0cc4acd027257b1c63f891af8c85 20733 src/main/update-service.cjs +45e65564e1e8b9db487dc6dda03a752c51130189f23ec3b1260dc62e3621a925 20806 src/main/update-service.cjs 00b5c44b7d04452498288d42a2c345e0e3922c8ddfabfee9eb04654f238811d5 197015 src/renderer/app.js 16efd2fca83004f781eae40ae0f706a004ce0bddf338dd087b8adf7eb10c1d84 85704 src/renderer/assets/itworx-mark.png 813b8cdeecac43794166f3db9d3c5d2c441e0292f9ab7bd465ba136d6201e95d 82476 src/renderer/assets/itworx-wordmark-dark.png 094c1b71cc2482a9db250ac175f45f3de68f53277dfbde371a03e61923d00988 75240 src/renderer/assets/itworx-wordmark-light.png 813b8cdeecac43794166f3db9d3c5d2c441e0292f9ab7bd465ba136d6201e95d 82476 src/renderer/assets/itworx-wordmark.png e1c463d6cda9f2b9b78c468845c0a7e8688f0362be5642074a1a5f7122dfe811 762 src/renderer/index.html -3faf63826ee693b1afcfcc632238dd71e7969641e2502ef6884b40f3855e9d6b 51166 src/renderer/mock-bridge.js +2b68194e9ca0caab1ea2d1e5f7222a42f962ea16f3dd9517a7a13af811963969 51166 src/renderer/mock-bridge.js e30ea37ee00f34110d0d252c79f9d8e0bb127c4db0bd1bf8b374609dea433361 72160 src/renderer/styles.css 0a1e9d9d6cd4d190eb7f85dbc6668d80600b1cf2749cc0c2c51cc428f506f20d 1121 src/shared/clone-target.cjs 5d425d5c2f939d0f6beebee7ebb0c77146cb7e318535ba7286ec7081a4dc2269 2497 src/shared/deployment-policy.cjs @@ -132,7 +133,7 @@ dd121d96ca265a027cd415a52064500a4541b2f8a662f4f4b25f2f996d52b5da 762 e7aebcc0d484a6a59d463d5cb26c11b3ad56e28f6535e7c38a0fe166a41565ea 13690 tests/git-integration.test.mjs 5ea94c6b241a02060d531fad94e449eecd3772eed2137581d4e2babfb09e56db 1239 tests/git-status.test.mjs 681ab7bcd02c4dd98d1d8d2092a3521c489d941131e7ffe5903971b940046474 2403 tests/git-workflows.test.mjs -52b96f0a6623778fbdc1e8dbfc892e1d77a3d6616058cd7b08d532b207aa5719 5531 tests/gitea-actions.test.mjs +e9c45a70635adea853a0a420caabd5ba465070296d2e73ffbb947d4ade52ed46 6165 tests/gitea-actions.test.mjs 267d76b868d8d06ea031c14acd09a7715fb44668a25ade51a9e62e0170888bc8 1522 tests/ipc-contract.test.mjs caf98cbd9de9b119dae610ee53fa333a7a11214f34762247452fbb85e8bbf725 2392 tests/log-redaction.test.mjs 96432a97d313f331694900bf0a2c21e38c20eac96d59147977aeed9055a9e3ad 2287 tests/partial-staging.test.mjs @@ -146,7 +147,7 @@ bab853feb0e22aa25af17989baaa632c01efa636533ea67407fecfdd973c7024 627 020eccfa9c4aef7a4ac4736d9af90518fcb6d1ad75aedcfaa1c92832a9e3d6d8 4609 tests/shell-verification.test.mjs 8a6a8477eb94b85ccef18cddd2640afb0d1eafa679c96bc7de20428d5d69e1be 1794 tests/tool-invocation.test.mjs 8f44579de3af7d7d23d55572d1d97cb0690425d4d9dffc30c6bf2ec3ff9c54eb 24553 tests/unraid-deployment.test.mjs -f1f0f13ac41f47c9df8ef778d7b98c09a589f61e6f76e14bc0e8943c5ede7560 14990 tests/update-service.test.mjs +11d6e6329f775617a1ce3657d0454cc97d7bcf3d9759f9a5a623c9d18e13d03e 15118 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/docs/RELEASE_NOTES_0.8.8.md b/docs/RELEASE_NOTES_0.8.8.md new file mode 100644 index 0000000..bc00cf6 --- /dev/null +++ b/docs/RELEASE_NOTES_0.8.8.md @@ -0,0 +1,13 @@ +# ForgeFlow 0.8.8 + +ForgeFlow 0.8.8 fixes the HTTP 404 returned while downloading packaged updates +from the configured Gitea server. + +The server's API requires release attachments to be addressed using both the +immutable release ID and attachment ID. ForgeFlow now uses that exact +release-scoped endpoint for the executable and its checksum file. + +Strict same-origin token protection and SHA-256 verification remain unchanged. +Install 0.8.8 manually when upgrading from 0.8.7 because the affected download +code runs before the corrected updater can be installed. Future packaged +updates can again be completed from inside ForgeFlow. diff --git a/package-lock.json b/package-lock.json index 7ed5f2f..5b524ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "forgeflow", - "version": "0.8.7", + "version": "0.8.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "forgeflow", - "version": "0.8.7", + "version": "0.8.8", "dependencies": { "ssh2": "1.17.0" }, diff --git a/package.json b/package.json index 324c96c..ad24d02 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "forgeflow", - "version": "0.8.7", + "version": "0.8.8", "private": true, "description": "Desktop release cockpit for local Git, Gitea Actions and controlled exact-commit deployments.", "main": "main.cjs", @@ -80,6 +80,7 @@ "docs/RELEASE_NOTES_0.8.5.md", "docs/RELEASE_NOTES_0.8.6.md", "docs/RELEASE_NOTES_0.8.7.md", + "docs/RELEASE_NOTES_0.8.8.md", "docs/ACCEPTANCE.md" ], "asarUnpack": [ diff --git a/scripts/verify.mjs b/scripts/verify.mjs index ec369e1..a9d0004 100644 --- a/scripts/verify.mjs +++ b/scripts/verify.mjs @@ -68,6 +68,7 @@ const required = [ "docs/RELEASE_NOTES_0.8.5.md", "docs/RELEASE_NOTES_0.8.6.md", "docs/RELEASE_NOTES_0.8.7.md", + "docs/RELEASE_NOTES_0.8.8.md", "docs/UPDATING.md", "docs/DIAGNOSTICS.md", "docs/DEPLOYMENT_SETUP.md", @@ -106,9 +107,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.8.7") +if (packageJson.version !== "0.8.8") throw new Error( - `Expected package version 0.8.7, got ${packageJson.version}.`, + `Expected package version 0.8.8, got ${packageJson.version}.`, ); const sourceManifest = await readFile( path.join(root, "SOURCE_MANIFEST.txt"), diff --git a/src/main/gitea-service.cjs b/src/main/gitea-service.cjs index 9bd4047..17423c4 100644 --- a/src/main/gitea-service.cjs +++ b/src/main/gitea-service.cjs @@ -331,11 +331,14 @@ class GiteaService { throw new Error("The update download exceeded the redirect limit."); } - async downloadReleaseAsset(owner, repo, assetId, options = {}) { + async downloadReleaseAsset(owner, repo, releaseId, assetId, options = {}) { + const numericReleaseId = Number(releaseId); const numericId = Number(assetId); + if (!Number.isSafeInteger(numericReleaseId) || numericReleaseId <= 0) + 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/assets/${numericId}`; + const assetPath = `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/${numericReleaseId}/assets/${numericId}`; return this.downloadAuthenticated(assetPath, options); } diff --git a/src/main/update-service.cjs b/src/main/update-service.cjs index 9546631..c5cfa3e 100644 --- a/src/main/update-service.cjs +++ b/src/main/update-service.cjs @@ -300,10 +300,16 @@ class UpdateService { } const [binary, checksumBytes] = await Promise.all([ - this.gitea.downloadReleaseAsset(update.owner, update.repo, asset.id), this.gitea.downloadReleaseAsset( update.owner, update.repo, + release.id, + asset.id, + ), + this.gitea.downloadReleaseAsset( + update.owner, + update.repo, + release.id, checksumAsset.id, ), ]); diff --git a/src/renderer/mock-bridge.js b/src/renderer/mock-bridge.js index a5e782b..3aa323a 100644 --- a/src/renderer/mock-bridge.js +++ b/src/renderer/mock-bridge.js @@ -564,7 +564,7 @@ await wait(80); snapshot(); return { - appVersion: "0.8.7-demo", + appVersion: "0.8.8-demo", platform: "win32", state: clone(state), git: { available: true, version: "git version 2.47.3" }, diff --git a/tests/gitea-actions.test.mjs b/tests/gitea-actions.test.mjs index 1c4dc80..91e0c5a 100644 --- a/tests/gitea-actions.test.mjs +++ b/tests/gitea-actions.test.mjs @@ -107,3 +107,22 @@ test('creates controlled pull requests and reads branch protection', async () => assert.deepEqual(create.options.body, { head: 'feature', base: 'main', title: 'Release feature', body: 'Summary' }); 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 () => { + const service = new GiteaService(makeStore()); + let requested = ''; + 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( + requested, + '/api/v1/repos/Jens/ForgeFlow/releases/107/assets/412', + ); + await assert.rejects( + () => service.downloadReleaseAsset('Jens', 'ForgeFlow', null, 412), + /invalid release ID/, + ); +}); diff --git a/tests/update-service.test.mjs b/tests/update-service.test.mjs index 4dac5e1..f2468bb 100644 --- a/tests/update-service.test.mjs +++ b/tests/update-service.test.mjs @@ -336,6 +336,7 @@ test("packaged updater downloads only a published checksum-matched Windows asset async getReleaseByTag(_owner, _repo, tag) { if (tag !== "v0.8.2") return null; return { + id: 82, tag_name: tag, draft: false, prerelease: false, @@ -353,7 +354,8 @@ test("packaged updater downloads only a published checksum-matched Windows asset ], }; }, - async downloadReleaseAsset(_owner, _repo, assetId) { + async downloadReleaseAsset(_owner, _repo, releaseId, assetId) { + assert.equal(releaseId, 82); return assetId === 42 ? Buffer.from(`${sha256} ${assetName}\n`) : binary; }, }; @@ -398,6 +400,7 @@ test("packaged updater rejects a binary whose checksum does not match", async () gitea: { async getReleaseByTag() { return { + id: 83, tag_name: "v0.8.2", assets: [ { @@ -413,7 +416,8 @@ test("packaged updater rejects a binary whose checksum does not match", async () ], }; }, - async downloadReleaseAsset(_owner, _repo, assetId) { + async downloadReleaseAsset(_owner, _repo, releaseId, assetId) { + assert.equal(releaseId, 83); return assetId === 52 ? Buffer.from(`${"0".repeat(64)} ${assetName}`) : binary;