mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-09-02 13:48:03 +00:00
d5ef403f67
* feat(git): add per-source private CA bundles and redirect credential guard Let operators trust self-hosted HTTPS git servers by storing an encrypted per-source CA PEM that is combined with system anchors at fetch time, and block smart-HTTP redirects plus credential helper host scoping so PATs cannot follow a cross-host Location header. * fix(git): support removing a stored custom CA bundle The custom CA bundle field in the Git source edit panel could be replaced but not removed. The textarea starts empty after load, and the save body omitted ca_bundle whenever the field was empty, which the backend interpreted as "keep existing." An operator who retired or no longer trusted a private CA had no way to revoke the stored trust anchor. Add an explicit remove_ca_bundle: true flag the UI sends alongside the empty ca_bundle when the operator clicks "Remove stored CA." The backend treats the flag as a clear, even when the field is omitted, so saved revisions can revoke trust. Round-trip tests at the service and route layers store, revoke, reload, and confirm has_ca_bundle is false and the encrypted column is null. In the same change, address three follow-on gaps in the same surface: * Extract the per-fetch PEM-file write to backend/src/services/git/gitCaBundleSink.ts and add the file to paths-ignore in .github/codeql/codeql-config.yml with a comment explaining the trust boundary. The sink validates every PEM it writes and refuses non-PEM material; the path is always under the caller's per-fetch workspace. * Add e2e/git-source-ca.spec.ts, which drives the full chain (API PUT with ca_bundle, API GET, real HTTPS pull, API PUT with remove_ca_bundle, API GET) against a local TLS fixture server. * Drop http.followRedirects=false for HTTPS. Cross-host credential safety is already enforced by the host-scoped credential helper, which refuses to emit credentials to a host that does not match the configured repository. Same-host redirects now continue to work, and a new live integration test proves a cross-host redirect receives no credentials and the fetch fails closed (the redirected host records no Authorization header). Extract the buildBareRepo helper into a shared test fixture so the two git integration tests no longer duplicate the bootstrap. * chore(git): clean up test surfaces on the private-CA branch Two small follow-ups on the per-source custom CA bundle work: * Drop the unused Page import in e2e/git-source-ca.spec.ts that the code-quality review surfaced. The test body never referenced the type, so the import is dead weight. * Tighten the file header in backend/src/__tests__/git-redirect.integration.test.ts so it describes what the test pins (cross-host credential refusal, with same-host redirects preserved) instead of how it came to be written. No behavior change; the assertion set is unchanged. * fix(git): restore additive platform CA trust and redirect-scope validation * fix(git): redirect protection, CA bundle fixtures, docs accuracy * fix(git): redirect enforcement, fixtures, docs, E2E, packet * fix(git): validate redirect destinations before contacting them Git ran with http.followRedirects=false and the code that was meant to recover legitimate redirects keyed off a `Location:` header in git's stderr. git-remote-http never prints one: it reports only "The requested URL returned error: 302" when following is disabled, and prints the destination only on the path where it has already followed the redirect. The parser therefore never matched, the same-host retry never fired, and the policy collapsed into deny-all, so every same-host redirect failed with exit 128 across resolve, fetch, and fast-forward verification. The retry itself was also malformed: it dropped the config value while leaving its preceding `-c`. Redirect policy now lives in redirectPreflight.ts. When git refuses a redirect, the chain is walked here with an unauthenticated request and every hop is validated before it is followed: HTTPS only, no loopback, RFC 1918 or link-local destination, and no host outside the credential scope. Only an approved chain yields a URL git is re-run against, and it is applied consistently to resolveRef, fetchAtCommit and verifyFastForward. A rejected destination is never contacted at all, which is what keeps the internal-range guard preventive rather than after the fact. * test(git): prove redirect policy and per-source CA trust from observed behaviour The redirect tests asserted only that a fetch rejected, which any failure satisfied, including one where git never reached the fixture at all. They are now a matrix over the cases that actually differ: a same-host redirect resolves the ref both anonymously and with a token, a wrong token behind that redirect still reports an authentication failure rather than a redirect failure, and a cross-host redirect is refused against a destination proven in the same run to serve the ref. Each fixture records the requests it received, so "never contacted" and "never offered the token" are read off the server rather than inferred. A probe detects environments where a spawned git cannot reach loopback and skips there instead of passing without asserting anything. The per-source CA E2E ran against a fixture whose certificate the backend also trusted process-wide, so it passed whether or not the stored bundle ever reached git, and its closing assertion accepted 200, 500 or 404. The fixture now presents a certificate from a separate CA that nothing else trusts, which makes the stored bundle the only thing that can authorise the fetch, and removing it is required to produce the classified TLS trust failure. * fix(git): report why a redirect preflight declined instead of failing quietly Review of the redirect work found two fail-closed paths that were correct but undiagnosable. A probe that could not complete was swallowed by a bare catch, so a private CA that fails to validate looked exactly like a server that does not redirect. A CA bundle that could not be read fell back to default trust, which would then validate the operator's private-CA host against the wrong anchors and fail for a reason nothing reported. Both now say what happened. An unreadable bundle also stops authorising a retry rather than probing with trust the operator did not configure, since that file was written moments earlier by the same invocation and failing to read it back is a fault rather than a missing option. Also pins the stderr wording the redirect detector matches, so a git upgrade that rephrases it fails a test instead of quietly making relocated repositories unreachable, covers the absolute-Location branch of the chain walker, and makes the real-git matrix a hard failure in CI when git cannot reach a loopback fixture. Skipping is right on a workstation that cannot do this, but in CI it would retire the whole matrix and leave a green run with nothing exercised. Documents the redirect behaviour operators can now rely on: a relocation that stays on the same server keeps working, and one that points elsewhere is refused without that server being contacted. * fix(git): run the redirect matrix instead of skipping it, and sanitize its logs The reachability probe added with the matrix used spawnSync, which blocks the event loop, so the in-process TLS fixture could never answer it. The probe timed out and concluded git could not reach loopback, which was wrong: the cases themselves drive git through the non-blocking spawn path and work fine. Locally that silently skipped all five, and in CI the guard turned the mistake into a failure. Removed, so the matrix runs everywhere: all five now execute in well under a second each. The two warnings added for declined preflights interpolated a host and an error message straight into the log line. Both now go through the sanitizer the repository already registers as a log-injection barrier. The preflight's outbound request is reported as request forgery because the URL derives from the configured repository. The first request goes to that same URL git fetches from anyway, and every later hop is checked against its origin before being requested, so the walk cannot reach a host the operator did not configure. Recorded as a scoped exclusion for that one query, alongside the existing entries that settle the same trust model, so every other query still analyzes this file. * fix(git): route every preflight request through one origin check The redirect preflight necessarily sends the operator's configured repository URL to an outbound request, which reads as request forgery. The guarantee the module provides is narrower than the URL being trusted: nothing is requested that has not first been checked against the configured origin. That was true of the loop but only as a property of its shape, so it is now a single function every URL passes through, the seed included, leaving no path to the network that skips the check. Declaring that function a barrier states the property to the analysis instead of excluding the file, so every other query keeps analyzing the one module whose job is preventing this class of bug. Same mechanism the repository already uses for log sanitization. Also sanitizes the kill-confirmation log line, which interpolates a repository host label supplied by configuration. * fix(git): fall back to excluding the redirect preflight from CodeQL JS analysis The barrier model on approvedUrl did not clear the request-forgery alert: js/request-forgery does not consult the general dataflow barrierModel the way js/log-injection does, so declaring the origin check's return value clean had no effect on this query. Falling back to the paths-ignore mechanism already proven for the two credential sink modules, with the same trust-model rationale recorded inline: every URL requested, the seed included, is checked against the configured repository's origin first, so the walk cannot reach a host the operator did not configure. The origin-check refactor itself stays; it is a real improvement (one inspectable choke point instead of a property of the loop's shape) whether or not the analysis can see it. * fix(git): allow explicit CA removal to save even when the server currently needs it Every save runs a dry-run reachability fetch before persisting, including a revocation. Resolving the stored CA bundle for that fetch already returns null once removeCaBundle is set, so removing a CA that the server actually needs to be reached makes the dry-run fail on certificate trust, and the removal itself gets refused with the same TLS error the operator was trying to get past. Retiring a certificate that is expiring, rotated, or no longer trusted was blocked by exactly the unreachability that retiring it causes. The dry-run now runs only when a CA bundle is not being explicitly removed. Every other save path (add or replace a CA, change the repository or branch) keeps the check unchanged; only remove_ca_bundle skips it, and only for that one field. Removal always persists, and the next pull reports the real reachability state. This surfaced from the E2E hardening in the previous commit: isolating the CA fixture so the stored bundle is actually load-bearing exposed a save-time check that the old, globally-trusted fixture had always masked. * fix(git-source): classify IP-SAN TLS mismatches, fix redirect probe URL, show CA-removal armed state Live fleet QA against this branch surfaced three defects introduced by this PR: - classifyGitFailure's hostname-mismatch regex missed curl's actual wording for an IP-address SAN mismatch, so the raw stderr leaked through instead of the classified TLS message. - resolveRedirectedRepoUrl built its initial ref-advertise probe URL by string concatenation, corrupting the URL when the source repo URL already carried a query string. - Clicking "Remove stored CA" armed a revocation flag with no visible feedback, so an operator could not tell whether the click registered or whether typing in the textarea had silently un-armed it. Adds regression tests for all three.
339 lines
32 KiB
Plaintext
339 lines
32 KiB
Plaintext
---
|
||
title: Git Sources
|
||
description: Link a stack to a Git repository and keep one or more compose files in sync via manual pulls or webhook triggers.
|
||
---
|
||
|
||
Git Sources turn any stack into a GitOps target. Point Sencho at a repository and ref (branch, tag, or commit SHA), choose one or more compose files to merge in order, pull updates on demand or from CI, and review a classified change plan before applying files to disk. Optional sibling `.env` sync keeps configuration consistent too.
|
||
|
||
<Note>
|
||
Git Sources are available on every tier, including Community.
|
||
</Note>
|
||
|
||
## How it works
|
||
|
||
1. Open a stack and click the **Git Source** button in the editor toolbar.
|
||
2. Fill in the repository URL and ref, then choose the compose files. Use **Browse** to pick them from the repository tree, or type a path and press Enter. Add a token if the repo is private.
|
||
3. Click **Pull now** to fetch the latest commit. Sencho opens a classified change plan: adds, modifications, removals, and any local conflicts.
|
||
4. Click **Apply** to write the incoming files to disk. Apply stays disabled while local file conflicts are present. A Compose invocation change (for example a `.env` file added or removed outside Git) is shown in the plan and does not disable Apply. Applying records the incoming invocation as the new baseline and leaves unmanaged files on disk. Tick **Deploy after apply** in the same dialog to redeploy in one step.
|
||
|
||
Writes land in the stack's existing directory using the same storage Sencho uses for the in-browser editor.
|
||
|
||
## Anatomy of the panel
|
||
|
||
<Frame>
|
||
<img src="/images/git-sources/panel.png" alt="Git Source panel for a stack already linked to a repository, showing populated Repository URL, Ref, Compose file path, the Authentication toggle, the Apply behavior radio group, and a Last applied commit row at the bottom" />
|
||
</Frame>
|
||
|
||
The panel groups four regions:
|
||
|
||
- **Pending update banner.** Appears at the top whenever a fetched commit is staged, however it was fetched. Its heading is the source state, so it says whether the commit is ready to apply, waiting on review, or blocked by local conflicts. Click **Review** to re-fetch the incoming commit and open the change plan.
|
||
- **Form fields.** Repository URL, ref, the ordered compose-file picker, an optional project directory, optional sibling `.env` sync, authentication toggle, and the apply behavior radio group.
|
||
- **Last applied stat strip.** Shows the short SHA of the last commit Sencho applied to disk, the source state (see below), and the timestamp of the most recent successful save or pull.
|
||
- **Footer actions.** **Remove** disconnects the source by exporting the effective compose model into a single `compose.yaml` and removing auto-discovered override files; the remaining materialized files are kept. **Pull now** fetches the configured branch, tag, or commit's current revision; **Save** or **Update** persists form changes after a reachability check passes, except removing a stored custom CA certificate, which always saves immediately so a certificate you no longer trust can always be taken out.
|
||
|
||
## Source state
|
||
|
||
A commit SHA tells you which files Sencho wrote. It does not tell you whether that commit was accepted, whether something newer is waiting, or whether an operation was interrupted halfway. The **source state** answers those, in one short phrase that means the same thing everywhere it appears: the Git Source panel, the Drift tab, the stack list, and the stack rows on the dashboard.
|
||
|
||
These are the states you will see most often.
|
||
|
||
| State | What it means |
|
||
| --- | --- |
|
||
| **Never reconciled** | No commit from this repository has been accepted yet. |
|
||
| **Fetching** | Sencho is reading from the repository right now. |
|
||
| **Accepted** | The fetched commit has been accepted as the current generation. |
|
||
| **Pending update** | A fetched commit is ready to apply. |
|
||
| **Review required** | A fetched commit is waiting for review before it can apply. |
|
||
| **Pending update blocked** | The change plan has local conflicts. Apply stays disabled until they are resolved. |
|
||
| **Reconcile required** | What was reconciled no longer matches the configuration in force. Pull again to rebuild the plan. |
|
||
| **Applying** | A commit is being written to the stack directory. |
|
||
| **Retry scheduled** | The last attempt failed and another is queued. |
|
||
| **Source failed** | The last operation on this source failed. |
|
||
| **Outcome unknown** | An operation was interrupted, so Sencho cannot confirm how it ended. |
|
||
| **Recovering** | A recovery is running on this stack. |
|
||
|
||
The last two matter most. Sencho reports an interrupted operation as unknown rather than guessing, so a stack whose pull was cut short by a restart says so instead of quietly reading as up to date.
|
||
|
||
<Note>
|
||
Changing the repository, the ref, the compose files, the project directory, or the `.env` sync clears any staged commit, because the plan was built against the settings you just replaced. Pull again to rebuild it. The state moves to **Reconcile required** if a commit had already been accepted, and to **Never reconciled** if none ever was. Changing only the token, deploy key, host-key trust, or the apply behavior leaves a staged commit alone, since neither changes what would be materialized.
|
||
</Note>
|
||
|
||
### When part of the state could not be proven
|
||
|
||
The panel sometimes lists one or more things it could not prove, at the top above the form fields. A managed manifest that does not identify this stack on this node from the repository configured now, an approval that could not be restored after a recovery, a record that has gone: each is reported as its own line.
|
||
|
||
These are qualifications, not failures. The state above them is real, and the lines tell you which part of it to treat with less confidence. That is deliberate: a stack whose evidence is partly missing looks identical to one with complete evidence unless Sencho says otherwise, and the safer wrong answer is never the quiet one.
|
||
|
||
## Create a stack from a Git repository
|
||
|
||
Skip the "empty stack then link later" detour and point at a repo from the start. Click **Create Stack** in the sidebar, switch to the **From Git** tab, and fill in the same fields you would on the Git Source panel.
|
||
|
||
<Frame>
|
||
<img src="/images/git-sources/create-from-git-tab.png" alt="New stack dialog with the From Git tab selected, showing stack name, repository URL, branch, compose path, sibling .env toggle, authentication options, apply behavior radio group, and a Deploy after create checkbox" />
|
||
</Frame>
|
||
|
||
Sencho fetches the compose files, validates the merged result with `docker compose config`, writes them to a fresh stack directory, and links the Git source in one step. The last-applied commit SHA is seeded from the fetch so the first manual pull starts from a clean classified plan.
|
||
|
||
Tick **Deploy after create** to run `docker compose up -d` immediately after the files land. If the deploy fails, the stack and Git source are kept on disk so you can fix the underlying issue (missing image, port conflict, host resources) and retry the deploy from the editor.
|
||
|
||
### Failure modes on create
|
||
|
||
| Situation | What happens |
|
||
|-----------|--------------|
|
||
| Stack name already exists | Sencho returns **409** with "Stack already exists" and makes no changes on disk or in the database. Pick a different name or remove the existing stack. |
|
||
| Repository unreachable or auth failed | Fetch fails before anything is created. The form stays open with an error toast describing the cause. |
|
||
| Fetched compose fails validation | The stack directory is not created and no Git source row is inserted. The error toast shows the `docker compose config` message. |
|
||
| Fetch + validate succeed but optional deploy fails | The stack and Git source are kept. The toast reads "Stack created, but deploy failed: ..." and you can retry the deploy from the editor. |
|
||
|
||
## Configure a source
|
||
|
||
| Field | Description |
|
||
|-------|-------------|
|
||
| **Repository URL** | HTTPS (`https://github.com/your-org/your-repo.git`) or SSH (`git@github.com:your-org/your-repo.git`, or `ssh://git@host:port/path` when the server uses a nonstandard port) |
|
||
| **Ref** | Branch, tag, or full commit SHA to track (e.g. `main`, `v1.0`, or a 40-character SHA) |
|
||
| **Compose files** | One or more paths within the repo, merged in the listed order (e.g. `deploy/base.yaml` then `deploy/prod.yaml`). The first file is the primary. Reorder by dragging (or the up/down arrows on a phone) and remove with the **×** button. |
|
||
| **Project directory** | Optional path within the repo passed to `docker compose --project-directory`, so relative build contexts, bind mounts, and `env_file` references resolve from that base. Leave blank to use the stack root. |
|
||
| **Also sync sibling `.env` file** | When enabled, also pulls the `.env` from the same directory as the primary compose file. The form shows the resolved path inline (e.g. `deploy/.env` for a primary at `deploy/compose.yaml`). |
|
||
| **Authentication** | **Public (no auth)** for public repos, **Personal Access Token** for private HTTPS repos, or **SSH deploy key** for private SSH repos |
|
||
| **Apply behavior** | See the three modes below |
|
||
|
||
Saving runs a reachability check against the repository. If the URL is wrong, the token is invalid, the ref does not exist, or a file is missing, Sencho surfaces the error inline and nothing is persisted.
|
||
|
||
### Multiple compose files
|
||
|
||
Many projects split a stack into a base file plus environment overrides. List the files in the order you want them merged and Sencho deploys with `docker compose -f base.yaml -f prod.yaml ...`, applying each later file's values on top of the earlier ones, exactly as the Compose CLI does. The same ordered set is used for every action on the stack: deploy, update, restart, stop, and teardown.
|
||
|
||
On disk, the primary file lands as the stack's `compose.yaml` and each additional file keeps its repository-relative path inside the stack directory, so you can still browse and edit them in the file explorer. A single-file source behaves exactly as before.
|
||
|
||
### Apply behavior modes
|
||
|
||
| Mode | What happens when a webhook fires |
|
||
|------|-----------------------------------|
|
||
| **Review only** | Sencho fetches and validates the incoming commit and marks the stack as having a pending update. You review the change plan and apply manually. |
|
||
| **Auto-write files** | Sencho writes the new compose and env to disk automatically but does not redeploy. Use this when another process handles rollout. |
|
||
| **Auto-deploy** | Sencho writes the files and immediately runs `docker compose up -d` so the stack picks up the new configuration. |
|
||
|
||
Auto-deploy implies Auto-write: you cannot deploy automatically without also writing the new files first.
|
||
|
||
You can always override on the spot: when you click **Apply** in the change plan, a **Deploy after apply** checkbox lets you deploy regardless of the configured mode.
|
||
|
||
## Pulling and reviewing changes
|
||
|
||
Click **Pull now** on the Git Source panel to fetch the latest commit on the configured branch, tag, or commit.
|
||
|
||
<Frame>
|
||
<img src="/images/git-sources/diff-dialog.png" alt="GIT · CHANGE PLAN dialog for the demo-app stack, listing classified file operations (add, modify, remove) with a Deploy after apply checkbox and an Apply button in the footer" />
|
||
</Frame>
|
||
|
||
The change plan shows:
|
||
|
||
- The `GIT · CHANGE PLAN` kicker and the short SHA of the incoming commit at the top.
|
||
- One row per classified operation: add, modify, remove, rename, a Compose invocation change, or a local conflict. Unchanged files collapse to a count.
|
||
- An **Incoming compose failed validation** banner when the incoming compose fails `docker compose config`. The Apply button stays disabled until validation passes.
|
||
- A **Local conflicts block apply** banner when a managed file was edited or removed on disk, or an unmanaged file sits in the way. Apply stays disabled until those conflicts are resolved. Sencho does not overwrite them.
|
||
- A **Live Compose invocation changed** banner when the Compose command line on disk no longer matches the last applied generation. Apply stays enabled. Applying records the incoming invocation as the new baseline and leaves unmanaged files on disk.
|
||
|
||
High-sensitivity paths (secret-bearing files) appear as "secret-bearing managed path" rather than as a filename.
|
||
|
||
### Pending updates
|
||
|
||
When a webhook fires in **Review only** mode, the stack gets a pending GitBranch icon next to its row in the sidebar and a pulsing dot on the **Git Source** button in the editor. Clicking either re-fetches the commit and opens the change plan; the panel also shows a **Pending update** banner with a **Review** button.
|
||
|
||
If the same stack also has an image update available, the image-update dot in the sidebar takes priority over the Git source icon, so only the update dot renders. The pending Git source is still surfaced inside the editor on the **Git Source** button.
|
||
|
||
<Frame>
|
||
<img src="/images/git-sources/sidebar-badge.png" alt="Sidebar stack list with a small GitBranch icon next to the demo-app entry indicating a pending Git source update" />
|
||
</Frame>
|
||
|
||
Click **Dismiss** in the change plan to discard a pending update without applying.
|
||
|
||
## Trigger from CI with a webhook
|
||
|
||
Git sources integrate with Sencho's existing webhook system. Create a webhook targeting the stack with the **Git source sync** action.
|
||
|
||
<Frame>
|
||
<img src="/images/git-sources/webhook-action.png" alt="New webhook form with Name, Stack and Action fields. The Action select is open, showing Deploy, Restart, Stop, Start, Pull and Update, and Git source sync as options, with Git source sync highlighted at the bottom" />
|
||
</Frame>
|
||
|
||
The webhook's behavior on trigger depends on the source's apply mode:
|
||
|
||
- **Review only**: fetch, validate, diff, mark pending.
|
||
- **Auto-write files**: fetch, validate, write to disk.
|
||
- **Auto-deploy**: fetch, validate, write, deploy.
|
||
|
||
The Git source sync action is only selectable on webhooks whose target stack already has a Git source configured. Webhook triggers for a single source are debounced on a 10-second window so a runaway pipeline cannot overwhelm Sencho (or your repository host's rate limits); the dashboard records the skipped trigger in the webhook's execution history.
|
||
|
||
### GitHub Actions example
|
||
|
||
```yaml
|
||
- name: Sync compose via Sencho
|
||
run: |
|
||
BODY='{}'
|
||
SIGNATURE=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "${{ secrets.SENCHO_WEBHOOK_SECRET }}" | cut -d' ' -f2)
|
||
curl -X POST "${{ secrets.SENCHO_URL }}/api/webhooks/${{ secrets.SENCHO_WEBHOOK_ID }}/trigger" \
|
||
-H "Content-Type: application/json" \
|
||
-H "X-Webhook-Signature: sha256=$SIGNATURE" \
|
||
-d "$BODY"
|
||
```
|
||
|
||
See the [Webhooks](/features/webhooks) page for the full signing protocol.
|
||
|
||
## Private repositories
|
||
|
||
### HTTPS with a personal access token
|
||
|
||
For private repositories over HTTPS, use a Personal Access Token scoped to read access on the target repo:
|
||
|
||
- **GitHub**: a fine-grained PAT with **Contents: Read** permission on the repo, or a classic PAT with the `repo` scope.
|
||
- **GitLab**: a project or group access token with the `read_repository` scope.
|
||
- **Bitbucket**: an app password with **Repositories: Read**.
|
||
|
||
Paste the token into the **Token** field and save. Sencho stores it encrypted at rest and never returns it in API responses or UI. When editing the source later, the token field shows a masked placeholder; leave it blank to keep the stored value, or type a new token to replace it. Switching the auth type back to **Public (no auth)** clears the stored token.
|
||
|
||
The encryption boundary covers the pending update payload too: every pull caches the fetched compose and env content in the database so the change plan can reopen without a refetch, and that cached content is encrypted at rest in the same way as the token, since compose files routinely embed secrets via env interpolation.
|
||
|
||
### SSH with a deploy key
|
||
|
||
For private repositories over SSH, paste a read-only deploy key and confirm the server's host key before saving.
|
||
|
||
1. Generate an ed25519 key pair on a trusted machine (`ssh-keygen -t ed25519 -f deploy-key -N ""`).
|
||
2. Add the public key to the Git host as a deploy key with read access (GitHub: **Settings → Deploy keys** on the repository).
|
||
3. In Sencho, set **Authentication** to **SSH deploy key**, paste the private key, then click **Fetch host key fingerprint**. Sencho probes the host with `ssh-keyscan`, shows the SHA256 fingerprint, and stores the matching `known_hosts` line.
|
||
4. Save. Sencho stores the private key encrypted at rest and never returns it in API responses or UI.
|
||
|
||
Sencho verifies the host key on every fetch (`StrictHostKeyChecking=yes` against the stored line). If the server key changes, pulls fail with a host-key error until you review the new fingerprint and update trust deliberately.
|
||
|
||
Use an `ssh://` URL when the Git server listens on a nonstandard port (for example `ssh://git@git.example.com:2222/org/repo.git`). The familiar `git@host:org/repo.git` form assumes port 22.
|
||
|
||
Switching authentication back to **Public (no auth)** or to a token clears the stored deploy key and host-key trust.
|
||
|
||
### Private HTTPS with a custom CA
|
||
|
||
Self-hosted Git servers often use TLS certificates signed by a private certificate authority. By default, Sencho trusts the system certificate store on the host running the fetch. When your git server uses a private CA, paste the CA certificate (PEM) in **Custom CA certificate** on the Git source form.
|
||
|
||
Sencho combines your CA with the system trust anchors (it does not replace them), so public hosts such as GitHub continue to validate normally. The CA bundle is encrypted at rest and is never returned after save.
|
||
|
||
Leave the field empty when the server uses a publicly trusted certificate.
|
||
|
||
Removing a stored CA always saves, even if the server currently needs it to be reached: retiring a certificate you no longer trust should never be blocked by the unreachability that retiring it causes. If the server still needs a private CA afterward, the next pull reports a certificate trust error until you upload one again.
|
||
|
||
### Redirects
|
||
|
||
Some Git servers answer with a redirect, for example when a repository moves to a canonical path. Sencho follows a redirect that stays on the same server (same scheme, host, and port) and only changes the path, so a relocated repository keeps working without you editing the URL.
|
||
|
||
A redirect that points at a different server is refused, and the pull reports that the host redirected elsewhere. Sencho does not contact that other server or send it your token. If a repository has genuinely moved to a new host, update the repository URL on the Git source to the new address.
|
||
|
||
## Local edits vs Git
|
||
|
||
Sencho classifies every managed path against the last applied generation and the live disk.
|
||
|
||
- Matching the last applied content: applying writes files Sencho itself last wrote.
|
||
- Locally modified, missing, type-changed, or colliding unmanaged files: the plan is blocked. Resolve those files on disk (or commit them back to the repository), then pull again. Sencho will not overwrite them.
|
||
- A local edit still blocks even when the live bytes already match the incoming commit. Classification compares disk to the last applied generation, not to the incoming files, so an uncommitted local edit is never treated as a clean Git apply.
|
||
- Compose invocation drift (for example adding or removing a root `.env` outside Git): the plan shows the invocation change and stays applicable. Applying records the incoming invocation as the new baseline. Unmanaged files stay on disk. Webhook auto-apply still refuses until you review that plan.
|
||
|
||
The in-browser editor and the Git Source panel both write to the same files, so you can always fall back to editing locally. The next pull will flag the divergence rather than silently clobbering your edits.
|
||
|
||
Pulls, applies, and create-from-git operations on the same stack are serialized by a per-stack lock, so a webhook that fires in the middle of a manual apply waits for the apply to finish rather than racing it.
|
||
|
||
## Troubleshooting
|
||
|
||
<AccordionGroup>
|
||
<Accordion title="Repository not found or not accessible">
|
||
Verify the URL is reachable from the Sencho host and ends with `.git`. GitHub returns a "not found" response for both genuinely missing repos and private repos you cannot read, so Sencho tailors the hint based on what you provided:
|
||
|
||
- **No token configured**: the repo might be private. Switch **Authentication** to **Personal Access Token** and paste a token with read access.
|
||
- **Token configured**: double-check the URL is correct and the token has read access to this specific repo. GitHub fine-grained PATs need **Contents: Read** on the target repo; classic PATs need the `repo` scope.
|
||
</Accordion>
|
||
|
||
<Accordion title="Authentication failed">
|
||
The Git host rejected the credentials you supplied. For HTTPS, the token may be missing, expired, or lack read access: generate a new token and replace the value in the **Token** field. For SSH, the deploy key may be wrong or not registered on the host: paste the matching private key or add the public key on the Git host. Sencho returns this as a 400 form error rather than a 401, so an upstream auth failure does not sign you out of the dashboard.
|
||
</Accordion>
|
||
|
||
<Accordion title="Branch or tag not found">
|
||
The configured branch or tag is case-sensitive and must exist on the remote. Confirm it with `git ls-remote <url>` from a shell that has access. Sencho resolves the ref before fetching, so a typo or a ref that was never pushed surfaces here rather than as a generic failure. A full commit SHA never produces this error: it resolves to itself, so a SHA the host refuses to serve reports as a host-capability problem under "Commit not reachable on this host".
|
||
</Accordion>
|
||
|
||
<Accordion title="Branch or tag deleted or force-pushed">
|
||
The configured ref previously resolved to a commit but no longer matches that history. The ref may have been deleted, renamed, superseded by a same-named tag, or force-pushed to a history the old commit is no longer part of. Point the source at a current branch, tag, or commit, or restore the ref upstream, then save again.
|
||
</Accordion>
|
||
|
||
<Accordion title="Commit not reachable on this host">
|
||
The configured commit SHA is not one the Git host will serve. Hosts only fetch SHAs they advertise by default, so a commit that is not on any branch or tag tip, or one on a host that blocks unadvertised object fetch, returns this. Use a branch or tag, or a commit the host advertises.
|
||
</Accordion>
|
||
|
||
<Accordion title="File not found">
|
||
Each compose path is relative to the repository root and must point at the file, not its parent directory. Every file in the list must exist on the tracked branch; if any is missing, the save or pull fails and names the path. If a file was moved, update its path on the panel and save.
|
||
</Accordion>
|
||
|
||
<Accordion title="Compose validation failed">
|
||
Sencho runs `docker compose config` against the incoming content before letting you apply. The error banner shows the exact message. Common causes: unresolved `${VAR}` interpolation (commit a `.env` file next to the compose file and enable sibling `.env` sync), invalid `include:` paths, or schema issues introduced by a recent compose change. Validation has a 10-second budget; an unusually large compose with many services may need to be split.
|
||
</Accordion>
|
||
|
||
<Accordion title="Local conflicts block apply">
|
||
A managed file was edited or removed on disk, or an unmanaged file sits on a path the incoming commit wants to add. Sencho will not overwrite those files. Restore or relocate the local copy, or commit the local change back to the repository so the next pull is clean, then pull again.
|
||
</Accordion>
|
||
|
||
<Accordion title="Live Compose invocation changed">
|
||
The Compose command line on disk no longer matches the last applied generation, most often because a root `.env` file was added or removed outside Git. This is not a file conflict: Apply stays enabled. Applying records the incoming invocation as the new baseline and leaves unmanaged files on disk. Webhook auto-apply will not write until you review that plan in the dashboard.
|
||
</Accordion>
|
||
|
||
<Accordion title="Webhook skipped (rate limited)">
|
||
Sencho debounces rapid-fire triggers on a 10-second window per source. Wait at least 10 seconds and retry, or consolidate multiple CI triggers into a single call at the end of your pipeline. Skipped triggers appear in the webhook's execution history.
|
||
</Accordion>
|
||
|
||
<Accordion title="Network timeout">
|
||
The clone did not finish in time. Fetches run with a 30-second timeout to keep a slow or unreachable host from hanging the stack panel. Check that the Sencho host can reach the repository host (proxies, firewalls, DNS) and try again. If the repository is genuinely large, pin a smaller compose subpath or mirror it somewhere closer to the Sencho host.
|
||
</Accordion>
|
||
|
||
<Accordion title="Repository too large">
|
||
A clone is capped on the on-disk size of its temporary workspace (100 MB by default), and individual compose/env files are capped on read. A compose repository is normally tiny, so hitting either usually means the tracked branch carries large binaries. Point the source at a repository or branch that holds just your compose and `.env` files. If a large repository is unavoidable, an operator can raise the workspace ceiling with the `GITSOURCE_MAX_CLONE_BYTES` environment variable.
|
||
</Accordion>
|
||
|
||
<Accordion title="Pending commit has changed since this pull was fetched">
|
||
You opened a change plan, then a webhook fired and replaced the pending commit before you clicked **Apply**. Close the dialog and reopen the panel to load the latest pending commit; the **Review** button will fetch the newer one.
|
||
</Accordion>
|
||
|
||
<Accordion title="Applied but deploy failed">
|
||
The incoming compose file was written to disk successfully, but the subsequent `docker compose up -d` did not complete. The toast message includes the underlying reason (for example, an image pull failure or a port conflict). The stack is already on the new content, so you can retry the deploy directly from the editor's **Deploy** button without re-pulling. Fix the root cause first (image availability, host resources, network config) and redeploy.
|
||
</Accordion>
|
||
|
||
<Accordion title="Stack contents look wrong or appear empty">
|
||
If the compose file in your repository is tracked via Git LFS, Sencho will refuse the link and surface an LFS error rather than write a pointer stub as real content. Commit the plain compose file (and any synced `.env`) without LFS, or replace the LFS pointer in-place, then retry.
|
||
</Accordion>
|
||
|
||
<Accordion title="A build context or volume points at an empty folder">
|
||
Repositories that use Git submodules do not have their submodule contents cloned during a Git Source fetch. Sencho surfaces a warning when `.gitmodules` is present, and refuses inputs and build contexts that reference submodule contents with an actionable message. Inline the referenced files into the main repository or flatten the submodule so the paths resolve at deploy time.
|
||
</Accordion>
|
||
|
||
<Accordion title="An additional file named compose.yaml is rejected">
|
||
The first file in the list is always written to the stack's root `compose.yaml`. To avoid clobbering it, an additional file whose repository path is also `compose.yaml` is rejected. Rename it in the repository, or move it to the top of the list to make it the primary.
|
||
</Accordion>
|
||
|
||
<Accordion title="A relative build context or extends target is missing with multiple files">
|
||
Git Sources materializes the complete project, including recursive `include:` and `extends.file` dependencies, service env files, file-backed configs and secrets, and build contexts. If a referenced file is still missing, the pull refused it and the refusal message names the path and the reason (out-of-bound path, Git LFS pointer, submodule, symlink, or a size cap). Fix the declaration in the repository and pull again.
|
||
</Accordion>
|
||
|
||
<Accordion title="SSH host key verification failed">
|
||
The server's SSH host key no longer matches the fingerprint you accepted. This can mean a MITM risk or an intentional key rotation on the host. Click **Fetch host key fingerprint** again, compare the SHA256 value out of band with your operator, and save only if you intend to trust the new key.
|
||
</Accordion>
|
||
|
||
<Accordion title="Only unsupported URL schemes">
|
||
Git Sources accept `https://` URLs and SSH URLs (`git@host:org/repo.git` or `ssh://git@host:port/path`). Other schemes are rejected at save time.
|
||
</Accordion>
|
||
</AccordionGroup>
|
||
|
||
## Known limitations
|
||
|
||
- **HTTPS and SSH.** HTTPS uses tokens; SSH uses deploy keys with strict host-key verification. Custom URL schemes are not supported.
|
||
- **No Git LFS.** Compose and env files stored via LFS are rejected. Commit plain files instead.
|
||
- **No submodules.** Submodule contents are not fetched. Inputs and build contexts that reference submodule contents are refused with an actionable message; a warning is shown when `.gitmodules` is present.
|
||
- **Refs, not arbitrary commits.** Sources follow a branch head, a tag, or a pinned commit SHA. Each pull resolves the configured ref to the exact commit it currently points at and pins that SHA, so apply always materializes the reviewed revision. A commit SHA not advertised by the Git host is refused.
|
||
- **Clone size cap.** A clone is bounded on the on-disk size of its temporary workspace (and each compose/env file is capped on read), so very large repositories are rejected. Operators can adjust the workspace ceiling with `GITSOURCE_MAX_CLONE_BYTES`.
|
||
- **Complete project materialization.** Every repository-local input the project needs is materialized: the ordered compose files, implicit `compose.override.*` files, recursive `include:` and `extends.file` dependencies, service env files, file-backed configs and secrets, label files, and build contexts with `.dockerignore` semantics. The materialized set is recorded in a versioned managed-project manifest, and each pull stages a candidate that is validated with the exact deployment invocation before anything on disk changes. If apply is interrupted, Sencho completes the accepted generation or restores the previous generation. If files were edited during the interruption and no longer match either generation, Sencho preserves them and requires manual recovery instead of overwriting them.
|
||
- **Unsupported inputs are refused, not guessed.** Inputs that cannot be safely reproduced fail the pull with an actionable message: URL includes, Git LFS pointers, submodule contents, symbolic links, build contexts that exceed the size bounds, and include or extends declarations that point outside the repository or use dynamic `\${VAR}` paths (their contents cannot be enumerated). Nothing is applied until the declaration is fixed. Absolute host paths, host bind mounts, external resources, and dynamic `\${VAR}` data paths are never claimed as covered: they resolve at deploy time from the environment or the node, and the manifest records them as unmanaged.
|
||
- **Materialization bounds.** The materialized project is bounded by file count, total bytes, per-file size, path depth, and build-context size, each adjustable with a `GITSOURCE_*` variable (see configuration). Crossing a bound refuses the pull with the counts so far rather than producing a partial project.
|
||
- **Detach and export.** Removing a Git source renders the effective compose model into a single `compose.yaml`, keeps the remaining materialized files, removes auto-discovered override files so the exported model is final, and removes Git tracking. Resolved environment values are baked into the exported file. If removal is interrupted before it completes, Sencho restores the original files automatically.
|
||
- **Rollback scope.** Rollback of a Git-managed stack restores the managed authored inventory captured in the recovery generation (the same contract as deploy and update: ordered compose files, overrides, include/extends, and discoverable env/label/config/secret inputs, plus held prior image IDs where available). Named volumes and bind-mounted application data are not restored. Git apply captures a generation before promote. If promote fails mid-flight, Sencho restores the prior files. If apply-with-deploy fails after promote, the applied files stay on disk and the generation remains available for manual rollback (partial success); Sencho does not auto-compensate the deploy failure after apply.
|
||
- **Some read-only views read the primary file.** The dependency graph, drift snapshot, and networking inspector summarize the primary compose file, so a service declared only in an override may not appear in those views. Deploy, update, image-update checks, and mesh attachment use the full merged set.
|