Skip to content
prod e051e98
Browse

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.

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]

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.

  1. 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:

ResultCauseAction
Workflows listedReady — verify parity (section 2)Continue
EmptyFile not on default branchPath B below
ErrorCLI not authenticatedgh 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.

  1. Compare the workflow blob on main vs staging.

    Terminal window
    git fetch origin main staging --no-tags
    for wf in .github/workflows/server-sync-staging.yml; do
    M=$(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 fix main first (Path B).

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:

  1. 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.

Path B — not on main at all. Cherry-pick only the workflow commits (keep main clean); full merge is the last resort:

  1. 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 main
    for sha in $(echo "$WF_COMMITS" | tac); do git cherry-pick "$sha"; done
    git push origin main && git checkout staging
    # Expected: the workflow now exists on main; you're back on staging
    • ✅ The workflow is on main and you’re back on staging.

If workflows still don’t appear, lint for YAML errors: actionlint .github/workflows/*.yml.

3. Re-audit clear_pathsGIT_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.

  1. Diff clear_paths against GIT_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.

If anything is missing: add it to GIT_ONLY_PATHS, commit, merge to main, and re-run the audit until clean.

Run the workflow and follow it to completion.

  1. 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 5
    RUN_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:

ErrorCauseFix
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 outusually transient; else wrong host/port secret or runner IP blockedRe-run first (≈half are transient); if it persists, confirm STAGING_HOST/STAGING_PORT hold raw values (IP + port, not the alias)
Host key verification failedserver host key changedRe-run — workflow usually accepts new keys
rsync: connection unexpectedly closedtransient transfer dropRe-run once
Workflow file not foundnot on mainSection 2, Path B
Unexpected value 'workflow_dispatch'YAML syntax error on the refactionlint, 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.

  1. 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.
  2. Re-extract host / port / user from ~/.ssh/config and 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 alias
    printf '%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_HOST and STAGING_PORT are raw values.
  3. Retry with exponential backoff.

    Terminal window
    for attempt in 1 2 3; do
    echo "=== Attempt $attempt ==="
    gh workflow run server-sync-staging.yml --ref staging --repo "$REPO"
    sleep 10
    RUN_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 60
    done
    # Expected: one attempt completes, or all three exhaust and you fall through to local fallback
    • ✅ A retry succeeds, or all three attempts fail consistently.
  4. 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.

Start from a clean local tree, then stage the PR changes without committing so you can accept/discard per file.

  1. Stage the PR changes without committing.

    Terminal window
    git checkout staging && git pull origin staging
    git fetch origin pull/PR_NUMBER/head:serversync-review
    git merge serversync-review --no-commit --no-ff
    git 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.
  2. 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 files
    git diff --cached --name-status
    git commit -m "ServerSync: capture staging installer changes"
    git push origin staging
    gh 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.

Read the status column when deciding accept/discard:

StatusMeaningDefault
Aaddedusually accept
Mmodifiedreview
Ddeletedreview 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.

Do not mark this step done until every box below is checked.

  • 🤖 Workflow available — ServerSync workflow available on main; version parity checked (--ref staging if drift).
  • 🤖 Audit cleanclear_pathsGIT_ONLY_PATHS audit returns clean.
  • 🔀 PR reviewed — workflow run succeeded; PR reviewed file-by-file.
  • 🤖 Protected files preserved — wanted changes committed; .env.example + lockfiles preserved.