From bffad670ef5d28777f98d70f9764a46f3e6d6d2c Mon Sep 17 00:00:00 2001 From: NuklearRabbit <145918611+NuklearRabbit@users.noreply.github.com> Date: Sun, 9 Aug 2026 00:43:08 +0200 Subject: [PATCH] fix: complete server pull deployment setup --- README.md | 2 +- SOURCE_MANIFEST.txt | 23 +++++++------ docs/RELEASE_NOTES_0.10.10.md | 10 ++++++ package-lock.json | 4 +-- package.json | 3 +- scripts/verify.mjs | 9 +++-- src/main/unraid-access-methods.cjs | 2 +- src/main/unraid-deployment-service.cjs | 2 +- src/main/unraid-inventory-methods.cjs | 25 ++++++++++++-- src/renderer/mock-repository-bridge.js | 2 +- tests/browser/forgeflow.spec.mjs | 7 ++-- tests/unraid-deployment.test.mjs | 46 ++++++++++++++++++++++++++ 12 files changed, 110 insertions(+), 25 deletions(-) create mode 100644 docs/RELEASE_NOTES_0.10.10.md diff --git a/README.md b/README.md index e169976..9915d36 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ForgeFlow is een Windows-desktopapp voor wie Git, Gitea en eigen Docker- of Unraid-servers gebruikt. Je ziet in één werkruimte wat lokaal gewijzigd is, wat op Gitea staat en welke exacte commit op de server draait. ForgeFlow begeleidt je daarna veilig door review, commit, push, deployment en verificatie. -> Huidige release: **0.10.9** · [download de laatste Windows-release](https://gitea.itworx.tech/Jens/ForgeFlow/releases/latest) +> Huidige release: **0.10.10** · [download de laatste Windows-release](https://gitea.itworx.tech/Jens/ForgeFlow/releases/latest) ![ForgeFlow release-overzicht](docs/screenshots/overview.png) diff --git a/SOURCE_MANIFEST.txt b/SOURCE_MANIFEST.txt index 7363164..e1a13f9 100644 --- a/SOURCE_MANIFEST.txt +++ b/SOURCE_MANIFEST.txt @@ -1,4 +1,4 @@ -ForgeFlow 0.10.9 source manifest +ForgeFlow 0.10.10 source manifest SHA-256 BYTES PATH (The manifest excludes itself, dependencies and generated release artifacts.) cedceb71eb846d99c7c4019031833c1c7f93b84a1c6073aec7d2435dc744ca3d 703 .gitea/workflows/quality.yml @@ -34,6 +34,7 @@ a0cd06a96f23a94e118feb012be0fa1ac51345951cb2ba8e67fb8c889c4c342a 5007 f79908fb3dad98c38030c6e6be7c79a1999e0478ed9c2496923891954438daa1 4581 docs/RELEASE_AUDIT_0.6.0.md 979a0b8e129979be6b265e8571d0a3c1e9ddd4ddb6b0bf55ae748d3478e51854 2296 docs/RELEASE_NOTES_0.10.0.md 0eb44bda2209a5979a6ac693ac4cd4d235c0015031e54b9e895990f37bf60054 1433 docs/RELEASE_NOTES_0.10.1.md +5d3240169765e3fb1d3cd391d09547101227e76dd4670ee46be8ca3a21553a03 894 docs/RELEASE_NOTES_0.10.10.md 8d713471a437a8a55b00d7e1dd95290680862107bc4e586cf27d727f6274e46c 577 docs/RELEASE_NOTES_0.10.2.md 0942fb2c4a4f972296423b5232687f7389e2c6417a9d48a3244beef9dec907b9 1164 docs/RELEASE_NOTES_0.10.3.md 8f4a0fe6dc250ae210cc2fc1c57c46091822ae6c2a58caa76e0091f255f9f30d 775 docs/RELEASE_NOTES_0.10.4.md @@ -103,14 +104,14 @@ c230b931abf2293d2d44b7a69b94c35f1142c093cc46b88739a0de5cbd6d1896 1532 4a561ead5ba7cdfaf4efce91842a4308c5f2a77980205879d83835efb8a579db 1067 LICENSE 970c2afb09f2b9d1e97a5a92c0bd824c054b72298c5a5d234396d5e34de065ae 13870 main.cjs 91a984a89dd57a084b9a2331763cacdb061582fb590f13df379d92c1a77a2ee1 352 OVERLAY-INSTRUCTIONS.md -f79efd5414d35bbcd1476956db1feaab43c3a8f6d22325ef1020a93c34f8e2ee 179806 package-lock.json -2b9c81b45209f775ced9af95b43e5a32e3bce9369406881c1aba1fdbf93ee402 5664 package.json +9c2567281d405b608b5381d79360288601a2a4211bdacf5a8cae990caa952f98 179808 package-lock.json +da986418af193d1334e4ab9e09a5dab41ebe32aace73127a145be834ef20bbd9 5704 package.json 2a597a5704c576783b8a72407fbc377fa7506b36a4596ea7f7bce126e394f837 1326 playwright.config.mjs 69318fdf054be7aa2fe86ead9847da9da65745d8d5de548c8346f3ba0afc4892 12175 preload.cjs abe5dd6fd68f2970cd19ef134094907c67219061d8fe9a1a08324c78de4ad437 484 PUBLISH-AND-ENABLE-UPDATE.cmd f018383f755352ca448e2ebb1e19b1dba412a3eb793d61e64b02953e300754fd 10538 Publish-ForgeFlow-Release.ps1 688fff7d2c989adb97ebb7fae38962656b70304a0aa5d27433c56adf7f136de0 4196 Publish-Missing-Binary-Release.ps1 -b1a5837a2087cc9a160f1e60c7cdfd750668314ddfdb42ce11a4368151e8d048 10313 README.md +0438b8de9bb3409543e4f3bc121bd3ccc094a5a879655db11aef1403c323f96e 10314 README.md 8f8dc5ff0c2967563457e4027d795f6f515253e0a459ecca5a9f5bcf0b658870 14650 reports/architecture-audit.json 5e923f0096895f588fe66b187779cdb7cb5af6644010c6dcc4d55876930ae376 1220 reports/architecture-audit.md 509c7bcff5280349bd9f45ed6151f70372bad7010a9ea582c13e2ccab91fe0cd 6272 scripts/acceptance.mjs @@ -126,7 +127,7 @@ b83d443f5724ac15393567f3a688aed8315fbe3e5966832c864a9466e0669464 8102 c76507857292c5713e1c699cf02e24b80265da39af2cecd148034bdb874adbb6 5246 scripts/test-authenticode-chain.ps1 4393f7dc5f417e6d601a68238f4e26791799a3634acec228fe4d79deaee85eb5 3109 scripts/validate-installed-connections.cjs e6127e1e62f39c70ddb1abf72f4d7e7b8e3f19ff1f219e1a3660353c2e0cdfac 2411 scripts/verify-release-signatures.mjs -b35a8701bba54338cce22c0afc3934f5c9747bcfedd85475ccde2c1b7352743d 20272 scripts/verify.mjs +2a80bfc448aed0a50597947afc75ae42c6830d5bcfd5c5f712957c85268b23d3 20660 scripts/verify.mjs 0b9f03ba3c67ff7cdb2916a902ad8ce25e81a7c90b210e4ae52d2ad029efabf3 2353 scripts/write-release-checksums.mjs 619515f524cb89960370ffcbd3fafd3c0e178b95f69c5868b1dd44777f23ec1e 2081 setup-windows.ps1 dd613d04b366f2cd071a1685a414016a5fb008082ed1b4cb8b24b79c100f640a 2412 src/main/audit-service.cjs @@ -155,11 +156,11 @@ fc7a5a2a42579a311f60a96bec4776942bf3f63428928820029b0dcf4874d1aa 2745 17e2a53f61cd7faba461b9f332967143087eaac95b72001462292976278ca305 7782 src/main/repository-service.cjs 52b6d88ed1f5c904a13cdde92e5f96d1e2b5971ceef49862152197353cdc6490 27928 src/main/server-inventory.cjs afef3841a3948b2121f8fba809aae4ea3da71bd2fda86973ba50200a5b1f89b2 14894 src/main/ssh-service.cjs -560084e3f10062f8f21b62f1cc50050350558e9505ab96ed235c6ab377da2546 25774 src/main/unraid-access-methods.cjs +d85780a19cac9ed5c89721a25916c35235e76012159363ff61924dba4215d171 25801 src/main/unraid-access-methods.cjs 2ede80cd1565a7f2c282cc58d35dc0889d58d7465346bc723026b9c8be4df0ac 9501 src/main/unraid-deploy-key-host.cjs 803a079499f7b8148495209dea43b505f6eb9bb587de19e82054e47183186c6e 30461 src/main/unraid-deployment-methods.cjs -2e63fdc0be0bf4d8e5c3d7d45ff5811d786b0821a08b82f511f1301696e12c9d 17157 src/main/unraid-deployment-service.cjs -0f95a26b7206603c4ea12d3ad4733b8d5730ca18a344ffad49e2a3ffe46a0699 37780 src/main/unraid-inventory-methods.cjs +b8a6ef72cfd5c5405e3f1a48848b5ef1575ca82ebbe8cad816dff0dc7335c8ec 17184 src/main/unraid-deployment-service.cjs +c3439857c985a44f8298f76aca46ad71f5fa30eabb32ddca9d18fc4f65dc4a38 38809 src/main/unraid-inventory-methods.cjs 2c0cf07921ca7ee5a9085ced44498c2e6798e5cc1e8a5ecf704c3cecabe39a25 27607 src/main/unraid-preflight-methods.cjs d45220176aed72d692f9ae5534f9d40bcc359a2d08e025e74a3b3b505b8b9ed4 16559 src/main/unraid-runtime-methods.cjs d4b3a07eeca687a49544a94ea574f7c6f7e0bc3aa9311b614d5244a468ca4a1b 11307 src/main/unraid-state-methods.cjs @@ -181,7 +182,7 @@ eef2f269ba4fbb76bf66ad328d481b461255d0acb753b30878dd4d4eaac57dc6 6924 a84da5aecbb16ce7983dba1f6d6aab1bf47b2e9a87c2933fa1afb8123f7ef7d6 1497 src/renderer/index.html 06180d9656dd254edfb6949c397f8e313954fc560ddcb22b3a35fce3c3e35655 21350 src/renderer/mock-bridge.js 5a2c0616a8a0ba23c182a8928abdf40a69612ce34c4cba47b1fd5f21fe5843d4 28617 src/renderer/mock-deployment-bridge.js -4ace18f1756da5f9ec3e4cf40a141cf3268e796b1fd9d788bd3ddab3652c7c04 20032 src/renderer/mock-repository-bridge.js +041daf59931aff37075d9ee238bdf9c397c4dd799580540f30216276f95cfd7a 20033 src/renderer/mock-repository-bridge.js 94fa265c2fe9ca8d644f0ce9b620b6f85d9b25dca5802c4e9195b66dcbe80120 6522 src/renderer/operations.js 1fc4c28859f7283aa2b771a50f1fd26addc8862b97a09250f806930ab4483f7a 79874 src/renderer/styles.css 4fbd3d7af3279a02c1da508758f6c7d9ca8de3d82f869be8439030ec81e8d941 101429 src/renderer/views.js @@ -198,7 +199,7 @@ f8853dce6fdf360d5df2fbe2b6df3e5687630c807fee5ba8436679b34ec737ea 2436 058aeaa5d9bfe377c7e322f213c7871ecc4151b5d08ef790992f4ee28d857658 743 START-FORGEFLOW-OVERLAY.ps1 f5b0ea887fcdeadec78c1ad49b0ec7979723562f5c0b730703acb77a37281ee0 1009 tests/acceptance.test.mjs a4e5947204ff6878e601e32477bc85b53cd0153baf95a161c8935b6e5466c257 1155 tests/audit-service.test.mjs -9346a48e2328fc617b2b94caa876a1d60f0098896538224c04b8b4e4c2c023a4 11566 tests/browser/forgeflow.spec.mjs +e8802b78b94af7245f7752e0a5f8697064aad2aae8bd002195ae052d69e59670 11619 tests/browser/forgeflow.spec.mjs 454edeaccb2bd41043bc918d3e3a6127db14339031d6a1c1562ac855e90455d2 4318 tests/clone-target.test.mjs ac17f8bbe9e388b80abef7792c8b184a1fd482c93f13d23a478e433961020f75 17214 tests/config-store.test.mjs f1463326aee79842d265687ae628189ce54e92544600f2bd14073780287cfb14 2502 tests/configuration-backup.test.mjs @@ -229,7 +230,7 @@ bab853feb0e22aa25af17989baaa632c01efa636533ea67407fecfdd973c7024 627 020eccfa9c4aef7a4ac4736d9af90518fcb6d1ad75aedcfaa1c92832a9e3d6d8 4609 tests/shell-verification.test.mjs 0d1bc4d623ce299337736c577ec61c8ffd6974ebe20335b72838d10eae35ecb1 7993 tests/ssh-service.test.mjs 8a6a8477eb94b85ccef18cddd2640afb0d1eafa679c96bc7de20428d5d69e1be 1794 tests/tool-invocation.test.mjs -1c5b40fb0a915ab0448bb9d2394f626c7b12142d91c23393a2d7537b3ea411ab 50853 tests/unraid-deployment.test.mjs +7b8e9118c91503c499e3194223f98ab3f689d6071e7f2316be2b700db592e06f 53633 tests/unraid-deployment.test.mjs bf95071f8d8bec6dcf2c889f1f9d83be1b56178dc0d2be84da555848b6319cbc 25650 tests/update-service.test.mjs 9cea5c1d5ba3e0972a0b5c7236cf1f7c5616373e0a39ea4a492ecebf70452e40 948 tests/validation.test.mjs 7ef4d4b9f5f3e6979293b29d571ce0e39f83197f3cade2d999a9cea7bacdd84d 1781 tests/zip-writer.test.mjs diff --git a/docs/RELEASE_NOTES_0.10.10.md b/docs/RELEASE_NOTES_0.10.10.md new file mode 100644 index 0000000..3daafbe --- /dev/null +++ b/docs/RELEASE_NOTES_0.10.10.md @@ -0,0 +1,10 @@ +# ForgeFlow 0.10.10 + +## Complete server-pull deployment repair + +- Missing repository-scoped read-only deploy keys can be provisioned and verified from Unraid against the exact Gitea branch. +- A repository deployment root can now remain above its Compose working directory without breaking workload recognition or being overwritten by inventory refresh. +- Nested Compose files are preserved as repository-relative deployment paths, including Ludarium, Launchpad and ITWorx MCP Hub layouts. +- The **Fix write access** action now receives its permission-report parser correctly instead of reporting a false write-access failure. +- Server-pull preflight proves every required deployment file at the exact Gitea commit before any container activation starts. +- Runtime secrets remain in server-side `.env` files and preserved appdata paths; no secret values are written to Git or diagnostic output. diff --git a/package-lock.json b/package-lock.json index 36a6d0d..9289a78 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "forgeflow", - "version": "0.10.9", + "version": "0.10.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "forgeflow", - "version": "0.10.9", + "version": "0.10.10", "dependencies": { "ssh2": "1.17.0" }, diff --git a/package.json b/package.json index fd5f1a8..2b8297f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "forgeflow", - "version": "0.10.9", + "version": "0.10.10", "private": true, "description": "Desktop release cockpit for local Git, Gitea Actions and controlled exact-commit deployments.", "main": "main.cjs", @@ -114,6 +114,7 @@ "docs/RELEASE_NOTES_0.10.7.md", "docs/RELEASE_NOTES_0.10.8.md", "docs/RELEASE_NOTES_0.10.9.md", + "docs/RELEASE_NOTES_0.10.10.md", "docs/CURRENT_STATE.md", "docs/MUTATION_MODEL.md", "docs/RELEASING.md", diff --git a/scripts/verify.mjs b/scripts/verify.mjs index 0c5d105..4ea9cb6 100644 --- a/scripts/verify.mjs +++ b/scripts/verify.mjs @@ -89,6 +89,7 @@ const required = [ "docs/RELEASE_NOTES_0.10.7.md", "docs/RELEASE_NOTES_0.10.8.md", "docs/RELEASE_NOTES_0.10.9.md", + "docs/RELEASE_NOTES_0.10.10.md", "docs/UPDATING.md", "docs/DIAGNOSTICS.md", "docs/DEPLOYMENT_SETUP.md", @@ -127,9 +128,9 @@ for (const file of required) await access(path.join(root, file)); const packageJson = JSON.parse( await readFile(path.join(root, "package.json"), "utf8"), ); -if (packageJson.version !== "0.10.9") +if (packageJson.version !== "0.10.10") throw new Error( - `Expected package version 0.10.9, got ${packageJson.version}.`, + `Expected package version 0.10.10, got ${packageJson.version}.`, ); const sourceManifest = await readFile( path.join(root, "SOURCE_MANIFEST.txt"), @@ -479,6 +480,10 @@ const release0109 = await readFile(path.join(root, "docs/RELEASE_NOTES_0.10.9.md for (const phrase of ["containers without healthchecks", "single-instance", "exact Gitea commit", "deploy-ready", "no containers are changed"]) { if (!release0109.includes(phrase)) throw new Error(`0.10.9 release notes are missing: ${phrase}`); } +const release01010 = await readFile(path.join(root, "docs/RELEASE_NOTES_0.10.10.md"), "utf8"); +for (const phrase of ["read-only deploy keys", "repository deployment root", "Compose working directory", "Fix write access", "exact Gitea commit"]) { + if (!release01010.includes(phrase)) throw new Error(`0.10.10 release notes are missing: ${phrase}`); +} const configSource = await readFile(path.join(root, "src/main/config-store.cjs"), "utf8"); for (const mode of ["server-git", "push-bundle", "monitor-only"]) { if (!configSource.includes(mode)) throw new Error(`Deployment configuration is missing mode: ${mode}`); diff --git a/src/main/unraid-access-methods.cjs b/src/main/unraid-access-methods.cjs index 2a742e9..7d56482 100644 --- a/src/main/unraid-access-methods.cjs +++ b/src/main/unraid-access-methods.cjs @@ -1,6 +1,6 @@ "use strict"; -function createUnraidAccessMethods({ shellQuote, path, bash, inventoryRemoteIdentity, checksSummary, crypto }) { +function createUnraidAccessMethods({ shellQuote, path, bash, inventoryRemoteIdentity, checksSummary, crypto, parsePermissionInspection }) { class UnraidAccessMethods { serverGitRemote(repository, profile) { const candidates = [repository.sshUrl, profile.cloneUrl, repository.preferredCloneUrl] diff --git a/src/main/unraid-deployment-service.cjs b/src/main/unraid-deployment-service.cjs index 113b9d8..11faa5e 100644 --- a/src/main/unraid-deployment-service.cjs +++ b/src/main/unraid-deployment-service.cjs @@ -490,7 +490,7 @@ for (const name of Object.getOwnPropertyNames(preflightMethods)) { if (name !== "constructor") Object.defineProperty(UnraidDeploymentService.prototype, name, Object.getOwnPropertyDescriptor(preflightMethods, name)); } -const accessMethods = createUnraidAccessMethods({ shellQuote, path, bash, inventoryRemoteIdentity, checksSummary, crypto }); +const accessMethods = createUnraidAccessMethods({ shellQuote, path, bash, inventoryRemoteIdentity, checksSummary, crypto, parsePermissionInspection }); for (const name of Object.getOwnPropertyNames(accessMethods)) { if (name !== "constructor") Object.defineProperty(UnraidDeploymentService.prototype, name, Object.getOwnPropertyDescriptor(accessMethods, name)); } diff --git a/src/main/unraid-inventory-methods.cjs b/src/main/unraid-inventory-methods.cjs index 39522a8..a2f54df 100644 --- a/src/main/unraid-inventory-methods.cjs +++ b/src/main/unraid-inventory-methods.cjs @@ -255,19 +255,38 @@ function createUnraidInventoryMethods({ } refreshedProfileFromWorkload(repository, server, workload, existingProfile) { + const configuredRoot = path.join(server.basePath, existingProfile.remoteFolder || "").replace(/\/+$/, ""); + const composeWorkingDir = String(workload.compose?.workingDir || "").replace(/\/+$/, ""); + const configuredRootOwnsCompose = Boolean( + configuredRoot + && composeWorkingDir + && (composeWorkingDir === configuredRoot || composeWorkingDir.startsWith(`${configuredRoot}/`)), + ); const detected = this.profileFromWorkload(repository, server, workload, { linkSource: existingProfile.workloadIdentity?.linkSource || "automatic-compose", deploymentMode: ["push-bundle", "server-git", "monitor-only"].includes(existingProfile.deploymentMode) ? existingProfile.deploymentMode : "push-bundle", - remoteFolder: workload.remoteFolderCandidate || existingProfile.remoteFolder, + remoteFolder: configuredRootOwnsCompose + ? existingProfile.remoteFolder + : workload.remoteFolderCandidate || existingProfile.remoteFolder, }); + const repositoryRelativeComposeFiles = configuredRootOwnsCompose + ? [...new Set((workload.compose?.configFiles || []).map((file) => { + const value = String(file || "").trim().replace(/\\/g, "/"); + if (value.startsWith(`${configuredRoot}/`)) return value.slice(configuredRoot.length + 1); + return value.startsWith("/") ? "" : value; + }).filter(Boolean))] + : []; + const composeFiles = repositoryRelativeComposeFiles.length + ? repositoryRelativeComposeFiles + : detected.composeFiles; return { ...existingProfile, deploymentMode: detected.deploymentMode, remoteFolder: detected.remoteFolder, - composeFile: detected.composeFile, - composeFiles: detected.composeFiles, + composeFile: composeFiles[0], + composeFiles, composeProject: detected.composeProject, composeWorkingDir: detected.composeWorkingDir, composeService: detected.composeService, diff --git a/src/renderer/mock-repository-bridge.js b/src/renderer/mock-repository-bridge.js index 6ae5501..3825cf4 100644 --- a/src/renderer/mock-repository-bridge.js +++ b/src/renderer/mock-repository-bridge.js @@ -5,7 +5,7 @@ function createMockRepositoryBridge(context) { await wait(80); snapshot(); return { - appVersion: "0.10.9-demo", + appVersion: "0.10.10-demo", platform: "win32", state: clone(state), git: { available: true, version: "git version 2.47.3" }, diff --git a/tests/browser/forgeflow.spec.mjs b/tests/browser/forgeflow.spec.mjs index 1a91dbb..92b6cb0 100644 --- a/tests/browser/forgeflow.spec.mjs +++ b/tests/browser/forgeflow.spec.mjs @@ -74,8 +74,11 @@ async function assertScrollableWhenOverflowing(page, selector) { return metrics.connected && metrics.clientHeight > 0; }).toBe(true); if (metrics.scrollHeight > metrics.clientHeight + 1) { - await target.evaluate((element) => { element.scrollTop = element.scrollHeight; }); - await expect.poll(() => target.evaluate((element) => element.scrollTop)).toBeGreaterThan(0); + await expect.poll(() => target.evaluate((element) => { + if (element.scrollHeight <= element.clientHeight + 1) return 1; + element.scrollTop = element.scrollHeight; + return element.scrollTop; + })).toBeGreaterThan(0); } } test("shell, overview, repositories and settings remain responsive and accessible", async ({ page }, testInfo) => { diff --git a/tests/unraid-deployment.test.mjs b/tests/unraid-deployment.test.mjs index 9aca03d..93932db 100644 --- a/tests/unraid-deployment.test.mjs +++ b/tests/unraid-deployment.test.mjs @@ -40,6 +40,36 @@ test("server release directories resolve to the stable deployment root", () => { assert.equal(deploymentRootCandidate("ludarium/source/deploy"), "ludarium/source/deploy"); }); +test("inventory refresh preserves a repository deployment root above its Compose working directory", () => { + const service = new UnraidDeploymentService({}); + const existing = { + id: "profile", provider: "ssh-unraid", serverId: "server", environment: "production", + branch: "main", deploymentMode: "server-git", remoteFolder: "App/source", + composeWorkingDir: "/mnt/user/appdata/App/source/ops", composeProject: "app", + composeFiles: ["ops/compose.yml"], composeServices: ["web"], containerName: "app-web", + workloadIdentity: { linkSource: "automatic-compose" }, preservePaths: [".env"], + }; + const workload = { + workloadId: "workload", kind: "compose-project", displayName: "app", + remoteFolderCandidate: "App/source/ops", selector: { kind: "compose-project", composeProject: "app" }, + compose: { + project: "app", workingDir: "/mnt/user/appdata/App/source/ops", + configFiles: ["/mnt/user/appdata/App/source/ops/compose.yml"], services: ["web"], + }, + containers: [{ name: "app-web", running: true, service: "web", mounts: [], ports: [] }], + metadata: {}, dockerMan: null, + }; + const refreshed = service.refreshedProfileFromWorkload( + { fullName: "Owner/App", name: "App", defaultBranch: "main", sshUrl: "git@gitea.test:Owner/App.git" }, + { id: "server", name: "Server", basePath: "/mnt/user/appdata" }, + workload, + existing, + ); + assert.equal(refreshed.remoteFolder, "App/source"); + assert.equal(refreshed.composeWorkingDir, "/mnt/user/appdata/App/source/ops"); + assert.deepEqual(refreshed.composeFiles, ["ops/compose.yml"]); +}); + test("server inspection key-value payload is decoded safely", () => { const b64 = (value) => Buffer.from(value).toString("base64"); const parsed = parseInspection( @@ -86,6 +116,22 @@ test("server pull provisions a pinned repository-scoped key and records access m assert.equal(result.remoteSha, "a".repeat(40)); }); +test("write-access inspection parses the remote permission report through the access module", async () => { + const encoded = (value) => Buffer.from(value).toString("base64"); + const profile = { id: "profile", provider: "ssh-unraid", serverId: "server", remoteFolder: "App", composeFiles: ["compose.yml"] }; + const service = new UnraidDeploymentService({ + store: { + getDeploymentProfile: () => profile, + getServer: () => ({ id: "server", basePath: "/mnt/user/appdata" }), + }, + ssh: { exec: async () => ({ stdout: `__FORGEFLOW_PERMISSIONS__\nI\t${encoded("deploy")}\t1000\t1000\t${encoded("users")}\tfalse\tfalse\nP\t${encoded("project-root")}\t${encoded("Project folder")}\t${encoded("/mnt/user/appdata/App")}\tdirectory\ttrue\ttrue\ttrue\ttrue\ttrue\ttrue\t${encoded("deploy")}\t${encoded("users")}\t2775\t${encoded("/mnt/user/appdata/App")}\t${encoded("Read/write probe passed.")}\n` }) }, + }); + const report = await service.inspectWriteAccess({ repository: { fullName: "Owner/App" }, profileId: "profile" }); + assert.equal(report.ready, true); + assert.equal(report.identity.user, "deploy"); + assert.equal(report.targets[0].effectiveWritable, true); +}); + test("server pull verification proves a repository-scoped read-only key and exact commit parity", async () => { const sha = "c".repeat(40); const profile = {