Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44aa452a76 | ||
|
|
a0435f4316 | ||
|
|
13f4fe7cd0 | ||
|
|
258f0b1324 | ||
|
|
f8c505e525 | ||
|
|
398f986d95 |
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
**Van lokale wijziging naar aantoonbaar juiste serverversie — zonder de Git- en deploymentcontext over verschillende tools te verspreiden.**
|
**Van lokale wijziging naar aantoonbaar juiste serverversie — zonder de Git- en deploymentcontext over verschillende tools te verspreiden.**
|
||||||
|
|
||||||
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 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.1** · [download de laatste Windows-release](https://gitea.itworx.tech/Jens/ForgeFlow/releases/latest)
|
> Huidige release: **0.10.3** · [download de laatste Windows-release](https://gitea.itworx.tech/Jens/ForgeFlow/releases/latest)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ ForgeFlow is een desktopapp voor teams die met Git, Gitea en eigen servers werke
|
|||||||
- **Eén duidelijke actielijst:** zie meteen welke repository aandacht nodig heeft en waarom.
|
- **Eén duidelijke actielijst:** zie meteen welke repository aandacht nodig heeft en waarom.
|
||||||
- **Veilige Git-flow:** review wijzigingen, stage volledige bestanden of afzonderlijke hunks, commit, push en herstel conflicten zonder contextwissel.
|
- **Veilige Git-flow:** review wijzigingen, stage volledige bestanden of afzonderlijke hunks, commit, push en herstel conflicten zonder contextwissel.
|
||||||
- **Deployment op een exacte commit:** ForgeFlow gebruikt volledige commit-SHA's en toont lokaal, Gitea en server naast elkaar.
|
- **Deployment op een exacte commit:** ForgeFlow gebruikt volledige commit-SHA's en toont lokaal, Gitea en server naast elkaar.
|
||||||
- **Volledige serverinventaris:** zie ook gestopte, DockerMan- en niet-Git-installaties, koppel twijfelgevallen handmatig en behoud hun bestaande Compose-identiteit.
|
- **Automatische serverinventaris:** ForgeFlow herkent draaiende en gestopte Docker-, Compose- en DockerMan-workloads, koppelt alleen op betrouwbaar bewijs en houdt tijdelijke of externe containers apart.
|
||||||
- **Veilige server-pull:** Unraid haalt de exacte commit uit Gitea met een unieke, repository-scoped read-only deploy key en een vastgepinde SSH-hostsleutel.
|
- **Veilige server-pull:** Unraid haalt de exacte commit uit Gitea met een unieke, repository-scoped read-only deploy key en een vastgepinde SSH-hostsleutel.
|
||||||
- **Ingebouwde Git Validator:** controleer repository-identiteit, branch protection, synchronisatie-instellingen, documentatie, geheimen en grote bestanden; veilige verbeteringen kunnen gericht worden toegepast.
|
- **Ingebouwde Git Validator:** controleer repository-identiteit, branch protection, synchronisatie-instellingen, documentatie, geheimen en grote bestanden; veilige verbeteringen kunnen gericht worden toegepast.
|
||||||
- **Lokale controle:** configuratie en credentials blijven op het toestel en diagnostische exports worden lokaal geredigeerd.
|
- **Lokale controle:** configuratie en credentials blijven op het toestel en diagnostische exports worden lokaal geredigeerd.
|
||||||
@@ -26,9 +26,9 @@ ForgeFlow is een desktopapp voor teams die met Git, Gitea en eigen servers werke
|
|||||||
2. Download de Windows-installer of portable executable.
|
2. Download de Windows-installer of portable executable.
|
||||||
3. Start ForgeFlow en doorloop de setupwizard.
|
3. Start ForgeFlow en doorloop de setupwizard.
|
||||||
4. Voeg je Gitea-server, token en lokale projectmappen toe.
|
4. Voeg je Gitea-server, token en lokale projectmappen toe.
|
||||||
5. Configureer optioneel een serververbinding en één of meer deploymentprofielen.
|
5. Voeg optioneel een Docker- of Unraid-server toe. Start daarna **Scan servers** om bestaande deployments te ontdekken en veilig aan repositories te koppelen.
|
||||||
|
|
||||||
Na installatie kun je nieuwe packaged releases vanuit **Settings → Updates** ophalen. Downloads worden tegen de gepubliceerde SHA-256-checksums gecontroleerd. Zie [UPDATING.md](docs/UPDATING.md) wanneer een oudere of source-only build nog niet binair kan updaten.
|
Vanaf versie 0.10.2 kun je nieuwe packaged releases vanuit **Settings → Updates** ophalen. ForgeFlow accepteert uitsluitend de release die bij de exacte Gitea-commit hoort en controleert de download tegen de gepubliceerde SHA-256-checksum. Zie [UPDATING.md](docs/UPDATING.md) voor oudere of source-only installaties.
|
||||||
|
|
||||||
### Eerst vrijblijvend bekijken
|
### Eerst vrijblijvend bekijken
|
||||||
|
|
||||||
@@ -57,9 +57,18 @@ In de repositorywerkruimte zie je de volledige keten **Local → Gitea → Serve
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
Elke deploymentkaart benoemt repository, container, omgeving, uitvoeringsmethode, live commit, Gitea-commit, vorige versie en healthstatus. ForgeFlow ondersteunt gecontroleerde deployments via Gitea Actions en SSH/Unraid, met preflightcontrole en rollback waar beschikbaar.
|
Elke deploymentkaart benoemt repository, container, omgeving, uitvoeringsmethode, live commit, Gitea-commit, vorige versie en healthstatus. Zo blijven ook tientallen containers visueel van elkaar te onderscheiden. ForgeFlow ondersteunt gecontroleerde deployments via Gitea Actions en SSH/Unraid, met preflightcontrole en rollback waar beschikbaar.
|
||||||
|
|
||||||
Bij server discovery probeert ForgeFlow bestaande containers aan Gitea-repositories te koppelen. Een exacte overeenkomst tussen de volledige live SHA en de actuele Gitea-SHA wordt als gelijklopende versie weergegeven; een runtime-healthcheck blijft een afzonderlijke voorwaarde voor een gezonde deployment.
|
Bij server discovery vergelijkt ForgeFlow runtime-, Compose-, DockerMan- en Git-bewijs met Gitea. Exact bewezen matches worden automatisch gekoppeld; kandidaten, historische mappen en externe containers worden niet als productie-deployment geforceerd. Een exacte overeenkomst tussen de volledige live SHA en de actuele Gitea-SHA wordt als gelijklopende versie weergegeven. Ontbreekt de live SHA, dan meldt ForgeFlow eerlijk dat verificatie nog onvolledig is.
|
||||||
|
|
||||||
|
De belangrijkste statussen zijn:
|
||||||
|
|
||||||
|
| Status | Wat je ermee doet |
|
||||||
|
| --- | --- |
|
||||||
|
| **Ready** | De repository, servertoegang, live commit en runtime zijn geverifieerd. |
|
||||||
|
| **Commit mismatch** | De workload is correct gekoppeld, maar Gitea en de server draaien niet dezelfde commit. |
|
||||||
|
| **Verification incomplete** | De koppeling bestaat, maar de server bevat nog onvoldoende commitbewijs. Een ForgeFlow-beheerde deployment vult dit veilig aan. |
|
||||||
|
| **Access failed** | Controleer of herstel de repositorygebonden read-only deploy key voordat je deployt. |
|
||||||
|
|
||||||
### 4. Verbeter de repository met Git Validator
|
### 4. Verbeter de repository met Git Validator
|
||||||
|
|
||||||
@@ -94,9 +103,10 @@ Een gelijke commit bewijst welke code draait; een geslaagde healthcheck bewijst
|
|||||||
- deploymentprofielen per repository en omgeving;
|
- deploymentprofielen per repository en omgeving;
|
||||||
- Gitea Actions en SSH/Unraid als gecontroleerde uitvoeringsroutes;
|
- Gitea Actions en SSH/Unraid als gecontroleerde uitvoeringsroutes;
|
||||||
- serverinventaris van draaiende en gestopte Docker-, Compose- en DockerMan-workloads;
|
- serverinventaris van draaiende en gestopte Docker-, Compose- en DockerMan-workloads;
|
||||||
- automatische koppeling op exact bewijs en een handmatige koppelwizard voor twijfelgevallen;
|
- automatische koppeling op exact bewijs, expliciete review voor echte twijfelgevallen en herkenning van tijdelijke, historische en externe workloads;
|
||||||
- server-pull als aanbevolen route, met een afzonderlijke read-only deploy key per repository;
|
- server-pull als aanbevolen route, met een afzonderlijke read-only deploy key per repository;
|
||||||
- directe checksum-gecontroleerde copy als alternatief zonder servertoegang tot Gitea;
|
- directe checksum-gecontroleerde copy als alternatief zonder servertoegang tot Gitea;
|
||||||
|
- reconciliatie van deployments die buiten ForgeFlow werden bijgewerkt, op basis van de actuele Gitea- en serverwaarheid;
|
||||||
- verificatie op volledige SHA, runtime health en recente serverwaarheid;
|
- verificatie op volledige SHA, runtime health en recente serverwaarheid;
|
||||||
- preflight, live logs, deploymenthistoriek en rollback naar de vorige bekende versie.
|
- preflight, live logs, deploymenthistoriek en rollback naar de vorige bekende versie.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
ForgeFlow 0.10.1 source manifest
|
ForgeFlow 0.10.3 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
|
||||||
@@ -34,6 +34,8 @@ a0cd06a96f23a94e118feb012be0fa1ac51345951cb2ba8e67fb8c889c4c342a 5007
|
|||||||
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
|
0eb44bda2209a5979a6ac693ac4cd4d235c0015031e54b9e895990f37bf60054 1433 docs/RELEASE_NOTES_0.10.1.md
|
||||||
|
8d713471a437a8a55b00d7e1dd95290680862107bc4e586cf27d727f6274e46c 577 docs/RELEASE_NOTES_0.10.2.md
|
||||||
|
0942fb2c4a4f972296423b5232687f7389e2c6417a9d48a3244beef9dec907b9 1164 docs/RELEASE_NOTES_0.10.3.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
|
||||||
@@ -73,10 +75,10 @@ ac76cb50fabde6a00f28d7e9eccd3ef1129a40665eabdc90d78690a38d424652 4195
|
|||||||
1ccde232c060395d7aedce27e89a7647b77afe28ab71de0a5a3efeded57369d3 140415 docs/screenshots/deploy-confirmation.png
|
1ccde232c060395d7aedce27e89a7647b77afe28ab71de0a5a3efeded57369d3 140415 docs/screenshots/deploy-confirmation.png
|
||||||
b39506254ffa2c73c389fb4795b3a745368bbeb7d8514cc47a636316d6d9a6aa 107166 docs/screenshots/deployment-run.png
|
b39506254ffa2c73c389fb4795b3a745368bbeb7d8514cc47a636316d6d9a6aa 107166 docs/screenshots/deployment-run.png
|
||||||
070e6700bdae8c628c907ba181bbf0dde0bbbbb4208f7a875503f933ff1b882e 118819 docs/screenshots/deployment-success.png
|
070e6700bdae8c628c907ba181bbf0dde0bbbbb4208f7a875503f933ff1b882e 118819 docs/screenshots/deployment-success.png
|
||||||
581375ee0727911f85b0441f09734c6215ea8dd6cfaba4a7555599df0edb24f1 333382 docs/screenshots/deployments.png
|
ed69b8beb948a2cf9a6deb6c82368e2bb44ffe8d8990a900dc878b0938d1084f 95937 docs/screenshots/deployments.png
|
||||||
87546583580e8591b1306f997d27445725b0bf5a5a79a839af3a727964e65bc2 103901 docs/screenshots/git-validator.png
|
3868ab978de2a7945761c53a9a718aecd54dc791605d07660bcd5cad62a33ea8 103569 docs/screenshots/git-validator.png
|
||||||
bbdbe91679b486cc92dec4758ce1cdaf24e3277d038c57e794a04c0dee7e3a5b 84046 docs/screenshots/overview.png
|
007681714895ac062c980db1dda806ac17d4f01019ce9c46491a108d17c2dbda 85338 docs/screenshots/overview.png
|
||||||
c8a5e80bb9fd2d442d2d23d30e6ac1528cf2330e6e19492b7c6799e2d1508b53 112868 docs/screenshots/repository-workspace.png
|
1f78414b00ec100af2ec9bf5c9a3e400b6c9bf6dca6fcc317fd951789acc4536 112852 docs/screenshots/repository-workspace.png
|
||||||
158cd3a13e9c4d081a63575fbafc77e0b23812f793a15096888b3667f41fa28c 5605 docs/SECURITY.md
|
158cd3a13e9c4d081a63575fbafc77e0b23812f793a15096888b3667f41fa28c 5605 docs/SECURITY.md
|
||||||
32a34ec13a284d3f9ceebbc107b25a844e3db096f8cafa4e43951fc2050c9a03 13552 docs/SETUP_GUIDE.md
|
32a34ec13a284d3f9ceebbc107b25a844e3db096f8cafa4e43951fc2050c9a03 13552 docs/SETUP_GUIDE.md
|
||||||
2fd71e9bcaeb4cb10c3fa2496b7e52fedf70c5b7f871cd587e22dc060c399079 4421 docs/SSH_UNRAID_DEPLOYMENT.md
|
2fd71e9bcaeb4cb10c3fa2496b7e52fedf70c5b7f871cd587e22dc060c399079 4421 docs/SSH_UNRAID_DEPLOYMENT.md
|
||||||
@@ -95,14 +97,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
|
||||||
d281af8b6fc3fc8a84bca985c6feb13c1edeaa72b5f0144816ea91dd53774a08 179806 package-lock.json
|
672410b1af3900733c067cf29ecaffb96a33500ccbd597bebe999e0c06b161e0 179806 package-lock.json
|
||||||
8587168403c255c8acb1bc829629f171598d7697df362836858a0fc89b500b78 5359 package.json
|
70d4bafa0f9633bfdef297bb2599c90ff224cf10b69134ad74c77ee574ce49ce 5436 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
|
||||||
75602a7e0ce9744d5fc5a73869eec4b47877e00cfcfea68695373a04f8aa5f31 9150 README.md
|
9e95c2fe6bca120bd2f7502afe9cf9d6a2aa0ee128261cccc955dd9b94ade833 10313 README.md
|
||||||
1fa7bf646321e07e40d98f4a7529d5f748c600edd5283f62ee13574b4e97280f 14329 reports/architecture-audit.json
|
1fa7bf646321e07e40d98f4a7529d5f748c600edd5283f62ee13574b4e97280f 14329 reports/architecture-audit.json
|
||||||
c1ff18f1367691332b189bb7589843a5e0bbde4817e3dd29df4ade7ea71dbd52 1114 reports/architecture-audit.md
|
c1ff18f1367691332b189bb7589843a5e0bbde4817e3dd29df4ade7ea71dbd52 1114 reports/architecture-audit.md
|
||||||
509c7bcff5280349bd9f45ed6151f70372bad7010a9ea582c13e2ccab91fe0cd 6272 scripts/acceptance.mjs
|
509c7bcff5280349bd9f45ed6151f70372bad7010a9ea582c13e2ccab91fe0cd 6272 scripts/acceptance.mjs
|
||||||
@@ -117,8 +119,8 @@ b83d443f5724ac15393567f3a688aed8315fbe3e5966832c864a9466e0669464 8102
|
|||||||
444b397d515d65a7ee59d3088cba869cbb812d2b8cc18fc5d255105e3edb58c2 1468 scripts/serve-demo.mjs
|
444b397d515d65a7ee59d3088cba869cbb812d2b8cc18fc5d255105e3edb58c2 1468 scripts/serve-demo.mjs
|
||||||
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
|
e6127e1e62f39c70ddb1abf72f4d7e7b8e3f19ff1f219e1a3660353c2e0cdfac 2411 scripts/verify-release-signatures.mjs
|
||||||
fb18540c46c0be38bbb0133354838ad75c11187fe6bc0c61860d975fe7761008 17502 scripts/verify.mjs
|
c1763bad24b747051ad18d911ba9c7176882663e3b6ce9921a2f8b1aae056bd7 18156 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
|
||||||
@@ -130,20 +132,20 @@ dd613d04b366f2cd071a1685a414016a5fb008082ed1b4cb8b24b79c100f640a 2412
|
|||||||
c157640e76d558906a9aa9881eda811196623ef1c65fa3467f32f0f84b0ddd0c 15095 src/main/diagnostics-service.cjs
|
c157640e76d558906a9aa9881eda811196623ef1c65fa3467f32f0f84b0ddd0c 15095 src/main/diagnostics-service.cjs
|
||||||
a2ef47d5330095b92c2bd22fcc39962091881f9cb60d02e261eb1dd1bd693170 1974 src/main/external-tools-service.cjs
|
a2ef47d5330095b92c2bd22fcc39962091881f9cb60d02e261eb1dd1bd693170 1974 src/main/external-tools-service.cjs
|
||||||
0b7476c2cfe1872601978c20a466c20fe58be35e81b2303e38a753fea62bbc27 32548 src/main/git-service.cjs
|
0b7476c2cfe1872601978c20a466c20fe58be35e81b2303e38a753fea62bbc27 32548 src/main/git-service.cjs
|
||||||
b23dfa041d9f4597d144601ab8569e69ba748a0d7a075c3ef01154eacc2640ae 7018 src/main/git-validator-policy.cjs
|
e28fc1ca2fd4c0116148f5005d793feddf04c36ef711d2d348560394d209a613 7253 src/main/git-validator-policy.cjs
|
||||||
ca050e9820528b555e6a1dfd89ac8be2f9a0cec6e91254897e6e3b6e116a7eb2 26317 src/main/git-validator-service.cjs
|
3a101b63ad3761c26350c2ac0793279a0b27672b91a5b1d8dc75bc44d92f0b52 27128 src/main/git-validator-service.cjs
|
||||||
2faaef0eeec1e473db82b94243674e3812ef5869358b0ec6abf8d645a8794623 20768 src/main/gitea-service.cjs
|
3cc53e24e023aa0d8bf36c35ce9672ca98e6c74066512c8b59ab42274e838c22 21307 src/main/gitea-service.cjs
|
||||||
2ad3b2e647377f687ad987fe248a142ad399ecac98e4b49965aa7efc6093e5fa 6914 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
|
||||||
dc9b5971c9fefe8c374aa31916f5513601ce86003fd48b1d0e51330a909ae3a5 3442 src/main/ipc/operations-handlers.cjs
|
dc9b5971c9fefe8c374aa31916f5513601ce86003fd48b1d0e51330a909ae3a5 3442 src/main/ipc/operations-handlers.cjs
|
||||||
2e2d0b26480b395906b7881e50b596c9a7701e6e534497d04d541bf3f3b3c057 15673 src/main/ipc/repository-handlers.cjs
|
629b0f4704cda6ed6288c5a6ec9e5d73d6ae3e2692c43581b258431b65b58e51 15887 src/main/ipc/repository-handlers.cjs
|
||||||
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
|
||||||
e64f7257d478955c675a133b3735b6afe138a69d2ad090898061e56f557c43e5 9926 src/main/production-acceptance-harness.cjs
|
e64f7257d478955c675a133b3735b6afe138a69d2ad090898061e56f557c43e5 9926 src/main/production-acceptance-harness.cjs
|
||||||
e89b54e7e3174b4b0a1dcd9058d8344e29431f9d16d0e6bb8d11559b691440a0 2508 src/main/repository-monitor.cjs
|
fc7a5a2a42579a311f60a96bec4776942bf3f63428928820029b0dcf4874d1aa 2745 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
|
||||||
afef3841a3948b2121f8fba809aae4ea3da71bd2fda86973ba50200a5b1f89b2 14894 src/main/ssh-service.cjs
|
afef3841a3948b2121f8fba809aae4ea3da71bd2fda86973ba50200a5b1f89b2 14894 src/main/ssh-service.cjs
|
||||||
@@ -163,19 +165,19 @@ d0bf607dd1de9d55f2947d0adf0997cd3ca5c269d10a5362cc1d8bc4d1a2a8ae 6706
|
|||||||
4227a05a20580a31127d2c929640defc3d36e8e3e89d6be830aab1940da81082 12267 src/renderer/actions/recovery.js
|
4227a05a20580a31127d2c929640defc3d36e8e3e89d6be830aab1940da81082 12267 src/renderer/actions/recovery.js
|
||||||
cdfaacdcd5ae04b0e5c79fefa21f5e09d5c810bcea504c5b6e1d6b744182ff84 15567 src/renderer/actions/setup-and-settings.js
|
cdfaacdcd5ae04b0e5c79fefa21f5e09d5c810bcea504c5b6e1d6b744182ff84 15567 src/renderer/actions/setup-and-settings.js
|
||||||
5d8110918b2957889047e38eb4ab2953b2b4476394d9328bd40ae6e4246e65ef 18584 src/renderer/actions/shell.js
|
5d8110918b2957889047e38eb4ab2953b2b4476394d9328bd40ae6e4246e65ef 18584 src/renderer/actions/shell.js
|
||||||
100077a82f14d5252753d017369ecef48ed6d00532166ae5a0b356ead549e02b 24087 src/renderer/app.js
|
edcfa0585af4b11ec3b7458969132bdf215830505859f1519f1635dc111ce48a 24333 src/renderer/app.js
|
||||||
16efd2fca83004f781eae40ae0f706a004ce0bddf338dd087b8adf7eb10c1d84 85704 src/renderer/assets/itworx-mark.png
|
16efd2fca83004f781eae40ae0f706a004ce0bddf338dd087b8adf7eb10c1d84 85704 src/renderer/assets/itworx-mark.png
|
||||||
813b8cdeecac43794166f3db9d3c5d2c441e0292f9ab7bd465ba136d6201e95d 82476 src/renderer/assets/itworx-wordmark-dark.png
|
813b8cdeecac43794166f3db9d3c5d2c441e0292f9ab7bd465ba136d6201e95d 82476 src/renderer/assets/itworx-wordmark-dark.png
|
||||||
094c1b71cc2482a9db250ac175f45f3de68f53277dfbde371a03e61923d00988 75240 src/renderer/assets/itworx-wordmark-light.png
|
094c1b71cc2482a9db250ac175f45f3de68f53277dfbde371a03e61923d00988 75240 src/renderer/assets/itworx-wordmark-light.png
|
||||||
813b8cdeecac43794166f3db9d3c5d2c441e0292f9ab7bd465ba136d6201e95d 82476 src/renderer/assets/itworx-wordmark.png
|
813b8cdeecac43794166f3db9d3c5d2c441e0292f9ab7bd465ba136d6201e95d 82476 src/renderer/assets/itworx-wordmark.png
|
||||||
1a577af2459715cf38d49d118da8c5f897a7ff1d3eb42ad41f121675eb871732 48389 src/renderer/dialogs.js
|
1a577af2459715cf38d49d118da8c5f897a7ff1d3eb42ad41f121675eb871732 48389 src/renderer/dialogs.js
|
||||||
977af9585074f0a404830c5a93de6939b4897d2eb98ba8ae75c3cf0023dea673 5782 src/renderer/events.js
|
eef2f269ba4fbb76bf66ad328d481b461255d0acb753b30878dd4d4eaac57dc6 6924 src/renderer/events.js
|
||||||
a84da5aecbb16ce7983dba1f6d6aab1bf47b2e9a87c2933fa1afb8123f7ef7d6 1497 src/renderer/index.html
|
a84da5aecbb16ce7983dba1f6d6aab1bf47b2e9a87c2933fa1afb8123f7ef7d6 1497 src/renderer/index.html
|
||||||
06180d9656dd254edfb6949c397f8e313954fc560ddcb22b3a35fce3c3e35655 21350 src/renderer/mock-bridge.js
|
06180d9656dd254edfb6949c397f8e313954fc560ddcb22b3a35fce3c3e35655 21350 src/renderer/mock-bridge.js
|
||||||
4b0a64610da0c446f15a43e4753b26f29de72e07793e2fa7b7322d4f07cf8050 27806 src/renderer/mock-deployment-bridge.js
|
9fa522dad0088e2981c4ca5c392e93d6c92e04ff23fd1083d9040a3aa52d2b55 28101 src/renderer/mock-deployment-bridge.js
|
||||||
26065ffa2359cd27b9c9b5b9fb67bcad83ba0f118960c7c024e7e9392dbb16a3 20032 src/renderer/mock-repository-bridge.js
|
948369ce1bfbc31becc95996feabccd6ae90c6297217f04149b78bd323d279f7 20032 src/renderer/mock-repository-bridge.js
|
||||||
94fa265c2fe9ca8d644f0ce9b620b6f85d9b25dca5802c4e9195b66dcbe80120 6522 src/renderer/operations.js
|
94fa265c2fe9ca8d644f0ce9b620b6f85d9b25dca5802c4e9195b66dcbe80120 6522 src/renderer/operations.js
|
||||||
abe196f5ecdd73e7b6ca67a41c90e55bfc507e084f786227264cc780b1ce83a3 78001 src/renderer/styles.css
|
6776e0adb690b8f36274bfc5b31e3140054893c46ba64621c4f56a0d91b52fc2 78323 src/renderer/styles.css
|
||||||
1703e64533b7e2717b27c5776296c7dd76331e6f97e8005aea9fd688f1aee3ae 94834 src/renderer/views.js
|
1703e64533b7e2717b27c5776296c7dd76331e6f97e8005aea9fd688f1aee3ae 94834 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
|
||||||
@@ -190,7 +192,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
|
||||||
33bc892963e89b868235b959498308a456b1285057bda524ba7c1d5a9ee2159e 8763 tests/browser/forgeflow.spec.mjs
|
2d57a66bb2a6461e4c3266f14998d505ba0582135b0bbcf8fce9f1542a70007a 10479 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
|
||||||
@@ -201,10 +203,10 @@ fae3634bae871abade4d487b94b4741b50e787804dbd6135249f634fdd83c6d0 3800
|
|||||||
dd121d96ca265a027cd415a52064500a4541b2f8a662f4f4b25f2f996d52b5da 762 tests/external-tools.test.mjs
|
dd121d96ca265a027cd415a52064500a4541b2f8a662f4f4b25f2f996d52b5da 762 tests/external-tools.test.mjs
|
||||||
e7aebcc0d484a6a59d463d5cb26c11b3ad56e28f6535e7c38a0fe166a41565ea 13690 tests/git-integration.test.mjs
|
e7aebcc0d484a6a59d463d5cb26c11b3ad56e28f6535e7c38a0fe166a41565ea 13690 tests/git-integration.test.mjs
|
||||||
5ea94c6b241a02060d531fad94e449eecd3772eed2137581d4e2babfb09e56db 1239 tests/git-status.test.mjs
|
5ea94c6b241a02060d531fad94e449eecd3772eed2137581d4e2babfb09e56db 1239 tests/git-status.test.mjs
|
||||||
c00bbd8eae5cef7856c8283d6b40dedb81083bf57ad762e89ab79e0f312da351 3271 tests/git-validator-policy.test.mjs
|
61e0b8cad926acd22b5b17e4044f7edcbe96b6977cbbe2b6fbe123406626fc89 4283 tests/git-validator-policy.test.mjs
|
||||||
73d00729696e5067ba33dd6d43b018d89ce7fdd561a60ab66648d3283fb54d21 5370 tests/git-validator.test.mjs
|
2b31459f14a5e36e30cf84c1054f634f4dba8676d29adeb9c2a8e18179f56fa0 6097 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
|
d633c59bd910008223c834c6d7f3e5666c685a0881944263ede2d42cc69d3151 18710 tests/gitea-actions.test.mjs
|
||||||
fcc9a063882840dd89d74c2785284c8f2f6a9e5acec482b6d89ed8de62efdb85 9635 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
|
||||||
@@ -213,7 +215,7 @@ caf98cbd9de9b119dae610ee53fa333a7a11214f34762247452fbb85e8bbf725 2392
|
|||||||
0cb884cf62c1cb02cf59a81662be055bcb5339d176de85e2a3eeb8e8573e11b3 6435 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
|
62e6d3df7051778124648c808caec634a6b2c439660dc556392ea10beec926d1 1870 tests/repository-monitor.test.mjs
|
||||||
ebd3c0825bc9e2f1690cfd51e93a96bd33e939eb9c546aa373dd948b8cf71a69 7781 tests/repository-service.test.mjs
|
ebd3c0825bc9e2f1690cfd51e93a96bd33e939eb9c546aa373dd948b8cf71a69 7781 tests/repository-service.test.mjs
|
||||||
d49c772e3c7ddaa12dc5a1d4fc4cb474a4d99ae06fa5dab5a6cf1c44acb9ed6f 3463 tests/security-validation.test.mjs
|
d49c772e3c7ddaa12dc5a1d4fc4cb474a4d99ae06fa5dab5a6cf1c44acb9ed6f 3463 tests/security-validation.test.mjs
|
||||||
bab853feb0e22aa25af17989baaa632c01efa636533ea67407fecfdd973c7024 627 tests/semver.test.mjs
|
bab853feb0e22aa25af17989baaa632c01efa636533ea67407fecfdd973c7024 627 tests/semver.test.mjs
|
||||||
@@ -222,7 +224,7 @@ e631e9ca49a5bac7075860aac2ff4d377a32a78377b70e06ecf833f0f192fd5f 11552
|
|||||||
0d1bc4d623ce299337736c577ec61c8ffd6974ebe20335b72838d10eae35ecb1 7993 tests/ssh-service.test.mjs
|
0d1bc4d623ce299337736c577ec61c8ffd6974ebe20335b72838d10eae35ecb1 7993 tests/ssh-service.test.mjs
|
||||||
8a6a8477eb94b85ccef18cddd2640afb0d1eafa679c96bc7de20428d5d69e1be 1794 tests/tool-invocation.test.mjs
|
8a6a8477eb94b85ccef18cddd2640afb0d1eafa679c96bc7de20428d5d69e1be 1794 tests/tool-invocation.test.mjs
|
||||||
4182b61e395aff310b9a964c973a43c3566df0b44c454054c3abdd9459e86e3b 49134 tests/unraid-deployment.test.mjs
|
4182b61e395aff310b9a964c973a43c3566df0b44c454054c3abdd9459e86e3b 49134 tests/unraid-deployment.test.mjs
|
||||||
c93d9706eb206b17db8ba490a1e93067f654c66595325f34532d0b1d7617fedc 19691 tests/update-service.test.mjs
|
edec1007826bff6b5457fca603d8c816a9e1eedb3ce398269f151144ae3d2197 19764 tests/update-service.test.mjs
|
||||||
9cea5c1d5ba3e0972a0b5c7236cf1f7c5616373e0a39ea4a492ecebf70452e40 948 tests/validation.test.mjs
|
9cea5c1d5ba3e0972a0b5c7236cf1f7c5616373e0a39ea4a492ecebf70452e40 948 tests/validation.test.mjs
|
||||||
7ef4d4b9f5f3e6979293b29d571ce0e39f83197f3cade2d999a9cea7bacdd84d 1781 tests/zip-writer.test.mjs
|
7ef4d4b9f5f3e6979293b29d571ce0e39f83197f3cade2d999a9cea7bacdd84d 1781 tests/zip-writer.test.mjs
|
||||||
8f36b542736f2933bad8b9464ad7fa37b68196009c81cf702ce3b677cd637dea 767 UPDATE_FROM_0.3.2.md
|
8f36b542736f2933bad8b9464ad7fa37b68196009c81cf702ce3b677cd637dea 767 UPDATE_FROM_0.3.2.md
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# ForgeFlow 0.10.2
|
||||||
|
|
||||||
|
## Packaged updater origin repair
|
||||||
|
|
||||||
|
- Gitea release assets that expose an internal HTTP `ROOT_URL` are safely rewritten to ForgeFlow's configured public HTTPS Gitea origin.
|
||||||
|
- Authentication remains same-origin: the Gitea token is never forwarded to an internal address, CDN or unrelated redirect target.
|
||||||
|
- Published Windows executables are still validated as PE files and against their release SHA-256 sidecars before staging.
|
||||||
|
- The live authenticated updater acceptance downloads the exact published installer and proves its byte count and SHA-256 digest.
|
||||||
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# ForgeFlow 0.10.3
|
||||||
|
|
||||||
|
## Responsive large workspaces
|
||||||
|
|
||||||
|
- Repository monitoring checks up to four local working trees concurrently while retaining overlap protection and per-repository pause controls.
|
||||||
|
- Global search, repository filtering and the command palette debounce full interface renders during rapid typing.
|
||||||
|
- Commit-message input updates readiness and action controls in place, preserving focus and cursor responsiveness.
|
||||||
|
- Interactive project illustrations and diff atmosphere effects perform at most one layout update per animation frame.
|
||||||
|
|
||||||
|
## Git Validator reliability
|
||||||
|
|
||||||
|
- Git Validator and every long repository tab now retain an explicit vertical scroll owner across compact, desktop and wide layouts.
|
||||||
|
- Standard, Strict and Production policies correctly treat configured warning severities as blockers; Minimal remains error-only.
|
||||||
|
- Documented suppressions no longer reduce the hygiene score or remain counted as active blockers.
|
||||||
|
- Repair requests are rescanned immediately before preview or execution, preventing stale or forged fixes.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Full quality gate, coverage thresholds and all responsive browser scenarios pass for this release.
|
||||||
|
Before Width: | Height: | Size: 326 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 110 KiB |
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "forgeflow",
|
"name": "forgeflow",
|
||||||
"version": "0.10.1",
|
"version": "0.10.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "forgeflow",
|
"name": "forgeflow",
|
||||||
"version": "0.10.1",
|
"version": "0.10.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ssh2": "1.17.0"
|
"ssh2": "1.17.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "forgeflow",
|
"name": "forgeflow",
|
||||||
"version": "0.10.1",
|
"version": "0.10.3",
|
||||||
"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",
|
||||||
@@ -106,6 +106,8 @@
|
|||||||
"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/RELEASE_NOTES_0.10.1.md",
|
||||||
|
"docs/RELEASE_NOTES_0.10.2.md",
|
||||||
|
"docs/RELEASE_NOTES_0.10.3.md",
|
||||||
"docs/CURRENT_STATE.md",
|
"docs/CURRENT_STATE.md",
|
||||||
"docs/MUTATION_MODEL.md",
|
"docs/MUTATION_MODEL.md",
|
||||||
"docs/RELEASING.md",
|
"docs/RELEASING.md",
|
||||||
|
|||||||
@@ -14,7 +14,14 @@ if (signedRelease && !/^CN=.+/i.test(expectedPublisher)) throw new Error("FORGEF
|
|||||||
const artifacts = ["Setup", "Portable"].map((kind) => path.join(root, "dist", `ForgeFlow-${kind}-${pkg.version}-win-x64.exe`));
|
const artifacts = ["Setup", "Portable"].map((kind) => path.join(root, "dist", `ForgeFlow-${kind}-${pkg.version}-win-x64.exe`));
|
||||||
for (const artifact of artifacts) {
|
for (const artifact of artifacts) {
|
||||||
const script = `$s=Get-AuthenticodeSignature -LiteralPath $env:FORGEFLOW_SIGNATURE_TARGET; [pscustomobject]@{Status=$s.Status.ToString();Subject=$s.SignerCertificate.Subject;Thumbprint=$s.SignerCertificate.Thumbprint;TimestampSubject=$s.TimeStamperCertificate.Subject}|ConvertTo-Json -Compress`;
|
const script = `$s=Get-AuthenticodeSignature -LiteralPath $env:FORGEFLOW_SIGNATURE_TARGET; [pscustomobject]@{Status=$s.Status.ToString();Subject=$s.SignerCertificate.Subject;Thumbprint=$s.SignerCertificate.Thumbprint;TimestampSubject=$s.TimeStamperCertificate.Subject}|ConvertTo-Json -Compress`;
|
||||||
const { stdout } = await execFileAsync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], { windowsHide: true, env: { ...process.env, FORGEFLOW_SIGNATURE_TARGET: artifact } });
|
let stdout;
|
||||||
|
try {
|
||||||
|
({ stdout } = await execFileAsync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], { windowsHide: true, env: { ...process.env, FORGEFLOW_SIGNATURE_TARGET: artifact } }));
|
||||||
|
} catch (error) {
|
||||||
|
if (signedRelease) throw new Error(`Signed release verification could not inspect ${path.basename(artifact)}: ${error.message}`);
|
||||||
|
console.log(`${path.basename(artifact)}: checksum-protected unsigned artifact (Authenticode inspection unavailable)`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const result = JSON.parse(stdout.trim());
|
const result = JSON.parse(stdout.trim());
|
||||||
const valid = result.Status === "Valid" && Boolean(result.TimestampSubject);
|
const valid = result.Status === "Valid" && Boolean(result.TimestampSubject);
|
||||||
const publisherMatches = !expectedPublisher || String(result.Subject || "").trim() === expectedPublisher;
|
const publisherMatches = !expectedPublisher || String(result.Subject || "").trim() === expectedPublisher;
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ const required = [
|
|||||||
"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/RELEASE_NOTES_0.10.1.md",
|
||||||
|
"docs/RELEASE_NOTES_0.10.2.md",
|
||||||
|
"docs/RELEASE_NOTES_0.10.3.md",
|
||||||
"docs/UPDATING.md",
|
"docs/UPDATING.md",
|
||||||
"docs/DIAGNOSTICS.md",
|
"docs/DIAGNOSTICS.md",
|
||||||
"docs/DEPLOYMENT_SETUP.md",
|
"docs/DEPLOYMENT_SETUP.md",
|
||||||
@@ -119,9 +121,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.1")
|
if (packageJson.version !== "0.10.3")
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Expected package version 0.10.1, got ${packageJson.version}.`,
|
`Expected package version 0.10.3, got ${packageJson.version}.`,
|
||||||
);
|
);
|
||||||
const sourceManifest = await readFile(
|
const sourceManifest = await readFile(
|
||||||
path.join(root, "SOURCE_MANIFEST.txt"),
|
path.join(root, "SOURCE_MANIFEST.txt"),
|
||||||
@@ -439,6 +441,14 @@ const release0101 = await readFile(path.join(root, "docs/RELEASE_NOTES_0.10.1.md
|
|||||||
for (const phrase of ["certificate-free updates", "case-insensitive", "read-only deploy keys", "SHA-256"]) {
|
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}`);
|
if (!release0101.includes(phrase)) throw new Error(`0.10.1 release notes are missing: ${phrase}`);
|
||||||
}
|
}
|
||||||
|
const release0102 = await readFile(path.join(root, "docs/RELEASE_NOTES_0.10.2.md"), "utf8");
|
||||||
|
for (const phrase of ["internal HTTP", "public HTTPS", "same-origin", "SHA-256"]) {
|
||||||
|
if (!release0102.includes(phrase)) throw new Error(`0.10.2 release notes are missing: ${phrase}`);
|
||||||
|
}
|
||||||
|
const release0103 = await readFile(path.join(root, "docs/RELEASE_NOTES_0.10.3.md"), "utf8");
|
||||||
|
for (const phrase of ["concurrently", "debounce", "animation frame", "Git Validator", "stale or forged"]) {
|
||||||
|
if (!release0103.includes(phrase)) throw new Error(`0.10.3 release notes are missing: ${phrase}`);
|
||||||
|
}
|
||||||
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}`);
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ function normalizePolicy(policy = {}) {
|
|||||||
enabledChecks: Array.isArray(custom.enabledChecks) ? [...new Set(custom.enabledChecks.map(String))] : null,
|
enabledChecks: Array.isArray(custom.enabledChecks) ? [...new Set(custom.enabledChecks.map(String))] : null,
|
||||||
severityOverrides: custom.severityOverrides && typeof custom.severityOverrides === "object" ? { ...custom.severityOverrides } : {},
|
severityOverrides: custom.severityOverrides && typeof custom.severityOverrides === "object" ? { ...custom.severityOverrides } : {},
|
||||||
blockingChecks: [...new Set((custom.blockingChecks || policy.blockingChecks || []).map(String))],
|
blockingChecks: [...new Set((custom.blockingChecks || policy.blockingChecks || []).map(String))],
|
||||||
|
blockingSeverities: [...new Set((custom.blockingSeverities || policy.blockingSeverities || base.severities || ["error"]).map(String))]
|
||||||
|
.filter((severity) => ["warning", "error"].includes(severity)),
|
||||||
allowSuppressions: custom.allowSuppressions ?? base.allowSuppressions ?? true,
|
allowSuppressions: custom.allowSuppressions ?? base.allowSuppressions ?? true,
|
||||||
maxSuppressionDays: Math.max(1, Number(custom.maxSuppressionDays ?? base.maxSuppressionDays ?? 30)),
|
maxSuppressionDays: Math.max(1, Number(custom.maxSuppressionDays ?? base.maxSuppressionDays ?? 30)),
|
||||||
};
|
};
|
||||||
@@ -57,7 +59,7 @@ function applyPolicy(checks, policyInput, suppressions = [], now = new Date()) {
|
|||||||
suppressed: Boolean(suppression),
|
suppressed: Boolean(suppression),
|
||||||
suppression: suppression || null,
|
suppression: suppression || null,
|
||||||
expiredSuppression: expiredSuppression || null,
|
expiredSuppression: expiredSuppression || null,
|
||||||
blocking: !suppression && status !== "pass" && (status === "error" || policy.blockingChecks.includes(check.id)),
|
blocking: !suppression && status !== "pass" && (policy.blockingSeverities.includes(status) || policy.blockingChecks.includes(check.id)),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return { policy, checks: relevant };
|
return { policy, checks: relevant };
|
||||||
|
|||||||
@@ -484,12 +484,23 @@ class GitValidatorService {
|
|||||||
throw new Error("Unsupported Git Validator repair action.");
|
throw new Error("Unsupported Git Validator repair action.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async resolveRepairCheck(repository, candidate) {
|
||||||
|
const checkId = String(candidate?.id || candidate?.checkId || "").trim();
|
||||||
|
if (!checkId) throw new Error("A current Git Validator check ID is required.");
|
||||||
|
const report = await this.scan(repository);
|
||||||
|
const current = report.checks.find((check) => check.id === checkId);
|
||||||
|
if (!current?.fixAction)
|
||||||
|
throw new Error("This finding is resolved, suppressed or no longer repairable. Scan again before repairing.");
|
||||||
|
if (candidate?.fixAction && candidate.fixAction !== current.fixAction)
|
||||||
|
throw new Error("The Git Validator repair request is stale. Scan again before repairing.");
|
||||||
|
return current;
|
||||||
|
}
|
||||||
summarize(repository, checks) {
|
summarize(repository, checks) {
|
||||||
const totalWeight = checks.reduce((sum, check) => sum + check.weight, 0);
|
const totalWeight = checks.reduce((sum, check) => sum + check.weight, 0);
|
||||||
const earned = checks.reduce(
|
const earned = checks.reduce(
|
||||||
(sum, check) =>
|
(sum, check) =>
|
||||||
sum +
|
sum +
|
||||||
(check.status === "pass"
|
(check.status === "pass" || check.suppressed
|
||||||
? check.weight
|
? check.weight
|
||||||
: check.status === "warning"
|
: check.status === "warning"
|
||||||
? check.weight * 0.45
|
? check.weight * 0.45
|
||||||
@@ -512,8 +523,9 @@ class GitValidatorService {
|
|||||||
checks,
|
checks,
|
||||||
summary: {
|
summary: {
|
||||||
passed: checks.filter((check) => check.status === "pass").length,
|
passed: checks.filter((check) => check.status === "pass").length,
|
||||||
warnings: checks.filter((check) => check.status === "warning").length,
|
warnings: checks.filter((check) => check.status === "warning" && !check.suppressed).length,
|
||||||
errors: checks.filter((check) => check.status === "error").length,
|
errors: checks.filter((check) => check.status === "error" && !check.suppressed).length,
|
||||||
|
suppressed: checks.filter((check) => check.suppressed).length,
|
||||||
repairable: checks.filter((check) => check.fixAction).length,
|
repairable: checks.filter((check) => check.fixAction).length,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -426,6 +426,12 @@ class GiteaService {
|
|||||||
if (!downloadUrl) {
|
if (!downloadUrl) {
|
||||||
throw new Error("Gitea did not provide a release asset download URL.");
|
throw new Error("Gitea did not provide a release asset download URL.");
|
||||||
}
|
}
|
||||||
|
const configuredBase = new URL(normalizeBaseUrl(this.store.data.gitea.baseUrl));
|
||||||
|
const publishedUrl = new URL(downloadUrl, configuredBase);
|
||||||
|
const releasePrefix = `/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/download/`.toLowerCase();
|
||||||
|
if (publishedUrl.origin !== configuredBase.origin && publishedUrl.protocol === "http:" && publishedUrl.pathname.toLowerCase().startsWith(releasePrefix)) {
|
||||||
|
downloadUrl = new URL(`${publishedUrl.pathname}${publishedUrl.search}`, configuredBase).toString();
|
||||||
|
}
|
||||||
return this.downloadAuthenticated(downloadUrl, options);
|
return this.downloadAuthenticated(downloadUrl, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -373,7 +373,8 @@ function registerRepositoryIpc({
|
|||||||
});
|
});
|
||||||
register("git-validator:preview-repair", async ({ fullName, check }) => {
|
register("git-validator:preview-repair", async ({ fullName, check }) => {
|
||||||
const repository = await resolveRepository({ fullName });
|
const repository = await resolveRepository({ fullName });
|
||||||
return gitValidator.previewRepair(repository, check);
|
const currentCheck = await gitValidator.resolveRepairCheck(repository, check);
|
||||||
|
return gitValidator.previewRepair(repository, currentCheck);
|
||||||
});
|
});
|
||||||
register("git-validator:export", async ({ fullName, format = "json" }) => {
|
register("git-validator:export", async ({ fullName, format = "json" }) => {
|
||||||
const repository = await resolveRepository({ fullName });
|
const repository = await resolveRepository({ fullName });
|
||||||
@@ -390,18 +391,19 @@ function registerRepositoryIpc({
|
|||||||
"add-editorconfig",
|
"add-editorconfig",
|
||||||
"protect-default-branch",
|
"protect-default-branch",
|
||||||
]);
|
]);
|
||||||
if (!allowed.has(check?.fixAction))
|
const currentCheck = await gitValidator.resolveRepairCheck(repository, check);
|
||||||
|
if (!allowed.has(currentCheck.fixAction))
|
||||||
throw new Error("Unsupported Git Validator repair request.");
|
throw new Error("Unsupported Git Validator repair request.");
|
||||||
const result = await gitValidator.repair(repository, check);
|
const result = await gitValidator.repair(repository, currentCheck);
|
||||||
await audit.append("git-validator.repair", {
|
await audit.append("git-validator.repair", {
|
||||||
repository: repository.fullName,
|
repository: repository.fullName,
|
||||||
checkId: check.id,
|
checkId: currentCheck.id,
|
||||||
action: check.fixAction,
|
action: currentCheck.fixAction,
|
||||||
});
|
});
|
||||||
await diagnostics.info("git-validator.repair.completed", {
|
await diagnostics.info("git-validator.repair.completed", {
|
||||||
repository: repository.fullName,
|
repository: repository.fullName,
|
||||||
checkId: check.id,
|
checkId: currentCheck.id,
|
||||||
action: check.fixAction,
|
action: currentCheck.fixAction,
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -40,7 +40,10 @@ class RepositoryMonitor {
|
|||||||
if (this.running || !this.paths.length) return;
|
if (this.running || !this.paths.length) return;
|
||||||
this.running = true;
|
this.running = true;
|
||||||
try {
|
try {
|
||||||
for (const localPath of this.paths) {
|
const queue = [...this.paths];
|
||||||
|
const workers = Array.from({ length: Math.min(4, queue.length) }, async () => {
|
||||||
|
while (queue.length) {
|
||||||
|
const localPath = queue.shift();
|
||||||
if (this.paused.has(localPath)) continue;
|
if (this.paused.has(localPath)) continue;
|
||||||
try {
|
try {
|
||||||
const status = await this.git.status(localPath);
|
const status = await this.git.status(localPath);
|
||||||
@@ -61,6 +64,8 @@ class RepositoryMonitor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
await Promise.all(workers);
|
||||||
} finally {
|
} finally {
|
||||||
this.running = false;
|
this.running = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ const ui = {
|
|||||||
setupValidation: null,
|
setupValidation: null,
|
||||||
activeDeployment: null,
|
activeDeployment: null,
|
||||||
operationPollTimer: null,
|
operationPollTimer: null,
|
||||||
|
inputRenderTimer: null,
|
||||||
isMock: false,
|
isMock: false,
|
||||||
refreshError: null,
|
refreshError: null,
|
||||||
autoRefreshPending: false,
|
autoRefreshPending: false,
|
||||||
@@ -190,6 +191,14 @@ const ui = {
|
|||||||
auditEvents: [],
|
auditEvents: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function scheduleInputRender(delay = 120) {
|
||||||
|
if (ui.inputRenderTimer) clearTimeout(ui.inputRenderTimer);
|
||||||
|
ui.inputRenderTimer = setTimeout(() => {
|
||||||
|
ui.inputRenderTimer = null;
|
||||||
|
render();
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
|
||||||
function selectedRepository() {
|
function selectedRepository() {
|
||||||
return (
|
return (
|
||||||
ui.repositories.find(
|
ui.repositories.find(
|
||||||
|
|||||||
@@ -17,20 +17,32 @@ app.addEventListener("click", async (event) => {
|
|||||||
app.addEventListener("input", (event) => {
|
app.addEventListener("input", (event) => {
|
||||||
if (event.target.id === "global-search") {
|
if (event.target.id === "global-search") {
|
||||||
ui.search = event.target.value;
|
ui.search = event.target.value;
|
||||||
render();
|
scheduleInputRender();
|
||||||
document.querySelector("#global-search")?.focus();
|
|
||||||
} else if (event.target.id === "repo-filter") {
|
} else if (event.target.id === "repo-filter") {
|
||||||
ui.repoSearch = event.target.value;
|
ui.repoSearch = event.target.value;
|
||||||
render();
|
scheduleInputRender();
|
||||||
document.querySelector("#repo-filter")?.focus();
|
|
||||||
} else if (event.target.id === "commit-message") {
|
} else if (event.target.id === "commit-message") {
|
||||||
ui.commitMessage = event.target.value;
|
ui.commitMessage = event.target.value;
|
||||||
const position = event.target.selectionStart;
|
const repository = selectedRepository();
|
||||||
render();
|
const hasSelection = Boolean(ui.selectedFiles.size || repository?.localStatus?.counts?.staged);
|
||||||
const next = document.querySelector("#commit-message");
|
const ready = Boolean(hasSelection && ui.commitMessage.trim());
|
||||||
if (next) {
|
const blocker = !hasSelection
|
||||||
next.focus();
|
? "Select files or stage one or more hunks."
|
||||||
next.setSelectionRange(position, position);
|
: ready
|
||||||
|
? ui.selectedFiles.size
|
||||||
|
? "Ready to commit. ForgeFlow stages the selected files automatically."
|
||||||
|
: "Ready to commit only the reviewed staged hunks."
|
||||||
|
: "Enter a commit message to enable commit and push.";
|
||||||
|
const readiness = document.querySelector(".commit-readiness");
|
||||||
|
if (readiness) {
|
||||||
|
readiness.classList.toggle("ready", ready);
|
||||||
|
readiness.classList.toggle("blocked", !ready);
|
||||||
|
readiness.innerHTML = `${icon(ready ? "check" : "warning")}<span>${escapeHtml(blocker)}</span>`;
|
||||||
|
}
|
||||||
|
for (const button of document.querySelectorAll('[data-action="commit-push"], [data-action="commit-only"]')) {
|
||||||
|
button.disabled = !ready;
|
||||||
|
if (ready) button.removeAttribute("title");
|
||||||
|
else button.title = blocker;
|
||||||
}
|
}
|
||||||
} else if (event.target.id === "setup-url")
|
} else if (event.target.id === "setup-url")
|
||||||
ui.setupDraft.baseUrl = event.target.value;
|
ui.setupDraft.baseUrl = event.target.value;
|
||||||
@@ -38,7 +50,7 @@ app.addEventListener("input", (event) => {
|
|||||||
ui.setupDraft.token = event.target.value;
|
ui.setupDraft.token = event.target.value;
|
||||||
else if (event.target.id === "palette-input") {
|
else if (event.target.id === "palette-input") {
|
||||||
ui.paletteQuery = event.target.value;
|
ui.paletteQuery = event.target.value;
|
||||||
render();
|
scheduleInputRender(60);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -102,35 +114,32 @@ document.addEventListener("keydown", (event) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let pointerAnimationFrame = null;
|
||||||
|
let pendingPointer = null;
|
||||||
|
|
||||||
document.addEventListener("pointermove", (event) => {
|
document.addEventListener("pointermove", (event) => {
|
||||||
const illustration = event.target.closest?.("[data-project-illustration]");
|
pendingPointer = { target: event.target, clientX: event.clientX, clientY: event.clientY };
|
||||||
|
if (pointerAnimationFrame) return;
|
||||||
|
pointerAnimationFrame = requestAnimationFrame(() => {
|
||||||
|
pointerAnimationFrame = null;
|
||||||
|
const current = pendingPointer;
|
||||||
|
pendingPointer = null;
|
||||||
|
if (!current) return;
|
||||||
|
const illustration = current.target.closest?.("[data-project-illustration]");
|
||||||
if (illustration) {
|
if (illustration) {
|
||||||
const bounds = illustration.getBoundingClientRect();
|
const bounds = illustration.getBoundingClientRect();
|
||||||
illustration.style.setProperty(
|
illustration.style.setProperty("--tilt-x", `${((current.clientY - bounds.top) / bounds.height - 0.5) * -7}deg`);
|
||||||
"--tilt-x",
|
illustration.style.setProperty("--tilt-y", `${((current.clientX - bounds.left) / bounds.width - 0.5) * 9}deg`);
|
||||||
`${((event.clientY - bounds.top) / bounds.height - 0.5) * -7}deg`,
|
|
||||||
);
|
|
||||||
illustration.style.setProperty(
|
|
||||||
"--tilt-y",
|
|
||||||
`${((event.clientX - bounds.left) / bounds.width - 0.5) * 9}deg`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
const diffPanel = current.target.closest?.(".diff-panel");
|
||||||
const diffPanel = event.target.closest?.(".diff-panel");
|
|
||||||
const atmosphere = diffPanel?.querySelector("[data-diff-atmosphere]");
|
const atmosphere = diffPanel?.querySelector("[data-diff-atmosphere]");
|
||||||
if (atmosphere) {
|
if (atmosphere) {
|
||||||
const bounds = diffPanel.getBoundingClientRect();
|
const bounds = diffPanel.getBoundingClientRect();
|
||||||
atmosphere.style.setProperty(
|
atmosphere.style.setProperty("--diff-tilt-x", `${((current.clientY - bounds.top) / bounds.height - 0.5) * -3}deg`);
|
||||||
"--diff-tilt-x",
|
atmosphere.style.setProperty("--diff-tilt-y", `${((current.clientX - bounds.left) / bounds.width - 0.5) * 4}deg`);
|
||||||
`${((event.clientY - bounds.top) / bounds.height - 0.5) * -3}deg`,
|
|
||||||
);
|
|
||||||
atmosphere.style.setProperty(
|
|
||||||
"--diff-tilt-y",
|
|
||||||
`${((event.clientX - bounds.left) / bounds.width - 0.5) * 4}deg`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("pointerout", (event) => {
|
document.addEventListener("pointerout", (event) => {
|
||||||
const illustration = event.target.closest?.("[data-project-illustration]");
|
const illustration = event.target.closest?.("[data-project-illustration]");
|
||||||
if (illustration && !illustration.contains(event.relatedTarget)) {
|
if (illustration && !illustration.contains(event.relatedTarget)) {
|
||||||
|
|||||||
@@ -461,13 +461,15 @@ function createMockDeploymentBridge(context) {
|
|||||||
},
|
},
|
||||||
async gitValidatorScan(fullName) {
|
async gitValidatorScan(fullName) {
|
||||||
await wait(260);
|
await wait(260);
|
||||||
|
const policy = state.gitValidatorPolicy || { id: "standard", label: "Standard", requiredScore: 70 };
|
||||||
|
const activeWarnings = 3;
|
||||||
return {
|
return {
|
||||||
repository: fullName,
|
repository: fullName,
|
||||||
checkedAt: iso(),
|
checkedAt: iso(),
|
||||||
score: 78,
|
score: 78,
|
||||||
grade: "Good",
|
grade: "Good",
|
||||||
policy: state.gitValidatorPolicy || { id: "standard", label: "Standard", requiredScore: 70 },
|
policy,
|
||||||
ready: true,
|
ready: 78 >= policy.requiredScore && (policy.id === "minimal" || activeWarnings === 0),
|
||||||
commitSha: "8cbaf303aa3bb9b4023a7c89aa13fb70ce612847",
|
commitSha: "8cbaf303aa3bb9b4023a7c89aa13fb70ce612847",
|
||||||
trend: { newlyFound: ["working-tree"], resolved: ["editorconfig"], regressions: [], suppressions: [] },
|
trend: { newlyFound: ["working-tree"], resolved: ["editorconfig"], regressions: [], suppressions: [] },
|
||||||
expiredSuppressions: [],
|
expiredSuppressions: [],
|
||||||
@@ -572,7 +574,12 @@ function createMockDeploymentBridge(context) {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
async gitValidatorSetPolicy(_fullName, policy) {
|
async gitValidatorSetPolicy(_fullName, policy) {
|
||||||
state.gitValidatorPolicy = { id: policy.id, label: policy.id[0].toUpperCase() + policy.id.slice(1) };
|
const requiredScores = { minimal: 55, standard: 70, strict: 82, production: 90 };
|
||||||
|
state.gitValidatorPolicy = {
|
||||||
|
id: policy.id,
|
||||||
|
label: policy.id[0].toUpperCase() + policy.id.slice(1),
|
||||||
|
requiredScore: requiredScores[policy.id] || 70,
|
||||||
|
};
|
||||||
return clone(state.gitValidatorPolicy);
|
return clone(state.gitValidatorPolicy);
|
||||||
},
|
},
|
||||||
async gitValidatorSuppress(_fullName, suppression) {
|
async gitValidatorSuppress(_fullName, suppression) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ function createMockRepositoryBridge(context) {
|
|||||||
await wait(80);
|
await wait(80);
|
||||||
snapshot();
|
snapshot();
|
||||||
return {
|
return {
|
||||||
appVersion: "0.10.0-demo",
|
appVersion: "0.10.3-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" },
|
||||||
|
|||||||
@@ -937,6 +937,21 @@ select:focus-visible {
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
.repo-content > .tab-page,
|
||||||
|
.repo-content > .validator-page {
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
}
|
||||||
|
.repo-content > .validator-empty,
|
||||||
|
.repo-content > .empty-state {
|
||||||
|
max-height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
}
|
||||||
.changes-layout {
|
.changes-layout {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -1286,7 +1301,7 @@ html[data-theme="light"] .diff-line.remove {
|
|||||||
padding: 18px;
|
padding: 18px;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
overflow: auto;
|
align-content: start;
|
||||||
}
|
}
|
||||||
.validator-empty {
|
.validator-empty {
|
||||||
min-height: 360px;
|
min-height: 360px;
|
||||||
@@ -2572,9 +2587,8 @@ kbd {
|
|||||||
font: 11px var(--font-mono);
|
font: 11px var(--font-mono);
|
||||||
}
|
}
|
||||||
.tab-page {
|
.tab-page {
|
||||||
min-height: 100%;
|
min-height: 0;
|
||||||
padding: 18px 19px 42px;
|
padding: 18px 19px 42px;
|
||||||
overflow: auto;
|
|
||||||
}
|
}
|
||||||
.git-tools-grid {
|
.git-tools-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
@@ -60,6 +60,24 @@ async function assertSurface(page) {
|
|||||||
expect(audit.headings.length).toBeGreaterThan(0);
|
expect(audit.headings.length).toBeGreaterThan(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function assertScrollableWhenOverflowing(page, selector) {
|
||||||
|
const target = page.locator(selector);
|
||||||
|
await expect(target).toBeVisible();
|
||||||
|
await expect(target).toHaveCSS("overflow-y", /auto|scroll/);
|
||||||
|
let metrics;
|
||||||
|
await expect.poll(async () => {
|
||||||
|
metrics = await target.evaluate((element) => ({
|
||||||
|
connected: element.isConnected,
|
||||||
|
clientHeight: element.clientHeight,
|
||||||
|
scrollHeight: element.scrollHeight,
|
||||||
|
}));
|
||||||
|
return metrics.connected && metrics.clientHeight > 0;
|
||||||
|
}).toBe(true);
|
||||||
|
if (metrics.scrollHeight > metrics.clientHeight + 1) {
|
||||||
|
await target.evaluate((element) => { element.scrollTop = element.scrollHeight; });
|
||||||
|
await expect.poll(() => target.evaluate((element) => element.scrollTop)).toBeGreaterThan(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
test("shell, overview, repositories and settings remain responsive and accessible", async ({ page }, testInfo) => {
|
test("shell, overview, repositories and settings remain responsive and accessible", async ({ page }, testInfo) => {
|
||||||
await assertSurface(page);
|
await assertSurface(page);
|
||||||
for (const view of ["overview", "deployments", "settings"]) {
|
for (const view of ["overview", "deployments", "settings"]) {
|
||||||
@@ -86,13 +104,28 @@ test("repository changes, Git tools and Git Validator complete their primary flo
|
|||||||
}
|
}
|
||||||
await page.locator('[data-action="repo-tab"][data-tab="validator"]').click();
|
await page.locator('[data-action="repo-tab"][data-tab="validator"]').click();
|
||||||
await expect(page.locator(".validator-score")).toBeVisible();
|
await expect(page.locator(".validator-score")).toBeVisible();
|
||||||
|
await assertScrollableWhenOverflowing(page, ".validator-page");
|
||||||
await expect(page.locator("#validator-policy")).toBeVisible();
|
await expect(page.locator("#validator-policy")).toBeVisible();
|
||||||
await page.locator("#validator-policy").selectOption("production");
|
await page.locator("#validator-policy").selectOption("production");
|
||||||
await expect(page.locator(".validator-hero")).toContainText(/Production policy/i);
|
await expect(page.locator(".validator-hero")).toContainText(/Production policy/i);
|
||||||
|
await expect(page.locator(".validator-hero")).toContainText(/review required/i);
|
||||||
await page.keyboard.press("Tab");
|
await page.keyboard.press("Tab");
|
||||||
await expect(page.locator(":focus")).toBeVisible();
|
await expect(page.locator(":focus")).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("every long application surface retains a working vertical scroll owner", async ({ page }) => {
|
||||||
|
for (const view of ["overview", "deployments", "diagnostics", "settings"]) {
|
||||||
|
await page.locator(`.nav-button[data-view="${view}"]`).click();
|
||||||
|
await assertScrollableWhenOverflowing(page, ".main-canvas");
|
||||||
|
}
|
||||||
|
await page.locator('[data-action="select-repo"]').first().click();
|
||||||
|
for (const tab of ["history", "deployments", "gittools", "validator", "settings"]) {
|
||||||
|
await page.locator(`[data-action="repo-tab"][data-tab="${tab}"]`).click();
|
||||||
|
const scrollRoot = page.locator(".repo-content > .tab-page, .repo-content > .validator-page");
|
||||||
|
if (await scrollRoot.count())
|
||||||
|
await assertScrollableWhenOverflowing(page, ".repo-content > .tab-page, .repo-content > .validator-page");
|
||||||
|
}
|
||||||
|
});
|
||||||
test("deployment inventory supports dense workloads without ambiguous blank cards", async ({ page }) => {
|
test("deployment inventory supports dense workloads without ambiguous blank cards", async ({ page }) => {
|
||||||
await page.locator('.nav-button[data-action="navigate"][data-view="deployments"]').click();
|
await page.locator('.nav-button[data-action="navigate"][data-view="deployments"]').click();
|
||||||
await expect(page.locator(".deploy-card, .server-inventory-panel .tool-row").first()).toBeVisible();
|
await expect(page.locator(".deploy-card, .server-inventory-panel .tool-row").first()).toBeVisible();
|
||||||
|
|||||||
@@ -23,6 +23,20 @@ test("Git Validator policies enforce score, blockers and enabled checks", () =>
|
|||||||
assert.equal(governed.checks[0].blocking, true);
|
assert.equal(governed.checks[0].blocking, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("built-in policies enforce their declared blocking severities", () => {
|
||||||
|
const finding = [{ id: "readme", status: "warning", category: "Documentation", weight: 5 }];
|
||||||
|
assert.equal(applyPolicy(finding, { id: "minimal" }, []).checks[0].blocking, false);
|
||||||
|
for (const id of ["standard", "strict", "production"])
|
||||||
|
assert.equal(applyPolicy(finding, { id }, []).checks[0].blocking, true, `${id} must block active warnings`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("documented suppressions remove active blockers", () => {
|
||||||
|
const now = new Date("2026-07-01T00:00:00.000Z");
|
||||||
|
const suppression = validateSuppression({ checkId: "readme", reason: "Tracked remediation work", author: "Jens", expiresAt: "2026-07-08T00:00:00.000Z", evidence: "ticket:FF-7" }, normalizePolicy({ id: "standard" }), now);
|
||||||
|
const check = applyPolicy([{ id: "readme", status: "warning", category: "Documentation", weight: 5 }], { id: "standard" }, [suppression], now).checks[0];
|
||||||
|
assert.equal(check.suppressed, true);
|
||||||
|
assert.equal(check.blocking, false);
|
||||||
|
});
|
||||||
test("suppressions require accountable evidence and reactivate after expiry", () => {
|
test("suppressions require accountable evidence and reactivate after expiry", () => {
|
||||||
const now = new Date("2026-07-01T00:00:00.000Z");
|
const now = new Date("2026-07-01T00:00:00.000Z");
|
||||||
const suppression = validateSuppression({ checkId: "signed-tags", reason: "Tracked under release hardening", author: "Jens", ticket: "FF-42", expiresAt: "2026-07-08T00:00:00.000Z", scope: "repository", evidence: "sha:abc" }, normalizePolicy({ id: "standard" }), now);
|
const suppression = validateSuppression({ checkId: "signed-tags", reason: "Tracked under release hardening", author: "Jens", ticket: "FF-42", expiresAt: "2026-07-08T00:00:00.000Z", scope: "repository", evidence: "sha:abc" }, normalizePolicy({ id: "standard" }), now);
|
||||||
|
|||||||
@@ -100,6 +100,24 @@ test("Git Validator recognizes remote aliases and secret-shaped tracked paths",
|
|||||||
assert.equal(isSensitiveTrackedPath(".env.example"), false);
|
assert.equal(isSensitiveTrackedPath(".env.example"), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Git Validator rejects stale or forged repair requests", async () => {
|
||||||
|
const validator = new GitValidatorService({ git: new GitService() });
|
||||||
|
validator.scan = async () => ({
|
||||||
|
checks: [{ id: "local-safety", fixAction: "configure-local-safety", status: "warning" }],
|
||||||
|
});
|
||||||
|
assert.equal(
|
||||||
|
(await validator.resolveRepairCheck({}, { id: "local-safety", fixAction: "configure-local-safety" })).id,
|
||||||
|
"local-safety",
|
||||||
|
);
|
||||||
|
await assert.rejects(
|
||||||
|
validator.resolveRepairCheck({}, { id: "local-safety", fixAction: "align-origin" }),
|
||||||
|
/stale/i,
|
||||||
|
);
|
||||||
|
await assert.rejects(
|
||||||
|
validator.resolveRepairCheck({}, { id: "resolved-check", fixAction: "align-origin" }),
|
||||||
|
/resolved|no longer repairable/i,
|
||||||
|
);
|
||||||
|
});
|
||||||
test("Git Validator reports reproducibility, CI and editor hygiene and creates reviewable defaults", async (t) => {
|
test("Git Validator reports reproducibility, CI and editor hygiene and creates reviewable defaults", async (t) => {
|
||||||
const root = await mkdtemp(path.join(os.tmpdir(), "forgeflow-hygiene-"));
|
const root = await mkdtemp(path.join(os.tmpdir(), "forgeflow-hygiene-"));
|
||||||
t.after(() => rm(root, { recursive: true, force: true }));
|
t.after(() => rm(root, { recursive: true, force: true }));
|
||||||
|
|||||||
@@ -156,6 +156,19 @@ test('uses a release-provided browser download URL without requesting metadata a
|
|||||||
assert.equal(requested, 'https://gitea.example.test/attachments/direct.exe');
|
assert.equal(requested, 'https://gitea.example.test/attachments/direct.exe');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('rewrites Gitea internal HTTP release URLs to the configured public origin', async () => {
|
||||||
|
const service = new GiteaService(makeStore());
|
||||||
|
let requested = '';
|
||||||
|
service.downloadAuthenticated = async (pathname) => {
|
||||||
|
requested = pathname;
|
||||||
|
return Buffer.from('asset');
|
||||||
|
};
|
||||||
|
await service.downloadReleaseAsset('Jens', 'ForgeFlow', 107, 412, {
|
||||||
|
downloadUrl: 'http://192.168.10.150:3000/Jens/ForgeFlow/releases/download/v0.10.1/ForgeFlow.exe',
|
||||||
|
});
|
||||||
|
assert.equal(requested, 'https://gitea.example.test/Jens/ForgeFlow/releases/download/v0.10.1/ForgeFlow.exe');
|
||||||
|
});
|
||||||
|
|
||||||
test('creates conservative default branch protection rules', async () => {
|
test('creates conservative default branch protection rules', async () => {
|
||||||
const service = new GiteaService(makeStore());
|
const service = new GiteaService(makeStore());
|
||||||
let request = null;
|
let request = null;
|
||||||
|
|||||||
@@ -28,3 +28,24 @@ test('repository monitor establishes a baseline and emits only on later changes'
|
|||||||
await monitor.tick();
|
await monitor.tick();
|
||||||
assert.equal(changes.length, 2);
|
assert.equal(changes.length, 2);
|
||||||
});
|
});
|
||||||
|
test('repository monitor checks multiple repositories concurrently with a bounded worker pool', async () => {
|
||||||
|
let active = 0;
|
||||||
|
let peak = 0;
|
||||||
|
const git = {
|
||||||
|
status: async (localPath) => {
|
||||||
|
active += 1;
|
||||||
|
peak = Math.max(peak, active);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 15));
|
||||||
|
active -= 1;
|
||||||
|
return { localPath, revision: 1 };
|
||||||
|
},
|
||||||
|
statusFingerprint: (status) => String(status.revision)
|
||||||
|
};
|
||||||
|
const store = { data: { preferences: { autoRefresh: true, repositoryPollSeconds: 2 } } };
|
||||||
|
const monitor = new RepositoryMonitor({ store, git });
|
||||||
|
monitor.setPaths(Array.from({ length: 10 }, (_, index) => `/repo-${index}`));
|
||||||
|
await monitor.tick();
|
||||||
|
assert.equal(peak, 4);
|
||||||
|
assert.equal(active, 0);
|
||||||
|
assert.equal(monitor.fingerprints.size, 10);
|
||||||
|
});
|
||||||
|
|||||||
@@ -518,6 +518,7 @@ test("Windows release pipeline preserves optional signing checks and emits prove
|
|||||||
assert.match(signatureSource, /FORGEFLOW_EXPECTED_PUBLISHER/);
|
assert.match(signatureSource, /FORGEFLOW_EXPECTED_PUBLISHER/);
|
||||||
assert.match(signatureSource, /TimestampSubject/);
|
assert.match(signatureSource, /TimestampSubject/);
|
||||||
assert.match(signatureSource, /Signed release verification failed/);
|
assert.match(signatureSource, /Signed release verification failed/);
|
||||||
|
assert.match(signatureSource, /Authenticode inspection unavailable/);
|
||||||
assert.match(checksumSource, /provenance\.json/);
|
assert.match(checksumSource, /provenance\.json/);
|
||||||
assert.match(checksumSource, /sbom\.cdx\.json/);
|
assert.match(checksumSource, /sbom\.cdx\.json/);
|
||||||
assert.match(checksumSource, /CycloneDX/);
|
assert.match(checksumSource, /CycloneDX/);
|
||||||
|
|||||||