Prepare ForgeFlow for public release
This commit is contained in:
@@ -10,8 +10,8 @@ jobs:
|
||||
quality:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
- run: npm run test:browser:ci
|
||||
- name: Preserve browser failure evidence
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: forgeflow-browser-failure-evidence
|
||||
path: artifacts/
|
||||
|
||||
@@ -9,7 +9,7 @@ jobs:
|
||||
secret-scan:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: https://gitea.com/actions/checkout@v4
|
||||
- uses: https://gitea.com/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Secret scan
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -23,8 +23,8 @@ jobs:
|
||||
# Browser quality runs against the dedicated bounded Windows 11 VM runner.
|
||||
runs-on: windows-native
|
||||
steps:
|
||||
- uses: https://gitea.com/actions/checkout@v4
|
||||
- uses: https://gitea.com/actions/setup-node@v4
|
||||
- uses: https://gitea.com/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: https://gitea.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
- run: npm run test:browser:ci
|
||||
- name: Preserve browser failure evidence
|
||||
if: failure()
|
||||
uses: https://gitea.com/actions/upload-artifact@v3.2.2-node20
|
||||
uses: https://gitea.com/actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: forgeflow-browser-failure-evidence
|
||||
path: artifacts/
|
||||
|
||||
@@ -15,23 +15,17 @@ jobs:
|
||||
release:
|
||||
runs-on: windows-native
|
||||
steps:
|
||||
- uses: https://gitea.com/actions/checkout@v4
|
||||
- uses: https://gitea.com/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- uses: https://gitea.com/actions/setup-node@v4
|
||||
- uses: https://gitea.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
- name: Build, sign and publish version bump
|
||||
- name: Validate version bump and build release artifacts
|
||||
shell: powershell
|
||||
env:
|
||||
GITEA_EVENT_NAME: ${{ gitea.event_name }}
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
FORGEFLOW_RELEASE_BASE_URL: ${{ gitea.server_url }}
|
||||
FORGEFLOW_RELEASE_OWNER: Jens
|
||||
FORGEFLOW_RELEASE_REPO: ForgeFlow
|
||||
FORGEFLOW_RELEASE_BRANCH: main
|
||||
FORGEFLOW_RELEASE_SIGNING_KEY_PEM: ${{ secrets.FORGEFLOW_RELEASE_SIGNING_KEY_PEM }}
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
Set-StrictMode -Version Latest
|
||||
@@ -56,25 +50,6 @@ jobs:
|
||||
throw "ForgeFlow version '$version' is not a stable semantic version."
|
||||
}
|
||||
|
||||
if ($env:FORGEFLOW_RELEASE_SIGNING_KEY_PEM) {
|
||||
$privateKeyPath = Join-Path $env:RUNNER_TEMP "forgeflow-release-signing-private.pem"
|
||||
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
||||
[System.IO.File]::WriteAllText($privateKeyPath, $env:FORGEFLOW_RELEASE_SIGNING_KEY_PEM, $utf8NoBom)
|
||||
$env:FORGEFLOW_UPDATE_SIGNING_PRIVATE_KEY = $privateKeyPath
|
||||
}
|
||||
|
||||
$effectiveKeyPath = if ($env:FORGEFLOW_UPDATE_SIGNING_PRIVATE_KEY) {
|
||||
$env:FORGEFLOW_UPDATE_SIGNING_PRIVATE_KEY
|
||||
} else {
|
||||
Join-Path $env:APPDATA "forgeflow\release-signing-private.pem"
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $effectiveKeyPath)) {
|
||||
throw "ForgeFlow release signing key is not provisioned for the windows-native runner. Provision the existing Ed25519 private key as FORGEFLOW_RELEASE_SIGNING_KEY_PEM or at $effectiveKeyPath."
|
||||
}
|
||||
if (-not $env:GITEA_TOKEN) {
|
||||
throw "Gitea Actions did not provide GITEA_TOKEN to the release job."
|
||||
}
|
||||
|
||||
& cmd.exe /d /s /c "npm ci --no-audit --no-fund"
|
||||
if ($LASTEXITCODE -ne 0) { throw "npm ci failed." }
|
||||
& cmd.exe /d /s /c "npx electron --version"
|
||||
@@ -87,9 +62,47 @@ jobs:
|
||||
if ($LASTEXITCODE -ne 0) { throw "ForgeFlow browser acceptance suite failed." }
|
||||
& cmd.exe /d /s /c "npm audit --omit=dev --audit-level=high"
|
||||
if ($LASTEXITCODE -ne 0) { throw "ForgeFlow production dependency audit failed." }
|
||||
& cmd.exe /d /s /c "npm run dist:win"
|
||||
if ($LASTEXITCODE -ne 0) { throw "ForgeFlow Windows build/signing failed." }
|
||||
& cmd.exe /d /s /c "npm run release:binary"
|
||||
if ($LASTEXITCODE -ne 0) { throw "ForgeFlow Gitea release publication failed." }
|
||||
& cmd.exe /d /s /c "npx electron-builder --win nsis portable"
|
||||
if ($LASTEXITCODE -ne 0) { throw "ForgeFlow Windows build failed." }
|
||||
& node scripts/write-release-checksums.mjs
|
||||
if ($LASTEXITCODE -ne 0) { throw "ForgeFlow checksum generation failed." }
|
||||
|
||||
Write-Host "ForgeFlow $version was fully validated, built, signed and published from exact main HEAD $env:GITEA_SHA."
|
||||
- name: Sign and publish validated artifacts
|
||||
shell: powershell
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
FORGEFLOW_RELEASE_BASE_URL: ${{ gitea.server_url }}
|
||||
FORGEFLOW_RELEASE_OWNER: Jens
|
||||
FORGEFLOW_RELEASE_REPO: ForgeFlow
|
||||
FORGEFLOW_RELEASE_BRANCH: main
|
||||
FORGEFLOW_RELEASE_SIGNING_KEY_PEM: ${{ secrets.FORGEFLOW_RELEASE_SIGNING_KEY_PEM }}
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
Set-StrictMode -Version Latest
|
||||
$privateKeyPath = Join-Path $env:RUNNER_TEMP "forgeflow-release-signing-private.pem"
|
||||
try {
|
||||
if (-not $env:FORGEFLOW_RELEASE_SIGNING_KEY_PEM) {
|
||||
throw "FORGEFLOW_RELEASE_SIGNING_KEY_PEM is not configured."
|
||||
}
|
||||
if (-not $env:GITEA_TOKEN) {
|
||||
throw "GITEA_TOKEN is not configured."
|
||||
}
|
||||
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
||||
[System.IO.File]::WriteAllText($privateKeyPath, $env:FORGEFLOW_RELEASE_SIGNING_KEY_PEM, $utf8NoBom)
|
||||
$env:FORGEFLOW_UPDATE_SIGNING_PRIVATE_KEY = $privateKeyPath
|
||||
& node scripts/sign-release-manifest.mjs
|
||||
if ($LASTEXITCODE -ne 0) { throw "ForgeFlow manifest signing failed." }
|
||||
& node scripts/verify-release-signatures.mjs
|
||||
if ($LASTEXITCODE -ne 0) { throw "ForgeFlow release signature verification failed." }
|
||||
& node scripts/prune-dist.mjs
|
||||
if ($LASTEXITCODE -ne 0) { throw "ForgeFlow artifact pruning failed." }
|
||||
& .\node_modules\.bin\electron.cmd scripts/publish-binary-release.cjs
|
||||
if ($LASTEXITCODE -ne 0) { throw "ForgeFlow Gitea release publication failed." }
|
||||
} finally {
|
||||
$env:FORGEFLOW_UPDATE_SIGNING_PRIVATE_KEY = $null
|
||||
if (Test-Path -LiteralPath $privateKeyPath) {
|
||||
Remove-Item -LiteralPath $privateKeyPath -Force
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "ForgeFlow artifacts were signed and published from exact main HEAD $env:GITEA_SHA."
|
||||
|
||||
@@ -138,6 +138,7 @@ Voor serverdetectie en SSH-deployments heb je daarnaast een bereikbare Docker- o
|
||||
- [SETUP_GUIDE.md](docs/SETUP_GUIDE.md) — Gitea, projectmappen en eerste ingebruikname;
|
||||
- [DEPLOYMENT_SETUP.md](docs/DEPLOYMENT_SETUP.md) — deploymentprofielen en verificatie;
|
||||
- [SSH_UNRAID_DEPLOYMENT.md](docs/SSH_UNRAID_DEPLOYMENT.md) — SSH- en Unraid-vereisten;
|
||||
- [DEPLOYMENT_MIGRATION_EXAMPLE.md](docs/DEPLOYMENT_MIGRATION_EXAMPLE.md) — veilig een bestaande servercheckout onder beheer brengen;
|
||||
- [DIAGNOSTICS.md](docs/DIAGNOSTICS.md) — veilige controles en supportbundels.
|
||||
|
||||
## Ontwikkelen vanuit de broncode
|
||||
|
||||
+39
-38
@@ -1,17 +1,13 @@
|
||||
ForgeFlow 0.10.15 source manifest
|
||||
SHA-256 BYTES PATH
|
||||
(The manifest excludes itself, dependencies and generated release artifacts.)
|
||||
(The manifest includes tracked and non-ignored source files, excluding itself.)
|
||||
ec40b1ed8e5152ca4175bbe97be43f0e2e911894112dc6a47c2c348069f79abf 87 .gitattributes
|
||||
cc690743121cd3e3a4f12499024456c0b28c4aca4af0f566e57aa9ca87124ef9 883 .gitea/workflows/chatgpt-validation.yml
|
||||
037209289e7a387d855ee061fec9b385db57c6a31ee71c770e684f70dcae28f8 5157 .gitea/workflows/managed-validation.yml
|
||||
d46a8c270028665527674987b05d427b7a83b4eefc6c8afef7250e5a008a57b2 1651 .gitea/workflows/quality.yml
|
||||
f523b7a00d6257033954620a515bbc1ec888332878a6eb14c6628dc40710250c 4252 .gitea/workflows/release.yml
|
||||
4a9e8a955ad8c9fa7ba3f8f89cf9920ac1d28c6e5b344782e12d02c3b0fab1ee 105 .gitignore
|
||||
c70a5dd662bc71e91e255b51b1a907939682259af2e27091178ef5ea231cfd77 1896 .gitea/workflows/quality.yml
|
||||
e6b76033c44516b625cd9f40e89824ebc8f74ef2a34666d6d44c042fc602c406 4905 .gitea/workflows/release.yml
|
||||
83fac3efff45f3dc926080b280ae190b6bb40eb8dae7b8cb5d27255759bc9c47 267 .gitignore
|
||||
f14b4987904bcb5814e4459a057ed4d20f58a633152288a761214dcd28780b56 3 .nvmrc
|
||||
d0b1bd421359311871224f9fa1cff5a802000933668017d9e42e5190f8d2d8e5 152 .playwright-mcp/page-2026-07-29T17-41-03-014Z.yml
|
||||
528fe408ad4b49c621dd57dd831cecf7ec00b8865069f6ed3b80fefc2e0b7823 8267 .playwright-mcp/page-2026-07-29T17-41-26-269Z.yml
|
||||
804c6dac953c5094671784919ff35ebda756306a04ef34fe01cd7cf7cd50b2dc 16909 .playwright-mcp/page-2026-07-29T17-41-46-590Z.yml
|
||||
0f17fdea98e15ebcf7f3ed356d31b0fc89be62f7bc1d25b26c8a41e4b5deca68 160 .playwright-mcp/page-2026-07-29T17-47-10-112Z.yml
|
||||
5f348bcf74baa845958884bd2258e1ce4121d386c945777b0e80912602e5b5f6 17227 .playwright-mcp/page-2026-07-29T17-47-19-366Z.yml
|
||||
89545860bd6f7566da81edc8328cd2a1ebf33e81a4b0dcf2cec74338c05e8cac 1753 build-windows.ps1
|
||||
0970821475a4452aa19e447e9397a95db836791f16890a1a83fd748ac033dc86 8830 build/icon-128.png
|
||||
09112c1425ca953d8dd8b2bcfd221e5a84b9f81752f7168f360e295030cbc8f2 521 build/icon-16.png
|
||||
@@ -24,15 +20,16 @@ ca32a76e708d565c4af659f0f4d2615fc32114c3f75aec1454862a3ed1e72c41 2263
|
||||
16efd2fca83004f781eae40ae0f706a004ce0bddf338dd087b8adf7eb10c1d84 85704 build/icon.png
|
||||
164c059453a5737110b4e5e98b6211650c757f0aff710f8f7523ffe0ff1815d7 113 build/update-signing-public.pem
|
||||
5f4aca19a35cbcaffa1a6993ce96b7d66052ec2b286022f2af74594e8a310568 15712 CHANGELOG.md
|
||||
3754dcaa776ead5dc60b4955ed4294fd8580577ecb9ce29cb0d9fbbaf9253313 811 CONTRIBUTING.md
|
||||
c612fcc44ff222db0c9a4cfd11a4076fafe080e4ada31e689a08739a4f14e74f 1650 docs/ACCEPTANCE.md
|
||||
a17f95d96d3c9fbc69d870874e6fbb7472091adefc454b24f835db1279511d72 8296 docs/ARCHITECTURE.md
|
||||
3785ad21872e3dc08fde9ef819e26f42161ba6ce6d235f87fcebd7859dfaf148 8293 docs/ARCHITECTURE.md
|
||||
e05458ee2696e3c57e2475bb42ae1f914f6a36e01768d7a26a3199f1fffed490 1157 docs/COVERAGE_POLICY.md
|
||||
9eb9eec82518c0bfc7686f5faaf690a93ed63c71d35f1dc5a2c5ec5199da652a 3124 docs/CURRENT_STATE.md
|
||||
8ea655d1912ac2e17f8834e33a566a8b14461b396ec4268c396ca189a1749b94 2205 docs/DEPENDENCY_AUDIT.md
|
||||
3b9c301313a6406c39ec09f5ad7247dafdf47f6c21505ab8970b206cb1734f31 2788 docs/DEPLOYMENT_MIGRATION_EXAMPLE.md
|
||||
30a92bcf5daadb019efa2f82cb820ea302490dd1d68fb772674dc3faccd3e594 2045 docs/DEPLOYMENT_SETUP.md
|
||||
eb42f979666e05d51c587e4223282914926a2b9b1ade9f3fb75525019ce7f738 4616 docs/DIAGNOSTICS.md
|
||||
1d8aa3c688a9d330b9a5303b09e9e3049c50bf04f97807c7ff659607baa34c32 1464 docs/ERROR_CODES.md
|
||||
a0cd06a96f23a94e118feb012be0fa1ac51345951cb2ba8e67fb8c889c4c342a 5007 docs/LUMAOPS_SERVER_AUDIT.md
|
||||
648dd6bca6b45668fb86eb3e1f6c5898dd8da0291b990f1bb5105cfd79421343 1301 docs/MUTATION_MODEL.md
|
||||
8dc95f69e6f6c8415702c8e79fb6b466c4d60061afded9140450e7e558eaefe5 3704 docs/PRODUCTION_READINESS_1.0.md
|
||||
f79908fb3dad98c38030c6e6be7c79a1999e0478ed9c2496923891954438daa1 4581 docs/RELEASE_AUDIT_0.6.0.md
|
||||
@@ -56,7 +53,7 @@ a0c00ff76acd1682bb5e0e8dcf6589c9480da436c9c6d30780a1ed58b4dad94f 1770
|
||||
5773ead01aa4c522c556295553787482d01b1f5242f053b2c61f120c4de4fa76 5963 docs/RELEASE_NOTES_0.3.0.md
|
||||
d46de73cf6c4cd5c2ba3f455a7a2af2e0d64ee9d94a97fd1a0bfb44e35c1624a 1093 docs/RELEASE_NOTES_0.3.1.md
|
||||
0d697d241a08d2427a6e7f5c2f27bd1830a41836a01e08eeff239c7ad5d89982 2445 docs/RELEASE_NOTES_0.3.2.md
|
||||
bc6933c303d3d9b3bfdbf678cae1a717bfe5a893780a1871af8b48589f62f0e3 2160 docs/RELEASE_NOTES_0.4.0.md
|
||||
df07794fb3923f3fb6a49dacbfcc5a371227edb592b4e86ecffaae0a8799f187 2154 docs/RELEASE_NOTES_0.4.0.md
|
||||
343862445061e1a8282a7aa9b2304e7d799e58f9956d50eb5352db18d790efad 1134 docs/RELEASE_NOTES_0.4.1.md
|
||||
e2d67c816a919f00f9e26bf59cf29e5e8cf894536b743d282075c646c5accc96 1605 docs/RELEASE_NOTES_0.4.2.md
|
||||
1aef74fb109541903c4dbc4d9c48d2bd63507420eaf8cceb31890797a5e4f5fd 670 docs/RELEASE_NOTES_0.4.3.md
|
||||
@@ -95,41 +92,42 @@ ed69b8beb948a2cf9a6deb6c82368e2bb44ffe8d8990a900dc878b0938d1084f 95937
|
||||
3868ab978de2a7945761c53a9a718aecd54dc791605d07660bcd5cad62a33ea8 103569 docs/screenshots/git-validator.png
|
||||
007681714895ac062c980db1dda806ac17d4f01019ce9c46491a108d17c2dbda 85338 docs/screenshots/overview.png
|
||||
1f78414b00ec100af2ec9bf5c9a3e400b6c9bf6dca6fcc317fd951789acc4536 112852 docs/screenshots/repository-workspace.png
|
||||
735950c1e77bd4a1cf5ee986a7a307600741e5fdb90918adb0687bd29a89aaec 5877 docs/SECURITY.md
|
||||
5cb9ee0e33a06db1796a740f12b0c41b917c68c1ae7cdb0070936a99c61c2c64 6036 docs/SECURITY.md
|
||||
32a34ec13a284d3f9ceebbc107b25a844e3db096f8cafa4e43951fc2050c9a03 13552 docs/SETUP_GUIDE.md
|
||||
2fd71e9bcaeb4cb10c3fa2496b7e52fedf70c5b7f871cd587e22dc060c399079 4421 docs/SSH_UNRAID_DEPLOYMENT.md
|
||||
b6a178215dab054006aae4944b8ffcbe7f6100691c30f08e221e3a2dbff4cd42 2147 docs/STATUS_ENDPOINT.md
|
||||
0adfeabb98168a7fc0b02bae8d4af436d3c59459012fb05b2216e02265190128 3139 docs/STITCH_REVIEW.md
|
||||
4983414a980075e6faae687b0d71c8e57bfe53fcb4cadb8b979b8abca636fe95 6654 docs/TEST_MATRIX.md
|
||||
03fb2fe52a863b9d3d536f3c8abe23e47b9851be7fd7ccbfb106554b9c595385 5013 docs/UPDATING.md
|
||||
3c34d42088b18e37d18afc848f91fbe5b74134ca74dd53df1e9eb30753fae750 4433 docs/UPDATING.md
|
||||
73f094a2f0db3de053e515feb2771cd5a4f3aa4178f2c5f37be01ca65ff1c938 2705 eslint.config.js
|
||||
c230b931abf2293d2d44b7a69b94c35f1142c093cc46b88739a0de5cbd6d1896 1532 examples/gitea-actions/deploy.yml
|
||||
5d2577d4f9f635a12dcc8795879c079b9e66630bb3d96e21510ef7ee13ebef05 3319 examples/gitea-actions/forgeflow-approved-deploy.yml
|
||||
4c792cc9fd57ed36da291300c252a6ef75b08a249cf6f2561e23c4c22522138a 1477 examples/gitea-actions/rollback.yml
|
||||
577f3fa2131a3baa84549a6523f5816ef9da94f5bac6bc274d4588b6e7ab6594 5688 examples/server/forgeflow-deploy
|
||||
4a84041fcf2d7f36d1807e2c19d6cab816f9635112756a675f32cd24d9757fbb 9661 examples/server/forgeflow-deploy
|
||||
4fe3eee5c2d8705964c24b8c4dd909883a05e7d6eb85629c84b0d64473e0a92b 258 examples/server/forgeflow-runner.sudoers
|
||||
0423fe2cc7f43fe793986a3f62a395668897cdf07348756aa7742a8cd40ac51c 569 examples/server/forgeflow-targets.conf
|
||||
106538d4a14a5a7b13419f9520c582b19809e8fafe2cb8c7dce2bc3e600dd10a 397 examples/server/nginx-forgeflow-status.conf
|
||||
2dff25fb39ce8fc7844026a50524b23f241bec5b614eb05371c7f908a080f69a 398 examples/server/status-example.json
|
||||
a0de3fe4e09b6f246e1513bccc170334e60f63f98c24377192b4335cbf16dff4 593 examples/server/status-example.json
|
||||
4a561ead5ba7cdfaf4efce91842a4308c5f2a77980205879d83835efb8a579db 1067 LICENSE
|
||||
e2daa28bbc01c68c3702add6ea8259dff5920b22f6fdc3c9193ed78a153f2e9e 14708 main.cjs
|
||||
91a984a89dd57a084b9a2331763cacdb061582fb590f13df379d92c1a77a2ee1 352 OVERLAY-INSTRUCTIONS.md
|
||||
9a2d30de6d28e120b970d46654ed5863dbaeaae0332f4a492b27481f14fe6f09 184858 package-lock.json
|
||||
b659f277b413a627b46f64e311d2f20544d82f4c203ca34b30c386b80bc6165e 6457 package.json
|
||||
63d403ada205000a0dfb158ec57cde1a4f58572d790589428ab5b00a99b32cbe 184860 package-lock.json
|
||||
ae06416348c2039eb4cafbf287c99342e04176bbbbb718646f160ff29b6af42f 6464 package.json
|
||||
1237df9ddcbb5ac7dc4316f18c34ff4a7030e3e0d56216ade6dd07369e5e2a04 1353 playwright.config.mjs
|
||||
e8f678b26a1b651ee0e06e499b0538d8a193d0565687e9f750b58f801e4fabd8 12473 preload.cjs
|
||||
7b0d173d0cf5a7f8db807580492bade379dba174a271013747f9f28a3793f55e 12409 preload.cjs
|
||||
abe5dd6fd68f2970cd19ef134094907c67219061d8fe9a1a08324c78de4ad437 484 PUBLISH-AND-ENABLE-UPDATE.cmd
|
||||
6d0858d6654c3c3dc7083ebbd234c88324afcebaecd7b772719440a8afbc2e4e 10736 Publish-ForgeFlow-Release.ps1
|
||||
33f3c4795705ab77c6e6603c88a32c123b3a286bc77e8e472b76970485699338 4386 Publish-Missing-Binary-Release.ps1
|
||||
31979a28ea83a3d4e65c86a2e5103ec8b201a834b449673c92adb2143bb46044 11302 README.md
|
||||
705470b50eefb87fcb4104c2ddcb85efed3d990fe1e0433b707ad1112b8d043d 16450 reports/architecture-audit.json
|
||||
7d72d8eb83ae5fc3b0b8da437f848d78ae3a1aab7028eec06553c4a9b6a5decb 1726 reports/architecture-audit.md
|
||||
5d9f13f34c9a9cca77968e472f1829147e51d3795779fa3f766b04cdd54fc698 11437 README.md
|
||||
0f1bf0696ca6a3de7c222a935953156cdd1bb0aa27f2215b8000901c4db2be31 17255 reports/architecture-audit.json
|
||||
6c50c58f464e2f93fb7255a59d6cbb76354755f63c6f1d4ff14a9a88c8c54574 1758 reports/architecture-audit.md
|
||||
509c7bcff5280349bd9f45ed6151f70372bad7010a9ea582c13e2ccab91fe0cd 6272 scripts/acceptance.mjs
|
||||
5f220dc8ee24d2339aa3eb696ac7a5bd6f55c993b5fd9001ec9784788f2a6e46 6747 scripts/apply-binary-update.ps1
|
||||
358d0ecbd50d8ba1ff9460c761cc2a1990fb27104d6ad74104a8800877343e19 10954 scripts/apply-source-update.ps1
|
||||
02e924227f6cad3777fd06660230c85df590d8ce95e134194a4d18970a240b88 4145 scripts/architecture-audit.mjs
|
||||
47a5b16e95934bfe510c18bf94547ae65acb980c0f0506ae156d1a486dfbdfc9 8985 scripts/audit-installed-deployments.cjs
|
||||
6d46dd6826069d842f20f9f22a99042257db936cdea0bee8d294d2d7ea290126 3893 scripts/doctor.mjs
|
||||
0244d42896b8c44f734d0bb6cdcb29b5981342be2f070ce89f8d9eaf3e4d49e6 1793 scripts/generate-source-manifest.mjs
|
||||
7b483476ddd909b085335cb78c9b0ffe71939c50011b5fbfcfdef6a340fa7ce8 2032 scripts/generate-source-manifest.mjs
|
||||
842436680521311594e798848b050ae4e488d0595f0de57315f6ec081c049fb9 1266 scripts/prune-dist.mjs
|
||||
19417a26a5af967b0f057fede45d1811a6d8ad65a0ed49ad4f5865f598457168 9358 scripts/publish-binary-release.cjs
|
||||
558ff442988f1396c174c7161ff5bd3ef0b2f43cfc31459ec7c3967faa146bc3 1694 scripts/serve-demo.mjs
|
||||
@@ -138,29 +136,30 @@ abe5dd6fd68f2970cd19ef134094907c67219061d8fe9a1a08324c78de4ad437 484
|
||||
c76507857292c5713e1c699cf02e24b80265da39af2cecd148034bdb874adbb6 5246 scripts/test-authenticode-chain.ps1
|
||||
4393f7dc5f417e6d601a68238f4e26791799a3634acec228fe4d79deaee85eb5 3109 scripts/validate-installed-connections.cjs
|
||||
e6127e1e62f39c70ddb1abf72f4d7e7b8e3f19ff1f219e1a3660353c2e0cdfac 2411 scripts/verify-release-signatures.mjs
|
||||
fe3b80152dcb1eab384ad05ae5742ffb8f543e14f3a1b9c9cb3b5c4c4761c983 22642 scripts/verify.mjs
|
||||
2187f4a6e5f162b428f123960c8121f671218e1164327c1b4f21bf45a4477d01 22686 scripts/verify.mjs
|
||||
c2c9e4ba251d93a530a52b2d0079787680261c314083bb99d2356fc177719613 2434 scripts/write-release-checksums.mjs
|
||||
c8780510ff6e77a16e9c4a88c3c981a0d9d1a4dff8eafd16afdeefbb4b03aac9 1064 SECURITY.md
|
||||
619515f524cb89960370ffcbd3fafd3c0e178b95f69c5868b1dd44777f23ec1e 2081 setup-windows.ps1
|
||||
dd613d04b366f2cd071a1685a414016a5fb008082ed1b4cb8b24b79c100f640a 2412 src/main/audit-service.cjs
|
||||
5506ac1e5e49006ffd028a29c89bd0b95485ea3f2abc22db4f2b9fedd959852f 33194 src/main/config-store.cjs
|
||||
b8c3a41b9a49bc1c0387813737247bf86c940d525131f00da7192a5b8c99aa8d 34144 src/main/config-store.cjs
|
||||
2fb04b1494b39f5d7c0720fa5fd298cd46fa85dc1b696d77657592347fcf1819 2731 src/main/configuration-backup.cjs
|
||||
86e9fc2eda66b4b563f6c4bbb87d3e8514340d484fb503b73137e63b6b05c3c9 14597 src/main/deploy-key-lifecycle-service.cjs
|
||||
7cbfe51973d6607203cb197652ed7f296a3f6b6b644df876957117866a47d802 2159 src/main/deployment-identity.cjs
|
||||
ce30ddac403d1adf21176e5df21b0cc3db435305d2628f51f1486eacf20df6f2 23708 src/main/deployment-service.cjs
|
||||
f22348297291199e858656248cec70f94f002144ea7ea0807bd844cc5016baaf 16277 src/main/diagnostics-service.cjs
|
||||
a2ef47d5330095b92c2bd22fcc39962091881f9cb60d02e261eb1dd1bd693170 1974 src/main/external-tools-service.cjs
|
||||
cc2dad2fdac386d41c37b1a8657ae2b3b4084fd8f35a97f8600c082c8d017552 16475 src/main/diagnostics-service.cjs
|
||||
7f452dc2c0e6f3a00eecf6cb1c2be75906ac40445ef3e104341c0c67208aab2e 2397 src/main/external-tools-service.cjs
|
||||
76fdc5576dcd6fdb88921009d4a923854650bd5efe5b837cd7438eba0dc733c8 48002 src/main/git-service.cjs
|
||||
e28fc1ca2fd4c0116148f5005d793feddf04c36ef711d2d348560394d209a613 7253 src/main/git-validator-policy.cjs
|
||||
3a101b63ad3761c26350c2ac0793279a0b27672b91a5b1d8dc75bc44d92f0b52 27128 src/main/git-validator-service.cjs
|
||||
3cc53e24e023aa0d8bf36c35ce9672ca98e6c74066512c8b59ab42274e838c22 21307 src/main/gitea-service.cjs
|
||||
2ad3b2e647377f687ad987fe248a142ad399ecac98e4b49965aa7efc6093e5fa 6914 src/main/inventory-classifier.cjs
|
||||
dafdb09133d2b6ec2161a3f0b09354551e54fc606c8107976fca37405643be91 3404 src/main/inventory-review-service.cjs
|
||||
5e10cf3759bbf9294909866ed16f206d394789f0564549fc0fe23cdd89118ca6 25110 src/main/ipc.cjs
|
||||
98b332589f86a4874a8adddc38e7844a4ddbe9360d38f9f77b2e95d58d748016 25720 src/main/ipc.cjs
|
||||
26efebb4c147ed560966e7e60e64a013b3476327b3bbdb4e4439949142fa7846 2250 src/main/ipc/channel.cjs
|
||||
b80357dd1f0aa18022d92db85b6cc8f29bc691ef11f9a0e90b4886ae5e19c763 12543 src/main/ipc/deployment-handlers.cjs
|
||||
748cddf497b4c204e5e6fa1bd049991a086fdfb38afe342e6b3617c85a111478 12467 src/main/ipc/deployment-handlers.cjs
|
||||
dc9b5971c9fefe8c374aa31916f5513601ce86003fd48b1d0e51330a909ae3a5 3442 src/main/ipc/operations-handlers.cjs
|
||||
8072252821b1245d121eac534a18eeb64f0d7d18429e272e21a6e90b010005b9 17272 src/main/ipc/repository-handlers.cjs
|
||||
62f2c80c8210e19370b8556b1f296cbae50dae6b758a39e209f8fb461691fd4c 4235 src/main/log-redaction.cjs
|
||||
fc7156aabacb3e85a4f220c490f94e9836e07fa5350a1d9e64f84f835ef51024 4961 src/main/log-redaction.cjs
|
||||
958595a99fb242c127f475f3d8622bdba4c07b2d658703f69fe3992227a9107e 12909 src/main/preflight-service.cjs
|
||||
720c4a0c554f46386d87c3ab6607d1fbcae66e50b69483c7dbba169d5128c851 680 src/main/process-error-policy.cjs
|
||||
3096b4181566cb93a27e56e248c92105d4f4df5aee39d73c6c7d8ae8c2231bc0 1570 src/main/process-runner.cjs
|
||||
@@ -177,7 +176,7 @@ bb4a99c3526fcf4db4fbae88a058e8598fd10a87990e7d502bfdc765328bdaa1 42766
|
||||
2c0cf07921ca7ee5a9085ced44498c2e6798e5cc1e8a5ecf704c3cecabe39a25 27607 src/main/unraid-preflight-methods.cjs
|
||||
d45220176aed72d692f9ae5534f9d40bcc359a2d08e025e74a3b3b505b8b9ed4 16559 src/main/unraid-runtime-methods.cjs
|
||||
4c5cf01922e1feb36a31b50af22e973d8aee3fecccd406e449690604111898ac 11608 src/main/unraid-state-methods.cjs
|
||||
0b25c3729c5fffe3cd412c2325616bb86a6c916ae248eeb39d837378bb78c144 26420 src/main/update-service.cjs
|
||||
29b8c5eca83b0e89c7d0716945b5316aac43947b5387e89562d9a024ebc4663c 27204 src/main/update-service.cjs
|
||||
b5c304531bec358d059189a27cd9db8fa20cefb7f817e5eb0287001f7353f6a7 985 src/renderer/actions/command.js
|
||||
d0bf607dd1de9d55f2947d0adf0997cd3ca5c269d10a5362cc1d8bc4d1a2a8ae 6706 src/renderer/actions/deployment-operation.js
|
||||
0db283b1a458ae0b31538940b1ddc931ffdb53bd04ceb7fd8903813f9200d071 17978 src/renderer/actions/deployment-profile.js
|
||||
@@ -196,10 +195,10 @@ b7698de13b872aa80d27b0a4d977c12ca2303b2246f05e6af4223db9b727e525 7433
|
||||
c4a71213d412166093f7bd8254b847de4d8beb58c1aaa356a0cdc8d728080326 1524 src/renderer/index.html
|
||||
06180d9656dd254edfb6949c397f8e313954fc560ddcb22b3a35fce3c3e35655 21350 src/renderer/mock-bridge.js
|
||||
870024aff376826a92c9cf7452689cc1ecc5d9034f055bea56734f3f7fcea5e5 28703 src/renderer/mock-deployment-bridge.js
|
||||
2ac602ba894e57baa37e68bfc5f50906d699263e757e134d41318f0dcb285362 25809 src/renderer/mock-repository-bridge.js
|
||||
ee33d1a77ab7152cb4f3dfbb611011f755a69ba1afb1016a28c997fcfdca97d6 25810 src/renderer/mock-repository-bridge.js
|
||||
94fa265c2fe9ca8d644f0ce9b620b6f85d9b25dca5802c4e9195b66dcbe80120 6522 src/renderer/operations.js
|
||||
9299c83e43eef194bac2946c43b2ffda6309ccbdcc5b9ce1775e668ae7f172ef 92319 src/renderer/styles.css
|
||||
17e729feac40e3a49cabedfc0c5d884c1d589c3d0531dc5bf2517504a6dc10bb 113587 src/renderer/views.js
|
||||
9ee96e6267d923448a45eb0a02d5352286400f2d7b4a006ba681b6806f2e1424 113808 src/renderer/views.js
|
||||
e9e72c072a5c5d04f59cd6763de0cfbf736c2a5ffa2f722143f3bad2bdbc630b 1411 src/shared/clone-target.cjs
|
||||
5d425d5c2f939d0f6beebee7ebb0c77146cb7e318535ba7286ec7081a4dc2269 2497 src/shared/deployment-policy.cjs
|
||||
029e600229714d033c28e2dcb77817aa8269847001782ae0012960e83ffd183f 3057 src/shared/git-status.cjs
|
||||
@@ -212,10 +211,12 @@ ee73fdf9c591c029243385cb2d2085c3005c7b08c5b9e1b89102201f0ab30759 5702
|
||||
f8853dce6fdf360d5df2fbe2b6df3e5687630c807fee5ba8436679b34ec737ea 2436 START_HERE.md
|
||||
058aeaa5d9bfe377c7e322f213c7871ecc4151b5d08ef790992f4ee28d857658 743 START-FORGEFLOW-OVERLAY.ps1
|
||||
f5b0ea887fcdeadec78c1ad49b0ec7979723562f5c0b730703acb77a37281ee0 1009 tests/acceptance.test.mjs
|
||||
2f2b21754dccd8b734d6c7bd4fdd655df79c7739a1de140e562520c8234aa7a2 2854 tests/approved-deployment-evidence.test.mjs
|
||||
720ff5b549a3dd70854eb1bac3589c77a2019a61148be8e41e112196c8821ee3 1079 tests/approved-deployment-one-shot.test.mjs
|
||||
a4e5947204ff6878e601e32477bc85b53cd0153baf95a161c8935b6e5466c257 1155 tests/audit-service.test.mjs
|
||||
6e119cda76b2ee36b93623d98f41371798227f9b3f7c20fbef035a7d7a50cc95 19402 tests/browser/forgeflow.spec.mjs
|
||||
1728c0a7abd92f4d7d9e68df32e4a6b00730555f23795e9b36416795d9d127af 5978 tests/clone-target.test.mjs
|
||||
aa2ae0e5a12bc47f8024e0d7408148e3af797e3f3f0ecb2525fb1cc54cf4e1f0 17378 tests/config-store.test.mjs
|
||||
8aa8789a984769b377f719abca42a428fffc4f89b3997d57184a8073d24121e2 18325 tests/config-store.test.mjs
|
||||
f1463326aee79842d265687ae628189ce54e92544600f2bd14073780287cfb14 2502 tests/configuration-backup.test.mjs
|
||||
144c8e217a334cd69f502938d944e0f2dac61703d5df47e287b9ed542918c779 8129 tests/dependency-wiring.test.mjs
|
||||
aad5948ea374d1e56e777005c73639654c96a90364dd398c949052cf5ae343a2 11130 tests/deploy-key-host.test.mjs
|
||||
@@ -223,8 +224,8 @@ b7e009fed4171d6dd6b4c3154ba1d3f7198e98f5b79b298687841fc8169447cd 9354
|
||||
49bf9cf9842e7899015013675208f83a95402065a082320927a677ee4bab0766 24875 tests/deployment-operations.test.mjs
|
||||
1dc6477bd07de78be189e6e8195ec339eb9d75820c4dbd5b073b8520ee21f6b5 1938 tests/deployment-policy.test.mjs
|
||||
bf4576901e32662d832687a2761852aa1b2cffe256de5044f18c6637c189463b 9780 tests/deployment-status.test.mjs
|
||||
fae3634bae871abade4d487b94b4741b50e787804dbd6135249f634fdd83c6d0 3800 tests/diagnostics.test.mjs
|
||||
dd121d96ca265a027cd415a52064500a4541b2f8a662f4f4b25f2f996d52b5da 762 tests/external-tools.test.mjs
|
||||
9a97e79618393d9f9818cb958bd038e0095489331fba0b29de313aaaaa1ce30d 4064 tests/diagnostics.test.mjs
|
||||
b63eb5bcd89c34629386c745e80c2f83f0709ebce62459b583e665d0a99654c6 938 tests/external-tools.test.mjs
|
||||
26e94450c6ab1dd0d5149d6812e66814a719f80d6f95fcb3a9dee7a3d7f46293 20559 tests/git-integration.test.mjs
|
||||
5ea94c6b241a02060d531fad94e449eecd3772eed2137581d4e2babfb09e56db 1239 tests/git-status.test.mjs
|
||||
61e0b8cad926acd22b5b17e4044f7edcbe96b6977cbbe2b6fbe123406626fc89 4283 tests/git-validator-policy.test.mjs
|
||||
@@ -233,7 +234,7 @@ dd121d96ca265a027cd415a52064500a4541b2f8a662f4f4b25f2f996d52b5da 762
|
||||
d633c59bd910008223c834c6d7f3e5666c685a0881944263ede2d42cc69d3151 18710 tests/gitea-actions.test.mjs
|
||||
fcc9a063882840dd89d74c2785284c8f2f6a9e5acec482b6d89ed8de62efdb85 9635 tests/inventory-classifier.test.mjs
|
||||
62b90c21c15b841af30d26ccb0b9e88d25674fa7dbff9dc231dd8a1dddc3d657 2025 tests/ipc-contract.test.mjs
|
||||
caf98cbd9de9b119dae610ee53fa333a7a11214f34762247452fbb85e8bbf725 2392 tests/log-redaction.test.mjs
|
||||
5c66b01ab11585aab6d923a9811ec76eef34e918f1a7f37227edd3f4337c932f 2981 tests/log-redaction.test.mjs
|
||||
96432a97d313f331694900bf0a2c21e38c20eac96d59147977aeed9055a9e3ad 2287 tests/partial-staging.test.mjs
|
||||
1b6c920e18a248f78acaed6187197c88ec8d911b62d5e2a9f8ad57b91ae80499 11827 tests/preflight.test.mjs
|
||||
7f2751ea2621f76b5427f442e931344d13e97faa7b6ef3151949bbd6a03097cf 1205 tests/process-error-policy.test.mjs
|
||||
@@ -251,7 +252,7 @@ a39d30f47813dfb98c998811f3d76ebbb1544ecfd017a165d44f9afb80d7daf9 9090
|
||||
c9354e4bf3720c28cff21c15ff8b9474ba4a23b7f55389de4326f4dffde54d78 9510 tests/ssh-service.test.mjs
|
||||
8a6a8477eb94b85ccef18cddd2640afb0d1eafa679c96bc7de20428d5d69e1be 1794 tests/tool-invocation.test.mjs
|
||||
3e4a1a6d6a744df9badcfece2cf8d09f8c34efb3c437cb08a6f2e6c9d428c0d4 59353 tests/unraid-deployment.test.mjs
|
||||
f5c1804f3f991cef02636a00b8bd8347f65bf863ea1749384ea75feceb522441 30792 tests/update-service.test.mjs
|
||||
05c0ef13fa2fa977174fdf8bdf76a7d606a049a43ad98901079cf5545d54d18d 31426 tests/update-service.test.mjs
|
||||
9cea5c1d5ba3e0972a0b5c7236cf1f7c5616373e0a39ea4a492ecebf70452e40 948 tests/validation.test.mjs
|
||||
7ef4d4b9f5f3e6979293b29d571ce0e39f83197f3cade2d999a9cea7bacdd84d 1781 tests/zip-writer.test.mjs
|
||||
8f36b542736f2933bad8b9464ad7fa37b68196009c81cf702ce3b677cd637dea 767 UPDATE_FROM_0.3.2.md
|
||||
|
||||
@@ -212,8 +212,8 @@ and last exit code. See `STATUS_ENDPOINT.md`.
|
||||
|
||||
## v0.4 services
|
||||
|
||||
- `UpdateService` reads `package.json` at an exact Gitea branch SHA, downloads an
|
||||
authenticated archive and launches the rollback-capable Windows source updater.
|
||||
- `UpdateService` reports `package.json` at an exact Gitea branch SHA, refuses
|
||||
unsigned source replacement and applies only publisher-signed packaged updates.
|
||||
- `SshService` provides pinned-host SSH execution with encrypted password or
|
||||
private-key passphrase storage.
|
||||
- `UnraidDeploymentService` inspects existing application folders and performs
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
# Deployment migration example
|
||||
|
||||
This example shows how to bring an existing Git-backed Docker or Unraid application under ForgeFlow control without exposing or overwriting runtime data.
|
||||
|
||||
Use synthetic names and values while testing. Replace them with your own repository, server and paths only in ForgeFlow's local configuration; do not commit credentials or environment-specific diagnostics.
|
||||
|
||||
## 1. Establish the authoritative repository
|
||||
|
||||
Before deploying, verify that the server checkout and Gitea repository represent the same application:
|
||||
|
||||
- compare the complete 40-character commit SHA;
|
||||
- confirm the configured remote belongs to the intended Gitea origin and repository;
|
||||
- preserve the root `.git` directory for exact-SHA verification and rollback;
|
||||
- resolve any remote URL mismatch explicitly instead of silently rewriting it.
|
||||
|
||||
ForgeFlow blocks deployment when the existing origin conflicts with the selected repository unless the user explicitly approves alignment.
|
||||
|
||||
## 2. Protect runtime data
|
||||
|
||||
Typical persistent paths include:
|
||||
|
||||
```text
|
||||
.env
|
||||
appdata/
|
||||
config/
|
||||
data/
|
||||
logs/
|
||||
compose.override.yml
|
||||
```
|
||||
|
||||
Keep those paths outside the tracked deployment payload and add runtime-only directories to `.dockerignore` when they are not build inputs. ForgeFlow uses a controlled Git reset without `git clean`, but the repository's own Compose and ignore rules remain authoritative.
|
||||
|
||||
## 3. Reuse the maintained Compose definition
|
||||
|
||||
Prefer the repository's existing `compose.yml` or `docker-compose.yml` when it already defines ports, volumes, device mappings, labels and health checks. These application-specific settings should be reviewed and versioned with the application rather than regenerated during deployment.
|
||||
|
||||
## 4. Handle nested repositories separately
|
||||
|
||||
A historical checkout such as `source/` may contain another `.git` directory. Treat this as a migration warning:
|
||||
|
||||
1. verify that the root Compose file builds from the intended root;
|
||||
2. back up the application folder;
|
||||
3. stop modifying the nested checkout;
|
||||
4. rename it temporarily;
|
||||
5. rebuild and verify the application from the root checkout;
|
||||
6. remove the legacy copy only after rollback has also been tested.
|
||||
|
||||
ForgeFlow reports nested repositories but does not delete them automatically.
|
||||
|
||||
## 5. Recommended profile
|
||||
|
||||
```text
|
||||
Provider: SSH / Unraid
|
||||
Server folder: example-app
|
||||
Branch: main
|
||||
Compose mode: Repository/server Compose
|
||||
Compose file: compose.yml
|
||||
Clone URL: a Git URL reachable from the server
|
||||
Healthcheck: the application's existing health endpoint
|
||||
Preserve paths: .env, appdata, config, data, logs, compose.override.yml
|
||||
```
|
||||
|
||||
Complete a preflight first, deploy one exact commit, verify both the live SHA and runtime health, and test rollback before treating the migration as production-ready.
|
||||
@@ -1,147 +0,0 @@
|
||||
# LumaOps server versus Gitea audit
|
||||
|
||||
This audit compares the supplied `lumaops_server.zip` and `LumaOps_gitea.zip`.
|
||||
|
||||
## Main result
|
||||
|
||||
The main Unraid working tree and the supplied Gitea checkout point to exactly the same commit:
|
||||
|
||||
```text
|
||||
d42d4a7f08240c478d07466e3fabec654dc71367
|
||||
```
|
||||
|
||||
Latest subject:
|
||||
|
||||
```text
|
||||
Preserve colors across Aura zone updates
|
||||
```
|
||||
|
||||
There is therefore no source-version drift at the root of the live LumaOps folder.
|
||||
|
||||
## Root Git repository
|
||||
|
||||
The root `.git` directory should remain in place. It enables:
|
||||
|
||||
- exact-SHA verification;
|
||||
- controlled fetch and reset;
|
||||
- a reliable previous-version reference;
|
||||
- rollback without copying a second complete source tree.
|
||||
|
||||
The archived server copy showed one root status difference for `scripts/unraid-hardware-setup.sh`: file mode `100755 → 100644`. This is consistent with Unix executable bits being lost during ZIP handling. The file content did not differ. Check the executable bit directly on Unraid before deployment.
|
||||
|
||||
|
||||
## Origin URL mismatch to resolve
|
||||
|
||||
The supplied server root uses:
|
||||
|
||||
```text
|
||||
ssh://git@127.0.0.1:222/NuklearRabbit/LumaOps.git
|
||||
```
|
||||
|
||||
The supplied Gitea checkout uses:
|
||||
|
||||
```text
|
||||
https://gitea.itworx.tech/Jens/LumaOps.git
|
||||
```
|
||||
|
||||
Although both archives currently point to the same commit, these are different
|
||||
repository paths. Before the first ForgeFlow deployment, choose the server-
|
||||
reachable URL for the authoritative `Jens/LumaOps` repository, for example an
|
||||
SSH URL through `127.0.0.1:222` when Gitea runs on the same Unraid host.
|
||||
|
||||
ForgeFlow 0.4 detects this mismatch. Deployment is blocked unless the profile
|
||||
matches the existing origin or **Align an existing server origin to this URL**
|
||||
is explicitly enabled.
|
||||
|
||||
## Runtime and persistent paths
|
||||
|
||||
The server copy contains runtime data that must not be replaced by source updates:
|
||||
|
||||
- `appdata/`
|
||||
- `data/`
|
||||
- `logs/`
|
||||
- `.env` and application-specific configuration
|
||||
|
||||
The repository `.gitignore` already excludes the principal runtime paths. ForgeFlow's SSH strategy uses Git reset without `git clean`, so untracked persistent data remains in place.
|
||||
|
||||
## Compose and Unraid integration
|
||||
|
||||
The root `docker-compose.yml` is already suitable as the authoritative deployment definition. It includes:
|
||||
|
||||
- build context at the project root;
|
||||
- container name `lumaops`;
|
||||
- the Unraid `dockerman` label;
|
||||
- a Web UI label;
|
||||
- an Unraid icon label;
|
||||
- `${WEB_PORT:-1223}:${APP_PORT:-8080}`;
|
||||
- persistent relative volumes;
|
||||
- USB, HID and I²C devices;
|
||||
- a healthcheck.
|
||||
|
||||
ForgeFlow should use this existing Compose file rather than generate a replacement. Ports and complex device mappings belong in the repository's maintained Compose definition.
|
||||
|
||||
The supplied `.dockerignore` already excludes `.git`, so keeping the root Git
|
||||
working tree does **not** copy Git history into the Docker build context. It does
|
||||
not yet explicitly exclude the existing runtime/legacy folders `appdata/`,
|
||||
`data/`, `logs/` and `source/`. Before the first production rebuild, add the
|
||||
paths that are not build inputs:
|
||||
|
||||
```text
|
||||
appdata/
|
||||
data/
|
||||
logs/
|
||||
source/
|
||||
.forgeflow/
|
||||
```
|
||||
|
||||
ForgeFlow 0.4 detects existing preserved paths and nested Git repositories that
|
||||
are missing from `.dockerignore` and reports them as a preflight warning. The
|
||||
tool does not silently edit a source-controlled `.dockerignore`; the correction
|
||||
should be committed to Gitea so every deployment uses the same build context.
|
||||
|
||||
## Nested `source/` repository
|
||||
|
||||
The server archive also contains a nested Git working tree under:
|
||||
|
||||
```text
|
||||
source/
|
||||
```
|
||||
|
||||
Its HEAD is:
|
||||
|
||||
```text
|
||||
b746a52af1613f4291235f5e8165b8197a269a79
|
||||
```
|
||||
|
||||
It was ahead of its own upstream and included rebase metadata in the supplied archive. The root Compose file uses build context `.` and does not reference `source/`. This strongly indicates that `source/` is an abandoned or historical checkout rather than the active deployment source.
|
||||
|
||||
ForgeFlow reports this as a nested-repository warning and does not remove it automatically.
|
||||
|
||||
Recommended migration:
|
||||
|
||||
1. Back up `/mnt/user/appdata/lumaops`.
|
||||
2. Verify on Unraid that `docker compose config` uses the root project.
|
||||
3. Stop changing files in `source/`.
|
||||
4. Rename it temporarily to `source.legacy-backup`.
|
||||
5. Rebuild and test LumaOps from the root.
|
||||
6. Remove the legacy copy only after a successful validation period.
|
||||
|
||||
Do not delete the root `.git` directory. Also do not delete the nested `source/`
|
||||
directory as part of the first ForgeFlow test. Treat its cleanup as a separate,
|
||||
backed-up migration after the root deployment and rollback have both been
|
||||
validated.
|
||||
|
||||
## Recommended ForgeFlow profile
|
||||
|
||||
```text
|
||||
Provider: SSH / Unraid
|
||||
Server folder: lumaops
|
||||
Branch: main
|
||||
Compose mode: Repository/server Compose
|
||||
Compose file: docker-compose.yml
|
||||
Clone URL: the Git URL reachable from Unraid
|
||||
Healthcheck: the existing LumaOps health URL, when exposed
|
||||
Preserve paths: .env, appdata, data, logs, config, compose.override.yml
|
||||
```
|
||||
|
||||
No folder rename is required for LumaOps because `lumaops` already aligns with the repository name.
|
||||
@@ -48,9 +48,9 @@ Deployment profiles support:
|
||||
- runtime-data preservation;
|
||||
- rollback to the previous SHA.
|
||||
|
||||
## LumaOps audit
|
||||
## Deployment migration example
|
||||
|
||||
The supplied server and Gitea roots both match commit `d42d4a7f08240c478d07466e3fabec654dc71367`. The root Git checkout and Compose file should remain. A stale nested `source/` Git checkout is documented for controlled cleanup.
|
||||
The documented migration flow keeps the root Git checkout and maintained Compose file in place, verifies the complete commit SHA and treats a stale nested `source/` checkout as separate, controlled cleanup.
|
||||
|
||||
## Validation
|
||||
|
||||
|
||||
+5
-4
@@ -23,7 +23,7 @@ flexibility.
|
||||
- session-only fallback when OS encryption is unavailable;
|
||||
- token omitted from renderer-visible public state;
|
||||
- encrypted token blob excluded from diagnostic bundles;
|
||||
- blank settings token field preserves the existing token;
|
||||
- a blank settings token field preserves the existing token only when the normalized Gitea origin is unchanged;
|
||||
- atomic config replacement and restrictive permissions where supported;
|
||||
- service URLs reject embedded user credentials;
|
||||
- no token is required by setup/build scripts or documentation.
|
||||
@@ -128,9 +128,10 @@ included model uses:
|
||||
require the exact user-confirmed pinned fingerprint;
|
||||
- remote folders and Compose paths are validated against traversal;
|
||||
- tracked server-side changes block exact-SHA reset;
|
||||
- updater tokens are sent only to the configured Gitea origin;
|
||||
- updater tokens are sent only to the configured Gitea origin, and changing that origin requires a newly entered token;
|
||||
- non-loopback Gitea connections require HTTPS;
|
||||
- packaged updates require a publisher-signed Ed25519 manifest that binds the
|
||||
source commit, artifact identity, byte length and SHA-256 digest;
|
||||
- update archives are checksummed and validated by the full local quality gate;
|
||||
- source backup is restored when an update fails.
|
||||
- packaged update bytes are rehashed immediately before apply;
|
||||
- integrated source replacement is disabled until source archives carry the
|
||||
same independent publisher signature.
|
||||
|
||||
+9
-21
@@ -2,31 +2,19 @@
|
||||
|
||||
ForgeFlow stores credentials, repository mappings, preferences, deployment profiles, diagnostics and operation history outside the source directory.
|
||||
|
||||
## Built-in source update
|
||||
## Source checkouts
|
||||
|
||||
Open **Settings → ForgeFlow updates** and choose:
|
||||
Integrated source replacement is disabled until source archives are covered by the same independent publisher signature as packaged releases. A server-provided commit SHA and a checksum calculated from the downloaded archive do not independently authenticate its publisher, while dependency installation can execute package lifecycle scripts.
|
||||
|
||||
1. **Check now**
|
||||
2. **Download update**
|
||||
3. **Apply & restart**
|
||||
Update a source checkout through Git instead:
|
||||
|
||||
The default update source is the configured Gitea instance, repository `Jens/ForgeFlow`, branch `main`.
|
||||
1. fetch the configured upstream;
|
||||
2. review the exact commit and release notes;
|
||||
3. switch to the intended release commit or tag;
|
||||
4. run `npm ci --ignore-scripts` and review the dependency lifecycle allowlist;
|
||||
5. run `npm run check` before starting ForgeFlow.
|
||||
|
||||
The updater pins the download to the exact remote commit, checks the archive SHA-256, starts an external PowerShell helper and waits for a structured `started` marker. ForgeFlow closes only after that marker exists. The helper then:
|
||||
|
||||
1. waits for the old process to exit;
|
||||
2. backs up the current source;
|
||||
3. extracts and validates the requested semantic version;
|
||||
4. mirrors the incoming source;
|
||||
5. runs `npm ci --no-audit --no-fund` when the published release contains `package-lock.json`, otherwise a pinned direct-dependency `npm install`;
|
||||
6. runs `npm run check`;
|
||||
7. writes the successful installation result before restart;
|
||||
8. launches the installed Electron executable directly;
|
||||
9. persists `success`, `failed` or `rolled-back` state for the next launch.
|
||||
|
||||
A failed validation restores the previous source. A successful installation is not rolled back merely because automatic restart fails; start ForgeFlow manually and the persisted result is shown.
|
||||
|
||||
Update logs and status files are stored beneath ForgeFlow's local user-data `updates` folder and exclude the Gitea token.
|
||||
The in-app updater remains available for signed packaged Windows releases.
|
||||
|
||||
## Packaged Windows updates
|
||||
|
||||
|
||||
Generated
+33
-33
@@ -68,9 +68,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@electron/asar/node_modules/brace-expansion": {
|
||||
"version": "1.1.17",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.17.tgz",
|
||||
"integrity": "sha512-w+aeW/mkgM4PyRMOJCgi3fOrTm5Q8QY1OSfn2TO2iuDj3ezIHqejmuxbjfPrqUkgqRew1iqkyAn0tr0ZwHD9+w==",
|
||||
"version": "1.1.18",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
||||
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -257,9 +257,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@electron/universal/node_modules/brace-expansion": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.3.tgz",
|
||||
"integrity": "sha512-DRdx5neNsG/QXbniLFWi2YmC/68oeOOmKz6zOjVk6ZS1ZLXgLIKqVEc6hWsmkjBbgii0SwaBTcJ5XKj5gzY/4A==",
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
|
||||
"integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -401,9 +401,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@eslint/config-array/node_modules/brace-expansion": {
|
||||
"version": "1.1.17",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.17.tgz",
|
||||
"integrity": "sha512-w+aeW/mkgM4PyRMOJCgi3fOrTm5Q8QY1OSfn2TO2iuDj3ezIHqejmuxbjfPrqUkgqRew1iqkyAn0tr0ZwHD9+w==",
|
||||
"version": "1.1.18",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
||||
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -482,9 +482,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
|
||||
"version": "1.1.17",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.17.tgz",
|
||||
"integrity": "sha512-w+aeW/mkgM4PyRMOJCgi3fOrTm5Q8QY1OSfn2TO2iuDj3ezIHqejmuxbjfPrqUkgqRew1iqkyAn0tr0ZwHD9+w==",
|
||||
"version": "1.1.18",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
||||
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1357,9 +1357,9 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
|
||||
"integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
|
||||
"version": "5.0.9",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
|
||||
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2014,9 +2014,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dir-compare/node_modules/brace-expansion": {
|
||||
"version": "1.1.17",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.17.tgz",
|
||||
"integrity": "sha512-w+aeW/mkgM4PyRMOJCgi3fOrTm5Q8QY1OSfn2TO2iuDj3ezIHqejmuxbjfPrqUkgqRew1iqkyAn0tr0ZwHD9+w==",
|
||||
"version": "1.1.18",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
||||
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2471,9 +2471,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/eslint/node_modules/brace-expansion": {
|
||||
"version": "1.1.17",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.17.tgz",
|
||||
"integrity": "sha512-w+aeW/mkgM4PyRMOJCgi3fOrTm5Q8QY1OSfn2TO2iuDj3ezIHqejmuxbjfPrqUkgqRew1iqkyAn0tr0ZwHD9+w==",
|
||||
"version": "1.1.18",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
||||
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2587,9 +2587,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-uri": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz",
|
||||
"integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==",
|
||||
"version": "3.1.6",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.6.tgz",
|
||||
"integrity": "sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -2652,9 +2652,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/filelist/node_modules/brace-expansion": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.3.tgz",
|
||||
"integrity": "sha512-DRdx5neNsG/QXbniLFWi2YmC/68oeOOmKz6zOjVk6ZS1ZLXgLIKqVEc6hWsmkjBbgii0SwaBTcJ5XKj5gzY/4A==",
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
|
||||
"integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2927,9 +2927,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/glob/node_modules/brace-expansion": {
|
||||
"version": "1.1.17",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.17.tgz",
|
||||
"integrity": "sha512-w+aeW/mkgM4PyRMOJCgi3fOrTm5Q8QY1OSfn2TO2iuDj3ezIHqejmuxbjfPrqUkgqRew1iqkyAn0tr0ZwHD9+w==",
|
||||
"version": "1.1.18",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
||||
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3365,9 +3365,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
|
||||
"integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",
|
||||
"integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@
|
||||
"scripts/prune-dist.mjs",
|
||||
"scripts/sign-release-manifest.mjs",
|
||||
"docs/RELEASE_NOTES_0.4.0.md",
|
||||
"docs/LUMAOPS_SERVER_AUDIT.md",
|
||||
"docs/DEPLOYMENT_MIGRATION_EXAMPLE.md",
|
||||
"docs/SSH_UNRAID_DEPLOYMENT.md",
|
||||
"docs/RELEASE_NOTES_0.4.1.md",
|
||||
"docs/RELEASE_NOTES_0.4.2.md",
|
||||
|
||||
@@ -120,7 +120,6 @@ contextBridge.exposeInMainWorld(
|
||||
overrideReason: options.overrideReason || '',
|
||||
}),
|
||||
rollback: (repository, profileId, targetSha) => invoke('deployment:rollback', { repository, profileId, targetSha }),
|
||||
healthcheck: (url) => invoke('deployment:health', { url }),
|
||||
refreshProfileState: (fullName, profileId) => invoke('deployment:profile-state', { fullName, profileId }),
|
||||
discoverServerDeployments: () => invoke('deployment:discover-server-workloads'),
|
||||
planServerReconciliation: (serverId) => invoke('deployment:plan-server-reconciliation', { serverId }),
|
||||
|
||||
@@ -1,28 +1,44 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { readdir, readFile, stat, writeFile } from 'node:fs/promises';
|
||||
import { execFile } from 'node:child_process';
|
||||
import { readFile, stat, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { promisify } from 'node:util';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const excludedDirectories = new Set(['.git', '.forgeflow', 'artifacts', 'coverage', 'dist', 'node_modules', 'playwright-report', 'ForgeFlow-runtime-win-x64']);
|
||||
const excludedFiles = new Set(['SOURCE_MANIFEST.txt']);
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
async function collect(directory, output = []) {
|
||||
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
||||
if (excludedDirectories.has(entry.name)) continue;
|
||||
const absolute = path.join(directory, entry.name);
|
||||
if (entry.isDirectory()) await collect(absolute, output);
|
||||
else if (!excludedFiles.has(entry.name)) output.push(absolute);
|
||||
async function collect() {
|
||||
const { stdout } = await execFileAsync(
|
||||
'git',
|
||||
['ls-files', '--cached', '--others', '--exclude-standard', '-z'],
|
||||
{ cwd: root, encoding: 'buffer', maxBuffer: 16 * 1024 * 1024 },
|
||||
);
|
||||
const relativePaths = stdout
|
||||
.toString('utf8')
|
||||
.split('\0')
|
||||
.filter(Boolean)
|
||||
.filter((relative) => !excludedFiles.has(relative));
|
||||
|
||||
const existing = [];
|
||||
for (const relative of relativePaths) {
|
||||
const absolute = path.resolve(root, relative);
|
||||
try {
|
||||
if ((await stat(absolute)).isFile()) existing.push(absolute);
|
||||
} catch (error) {
|
||||
if (error?.code !== 'ENOENT') throw error;
|
||||
}
|
||||
}
|
||||
return output;
|
||||
return existing;
|
||||
}
|
||||
|
||||
const packageJson = JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8'));
|
||||
const files = (await collect(root)).sort((left, right) => left.localeCompare(right, 'en'));
|
||||
const files = (await collect()).sort((left, right) => left.localeCompare(right, 'en'));
|
||||
const lines = [
|
||||
`ForgeFlow ${packageJson.version} source manifest`,
|
||||
'SHA-256 BYTES PATH',
|
||||
'(The manifest excludes itself, dependencies and generated release artifacts.)'
|
||||
'(The manifest includes tracked and non-ignored source files, excluding itself.)'
|
||||
];
|
||||
|
||||
for (const absolute of files) {
|
||||
|
||||
+6
-6
@@ -101,7 +101,7 @@ const required = [
|
||||
"docs/DIAGNOSTICS.md",
|
||||
"docs/DEPLOYMENT_SETUP.md",
|
||||
"docs/SSH_UNRAID_DEPLOYMENT.md",
|
||||
"docs/LUMAOPS_SERVER_AUDIT.md",
|
||||
"docs/DEPLOYMENT_MIGRATION_EXAMPLE.md",
|
||||
"docs/STATUS_ENDPOINT.md",
|
||||
"docs/TEST_MATRIX.md",
|
||||
"docs/RELEASE_NOTES_0.4.0.md",
|
||||
@@ -235,8 +235,8 @@ const sshGuide = await readFile(
|
||||
path.join(root, "docs/SSH_UNRAID_DEPLOYMENT.md"),
|
||||
"utf8",
|
||||
);
|
||||
const audit = await readFile(
|
||||
path.join(root, "docs/LUMAOPS_SERVER_AUDIT.md"),
|
||||
const migrationExample = await readFile(
|
||||
path.join(root, "docs/DEPLOYMENT_MIGRATION_EXAMPLE.md"),
|
||||
"utf8",
|
||||
);
|
||||
const releaseNotes = await readFile(
|
||||
@@ -264,11 +264,11 @@ if (
|
||||
);
|
||||
}
|
||||
if (
|
||||
!audit.includes("d42d4a7f08240c478d07466e3fabec654dc71367") ||
|
||||
!audit.includes("source/")
|
||||
!migrationExample.includes("complete 40-character commit SHA") ||
|
||||
!migrationExample.includes("source/")
|
||||
) {
|
||||
throw new Error(
|
||||
"LumaOps audit is missing the exact matching SHA or nested repository finding.",
|
||||
"Deployment migration example is missing exact-SHA or nested repository guidance.",
|
||||
);
|
||||
}
|
||||
for (const phrase of [
|
||||
|
||||
@@ -4,7 +4,7 @@ const fs = require('node:fs/promises');
|
||||
const path = require('node:path');
|
||||
const crypto = require('node:crypto');
|
||||
const { safeStorage } = require('electron');
|
||||
const { assertHttpUrl, assertWorkflowFileName, assertBranchName, assertEnvironmentName, assertCloneRemote, assertRepositoryRelativePath, assertRepositoryRelativePaths } = require('../shared/validation.cjs');
|
||||
const { normalizeBaseUrl, assertHttpUrl, assertWorkflowFileName, assertBranchName, assertEnvironmentName, assertCloneRemote, assertRepositoryRelativePath, assertRepositoryRelativePaths } = require('../shared/validation.cjs');
|
||||
|
||||
const DEFAULT_CONFIG = {
|
||||
schemaVersion: 13,
|
||||
@@ -266,7 +266,16 @@ class ConfigStore {
|
||||
const basePath = String(source.basePath || existing?.basePath || '/mnt/user/appdata').trim().replace(/\/+$/, '');
|
||||
if (!basePath.startsWith('/') || /[\r\n\0]/.test(basePath)) throw new Error('The server base path must be an absolute Unix path.');
|
||||
const privateKeyPath = String(source.privateKeyPath || existing?.privateKeyPath || '').trim();
|
||||
const hostFingerprint = String(source.hostFingerprint || existing?.hostFingerprint || '').trim();
|
||||
const credentialIdentityChanged = Boolean(existing && [
|
||||
['host', existing.host, host],
|
||||
['port', existing.port, port],
|
||||
['username', existing.username, username],
|
||||
['authType', existing.authType, authType],
|
||||
['privateKeyPath', existing.privateKeyPath, privateKeyPath]
|
||||
].some(([, previous, next]) => String(previous || '') !== String(next || '')));
|
||||
const hostFingerprint = credentialIdentityChanged
|
||||
? ''
|
||||
: String(source.hostFingerprint || existing?.hostFingerprint || '').trim();
|
||||
const scanRoots = uniqueStrings(source.scanRoots || existing?.scanRoots || [basePath]).map((value) => value.replace(/\/+$/, '')).filter((value) => value.startsWith('/') && !/[\r\n\0]/.test(value));
|
||||
const scanExcludes = uniqueStrings(source.scanExcludes || existing?.scanExcludes || ['backups', 'archives', 'releases', 'staging', 'testdata']).filter((value) => /^[a-zA-Z0-9._*-]+$/.test(value));
|
||||
return {
|
||||
@@ -281,8 +290,8 @@ class ConfigStore {
|
||||
scanExcludes,
|
||||
privateKeyPath,
|
||||
hostFingerprint,
|
||||
encryptedPassword: existing?.encryptedPassword || null,
|
||||
encryptedPassphrase: existing?.encryptedPassphrase || null,
|
||||
encryptedPassword: credentialIdentityChanged ? null : existing?.encryptedPassword || null,
|
||||
encryptedPassphrase: credentialIdentityChanged ? null : existing?.encryptedPassphrase || null,
|
||||
createdAt: existing?.createdAt || new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
@@ -385,10 +394,19 @@ class ConfigStore {
|
||||
}
|
||||
|
||||
async updateGitea({ baseUrl, token, user }) {
|
||||
const nextBaseUrl = normalizeBaseUrl(baseUrl);
|
||||
const currentBaseUrl = this.data.gitea.baseUrl
|
||||
? normalizeBaseUrl(this.data.gitea.baseUrl)
|
||||
: '';
|
||||
if (!String(token || '').trim() && nextBaseUrl !== currentBaseUrl && this.getToken()) {
|
||||
const error = new Error('Enter a new Gitea token when changing the server address.');
|
||||
error.code = 'GITEA_TOKEN_ORIGIN_CHANGED';
|
||||
throw error;
|
||||
}
|
||||
const tokenState = this.setToken(token, { preserveExisting: true });
|
||||
this.data.gitea = {
|
||||
...this.data.gitea,
|
||||
baseUrl,
|
||||
baseUrl: nextBaseUrl,
|
||||
user: user || this.data.gitea.user,
|
||||
encryptedToken: this.data.gitea.encryptedToken
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const crypto = require('node:crypto');
|
||||
const { createZip } = require('../shared/zip-writer.cjs');
|
||||
const { sanitizeForDiagnostics, redactSecrets } = require('./log-redaction.cjs');
|
||||
const { sanitizeForDiagnostics } = require('./log-redaction.cjs');
|
||||
|
||||
const LEVELS = { debug: 10, info: 20, warning: 30, error: 40 };
|
||||
|
||||
@@ -202,7 +202,7 @@ class DiagnosticsService {
|
||||
return this.getStatus();
|
||||
}
|
||||
|
||||
async collectLogs(maxBytes = 20 * 1024 * 1024) {
|
||||
async collectLogs(maxBytes = 20 * 1024 * 1024, { strictIdentifiers = false } = {}) {
|
||||
await this.flush();
|
||||
const output = [];
|
||||
let used = 0;
|
||||
@@ -211,7 +211,16 @@ class DiagnosticsService {
|
||||
const remaining = maxBytes - used;
|
||||
const content = await fs.readFile(file.path);
|
||||
const slice = content.length > remaining ? content.subarray(content.length - remaining) : content;
|
||||
output.push({ name: `logs/${file.name}`, data: Buffer.from(redactSecrets(slice.toString('utf8'), this.secretProvider?.() || []), 'utf8') });
|
||||
output.push({
|
||||
name: `logs/${file.name}`,
|
||||
data: Buffer.from(
|
||||
sanitizeForDiagnostics(slice.toString('utf8'), {
|
||||
secrets: this.secretProvider?.() || [],
|
||||
strictIdentifiers,
|
||||
}),
|
||||
'utf8',
|
||||
),
|
||||
});
|
||||
used += slice.length;
|
||||
}
|
||||
return output;
|
||||
@@ -345,7 +354,7 @@ class DiagnosticsService {
|
||||
{ name: 'operations-sanitized.json', data: safeJson(sanitizedOperations) },
|
||||
{ name: 'preflight.json', data: safeJson(sanitize(preflight || {})) },
|
||||
{ name: 'context.json', data: safeJson(sanitize(extra || {})) },
|
||||
...(await this.collectLogs())
|
||||
...(await this.collectLogs(20 * 1024 * 1024, { strictIdentifiers: strict }))
|
||||
];
|
||||
|
||||
const safetyAudit = auditBundleEntries(entries, this.secretProvider?.() || []);
|
||||
|
||||
@@ -3,13 +3,26 @@
|
||||
const { spawn } = require('node:child_process');
|
||||
const path = require('node:path');
|
||||
|
||||
function normalizeTool(tool, defaults) {
|
||||
const TOOL_PROFILES = Object.freeze({
|
||||
editor: Object.freeze({
|
||||
code: ['--reuse-window', '--goto', '{file}:{line}'],
|
||||
'code.exe': ['--reuse-window', '--goto', '{file}:{line}'],
|
||||
codium: ['--reuse-window', '--goto', '{file}:{line}'],
|
||||
'codium.exe': ['--reuse-window', '--goto', '{file}:{line}'],
|
||||
}),
|
||||
terminal: Object.freeze({
|
||||
wt: ['-d', '{path}'],
|
||||
'wt.exe': ['-d', '{path}'],
|
||||
}),
|
||||
});
|
||||
|
||||
function normalizeTool(tool, defaults, kind) {
|
||||
const source = tool && typeof tool === 'object' ? tool : {};
|
||||
const executable = String(source.executable || defaults.executable).trim();
|
||||
if (!executable || /[\r\n\0]/.test(executable)) throw new Error('Tool executable is invalid.');
|
||||
const args = (Array.isArray(source.args) ? source.args : defaults.args).map((item) => String(item)).slice(0, 20);
|
||||
if (args.some((item) => /[\r\n\0]/.test(item))) throw new Error('Tool argument is invalid.');
|
||||
return { executable, args };
|
||||
const profile = TOOL_PROFILES[kind]?.[executable.toLowerCase()];
|
||||
if (!profile) throw new Error(`Unsupported ${kind || 'external'} tool. Select a built-in trusted tool profile.`);
|
||||
return { executable, args: [...profile] };
|
||||
}
|
||||
|
||||
function expandTool(tool, context) {
|
||||
@@ -27,7 +40,7 @@ class ExternalToolsService {
|
||||
const defaults = kind === 'terminal'
|
||||
? { executable: 'wt.exe', args: ['-d', '{path}'] }
|
||||
: { executable: 'code', args: ['--reuse-window', '--goto', '{file}:{line}'] };
|
||||
const configured = normalizeTool(this.store.data.preferences?.[kind], defaults);
|
||||
const configured = normalizeTool(this.store.data.preferences?.[kind], defaults, kind);
|
||||
const invocation = expandTool(configured, { path: root, file: candidate, line });
|
||||
const child = spawn(invocation.executable, invocation.args, { cwd: root, detached: true, stdio: 'ignore', windowsHide: false, shell: false });
|
||||
child.unref();
|
||||
@@ -35,4 +48,4 @@ class ExternalToolsService {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ExternalToolsService, normalizeTool, expandTool };
|
||||
module.exports = { ExternalToolsService, normalizeTool, expandTool, TOOL_PROFILES };
|
||||
|
||||
+18
-2
@@ -20,6 +20,7 @@ const {
|
||||
readEncryptedBackup,
|
||||
} = require("./configuration-backup.cjs");
|
||||
const { evaluateDeploymentPolicy } = require("../shared/deployment-policy.cjs");
|
||||
const { normalizeBaseUrl } = require("../shared/validation.cjs");
|
||||
function registerIpc({
|
||||
store,
|
||||
git,
|
||||
@@ -253,8 +254,23 @@ function registerIpc({
|
||||
});
|
||||
|
||||
register("settings:update-gitea", async ({ baseUrl, token }) => {
|
||||
const effectiveToken = String(token || "").trim() || store.getToken();
|
||||
const validation = await gitea.validateConnection(baseUrl, effectiveToken);
|
||||
const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
|
||||
const currentBaseUrl = store.data.gitea.baseUrl
|
||||
? normalizeBaseUrl(store.data.gitea.baseUrl)
|
||||
: "";
|
||||
const submittedToken = String(token || "").trim();
|
||||
if (!submittedToken && normalizedBaseUrl !== currentBaseUrl) {
|
||||
const error = new Error(
|
||||
"Enter a new Gitea token when changing the server address. Stored tokens are bound to their original origin.",
|
||||
);
|
||||
error.code = "GITEA_TOKEN_ORIGIN_CHANGED";
|
||||
throw error;
|
||||
}
|
||||
const effectiveToken = submittedToken || store.getToken();
|
||||
const validation = await gitea.validateConnection(
|
||||
normalizedBaseUrl,
|
||||
effectiveToken,
|
||||
);
|
||||
const tokenState = await store.updateGitea({
|
||||
baseUrl: validation.baseUrl,
|
||||
token,
|
||||
|
||||
@@ -96,7 +96,6 @@ function registerDeploymentIpc({
|
||||
});
|
||||
},
|
||||
);
|
||||
register("deployment:health", ({ url }) => deployments.checkHealth(url));
|
||||
register("deployment:link-server-workload", async ({ repository, serverId, workloadId, deploymentMode = "server-git", remoteFolder = "" }) => {
|
||||
const current = await resolveRepository(repository);
|
||||
const result = await unraid.linkServerWorkload({
|
||||
|
||||
@@ -49,6 +49,13 @@ function stableAlias(value, prefix = 'item') {
|
||||
return `${prefix}-${hash}`;
|
||||
}
|
||||
|
||||
function redactPrivateInfrastructure(value) {
|
||||
return String(value ?? '')
|
||||
.replace(/\b(?:10(?:\.\d{1,3}){3}|127(?:\.\d{1,3}){3}|169\.254(?:\.\d{1,3}){2}|172\.(?:1[6-9]|2\d|3[01])(?:\.\d{1,3}){2}|192\.168(?:\.\d{1,3}){2})\b/g, '<PRIVATE_ADDRESS>')
|
||||
.replace(/\b(?:https?|ssh):\/\/[^\s"'<>]+/gi, '<PRIVATE_URL>')
|
||||
.replace(/\/(?:mnt|srv|opt|var\/lib)\/[^\s"'<>]*/g, '<SERVER_PATH>');
|
||||
}
|
||||
|
||||
function sanitizeForDiagnostics(value, options = {}, seen = new WeakSet()) {
|
||||
const {
|
||||
secrets = [],
|
||||
@@ -63,6 +70,7 @@ function sanitizeForDiagnostics(value, options = {}, seen = new WeakSet()) {
|
||||
if (typeof value === 'string') {
|
||||
let output = redactSecrets(value, secrets);
|
||||
if (pathMode === 'alias') output = pathAlias(output, { homeDir, cwd });
|
||||
if (strictIdentifiers) output = redactPrivateInfrastructure(output);
|
||||
return output;
|
||||
}
|
||||
if (value instanceof Error) {
|
||||
@@ -80,7 +88,7 @@ function sanitizeForDiagnostics(value, options = {}, seen = new WeakSet()) {
|
||||
output[key] = '[REDACTED]';
|
||||
continue;
|
||||
}
|
||||
if (strictIdentifiers && ['fullName', 'repository', 'owner', 'user', 'login', 'email'].includes(key)) {
|
||||
if (strictIdentifiers && ['full_name', 'repository', 'owner', 'user', 'login', 'email', 'host', 'hostname', 'username', 'base_path', 'private_key_path', 'local_path', 'remote_folder', 'remote_url', 'clone_url', 'status_url', 'healthcheck_url', 'web_ui_url', 'workspace_roots', 'scan_roots'].includes(normalizedKey.toLowerCase())) {
|
||||
output[key] = stableAlias(typeof item === 'object' ? JSON.stringify(item) : item, key.toLowerCase());
|
||||
continue;
|
||||
}
|
||||
@@ -90,4 +98,4 @@ function sanitizeForDiagnostics(value, options = {}, seen = new WeakSet()) {
|
||||
return output;
|
||||
}
|
||||
|
||||
module.exports = { redactSecrets, sanitizeForDiagnostics, pathAlias, stableAlias, SENSITIVE_KEY };
|
||||
module.exports = { redactSecrets, sanitizeForDiagnostics, pathAlias, stableAlias, redactPrivateInfrastructure, SENSITIVE_KEY };
|
||||
|
||||
@@ -97,6 +97,12 @@ function delay(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function requireSignedSourceUpdate(message) {
|
||||
const error = new Error(message);
|
||||
error.code = "SIGNED_SOURCE_UPDATE_REQUIRED";
|
||||
throw error;
|
||||
}
|
||||
|
||||
function resolveWindowsPowerShellPath(environment = process.env) {
|
||||
const windowsRoot = environment.SystemRoot || environment.WINDIR;
|
||||
if (windowsRoot) {
|
||||
@@ -325,6 +331,11 @@ class UpdateService {
|
||||
return this.downloadPackaged(update);
|
||||
}
|
||||
|
||||
requireSignedSourceUpdate(
|
||||
"Integrated source updates are disabled because source archives do not yet carry an independently signed publisher manifest. Update a source checkout with Git after reviewing the exact commit.",
|
||||
);
|
||||
|
||||
/* c8 ignore start -- retained for a future signed source-archive implementation */
|
||||
await fs.mkdir(this.updateDirectory, { recursive: true });
|
||||
const archiveUrl = `${this.store.data.gitea.baseUrl.replace(/\/+$/, "")}/${encodeURIComponent(update.owner)}/${encodeURIComponent(update.repo)}/archive/${update.remoteSha}.zip`;
|
||||
const archive = await this.gitea.downloadAuthenticated(archiveUrl);
|
||||
@@ -354,6 +365,7 @@ class UpdateService {
|
||||
sha256,
|
||||
});
|
||||
return { ...metadata, downloaded: true };
|
||||
/* c8 ignore stop */
|
||||
}
|
||||
|
||||
async downloadPackaged(update) {
|
||||
@@ -528,6 +540,10 @@ class UpdateService {
|
||||
"The integrated updater currently supports Windows only.",
|
||||
);
|
||||
if (update.kind === "binary") return this.applyPackaged(update);
|
||||
requireSignedSourceUpdate(
|
||||
"This source archive cannot be applied because it has no independently signed publisher manifest.",
|
||||
);
|
||||
/* c8 ignore start -- legacy helper retained only for migration compatibility */
|
||||
const stat = await fs.stat(update.archivePath).catch(() => null);
|
||||
if (!stat?.isFile())
|
||||
throw new Error("The staged update archive is no longer available.");
|
||||
@@ -662,6 +678,7 @@ class UpdateService {
|
||||
logPath,
|
||||
statusPath,
|
||||
};
|
||||
/* c8 ignore stop */
|
||||
}
|
||||
|
||||
async applyPackaged(update) {
|
||||
@@ -850,4 +867,5 @@ module.exports = {
|
||||
waitForUpdaterStarted,
|
||||
readJsonFile,
|
||||
readLogTail,
|
||||
requireSignedSourceUpdate,
|
||||
};
|
||||
|
||||
@@ -761,8 +761,8 @@ function renderSettings() {
|
||||
const update = ui.updateStatus;
|
||||
const servers = state.servers || [];
|
||||
return `<div class="settings-layout"><aside class="settings-nav"><button class="nav-button active">${icon("settings")}<span>General</span></button><button class="nav-button" data-action="check-updates">${icon("update")}<span>Updates</span></button><button class="nav-button" data-action="open-add-server">${icon("server")}<span>Servers</span></button><button class="nav-button" data-action="reset-app">${icon("trash")}<span>Reset setup</span></button></aside><div class="settings-content"><div class="page-header"><div><div class="eyebrow">Application</div><h1>Settings</h1><p>Connections, project discovery, secure SSH servers and application updates.</p></div></div>
|
||||
<section class="settings-group"><h2>Gitea connection</h2><div class="form-grid"><div class="field full"><label for="settings-gitea-url">Instance URL</label><input id="settings-gitea-url" class="input" value="${attr(state.gitea.baseUrl)}" placeholder="https://gitea.example.com" /></div><div class="field full"><label for="settings-gitea-token">New access token</label><input id="settings-gitea-token" class="input" type="password" placeholder="Leave empty to keep the existing token" /></div></div><div class="connection-card" style="margin-top:10px"><div><strong>${state.gitea.hasToken ? `Connected as ${escapeHtml(state.gitea.user?.login || "user")}` : "Not connected"}</strong><div class="queue-sub">${escapeHtml(state.gitea.baseUrl || "No Gitea instance configured")}</div></div><button class="button primary" data-action="save-gitea-settings">Validate & save</button></div></section>
|
||||
<section class="settings-group"><div class="section-heading"><div><h2>ForgeFlow updates</h2><span class="meta">Secure source update from ${escapeHtml(state.updates?.owner || "Jens")}/${escapeHtml(state.updates?.repo || "ForgeFlow")}</span></div><button class="button" data-action="check-updates" ${ui.updateChecking ? "disabled" : ""}>${icon("update")}${ui.updateChecking ? "Checking…" : "Check now"}</button></div><div class="form-grid"><div class="field"><label>Repository owner</label><input id="update-owner" class="input" value="${attr(state.updates?.owner || "Jens")}"/></div><div class="field"><label>Repository name</label><input id="update-repo" class="input" value="${attr(state.updates?.repo || "ForgeFlow")}"/></div><div class="field"><label>Release branch</label><input id="update-branch" class="input" value="${attr(state.updates?.branch || "main")}"/></div><div class="field"><label>Automatic startup check</label><select id="update-auto-check" class="select"><option value="true" ${state.updates?.autoCheck !== false ? "selected" : ""}>Enabled</option><option value="false" ${state.updates?.autoCheck === false ? "selected" : ""}>Disabled</option></select></div></div><div class="update-card ${update?.available ? "available" : ""}"><div>${icon(update?.available ? "download" : "check")}<span><strong>${update ? (update.available ? `ForgeFlow ${escapeHtml(update.remoteVersion)} is available` : `ForgeFlow ${escapeHtml(update.currentVersion)} is up to date`) : `Current version ${escapeHtml(ui.boot.appVersion)}`}</strong><small>${update ? `Branch ${escapeHtml(update.branch)} · commit ${escapeHtml(update.shortSha)} · checked ${formatDate(update.checkedAt)}` : "No update check in this session."}</small></span></div><div class="stack horizontal compact">${update?.available && !update.downloaded ? `<button class="button primary" data-action="download-update">${icon("download")}Download update</button>` : ""}${update?.downloaded ? `<button class="button success" data-action="apply-update">${icon("update")}Apply & restart</button>` : ""}<button class="button" data-action="save-update-settings">Save update settings</button></div></div><div class="notice" style="margin-top:10px">${icon("shield")}The updater downloads an authenticated ZIP for the exact remote commit, verifies its SHA-256 checksum, runs the complete quality gate and restores the previous source version if validation fails.</div></section>
|
||||
<section class="settings-group"><h2>Gitea connection</h2><div class="form-grid"><div class="field full"><label for="settings-gitea-url">Instance URL</label><input id="settings-gitea-url" class="input" value="${attr(state.gitea.baseUrl)}" placeholder="https://gitea.example.com" /></div><div class="field full"><label for="settings-gitea-token">New access token</label><input id="settings-gitea-token" class="input" type="password" placeholder="Leave empty only when keeping the same server" /></div></div><div class="connection-card" style="margin-top:10px"><div><strong>${state.gitea.hasToken ? `Connected as ${escapeHtml(state.gitea.user?.login || "user")}` : "Not connected"}</strong><div class="queue-sub">${escapeHtml(state.gitea.baseUrl || "No Gitea instance configured")}</div></div><button class="button primary" data-action="save-gitea-settings">Validate & save</button></div></section>
|
||||
<section class="settings-group"><div class="section-heading"><div><h2>ForgeFlow updates</h2><span class="meta">Signed packaged updates from ${escapeHtml(state.updates?.owner || "Jens")}/${escapeHtml(state.updates?.repo || "ForgeFlow")}</span></div><button class="button" data-action="check-updates" ${ui.updateChecking ? "disabled" : ""}>${icon("update")}${ui.updateChecking ? "Checking…" : "Check now"}</button></div><div class="form-grid"><div class="field"><label>Repository owner</label><input id="update-owner" class="input" value="${attr(state.updates?.owner || "Jens")}"/></div><div class="field"><label>Repository name</label><input id="update-repo" class="input" value="${attr(state.updates?.repo || "ForgeFlow")}"/></div><div class="field"><label>Release branch</label><input id="update-branch" class="input" value="${attr(state.updates?.branch || "main")}"/></div><div class="field"><label>Automatic startup check</label><select id="update-auto-check" class="select"><option value="true" ${state.updates?.autoCheck !== false ? "selected" : ""}>Enabled</option><option value="false" ${state.updates?.autoCheck === false ? "selected" : ""}>Disabled</option></select></div></div><div class="update-card ${update?.available ? "available" : ""}"><div>${icon(update?.available ? "download" : "check")}<span><strong>${update ? (update.available ? `ForgeFlow ${escapeHtml(update.remoteVersion)} is available${update.packaged ? "" : " in the source repository"}` : `ForgeFlow ${escapeHtml(update.currentVersion)} is up to date`) : `Current version ${escapeHtml(ui.boot.appVersion)}`}</strong><small>${update ? `Branch ${escapeHtml(update.branch)} · commit ${escapeHtml(update.shortSha)} · checked ${formatDate(update.checkedAt)}` : "No update check in this session."}</small></span></div><div class="stack horizontal compact">${update?.available && update.packaged && !update.downloaded ? `<button class="button primary" data-action="download-update">${icon("download")}Download signed update</button>` : ""}${update?.downloaded ? `<button class="button success" data-action="apply-update">${icon("update")}Apply & restart</button>` : ""}<button class="button" data-action="save-update-settings">Save update settings</button></div></div><div class="notice" style="margin-top:10px">${icon("shield")}${update && !update.packaged ? "Source checkouts must be updated with Git after reviewing the exact commit. Integrated source replacement remains disabled until source archives are publisher-signed." : "Packaged updates require an Ed25519 publisher signature that binds the exact commit, artifact name, size and SHA-256 digest."}</div></section>
|
||||
<section class="settings-group"><div class="section-heading"><div><h2>SSH / Unraid servers</h2><span class="meta">Credentials are encrypted locally; a new host fingerprint is shown before authentication.</span></div><button class="button primary" data-action="open-add-server">${icon("plus")}Add server</button></div>${servers.length ? `<div class="server-list">${servers.map((server) => `<article class="server-card"><div class="server-card-main">${icon("server")}<div><strong>${escapeHtml(server.name)}</strong><span>${escapeHtml(server.username)}@${escapeHtml(server.host)}:${escapeHtml(server.port)} · ${escapeHtml(server.basePath)}</span><small>${server.hostFingerprint ? `Trusted ${escapeHtml(server.hostFingerprint)}` : "Host identity not trusted yet"}</small></div></div><div class="stack horizontal compact"><button class="button" data-action="test-server" data-server-id="${attr(server.id)}">${server.hostFingerprint ? "Test connection" : "Preview & trust fingerprint"}</button><button class="button" data-action="edit-server" data-server-id="${attr(server.id)}">Edit</button><button class="icon-button danger" data-action="delete-server" data-server-id="${attr(server.id)}" title="Delete server">${icon("trash")}</button></div></article>`).join("")}</div>` : '<div class="empty-state compact"><p>No SSH server configured. Add your Unraid server before creating an SSH deployment profile.</p></div>'}</section>
|
||||
<section class="settings-group"><div class="section-heading"><div><h2>Git remote maintenance</h2><span class="meta">Standardize linked repositories to the current Gitea SSH URLs.</span></div><button class="button" data-action="normalize-origins">${icon("link")}Normalize all origins</button></div><p>This replaces legacy aliases and renamed owners only after an explicit click. Local commits and files are not changed.</p></section>
|
||||
<section class="settings-group"><h2>Project roots</h2><p>The first folder is the default clone destination. ForgeFlow automatically creates one subfolder per repository.</p><div class="stack">${state.workspaceRoots.map((root, index) => `<div class="root-row">${index === 0 ? '<span class="status-pill success">Default</span>' : ""}<input class="input" data-root-index="${index}" value="${attr(root)}" aria-label="Project root ${index + 1}"/><button class="icon-button" data-action="remove-root" data-index="${index}" title="Remove">${icon("trash")}</button></div>`).join("")}<button class="button" data-action="add-root">${icon("plus")}Add project root</button><button class="button primary" data-action="save-roots">Save folders & rescan</button></div></section>
|
||||
|
||||
@@ -193,7 +193,11 @@ test("setup, Gitea updates and generic patches retain normalized public state",
|
||||
assert.equal(completed.state.setupComplete, true);
|
||||
assert.equal(completed.state.gitea.hasToken, true);
|
||||
assert.deepEqual(completed.state.workspaceRoots, ["C:/Projects"]);
|
||||
const update = await store.updateGitea({ baseUrl: "https://new.test", token: "", user: null });
|
||||
await assert.rejects(
|
||||
store.updateGitea({ baseUrl: "https://new.test", token: "", user: null }),
|
||||
(error) => error.code === "GITEA_TOKEN_ORIGIN_CHANGED"
|
||||
);
|
||||
const update = await store.updateGitea({ baseUrl: "https://gitea.test", token: "", user: null });
|
||||
assert.equal(update.preserved, true);
|
||||
assert.equal(store.data.gitea.user.login, "jens");
|
||||
const patched = await store.patch({ appearance: "light", workspaceRoots: ["D:/Code", "D:/Code"] });
|
||||
@@ -215,6 +219,20 @@ test("server saves reject absent credentials before mutating configuration", asy
|
||||
assert.deepEqual(store.data.servers, []);
|
||||
});
|
||||
|
||||
test("server credentials and trust are cleared when the connection identity changes", async (t) => {
|
||||
const { store } = await storeFixture(t);
|
||||
store.encryptSecret = (value) => `encrypted:${value}`;
|
||||
const saved = await store.saveServer({ host: "server-one", username: "deploy", authType: "password", basePath: "/mnt/apps", hostFingerprint: "SHA256:trusted" }, { password: "test-password" });
|
||||
await assert.rejects(
|
||||
store.saveServer({ ...saved, host: "server-two" }, {}),
|
||||
/password is required/i
|
||||
);
|
||||
assert.equal(store.data.servers[0].host, "server-one");
|
||||
const changed = await store.saveServer({ ...saved, host: "server-two" }, { password: "replacement-password" });
|
||||
assert.equal(changed.hostFingerprint, "");
|
||||
assert.equal(changed.hasPassword, true);
|
||||
});
|
||||
|
||||
test("deployment profile normalization covers safe defaults and every optional Unraid control", async (t) => {
|
||||
const { store } = await storeFixture(t);
|
||||
const actions = store.normalizeDeploymentProfile({ environment: "qa", statusUrl: "https://app.test/status" });
|
||||
|
||||
@@ -41,7 +41,9 @@ test('writes structured local diagnostics and exports a secret-free support bund
|
||||
authorization: `token ${secret}`,
|
||||
password: 'unsafe-password',
|
||||
message: `request failed with ${secret}`,
|
||||
path: path.join(os.homedir(), 'private', 'repository')
|
||||
path: path.join(os.homedir(), 'private', 'repository'),
|
||||
host: '192.168.10.20',
|
||||
basePath: '/mnt/user/appdata/private-app'
|
||||
});
|
||||
await service.flush();
|
||||
|
||||
@@ -57,7 +59,7 @@ test('writes structured local diagnostics and exports a secret-free support bund
|
||||
const result = await service.exportSupportBundle({
|
||||
destinationPath: destination,
|
||||
privacyMode: 'strict',
|
||||
publicState: { gitea: { baseUrl: 'https://gitea.example.test', hasToken: true, encryptedToken: 'ciphertext' }, preferences: {} },
|
||||
publicState: { gitea: { baseUrl: 'https://gitea.example.test', hasToken: true, encryptedToken: 'ciphertext' }, servers: [{ host: '192.168.10.20', username: 'deploy', basePath: '/mnt/user/appdata/private-app' }], preferences: {} },
|
||||
repositories: [{ id: 1, fullName: 'jens/private-repo', localPath: path.join(os.homedir(), 'private-repo'), localStatus: { head: 'a'.repeat(40), branch: { head: 'main' }, counts: {}, clean: true } }],
|
||||
operations: [{ repository: 'jens/private-repo', status: 'failed', runnerLog: `Authorization: token ${secret}` }],
|
||||
preflight: { checks: [] }
|
||||
@@ -67,6 +69,7 @@ test('writes structured local diagnostics and exports a secret-free support bund
|
||||
const bundleText = [...entries.values()].map((value) => value.toString('utf8')).join('\n');
|
||||
assert.doesNotMatch(bundleText, new RegExp(secret));
|
||||
assert.doesNotMatch(bundleText, /ciphertext|unsafe-password|jens\/private-repo/);
|
||||
assert.doesNotMatch(bundleText, /192\.168\.10\.20|\/mnt\/user\/appdata\/private-app/);
|
||||
assert.match(entries.get('manifest.json').toString(), /"containsSecrets": false/);
|
||||
assert.match(entries.get('repositories-sanitized.json').toString(), /fullname-[a-f0-9]{12}/);
|
||||
|
||||
|
||||
@@ -5,9 +5,10 @@ import toolsModule from '../src/main/external-tools-service.cjs';
|
||||
const { normalizeTool, expandTool } = toolsModule;
|
||||
|
||||
test('external tool templates expand as argument arrays without a shell', () => {
|
||||
const tool = normalizeTool({ executable: 'code.exe', args: ['--goto', '{file}:{line}', '{path}'] }, {});
|
||||
const tool = normalizeTool({ executable: 'code.exe', args: ['--malicious', 'ignored'] }, { executable: 'code.exe' }, 'editor');
|
||||
const invocation = expandTool(tool, { path: 'C:\\Projects\\App', file: 'C:\\Projects\\App\\src\\app.js', line: 12 });
|
||||
assert.equal(invocation.executable, 'code.exe');
|
||||
assert.deepEqual(invocation.args, ['--goto', 'C:\\Projects\\App\\src\\app.js:12', 'C:\\Projects\\App']);
|
||||
assert.throws(() => normalizeTool({ executable: 'code.exe\ncalc.exe', args: [] }, {}), /invalid/);
|
||||
assert.deepEqual(invocation.args, ['--reuse-window', '--goto', 'C:\\Projects\\App\\src\\app.js:12']);
|
||||
assert.throws(() => normalizeTool({ executable: 'code.exe\ncalc.exe', args: [] }, {}, 'editor'), /invalid/);
|
||||
assert.throws(() => normalizeTool({ executable: 'powershell.exe', args: ['-Command', 'calc'] }, {}, 'terminal'), /unsupported terminal tool/i);
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import redaction from '../src/main/log-redaction.cjs';
|
||||
|
||||
const { redactSecrets, sanitizeForDiagnostics, pathAlias, stableAlias } = redaction;
|
||||
const { redactSecrets, sanitizeForDiagnostics, pathAlias, stableAlias, redactPrivateInfrastructure } = redaction;
|
||||
|
||||
test('redacts runtime credentials, structured secrets, private keys and URL credentials', () => {
|
||||
const token = ['gitea', 'TEST', 'ONLY', 'SecretToken123456'].join('_');
|
||||
@@ -31,15 +31,22 @@ test('sanitizes nested sensitive keys and aliases user paths', () => {
|
||||
});
|
||||
|
||||
test('strict privacy mode replaces stable identifiers deterministically', () => {
|
||||
const first = sanitizeForDiagnostics({ fullName: 'jens/private-project', login: 'jens' }, { strictIdentifiers: true });
|
||||
const second = sanitizeForDiagnostics({ fullName: 'jens/private-project', login: 'jens' }, { strictIdentifiers: true });
|
||||
const first = sanitizeForDiagnostics({ fullName: 'jens/private-project', login: 'jens', host: '192.168.10.20', basePath: '/mnt/user/appdata' }, { strictIdentifiers: true });
|
||||
const second = sanitizeForDiagnostics({ fullName: 'jens/private-project', login: 'jens', host: '192.168.10.20', basePath: '/mnt/user/appdata' }, { strictIdentifiers: true });
|
||||
assert.equal(first.fullName, second.fullName);
|
||||
assert.equal(first.login, second.login);
|
||||
assert.notEqual(first.fullName, 'jens/private-project');
|
||||
assert.match(first.fullName, /^fullname-[a-f0-9]{12}$/);
|
||||
assert.notEqual(first.host, '192.168.10.20');
|
||||
assert.notEqual(first.basePath, '/mnt/user/appdata');
|
||||
assert.equal(stableAlias('same', 'repo'), stableAlias('same', 'repo'));
|
||||
});
|
||||
|
||||
test('strict privacy redacts private addresses, infrastructure URLs and server paths in log text', () => {
|
||||
const result = redactPrivateInfrastructure('host 192.168.10.20 url https://internal.example.test/status path /mnt/user/appdata/example');
|
||||
assert.doesNotMatch(result, /192\.168\.10\.20|internal\.example\.test|\/mnt\/user\/appdata/);
|
||||
});
|
||||
|
||||
test('path aliasing handles slash variants', () => {
|
||||
const result = pathAlias('C:\\Users\\Jens\\src and C:/Users/Jens/src', { homeDir: 'C:\\Users\\Jens', cwd: 'D:\\ForgeFlow' });
|
||||
assert.doesNotMatch(result, /Users[\\/]Jens/);
|
||||
|
||||
@@ -118,7 +118,7 @@ test("update repository parts reject path injection", async () => {
|
||||
await rm(temp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("source updater confirms an external STARTED marker before ForgeFlow may close", async () => {
|
||||
test("source updater refuses an unsigned archive before launching a helper", async () => {
|
||||
const temp = await mkdtemp(
|
||||
path.join(os.tmpdir(), "forgeflow-update-handshake-"),
|
||||
);
|
||||
@@ -181,10 +181,11 @@ test("source updater confirms an external STARTED marker before ForgeFlow may cl
|
||||
remoteSha: "a".repeat(40),
|
||||
sha256: "b".repeat(64),
|
||||
};
|
||||
const result = await service.apply();
|
||||
assert.equal(result.confirmed, true);
|
||||
assert.ok(capturedArgs.includes("-StatusPath"));
|
||||
assert.ok(capturedArgs.includes("-UpdateId"));
|
||||
await assert.rejects(
|
||||
service.apply(),
|
||||
(error) => error.code === "SIGNED_SOURCE_UPDATE_REQUIRED",
|
||||
);
|
||||
assert.equal(capturedArgs, null);
|
||||
await rm(temp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
@@ -710,11 +711,12 @@ test("release manifest verification rejects a different publisher key", () => {
|
||||
});
|
||||
|
||||
test("Windows release pipeline emits signed provenance, manifest and SBOM evidence", async () => {
|
||||
const [pkgSource, signatureSource, checksumSource, manifestSigner] = await Promise.all([
|
||||
const [pkgSource, signatureSource, checksumSource, manifestSigner, releaseWorkflow] = await Promise.all([
|
||||
readFile(new URL("../package.json", import.meta.url), "utf8"),
|
||||
readFile(new URL("../scripts/verify-release-signatures.mjs", import.meta.url), "utf8"),
|
||||
readFile(new URL("../scripts/write-release-checksums.mjs", import.meta.url), "utf8"),
|
||||
readFile(new URL("../scripts/sign-release-manifest.mjs", import.meta.url), "utf8"),
|
||||
readFile(new URL("../.gitea/workflows/release.yml", import.meta.url), "utf8"),
|
||||
]);
|
||||
assert.match(pkgSource, /verify-release-signatures\.mjs/);
|
||||
assert.match(signatureSource, /FORGEFLOW_SIGNED_RELEASE/);
|
||||
@@ -729,6 +731,16 @@ test("Windows release pipeline emits signed provenance, manifest and SBOM eviden
|
||||
assert.match(manifestSigner, /Ed25519/);
|
||||
assert.match(manifestSigner, /release-manifest\.json/);
|
||||
assert.match(pkgSource, /sign-release-manifest\.mjs/);
|
||||
assert.doesNotMatch(releaseWorkflow, /checkout@v\d|setup-node@v\d/);
|
||||
assert.match(releaseWorkflow, /checkout@[a-f0-9]{40}/);
|
||||
assert.match(releaseWorkflow, /setup-node@[a-f0-9]{40}/);
|
||||
assert.ok(
|
||||
releaseWorkflow.indexOf("Validate version bump and build release artifacts") <
|
||||
releaseWorkflow.indexOf("FORGEFLOW_RELEASE_SIGNING_KEY_PEM"),
|
||||
"signing secrets must not be present during dependency installation and quality checks",
|
||||
);
|
||||
assert.match(releaseWorkflow, /finally \{/);
|
||||
assert.match(releaseWorkflow, /Remove-Item -LiteralPath \$privateKeyPath -Force/);
|
||||
const publisher = await readFile(new URL("../scripts/publish-binary-release.cjs", import.meta.url), "utf8");
|
||||
assert.match(publisher, /draft: true/);
|
||||
assert.match(publisher, /requiredAssets/);
|
||||
|
||||
Reference in New Issue
Block a user