Repository navigation
feat(git): git stacks in the web UI (#138, part 3) - #178
Conversation
The Credentials tab now also holds git repository tokens and git stacks' deploy keys, but its heading, its empty-table text and the save and delete log lines still said "registry credential". Deleting a deploy key logged "Deleted registry credential", which is the line someone would search the syslog for. Say "credential" in those four places. The save log line already names the provider. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The test's name said the secret goes to the callback only, but it checks the opposite: whatever the callback returns, the secret included, goes back to the caller, as useCredential's comment says. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Testing a git credential asked the repository with the 60-second limit
of a check or deploy, so a host that does not answer held the browser's
request for a minute. The registry test waits at most 15 seconds; the
git test now does too.
The failure message also said "Could not reach" twice ("Could not reach
<url> (used by 'x'): Could not reach the repository: ..."). It now names
the repository and the stack once, then gives the reason.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ry's An ssh stack's deploy key is found again by the stack's name and host, so that a second attempt at adding the stack uses the key the user has just added to the repository. A key left behind by a deleted stack of the same name matches too, even when the new stack is for another repository, and GitHub refuses a deploy key that another repository already has. The vault does not record the repository, so rather than add a field to it, the message now names the reused key and says to delete it first when it belongs to another repository. The docs say the same next to the deploy key steps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a host gave no keys, the message asked "Is the address right?" whatever the reason, though a DNS failure or an unreachable host on this server looks the same to the user. It now gives ssh-keyscan's own error when it prints one (a name that does not resolve, for one; a refused or unreachable port prints nothing), or says that no key came back, and asks to check both the address and that this server can reach the host. The first test that runs ssh-keyscan checks the message against a port nothing listens on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts it
compose-git deploy starts compose.sh with the checks' environment, in
the compose file's folder, so up deploys what the checks saw. The web
UI's deploy starts compose.sh too, from the web server's folder and
environment, so ${PWD} in a compose file (or a variable that environment
happens to carry, such as DOCKER_HOST) could still differ between the
checks and up.
compose.sh gitdeploy now does it itself, whatever starts it: it runs
docker in the compose file's folder (git_stack.php compose-folder, the
folder the checks use), with PATH, HOME, LC_ALL=C and PWD set to that
folder, plus DOCKER_CONFIG (the registry credential) and TERM when set.
That covers every docker call of the deploy: docker compose, the docker
network calls of the missing-network step (create_missing_external_networks
in common.sh takes the docker command to use) and the image cleanup.
Nothing else of compose.sh's environment reaches docker. PATH is
compose.sh's own, which is the fixed one for compose-git and the
system's for the web UI.
The Bats docker stand-in now reads its settings from a file, since the
environment no longer reaches it, and records the environment of up,
network create and rmi.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
For a git stack the Sources tab shows the repository, branch, compose file, clone, deployed and checked-out commits, any failed commit, files changed in the clone, and an ssh stack's deploy key, in place of the Compose Source choice. Saving the editor's settings no longer touches a git stack's indirect files: its compose file is the one in its clone, set by git.json, and a form that sent no compose source would have turned it back into a project-folder stack. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add Stack gets a fourth compose source, "Git repository": the repository address, branch, the compose file's path in the repository, and for an https repository a git token from the Credentials tab. Creating the stack clones the repository through the same code as compose-git add, and deploys nothing. It then goes back to the stack list, saying the stack was added (with an ssh stack's host key fingerprints), rather than opening the editor: a git stack's compose file is changed in its repository. The dialog stays open while it clones, and shows any error in place. An ssh repository gets a deploy key of its own. When the server refuses the clone, which usually means it does not know the key yet, the dialog shows the key in a panel of its own, with a Copy key button, and the clone's error under it. For that, GitClone::create() throws GitCloneFailedException when git clone itself fails, keeping everything git said: only the lines before the "fatal:" line tell a refused key from an unreachable host. The other failures (the folder already exists, the compose file is not on the branch) are shown as the error they are. The latter says no clone was made. A refusal worded in a way not recognised still gets the key, inside the error as before. GitStackManager throws GitDeployKeyNotAddedException for the panel, so the web answer carries the key apart from the error; compose-git prints the same message as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
compose-git deploy's command line for compose.sh gitdeploy moves into buildGitDeployCommand() in Helpers.php, and its choice of profiles (the ones asked for, else the running ones, else the defaults) into gitDeployProfiles() beside it, so the web UI's deploy (next commit) runs exactly the same command. No change in behaviour. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A git stack's menu gets "Pull and Redeploy" (the branch's latest commit)
and "Deploy Commit..." (a commit given by its full id, to go back to an
older version). Both run compose.sh gitdeploy in the usual terminal
window, with the same command as compose-git deploy, and wait for healthy
containers when the stack's setting says so.
Before deploying, the menu lists any files changed in the clone, which
would stop the deploy, and offers to save them as a patch in the stack
folder's git-changes folder and discard them. A problem found in the
clone without asking the remote (the clone missing, say) would stop the
deploy, so the menu shows it instead and offers no Deploy.
A compose action that is refused before it starts with {error, message}
(other than the identity chooser's) is now shown as an error, instead of
being opened as a terminal URL.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Compose tab of a git stack shows a banner: the file is in the plugin's clone, so a change belongs in the repository followed by Pull and Redeploy. An edit saved here is a local change, which the next deploy stops on, and changes that must survive deploys belong in the Labels tab (the plugin's override) or the .env tab. Git stacks stay editable: a broken deploy can always be fixed in place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… list
Under the project name, a git stack shows its branch and the short id of
the commit it has deployed ("main @ 23cf507", or "not deployed"), and
"failed" when its last deploy failed. It is read from the stack's own
files, with no git command, so the list stays as fast as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Check for Changes" asks the repository for the branch's latest commit, as compose-git check does, and gives one of three answers: - up to date; - a newer commit changes the stack's folder (the folder holding its compose file in the repository): Pull and Redeploy is offered; - a newer commit changes only files outside that folder: in a repository that holds several stacks, usually a commit to another stack. The dialog says so, and why a stack can still need it (a bind mount or build context of ../something, an env_file, extends or include in another folder), and offers Deploy anyway. Until now any commit on the branch read as "a newer commit is waiting", so in a shared repository every stack looked behind after a commit to one of them. The folder test is the one deploys already use to decide whether to recreate containers (GitClone::stackFilesChanged()). When a newer commit's changes cannot be identified, the answer is the second one, and the dialog says why: nothing is deployed yet, or the deployed commit is no longer in the clone (deployedCommitMissing; it was made again after a force-push, say). To compare, a newer commit is fetched into the clone under the stack's lock, as a deploy would fetch it; no container and no checked-out file changes. A check while another operation on the stack is running says so instead of waiting. compose-git check gives the same answers: exit status 3 now means a newer commit changes the stack's folder (or its changes cannot be identified), and the new 4 means one changes only files outside it. With --all, 3 wins, then 1 (a stack could not be checked), then 4. A script that should deploy whenever the branch moves treats 4 like 3; the help says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Sources tab of a stack that is not a git stack gets "Move this stack into git...": the repository address, branch, compose file path and credential, as in Add Stack. It runs the same code as compose-git convert: the stack keeps its name, .env and the plugin's override, its old compose file is moved into a dated pre-git folder in the stack folder, and its containers change only at the next deploy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Delete removes only the stack folder, as for any stack. For a git stack it now names the clone left on disk (instead of the compose file inside it), and that an ssh stack's deploy key stays on the Credentials tab. The clone is neither deleted nor moved: a container may keep data in it, and once git.json is gone the plugin cannot prove the clone is its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The git stacks guide now covers the web UI next to the command line: the quick start and the move-into-git steps say where each step is on the Compose page, a new "In the web UI" section maps each screen to its compose-git command, and "No web UI yet" leaves the limitations. A new section, "The stack's folder, and files outside it", explains the three answers of Check for Changes and compose-git check, the new exit status 4 (a newer commit changes only files outside the stack's folder), and what to do when a stack uses files from elsewhere in the repository. The command reference adds exit status 4 and that check, like convert and reclone, refuses rather than waits while another operation on the stack is running. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A mutation test, off unless E2E_ENABLE_MUTATION_TESTS=1 and a test repository is given (E2E_GIT_TEST_REPO, E2E_GIT_TEST_COMPOSE_PATH). It adds a GUID-named git stack in the Add Stack dialog, checks the editor's banner and Sources tab, the stack list line and the stack menu's git items, deploys it in the background, checks it is up to date, and deletes it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e the compose file The Sources tab listed a hand-made commit and changed files as one line of orange text, easy to miss although the next deploy stops on them. They now go in an orange-edged box like the deploy key panel, with a warning icon, the files, and what to do: Pull and Redeploy can save them as a patch and discard them, and a change to keep belongs in the repository. The same box is shown on the Compose tab above the file, where an edit in the clone is most likely made or noticed. Saving a file in the editor reads the clone again, so the box shows as soon as the edit is saved. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Check-for-changes can return stale deployment state when a concurrent deployment completes during its remote query.
5 open findings
What changed in this PR
Adds web UI support for Git-backed stacks, completing part 3 of #138 by exposing existing Git stack operations through Compose Manager.
Changes:
- Adds create, convert, inspect, check, deploy, and delete flows for Git stacks.
- Aligns web and CLI deployment behavior, environment handling, and change detection.
- Expands unit, Bats, Playwright, and documentation coverage.
| File | Description |
|---|---|
tests/unit/GitStackWebActionsTest.php |
Tests web Git-stack actions. |
tests/unit/GitStackManagerTest.php |
Tests checks, deploy commands, and exit codes. |
tests/unit/GitSshTest.php |
Tests SSH host-key errors. |
tests/unit/GitDeployTest.php |
Tests Compose working-directory resolution. |
tests/unit/gitdeploy.bats |
Tests isolated deploy environments. |
tests/unit/GitCloneTest.php |
Tests clone-failure classification. |
tests/unit/CredentialVaultTest.php |
Clarifies credential callback coverage. |
tests/e2e/tests/compose-git-stacks.spec.ts |
Adds Git-stack UI lifecycle coverage. |
tests/e2e/README.md |
Documents Git E2E configuration. |
tests/bootstrap.php |
Extends test logging and constants. |
source/compose.manager/sheets/EditorModal.css |
Styles Git source and warning panels. |
source/compose.manager/scripts/git_stack.php |
Exposes the Compose working directory. |
source/compose.manager/scripts/compose.sh |
Isolates Git deploy execution. |
source/compose.manager/scripts/compose_git.php |
Refines checks, statuses, and deployment. |
source/compose.manager/scripts/common.sh |
Supports configurable Docker invocation. |
source/compose.manager/javascript/credentialManager.js |
Updates Git credential guidance. |
source/compose.manager/javascript/composeManagerMain.js |
Implements Git-stack UI workflows. |
source/compose.manager/include/Helpers.php |
Builds and launches Git deploy commands. |
source/compose.manager/include/GitStackWebActions.php |
Adds the web action service. |
source/compose.manager/include/GitStackManager.php |
Adds richer checks and web support. |
source/compose.manager/include/GitSsh.php |
Improves host-key diagnostics. |
source/compose.manager/include/GitDeploy.php |
Exposes deploy working-directory logic. |
source/compose.manager/include/GitClone.php |
Classifies clone failures and local commits. |
source/compose.manager/include/Exec.php |
Registers Git-stack AJAX actions. |
source/compose.manager/include/ComposeUtil.php |
Routes Git deployment requests. |
source/compose.manager/include/ComposeManager.php |
Adds editor Git-source controls. |
source/compose.manager/include/ComposeList.php |
Shows Git branch and deployment state. |
source/compose.manager/compose.manager.settings.page |
Generalizes the credentials heading. |
docs/git-stacks.md |
Documents web workflows and check semantics. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…hanges Check for Changes read what was deployed before asking the remote, and took the stack's lock only for the fetch. A deploy or rollback that finished while the remote was being asked was not counted: the check could report the old commit as deployed and offer a redeploy that was no longer needed. The remote is still asked before the lock is taken, so a slow remote does not hold up a deploy. The deployed commit is then read under the lock, and the answer is built from that. A check is now refused while another operation on the stack runs, also when nothing needs fetching, because that operation may be about to change the deployed commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The two read-only boxes that show a stack's deploy key had no label, so a screen reader announced an unlabelled text box. The Sources tab's box is now labelled by the help text above it, and the box in the Add Stack and Move into git panels gets an aria-label. The other fields these dialogs add already have a label. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GitClone::isCommitId() accepts a 40-character SHA-1 id and a 64-character SHA-256 id, but the messages that ask for a commit id said it has to be 40 characters. That was wrong for a SHA-256 repository. They now name both lengths: the Deploy a commit prompt and its error, the web request's error, and compose-git deploy's --commit help. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks, all five fixed as new commits on top:
|
Every ssh stack got a deploy key of its own. A repository that holds
several stacks, one folder each, then needed one key per stack in its
settings, each with exactly the same read-only access. GitHub refuses
one key on two repositories, but not one key for several stacks of the
same repository.
A new ssh stack now uses, in this order:
- the key of another ssh stack of the same repository (the same user,
host, port and path), which the repository already knows, so no key
needs adding at all;
- a key made for the repository earlier, as by a first attempt whose
clone failed because the key had not been added yet;
- a key named after the stack by an older version of the plugin, as
before, but only while no git stack uses it: with keys shared, a
stack of another repository may still use it, and a key in use
cannot be deleted to make way;
- a new key, now named after the repository ("deploy key for
git@github.com:you/stacks.git"), so later stacks of the repository
find it.
The other stacks' git.json files are read as plain JSON, as the
Credentials tab reads them, so a stack whose settings fail a check
still counts; one that cannot be read at all is named in a message.
Keys made before keep their names and their stacks. The Credentials
tab already lists every stack that uses a key, and refuses to delete
one in use. The help, the docs, the Add Stack and Move into git
dialogs, the Sources tab and the delete message say a key belongs to
the repository.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stacks added before a repository's stacks shared one deploy key each have a key of their own, and an ssh stack's key could not be changed. compose-git credential <stack> <name> now takes, for an ssh stack, a deploy key of the same repository: one that another stack of the repository uses, or the one made for the repository. The repository is reached with that key first, and nothing is saved unless that works. The old key stays on the Credentials tab, and the command says to delete it there and from the repository once no stack uses it. Naming the key the stack already uses changes nothing, and says so. Anything else is still refused for an ssh stack, now naming the keys it could be given: no key at all, an HTTPS token, another repository's key, or a key no stack of the repository uses. The move is on the command line only, as changing a credential already was. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Two more commits on top, for something I ran into while setting up the Add Stack flow on my own server. My stacks live in one repository, one folder per stack. Over ssh, every stack got a deploy key of its own, so
Tested with the unit suite and over real ssh against a local Forgejo on an Unraid VM. I can split these into a |


The third of four PRs for git-backed stacks (#138). #171 and #174 added git stacks and private repositories
behind the
compose-gitcommand; this one puts them on the Compose page. Every screen calls the same code as thematching
compose-gitcommand, so the web UI and the command line behave the same.What it adds
to the stack list; nothing is started. An ssh repository gets its own deploy key, and the dialog shows it when
the repository refuses the clone.
compose.sh gitdeployin the usual terminal window, asking for profiles as Update does. Check for Changes sayswhether a newer commit changes the stack's folder or only files outside it (in a shared repository, usually a
commit to another stack). A clone with a problem (missing, unreadable) is not deployed, and the dialog says why.
Source choice, and other stacks get Move this stack into git.... The Compose tab says the file is in the
clone. Changes made in the clone are shown in a box on both tabs, since the next deploy stops on them.
Changes to existing behaviour
indirectfiles, which could have turned it backinto a project-folder stack.
performComposeAction()shows a refusal that comes back as{error, message}instead of opening it as aterminal URL; this also fixes
echoComposeCommand()'s existingwait_conflictanswer.compose.sh gitdeployruns docker in the compose file's folder with only the environment the deploy's checksused, whatever starts it: the web UI starts it from the web server, whose folder and environment were never
checked.
compose-git checkexits 4 for a newer commit that changes only files outside the stack's folder; 3 now meansone changes the folder (or nothing is deployed yet).
Reading it: in order, each commit one step with its tests. 1-5 are small follow-ups to #174; 6 is the
gitdeployenvironment; 7-15 add the Sources tab, Add Stack, a refactor with no change in behaviour, the deploymenu items, the Compose tab banner, the stack list line, Check for Changes, Move into git and Delete; 16 is the
docs, 17 a Playwright spec, 18 the changed-in-the-clone box. The server side is one new class,
GitStackWebActions, plusechoGitDeployCommand()next toechoComposeCommand().Testing: 44 new PHPUnit tests and 3 Bats tests; the full suite passes at every commit and PHPStan reports
nothing. The new Playwright spec (off unless mutation tests are enabled and a test repository is given) adds,
deploys, checks and deletes a git stack through the UI, and passes on Unraid 7.3.2 with the existing specs. By
hand on the same server: every state the UI shows (behind, a commit to another stack, changes in the clone, a
hand-made commit, a missing clone, a refused deploy key, a failed deploy, a lost deployed commit), converting a
running stack, and deleting a git stack.