1 Commits
Author SHA1 Message Date
NuklearRabbit f7d6bc374f fix: reconcile server deployments across repository views
ForgeFlow quality gate / quality (push) Canceled after 0s
2026-08-01 18:57:55 +02:00
11 changed files with 86 additions and 32 deletions
+1 -1
View File
@@ -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.6** · [download de laatste Windows-release](https://gitea.itworx.tech/Jens/ForgeFlow/releases/latest) > Huidige release: **0.10.7** · [download de laatste Windows-release](https://gitea.itworx.tech/Jens/ForgeFlow/releases/latest)
![ForgeFlow release-overzicht](docs/screenshots/overview.png) ![ForgeFlow release-overzicht](docs/screenshots/overview.png)
+11 -10
View File
@@ -1,4 +1,4 @@
ForgeFlow 0.10.6 source manifest ForgeFlow 0.10.7 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
@@ -39,6 +39,7 @@ f79908fb3dad98c38030c6e6be7c79a1999e0478ed9c2496923891954438daa1 4581
8f4a0fe6dc250ae210cc2fc1c57c46091822ae6c2a58caa76e0091f255f9f30d 775 docs/RELEASE_NOTES_0.10.4.md 8f4a0fe6dc250ae210cc2fc1c57c46091822ae6c2a58caa76e0091f255f9f30d 775 docs/RELEASE_NOTES_0.10.4.md
05ed618f5a74a854363930128ca98939808517eedd28b9a508660a0c46e91d97 884 docs/RELEASE_NOTES_0.10.5.md 05ed618f5a74a854363930128ca98939808517eedd28b9a508660a0c46e91d97 884 docs/RELEASE_NOTES_0.10.5.md
94bfb2783c1befad1197e1c5e32fc002222c94a28d70d48360a8d53ecd260d5c 772 docs/RELEASE_NOTES_0.10.6.md 94bfb2783c1befad1197e1c5e32fc002222c94a28d70d48360a8d53ecd260d5c 772 docs/RELEASE_NOTES_0.10.6.md
226a3b2d4bc7f54841749a283fcdd71b643cd585ba74d673084bee829fef6ea2 903 docs/RELEASE_NOTES_0.10.7.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
@@ -100,14 +101,14 @@ 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
32b8a6a210910623eb078956a2f06a33b2649d60333a567e620d46c6ed4bcc0d 179806 package-lock.json df9841fba56ae74ff14800075d69b92ed24e0aca89085f47b0b3283a9321afc8 179806 package-lock.json
681fb8a118b372ad9b62f8940d7c921eb20267c4eaf8a99431370f8d64d39992 5550 package.json 4be2b648fda413f2f188309691d5a68eeabcad4fa6cca52be0c9b448b425c618 5588 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
51dff94dcdf3ae86ff1d544bb52eeab2ab51421a51b35c5dbe8f7c9f8d23c50a 10313 README.md e4468143ec620cc5369065f1e51ef378fdcf3de4927f3280119ac01c050c3fc4 10313 README.md
8f8dc5ff0c2967563457e4027d795f6f515253e0a459ecca5a9f5bcf0b658870 14650 reports/architecture-audit.json 8f8dc5ff0c2967563457e4027d795f6f515253e0a459ecca5a9f5bcf0b658870 14650 reports/architecture-audit.json
5e923f0096895f588fe66b187779cdb7cb5af6644010c6dcc4d55876930ae376 1220 reports/architecture-audit.md 5e923f0096895f588fe66b187779cdb7cb5af6644010c6dcc4d55876930ae376 1220 reports/architecture-audit.md
509c7bcff5280349bd9f45ed6151f70372bad7010a9ea582c13e2ccab91fe0cd 6272 scripts/acceptance.mjs 509c7bcff5280349bd9f45ed6151f70372bad7010a9ea582c13e2ccab91fe0cd 6272 scripts/acceptance.mjs
@@ -123,7 +124,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
85134984297c90a5de812c075d7f75eaa5242d5ee33713c6a4a2fe9703f82be9 19210 scripts/verify.mjs bc88d892ea1bb43721787fafa04d0fa2684ee8faa03e3a33d7cfc8a7984c56fe 19558 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
@@ -156,7 +157,7 @@ afef3841a3948b2121f8fba809aae4ea3da71bd2fda86973ba50200a5b1f89b2 14894
2ede80cd1565a7f2c282cc58d35dc0889d58d7465346bc723026b9c8be4df0ac 9501 src/main/unraid-deploy-key-host.cjs 2ede80cd1565a7f2c282cc58d35dc0889d58d7465346bc723026b9c8be4df0ac 9501 src/main/unraid-deploy-key-host.cjs
803a079499f7b8148495209dea43b505f6eb9bb587de19e82054e47183186c6e 30461 src/main/unraid-deployment-methods.cjs 803a079499f7b8148495209dea43b505f6eb9bb587de19e82054e47183186c6e 30461 src/main/unraid-deployment-methods.cjs
2e63fdc0be0bf4d8e5c3d7d45ff5811d786b0821a08b82f511f1301696e12c9d 17157 src/main/unraid-deployment-service.cjs 2e63fdc0be0bf4d8e5c3d7d45ff5811d786b0821a08b82f511f1301696e12c9d 17157 src/main/unraid-deployment-service.cjs
f0861356c1ff4ca361c7004c1f7d935858f51cc74335a7ad2136021f2e612220 36016 src/main/unraid-inventory-methods.cjs e998db9bc9a51b5ef182ef23f7e286af8b92c34903fbc7d6211ef1218701cc79 37937 src/main/unraid-inventory-methods.cjs
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
@@ -178,10 +179,10 @@ eef2f269ba4fbb76bf66ad328d481b461255d0acb753b30878dd4d4eaac57dc6 6924
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
5a2c0616a8a0ba23c182a8928abdf40a69612ce34c4cba47b1fd5f21fe5843d4 28617 src/renderer/mock-deployment-bridge.js 5a2c0616a8a0ba23c182a8928abdf40a69612ce34c4cba47b1fd5f21fe5843d4 28617 src/renderer/mock-deployment-bridge.js
b30f44e8e4285e40e7ef87474aeb34467a40dbedc9023a080deaa204ce84bbeb 20032 src/renderer/mock-repository-bridge.js 87859b1a04d39d1f5a47ce1f3f2e280367d569453f50b546d60e02a1604f216b 20032 src/renderer/mock-repository-bridge.js
94fa265c2fe9ca8d644f0ce9b620b6f85d9b25dca5802c4e9195b66dcbe80120 6522 src/renderer/operations.js 94fa265c2fe9ca8d644f0ce9b620b6f85d9b25dca5802c4e9195b66dcbe80120 6522 src/renderer/operations.js
1fc4c28859f7283aa2b771a50f1fd26addc8862b97a09250f806930ab4483f7a 79874 src/renderer/styles.css 1fc4c28859f7283aa2b771a50f1fd26addc8862b97a09250f806930ab4483f7a 79874 src/renderer/styles.css
5230c238acb87381303ec30026906cf8a7163c33380859d351b268adfc26b4f4 100196 src/renderer/views.js 8d366214f5ce9a87911439632de1c68fe5724e564412f651190c4066f9818fac 101196 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
@@ -195,7 +196,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
b46c9624c20bae767e50b4ab85e6b0ea6a9ee4a5c5c30f55554217f25134f1e7 11218 tests/browser/forgeflow.spec.mjs 809bb7addda0762098289451b4df66bde87f75f730cacc7aa7c56ecae3ff29da 11405 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
@@ -226,7 +227,7 @@ e631e9ca49a5bac7075860aac2ff4d377a32a78377b70e06ecf833f0f192fd5f 11552
020eccfa9c4aef7a4ac4736d9af90518fcb6d1ad75aedcfaa1c92832a9e3d6d8 4609 tests/shell-verification.test.mjs 020eccfa9c4aef7a4ac4736d9af90518fcb6d1ad75aedcfaa1c92832a9e3d6d8 4609 tests/shell-verification.test.mjs
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 eff3cc63d2622a4924e43516b3b00bef61fa62a489995bebaee5c48b11cdf8c9 48945 tests/unraid-deployment.test.mjs
9c58c600d925db6a3ad526e661010c05619e24d61c9d19ec0e207a09ae4cb93a 23981 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
+10
View File
@@ -0,0 +1,10 @@
# ForgeFlow 0.10.7
## Reliable server-to-repository recognition
- Live, running workloads with one unique exact provenance or runtime-identity match are now linked automatically during normal server discovery.
- Automatic adoption creates only ForgeFlow configuration and observed state; it performs no container changes and never automatically removes stale profiles.
- Ambiguous, duplicate, external and monitoring-only workloads remain behind explicit **Review & link** confirmation.
- Every linked repository now displays an `S` deployment badge with its profile count in the repository sidebar.
- The repository release rail reports **Linked** with container and server identity even when a legacy workload has no verifiable live commit yet.
- DevRunbook-style DockerMan deployments therefore show the same linked relationship in Deployments, the repository sidebar and the repository workspace.
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "forgeflow", "name": "forgeflow",
"version": "0.10.6", "version": "0.10.7",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "forgeflow", "name": "forgeflow",
"version": "0.10.6", "version": "0.10.7",
"dependencies": { "dependencies": {
"ssh2": "1.17.0" "ssh2": "1.17.0"
}, },
+2 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "forgeflow", "name": "forgeflow",
"version": "0.10.6", "version": "0.10.7",
"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",
@@ -111,6 +111,7 @@
"docs/RELEASE_NOTES_0.10.4.md", "docs/RELEASE_NOTES_0.10.4.md",
"docs/RELEASE_NOTES_0.10.5.md", "docs/RELEASE_NOTES_0.10.5.md",
"docs/RELEASE_NOTES_0.10.6.md", "docs/RELEASE_NOTES_0.10.6.md",
"docs/RELEASE_NOTES_0.10.7.md",
"docs/CURRENT_STATE.md", "docs/CURRENT_STATE.md",
"docs/MUTATION_MODEL.md", "docs/MUTATION_MODEL.md",
"docs/RELEASING.md", "docs/RELEASING.md",
+7 -2
View File
@@ -86,6 +86,7 @@ const required = [
"docs/RELEASE_NOTES_0.10.4.md", "docs/RELEASE_NOTES_0.10.4.md",
"docs/RELEASE_NOTES_0.10.5.md", "docs/RELEASE_NOTES_0.10.5.md",
"docs/RELEASE_NOTES_0.10.6.md", "docs/RELEASE_NOTES_0.10.6.md",
"docs/RELEASE_NOTES_0.10.7.md",
"docs/UPDATING.md", "docs/UPDATING.md",
"docs/DIAGNOSTICS.md", "docs/DIAGNOSTICS.md",
"docs/DEPLOYMENT_SETUP.md", "docs/DEPLOYMENT_SETUP.md",
@@ -124,9 +125,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.6") if (packageJson.version !== "0.10.7")
throw new Error( throw new Error(
`Expected package version 0.10.6, got ${packageJson.version}.`, `Expected package version 0.10.7, got ${packageJson.version}.`,
); );
const sourceManifest = await readFile( const sourceManifest = await readFile(
path.join(root, "SOURCE_MANIFEST.txt"), path.join(root, "SOURCE_MANIFEST.txt"),
@@ -464,6 +465,10 @@ const release0106 = await readFile(path.join(root, "docs/RELEASE_NOTES_0.10.6.md
for (const phrase of ["detached", "PowerShell", "production Node spawn", "source updater", "one-time direct installation"]) { 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}`); if (!release0106.includes(phrase)) throw new Error(`0.10.6 release notes are missing: ${phrase}`);
} }
const release0107 = await readFile(path.join(root, "docs/RELEASE_NOTES_0.10.7.md"), "utf8");
for (const phrase of ["exact provenance", "automatic", "repository sidebar", "DevRunbook", "no container changes"]) {
if (!release0107.includes(phrase)) throw new Error(`0.10.7 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}`);
+31 -4
View File
@@ -384,15 +384,41 @@ function createUnraidInventoryMethods({
}; };
} }
async scanServerInventory(serverId, repositories) { async scanServerInventory(serverId, repositories, { autoLink = false } = {}) {
const { server, inventory, workloads } = await this.collectServerInventory(serverId, repositories); const { server, inventory, workloads } = await this.collectServerInventory(serverId, repositories);
const response = this.inventoryResponse(server, inventory, workloads); let adopted = 0;
const adoptedLinks = [];
if (autoLink) {
const plan = this.reconciliationPlan(server, workloads, repositories, { autoLink: true });
if (plan.additions.length) await this.store.createRecoverySnapshot?.(`automatic-server-links-${serverId}`);
const linkedRepositories = new Set(workloads
.filter((workload) => workload.link?.repositoryFullName)
.map((workload) => String(workload.link.repositoryFullName).toLowerCase()));
for (const addition of plan.additions) {
const workload = workloads.find((item) => item.workloadId === addition.workloadId);
const repository = (repositories || []).find((item) => String(item.fullName).toLowerCase() === String(addition.repositoryFullName).toLowerCase());
const key = String(repository?.fullName || "").toLowerCase();
if (!workload || !repository || linkedRepositories.has(key)) continue;
const linkSource = addition.evidence === "exact-provenance" ? "automatic" : "automatic-runtime-identity";
const profile = this.profileFromWorkload(repository, server, workload, { linkSource, deploymentMode: "server-git" });
const saved = await this.store.saveDeploymentProfile(repository.fullName, profile);
await this.saveWorkloadState(saved, workload, server);
workload.status = "linked";
workload.link = { status: "linked", profileId: saved.id, repositoryFullName: repository.fullName, source: linkSource };
linkedRepositories.add(key);
adopted += 1;
adoptedLinks.push({ repositoryFullName: repository.fullName, profileId: saved.id, workloadId: workload.workloadId });
}
}
const response = this.inventoryResponse(server, inventory, workloads, { adopted });
await this.diagnostics?.info("unraid.workloads.scanned", { await this.diagnostics?.info("unraid.workloads.scanned", {
serverId, serverId,
detected: response.detected, detected: response.detected,
linked: response.linked, linked: response.linked,
needsReview: response.needsReview, needsReview: response.needsReview,
readOnly: true, adopted,
adoptedLinks,
readOnly: !autoLink,
}); });
return response; return response;
} }
@@ -428,6 +454,7 @@ function createUnraidInventoryMethods({
evidence: candidate.exact ? "exact-provenance" : "exact-runtime-identity", evidence: candidate.exact ? "exact-provenance" : "exact-runtime-identity",
impact: "Create a server-pull deployment profile; no container changes", impact: "Create a server-pull deployment profile; no container changes",
}); });
linkedRepositories.add(String(candidate.repositoryFullName).toLowerCase());
} else if (["suggested", "ambiguous"].includes(workload.status) || (workload.runtime?.running && workload.candidates?.length)) { } else if (["suggested", "ambiguous"].includes(workload.status) || (workload.runtime?.running && workload.candidates?.length)) {
conflicts.push({ conflicts.push({
workloadId: workload.workloadId, workloadId: workload.workloadId,
@@ -559,7 +586,7 @@ function createUnraidInventoryMethods({
} }
async discoverServerWorkloads(serverId, repositories) { async discoverServerWorkloads(serverId, repositories) {
return this.scanServerInventory(serverId, repositories); return this.scanServerInventory(serverId, repositories, { autoLink: true });
} }
async linkServerWorkload({ repository, serverId, workloadId, deploymentMode = "server-git", remoteFolder = "" }) { async linkServerWorkload({ repository, serverId, workloadId, deploymentMode = "server-git", remoteFolder = "" }) {
+1 -1
View File
@@ -5,7 +5,7 @@ function createMockRepositoryBridge(context) {
await wait(80); await wait(80);
snapshot(); snapshot();
return { return {
appVersion: "0.10.6-demo", appVersion: "0.10.7-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" },
+12 -2
View File
@@ -31,6 +31,9 @@ function renderTitlebar() {
function renderRepositoryRow(repository) { function renderRepositoryRow(repository) {
const status = repository.localStatus; const status = repository.localStatus;
const profiles = repository.deploymentProfiles || [];
const workloads = linkedWorkloadsForRepository(repository);
const runningWorkloads = workloads.filter((workload) => workload.runtime?.running);
const badges = []; const badges = [];
if (status?.counts.conflicts) if (status?.counts.conflicts)
badges.push('<span class="mini-badge danger" title="Conflicts">!</span>'); badges.push('<span class="mini-badge danger" title="Conflicts">!</span>');
@@ -50,10 +53,14 @@ function renderRepositoryRow(repository) {
badges.push( badges.push(
'<span class="mini-badge success" title="Ready to deploy">↗</span>', '<span class="mini-badge success" title="Ready to deploy">↗</span>',
); );
if (profiles.length)
badges.push(
`<span class="mini-badge ${runningWorkloads.length ? "success" : "warning"} deployment-badge" title="${attr(`${profiles.length} server deployment${profiles.length === 1 ? "" : "s"} linked${runningWorkloads.length ? ` · ${runningWorkloads.length} running` : ""}`)}">S${profiles.length}</span>`,
);
if (!repository.localPath) if (!repository.localPath)
badges.push('<span class="mini-badge" title="No local folder">—</span>'); badges.push('<span class="mini-badge" title="No local folder">—</span>');
const branch = status?.branch.head || repository.defaultBranch || "remote"; const branch = status?.branch.head || repository.defaultBranch || "remote";
return `<button class="repo-row ${String(repository.id) === String(ui.selectedRepoId) ? "active" : ""} ${repository.attention ? "attention" : ""}" data-action="select-repo" data-id="${attr(repository.id)}"> return `<button class="repo-row ${String(repository.id) === String(ui.selectedRepoId) ? "active" : ""} ${repository.attention ? "attention" : ""}" data-action="select-repo" data-id="${attr(repository.id)}" data-deployment-count="${profiles.length}">
<span class="repo-icon">${repository.favorite ? icon("star") : icon(repository.localPath ? "git" : "cloud")}</span> <span class="repo-icon">${repository.favorite ? icon("star") : icon(repository.localPath ? "git" : "cloud")}</span>
<span class="repo-main"><span class="repo-name">${escapeHtml(repository.name)}</span><span class="repo-sub"><span>${escapeHtml(branch)}</span>${status?.shortHead ? `<span>• ${escapeHtml(status.shortHead)}</span>` : ""}</span></span> <span class="repo-main"><span class="repo-name">${escapeHtml(repository.name)}</span><span class="repo-sub"><span>${escapeHtml(branch)}</span>${status?.shortHead ? `<span>• ${escapeHtml(status.shortHead)}</span>` : ""}</span></span>
<span class="repo-badges">${badges.join("")}</span> <span class="repo-badges">${badges.join("")}</span>
@@ -432,6 +439,7 @@ function renderRepositoryWorkspace(repository) {
const profiles = repository.deploymentProfiles || []; const profiles = repository.deploymentProfiles || [];
const linkedWorkloads = linkedWorkloadsForRepository(repository); const linkedWorkloads = linkedWorkloadsForRepository(repository);
const profile = selectedProfile(repository); const profile = selectedProfile(repository);
const profileWorkload = linkedWorkloads.find((workload) => workload.link?.profileId === profile?.id);
const serverState = profile?.state || {}; const serverState = profile?.state || {};
const localTone = status?.counts.conflicts const localTone = status?.counts.conflicts
? "danger" ? "danger"
@@ -452,6 +460,8 @@ function renderRepositoryWorkspace(repository) {
? "danger" ? "danger"
: serverState.healthy === true : serverState.healthy === true
? "success" ? "success"
: profileWorkload?.runtime?.running
? "success"
: ""; : "";
const content = ( const content = (
{ {
@@ -474,7 +484,7 @@ function renderRepositoryWorkspace(repository) {
: ""; : "";
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) : profile ? "Linked" : "Unknown", profileWorkload ? `${profileWorkload.displayName || profile.containerName || "Container"} · ${profileWorkload.runtime?.running ? "running" : "stopped"} on ${profileWorkload.serverName}` : profile ? (serverState.checkedAt ? `checked ${formatDate(serverState.checkedAt)}` : "profile linked · awaiting live scan") : "No deployment profile", serverTone)}</div>
${deploymentLinks} ${deploymentLinks}
<nav class="tabs">${[ <nav class="tabs">${[
["changes", "Changes"], ["changes", "Changes"],
+2
View File
@@ -142,6 +142,8 @@ test("deployment inventory supports dense workloads without ambiguous blank card
const repositoryLink = page.locator('[data-action="open-deployment-link"]'); const repositoryLink = page.locator('[data-action="open-deployment-link"]');
if (await repositoryLink.count()) { if (await repositoryLink.count()) {
await repositoryLink.first().click(); await repositoryLink.first().click();
await expect(page.locator('.repo-row.active')).toHaveAttribute("data-deployment-count", /^[1-9]/);
await expect(page.locator('.repo-row.active .deployment-badge')).toBeVisible();
await expect(page.locator('.tab[data-action="repo-tab"][data-tab="deployments"]')).toHaveClass(/active/); 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-workloads")).toBeVisible();
await expect(page.locator(".repository-workload-row").first()).toContainText("Repository linked"); await expect(page.locator(".repository-workload-row").first()).toContainText("Repository linked");
+7 -9
View File
@@ -168,7 +168,7 @@ test("server workload inventory links running containers to exact Gitea checkout
); );
}); });
test("server discovery is read-only and explicit reconciliation adopts a verified deployment", async () => { test("low-level inventory scan is read-only and user discovery auto-links exact provenance", async () => {
const b64 = (value) => Buffer.from(value).toString("base64"); const b64 = (value) => Buffer.from(value).toString("base64");
const sha = "b".repeat(40); const sha = "b".repeat(40);
const container = { const container = {
@@ -218,17 +218,15 @@ test("server discovery is read-only and explicit reconciliation adopts a verifie
sshUrl: "git@gitea.itworx.tech:Jens/Portfolio.git", sshUrl: "git@gitea.itworx.tech:Jens/Portfolio.git",
}, },
]; ];
const discovery = await service.discoverServerWorkloads("unraid", repositories); const readOnlyDiscovery = await service.scanServerInventory("unraid", repositories);
assert.equal(discovery.adopted, 0); assert.equal(readOnlyDiscovery.adopted, 0);
assert.equal(discovery.verified, 0); assert.equal(readOnlyDiscovery.verified, 0);
assert.equal(profiles.length, 0); assert.equal(profiles.length, 0);
assert.equal(states.size, 0); assert.equal(states.size, 0);
const preview = await service.planServerInventoryReconciliation("unraid", repositories, { autoLink: true }); const discovery = await service.discoverServerWorkloads("unraid", repositories);
assert.equal(preview.plan.summary.additions, 1); assert.equal(discovery.adopted, 1);
const result = await service.reconcileServerInventory("unraid", repositories, { autoLink: true, expectedPlanId: preview.plan.id }); assert.equal(discovery.verified, 1);
assert.equal(result.adopted, 1);
assert.equal(result.verified, 1);
assert.equal(profiles[0].containerName, "Portfolio"); assert.equal(profiles[0].containerName, "Portfolio");
assert.equal(profiles[0].adoptedFromServer, true); assert.equal(profiles[0].adoptedFromServer, true);
assert.equal(states.get(profiles[0].id).matchesGitea, true); assert.equal(states.get(profiles[0].id).matchesGitea, true);