This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
ForgeFlow is een desktopapp voor teams die met Git, Gitea en eigen servers werken. De app toont wat lokaal gewijzigd is, wat al op Gitea staat en welke exacte commit op de server draait. Daarna begeleidt ForgeFlow je door review, commit, push, deployment en verificatie.
|
ForgeFlow is een desktopapp voor teams die met Git, Gitea en eigen servers werken. De app toont wat lokaal gewijzigd is, wat al op Gitea staat en welke exacte commit op de server draait. Daarna begeleidt ForgeFlow je door review, commit, push, deployment en verificatie.
|
||||||
|
|
||||||
> Huidige release: **0.10.0** · [download de laatste Windows-release](https://gitea.itworx.tech/Jens/ForgeFlow/releases/latest)
|
> Huidige release: **0.10.1** · [download de laatste Windows-release](https://gitea.itworx.tech/Jens/ForgeFlow/releases/latest)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
+15
-14
@@ -1,4 +1,4 @@
|
|||||||
ForgeFlow 0.10.0 source manifest
|
ForgeFlow 0.10.1 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
|
||||||
@@ -23,7 +23,7 @@ ca32a76e708d565c4af659f0f4d2615fc32114c3f75aec1454862a3ed1e72c41 2263
|
|||||||
c612fcc44ff222db0c9a4cfd11a4076fafe080e4ada31e689a08739a4f14e74f 1650 docs/ACCEPTANCE.md
|
c612fcc44ff222db0c9a4cfd11a4076fafe080e4ada31e689a08739a4f14e74f 1650 docs/ACCEPTANCE.md
|
||||||
a17f95d96d3c9fbc69d870874e6fbb7472091adefc454b24f835db1279511d72 8296 docs/ARCHITECTURE.md
|
a17f95d96d3c9fbc69d870874e6fbb7472091adefc454b24f835db1279511d72 8296 docs/ARCHITECTURE.md
|
||||||
e05458ee2696e3c57e2475bb42ae1f914f6a36e01768d7a26a3199f1fffed490 1157 docs/COVERAGE_POLICY.md
|
e05458ee2696e3c57e2475bb42ae1f914f6a36e01768d7a26a3199f1fffed490 1157 docs/COVERAGE_POLICY.md
|
||||||
72e846f591c47a0291e7466e58e052d3d5afcf551c4e6c848632ac3c552a1244 3043 docs/CURRENT_STATE.md
|
b9e39748ff125031be0ee8a963ef0d457c342f7998113fc2dd042ec237ae32ad 3084 docs/CURRENT_STATE.md
|
||||||
8ea655d1912ac2e17f8834e33a566a8b14461b396ec4268c396ca189a1749b94 2205 docs/DEPENDENCY_AUDIT.md
|
8ea655d1912ac2e17f8834e33a566a8b14461b396ec4268c396ca189a1749b94 2205 docs/DEPENDENCY_AUDIT.md
|
||||||
30a92bcf5daadb019efa2f82cb820ea302490dd1d68fb772674dc3faccd3e594 2045 docs/DEPLOYMENT_SETUP.md
|
30a92bcf5daadb019efa2f82cb820ea302490dd1d68fb772674dc3faccd3e594 2045 docs/DEPLOYMENT_SETUP.md
|
||||||
eb42f979666e05d51c587e4223282914926a2b9b1ade9f3fb75525019ce7f738 4616 docs/DIAGNOSTICS.md
|
eb42f979666e05d51c587e4223282914926a2b9b1ade9f3fb75525019ce7f738 4616 docs/DIAGNOSTICS.md
|
||||||
@@ -33,6 +33,7 @@ a0cd06a96f23a94e118feb012be0fa1ac51345951cb2ba8e67fb8c889c4c342a 5007
|
|||||||
8dc95f69e6f6c8415702c8e79fb6b466c4d60061afded9140450e7e558eaefe5 3704 docs/PRODUCTION_READINESS_1.0.md
|
8dc95f69e6f6c8415702c8e79fb6b466c4d60061afded9140450e7e558eaefe5 3704 docs/PRODUCTION_READINESS_1.0.md
|
||||||
f79908fb3dad98c38030c6e6be7c79a1999e0478ed9c2496923891954438daa1 4581 docs/RELEASE_AUDIT_0.6.0.md
|
f79908fb3dad98c38030c6e6be7c79a1999e0478ed9c2496923891954438daa1 4581 docs/RELEASE_AUDIT_0.6.0.md
|
||||||
979a0b8e129979be6b265e8571d0a3c1e9ddd4ddb6b0bf55ae748d3478e51854 2296 docs/RELEASE_NOTES_0.10.0.md
|
979a0b8e129979be6b265e8571d0a3c1e9ddd4ddb6b0bf55ae748d3478e51854 2296 docs/RELEASE_NOTES_0.10.0.md
|
||||||
|
0eb44bda2209a5979a6ac693ac4cd4d235c0015031e54b9e895990f37bf60054 1433 docs/RELEASE_NOTES_0.10.1.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
|
||||||
@@ -94,21 +95,21 @@ 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
|
||||||
bf3f507e676b3fd834240d72b945be4cd3b4018e5ebe570594eb27b8047c3969 179806 package-lock.json
|
d281af8b6fc3fc8a84bca985c6feb13c1edeaa72b5f0144816ea91dd53774a08 179806 package-lock.json
|
||||||
aaf36269e9636f942927a73254c1881e6e8fd886a1e4d5b4b8128404a62b25d3 5321 package.json
|
8587168403c255c8acb1bc829629f171598d7697df362836858a0fc89b500b78 5359 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
|
||||||
794bbe1937077788f34c64398fd73dc9a3c43d095084aa32673f3e968b115de2 9150 README.md
|
75602a7e0ce9744d5fc5a73869eec4b47877e00cfcfea68695373a04f8aa5f31 9150 README.md
|
||||||
3b2572a4d3a8aa3101bae6af49617e10062c43d72430314e1a31b04bc2933902 14329 reports/architecture-audit.json
|
1fa7bf646321e07e40d98f4a7529d5f748c600edd5283f62ee13574b4e97280f 14329 reports/architecture-audit.json
|
||||||
285dace9a76c800cca8d1222c9322690575c0a92c2a0d507d1b9910b02864b7c 1114 reports/architecture-audit.md
|
c1ff18f1367691332b189bb7589843a5e0bbde4817e3dd29df4ade7ea71dbd52 1114 reports/architecture-audit.md
|
||||||
509c7bcff5280349bd9f45ed6151f70372bad7010a9ea582c13e2ccab91fe0cd 6272 scripts/acceptance.mjs
|
509c7bcff5280349bd9f45ed6151f70372bad7010a9ea582c13e2ccab91fe0cd 6272 scripts/acceptance.mjs
|
||||||
00d57bda5af8c8eda294b72d18b318f024a307b81b0d9205a0821f5240151e31 3814 scripts/apply-binary-update.ps1
|
00d57bda5af8c8eda294b72d18b318f024a307b81b0d9205a0821f5240151e31 3814 scripts/apply-binary-update.ps1
|
||||||
f8359a69d20deb2dfe10042d1bec7b12a95e76e58e36bc5f265f073c3111d056 10287 scripts/apply-source-update.ps1
|
f8359a69d20deb2dfe10042d1bec7b12a95e76e58e36bc5f265f073c3111d056 10287 scripts/apply-source-update.ps1
|
||||||
02e924227f6cad3777fd06660230c85df590d8ce95e134194a4d18970a240b88 4145 scripts/architecture-audit.mjs
|
02e924227f6cad3777fd06660230c85df590d8ce95e134194a4d18970a240b88 4145 scripts/architecture-audit.mjs
|
||||||
fca922d7d1de598a0153f3828300c7d0327e0189c06a53a23d9fc33b36d45d91 4741 scripts/audit-installed-deployments.cjs
|
4490bed84761f76e1fd87ee3117fe53e82e60b7760329c97d68772d6d820ebae 8521 scripts/audit-installed-deployments.cjs
|
||||||
6d46dd6826069d842f20f9f22a99042257db936cdea0bee8d294d2d7ea290126 3893 scripts/doctor.mjs
|
6d46dd6826069d842f20f9f22a99042257db936cdea0bee8d294d2d7ea290126 3893 scripts/doctor.mjs
|
||||||
0244d42896b8c44f734d0bb6cdcb29b5981342be2f070ce89f8d9eaf3e4d49e6 1793 scripts/generate-source-manifest.mjs
|
0244d42896b8c44f734d0bb6cdcb29b5981342be2f070ce89f8d9eaf3e4d49e6 1793 scripts/generate-source-manifest.mjs
|
||||||
842436680521311594e798848b050ae4e488d0595f0de57315f6ec081c049fb9 1266 scripts/prune-dist.mjs
|
842436680521311594e798848b050ae4e488d0595f0de57315f6ec081c049fb9 1266 scripts/prune-dist.mjs
|
||||||
@@ -117,7 +118,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
|
||||||
50880ac76b7d681dc65019dc794efc3cea4ffd379507fe0518985312f5b39304 2096 scripts/verify-release-signatures.mjs
|
50880ac76b7d681dc65019dc794efc3cea4ffd379507fe0518985312f5b39304 2096 scripts/verify-release-signatures.mjs
|
||||||
e8c5d00737d4c5e2c37ff683e569e8d7a0510be233db3840460c78be69f2c4bc 17163 scripts/verify.mjs
|
fb18540c46c0be38bbb0133354838ad75c11187fe6bc0c61860d975fe7761008 17502 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
|
||||||
@@ -132,7 +133,7 @@ a2ef47d5330095b92c2bd22fcc39962091881f9cb60d02e261eb1dd1bd693170 1974
|
|||||||
b23dfa041d9f4597d144601ab8569e69ba748a0d7a075c3ef01154eacc2640ae 7018 src/main/git-validator-policy.cjs
|
b23dfa041d9f4597d144601ab8569e69ba748a0d7a075c3ef01154eacc2640ae 7018 src/main/git-validator-policy.cjs
|
||||||
ca050e9820528b555e6a1dfd89ac8be2f9a0cec6e91254897e6e3b6e116a7eb2 26317 src/main/git-validator-service.cjs
|
ca050e9820528b555e6a1dfd89ac8be2f9a0cec6e91254897e6e3b6e116a7eb2 26317 src/main/git-validator-service.cjs
|
||||||
2faaef0eeec1e473db82b94243674e3812ef5869358b0ec6abf8d645a8794623 20768 src/main/gitea-service.cjs
|
2faaef0eeec1e473db82b94243674e3812ef5869358b0ec6abf8d645a8794623 20768 src/main/gitea-service.cjs
|
||||||
8c3c7b9bf0893276f5ab520efba17fc143c64377128d754411cd911180025c4a 5743 src/main/inventory-classifier.cjs
|
2ad3b2e647377f687ad987fe248a142ad399ecac98e4b49965aa7efc6093e5fa 6914 src/main/inventory-classifier.cjs
|
||||||
dafdb09133d2b6ec2161a3f0b09354551e54fc606c8107976fca37405643be91 3404 src/main/inventory-review-service.cjs
|
dafdb09133d2b6ec2161a3f0b09354551e54fc606c8107976fca37405643be91 3404 src/main/inventory-review-service.cjs
|
||||||
00989577aed509a7ddfdf9f4df09a196a393a85b5ea59b21089002215e69f065 25949 src/main/ipc.cjs
|
00989577aed509a7ddfdf9f4df09a196a393a85b5ea59b21089002215e69f065 25949 src/main/ipc.cjs
|
||||||
0eb1cfdcd3a37a0ec9502bf753966f87230c03580335798bef9265add42ee6fa 12530 src/main/ipc/deployment-handlers.cjs
|
0eb1cfdcd3a37a0ec9502bf753966f87230c03580335798bef9265add42ee6fa 12530 src/main/ipc/deployment-handlers.cjs
|
||||||
@@ -141,7 +142,7 @@ dc9b5971c9fefe8c374aa31916f5513601ce86003fd48b1d0e51330a909ae3a5 3442
|
|||||||
62f2c80c8210e19370b8556b1f296cbae50dae6b758a39e209f8fb461691fd4c 4235 src/main/log-redaction.cjs
|
62f2c80c8210e19370b8556b1f296cbae50dae6b758a39e209f8fb461691fd4c 4235 src/main/log-redaction.cjs
|
||||||
958595a99fb242c127f475f3d8622bdba4c07b2d658703f69fe3992227a9107e 12909 src/main/preflight-service.cjs
|
958595a99fb242c127f475f3d8622bdba4c07b2d658703f69fe3992227a9107e 12909 src/main/preflight-service.cjs
|
||||||
3096b4181566cb93a27e56e248c92105d4f4df5aee39d73c6c7d8ae8c2231bc0 1570 src/main/process-runner.cjs
|
3096b4181566cb93a27e56e248c92105d4f4df5aee39d73c6c7d8ae8c2231bc0 1570 src/main/process-runner.cjs
|
||||||
a31f01516122a434f3fdd29c02dc548731d0bba66f2a889df21e62dc1880b7b5 10229 src/main/production-acceptance-harness.cjs
|
e64f7257d478955c675a133b3735b6afe138a69d2ad090898061e56f557c43e5 9926 src/main/production-acceptance-harness.cjs
|
||||||
e89b54e7e3174b4b0a1dcd9058d8344e29431f9d16d0e6bb8d11559b691440a0 2508 src/main/repository-monitor.cjs
|
e89b54e7e3174b4b0a1dcd9058d8344e29431f9d16d0e6bb8d11559b691440a0 2508 src/main/repository-monitor.cjs
|
||||||
17e2a53f61cd7faba461b9f332967143087eaac95b72001462292976278ca305 7782 src/main/repository-service.cjs
|
17e2a53f61cd7faba461b9f332967143087eaac95b72001462292976278ca305 7782 src/main/repository-service.cjs
|
||||||
52b6d88ed1f5c904a13cdde92e5f96d1e2b5971ceef49862152197353cdc6490 27928 src/main/server-inventory.cjs
|
52b6d88ed1f5c904a13cdde92e5f96d1e2b5971ceef49862152197353cdc6490 27928 src/main/server-inventory.cjs
|
||||||
@@ -150,7 +151,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
|
||||||
de0378e1344ff102552b01018451346c30c16a76eb00e7c4349c82f7b39d52a6 35210 src/main/unraid-inventory-methods.cjs
|
f0861356c1ff4ca361c7004c1f7d935858f51cc74335a7ad2136021f2e612220 36016 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
|
||||||
@@ -204,12 +205,12 @@ c00bbd8eae5cef7856c8283d6b40dedb81083bf57ad762e89ab79e0f312da351 3271
|
|||||||
73d00729696e5067ba33dd6d43b018d89ce7fdd561a60ab66648d3283fb54d21 5370 tests/git-validator.test.mjs
|
73d00729696e5067ba33dd6d43b018d89ce7fdd561a60ab66648d3283fb54d21 5370 tests/git-validator.test.mjs
|
||||||
681ab7bcd02c4dd98d1d8d2092a3521c489d941131e7ffe5903971b940046474 2403 tests/git-workflows.test.mjs
|
681ab7bcd02c4dd98d1d8d2092a3521c489d941131e7ffe5903971b940046474 2403 tests/git-workflows.test.mjs
|
||||||
ca2c2c47b2532a74c7a77b9473ff417e0a34f0dbd8801936fd0e301a38f06a9a 18128 tests/gitea-actions.test.mjs
|
ca2c2c47b2532a74c7a77b9473ff417e0a34f0dbd8801936fd0e301a38f06a9a 18128 tests/gitea-actions.test.mjs
|
||||||
8f260f35aaf162999ddcd0f851a4f215222d9de0880d602b8322facdaa4c2cb0 9190 tests/inventory-classifier.test.mjs
|
fcc9a063882840dd89d74c2785284c8f2f6a9e5acec482b6d89ed8de62efdb85 9635 tests/inventory-classifier.test.mjs
|
||||||
9643622a03ea0a88fb7d72ce43e469ff4f814902f4b3d2a672990637d66ef075 2009 tests/ipc-contract.test.mjs
|
9643622a03ea0a88fb7d72ce43e469ff4f814902f4b3d2a672990637d66ef075 2009 tests/ipc-contract.test.mjs
|
||||||
caf98cbd9de9b119dae610ee53fa333a7a11214f34762247452fbb85e8bbf725 2392 tests/log-redaction.test.mjs
|
caf98cbd9de9b119dae610ee53fa333a7a11214f34762247452fbb85e8bbf725 2392 tests/log-redaction.test.mjs
|
||||||
96432a97d313f331694900bf0a2c21e38c20eac96d59147977aeed9055a9e3ad 2287 tests/partial-staging.test.mjs
|
96432a97d313f331694900bf0a2c21e38c20eac96d59147977aeed9055a9e3ad 2287 tests/partial-staging.test.mjs
|
||||||
1b6c920e18a248f78acaed6187197c88ec8d911b62d5e2a9f8ad57b91ae80499 11827 tests/preflight.test.mjs
|
1b6c920e18a248f78acaed6187197c88ec8d911b62d5e2a9f8ad57b91ae80499 11827 tests/preflight.test.mjs
|
||||||
f89643919df44232b2b112cdf68fe332b438d3d39c7ecab976d74836de286788 6526 tests/production-acceptance.test.mjs
|
0cb884cf62c1cb02cf59a81662be055bcb5339d176de85e2a3eeb8e8573e11b3 6435 tests/production-acceptance.test.mjs
|
||||||
629ba26395c0b49cc5fdee6b0646d75369eb6338e1cc7b59509938f97eea08ec 9601 tests/renderer-workflow.test.mjs
|
629ba26395c0b49cc5fdee6b0646d75369eb6338e1cc7b59509938f97eea08ec 9601 tests/renderer-workflow.test.mjs
|
||||||
2b4956fa4df4624a04117737e57ba74020564330ff71303b5746d8ccc881e880 854 tests/repository-matching.test.mjs
|
2b4956fa4df4624a04117737e57ba74020564330ff71303b5746d8ccc881e880 854 tests/repository-matching.test.mjs
|
||||||
f679072548554a64974f0452337ce5e7b0c567343c287223770cc0974b905348 1068 tests/repository-monitor.test.mjs
|
f679072548554a64974f0452337ce5e7b0c567343c287223770cc0974b905348 1068 tests/repository-monitor.test.mjs
|
||||||
|
|||||||
@@ -51,4 +51,4 @@ ForgeFlow writes its configuration atomically. Explicit server reconciliation ad
|
|||||||
- Read-only repository-scoped deploy keys for server pull.
|
- Read-only repository-scoped deploy keys for server pull.
|
||||||
- SSH host-key changes fail closed.
|
- SSH host-key changes fail closed.
|
||||||
- Live commit, remote commit and runtime health remain separate evidence.
|
- Live commit, remote commit and runtime health remain separate evidence.
|
||||||
- Production signing must fail closed when the required external certificate is absent.
|
- Packaged updates must fail closed on missing or mismatched release assets and SHA-256 evidence; paid code signing is optional.
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# ForgeFlow 0.10.1
|
||||||
|
|
||||||
|
## Reliable certificate-free updates
|
||||||
|
|
||||||
|
- Windows installer and portable releases are supported without paid signing services.
|
||||||
|
- Packaged updates remain protected by exact Gitea release assets, PE validation and SHA-256 verification before staging and immediately before replacement.
|
||||||
|
- Old ForgeFlow versions are pruned from `dist` after each successful build.
|
||||||
|
|
||||||
|
## Deployment inventory correctness
|
||||||
|
|
||||||
|
- Repository matching is case-insensitive, so `Jens/Repo` and `jens/repo` refresh the same deployment profile.
|
||||||
|
- Running repository workloads are linked conservatively; third-party DockerMan applications remain visible as external monitoring-only workloads instead of generating hundreds of false repository problems.
|
||||||
|
- Historical and stopped duplicate definitions no longer require repetitive manual review.
|
||||||
|
- Shadowed automatic profiles are retired only after a recovery snapshot and a stable reviewed reconciliation plan.
|
||||||
|
- Live runtime, container health and commit evidence are refreshed before readiness is reported.
|
||||||
|
|
||||||
|
## Server pull verification
|
||||||
|
|
||||||
|
- Existing running repository workloads can receive repository-scoped read-only deploy keys without changing containers.
|
||||||
|
- The audit command supports compact inventory, reconciliation and access evidence for operational verification.
|
||||||
|
- Release acceptance no longer assumes an external Authenticode certificate while retaining checksum, provenance and SBOM checks.
|
||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "forgeflow",
|
"name": "forgeflow",
|
||||||
"version": "0.10.0",
|
"version": "0.10.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "forgeflow",
|
"name": "forgeflow",
|
||||||
"version": "0.10.0",
|
"version": "0.10.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ssh2": "1.17.0"
|
"ssh2": "1.17.0"
|
||||||
},
|
},
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "forgeflow",
|
"name": "forgeflow",
|
||||||
"version": "0.10.0",
|
"version": "0.10.1",
|
||||||
"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",
|
||||||
@@ -105,6 +105,7 @@
|
|||||||
"docs/RELEASE_NOTES_0.9.4.md",
|
"docs/RELEASE_NOTES_0.9.4.md",
|
||||||
"docs/RELEASE_NOTES_0.9.5.md",
|
"docs/RELEASE_NOTES_0.9.5.md",
|
||||||
"docs/RELEASE_NOTES_0.10.0.md",
|
"docs/RELEASE_NOTES_0.10.0.md",
|
||||||
|
"docs/RELEASE_NOTES_0.10.1.md",
|
||||||
"docs/CURRENT_STATE.md",
|
"docs/CURRENT_STATE.md",
|
||||||
"docs/MUTATION_MODEL.md",
|
"docs/MUTATION_MODEL.md",
|
||||||
"docs/RELEASING.md",
|
"docs/RELEASING.md",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"generatedAt": "2026-07-29T17:40:19.842Z",
|
"generatedAt": "2026-07-29T22:49:48.766Z",
|
||||||
"thresholds": {
|
"thresholds": {
|
||||||
"preferredMaximumLines": 750,
|
"preferredMaximumLines": 750,
|
||||||
"justificationRequiredLines": 1000
|
"justificationRequiredLines": 1000
|
||||||
@@ -144,6 +144,21 @@
|
|||||||
],
|
],
|
||||||
"hotspotScore": 93
|
"hotspotScore": 93
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"file": "src/main/unraid-inventory-methods.cjs",
|
||||||
|
"lines": 591,
|
||||||
|
"branches": 61,
|
||||||
|
"functions": 76,
|
||||||
|
"ipcHandlers": 0,
|
||||||
|
"responsibilities": [
|
||||||
|
"inventory",
|
||||||
|
"deployment",
|
||||||
|
"git",
|
||||||
|
"security",
|
||||||
|
"updates"
|
||||||
|
],
|
||||||
|
"hotspotScore": 91
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"file": "src/renderer/actions/setup-and-settings.js",
|
"file": "src/renderer/actions/setup-and-settings.js",
|
||||||
"lines": 427,
|
"lines": 427,
|
||||||
@@ -173,21 +188,6 @@
|
|||||||
],
|
],
|
||||||
"hotspotScore": 86
|
"hotspotScore": 86
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"file": "src/main/unraid-inventory-methods.cjs",
|
|
||||||
"lines": 580,
|
|
||||||
"branches": 56,
|
|
||||||
"functions": 75,
|
|
||||||
"ipcHandlers": 0,
|
|
||||||
"responsibilities": [
|
|
||||||
"inventory",
|
|
||||||
"deployment",
|
|
||||||
"git",
|
|
||||||
"security",
|
|
||||||
"updates"
|
|
||||||
],
|
|
||||||
"hotspotScore": 86
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"file": "src/renderer/actions/deployment-profile.js",
|
"file": "src/renderer/actions/deployment-profile.js",
|
||||||
"lines": 402,
|
"lines": 402,
|
||||||
@@ -460,6 +460,21 @@
|
|||||||
],
|
],
|
||||||
"hotspotScore": 52
|
"hotspotScore": 52
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"file": "src/main/inventory-classifier.cjs",
|
||||||
|
"lines": 84,
|
||||||
|
"branches": 22,
|
||||||
|
"functions": 11,
|
||||||
|
"ipcHandlers": 0,
|
||||||
|
"responsibilities": [
|
||||||
|
"inventory",
|
||||||
|
"deployment",
|
||||||
|
"git",
|
||||||
|
"security",
|
||||||
|
"updates"
|
||||||
|
],
|
||||||
|
"hotspotScore": 52
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"file": "src/renderer/mock-deployment-bridge.js",
|
"file": "src/renderer/mock-deployment-bridge.js",
|
||||||
"lines": 679,
|
"lines": 679,
|
||||||
@@ -476,21 +491,6 @@
|
|||||||
],
|
],
|
||||||
"hotspotScore": 51
|
"hotspotScore": 51
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"file": "src/main/inventory-classifier.cjs",
|
|
||||||
"lines": 72,
|
|
||||||
"branches": 20,
|
|
||||||
"functions": 11,
|
|
||||||
"ipcHandlers": 0,
|
|
||||||
"responsibilities": [
|
|
||||||
"inventory",
|
|
||||||
"deployment",
|
|
||||||
"git",
|
|
||||||
"security",
|
|
||||||
"updates"
|
|
||||||
],
|
|
||||||
"hotspotScore": 50
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"file": "src/renderer/operations.js",
|
"file": "src/renderer/operations.js",
|
||||||
"lines": 212,
|
"lines": 212,
|
||||||
@@ -508,8 +508,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "src/main/production-acceptance-harness.cjs",
|
"file": "src/main/production-acceptance-harness.cjs",
|
||||||
"lines": 152,
|
"lines": 150,
|
||||||
"branches": 19,
|
"branches": 17,
|
||||||
"functions": 28,
|
"functions": 28,
|
||||||
"ipcHandlers": 0,
|
"ipcHandlers": 0,
|
||||||
"responsibilities": [
|
"responsibilities": [
|
||||||
@@ -519,7 +519,7 @@
|
|||||||
"security",
|
"security",
|
||||||
"updates"
|
"updates"
|
||||||
],
|
],
|
||||||
"hotspotScore": 49
|
"hotspotScore": 47
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "src/main/unraid-state-methods.cjs",
|
"file": "src/main/unraid-state-methods.cjs",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# ForgeFlow architecture audit
|
# ForgeFlow architecture audit
|
||||||
|
|
||||||
Generated 2026-07-29T17:40:19.842Z. Complexity is a deterministic decision-point count used for hotspot ranking, not a claim of exact McCabe complexity.
|
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.
|
||||||
|
|
||||||
## Files above 750 lines
|
## Files above 750 lines
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ app.setPath("userData", userDataPath);
|
|||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
try {
|
try {
|
||||||
const configureAccess = process.argv.includes("--configure-access");
|
const configureAccess = process.argv.includes("--configure-access");
|
||||||
|
const reconcile = process.argv.includes("--reconcile");
|
||||||
|
const summaryOnly = process.argv.includes("--summary");
|
||||||
|
const inventoryOnly = process.argv.includes("--inventory-only");
|
||||||
const repositoryFilter = new Set(String(process.argv.find((value) => value.startsWith("--repository=")) || "")
|
const repositoryFilter = new Set(String(process.argv.find((value) => value.startsWith("--repository=")) || "")
|
||||||
.slice("--repository=".length).toLowerCase().split(",").map((value) => value.trim()).filter(Boolean));
|
.slice("--repository=".length).toLowerCase().split(",").map((value) => value.trim()).filter(Boolean));
|
||||||
const store = new ConfigStore(userDataPath);
|
const store = new ConfigStore(userDataPath);
|
||||||
@@ -29,14 +32,28 @@ app.whenReady().then(async () => {
|
|||||||
const reports = [];
|
const reports = [];
|
||||||
for (const server of store.data.servers || []) {
|
for (const server of store.data.servers || []) {
|
||||||
const report = await deployments.scanServerInventory(server.id, repositories);
|
const report = await deployments.scanServerInventory(server.id, repositories);
|
||||||
|
let reconciliation = null;
|
||||||
|
if (reconcile) {
|
||||||
|
for (let attempt = 1; attempt <= 3 && !reconciliation; attempt += 1) {
|
||||||
|
const preview = await deployments.planServerInventoryReconciliation(server.id, repositories, { autoLink: true });
|
||||||
|
try {
|
||||||
|
reconciliation = await deployments.reconcileServerInventory(server.id, repositories, { autoLink: true, expectedPlanId: preview.plan.id });
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code !== "RECONCILIATION_PLAN_STALE" || attempt === 3) throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
const access = [];
|
const access = [];
|
||||||
const seenProfiles = new Set();
|
const seenProfiles = new Set();
|
||||||
for (const repository of repositories) {
|
for (const repository of inventoryOnly || configureAccess ? [] : repositories) {
|
||||||
for (const profile of repository.deploymentProfiles || store.getDeploymentProfiles(repository.fullName) || []) {
|
for (const profile of repository.deploymentProfiles || store.getDeploymentProfiles(repository.fullName) || []) {
|
||||||
if (profile.serverId !== server.id || profile.deploymentMode !== "server-git" || seenProfiles.has(profile.id)) continue;
|
if (profile.serverId !== server.id || profile.deploymentMode !== "server-git" || seenProfiles.has(profile.id)) continue;
|
||||||
seenProfiles.add(profile.id);
|
seenProfiles.add(profile.id);
|
||||||
try {
|
try {
|
||||||
access.push(await deployments.verifyServerGitProfile({ repository, profileId: profile.id }));
|
let verification = await deployments.verifyServerGitProfile({ repository, profileId: profile.id });
|
||||||
|
await deployments.refreshProfileState(repository.fullName, profile.id, verification.branchSha);
|
||||||
|
verification = await deployments.verifyServerGitProfile({ repository, profileId: profile.id });
|
||||||
|
access.push(verification);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
access.push({ repository: repository.fullName, profileId: profile.id, readiness: "Verification incomplete", ready: false, error: error.message });
|
access.push({ repository: repository.fullName, profileId: profile.id, readiness: "Verification incomplete", ready: false, error: error.message });
|
||||||
}
|
}
|
||||||
@@ -59,6 +76,13 @@ app.whenReady().then(async () => {
|
|||||||
}
|
}
|
||||||
reports.push({
|
reports.push({
|
||||||
server: server.name,
|
server: server.name,
|
||||||
|
reconciliation: reconciliation ? {
|
||||||
|
adopted: reconciliation.adopted,
|
||||||
|
refreshed: reconciliation.refreshed,
|
||||||
|
retired: reconciliation.retired,
|
||||||
|
staleProfiles: reconciliation.staleProfiles,
|
||||||
|
recoverySnapshot: reconciliation.recoverySnapshot,
|
||||||
|
} : null,
|
||||||
capabilities: report.capabilities,
|
capabilities: report.capabilities,
|
||||||
warnings: (report.warnings || []).map((warning) => String(warning).slice(0, 300)),
|
warnings: (report.warnings || []).map((warning) => String(warning).slice(0, 300)),
|
||||||
summary: {
|
summary: {
|
||||||
@@ -67,6 +91,12 @@ app.whenReady().then(async () => {
|
|||||||
linked: report.linked,
|
linked: report.linked,
|
||||||
needsReview: report.needsReview,
|
needsReview: report.needsReview,
|
||||||
},
|
},
|
||||||
|
reviewBreakdown: report.workloads.filter((workload) => !workload.reviewDecision && (workload.classification?.type === "stale-link" || (workload.runtime?.running && workload.classification?.type === "duplicate") || (workload.runtime?.running && !["system-container", "external-container", "temporary-runtime", "backup", "release-folder", "historical-compose", "manually-excluded"].includes(workload.classification?.type) && ["suggested", "ambiguous", "unmatched"].includes(workload.status)))).reduce((counts, workload) => {
|
||||||
|
const key = `${workload.classification?.type || "unknown"}:${workload.status || "unknown"}`;
|
||||||
|
counts[key] = (counts[key] || 0) + 1;
|
||||||
|
return counts;
|
||||||
|
}, {}),
|
||||||
|
reviewSamples: report.workloads.filter((workload) => !workload.reviewDecision && (workload.classification?.type === "stale-link" || (workload.runtime?.running && workload.classification?.type === "duplicate") || (workload.runtime?.running && !["system-container", "external-container", "temporary-runtime", "backup", "release-folder", "historical-compose", "manually-excluded"].includes(workload.classification?.type) && ["suggested", "ambiguous", "unmatched"].includes(workload.status)))).slice(0, 30).map((workload) => ({ name: workload.displayName, type: workload.classification?.type, status: workload.status, running: workload.runtime?.running, folder: workload.remoteFolderCandidate, containers: (workload.containers || []).map((container) => container.name) })),
|
||||||
access,
|
access,
|
||||||
workloads: report.workloads.filter((workload) => workload.link || (workload.runtime?.running && workload.status !== "unmatched")).map((workload) => ({
|
workloads: report.workloads.filter((workload) => workload.link || (workload.runtime?.running && workload.status !== "unmatched")).map((workload) => ({
|
||||||
name: workload.displayName,
|
name: workload.displayName,
|
||||||
@@ -79,7 +109,18 @@ app.whenReady().then(async () => {
|
|||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
console.log(JSON.stringify(reports, null, 2));
|
const output = summaryOnly ? reports.map((report) => ({
|
||||||
|
server: report.server,
|
||||||
|
capabilities: report.capabilities,
|
||||||
|
warnings: report.warnings,
|
||||||
|
summary: report.summary,
|
||||||
|
reviewBreakdown: Object.fromEntries(Object.entries(report.reviewBreakdown || {}).sort(([left], [right]) => left.localeCompare(right))),
|
||||||
|
reviewSamples: report.reviewSamples,
|
||||||
|
reconciliation: report.reconciliation,
|
||||||
|
access: report.access.map((item) => ({ repository: item.repository, profileId: item.profileId || null, ready: item.ready, readiness: item.readiness || item.action || null, remoteSha: item.remoteSha || item.branchSha || null, liveSha: item.liveSha || null, error: item.error || null })),
|
||||||
|
review: report.workloads.filter((item) => !item.repository && item.running).map((item) => ({ name: item.name, confidence: item.confidence, folder: item.folder })),
|
||||||
|
})) : reports;
|
||||||
|
console.log(JSON.stringify(output, null, 2));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error?.stack || error?.message || String(error));
|
console.error(error?.stack || error?.message || String(error));
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
|
|||||||
+7
-2
@@ -80,6 +80,7 @@ const required = [
|
|||||||
"docs/RELEASE_NOTES_0.9.4.md",
|
"docs/RELEASE_NOTES_0.9.4.md",
|
||||||
"docs/RELEASE_NOTES_0.9.5.md",
|
"docs/RELEASE_NOTES_0.9.5.md",
|
||||||
"docs/RELEASE_NOTES_0.10.0.md",
|
"docs/RELEASE_NOTES_0.10.0.md",
|
||||||
|
"docs/RELEASE_NOTES_0.10.1.md",
|
||||||
"docs/UPDATING.md",
|
"docs/UPDATING.md",
|
||||||
"docs/DIAGNOSTICS.md",
|
"docs/DIAGNOSTICS.md",
|
||||||
"docs/DEPLOYMENT_SETUP.md",
|
"docs/DEPLOYMENT_SETUP.md",
|
||||||
@@ -118,9 +119,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.0")
|
if (packageJson.version !== "0.10.1")
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Expected package version 0.10.0, got ${packageJson.version}.`,
|
`Expected package version 0.10.1, got ${packageJson.version}.`,
|
||||||
);
|
);
|
||||||
const sourceManifest = await readFile(
|
const sourceManifest = await readFile(
|
||||||
path.join(root, "SOURCE_MANIFEST.txt"),
|
path.join(root, "SOURCE_MANIFEST.txt"),
|
||||||
@@ -434,6 +435,10 @@ for (const phrase of [
|
|||||||
]) {
|
]) {
|
||||||
if (!release0100.includes(phrase)) throw new Error(`0.10.0 release notes are missing: ${phrase}`);
|
if (!release0100.includes(phrase)) throw new Error(`0.10.0 release notes are missing: ${phrase}`);
|
||||||
}
|
}
|
||||||
|
const release0101 = await readFile(path.join(root, "docs/RELEASE_NOTES_0.10.1.md"), "utf8");
|
||||||
|
for (const phrase of ["certificate-free updates", "case-insensitive", "read-only deploy keys", "SHA-256"]) {
|
||||||
|
if (!release0101.includes(phrase)) throw new Error(`0.10.1 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}`);
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ const { deploymentIdentity, deploymentEvidenceHash, deploymentAuthorityKey } = r
|
|||||||
const BACKUP = /(?:^|[\\/._-])(backup|bak|archive|snapshot|old|previous)(?:[\\/._-]|$)/i;
|
const BACKUP = /(?:^|[\\/._-])(backup|bak|archive|snapshot|old|previous)(?:[\\/._-]|$)/i;
|
||||||
const RELEASE = /(?:^|[\\/])(releases?|versions?)(?:[\\/]|$)/i;
|
const RELEASE = /(?:^|[\\/])(releases?|versions?)(?:[\\/]|$)/i;
|
||||||
const STAGING = /(?:^|[\\/._-])(staging|stage|test|qa|preview)(?:[\\/._-]|$)/i;
|
const STAGING = /(?:^|[\\/._-])(staging|stage|test|qa|preview)(?:[\\/._-]|$)/i;
|
||||||
|
const TEMPORARY = /(?:^|[\\/._-])(candidate|rollback|ephemeral)(?:[\\/._-]|$)|^GITEA-ACTIONS-TASK-/i;
|
||||||
const SYSTEM = /^(?:traefik|nginx-proxy-manager|watchtower|portainer|dockerman|unraid-|cloudflared|redis|postgres|mariadb|mysql)(?:$|[-_.])/i;
|
const SYSTEM = /^(?:traefik|nginx-proxy-manager|watchtower|portainer|dockerman|unraid-|cloudflared|redis|postgres|mariadb|mysql)(?:$|[-_.])/i;
|
||||||
|
|
||||||
function baseClassification(workload) {
|
function baseClassification(workload) {
|
||||||
const location = `${workload.compose?.workingDir || ""} ${(workload.compose?.configFiles || []).join(" ")}`;
|
const location = `${workload.compose?.workingDir || ""} ${(workload.compose?.configFiles || []).join(" ")}`;
|
||||||
|
const sourceRepository = String(workload.metadata?.sourceRepository || "").trim();
|
||||||
|
const hasGitProvenance = /^(?:git@|ssh:\/\/|https?:\/\/)/i.test(sourceRepository);
|
||||||
const decision = workload.reviewDecision;
|
const decision = workload.reviewDecision;
|
||||||
if (["manual-exclude", "exclude-scan-root", "ignore"].includes(decision?.action)) return { type: "manually-excluded", reason: decision.reason || "Persisted manual exclusion", decisionAction: decision.action };
|
if (["manual-exclude", "exclude-scan-root", "ignore"].includes(decision?.action)) return { type: "manually-excluded", reason: decision.reason || "Persisted manual exclusion", decisionAction: decision.action };
|
||||||
if (["mark-historical", "archive-link"].includes(decision?.action)) return { type: "historical-compose", reason: decision.reason || "Reviewed as historical", decisionAction: decision.action };
|
if (["mark-historical", "archive-link"].includes(decision?.action)) return { type: "historical-compose", reason: decision.reason || "Reviewed as historical", decisionAction: decision.action };
|
||||||
@@ -17,12 +20,13 @@ function baseClassification(workload) {
|
|||||||
if (BACKUP.test(location)) return { type: "backup", reason: "Path matches backup/archive evidence" };
|
if (BACKUP.test(location)) return { type: "backup", reason: "Path matches backup/archive evidence" };
|
||||||
if (RELEASE.test(location)) return { type: "release-folder", reason: "Path is below a release/version directory" };
|
if (RELEASE.test(location)) return { type: "release-folder", reason: "Path is below a release/version directory" };
|
||||||
if (STAGING.test(location)) return { type: "staging", reason: "Path or project identifies a staging/test workload" };
|
if (STAGING.test(location)) return { type: "staging", reason: "Path or project identifies a staging/test workload" };
|
||||||
|
if (TEMPORARY.test(`${workload.displayName || ""} ${location}`)) return { type: "temporary-runtime", reason: "Runtime identity marks a candidate, rollback or CI workload" };
|
||||||
if (SYSTEM.test(workload.displayName || "") && !workload.metadata?.sourceRepository) return { type: "system-container", reason: "Known infrastructure identity without repository provenance" };
|
if (SYSTEM.test(workload.displayName || "") && !workload.metadata?.sourceRepository) return { type: "system-container", reason: "Known infrastructure identity without repository provenance" };
|
||||||
if (workload.link && workload.runtime?.running) return { type: "active-application", reason: "Linked deployment with running container evidence" };
|
if (workload.link && workload.runtime?.running) return { type: "active-application", reason: "Linked deployment with running container evidence" };
|
||||||
if (workload.link && !workload.runtime?.running) return { type: "stopped-application", reason: "Linked deployment without a running container" };
|
if (workload.link && !workload.runtime?.running) return { type: "stopped-application", reason: "Linked deployment without a running container" };
|
||||||
if (!workload.containers?.length && workload.compose?.configFiles?.length) return { type: "historical-compose", reason: "Compose definition exists without container runtime" };
|
if (!workload.containers?.length && workload.compose?.configFiles?.length) return { type: "historical-compose", reason: "Compose definition exists without container runtime" };
|
||||||
if (workload.status === "ambiguous") return { type: "ambiguous", reason: "Multiple candidates have equivalent evidence" };
|
if (workload.status === "ambiguous") return { type: "ambiguous", reason: "Multiple candidates have equivalent evidence" };
|
||||||
if (!workload.metadata?.sourceRepository && !workload.candidates?.length) return { type: "orphan-container", reason: "Runtime has no repository provenance or candidate" };
|
if (!workload.candidates?.length) return { type: "external-container", reason: hasGitProvenance ? "Repository provenance does not match an accessible configured Gitea repository" : "Runtime has no Git repository provenance and remains monitoring-only" };
|
||||||
if (!workload.runtime?.running && workload.candidates?.length) return { type: "stopped-application", reason: "Stopped runtime has repository evidence" };
|
if (!workload.runtime?.running && workload.candidates?.length) return { type: "stopped-application", reason: "Stopped runtime has repository evidence" };
|
||||||
return { type: workload.runtime?.running ? "active-application" : "ambiguous", reason: workload.runtime?.running ? "Running application evidence" : "Insufficient authoritative evidence" };
|
return { type: workload.runtime?.running ? "active-application" : "ambiguous", reason: workload.runtime?.running ? "Running application evidence" : "Insufficient authoritative evidence" };
|
||||||
}
|
}
|
||||||
@@ -46,8 +50,12 @@ function classifyInventory(workloads, profiles = [], decisions = []) {
|
|||||||
}
|
}
|
||||||
workload.evidenceHash = hash;
|
workload.evidenceHash = hash;
|
||||||
workload.classification = baseClassification(workload);
|
workload.classification = baseClassification(workload);
|
||||||
|
if (workload.link && ["backup", "release-folder", "staging", "temporary-runtime", "historical-compose", "system-container", "external-container", "manually-excluded"].includes(workload.classification.type)) {
|
||||||
|
workload.shadowedLink = workload.link;
|
||||||
|
workload.link = null;
|
||||||
|
}
|
||||||
const key = deploymentAuthorityKey(identity);
|
const key = deploymentAuthorityKey(identity);
|
||||||
if (identity.repository) {
|
if (identity.repository && (workload.link || workload.candidates?.length) && !["backup", "release-folder", "staging", "temporary-runtime", "historical-compose", "system-container", "external-container", "manually-excluded"].includes(workload.classification.type)) {
|
||||||
const group = authorities.get(key) || [];
|
const group = authorities.get(key) || [];
|
||||||
group.push(workload);
|
group.push(workload);
|
||||||
authorities.set(key, group);
|
authorities.set(key, group);
|
||||||
@@ -55,17 +63,21 @@ function classifyInventory(workloads, profiles = [], decisions = []) {
|
|||||||
return workload;
|
return workload;
|
||||||
});
|
});
|
||||||
for (const group of authorities.values()) {
|
for (const group of authorities.values()) {
|
||||||
if (group.length < 2) continue;
|
if (group.length < 2) {
|
||||||
|
group[0].authoritative = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const ranked = [...group].sort((a, b) => Number(b.reviewDecision?.action === "select-authoritative") - Number(a.reviewDecision?.action === "select-authoritative") || Number(b.runtime?.running) - Number(a.runtime?.running) || Number(Boolean(b.link)) - Number(Boolean(a.link)) || Number(Boolean(b.metadata?.liveRevision)) - Number(Boolean(a.metadata?.liveRevision)));
|
const ranked = [...group].sort((a, b) => Number(b.reviewDecision?.action === "select-authoritative") - Number(a.reviewDecision?.action === "select-authoritative") || Number(b.runtime?.running) - Number(a.runtime?.running) || Number(Boolean(b.link)) - Number(Boolean(a.link)) || Number(Boolean(b.metadata?.liveRevision)) - Number(Boolean(a.metadata?.liveRevision)));
|
||||||
ranked[0].authoritative = true;
|
ranked[0].authoritative = true;
|
||||||
for (const duplicate of ranked.slice(1)) {
|
for (const duplicate of ranked.slice(1)) {
|
||||||
duplicate.authoritative = false;
|
duplicate.authoritative = false;
|
||||||
duplicate.classification = { type: "duplicate", reason: `Conflicts with authoritative workload ${ranked[0].workloadId}`, authoritativeWorkloadId: ranked[0].workloadId };
|
duplicate.classification = { type: "duplicate", reason: `Conflicts with authoritative workload ${ranked[0].workloadId}`, authoritativeWorkloadId: ranked[0].workloadId };
|
||||||
duplicate.status = "duplicate";
|
duplicate.status = "duplicate";
|
||||||
|
duplicate.shadowedLink = duplicate.link;
|
||||||
duplicate.link = null;
|
duplicate.link = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { classifyInventory, classifyWorkload: baseClassification, inventoryPathPatterns: { BACKUP, RELEASE, STAGING, SYSTEM } };
|
module.exports = { classifyInventory, classifyWorkload: baseClassification, inventoryPathPatterns: { BACKUP, RELEASE, STAGING, TEMPORARY, SYSTEM } };
|
||||||
|
|||||||
@@ -128,21 +128,19 @@ class ProductionAcceptanceHarness {
|
|||||||
const binary = Buffer.from(options.binary || "MZ-forgeflow-acceptance-binary");
|
const binary = Buffer.from(options.binary || "MZ-forgeflow-acceptance-binary");
|
||||||
const name = `ForgeFlow-Portable-${version}-win-x64.exe`;
|
const name = `ForgeFlow-Portable-${version}-win-x64.exe`;
|
||||||
const checksum = crypto.createHash("sha256").update(binary).digest("hex");
|
const checksum = crypto.createHash("sha256").update(binary).digest("hex");
|
||||||
const manifest = { version, draft: options.draft === true, assets: options.missingAsset ? [] : [{ name, sha256: options.badChecksum ? "0".repeat(64) : checksum, signer: options.signer || "CN=ForgeFlow Test", timestamped: options.timestamped !== false }], provenance: { commitSha: options.commitSha || this.initialSha }, sbom: { bomFormat: "CycloneDX" } };
|
const manifest = { version, draft: options.draft === true, assets: options.missingAsset ? [] : [{ name, sha256: options.badChecksum ? "0".repeat(64) : checksum }], provenance: { commitSha: options.commitSha || this.initialSha }, sbom: { bomFormat: "CycloneDX" } };
|
||||||
await fs.writeFile(path.join(this.paths.releases, `${version}.json`), JSON.stringify(manifest, null, 2));
|
await fs.writeFile(path.join(this.paths.releases, `${version}.json`), JSON.stringify(manifest, null, 2));
|
||||||
if (!options.missingAsset) await fs.writeFile(path.join(this.paths.releases, name), binary);
|
if (!options.missingAsset) await fs.writeFile(path.join(this.paths.releases, name), binary);
|
||||||
return manifest;
|
return manifest;
|
||||||
}
|
}
|
||||||
|
|
||||||
async verifyRelease(version, expectedSigner = "CN=ForgeFlow Test") {
|
async verifyRelease(version) {
|
||||||
const manifest = JSON.parse(await fs.readFile(path.join(this.paths.releases, `${version}.json`), "utf8"));
|
const manifest = JSON.parse(await fs.readFile(path.join(this.paths.releases, `${version}.json`), "utf8"));
|
||||||
if (manifest.draft) throw new Error("Incomplete draft release rejected.");
|
if (manifest.draft) throw new Error("Incomplete draft release rejected.");
|
||||||
const asset = manifest.assets[0];
|
const asset = manifest.assets[0];
|
||||||
if (!asset) throw new Error("Required release asset is missing.");
|
if (!asset) throw new Error("Required release asset is missing.");
|
||||||
const binary = await fs.readFile(path.join(this.paths.releases, asset.name));
|
const binary = await fs.readFile(path.join(this.paths.releases, asset.name));
|
||||||
if (crypto.createHash("sha256").update(binary).digest("hex") !== asset.sha256) throw new Error("Release checksum mismatch.");
|
if (crypto.createHash("sha256").update(binary).digest("hex") !== asset.sha256) throw new Error("Release checksum mismatch.");
|
||||||
if (asset.signer !== expectedSigner) throw new Error("Release signer mismatch.");
|
|
||||||
if (!asset.timestamped) throw new Error("Release signature timestamp is missing.");
|
|
||||||
if (!manifest.provenance?.commitSha || manifest.sbom?.bomFormat !== "CycloneDX") throw new Error("Release provenance or SBOM is missing.");
|
if (!manifest.provenance?.commitSha || manifest.sbom?.bomFormat !== "CycloneDX") throw new Error("Release provenance or SBOM is missing.");
|
||||||
return { verified: true, version, asset: asset.name };
|
return { verified: true, version, asset: asset.name };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -361,9 +361,16 @@ function createUnraidInventoryMethods({
|
|||||||
verified: workloads.filter((item) => item.runtime.health === "healthy" && item.link).length,
|
verified: workloads.filter((item) => item.runtime.health === "healthy" && item.link).length,
|
||||||
linked: workloads.filter((item) => item.status === "linked").length,
|
linked: workloads.filter((item) => item.status === "linked").length,
|
||||||
unmatched: workloads.filter((item) => !item.link).length,
|
unmatched: workloads.filter((item) => !item.link).length,
|
||||||
needsReview: workloads.filter((item) => !item.reviewDecision && (["suggested", "ambiguous", "unmatched", "duplicate", "stale"].includes(item.status) || ["orphan-container", "historical-compose", "stale-link"].includes(item.classification?.type))).length,
|
needsReview: workloads.filter((item) => {
|
||||||
|
if (item.reviewDecision) return false;
|
||||||
|
const type = item.classification?.type;
|
||||||
|
if (type === "duplicate") return item.runtime?.running === true;
|
||||||
|
if (type === "stale-link") return true;
|
||||||
|
if (["system-container", "external-container", "temporary-runtime", "backup", "release-folder", "historical-compose", "manually-excluded"].includes(type)) return false;
|
||||||
|
return item.runtime?.running && ["suggested", "ambiguous", "unmatched"].includes(item.status);
|
||||||
|
}).length,
|
||||||
duplicates: workloads.filter((item) => item.classification?.type === "duplicate").length,
|
duplicates: workloads.filter((item) => item.classification?.type === "duplicate").length,
|
||||||
excluded: workloads.filter((item) => ["system-container", "backup", "release-folder", "historical-compose", "manually-excluded"].includes(item.classification?.type)).length,
|
excluded: workloads.filter((item) => ["system-container", "external-container", "temporary-runtime", "backup", "release-folder", "historical-compose", "manually-excluded"].includes(item.classification?.type)).length,
|
||||||
running: workloads.filter((item) => item.runtime.running).length,
|
running: workloads.filter((item) => item.runtime.running).length,
|
||||||
stopped: workloads.filter((item) => !item.runtime.running).length,
|
stopped: workloads.filter((item) => !item.runtime.running).length,
|
||||||
};
|
};
|
||||||
@@ -398,8 +405,8 @@ function createUnraidInventoryMethods({
|
|||||||
const updates = [];
|
const updates = [];
|
||||||
const conflicts = [];
|
const conflicts = [];
|
||||||
for (const workload of workloads) {
|
for (const workload of workloads) {
|
||||||
if (["duplicate", "backup", "release-folder", "historical-compose", "system-container", "manually-excluded", "stale-link"].includes(workload.classification?.type)) {
|
if (["duplicate", "backup", "release-folder", "historical-compose", "system-container", "external-container", "temporary-runtime", "manually-excluded", "stale-link"].includes(workload.classification?.type)) {
|
||||||
if (!workload.reviewDecision && ["duplicate", "historical-compose", "stale-link"].includes(workload.classification?.type)) conflicts.push({ workloadId: workload.workloadId, displayName: workload.displayName, status: workload.classification.type, reason: workload.classification.reason, candidates: (workload.candidates || []).slice(0, 5).map((item) => ({ repositoryFullName: item.repositoryFullName, score: item.score, exact: item.exact === true })) });
|
if (!workload.reviewDecision && (["historical-compose", "stale-link"].includes(workload.classification?.type) || (workload.classification?.type === "duplicate" && workload.runtime?.running))) conflicts.push({ workloadId: workload.workloadId, displayName: workload.displayName, status: workload.classification.type, reason: workload.classification.reason, candidates: (workload.candidates || []).slice(0, 5).map((item) => ({ repositoryFullName: item.repositoryFullName, score: item.score, exact: item.exact === true })) });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (workload.link?.profileId && workload.link?.repositoryFullName) {
|
if (workload.link?.profileId && workload.link?.repositoryFullName) {
|
||||||
@@ -485,8 +492,8 @@ function createUnraidInventoryMethods({
|
|||||||
.filter((workload) => workload.runtime?.running && workload.link?.profileId)
|
.filter((workload) => workload.runtime?.running && workload.link?.profileId)
|
||||||
.map((workload) => workload.link.profileId));
|
.map((workload) => workload.link.profileId));
|
||||||
const shadowedAutomaticProfiles = workloads
|
const shadowedAutomaticProfiles = workloads
|
||||||
.filter((workload) => !workload.runtime?.running && workload.link?.profileId && !runningProfileIds.has(workload.link.profileId) && runningRepositoryLinks.has(String(workload.link.repositoryFullName).toLowerCase()))
|
.filter((workload) => !workload.runtime?.running && workload.shadowedLink?.profileId && !runningProfileIds.has(workload.shadowedLink.profileId) && runningRepositoryLinks.has(String(workload.shadowedLink.repositoryFullName).toLowerCase()))
|
||||||
.map((workload) => this.allSshProfiles().find((profile) => profile.id === workload.link.profileId && String(profile._repositoryFullName).toLowerCase() === String(workload.link.repositoryFullName).toLowerCase()))
|
.map((workload) => this.allSshProfiles().find((profile) => profile.id === workload.shadowedLink.profileId && String(profile._repositoryFullName).toLowerCase() === String(workload.shadowedLink.repositoryFullName).toLowerCase()))
|
||||||
.filter((profile) => profile && String(profile.workloadIdentity?.linkSource || "").startsWith("automatic"));
|
.filter((profile) => profile && String(profile.workloadIdentity?.linkSource || "").startsWith("automatic"));
|
||||||
staleProfiles = [...new Map([...staleAutomaticProfiles, ...shadowedAutomaticProfiles].map((profile) => [profile.id, {
|
staleProfiles = [...new Map([...staleAutomaticProfiles, ...shadowedAutomaticProfiles].map((profile) => [profile.id, {
|
||||||
profileId: profile.id,
|
profileId: profile.id,
|
||||||
@@ -496,7 +503,7 @@ function createUnraidInventoryMethods({
|
|||||||
}
|
}
|
||||||
for (const workload of workloads) {
|
for (const workload of workloads) {
|
||||||
if (workload.status !== "linked" || !workload.link?.profileId || !workload.link?.repositoryFullName) continue;
|
if (workload.status !== "linked" || !workload.link?.profileId || !workload.link?.repositoryFullName) continue;
|
||||||
const repository = (repositories || []).find((item) => item.fullName === workload.link.repositoryFullName);
|
const repository = (repositories || []).find((item) => String(item.fullName).toLowerCase() === String(workload.link.repositoryFullName).toLowerCase());
|
||||||
const existingProfile = this.store.getDeploymentProfile?.(workload.link.repositoryFullName, workload.link.profileId)
|
const existingProfile = this.store.getDeploymentProfile?.(workload.link.repositoryFullName, workload.link.profileId)
|
||||||
|| this.allSshProfiles().find((item) => item.id === workload.link.profileId && item._repositoryFullName === workload.link.repositoryFullName);
|
|| this.allSshProfiles().find((item) => item.id === workload.link.profileId && item._repositoryFullName === workload.link.repositoryFullName);
|
||||||
if (!repository || !existingProfile) continue;
|
if (!repository || !existingProfile) continue;
|
||||||
@@ -511,7 +518,7 @@ function createUnraidInventoryMethods({
|
|||||||
.map((item) => String(item.link.repositoryFullName).toLowerCase()));
|
.map((item) => String(item.link.repositoryFullName).toLowerCase()));
|
||||||
for (const workload of workloads) {
|
for (const workload of workloads) {
|
||||||
if (workload.status === "linked") continue;
|
if (workload.status === "linked") continue;
|
||||||
if (["duplicate", "backup", "release-folder", "historical-compose", "system-container", "manually-excluded"].includes(workload.classification?.type)) continue;
|
if (["duplicate", "backup", "release-folder", "historical-compose", "system-container", "external-container", "temporary-runtime", "manually-excluded"].includes(workload.classification?.type)) continue;
|
||||||
const candidate = workload.candidates[0];
|
const candidate = workload.candidates[0];
|
||||||
const uniqueCandidate = workload.candidates.length === 1;
|
const uniqueCandidate = workload.candidates.length === 1;
|
||||||
if (candidate && alreadyLinkedRepositories.has(String(candidate.repositoryFullName).toLowerCase())) continue;
|
if (candidate && alreadyLinkedRepositories.has(String(candidate.repositoryFullName).toLowerCase())) continue;
|
||||||
@@ -523,7 +530,7 @@ function createUnraidInventoryMethods({
|
|||||||
&& Boolean(workload.remoteFolderCandidate)
|
&& Boolean(workload.remoteFolderCandidate)
|
||||||
&& !alreadyLinkedRepositories.has(String(candidate.repositoryFullName).toLowerCase());
|
&& !alreadyLinkedRepositories.has(String(candidate.repositoryFullName).toLowerCase());
|
||||||
if (!exactMatch && !exactRuntimeIdentity) continue;
|
if (!exactMatch && !exactRuntimeIdentity) continue;
|
||||||
const repository = (repositories || []).find((item) => item.fullName === candidate.repositoryFullName);
|
const repository = (repositories || []).find((item) => String(item.fullName).toLowerCase() === String(candidate.repositoryFullName).toLowerCase());
|
||||||
if (!repository) continue;
|
if (!repository) continue;
|
||||||
const linkSource = exactMatch ? "automatic" : "automatic-runtime-identity";
|
const linkSource = exactMatch ? "automatic" : "automatic-runtime-identity";
|
||||||
const profile = this.profileFromWorkload(repository, server, workload, { linkSource, deploymentMode: "server-git" });
|
const profile = this.profileFromWorkload(repository, server, workload, { linkSource, deploymentMode: "server-git" });
|
||||||
@@ -535,6 +542,10 @@ function createUnraidInventoryMethods({
|
|||||||
adopted += 1;
|
adopted += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (const stale of staleProfiles) {
|
||||||
|
await this.store.deleteDeploymentProfile(stale.repositoryFullName, stale.profileId);
|
||||||
|
retired += 1;
|
||||||
|
}
|
||||||
const response = this.inventoryResponse(server, inventory, workloads, { adopted, refreshed, retired, staleProfiles });
|
const response = this.inventoryResponse(server, inventory, workloads, { adopted, refreshed, retired, staleProfiles });
|
||||||
response.recoverySnapshot = recoverySnapshot;
|
response.recoverySnapshot = recoverySnapshot;
|
||||||
await this.diagnostics?.info("unraid.workloads.reconciled", {
|
await this.diagnostics?.info("unraid.workloads.reconciled", {
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ test("running duplicate is authoritative and historical folder is never linked",
|
|||||||
const historical = workload("portfolio-old", { running: false, root: "/mnt/user/appdata/portfolio/releases/old", link: { profileId: "profile-old", repositoryFullName: "Jens/Portfolio" } });
|
const historical = workload("portfolio-old", { running: false, root: "/mnt/user/appdata/portfolio/releases/old", link: { profileId: "profile-old", repositoryFullName: "Jens/Portfolio" } });
|
||||||
const result = classifyInventory([historical, active], [{ id: "profile", environment: "production" }, { id: "profile-old", environment: "production" }]);
|
const result = classifyInventory([historical, active], [{ id: "profile", environment: "production" }, { id: "profile-old", environment: "production" }]);
|
||||||
assert.equal(result.find((item) => item.workloadId === "portfolio-current").authoritative, true);
|
assert.equal(result.find((item) => item.workloadId === "portfolio-current").authoritative, true);
|
||||||
assert.equal(result.find((item) => item.workloadId === "portfolio-old").classification.type, "duplicate");
|
assert.equal(result.find((item) => item.workloadId === "portfolio-old").classification.type, "backup");
|
||||||
|
assert.equal(result.find((item) => item.workloadId === "portfolio-old").shadowedLink.profileId, "profile-old");
|
||||||
assert.equal(result.find((item) => item.workloadId === "portfolio-old").link, null);
|
assert.equal(result.find((item) => item.workloadId === "portfolio-old").link, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -40,9 +41,11 @@ for (const [label, item, expected] of [
|
|||||||
["backup Compose folder", workload("backup", { root: "/mnt/user/appdata/portfolio-backup", running: false }), "backup"],
|
["backup Compose folder", workload("backup", { root: "/mnt/user/appdata/portfolio-backup", running: false }), "backup"],
|
||||||
["release directory", workload("release", { root: "/mnt/user/appdata/portfolio/releases/a1", running: false }), "release-folder"],
|
["release directory", workload("release", { root: "/mnt/user/appdata/portfolio/releases/a1", running: false }), "release-folder"],
|
||||||
["staging workload", workload("stage", { root: "/mnt/user/appdata/portfolio-staging" }), "staging"],
|
["staging workload", workload("stage", { root: "/mnt/user/appdata/portfolio-staging" }), "staging"],
|
||||||
|
["candidate runtime", workload("candidate", { name: "portfolio-candidate-039" }), "temporary-runtime"],
|
||||||
["stopped legitimate app", workload("stopped", { running: false }), "stopped-application"],
|
["stopped legitimate app", workload("stopped", { running: false }), "stopped-application"],
|
||||||
["Compose without container", workload("historical", { noContainers: true, running: false }), "historical-compose"],
|
["Compose without container", workload("historical", { noContainers: true, running: false }), "historical-compose"],
|
||||||
["container without repository", workload("orphan", { remote: "", candidates: [], status: "unmatched" }), "orphan-container"],
|
["external container without Git provenance", workload("external", { remote: "", candidates: [], status: "unmatched" }), "external-container"],
|
||||||
|
["external container with inaccessible repository provenance", workload("external-git", { remote: "https://github.com/vendor/image.git", candidates: [], status: "unmatched" }), "external-container"],
|
||||||
["system container", workload("infra", { name: "watchtower", remote: "", candidates: [] }), "system-container"],
|
["system container", workload("infra", { name: "watchtower", remote: "", candidates: [] }), "system-container"],
|
||||||
["ambiguous exact matches", workload("ambiguous", { status: "ambiguous", candidates: [{ repositoryFullName: "Jens/A", score: 100, exact: true }, { repositoryFullName: "Jens/B", score: 100, exact: true }] }), "ambiguous"],
|
["ambiguous exact matches", workload("ambiguous", { status: "ambiguous", candidates: [{ repositoryFullName: "Jens/A", score: 100, exact: true }, { repositoryFullName: "Jens/B", score: 100, exact: true }] }), "ambiguous"],
|
||||||
["profile whose server workload disappeared", { ...workload("stale", { running: false, noContainers: true, link: { profileId: "profile-stale", repositoryFullName: "Jens/Portfolio" } }), metadata: { sourceRepository: "git@gitea.test:Jens/Portfolio.git", branch: "main", staleLink: true } }, "stale-link"],
|
["profile whose server workload disappeared", { ...workload("stale", { running: false, noContainers: true, link: { profileId: "profile-stale", repositoryFullName: "Jens/Portfolio" } }), metadata: { sourceRepository: "git@gitea.test:Jens/Portfolio.git", branch: "main", staleLink: true } }, "stale-link"],
|
||||||
@@ -75,7 +78,7 @@ test("review decisions drive classification and explicit authority", () => {
|
|||||||
{ workloadId: secondary.workloadId, evidenceHash: secondary.evidenceHash, action: "select-authoritative", reason: "Verified production instance" },
|
{ workloadId: secondary.workloadId, evidenceHash: secondary.evidenceHash, action: "select-authoritative", reason: "Verified production instance" },
|
||||||
];
|
];
|
||||||
const result = classifyInventory([workload("primary"), workload("secondary")], [], decisions);
|
const result = classifyInventory([workload("primary"), workload("secondary")], [], decisions);
|
||||||
assert.equal(result.find((item) => item.workloadId === "primary").classification.type, "duplicate");
|
assert.equal(result.find((item) => item.workloadId === "primary").classification.type, "historical-compose");
|
||||||
assert.equal(result.find((item) => item.workloadId === "secondary").authoritative, true);
|
assert.equal(result.find((item) => item.workloadId === "secondary").authoritative, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -104,14 +104,12 @@ test("corrupt configuration is recoverable from the migration backup", async (t)
|
|||||||
assert.equal(JSON.parse(await readFile(harness.paths.config, "utf8")).version, "0.10.0");
|
assert.equal(JSON.parse(await readFile(harness.paths.config, "utf8")).version, "0.10.0");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("release verification rejects checksum, signer, timestamp, missing asset and drafts", async (t) => {
|
test("release verification rejects checksum failures, missing assets and drafts without requiring paid signing", async (t) => {
|
||||||
const harness = await fixture(t);
|
const harness = await fixture(t);
|
||||||
await harness.publishRelease("1.0.0-ok");
|
await harness.publishRelease("1.0.0-ok");
|
||||||
assert.equal((await harness.verifyRelease("1.0.0-ok")).verified, true);
|
assert.equal((await harness.verifyRelease("1.0.0-ok")).verified, true);
|
||||||
for (const [version, options, error] of [
|
for (const [version, options, error] of [
|
||||||
["1.0.0-checksum", { badChecksum: true }, /checksum/],
|
["1.0.0-checksum", { badChecksum: true }, /checksum/],
|
||||||
["1.0.0-signer", { signer: "CN=Wrong" }, /signer/],
|
|
||||||
["1.0.0-time", { timestamped: false }, /timestamp/],
|
|
||||||
["1.0.0-missing", { missingAsset: true }, /asset is missing/],
|
["1.0.0-missing", { missingAsset: true }, /asset is missing/],
|
||||||
["1.0.0-draft", { draft: true }, /draft release/],
|
["1.0.0-draft", { draft: true }, /draft release/],
|
||||||
]) {
|
]) {
|
||||||
|
|||||||
Reference in New Issue
Block a user