7 · ServerSync capture
Objective — capture the server-side changes the web installer made (config values, storage markers, generated files) back into Git as a reviewable PR via the ServerSync GitHub Actions workflow — so the next environment starts from a complete picture.
Background
Section titled “Background”The web installer creates files on the server (config values, storage markers, generated assets) that aren’t in your repo. ServerSync is a GitHub Actions workflow that captures those changes back into Git as a reviewable PR — so the next environment starts from a complete picture.
Where this page sits in the phase:
flowchart LR Install[Web installer on server] --> Files[Generated / config files] Files --> WF[ServerSync workflow] WF --> PR[PR back to git] PR --> Repo[Repo stays source of truth]1. Verify the workflow is available
Section titled “1. Verify the workflow is available”GitHub only surfaces workflows from the default branch (usually main). Two failure modes hide here: the file isn’t on main at all, or it’s on main but stale versus staging.
-
List the workflows GitHub sees.
Terminal window REPO=$(gh repo view --json nameWithOwner -q '.nameWithOwner')gh workflow list --repo "$REPO"# Expected: the ServerSync workflow appears in the list- ✅ Workflows are listed (or you route to the table below to diagnose).
Read the result:
| Result | Cause | Action |
|---|---|---|
| Workflows listed | Ready — verify parity (section 2) | Continue |
| Empty | File not on default branch | Path B below |
| Error | CLI not authenticated | gh auth status |
2. Version parity: is main’s workflow current?
Section titled “2. Version parity: is main’s workflow current?”If you edited the workflow on staging (added GIT_ONLY_PATHS, changed CODE_EXCLUDES) but haven’t merged to main, running without --ref uses main’s stale copy — which can propose deleting protected files.
-
Compare the workflow blob on
mainvsstaging.Terminal window git fetch origin main staging --no-tagsfor wf in .github/workflows/server-sync-staging.yml; doM=$(git ls-tree origin/main "$wf" | awk '{print $3}')S=$(git ls-tree origin/staging "$wf" | awk '{print $3}')[ -z "$M" ] && { echo "$wf: NOT ON MAIN → Path B"; continue; }[ "$M" = "$S" ] && echo "$wf: in sync (no --ref needed)" || echo "$wf: DRIFT → use --ref staging (Path A)"done# Expected: "in sync", "DRIFT → use --ref staging", or "NOT ON MAIN → Path B"- ✅ You know whether to run plain, with
--ref staging(Path A), or to fixmainfirst (Path B).
- ✅ You know whether to run plain, with
Pick the path the comparison pointed at:
Path A — on main but staging is newer (recommended). Don’t merge staging → main just to refresh a workflow mid-Phase-5 (it promotes unvalidated commits). Use --ref to run staging’s definition directly:
-
Run
staging’s workflow definition directly.Terminal window gh workflow run server-sync-staging.yml --ref staging --repo "$REPO"git show origin/staging:.github/workflows/server-sync-staging.yml | grep -A30 "GIT_ONLY_PATHS"# Expected: the workflow dispatches against staging's definition- ✅ The run uses
staging’s up-to-date workflow definition.
- ✅ The run uses
Path B — not on main at all. Cherry-pick only the workflow commits (keep main clean); full merge is the last resort:
-
Cherry-pick the workflow commits onto
main.Terminal window WF_COMMITS=$(git log origin/main..origin/staging --oneline -- .github/workflows/server-sync-staging.yml | awk '{print $1}')git checkout mainfor sha in $(echo "$WF_COMMITS" | tac); do git cherry-pick "$sha"; donegit push origin main && git checkout staging# Expected: the workflow now exists on main; you're back on staging- ✅ The workflow is on
mainand you’re back onstaging.
- ✅ The workflow is on
If workflows still don’t appear, lint for YAML errors: actionlint .github/workflows/*.yml.
3. Re-audit clear_paths ↔ GIT_ONLY_PATHS
Section titled “3. Re-audit clear_paths ↔ GIT_ONLY_PATHS”This audit prevents ServerSync from proposing the deletion of files that should only exist in Git.
-
Diff
clear_pathsagainstGIT_ONLY_PATHS.Terminal window CLEAR_PATHS=$(awk '/add\(.clear_paths./{c=1;next} c&&/\]\);/{c=0} c' deploy.php \| grep -oE "'[^']+'" | tr -d "'" | sort -u)PROTECTED=$(awk '/^[[:space:]]*GIT_ONLY_PATHS:/{c=1;next}c && /^[[:space:]]{2}[A-Za-z_]+:/{c=0}c && /^[[:space:]]*-[[:space:]]/{sub(/^[[:space:]]*-[[:space:]]*/,""); print}' .github/workflows/server-sync-staging.yml | sort -u)MISSING=$(comm -23 <(echo "$CLEAR_PATHS") <(echo "$PROTECTED"))[ -n "$MISSING" ] && { echo "STOP — unprotected paths:"; echo "$MISSING"; } || echo "All clear_paths protected"# Expected: "All clear_paths protected"- ✅ The audit prints
All clear_paths protected.
- ✅ The audit prints
If anything is missing: add it to GIT_ONLY_PATHS, commit, merge to main, and re-run the audit until clean.
4. Trigger the workflow and watch it
Section titled “4. Trigger the workflow and watch it”Run the workflow and follow it to completion.
-
Trigger the run and watch it.
Terminal window gh workflow run server-sync-staging.yml --ref staging --repo "$REPO" # drop --ref if section 2 said "in sync"sleep 5RUN_ID=$(gh run list --workflow=server-sync-staging.yml --limit=1 --json databaseId -q '.[0].databaseId' --repo "$REPO")gh run watch "$RUN_ID" --repo "$REPO"# Expected: the run completes; a PR opens (or it reports "no changes")- ✅ The run completes and opens a PR (or reports “no changes”).
If it fails, read the failing step (gh run view "$RUN_ID" --log-failed --repo "$REPO" | tail -60) and match the error:
| Error | Cause | Fix |
|---|---|---|
Permission denied (publickey,password) | SSH key mismatch (or staging workflow using the prod SSH_PRIVATE_KEY_BASE64 secret instead of STAGING_…) | Check the Actions SSH secret vs the server’s authorized key |
Connection timed out | usually transient; else wrong host/port secret or runner IP blocked | Re-run first (≈half are transient); if it persists, confirm STAGING_HOST/STAGING_PORT hold raw values (IP + port, not the alias) |
Host key verification failed | server host key changed | Re-run — workflow usually accepts new keys |
rsync: connection unexpectedly closed | transient transfer drop | Re-run once |
Workflow file not found | not on main | Section 2, Path B |
Unexpected value 'workflow_dispatch' | YAML syntax error on the ref | actionlint, fix, commit, re-run |
Connection timed out — full recovery sequence
Section titled “Connection timed out — full recovery sequence”Re-running clears roughly half of these. If it survives a few retries, the runner genuinely can’t reach the host. Work the sequence below in order. The runner has no access to ~/.ssh/config, so the STAGING_* secrets must hold raw values (IP + port), never the alias name.
-
Verify the secrets exist and have the right shape (values are masked — you’re checking presence, not content).
Terminal window REPO=$(gh repo view --json nameWithOwner -q '.nameWithOwner')gh secret list --repo "$REPO" | grep -E "STAGING_HOST|STAGING_PORT|STAGING_USER|STAGING_DEPLOY_PATH|STAGING_SSH_PRIVATE_KEY_BASE64"# Expected: all five STAGING_ secrets present- ✅ All five
STAGING_*secrets are listed.
- ✅ All five
-
Re-extract host / port / user from
~/.ssh/configand re-upload as raw values.Terminal window STAGING_ALIAS="<staging-alias>"HOST=$(awk "/^Host $STAGING_ALIAS\$/,/^Host /{if(\$1==\"HostName\")print \$2}" ~/.ssh/config | head -1)PORT=$(awk "/^Host $STAGING_ALIAS\$/,/^Host /{if(\$1==\"Port\")print \$2}" ~/.ssh/config | head -1)USER_VAL=$(awk "/^Host $STAGING_ALIAS\$/,/^Host /{if(\$1==\"User\")print \$2}" ~/.ssh/config | head -1)echo "HOST=$HOST PORT=$PORT USER=$USER_VAL" # HOST must be the raw IP, not the aliasprintf '%s' "$HOST" | gh secret set STAGING_HOST --repo "$REPO"printf '%s' "$PORT" | gh secret set STAGING_PORT --repo "$REPO"printf '%s' "$USER_VAL" | gh secret set STAGING_USER --repo "$REPO"# Expected: HOST holds server IP; PORT holds raw SSH port- ✅
STAGING_HOSTandSTAGING_PORTare raw values.
- ✅
-
Retry with exponential backoff.
Terminal window for attempt in 1 2 3; doecho "=== Attempt $attempt ==="gh workflow run server-sync-staging.yml --ref staging --repo "$REPO"sleep 10RUN_ID=$(gh run list --workflow=server-sync-staging.yml --limit=1 --json databaseId -q '.[0].databaseId' --repo "$REPO")gh run watch "$RUN_ID" --repo "$REPO" --exit-status && break[ "$attempt" -lt 3 ] && sleep 60done# Expected: one attempt completes, or all three exhaust and you fall through to local fallback- ✅ A retry succeeds, or all three attempts fail consistently.
-
Still failing after 3 attempts → run ServerSync locally.
Terminal window bash Admin-Local/Guides-v2/Templates/Scripts/server-sync-local.sh <staging-alias># Expected: the same capture branch / PR the workflow would have produced- ✅ The local run produces the capture without depending on GitHub runner reachability.
A successful run opens a PR (or reports “no changes”). Treat it like any code review — scrutinize anything under config/, storage/, or resources/lang/ that may carry installer-generated values you don’t want committed.
5. Review the PR file-by-file
Section titled “5. Review the PR file-by-file”Start from a clean local tree, then stage the PR changes without committing so you can accept/discard per file.
-
Stage the PR changes without committing.
Terminal window git checkout staging && git pull origin staginggit fetch origin pull/PR_NUMBER/head:serversync-reviewgit merge serversync-review --no-commit --no-ffgit diff --cached --name-status# Expected: the staged changes listed by status (A / M / D), nothing committed yet- ✅ The PR changes are staged and listed by status, with nothing committed yet.
-
Discard anything that should stay in Git, then commit and close.
Terminal window git checkout HEAD -- .env.example composer.lock package-lock.json # common keep-in-git filesgit diff --cached --name-statusgit commit -m "ServerSync: capture staging installer changes"git push origin staginggh pr close PR_NUMBER --repo "$REPO" --comment "Reviewed and merged file-by-file"git branch -d serversync-review# Expected: wanted changes committed and pushed; protected files preserved; PR closed- ✅ Wanted changes are committed and pushed; protected files (
.env.example, lockfiles) are preserved; the PR is closed.
- ✅ Wanted changes are committed and pushed; protected files (
Read the status column when deciding accept/discard:
| Status | Meaning | Default |
|---|---|---|
A | added | usually accept |
M | modified | review |
D | deleted | review carefully |
If the PR diff is already clean end-to-end, the shortcut is gh pr merge PR_NUMBER --squash — but only after you’ve actually read it.
Checklist
Section titled “Checklist”Do not mark this step done until every box below is checked.
- 🤖 Workflow available — ServerSync workflow available on
main; version parity checked (--ref stagingif drift). - 🤖 Audit clean —
clear_paths↔GIT_ONLY_PATHSaudit returns clean. - 🔀 PR reviewed — workflow run succeeded; PR reviewed file-by-file.
- 🤖 Protected files preserved — wanted changes committed;
.env.example+ lockfiles preserved.