Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
958d5b84d3 | ||
|
|
8fa4891075 | ||
|
|
58d361bbab | ||
|
|
acad1f8932 |
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
ForgeFlow is een Windows-desktopapp voor wie Git, Gitea en eigen Docker- of Unraid-servers gebruikt. Je ziet in één werkruimte wat lokaal gewijzigd is, wat op Gitea staat en welke exacte commit op de server draait. ForgeFlow begeleidt je daarna veilig door review, commit, push, deployment en verificatie.
|
ForgeFlow is een Windows-desktopapp voor wie Git, Gitea en eigen Docker- of Unraid-servers gebruikt. Je ziet in één werkruimte wat lokaal gewijzigd is, wat op Gitea staat en welke exacte commit op de server draait. ForgeFlow begeleidt je daarna veilig door review, commit, push, deployment en verificatie.
|
||||||
|
|
||||||
> Huidige release: **0.10.3** · [download de laatste Windows-release](https://gitea.itworx.tech/Jens/ForgeFlow/releases/latest)
|
> Huidige release: **0.10.6** · [download de laatste Windows-release](https://gitea.itworx.tech/Jens/ForgeFlow/releases/latest)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
+20
-17
@@ -1,4 +1,4 @@
|
|||||||
ForgeFlow 0.10.3 source manifest
|
ForgeFlow 0.10.6 source manifest
|
||||||
SHA-256 BYTES PATH
|
SHA-256 BYTES PATH
|
||||||
(The manifest excludes itself, dependencies and generated release artifacts.)
|
(The manifest excludes itself, dependencies and generated release artifacts.)
|
||||||
cedceb71eb846d99c7c4019031833c1c7f93b84a1c6073aec7d2435dc744ca3d 703 .gitea/workflows/quality.yml
|
cedceb71eb846d99c7c4019031833c1c7f93b84a1c6073aec7d2435dc744ca3d 703 .gitea/workflows/quality.yml
|
||||||
@@ -36,6 +36,9 @@ f79908fb3dad98c38030c6e6be7c79a1999e0478ed9c2496923891954438daa1 4581
|
|||||||
0eb44bda2209a5979a6ac693ac4cd4d235c0015031e54b9e895990f37bf60054 1433 docs/RELEASE_NOTES_0.10.1.md
|
0eb44bda2209a5979a6ac693ac4cd4d235c0015031e54b9e895990f37bf60054 1433 docs/RELEASE_NOTES_0.10.1.md
|
||||||
8d713471a437a8a55b00d7e1dd95290680862107bc4e586cf27d727f6274e46c 577 docs/RELEASE_NOTES_0.10.2.md
|
8d713471a437a8a55b00d7e1dd95290680862107bc4e586cf27d727f6274e46c 577 docs/RELEASE_NOTES_0.10.2.md
|
||||||
0942fb2c4a4f972296423b5232687f7389e2c6417a9d48a3244beef9dec907b9 1164 docs/RELEASE_NOTES_0.10.3.md
|
0942fb2c4a4f972296423b5232687f7389e2c6417a9d48a3244beef9dec907b9 1164 docs/RELEASE_NOTES_0.10.3.md
|
||||||
|
8f4a0fe6dc250ae210cc2fc1c57c46091822ae6c2a58caa76e0091f255f9f30d 775 docs/RELEASE_NOTES_0.10.4.md
|
||||||
|
05ed618f5a74a854363930128ca98939808517eedd28b9a508660a0c46e91d97 884 docs/RELEASE_NOTES_0.10.5.md
|
||||||
|
94bfb2783c1befad1197e1c5e32fc002222c94a28d70d48360a8d53ecd260d5c 772 docs/RELEASE_NOTES_0.10.6.md
|
||||||
a0c00ff76acd1682bb5e0e8dcf6589c9480da436c9c6d30780a1ed58b4dad94f 1770 docs/RELEASE_NOTES_0.2.0.md
|
a0c00ff76acd1682bb5e0e8dcf6589c9480da436c9c6d30780a1ed58b4dad94f 1770 docs/RELEASE_NOTES_0.2.0.md
|
||||||
5773ead01aa4c522c556295553787482d01b1f5242f053b2c61f120c4de4fa76 5963 docs/RELEASE_NOTES_0.3.0.md
|
5773ead01aa4c522c556295553787482d01b1f5242f053b2c61f120c4de4fa76 5963 docs/RELEASE_NOTES_0.3.0.md
|
||||||
d46de73cf6c4cd5c2ba3f455a7a2af2e0d64ee9d94a97fd1a0bfb44e35c1624a 1093 docs/RELEASE_NOTES_0.3.1.md
|
d46de73cf6c4cd5c2ba3f455a7a2af2e0d64ee9d94a97fd1a0bfb44e35c1624a 1093 docs/RELEASE_NOTES_0.3.1.md
|
||||||
@@ -97,19 +100,19 @@ c230b931abf2293d2d44b7a69b94c35f1142c093cc46b88739a0de5cbd6d1896 1532
|
|||||||
4a561ead5ba7cdfaf4efce91842a4308c5f2a77980205879d83835efb8a579db 1067 LICENSE
|
4a561ead5ba7cdfaf4efce91842a4308c5f2a77980205879d83835efb8a579db 1067 LICENSE
|
||||||
1f0f388df4397e548887bbc7579fd3c864581b86469c01703201ece7a6cbf931 13667 main.cjs
|
1f0f388df4397e548887bbc7579fd3c864581b86469c01703201ece7a6cbf931 13667 main.cjs
|
||||||
91a984a89dd57a084b9a2331763cacdb061582fb590f13df379d92c1a77a2ee1 352 OVERLAY-INSTRUCTIONS.md
|
91a984a89dd57a084b9a2331763cacdb061582fb590f13df379d92c1a77a2ee1 352 OVERLAY-INSTRUCTIONS.md
|
||||||
672410b1af3900733c067cf29ecaffb96a33500ccbd597bebe999e0c06b161e0 179806 package-lock.json
|
32b8a6a210910623eb078956a2f06a33b2649d60333a567e620d46c6ed4bcc0d 179806 package-lock.json
|
||||||
70d4bafa0f9633bfdef297bb2599c90ff224cf10b69134ad74c77ee574ce49ce 5436 package.json
|
681fb8a118b372ad9b62f8940d7c921eb20267c4eaf8a99431370f8d64d39992 5550 package.json
|
||||||
2a597a5704c576783b8a72407fbc377fa7506b36a4596ea7f7bce126e394f837 1326 playwright.config.mjs
|
2a597a5704c576783b8a72407fbc377fa7506b36a4596ea7f7bce126e394f837 1326 playwright.config.mjs
|
||||||
69318fdf054be7aa2fe86ead9847da9da65745d8d5de548c8346f3ba0afc4892 12175 preload.cjs
|
69318fdf054be7aa2fe86ead9847da9da65745d8d5de548c8346f3ba0afc4892 12175 preload.cjs
|
||||||
abe5dd6fd68f2970cd19ef134094907c67219061d8fe9a1a08324c78de4ad437 484 PUBLISH-AND-ENABLE-UPDATE.cmd
|
abe5dd6fd68f2970cd19ef134094907c67219061d8fe9a1a08324c78de4ad437 484 PUBLISH-AND-ENABLE-UPDATE.cmd
|
||||||
f018383f755352ca448e2ebb1e19b1dba412a3eb793d61e64b02953e300754fd 10538 Publish-ForgeFlow-Release.ps1
|
f018383f755352ca448e2ebb1e19b1dba412a3eb793d61e64b02953e300754fd 10538 Publish-ForgeFlow-Release.ps1
|
||||||
688fff7d2c989adb97ebb7fae38962656b70304a0aa5d27433c56adf7f136de0 4196 Publish-Missing-Binary-Release.ps1
|
688fff7d2c989adb97ebb7fae38962656b70304a0aa5d27433c56adf7f136de0 4196 Publish-Missing-Binary-Release.ps1
|
||||||
9e95c2fe6bca120bd2f7502afe9cf9d6a2aa0ee128261cccc955dd9b94ade833 10313 README.md
|
51dff94dcdf3ae86ff1d544bb52eeab2ab51421a51b35c5dbe8f7c9f8d23c50a 10313 README.md
|
||||||
1fa7bf646321e07e40d98f4a7529d5f748c600edd5283f62ee13574b4e97280f 14329 reports/architecture-audit.json
|
8f8dc5ff0c2967563457e4027d795f6f515253e0a459ecca5a9f5bcf0b658870 14650 reports/architecture-audit.json
|
||||||
c1ff18f1367691332b189bb7589843a5e0bbde4817e3dd29df4ade7ea71dbd52 1114 reports/architecture-audit.md
|
5e923f0096895f588fe66b187779cdb7cb5af6644010c6dcc4d55876930ae376 1220 reports/architecture-audit.md
|
||||||
509c7bcff5280349bd9f45ed6151f70372bad7010a9ea582c13e2ccab91fe0cd 6272 scripts/acceptance.mjs
|
509c7bcff5280349bd9f45ed6151f70372bad7010a9ea582c13e2ccab91fe0cd 6272 scripts/acceptance.mjs
|
||||||
00d57bda5af8c8eda294b72d18b318f024a307b81b0d9205a0821f5240151e31 3814 scripts/apply-binary-update.ps1
|
8fe616851e6a089a59fd134905465019c33d26f6408ebd0613fff587b6c589e0 4400 scripts/apply-binary-update.ps1
|
||||||
f8359a69d20deb2dfe10042d1bec7b12a95e76e58e36bc5f265f073c3111d056 10287 scripts/apply-source-update.ps1
|
706b49e001837c91134044d3af132d58fb14955245bc1d5c36b413eb65867002 10486 scripts/apply-source-update.ps1
|
||||||
02e924227f6cad3777fd06660230c85df590d8ce95e134194a4d18970a240b88 4145 scripts/architecture-audit.mjs
|
02e924227f6cad3777fd06660230c85df590d8ce95e134194a4d18970a240b88 4145 scripts/architecture-audit.mjs
|
||||||
4490bed84761f76e1fd87ee3117fe53e82e60b7760329c97d68772d6d820ebae 8521 scripts/audit-installed-deployments.cjs
|
4490bed84761f76e1fd87ee3117fe53e82e60b7760329c97d68772d6d820ebae 8521 scripts/audit-installed-deployments.cjs
|
||||||
6d46dd6826069d842f20f9f22a99042257db936cdea0bee8d294d2d7ea290126 3893 scripts/doctor.mjs
|
6d46dd6826069d842f20f9f22a99042257db936cdea0bee8d294d2d7ea290126 3893 scripts/doctor.mjs
|
||||||
@@ -120,7 +123,7 @@ b83d443f5724ac15393567f3a688aed8315fbe3e5966832c864a9466e0669464 8102
|
|||||||
c76507857292c5713e1c699cf02e24b80265da39af2cecd148034bdb874adbb6 5246 scripts/test-authenticode-chain.ps1
|
c76507857292c5713e1c699cf02e24b80265da39af2cecd148034bdb874adbb6 5246 scripts/test-authenticode-chain.ps1
|
||||||
4393f7dc5f417e6d601a68238f4e26791799a3634acec228fe4d79deaee85eb5 3109 scripts/validate-installed-connections.cjs
|
4393f7dc5f417e6d601a68238f4e26791799a3634acec228fe4d79deaee85eb5 3109 scripts/validate-installed-connections.cjs
|
||||||
e6127e1e62f39c70ddb1abf72f4d7e7b8e3f19ff1f219e1a3660353c2e0cdfac 2411 scripts/verify-release-signatures.mjs
|
e6127e1e62f39c70ddb1abf72f4d7e7b8e3f19ff1f219e1a3660353c2e0cdfac 2411 scripts/verify-release-signatures.mjs
|
||||||
c1763bad24b747051ad18d911ba9c7176882663e3b6ce9921a2f8b1aae056bd7 18156 scripts/verify.mjs
|
85134984297c90a5de812c075d7f75eaa5242d5ee33713c6a4a2fe9703f82be9 19210 scripts/verify.mjs
|
||||||
0b9f03ba3c67ff7cdb2916a902ad8ce25e81a7c90b210e4ae52d2ad029efabf3 2353 scripts/write-release-checksums.mjs
|
0b9f03ba3c67ff7cdb2916a902ad8ce25e81a7c90b210e4ae52d2ad029efabf3 2353 scripts/write-release-checksums.mjs
|
||||||
619515f524cb89960370ffcbd3fafd3c0e178b95f69c5868b1dd44777f23ec1e 2081 setup-windows.ps1
|
619515f524cb89960370ffcbd3fafd3c0e178b95f69c5868b1dd44777f23ec1e 2081 setup-windows.ps1
|
||||||
dd613d04b366f2cd071a1685a414016a5fb008082ed1b4cb8b24b79c100f640a 2412 src/main/audit-service.cjs
|
dd613d04b366f2cd071a1685a414016a5fb008082ed1b4cb8b24b79c100f640a 2412 src/main/audit-service.cjs
|
||||||
@@ -157,14 +160,14 @@ f0861356c1ff4ca361c7004c1f7d935858f51cc74335a7ad2136021f2e612220 36016
|
|||||||
9e682411f73450f595b5cc4dfb28939c6c58b9547d0a91e287c3efb4955e8840 26299 src/main/unraid-preflight-methods.cjs
|
9e682411f73450f595b5cc4dfb28939c6c58b9547d0a91e287c3efb4955e8840 26299 src/main/unraid-preflight-methods.cjs
|
||||||
d45220176aed72d692f9ae5534f9d40bcc359a2d08e025e74a3b3b505b8b9ed4 16559 src/main/unraid-runtime-methods.cjs
|
d45220176aed72d692f9ae5534f9d40bcc359a2d08e025e74a3b3b505b8b9ed4 16559 src/main/unraid-runtime-methods.cjs
|
||||||
d4b3a07eeca687a49544a94ea574f7c6f7e0bc3aa9311b614d5244a468ca4a1b 11307 src/main/unraid-state-methods.cjs
|
d4b3a07eeca687a49544a94ea574f7c6f7e0bc3aa9311b614d5244a468ca4a1b 11307 src/main/unraid-state-methods.cjs
|
||||||
b654a9e45044ad32c61fabe4a6d897288615ec83739b53e3241ff881e32f56bd 21677 src/main/update-service.cjs
|
a387ac9735624ef89d964ea59e035dae256b9915ba2cee22ae5cbc2894f5808a 22000 src/main/update-service.cjs
|
||||||
b5c304531bec358d059189a27cd9db8fa20cefb7f817e5eb0287001f7353f6a7 985 src/renderer/actions/command.js
|
b5c304531bec358d059189a27cd9db8fa20cefb7f817e5eb0287001f7353f6a7 985 src/renderer/actions/command.js
|
||||||
d0bf607dd1de9d55f2947d0adf0997cd3ca5c269d10a5362cc1d8bc4d1a2a8ae 6706 src/renderer/actions/deployment-operation.js
|
d0bf607dd1de9d55f2947d0adf0997cd3ca5c269d10a5362cc1d8bc4d1a2a8ae 6706 src/renderer/actions/deployment-operation.js
|
||||||
0f2070aa3b5c404aedf643837dfd7c5d547e8f45b2e9c97e4cfcf11951555474 17705 src/renderer/actions/deployment-profile.js
|
0f2070aa3b5c404aedf643837dfd7c5d547e8f45b2e9c97e4cfcf11951555474 17705 src/renderer/actions/deployment-profile.js
|
||||||
48bed91dd2a85bb51ee7307f7acc3b79c881ce8cf63b22ba79d5d079b265eb4b 7785 src/renderer/actions/inventory.js
|
48bed91dd2a85bb51ee7307f7acc3b79c881ce8cf63b22ba79d5d079b265eb4b 7785 src/renderer/actions/inventory.js
|
||||||
4227a05a20580a31127d2c929640defc3d36e8e3e89d6be830aab1940da81082 12267 src/renderer/actions/recovery.js
|
4227a05a20580a31127d2c929640defc3d36e8e3e89d6be830aab1940da81082 12267 src/renderer/actions/recovery.js
|
||||||
cdfaacdcd5ae04b0e5c79fefa21f5e09d5c810bcea504c5b6e1d6b744182ff84 15567 src/renderer/actions/setup-and-settings.js
|
cdfaacdcd5ae04b0e5c79fefa21f5e09d5c810bcea504c5b6e1d6b744182ff84 15567 src/renderer/actions/setup-and-settings.js
|
||||||
5d8110918b2957889047e38eb4ab2953b2b4476394d9328bd40ae6e4246e65ef 18584 src/renderer/actions/shell.js
|
a980f2e86d8286ea605a7259b9e9adcf3fda4f657b8d54a5d2d8765a7bbbec13 19065 src/renderer/actions/shell.js
|
||||||
edcfa0585af4b11ec3b7458969132bdf215830505859f1519f1635dc111ce48a 24333 src/renderer/app.js
|
edcfa0585af4b11ec3b7458969132bdf215830505859f1519f1635dc111ce48a 24333 src/renderer/app.js
|
||||||
16efd2fca83004f781eae40ae0f706a004ce0bddf338dd087b8adf7eb10c1d84 85704 src/renderer/assets/itworx-mark.png
|
16efd2fca83004f781eae40ae0f706a004ce0bddf338dd087b8adf7eb10c1d84 85704 src/renderer/assets/itworx-mark.png
|
||||||
813b8cdeecac43794166f3db9d3c5d2c441e0292f9ab7bd465ba136d6201e95d 82476 src/renderer/assets/itworx-wordmark-dark.png
|
813b8cdeecac43794166f3db9d3c5d2c441e0292f9ab7bd465ba136d6201e95d 82476 src/renderer/assets/itworx-wordmark-dark.png
|
||||||
@@ -174,11 +177,11 @@ edcfa0585af4b11ec3b7458969132bdf215830505859f1519f1635dc111ce48a 24333
|
|||||||
eef2f269ba4fbb76bf66ad328d481b461255d0acb753b30878dd4d4eaac57dc6 6924 src/renderer/events.js
|
eef2f269ba4fbb76bf66ad328d481b461255d0acb753b30878dd4d4eaac57dc6 6924 src/renderer/events.js
|
||||||
a84da5aecbb16ce7983dba1f6d6aab1bf47b2e9a87c2933fa1afb8123f7ef7d6 1497 src/renderer/index.html
|
a84da5aecbb16ce7983dba1f6d6aab1bf47b2e9a87c2933fa1afb8123f7ef7d6 1497 src/renderer/index.html
|
||||||
06180d9656dd254edfb6949c397f8e313954fc560ddcb22b3a35fce3c3e35655 21350 src/renderer/mock-bridge.js
|
06180d9656dd254edfb6949c397f8e313954fc560ddcb22b3a35fce3c3e35655 21350 src/renderer/mock-bridge.js
|
||||||
9fa522dad0088e2981c4ca5c392e93d6c92e04ff23fd1083d9040a3aa52d2b55 28101 src/renderer/mock-deployment-bridge.js
|
5a2c0616a8a0ba23c182a8928abdf40a69612ce34c4cba47b1fd5f21fe5843d4 28617 src/renderer/mock-deployment-bridge.js
|
||||||
948369ce1bfbc31becc95996feabccd6ae90c6297217f04149b78bd323d279f7 20032 src/renderer/mock-repository-bridge.js
|
b30f44e8e4285e40e7ef87474aeb34467a40dbedc9023a080deaa204ce84bbeb 20032 src/renderer/mock-repository-bridge.js
|
||||||
94fa265c2fe9ca8d644f0ce9b620b6f85d9b25dca5802c4e9195b66dcbe80120 6522 src/renderer/operations.js
|
94fa265c2fe9ca8d644f0ce9b620b6f85d9b25dca5802c4e9195b66dcbe80120 6522 src/renderer/operations.js
|
||||||
6776e0adb690b8f36274bfc5b31e3140054893c46ba64621c4f56a0d91b52fc2 78323 src/renderer/styles.css
|
1fc4c28859f7283aa2b771a50f1fd26addc8862b97a09250f806930ab4483f7a 79874 src/renderer/styles.css
|
||||||
1703e64533b7e2717b27c5776296c7dd76331e6f97e8005aea9fd688f1aee3ae 94834 src/renderer/views.js
|
5230c238acb87381303ec30026906cf8a7163c33380859d351b268adfc26b4f4 100196 src/renderer/views.js
|
||||||
0a1e9d9d6cd4d190eb7f85dbc6668d80600b1cf2749cc0c2c51cc428f506f20d 1121 src/shared/clone-target.cjs
|
0a1e9d9d6cd4d190eb7f85dbc6668d80600b1cf2749cc0c2c51cc428f506f20d 1121 src/shared/clone-target.cjs
|
||||||
5d425d5c2f939d0f6beebee7ebb0c77146cb7e318535ba7286ec7081a4dc2269 2497 src/shared/deployment-policy.cjs
|
5d425d5c2f939d0f6beebee7ebb0c77146cb7e318535ba7286ec7081a4dc2269 2497 src/shared/deployment-policy.cjs
|
||||||
029e600229714d033c28e2dcb77817aa8269847001782ae0012960e83ffd183f 3057 src/shared/git-status.cjs
|
029e600229714d033c28e2dcb77817aa8269847001782ae0012960e83ffd183f 3057 src/shared/git-status.cjs
|
||||||
@@ -192,7 +195,7 @@ f8853dce6fdf360d5df2fbe2b6df3e5687630c807fee5ba8436679b34ec737ea 2436
|
|||||||
058aeaa5d9bfe377c7e322f213c7871ecc4151b5d08ef790992f4ee28d857658 743 START-FORGEFLOW-OVERLAY.ps1
|
058aeaa5d9bfe377c7e322f213c7871ecc4151b5d08ef790992f4ee28d857658 743 START-FORGEFLOW-OVERLAY.ps1
|
||||||
f5b0ea887fcdeadec78c1ad49b0ec7979723562f5c0b730703acb77a37281ee0 1009 tests/acceptance.test.mjs
|
f5b0ea887fcdeadec78c1ad49b0ec7979723562f5c0b730703acb77a37281ee0 1009 tests/acceptance.test.mjs
|
||||||
a4e5947204ff6878e601e32477bc85b53cd0153baf95a161c8935b6e5466c257 1155 tests/audit-service.test.mjs
|
a4e5947204ff6878e601e32477bc85b53cd0153baf95a161c8935b6e5466c257 1155 tests/audit-service.test.mjs
|
||||||
2d57a66bb2a6461e4c3266f14998d505ba0582135b0bbcf8fce9f1542a70007a 10479 tests/browser/forgeflow.spec.mjs
|
b46c9624c20bae767e50b4ab85e6b0ea6a9ee4a5c5c30f55554217f25134f1e7 11218 tests/browser/forgeflow.spec.mjs
|
||||||
454edeaccb2bd41043bc918d3e3a6127db14339031d6a1c1562ac855e90455d2 4318 tests/clone-target.test.mjs
|
454edeaccb2bd41043bc918d3e3a6127db14339031d6a1c1562ac855e90455d2 4318 tests/clone-target.test.mjs
|
||||||
ac17f8bbe9e388b80abef7792c8b184a1fd482c93f13d23a478e433961020f75 17214 tests/config-store.test.mjs
|
ac17f8bbe9e388b80abef7792c8b184a1fd482c93f13d23a478e433961020f75 17214 tests/config-store.test.mjs
|
||||||
f1463326aee79842d265687ae628189ce54e92544600f2bd14073780287cfb14 2502 tests/configuration-backup.test.mjs
|
f1463326aee79842d265687ae628189ce54e92544600f2bd14073780287cfb14 2502 tests/configuration-backup.test.mjs
|
||||||
@@ -224,7 +227,7 @@ e631e9ca49a5bac7075860aac2ff4d377a32a78377b70e06ecf833f0f192fd5f 11552
|
|||||||
0d1bc4d623ce299337736c577ec61c8ffd6974ebe20335b72838d10eae35ecb1 7993 tests/ssh-service.test.mjs
|
0d1bc4d623ce299337736c577ec61c8ffd6974ebe20335b72838d10eae35ecb1 7993 tests/ssh-service.test.mjs
|
||||||
8a6a8477eb94b85ccef18cddd2640afb0d1eafa679c96bc7de20428d5d69e1be 1794 tests/tool-invocation.test.mjs
|
8a6a8477eb94b85ccef18cddd2640afb0d1eafa679c96bc7de20428d5d69e1be 1794 tests/tool-invocation.test.mjs
|
||||||
4182b61e395aff310b9a964c973a43c3566df0b44c454054c3abdd9459e86e3b 49134 tests/unraid-deployment.test.mjs
|
4182b61e395aff310b9a964c973a43c3566df0b44c454054c3abdd9459e86e3b 49134 tests/unraid-deployment.test.mjs
|
||||||
edec1007826bff6b5457fca603d8c816a9e1eedb3ce398269f151144ae3d2197 19764 tests/update-service.test.mjs
|
9c58c600d925db6a3ad526e661010c05619e24d61c9d19ec0e207a09ae4cb93a 23981 tests/update-service.test.mjs
|
||||||
9cea5c1d5ba3e0972a0b5c7236cf1f7c5616373e0a39ea4a492ecebf70452e40 948 tests/validation.test.mjs
|
9cea5c1d5ba3e0972a0b5c7236cf1f7c5616373e0a39ea4a492ecebf70452e40 948 tests/validation.test.mjs
|
||||||
7ef4d4b9f5f3e6979293b29d571ce0e39f83197f3cade2d999a9cea7bacdd84d 1781 tests/zip-writer.test.mjs
|
7ef4d4b9f5f3e6979293b29d571ce0e39f83197f3cade2d999a9cea7bacdd84d 1781 tests/zip-writer.test.mjs
|
||||||
8f36b542736f2933bad8b9464ad7fa37b68196009c81cf702ce3b677cd637dea 767 UPDATE_FROM_0.3.2.md
|
8f36b542736f2933bad8b9464ad7fa37b68196009c81cf702ce3b677cd637dea 767 UPDATE_FROM_0.3.2.md
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# ForgeFlow 0.10.4
|
||||||
|
|
||||||
|
## Permanent packaged updater handshake repair
|
||||||
|
|
||||||
|
- Binary and source update helpers now use a Windows PowerShell 5.1-compatible atomic status replacement with a real temporary backup path.
|
||||||
|
- A deterministic overwrite fallback preserves lifecycle reporting on filesystems that do not implement atomic replacement.
|
||||||
|
- The binary helper exposes a side-effect-free handshake-only verification mode exercised by the real Windows PowerShell executable during tests.
|
||||||
|
- existing installations with the defective helper require this one-time installer upgrade; every subsequent packaged update uses the repaired helper automatically.
|
||||||
|
- Startup failures retain request-scoped status and helper-log evidence instead of collapsing into an unexplained exit-code message.
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# ForgeFlow 0.10.5
|
||||||
|
|
||||||
|
## Consistent repository and deployment links
|
||||||
|
|
||||||
|
- Every repository workspace now shows all configured deployment environments in a compact, directly actionable strip.
|
||||||
|
- The repository deployment tab includes every detected server workload linked to that repository, including its container, Compose identity, server and runtime state.
|
||||||
|
- A workload is only labelled linked when its repository and resolved profile both exist in the current ForgeFlow configuration.
|
||||||
|
- Stale or incomplete metadata is shown as **Link unresolved** and routed through explicit reconciliation instead of being presented as a healthy deployment.
|
||||||
|
- The global deployment inventory links directly to the correct repository deployment profile.
|
||||||
|
- Responsive browser coverage now verifies valid links, unresolved links, repository navigation and scrolling across dark/light and scaled layouts.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# ForgeFlow 0.10.6
|
||||||
|
|
||||||
|
## Permanent Windows updater launch repair
|
||||||
|
|
||||||
|
- ForgeFlow no longer launches hidden PowerShell update helpers with Node's defective Windows `detached` process mode.
|
||||||
|
- Binary and source updater processes remain hidden, are explicitly unreferenced after their verified handshake, and continue independently when ForgeFlow closes.
|
||||||
|
- A real Windows regression test now exercises the exact production Node spawn options instead of using a different process API.
|
||||||
|
- Startup is still fail-closed: ForgeFlow remains open unless the request-scoped helper status reaches `started`.
|
||||||
|
- Versions 0.10.4 and 0.10.5 need a one-time direct installation of 0.10.6 because their installed launcher cannot execute its own helper; updates after 0.10.6 use the repaired path.
|
||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "forgeflow",
|
"name": "forgeflow",
|
||||||
"version": "0.10.3",
|
"version": "0.10.6",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "forgeflow",
|
"name": "forgeflow",
|
||||||
"version": "0.10.3",
|
"version": "0.10.6",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ssh2": "1.17.0"
|
"ssh2": "1.17.0"
|
||||||
},
|
},
|
||||||
|
|||||||
+4
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "forgeflow",
|
"name": "forgeflow",
|
||||||
"version": "0.10.3",
|
"version": "0.10.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Desktop release cockpit for local Git, Gitea Actions and controlled exact-commit deployments.",
|
"description": "Desktop release cockpit for local Git, Gitea Actions and controlled exact-commit deployments.",
|
||||||
"main": "main.cjs",
|
"main": "main.cjs",
|
||||||
@@ -108,6 +108,9 @@
|
|||||||
"docs/RELEASE_NOTES_0.10.1.md",
|
"docs/RELEASE_NOTES_0.10.1.md",
|
||||||
"docs/RELEASE_NOTES_0.10.2.md",
|
"docs/RELEASE_NOTES_0.10.2.md",
|
||||||
"docs/RELEASE_NOTES_0.10.3.md",
|
"docs/RELEASE_NOTES_0.10.3.md",
|
||||||
|
"docs/RELEASE_NOTES_0.10.4.md",
|
||||||
|
"docs/RELEASE_NOTES_0.10.5.md",
|
||||||
|
"docs/RELEASE_NOTES_0.10.6.md",
|
||||||
"docs/CURRENT_STATE.md",
|
"docs/CURRENT_STATE.md",
|
||||||
"docs/MUTATION_MODEL.md",
|
"docs/MUTATION_MODEL.md",
|
||||||
"docs/RELEASING.md",
|
"docs/RELEASING.md",
|
||||||
|
|||||||
+106
-90
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"generatedAt": "2026-07-29T22:49:48.766Z",
|
"generatedAt": "2026-08-01T15:23:33.157Z",
|
||||||
"thresholds": {
|
"thresholds": {
|
||||||
"preferredMaximumLines": 750,
|
"preferredMaximumLines": 750,
|
||||||
"justificationRequiredLines": 1000
|
"justificationRequiredLines": 1000
|
||||||
@@ -9,9 +9,9 @@
|
|||||||
"cyclomaticHotspots": [
|
"cyclomaticHotspots": [
|
||||||
{
|
{
|
||||||
"file": "src/renderer/actions/shell.js",
|
"file": "src/renderer/actions/shell.js",
|
||||||
"lines": 506,
|
"lines": 518,
|
||||||
"branches": 98,
|
"branches": 101,
|
||||||
"functions": 84,
|
"functions": 86,
|
||||||
"ipcHandlers": 0,
|
"ipcHandlers": 0,
|
||||||
"responsibilities": [
|
"responsibilities": [
|
||||||
"inventory",
|
"inventory",
|
||||||
@@ -20,50 +20,7 @@
|
|||||||
"renderer",
|
"renderer",
|
||||||
"updates"
|
"updates"
|
||||||
],
|
],
|
||||||
"hotspotScore": 128
|
"hotspotScore": 131
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "src/main/server-inventory.cjs",
|
|
||||||
"lines": 578,
|
|
||||||
"branches": 89,
|
|
||||||
"functions": 104,
|
|
||||||
"ipcHandlers": 0,
|
|
||||||
"responsibilities": [
|
|
||||||
"inventory",
|
|
||||||
"deployment",
|
|
||||||
"git",
|
|
||||||
"security",
|
|
||||||
"updates"
|
|
||||||
],
|
|
||||||
"hotspotScore": 119
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "src/main/git-service.cjs",
|
|
||||||
"lines": 632,
|
|
||||||
"branches": 98,
|
|
||||||
"functions": 109,
|
|
||||||
"ipcHandlers": 0,
|
|
||||||
"responsibilities": [
|
|
||||||
"git"
|
|
||||||
],
|
|
||||||
"hotspotScore": 98
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"mixedResponsibilityModules": [
|
|
||||||
{
|
|
||||||
"file": "src/renderer/actions/shell.js",
|
|
||||||
"lines": 506,
|
|
||||||
"branches": 98,
|
|
||||||
"functions": 84,
|
|
||||||
"ipcHandlers": 0,
|
|
||||||
"responsibilities": [
|
|
||||||
"inventory",
|
|
||||||
"deployment",
|
|
||||||
"git",
|
|
||||||
"renderer",
|
|
||||||
"updates"
|
|
||||||
],
|
|
||||||
"hotspotScore": 128
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "src/main/server-inventory.cjs",
|
"file": "src/main/server-inventory.cjs",
|
||||||
@@ -82,9 +39,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "src/renderer/app.js",
|
"file": "src/renderer/app.js",
|
||||||
"lines": 667,
|
"lines": 676,
|
||||||
"branches": 74,
|
"branches": 75,
|
||||||
"functions": 110,
|
"functions": 113,
|
||||||
"ipcHandlers": 0,
|
"ipcHandlers": 0,
|
||||||
"responsibilities": [
|
"responsibilities": [
|
||||||
"inventory",
|
"inventory",
|
||||||
@@ -94,7 +51,66 @@
|
|||||||
"security",
|
"security",
|
||||||
"updates"
|
"updates"
|
||||||
],
|
],
|
||||||
"hotspotScore": 114
|
"hotspotScore": 115
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "src/main/git-service.cjs",
|
||||||
|
"lines": 632,
|
||||||
|
"branches": 98,
|
||||||
|
"functions": 109,
|
||||||
|
"ipcHandlers": 0,
|
||||||
|
"responsibilities": [
|
||||||
|
"git"
|
||||||
|
],
|
||||||
|
"hotspotScore": 98
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mixedResponsibilityModules": [
|
||||||
|
{
|
||||||
|
"file": "src/renderer/actions/shell.js",
|
||||||
|
"lines": 518,
|
||||||
|
"branches": 101,
|
||||||
|
"functions": 86,
|
||||||
|
"ipcHandlers": 0,
|
||||||
|
"responsibilities": [
|
||||||
|
"inventory",
|
||||||
|
"deployment",
|
||||||
|
"git",
|
||||||
|
"renderer",
|
||||||
|
"updates"
|
||||||
|
],
|
||||||
|
"hotspotScore": 131
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "src/main/server-inventory.cjs",
|
||||||
|
"lines": 578,
|
||||||
|
"branches": 89,
|
||||||
|
"functions": 104,
|
||||||
|
"ipcHandlers": 0,
|
||||||
|
"responsibilities": [
|
||||||
|
"inventory",
|
||||||
|
"deployment",
|
||||||
|
"git",
|
||||||
|
"security",
|
||||||
|
"updates"
|
||||||
|
],
|
||||||
|
"hotspotScore": 119
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "src/renderer/app.js",
|
||||||
|
"lines": 676,
|
||||||
|
"branches": 75,
|
||||||
|
"functions": 113,
|
||||||
|
"ipcHandlers": 0,
|
||||||
|
"responsibilities": [
|
||||||
|
"inventory",
|
||||||
|
"deployment",
|
||||||
|
"git",
|
||||||
|
"renderer",
|
||||||
|
"security",
|
||||||
|
"updates"
|
||||||
|
],
|
||||||
|
"hotspotScore": 115
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "src/main/ipc.cjs",
|
"file": "src/main/ipc.cjs",
|
||||||
@@ -130,9 +146,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "src/renderer/views.js",
|
"file": "src/renderer/views.js",
|
||||||
"lines": 693,
|
"lines": 737,
|
||||||
"branches": 53,
|
"branches": 55,
|
||||||
"functions": 138,
|
"functions": 156,
|
||||||
"ipcHandlers": 0,
|
"ipcHandlers": 0,
|
||||||
"responsibilities": [
|
"responsibilities": [
|
||||||
"inventory",
|
"inventory",
|
||||||
@@ -142,7 +158,7 @@
|
|||||||
"security",
|
"security",
|
||||||
"updates"
|
"updates"
|
||||||
],
|
],
|
||||||
"hotspotScore": 93
|
"hotspotScore": 95
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "src/main/unraid-inventory-methods.cjs",
|
"file": "src/main/unraid-inventory-methods.cjs",
|
||||||
@@ -176,8 +192,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "src/main/gitea-service.cjs",
|
"file": "src/main/gitea-service.cjs",
|
||||||
"lines": 618,
|
"lines": 624,
|
||||||
"branches": 66,
|
"branches": 67,
|
||||||
"functions": 57,
|
"functions": 57,
|
||||||
"ipcHandlers": 0,
|
"ipcHandlers": 0,
|
||||||
"responsibilities": [
|
"responsibilities": [
|
||||||
@@ -186,7 +202,7 @@
|
|||||||
"security",
|
"security",
|
||||||
"updates"
|
"updates"
|
||||||
],
|
],
|
||||||
"hotspotScore": 86
|
"hotspotScore": 87
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "src/renderer/actions/deployment-profile.js",
|
"file": "src/renderer/actions/deployment-profile.js",
|
||||||
@@ -312,6 +328,35 @@
|
|||||||
],
|
],
|
||||||
"hotspotScore": 73
|
"hotspotScore": 73
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"file": "src/renderer/events.js",
|
||||||
|
"lines": 181,
|
||||||
|
"branches": 35,
|
||||||
|
"functions": 27,
|
||||||
|
"ipcHandlers": 0,
|
||||||
|
"responsibilities": [
|
||||||
|
"inventory",
|
||||||
|
"deployment",
|
||||||
|
"git",
|
||||||
|
"renderer",
|
||||||
|
"security"
|
||||||
|
],
|
||||||
|
"hotspotScore": 65
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "src/main/git-validator-service.cjs",
|
||||||
|
"lines": 600,
|
||||||
|
"branches": 43,
|
||||||
|
"functions": 77,
|
||||||
|
"ipcHandlers": 0,
|
||||||
|
"responsibilities": [
|
||||||
|
"deployment",
|
||||||
|
"git",
|
||||||
|
"security",
|
||||||
|
"updates"
|
||||||
|
],
|
||||||
|
"hotspotScore": 63
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"file": "src/main/ssh-service.cjs",
|
"file": "src/main/ssh-service.cjs",
|
||||||
"lines": 333,
|
"lines": 333,
|
||||||
@@ -341,35 +386,6 @@
|
|||||||
],
|
],
|
||||||
"hotspotScore": 61
|
"hotspotScore": 61
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"file": "src/renderer/events.js",
|
|
||||||
"lines": 172,
|
|
||||||
"branches": 31,
|
|
||||||
"functions": 26,
|
|
||||||
"ipcHandlers": 0,
|
|
||||||
"responsibilities": [
|
|
||||||
"inventory",
|
|
||||||
"deployment",
|
|
||||||
"git",
|
|
||||||
"renderer",
|
|
||||||
"security"
|
|
||||||
],
|
|
||||||
"hotspotScore": 61
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "src/main/git-validator-service.cjs",
|
|
||||||
"lines": 588,
|
|
||||||
"branches": 40,
|
|
||||||
"functions": 74,
|
|
||||||
"ipcHandlers": 0,
|
|
||||||
"responsibilities": [
|
|
||||||
"deployment",
|
|
||||||
"git",
|
|
||||||
"security",
|
|
||||||
"updates"
|
|
||||||
],
|
|
||||||
"hotspotScore": 60
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"file": "src/main/unraid-deployment-service.cjs",
|
"file": "src/main/unraid-deployment-service.cjs",
|
||||||
"lines": 525,
|
"lines": 525,
|
||||||
@@ -477,7 +493,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "src/renderer/mock-deployment-bridge.js",
|
"file": "src/renderer/mock-deployment-bridge.js",
|
||||||
"lines": 679,
|
"lines": 699,
|
||||||
"branches": 11,
|
"branches": 11,
|
||||||
"functions": 87,
|
"functions": 87,
|
||||||
"ipcHandlers": 0,
|
"ipcHandlers": 0,
|
||||||
@@ -686,7 +702,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "src/main/ipc/repository-handlers.cjs",
|
"file": "src/main/ipc/repository-handlers.cjs",
|
||||||
"lines": 411,
|
"lines": 413,
|
||||||
"branches": 16,
|
"branches": 16,
|
||||||
"functions": 79,
|
"functions": 79,
|
||||||
"ipcHandlers": 51,
|
"ipcHandlers": 51,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# ForgeFlow architecture audit
|
# ForgeFlow architecture audit
|
||||||
|
|
||||||
Generated 2026-07-29T22:49:48.766Z. Complexity is a deterministic decision-point count used for hotspot ranking, not a claim of exact McCabe complexity.
|
Generated 2026-08-01T15:23:33.157Z. Complexity is a deterministic decision-point count used for hotspot ranking, not a claim of exact McCabe complexity.
|
||||||
|
|
||||||
## Files above 750 lines
|
## Files above 750 lines
|
||||||
|
|
||||||
@@ -18,8 +18,9 @@ No findings.
|
|||||||
|
|
||||||
| File | Lines | Decisions | Functions | IPC handlers | Responsibilities |
|
| File | Lines | Decisions | Functions | IPC handlers | Responsibilities |
|
||||||
|---|---:|---:|---:|---:|---|
|
|---|---:|---:|---:|---:|---|
|
||||||
| `src/renderer/actions/shell.js` | 506 | 98 | 84 | 0 | inventory, deployment, git, renderer, updates |
|
| `src/renderer/actions/shell.js` | 518 | 101 | 86 | 0 | inventory, deployment, git, renderer, updates |
|
||||||
| `src/main/server-inventory.cjs` | 578 | 89 | 104 | 0 | inventory, deployment, git, security, updates |
|
| `src/main/server-inventory.cjs` | 578 | 89 | 104 | 0 | inventory, deployment, git, security, updates |
|
||||||
|
| `src/renderer/app.js` | 676 | 75 | 113 | 0 | inventory, deployment, git, renderer, security, updates |
|
||||||
| `src/main/git-service.cjs` | 632 | 98 | 109 | 0 | git |
|
| `src/main/git-service.cjs` | 632 | 98 | 109 | 0 | git |
|
||||||
|
|
||||||
## Interpretation
|
## Interpretation
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ param(
|
|||||||
[Parameter(Mandatory = $true)][int]$ParentPid,
|
[Parameter(Mandatory = $true)][int]$ParentPid,
|
||||||
[Parameter(Mandatory = $true)][string]$LogPath,
|
[Parameter(Mandatory = $true)][string]$LogPath,
|
||||||
[Parameter(Mandatory = $true)][string]$StatusPath,
|
[Parameter(Mandatory = $true)][string]$StatusPath,
|
||||||
[Parameter(Mandatory = $true)][string]$UpdateId
|
[Parameter(Mandatory = $true)][string]$UpdateId,
|
||||||
|
[switch]$HandshakeOnly
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
@@ -27,12 +28,26 @@ function Write-UpdateState {
|
|||||||
updatedAt = [DateTime]::UtcNow.ToString("o")
|
updatedAt = [DateTime]::UtcNow.ToString("o")
|
||||||
}
|
}
|
||||||
if ($State -in @("success", "failed", "rolled-back")) { $payload.completedAt = [DateTime]::UtcNow.ToString("o") }
|
if ($State -in @("success", "failed", "rolled-back")) { $payload.completedAt = [DateTime]::UtcNow.ToString("o") }
|
||||||
|
$directory = Split-Path -Parent $StatusPath
|
||||||
|
if ($directory) { New-Item -ItemType Directory -Force -Path $directory | Out-Null }
|
||||||
$temporary = "$StatusPath.$PID.tmp"
|
$temporary = "$StatusPath.$PID.tmp"
|
||||||
$payload | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $temporary -Encoding UTF8
|
$backup = "$StatusPath.$PID.bak"
|
||||||
if (Test-Path -LiteralPath $StatusPath) { [IO.File]::Replace($temporary, $StatusPath, $null) }
|
$json = $payload | ConvertTo-Json -Depth 4
|
||||||
else { Move-Item -LiteralPath $temporary -Destination $StatusPath }
|
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
||||||
|
[IO.File]::WriteAllText($temporary, $json, $utf8NoBom)
|
||||||
|
try {
|
||||||
|
if ([IO.File]::Exists($StatusPath)) {
|
||||||
|
[IO.File]::Replace($temporary, $StatusPath, $backup)
|
||||||
|
[IO.File]::Delete($backup)
|
||||||
|
} else {
|
||||||
|
[IO.File]::Move($temporary, $StatusPath)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
[IO.File]::Copy($temporary, $StatusPath, $true)
|
||||||
|
[IO.File]::Delete($temporary)
|
||||||
|
if ([IO.File]::Exists($backup)) { [IO.File]::Delete($backup) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function Write-Log([string]$Message) {
|
function Write-Log([string]$Message) {
|
||||||
"{0} {1}" -f [DateTime]::UtcNow.ToString("o"), $Message | Add-Content -LiteralPath $LogPath -Encoding UTF8
|
"{0} {1}" -f [DateTime]::UtcNow.ToString("o"), $Message | Add-Content -LiteralPath $LogPath -Encoding UTF8
|
||||||
}
|
}
|
||||||
@@ -40,6 +55,10 @@ function Write-Log([string]$Message) {
|
|||||||
try {
|
try {
|
||||||
Write-UpdateState -State "started" -Message "Binary updater owns the update request."
|
Write-UpdateState -State "started" -Message "Binary updater owns the update request."
|
||||||
Write-Log "Validating ForgeFlow $ExpectedVersion binary update."
|
Write-Log "Validating ForgeFlow $ExpectedVersion binary update."
|
||||||
|
if ($HandshakeOnly) {
|
||||||
|
Write-Log "Handshake-only verification completed successfully."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
$actualSha256 = (Get-FileHash -LiteralPath $BinaryPath -Algorithm SHA256).Hash.ToLowerInvariant()
|
$actualSha256 = (Get-FileHash -LiteralPath $BinaryPath -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||||
if ($actualSha256 -ne $ExpectedSha256.ToLowerInvariant()) { throw "Binary update SHA-256 verification failed." }
|
if ($actualSha256 -ne $ExpectedSha256.ToLowerInvariant()) { throw "Binary update SHA-256 verification failed." }
|
||||||
if (-not (Test-Path -LiteralPath $CurrentExecutable -PathType Leaf)) { throw "Current ForgeFlow executable was not found." }
|
if (-not (Test-Path -LiteralPath $CurrentExecutable -PathType Leaf)) { throw "Current ForgeFlow executable was not found." }
|
||||||
|
|||||||
@@ -54,15 +54,20 @@ function Write-UpdateState {
|
|||||||
if ([System.IO.File]::Exists($StatusPath)) {
|
if ([System.IO.File]::Exists($StatusPath)) {
|
||||||
# Windows PowerShell 5.1 does not reliably let Move-Item -Force replace
|
# Windows PowerShell 5.1 does not reliably let Move-Item -Force replace
|
||||||
# an existing file. File.Replace is atomic on the local NTFS volume.
|
# an existing file. File.Replace is atomic on the local NTFS volume.
|
||||||
[System.IO.File]::Replace($temporary, $StatusPath, $null)
|
$backup = "$StatusPath.$PID.bak"
|
||||||
|
[System.IO.File]::Replace($temporary, $StatusPath, $backup)
|
||||||
} else {
|
} else {
|
||||||
[System.IO.File]::Move($temporary, $StatusPath)
|
[System.IO.File]::Move($temporary, $StatusPath)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
# Some filesystems do not implement File.Replace. Copy with overwrite is
|
# Some filesystems do not implement File.Replace. Copy with overwrite is
|
||||||
# the deterministic fallback; the temporary file is removed afterwards.
|
# the deterministic fallback; the temporary file is removed afterwards.
|
||||||
[System.IO.File]::Copy($temporary, $StatusPath, $true)
|
if ([System.IO.File]::Exists($temporary)) {
|
||||||
[System.IO.File]::Delete($temporary)
|
[System.IO.File]::Copy($temporary, $StatusPath, $true)
|
||||||
|
[System.IO.File]::Delete($temporary)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if ([System.IO.File]::Exists($backup)) { [System.IO.File]::Delete($backup) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+17
-2
@@ -83,6 +83,9 @@ const required = [
|
|||||||
"docs/RELEASE_NOTES_0.10.1.md",
|
"docs/RELEASE_NOTES_0.10.1.md",
|
||||||
"docs/RELEASE_NOTES_0.10.2.md",
|
"docs/RELEASE_NOTES_0.10.2.md",
|
||||||
"docs/RELEASE_NOTES_0.10.3.md",
|
"docs/RELEASE_NOTES_0.10.3.md",
|
||||||
|
"docs/RELEASE_NOTES_0.10.4.md",
|
||||||
|
"docs/RELEASE_NOTES_0.10.5.md",
|
||||||
|
"docs/RELEASE_NOTES_0.10.6.md",
|
||||||
"docs/UPDATING.md",
|
"docs/UPDATING.md",
|
||||||
"docs/DIAGNOSTICS.md",
|
"docs/DIAGNOSTICS.md",
|
||||||
"docs/DEPLOYMENT_SETUP.md",
|
"docs/DEPLOYMENT_SETUP.md",
|
||||||
@@ -121,9 +124,9 @@ for (const file of required) await access(path.join(root, file));
|
|||||||
const packageJson = JSON.parse(
|
const packageJson = JSON.parse(
|
||||||
await readFile(path.join(root, "package.json"), "utf8"),
|
await readFile(path.join(root, "package.json"), "utf8"),
|
||||||
);
|
);
|
||||||
if (packageJson.version !== "0.10.3")
|
if (packageJson.version !== "0.10.6")
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Expected package version 0.10.3, got ${packageJson.version}.`,
|
`Expected package version 0.10.6, got ${packageJson.version}.`,
|
||||||
);
|
);
|
||||||
const sourceManifest = await readFile(
|
const sourceManifest = await readFile(
|
||||||
path.join(root, "SOURCE_MANIFEST.txt"),
|
path.join(root, "SOURCE_MANIFEST.txt"),
|
||||||
@@ -449,6 +452,18 @@ const release0103 = await readFile(path.join(root, "docs/RELEASE_NOTES_0.10.3.md
|
|||||||
for (const phrase of ["concurrently", "debounce", "animation frame", "Git Validator", "stale or forged"]) {
|
for (const phrase of ["concurrently", "debounce", "animation frame", "Git Validator", "stale or forged"]) {
|
||||||
if (!release0103.includes(phrase)) throw new Error(`0.10.3 release notes are missing: ${phrase}`);
|
if (!release0103.includes(phrase)) throw new Error(`0.10.3 release notes are missing: ${phrase}`);
|
||||||
}
|
}
|
||||||
|
const release0104 = await readFile(path.join(root, "docs/RELEASE_NOTES_0.10.4.md"), "utf8");
|
||||||
|
for (const phrase of ["Windows PowerShell 5.1", "atomic status", "handshake-only", "existing installations"]) {
|
||||||
|
if (!release0104.includes(phrase)) throw new Error(`0.10.4 release notes are missing: ${phrase}`);
|
||||||
|
}
|
||||||
|
const release0105 = await readFile(path.join(root, "docs/RELEASE_NOTES_0.10.5.md"), "utf8");
|
||||||
|
for (const phrase of ["repository workspace", "resolved profile", "Link unresolved", "reconciliation", "server workload"]) {
|
||||||
|
if (!release0105.includes(phrase)) throw new Error(`0.10.5 release notes are missing: ${phrase}`);
|
||||||
|
}
|
||||||
|
const release0106 = await readFile(path.join(root, "docs/RELEASE_NOTES_0.10.6.md"), "utf8");
|
||||||
|
for (const phrase of ["detached", "PowerShell", "production Node spawn", "source updater", "one-time direct installation"]) {
|
||||||
|
if (!release0106.includes(phrase)) throw new Error(`0.10.6 release notes are missing: ${phrase}`);
|
||||||
|
}
|
||||||
const configSource = await readFile(path.join(root, "src/main/config-store.cjs"), "utf8");
|
const configSource = await readFile(path.join(root, "src/main/config-store.cjs"), "utf8");
|
||||||
for (const mode of ["server-git", "push-bundle", "monitor-only"]) {
|
for (const mode of ["server-git", "push-bundle", "monitor-only"]) {
|
||||||
if (!configSource.includes(mode)) throw new Error(`Deployment configuration is missing mode: ${mode}`);
|
if (!configSource.includes(mode)) throw new Error(`Deployment configuration is missing mode: ${mode}`);
|
||||||
|
|||||||
+23
-12
@@ -33,6 +33,18 @@ function resolveWindowsPowerShellPath(environment = process.env) {
|
|||||||
return "powershell.exe";
|
return "powershell.exe";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function windowsUpdaterSpawnOptions(cwd) {
|
||||||
|
return {
|
||||||
|
// A detached hidden PowerShell child can exit successfully on Windows
|
||||||
|
// without ever executing its -File script. Normal Windows children survive
|
||||||
|
// their parent; unref() below releases the event-loop reference instead.
|
||||||
|
detached: false,
|
||||||
|
stdio: "ignore",
|
||||||
|
windowsHide: true,
|
||||||
|
cwd,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function readJsonFile(filePath) {
|
async function readJsonFile(filePath) {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(await fs.readFile(filePath, "utf8"));
|
return JSON.parse(await fs.readFile(filePath, "utf8"));
|
||||||
@@ -450,12 +462,11 @@ class UpdateService {
|
|||||||
const childState = { exited: false, code: null, error: null };
|
const childState = { exited: false, code: null, error: null };
|
||||||
let child;
|
let child;
|
||||||
try {
|
try {
|
||||||
child = this.spawnProcess(executable, args, {
|
child = this.spawnProcess(
|
||||||
detached: true,
|
executable,
|
||||||
stdio: "ignore",
|
args,
|
||||||
windowsHide: true,
|
windowsUpdaterSpawnOptions(this.sourcePath),
|
||||||
cwd: this.sourcePath,
|
);
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
error.code ||= "UPDATE_HELPER_SPAWN_FAILED";
|
error.code ||= "UPDATE_HELPER_SPAWN_FAILED";
|
||||||
throw error;
|
throw error;
|
||||||
@@ -591,12 +602,11 @@ class UpdateService {
|
|||||||
"-UpdateId",
|
"-UpdateId",
|
||||||
updateId,
|
updateId,
|
||||||
];
|
];
|
||||||
const child = this.spawnProcess(executable, args, {
|
const child = this.spawnProcess(
|
||||||
detached: true,
|
executable,
|
||||||
stdio: "ignore",
|
args,
|
||||||
windowsHide: true,
|
windowsUpdaterSpawnOptions(this.updateDirectory),
|
||||||
cwd: this.updateDirectory,
|
);
|
||||||
});
|
|
||||||
const childState = { exited: false, code: null, error: null };
|
const childState = { exited: false, code: null, error: null };
|
||||||
child.once?.("error", (error) => {
|
child.once?.("error", (error) => {
|
||||||
childState.error = error;
|
childState.error = error;
|
||||||
@@ -700,6 +710,7 @@ module.exports = {
|
|||||||
UpdateService,
|
UpdateService,
|
||||||
safeRepositoryPart,
|
safeRepositoryPart,
|
||||||
resolveWindowsPowerShellPath,
|
resolveWindowsPowerShellPath,
|
||||||
|
windowsUpdaterSpawnOptions,
|
||||||
waitForUpdaterStarted,
|
waitForUpdaterStarted,
|
||||||
readJsonFile,
|
readJsonFile,
|
||||||
readLogTail,
|
readLogTail,
|
||||||
|
|||||||
@@ -9,6 +9,18 @@ async function handleShellActions(event, target, action, repository) {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
} else if (action === "select-repo") selectRepository(target.dataset.id);
|
} else if (action === "select-repo") selectRepository(target.dataset.id);
|
||||||
|
else if (action === "open-deployment-link") {
|
||||||
|
if (!repository) return true;
|
||||||
|
selectRepository(repository.id, false);
|
||||||
|
ui.selectedProfileId = target.dataset.profileId || selectedProfile(repository)?.id || null;
|
||||||
|
ui.repositoryTab = "deployments";
|
||||||
|
ui.currentView = "repository";
|
||||||
|
render();
|
||||||
|
} else if (action === "select-deployment-profile") {
|
||||||
|
ui.selectedProfileId = target.dataset.profileId || null;
|
||||||
|
ui.repositoryTab = "deployments";
|
||||||
|
render();
|
||||||
|
}
|
||||||
else if (action === "refresh") {
|
else if (action === "refresh") {
|
||||||
await refreshRepositories(true);
|
await refreshRepositories(true);
|
||||||
await refreshActiveOperations(false);
|
await refreshActiveOperations(false);
|
||||||
|
|||||||
@@ -238,13 +238,13 @@ function createMockDeploymentBridge(context) {
|
|||||||
{
|
{
|
||||||
serverId: "server-unraid",
|
serverId: "server-unraid",
|
||||||
serverName: "Unraid",
|
serverName: "Unraid",
|
||||||
detected: 2,
|
detected: 3,
|
||||||
adopted: 0,
|
adopted: 0,
|
||||||
verified: 1,
|
verified: 1,
|
||||||
linked: 1,
|
linked: 2,
|
||||||
unmatched: 0,
|
unmatched: 0,
|
||||||
needsReview: 1,
|
needsReview: 2,
|
||||||
running: 2,
|
running: 3,
|
||||||
stopped: 0,
|
stopped: 0,
|
||||||
capabilities: {
|
capabilities: {
|
||||||
docker: true,
|
docker: true,
|
||||||
@@ -296,6 +296,19 @@ function createMockDeploymentBridge(context) {
|
|||||||
reasons: ["container and repository names are similar"],
|
reasons: ["container and repository names are similar"],
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
workloadId: "workload-demo-unresolved",
|
||||||
|
displayName: "Legacy Worker",
|
||||||
|
status: "linked",
|
||||||
|
runtime: { running: true, health: "healthy" },
|
||||||
|
containers: [{ name: "legacy-worker", running: true }],
|
||||||
|
candidates: [],
|
||||||
|
link: {
|
||||||
|
profileId: "profile-that-no-longer-exists",
|
||||||
|
repositoryFullName: "jens/removed-repository",
|
||||||
|
source: "manual",
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ function createMockRepositoryBridge(context) {
|
|||||||
await wait(80);
|
await wait(80);
|
||||||
snapshot();
|
snapshot();
|
||||||
return {
|
return {
|
||||||
appVersion: "0.10.3-demo",
|
appVersion: "0.10.6-demo",
|
||||||
platform: "win32",
|
platform: "win32",
|
||||||
state: clone(state),
|
state: clone(state),
|
||||||
git: { available: true, version: "git version 2.47.3" },
|
git: { available: true, version: "git version 2.47.3" },
|
||||||
|
|||||||
@@ -849,6 +849,64 @@ select:focus-visible {
|
|||||||
.release-node:last-child {
|
.release-node:last-child {
|
||||||
border-right: 0;
|
border-right: 0;
|
||||||
}
|
}
|
||||||
|
.repository-deployment-summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-height: 48px;
|
||||||
|
padding: 7px 12px;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
background: linear-gradient(90deg, color-mix(in srgb, var(--accent) 7%, var(--surface-1)), var(--surface-1) 42%);
|
||||||
|
}
|
||||||
|
.repository-deployment-summary-label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 760;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.repository-deployment-summary-label svg {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
.repository-deployment-chips {
|
||||||
|
display: flex;
|
||||||
|
gap: 7px;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
.repository-deployment-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
max-width: 280px;
|
||||||
|
padding: 6px 9px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: color-mix(in srgb, var(--surface-2) 88%, transparent);
|
||||||
|
color: var(--text);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.repository-deployment-chip:hover {
|
||||||
|
border-color: color-mix(in srgb, var(--accent) 48%, var(--line));
|
||||||
|
background: color-mix(in srgb, var(--accent) 10%, var(--surface-2));
|
||||||
|
}
|
||||||
|
.repository-deployment-chip strong,
|
||||||
|
.repository-deployment-chip span:last-child {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.repository-deployment-chip strong { font-size: 11px; }
|
||||||
|
.repository-deployment-chip span:last-child { color: var(--text-muted); font-size: 10px; }
|
||||||
.release-node:not(:last-child)::after {
|
.release-node:not(:last-child)::after {
|
||||||
content: "›";
|
content: "›";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
+49
-5
@@ -188,6 +188,16 @@ function releaseNode(label, value, description, tone = "") {
|
|||||||
return `<div class="release-node"><div class="release-label">${label}</div><div class="release-value"><span class="state-dot ${tone}"></span><strong>${escapeHtml(value)}</strong><span>${escapeHtml(description)}</span></div></div>`;
|
return `<div class="release-node"><div class="release-label">${label}</div><div class="release-value"><span class="state-dot ${tone}"></span><strong>${escapeHtml(value)}</strong><span>${escapeHtml(description)}</span></div></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function linkedWorkloadsForRepository(repository) {
|
||||||
|
const fullName = String(repository?.fullName || "").toLowerCase();
|
||||||
|
if (!fullName) return [];
|
||||||
|
return (ui.serverDiscovery || []).flatMap((server) =>
|
||||||
|
(server.workloads || [])
|
||||||
|
.filter((workload) => String(workload.link?.repositoryFullName || "").toLowerCase() === fullName)
|
||||||
|
.map((workload) => ({ ...workload, serverId: server.serverId, serverName: server.serverName || server.server?.name || "Server" })),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function diffAtmosphere(diff) {
|
function diffAtmosphere(diff) {
|
||||||
if (!ui.selectedFile) return "";
|
if (!ui.selectedFile) return "";
|
||||||
const lines = String(diff || "").split("\n");
|
const lines = String(diff || "").split("\n");
|
||||||
@@ -359,8 +369,15 @@ function renderProfileCard(repository, profile, compact = false) {
|
|||||||
}
|
}
|
||||||
function renderRepositoryDeployments(repository) {
|
function renderRepositoryDeployments(repository) {
|
||||||
const profiles = repository.deploymentProfiles || [];
|
const profiles = repository.deploymentProfiles || [];
|
||||||
|
const workloads = linkedWorkloadsForRepository(repository);
|
||||||
|
const profileIds = new Set(profiles.map((profile) => profile.id));
|
||||||
|
const workloadRows = workloads.map((workload) => {
|
||||||
|
const containers = (workload.containers || []).map((container) => container.name).filter(Boolean);
|
||||||
|
const profileResolved = Boolean(workload.link?.profileId && profileIds.has(workload.link.profileId));
|
||||||
|
return `<div class="tool-row repository-workload-row"><div><strong>${escapeHtml(workload.displayName || containers[0] || "Server workload")}</strong><span>${escapeHtml(workload.serverName)} · ${containers.length ? escapeHtml(containers.join(", ")) : "container identity unavailable"} · ${workload.runtime?.running ? "running" : "stopped"}</span><span>${escapeHtml(workload.compose?.project ? `Compose ${workload.compose.project}` : workload.remoteFolderCandidate || "Docker workload")}</span></div><div class="stack horizontal compact"><span class="status-pill ${profileResolved ? "success" : "danger"}">${profileResolved ? "Repository linked" : "Link needs reconciliation"}</span>${profileResolved ? `<button class="button ghost" data-action="select-deployment-profile" data-profile-id="${attr(workload.link.profileId)}">Open profile</button>` : `<button class="button" data-action="navigate" data-view="deployments">Review inventory</button>`}</div></div>`;
|
||||||
|
}).join("");
|
||||||
const repoOps = repositoryOperations(repository).slice(0, 10);
|
const repoOps = repositoryOperations(repository).slice(0, 10);
|
||||||
return `<div class="tab-page"><div class="section-heading"><div><h2>Deployment environments</h2><span class="meta">Exact-commit Gitea Actions or pinned SSH / Unraid deployments</span></div><button class="button primary" data-action="configure-deployment">${icon("plus")}Add environment</button></div>${profiles.length ? `<div class="deploy-card-grid">${profiles.map((profile) => renderProfileCard(repository, profile)).join("")}</div>` : '<div class="empty-state panel"><div class="empty-icon">↗</div><h3>No deployment profile</h3><p>Connect a Gitea Actions workflow or a trusted SSH / Unraid server.</p><button class="button primary" data-action="configure-deployment">Configure deployment</button></div>'}<section class="section-block"><div class="section-heading"><h2>Release history</h2></div><div class="panel">${repoOps.length ? `<table class="data-table"><thead><tr><th>Action</th><th>Environment</th><th>Commit</th><th>Status</th><th>Updated</th><th></th></tr></thead><tbody>${repoOps.map((operation) => `<tr><td>${escapeHtml(operation.action || "deploy")}</td><td>${escapeHtml(operation.environment)}</td><td class="mono">${escapeHtml(operation.shortSha || shortSha(operation.sha))}</td><td><span class="status-pill ${toneForStatus(operation.status)}">${escapeHtml(operation.status)}</span></td><td>${formatDate(operation.updatedAt || operation.createdAt)}</td><td><button class="button ghost" data-action="open-operation" data-operation-id="${attr(operation.id)}">Open</button></td></tr>`).join("")}</tbody></table>` : '<div class="empty-state compact"><p>No releases for this repository yet.</p></div>'}</div></section></div>`;
|
return `<div class="tab-page"><div class="section-heading"><div><h2>Deployment environments</h2><span class="meta">${profiles.length} configured profile${profiles.length === 1 ? "" : "s"} · ${workloads.length} server workload${workloads.length === 1 ? "" : "s"} linked to this repository</span></div><button class="button primary" data-action="configure-deployment">${icon("plus")}Add environment</button></div>${workloads.length ? `<section class="panel repository-workloads"><div class="panel-header"><div><h3>Detected on server</h3><span class="meta">Live Docker / Compose identities resolved back to this repository</span></div></div><div class="panel-body"><div class="tool-list">${workloadRows}</div></div></section>` : ""}${profiles.length ? `<div class="deploy-card-grid">${profiles.map((profile) => renderProfileCard(repository, profile)).join("")}</div>` : '<div class="empty-state panel"><div class="empty-icon">↗</div><h3>No deployment profile</h3><p>Connect a Gitea Actions workflow or a trusted SSH / Unraid server.</p><button class="button primary" data-action="configure-deployment">Configure deployment</button></div>'}<section class="section-block"><div class="section-heading"><h2>Release history</h2></div><div class="panel">${repoOps.length ? `<table class="data-table"><thead><tr><th>Action</th><th>Environment</th><th>Commit</th><th>Status</th><th>Updated</th><th></th></tr></thead><tbody>${repoOps.map((operation) => `<tr><td>${escapeHtml(operation.action || "deploy")}</td><td>${escapeHtml(operation.environment)}</td><td class="mono">${escapeHtml(operation.shortSha || shortSha(operation.sha))}</td><td><span class="status-pill ${toneForStatus(operation.status)}">${escapeHtml(operation.status)}</span></td><td>${formatDate(operation.updatedAt || operation.createdAt)}</td><td><button class="button ghost" data-action="open-operation" data-operation-id="${attr(operation.id)}">Open</button></td></tr>`).join("")}</tbody></table>` : '<div class="empty-state compact"><p>No releases for this repository yet.</p></div>'}</div></section></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderGitTools(repository) {
|
function renderGitTools(repository) {
|
||||||
@@ -412,6 +429,8 @@ function renderGitValidator(repository) {
|
|||||||
|
|
||||||
function renderRepositoryWorkspace(repository) {
|
function renderRepositoryWorkspace(repository) {
|
||||||
const status = repository.localStatus;
|
const status = repository.localStatus;
|
||||||
|
const profiles = repository.deploymentProfiles || [];
|
||||||
|
const linkedWorkloads = linkedWorkloadsForRepository(repository);
|
||||||
const profile = selectedProfile(repository);
|
const profile = selectedProfile(repository);
|
||||||
const serverState = profile?.state || {};
|
const serverState = profile?.state || {};
|
||||||
const localTone = status?.counts.conflicts
|
const localTone = status?.counts.conflicts
|
||||||
@@ -444,9 +463,19 @@ function renderRepositoryWorkspace(repository) {
|
|||||||
settings: renderRepositorySettings,
|
settings: renderRepositorySettings,
|
||||||
}[ui.repositoryTab] || renderChanges
|
}[ui.repositoryTab] || renderChanges
|
||||||
)(repository);
|
)(repository);
|
||||||
|
const deploymentLinks = profiles.length
|
||||||
|
? `<div class="repository-deployment-summary"><span class="repository-deployment-summary-label">${icon("server")}Linked deployments</span><div class="repository-deployment-chips">${profiles.map((item) => {
|
||||||
|
const workload = linkedWorkloads.find((candidate) => candidate.link?.profileId === item.id);
|
||||||
|
const itemState = item.state || {};
|
||||||
|
const tone = itemState.healthy === false ? "danger" : itemState.healthy === true ? "success" : workload?.runtime?.running ? "success" : "warning";
|
||||||
|
const identity = workload?.displayName || item.containerName || item.remoteFolder || item.environment;
|
||||||
|
return `<button class="repository-deployment-chip" data-action="select-deployment-profile" data-profile-id="${attr(item.id)}"><span class="state-dot ${tone}"></span><strong>${escapeHtml(identity)}</strong><span>${escapeHtml(item.environment)}${workload?.serverName ? ` · ${escapeHtml(workload.serverName)}` : ""}</span></button>`;
|
||||||
|
}).join("")}</div><button class="button ghost" data-action="select-deployment-profile" data-profile-id="${attr(profile?.id || profiles[0].id)}">View all</button></div>`
|
||||||
|
: "";
|
||||||
return `<div class="repo-workspace"><header class="repo-header illustrated-repo-header"><div class="repo-heading"><h1><button class="favorite-button ${repository.favorite ? "active" : ""}" data-action="toggle-favorite" title="Toggle favorite">${icon("star")}</button>${escapeHtml(repository.fullName)}</h1><p>${escapeHtml(repository.localPath || "No local working tree linked")}</p></div>${projectIllustration("repo")}<div class="repo-header-actions"><button class="button" data-action="fetch" ${!repository.localPath ? "disabled" : ""}>${icon("refresh")}Fetch</button><button class="button" data-action="open-path" ${!repository.localPath ? "disabled" : ""}>${icon("folder")}Folder</button><button class="button" data-action="open-gitea" ${!repository.htmlUrl ? "disabled" : ""}>${icon("external")}Gitea</button></div></header>
|
return `<div class="repo-workspace"><header class="repo-header illustrated-repo-header"><div class="repo-heading"><h1><button class="favorite-button ${repository.favorite ? "active" : ""}" data-action="toggle-favorite" title="Toggle favorite">${icon("star")}</button>${escapeHtml(repository.fullName)}</h1><p>${escapeHtml(repository.localPath || "No local working tree linked")}</p></div>${projectIllustration("repo")}<div class="repo-header-actions"><button class="button" data-action="fetch" ${!repository.localPath ? "disabled" : ""}>${icon("refresh")}Fetch</button><button class="button" data-action="open-path" ${!repository.localPath ? "disabled" : ""}>${icon("folder")}Folder</button><button class="button" data-action="open-gitea" ${!repository.htmlUrl ? "disabled" : ""}>${icon("external")}Gitea</button></div></header>
|
||||||
${repository.localPath ? `<div class="repo-quick-actions"><button class="button" data-action="open-editor">${icon("external")}Open in editor</button><button class="button" data-action="open-terminal">${icon("terminal")}Open terminal</button><button class="button" data-action="check-branch-protection">${icon("shield")}Check branch protection</button><button class="button primary" data-action="open-pull-request">${icon("git")}Create pull request</button>${ui.branchProtection ? `<span class="status-pill ${ui.branchProtection.protected ? "warning" : "success"}">${ui.branchProtection.protected ? `Protected · ${ui.branchProtection.requiredApprovals || 0} approval(s)` : "Direct pushes allowed"}</span>` : ""}</div>` : ""}
|
${repository.localPath ? `<div class="repo-quick-actions"><button class="button" data-action="open-editor">${icon("external")}Open in editor</button><button class="button" data-action="open-terminal">${icon("terminal")}Open terminal</button><button class="button" data-action="check-branch-protection">${icon("shield")}Check branch protection</button><button class="button primary" data-action="open-pull-request">${icon("git")}Create pull request</button>${ui.branchProtection ? `<span class="status-pill ${ui.branchProtection.protected ? "warning" : "success"}">${ui.branchProtection.protected ? `Protected · ${ui.branchProtection.requiredApprovals || 0} approval(s)` : "Direct pushes allowed"}</span>` : ""}</div>` : ""}
|
||||||
<div class="release-rail">${releaseNode("Local", status?.shortHead || "Not linked", status ? `${status.counts.changed} changes · ${status.branch.head}` : "No working tree", localTone)}${releaseNode("Gitea", status?.shortHead || "Unknown", status?.branch.upstream ? `${status.branch.ahead} ahead · ${status.branch.behind} behind` : "Branch not published", remoteTone)}${releaseNode(`Server${profile ? ` · ${profile.environment}` : ""}`, serverState.liveSha ? shortSha(serverState.liveSha) : "Unknown", profile ? (serverState.checkedAt ? `checked ${formatDate(serverState.checkedAt)}` : "not checked") : "No deployment profile", serverTone)}</div>
|
<div class="release-rail">${releaseNode("Local", status?.shortHead || "Not linked", status ? `${status.counts.changed} changes · ${status.branch.head}` : "No working tree", localTone)}${releaseNode("Gitea", status?.shortHead || "Unknown", status?.branch.upstream ? `${status.branch.ahead} ahead · ${status.branch.behind} behind` : "Branch not published", remoteTone)}${releaseNode(`Server${profile ? ` · ${profile.environment}` : ""}`, serverState.liveSha ? shortSha(serverState.liveSha) : "Unknown", profile ? (serverState.checkedAt ? `checked ${formatDate(serverState.checkedAt)}` : "not checked") : "No deployment profile", serverTone)}</div>
|
||||||
|
${deploymentLinks}
|
||||||
<nav class="tabs">${[
|
<nav class="tabs">${[
|
||||||
["changes", "Changes"],
|
["changes", "Changes"],
|
||||||
["history", "History"],
|
["history", "History"],
|
||||||
@@ -531,9 +560,13 @@ function renderServerInventory() {
|
|||||||
? visibleWorkloads.map((workload) => {
|
? visibleWorkloads.map((workload) => {
|
||||||
const containers = (workload.containers || []).map((container) => container.name).filter(Boolean).join(", ");
|
const containers = (workload.containers || []).map((container) => container.name).filter(Boolean).join(", ");
|
||||||
const topCandidate = workload.candidates?.[0];
|
const topCandidate = workload.candidates?.[0];
|
||||||
const linked = (workload.status === "linked" || Boolean(workload.link)) && workload.classification?.type !== "stale-link";
|
const linkedRepository = ui.repositories.find((repository) => String(repository.fullName).toLowerCase() === String(workload.link?.repositoryFullName || "").toLowerCase());
|
||||||
|
const linkedProfile = linkedRepository?.deploymentProfiles?.find((profile) => profile.id === workload.link?.profileId);
|
||||||
|
const claimsLink = workload.status === "linked" || Boolean(workload.link);
|
||||||
|
const linked = Boolean(claimsLink && linkedRepository && linkedProfile) && workload.classification?.type !== "stale-link";
|
||||||
|
const inconsistentLink = claimsLink && !linked;
|
||||||
const classification = workload.classification?.type || workload.status || "review";
|
const classification = workload.classification?.type || workload.status || "review";
|
||||||
const statusTone = linked && !workload.reviewDecisionStale ? "success" : ["ambiguous", "duplicate", "orphan-container"].includes(classification) || workload.reviewDecisionStale ? "danger" : "warning";
|
const statusTone = linked && !workload.reviewDecisionStale ? "success" : inconsistentLink || ["ambiguous", "duplicate", "orphan-container"].includes(classification) || workload.reviewDecisionStale ? "danger" : "warning";
|
||||||
const detail = workload.compose?.project
|
const detail = workload.compose?.project
|
||||||
? `Compose ${workload.compose.project} · ${(workload.compose.services || []).join(", ") || "services unknown"}`
|
? `Compose ${workload.compose.project} · ${(workload.compose.services || []).join(", ") || "services unknown"}`
|
||||||
: workload.dockerMan?.templatePath
|
: workload.dockerMan?.templatePath
|
||||||
@@ -541,6 +574,8 @@ function renderServerInventory() {
|
|||||||
: `Container installation · ${containers || "unnamed"}`;
|
: `Container installation · ${containers || "unnamed"}`;
|
||||||
const candidate = linked
|
const candidate = linked
|
||||||
? `Linked to ${workload.link?.repositoryFullName || "repository"}`
|
? `Linked to ${workload.link?.repositoryFullName || "repository"}`
|
||||||
|
: inconsistentLink
|
||||||
|
? `Stored link cannot be resolved to a loaded repository profile`
|
||||||
: topCandidate
|
: topCandidate
|
||||||
? `${topCandidate.repositoryFullName} suggested · ${topCandidate.confidence || topCandidate.status || "review required"}`
|
? `${topCandidate.repositoryFullName} suggested · ${topCandidate.confidence || topCandidate.status || "review required"}`
|
||||||
: "No repository candidate; select one manually";
|
: "No repository candidate; select one manually";
|
||||||
@@ -549,10 +584,19 @@ function renderServerInventory() {
|
|||||||
? `<button class="button primary" data-action="quick-link-server-workload" data-server-id="${attr(server.serverId)}" data-workload-id="${attr(workload.workloadId)}" data-repository="${attr(topCandidate.repositoryFullName)}">${icon("link")}Link to ${escapeHtml(topCandidate.repositoryName || topCandidate.repositoryFullName)}</button>`
|
? `<button class="button primary" data-action="quick-link-server-workload" data-server-id="${attr(server.serverId)}" data-workload-id="${attr(workload.workloadId)}" data-repository="${attr(topCandidate.repositoryFullName)}">${icon("link")}Link to ${escapeHtml(topCandidate.repositoryName || topCandidate.repositoryFullName)}</button>`
|
||||||
: `<button class="button primary" data-action="link-server-workload" data-server-id="${attr(server.serverId)}" data-workload-id="${attr(workload.workloadId)}">${icon("link")}Review & link</button>`;
|
: `<button class="button primary" data-action="link-server-workload" data-server-id="${attr(server.serverId)}" data-workload-id="${attr(workload.workloadId)}">${icon("link")}Review & link</button>`;
|
||||||
const evidenceNote = workload.reviewDecisionStale ? "Saved decision is stale because server evidence changed" : workload.classification?.reason || "Awaiting review";
|
const evidenceNote = workload.reviewDecisionStale ? "Saved decision is stale because server evidence changed" : workload.classification?.reason || "Awaiting review";
|
||||||
return `<div class="tool-row"><div><strong>${escapeHtml(workload.displayName)}</strong><span>${escapeHtml(detail)} · ${workload.runtime?.running ? "running" : "stopped"}</span><span>${escapeHtml(candidate)}</span><span class="${workload.reviewDecisionStale ? "text-warning" : "meta"}">${escapeHtml(evidenceNote)}</span>${workload.metadata?.composeDefinitionError ? `<span class="text-warning">Compose file found; validation warning: ${escapeHtml(workload.metadata.composeDefinitionError)}</span>` : ""}</div><div class="stack horizontal compact"><span class="status-pill ${statusTone}">${escapeHtml(workload.reviewDecisionStale ? "Decision stale" : linked ? "Linked" : classification)}</span>${linked ? `<button class="button ghost" data-action="edit-deployment-profile" data-profile-id="${attr(workload.link?.profileId || "")}">Open link</button>` : linkButton}</div></div>`;
|
return `<div class="tool-row"><div><strong>${escapeHtml(workload.displayName)}</strong><span>${escapeHtml(detail)} · ${workload.runtime?.running ? "running" : "stopped"}</span><span>${escapeHtml(candidate)}</span><span class="${workload.reviewDecisionStale || inconsistentLink ? "text-warning" : "meta"}">${escapeHtml(inconsistentLink ? "Reconcile this inventory link before deployment" : evidenceNote)}</span>${workload.metadata?.composeDefinitionError ? `<span class="text-warning">Compose file found; validation warning: ${escapeHtml(workload.metadata.composeDefinitionError)}</span>` : ""}</div><div class="stack horizontal compact"><span class="status-pill ${statusTone}">${escapeHtml(workload.reviewDecisionStale ? "Decision stale" : linked ? "Linked" : inconsistentLink ? "Link unresolved" : classification)}</span>${linked ? `<button class="button ghost" data-action="open-deployment-link" data-repository-id="${attr(linkedRepository.id)}" data-profile-id="${attr(linkedProfile.id)}">Open in repository</button>` : inconsistentLink ? `<button class="button" data-action="plan-server-reconciliation" data-server-id="${attr(server.serverId)}">Reconcile</button>` : linkButton}</div></div>`;
|
||||||
}).join("")
|
}).join("")
|
||||||
: `<div class="empty-state compact"><p>${server.error ? "No inventory could be read until the SSH connection works." : "Docker returned no containers, Compose projects or DockerMan templates."}</p></div>`;
|
: `<div class="empty-state compact"><p>${server.error ? "No inventory could be read until the SSH connection works." : "Docker returned no containers, Compose projects or DockerMan templates."}</p></div>`;
|
||||||
return `<section class="panel server-inventory-panel"><div class="panel-header"><div><h3>${escapeHtml(server.serverName || server.server?.name || server.serverId)}</h3><span class="meta">${server.running || 0} running · ${server.linked || 0} repository links · ${visibleWorkloads.filter((workload) => !workload.link).length} to review${hiddenCount ? ` · ${hiddenCount} unrelated/system workloads hidden` : ""}</span></div><div class="stack horizontal compact"><span class="status-pill ${server.error ? "danger" : capabilities.docker && capabilities.compose ? "success" : "warning"}">${server.error ? "Scan failed" : escapeHtml(capabilityText)}</span>${server.error ? "" : `<button class="button" data-action="plan-server-reconciliation" data-server-id="${attr(server.serverId)}">${icon("shield")}Review reconciliation</button>`}</div></div><div class="panel-body">${errorBlock}${warnings}<div class="tool-list">${workloads}</div></div></section>`;
|
const resolvedLinks = visibleWorkloads.filter((workload) => {
|
||||||
|
const repository = ui.repositories.find((item) => String(item.fullName).toLowerCase() === String(workload.link?.repositoryFullName || "").toLowerCase());
|
||||||
|
return repository?.deploymentProfiles?.some((profile) => profile.id === workload.link?.profileId);
|
||||||
|
}).length;
|
||||||
|
const unresolvedLinks = visibleWorkloads.filter((workload) => {
|
||||||
|
if (!(workload.status === "linked" || workload.link)) return false;
|
||||||
|
const repository = ui.repositories.find((item) => String(item.fullName).toLowerCase() === String(workload.link?.repositoryFullName || "").toLowerCase());
|
||||||
|
return !repository?.deploymentProfiles?.some((profile) => profile.id === workload.link?.profileId);
|
||||||
|
}).length;
|
||||||
|
return `<section class="panel server-inventory-panel"><div class="panel-header"><div><h3>${escapeHtml(server.serverName || server.server?.name || server.serverId)}</h3><span class="meta">${server.running || 0} running · ${resolvedLinks} visible repository links${unresolvedLinks ? ` · ${unresolvedLinks} unresolved` : ""} · ${visibleWorkloads.filter((workload) => !workload.link).length} to review${hiddenCount ? ` · ${hiddenCount} unrelated/system workloads hidden` : ""}</span></div><div class="stack horizontal compact"><span class="status-pill ${server.error ? "danger" : capabilities.docker && capabilities.compose ? "success" : "warning"}">${server.error ? "Scan failed" : escapeHtml(capabilityText)}</span>${server.error ? "" : `<button class="button" data-action="plan-server-reconciliation" data-server-id="${attr(server.serverId)}">${icon("shield")}Review reconciliation</button>`}</div></div><div class="panel-body">${errorBlock}${warnings}<div class="tool-list">${workloads}</div></div></section>`;
|
||||||
}).join("");
|
}).join("");
|
||||||
const empty = configuredServers.length
|
const empty = configuredServers.length
|
||||||
? `<div class="empty-state panel"><h3>Server inventory has not completed</h3><p>ForgeFlow will query Docker directly. A failed connection is shown explicitly instead of being reported as zero deployments.</p><button class="button primary" data-action="scan-server-inventory">Scan servers now</button></div>`
|
? `<div class="empty-state panel"><h3>Server inventory has not completed</h3><p>ForgeFlow will query Docker directly. A failed connection is shown explicitly instead of being reported as zero deployments.</p><button class="button primary" data-action="scan-server-inventory">Scan servers now</button></div>`
|
||||||
|
|||||||
@@ -135,6 +135,17 @@ test("deployment inventory supports dense workloads without ambiguous blank card
|
|||||||
for (let index = 0; index < Math.min(count, 25); index += 1) {
|
for (let index = 0; index < Math.min(count, 25); index += 1) {
|
||||||
await expect(cards.nth(index)).not.toHaveText(/^\s*$/);
|
await expect(cards.nth(index)).not.toHaveText(/^\s*$/);
|
||||||
}
|
}
|
||||||
|
const unresolved = page.locator(".tool-row", { hasText: "Legacy Worker" });
|
||||||
|
await expect(unresolved).toContainText("Link unresolved");
|
||||||
|
await expect(unresolved).not.toContainText(/^Linked$/);
|
||||||
|
await expect(page.locator(".server-inventory-panel").first()).toContainText("1 unresolved");
|
||||||
|
const repositoryLink = page.locator('[data-action="open-deployment-link"]');
|
||||||
|
if (await repositoryLink.count()) {
|
||||||
|
await repositoryLink.first().click();
|
||||||
|
await expect(page.locator('.tab[data-action="repo-tab"][data-tab="deployments"]')).toHaveClass(/active/);
|
||||||
|
await expect(page.locator(".repository-workloads")).toBeVisible();
|
||||||
|
await expect(page.locator(".repository-workload-row").first()).toContainText("Repository linked");
|
||||||
|
}
|
||||||
await assertSurface(page);
|
await assertSurface(page);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,27 @@ import path from "node:path";
|
|||||||
import { createRequire } from "node:module";
|
import { createRequire } from "node:module";
|
||||||
import { EventEmitter } from "node:events";
|
import { EventEmitter } from "node:events";
|
||||||
import { createHash } from "node:crypto";
|
import { createHash } from "node:crypto";
|
||||||
|
import { execFile, spawn } from "node:child_process";
|
||||||
|
import { promisify } from "node:util";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { setTimeout as delay } from "node:timers/promises";
|
||||||
const require = createRequire(import.meta.url);
|
const require = createRequire(import.meta.url);
|
||||||
|
const execFileAsync = promisify(execFile);
|
||||||
const {
|
const {
|
||||||
UpdateService,
|
UpdateService,
|
||||||
waitForUpdaterStarted,
|
waitForUpdaterStarted,
|
||||||
|
windowsUpdaterSpawnOptions,
|
||||||
} = require("../src/main/update-service.cjs");
|
} = require("../src/main/update-service.cjs");
|
||||||
|
|
||||||
|
test("Windows updater uses a hidden non-detached PowerShell child", () => {
|
||||||
|
assert.deepEqual(windowsUpdaterSpawnOptions("C:\\updates"), {
|
||||||
|
detached: false,
|
||||||
|
stdio: "ignore",
|
||||||
|
windowsHide: true,
|
||||||
|
cwd: "C:\\updates",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("update check pins version to an exact branch commit", async () => {
|
test("update check pins version to an exact branch commit", async () => {
|
||||||
const temp = await mkdtemp(path.join(os.tmpdir(), "forgeflow-update-test-"));
|
const temp = await mkdtemp(path.join(os.tmpdir(), "forgeflow-update-test-"));
|
||||||
const saved = [];
|
const saved = [];
|
||||||
@@ -318,7 +333,7 @@ test("PowerShell helper replaces an existing launching status with a Windows-saf
|
|||||||
);
|
);
|
||||||
assert.match(
|
assert.match(
|
||||||
script,
|
script,
|
||||||
/System\.IO\.File\]::Replace\(\$temporary, \$StatusPath, \$null\)/,
|
/System\.IO\.File\]::Replace\(\$temporary, \$StatusPath, \$backup\)/,
|
||||||
);
|
);
|
||||||
assert.match(
|
assert.match(
|
||||||
script,
|
script,
|
||||||
@@ -332,6 +347,69 @@ test("PowerShell helper replaces an existing launching status with a Windows-saf
|
|||||||
assert.match(script, /Handshake-only verification completed successfully/);
|
assert.match(script, /Handshake-only verification completed successfully/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("binary helper confirms startup through real Windows PowerShell", { skip: process.platform !== "win32" }, async () => {
|
||||||
|
const temp = await mkdtemp(path.join(os.tmpdir(), "forgeflow-binary-handshake-"));
|
||||||
|
const statusPath = path.join(temp, "status.json");
|
||||||
|
const logPath = path.join(temp, "helper.log");
|
||||||
|
await writeFile(statusPath, JSON.stringify({ state: "launching", updateId: "binary-handshake" }));
|
||||||
|
const powershell = path.join(process.env.SystemRoot || process.env.WINDIR, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
||||||
|
const scriptPath = fileURLToPath(new URL("../scripts/apply-binary-update.ps1", import.meta.url));
|
||||||
|
const { stdout, stderr } = await execFileAsync(powershell, [
|
||||||
|
"-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", scriptPath,
|
||||||
|
"-BinaryPath", path.join(temp, "unused.exe"), "-ExpectedSha256", "0".repeat(64),
|
||||||
|
"-ExpectedVersion", "9.9.9", "-CurrentExecutable", path.join(temp, "unused-current.exe"),
|
||||||
|
"-Portable", "False", "-ParentPid", "999999", "-LogPath", logPath,
|
||||||
|
"-StatusPath", statusPath, "-UpdateId", "binary-handshake", "-HandshakeOnly"
|
||||||
|
], { windowsHide: true });
|
||||||
|
assert.equal(stdout, "");
|
||||||
|
assert.equal(stderr, "");
|
||||||
|
const status = JSON.parse(await readFile(statusPath, "utf8"));
|
||||||
|
assert.equal(status.updateId, "binary-handshake");
|
||||||
|
assert.equal(status.state, "started");
|
||||||
|
assert.match(await readFile(logPath, "utf8"), /Handshake-only verification completed successfully/);
|
||||||
|
await rm(temp, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("binary helper confirms startup through the production Node spawn options", { skip: process.platform !== "win32" }, async () => {
|
||||||
|
const temp = await mkdtemp(path.join(os.tmpdir(), "forgeflow-binary-node-spawn-"));
|
||||||
|
const statusPath = path.join(temp, "status.json");
|
||||||
|
const logPath = path.join(temp, "helper.log");
|
||||||
|
const powershell = path.join(process.env.SystemRoot || process.env.WINDIR, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
||||||
|
const scriptPath = fileURLToPath(new URL("../scripts/apply-binary-update.ps1", import.meta.url));
|
||||||
|
const updateId = "binary-node-spawn";
|
||||||
|
await writeFile(statusPath, JSON.stringify({ state: "launching", updateId }));
|
||||||
|
const child = spawn(powershell, [
|
||||||
|
"-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", scriptPath,
|
||||||
|
"-BinaryPath", path.join(temp, "unused.exe"), "-ExpectedSha256", "0".repeat(64),
|
||||||
|
"-ExpectedVersion", "9.9.9", "-CurrentExecutable", path.join(temp, "unused-current.exe"),
|
||||||
|
"-Portable", "False", "-ParentPid", String(process.pid), "-LogPath", logPath,
|
||||||
|
"-StatusPath", statusPath, "-UpdateId", updateId, "-HandshakeOnly",
|
||||||
|
], windowsUpdaterSpawnOptions(temp));
|
||||||
|
const childState = { exited: false, code: null, error: null };
|
||||||
|
child.once("error", (error) => { childState.error = error; });
|
||||||
|
child.once("exit", (code) => { childState.exited = true; childState.code = code; });
|
||||||
|
const status = await waitForUpdaterStarted(statusPath, {
|
||||||
|
timeoutMs: 5000,
|
||||||
|
pollMs: 25,
|
||||||
|
childState,
|
||||||
|
expectedUpdateId: updateId,
|
||||||
|
logPath,
|
||||||
|
});
|
||||||
|
assert.equal(status.state, "started");
|
||||||
|
let log = "";
|
||||||
|
for (let attempt = 0; attempt < 40 && !log.includes("Handshake-only verification completed successfully"); attempt += 1) {
|
||||||
|
await delay(25);
|
||||||
|
log = await readFile(logPath, "utf8").catch(() => "");
|
||||||
|
}
|
||||||
|
assert.match(log, /Handshake-only verification completed successfully/);
|
||||||
|
if (child.exitCode === null) {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
child.once("exit", resolve);
|
||||||
|
child.once("error", reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await rm(temp, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
|
||||||
|
});
|
||||||
test("early helper exit reports the helper log instead of only an exit code", async () => {
|
test("early helper exit reports the helper log instead of only an exit code", async () => {
|
||||||
const temp = await mkdtemp(
|
const temp = await mkdtemp(
|
||||||
path.join(os.tmpdir(), "forgeflow-update-log-tail-"),
|
path.join(os.tmpdir(), "forgeflow-update-log-tail-"),
|
||||||
|
|||||||
Reference in New Issue
Block a user