4 · CI + ServerSync
Objective — wire GitHub Actions for push-to-deploy plus ServerSync (so server-side changes flow back into Git and the repo stays the source of truth): author the workflows, set the secrets, keep clear_paths ↔ GIT_ONLY_PATHS symmetric, and lint them. The first live workflow run is [RUN-LIVE] work in Phase 5 or Phase 12.
Steps at a glance:
- Author the workflow — Create
.github/workflows/deploy.yml. The CodeCanyon kit ships a template with placeholders — customize hostnames, branches, and paths rather than writing from scratch. - Configure GitHub Secrets — Set these under Repo → Settings → Secrets and variables → Actions:.
- Keep
clear_paths↔GIT_ONLY_PATHSsymmetric — ServerSync pushes server-side changes back to Git. Two lists must mirror each other or you get drift or accidental deletion:. - Validate the YAML before pushing — Catch syntax and expression errors locally so a bad workflow never
lands on
develop(the default branch). - Commit and publish the workflow source — Put the workflow files on
developso GitHub can see them. Do not require a live deploy run in Phase 4; Phase 5 triggers and verifies the first non-production run. - Mirror the gates locally — git hooks — CI catches problems after you push. Git hooks (via Husky) catch them before — so a leaked secret or a style error never leaves your machine.
Background
Section titled “Background”flowchart LR Push[git push main] --> GA[GitHub Actions] GA --> Dep[Deployer SSH deploy] Dep --> Server[Production server] Server --> SS[ServerSync workflow] SS --> PR[Reviewable PR to git] PR --> Push1. Author the workflow
Section titled “1. Author the workflow”Create .github/workflows/deploy.yml. The CodeCanyon kit ships a template with placeholders — customize hostnames, branches, and paths rather than writing from scratch.
-
Write the deploy workflow from the kit template.
name: Deployon:push:branches: [main] # production# add 'staging' for a staging deploy jobjobs:deploy:runs-on: ubuntu-lateststeps:- uses: actions/checkout@v4- uses: shivammathur/setup-php@v2with:php-version: '8.2' # match deploy.php's bin/php + composer.json# Load the private key into an ssh-agent — Deployer authenticates over# SSH, so the key MUST be in an agent, not just an env var (an env var# alone fails with "Permission denied (publickey)").- uses: webfactory/ssh-agent@v0.9.0with:ssh-private-key: ${{ secrets.DEPLOYER_SSH_KEY }}# Pin the server's host key so SSH doesn't prompt (and the run doesn't hang).- name: Trust the server host keyrun: |mkdir -p ~/.ssh && chmod 700 ~/.sshecho "${{ secrets.KNOWN_HOSTS }}" >> ~/.ssh/known_hostschmod 600 ~/.ssh/known_hosts- run: composer install --no-dev --optimize-autoloader- name: Deployrun: vendor/bin/dep deploy "$DEPLOY_TARGET"env:DEPLOY_TARGET: staging # replace with the selected target from Zaj-PROJECT.md# ServerSync pushes generated artifacts back to the repo with this PAT.SERVERSYNC_PAT: ${{ secrets.SERVERSYNC_PAT }}- ✅
.github/workflows/deploy.ymlexists with every<PLACEHOLDER>(host, branch, PHP version, deploy command) replaced; the PHP version matchesbin/phpin 1 · Deployer. Phase 5 proves the selected non-production command live.
- ✅
2. Configure GitHub Secrets
Section titled “2. Configure GitHub Secrets”Set these under Repo → Settings → Secrets and variables → Actions. Non-PAT values can be set with gh secret set --body-file -; the fine-grained PAT is browser/UI-only unless the user explicitly pastes it into the secure prompt.
| Secret | Purpose | How to set safely |
|---|---|---|
DEPLOYER_SSH_KEY | Private SSH key the runner uses to reach the server | gh secret set DEPLOYER_SSH_KEY --body-file <key-file> or browser paste |
SERVERSYNC_PAT / GIT_AUTO_COMMIT_PAT | Fine-grained PAT so ServerSync can push/open PRs | Browser UI or secure prompt only; never -b "<pat>" in shell history |
KNOWN_HOSTS | Server host key, to avoid interactive prompts | ssh-keyscan -p <port> <host> | gh secret set KNOWN_HOSTS --body-file - |
Generate the PAT with the minimum scope (contents: write on this repo only) and set an expiry. Add the SSH public key to the server’s ~/.ssh/authorized_keys (or repo deploy keys) and the private key to DEPLOYER_SSH_KEY.
3. Keep clear_paths ↔ GIT_ONLY_PATHS symmetric
Section titled “3. Keep clear_paths ↔ GIT_ONLY_PATHS symmetric”ServerSync pushes server-side changes back to Git. Two lists must mirror each other or you get drift or accidental deletion:
clear_paths(indeploy.php) — paths Deployer wipes from a release before linking shared state.GIT_ONLY_PATHS(in the workflow / sync config) — paths ServerSync treats as Git-owned and won’t pull from the server.
Anything you clear on deploy must be Git-owned on sync, and vice-versa.
For setup-new projects, the symmetric Git-owned/dev-only set includes the Admin-Local/ scaffold, Tools/, agent configs, MCP/Boost configs, and the Zaj-owned root docs:
Admin-Local Tools .cursor .claude .agent .ai .cursorignore.github tests .editorconfig phpunit.xml .phpunit.result.cacheREADME.md Zaj-PROJECT.md Zaj-PROGRESS.md Zaj-BACKLOG.md Zaj-CHANGELOG.md Zaj-CUSTOMIZATIONS.mdCHANGELOG.md _CHANGELOG.md _CHANGELOG-PUBLIC.mdCLAUDE.md CLAUDE.local.md GEMINI.md AGENTS.md ONBOARDING.md DECISIONS.mddeploy.php .env.tpl .env.example .mcp.json boost.json.temp assetsZaj-* files and 90-DevLog/ stay in Git for humans and agents, but they are not production runtime files.
-
Audit the two lists side by side.
Terminal window grep -A20 "clear_paths" deploy.phpgrep "GIT_ONLY_PATHS" .github/workflows/*.yml# Expected: the two lists contain the same paths — reconcile any that appear in only one- ✅ Every path in
clear_pathsalso appears inGIT_ONLY_PATHS, and vice-versa.
- ✅ Every path in
-
Patch a protected security hook without bypassing the secret guard.
Do not use
perl -i -peor other in-place editors to punch through the pre-commit secret scanner — that defeats the guard this page installs. Instead:- Commit the hook as a tracked file (e.g.
.githooks/pre-commit) and point Husky at it, or - Add the hook path to the guard’s allowlist in your agent config, then edit normally, or
- Apply a reviewed patch:
git apply hooks/pre-commit.patch(patch file is in Git; no credentials in the diff).
Terminal window git apply --check .githooks/pre-commit.patch && git apply .githooks/pre-commit.patchchmod +x .githooks/pre-commit# Expected: the hook updates from a versioned patch — no guard bypass- ✅ The security hook is updated from a tracked patch or allowlisted edit — never via a guard-bypass one-liner.
- Commit the hook as a tracked file (e.g.
4. Validate the YAML before pushing
Section titled “4. Validate the YAML before pushing”Catch syntax and expression errors locally so a bad workflow never lands on develop (the default branch).
-
Lint (or parse) the workflow.
Terminal window actionlint -shellcheck= .github/workflows/deploy.yml # lints syntax + expressions; skip shellcheck if absent# no actionlint? quick parse check:python3 -c "import yaml,sys; yaml.safe_load(open('.github/workflows/deploy.yml'))" && echo OK# Expected: actionlint reports no issues, or the parse check prints "OK"- ✅
actionlintpasses (or the YAML parse check printsOK).
- ✅
5. Commit and publish the workflow source
Section titled “5. Commit and publish the workflow source”Put the workflow files on develop so GitHub can see them. In this playbook, develop is the GitHub default branch; main is the release/tag branch that production deploys from later. Do not require a live deploy run in Phase 4.
-
Commit and push the workflow +
deploy.phptodevelop.Terminal window git add .github/workflows/deploy.yml deploy.phpgit commit -m "ci: GitHub Actions deploy + ServerSync"git push origin develop# Expected: workflow source is on the default branch; no live deploy is required in Phase 4- ✅ The workflow source lands on
develop.
- ✅ The workflow source lands on
-
Confirm GitHub sees the workflow. This proves the authored workflow is installed without requiring it to mutate a server yet.
Terminal window gh workflow list | grep -E 'Deploy|ServerSync'# Expected: the deploy and ServerSync workflow names appear- ✅ The workflows appear in GitHub. The first non-production deploy run is verified in Phase 5 Step 3; the first ServerSync capture is verified in Phase 5 Step 7.
6. Mirror the gates locally — git hooks
Section titled “6. Mirror the gates locally — git hooks”CI catches problems after you push. Git hooks catch them before — so a leaked secret or a style error never leaves your machine. Use the repository’s active hook directory instead of blindly adding Husky over an existing hook setup.
-
Detect or create the active hook directory.
Terminal window HOOK_DIR="$(git config core.hooksPath || true)"if [ -z "$HOOK_DIR" ]; thennpm install --save-dev huskynpx husky initHOOK_DIR=".husky"fimkdir -p "$HOOK_DIR"printf 'Using hook dir: %s\n' "$HOOK_DIR"# Expected: existing core.hooksPath is reused, or .husky/ is created once- ✅ The active hook directory exists; existing
.githooks/or.husky/setups are respected.
- ✅ The active hook directory exists; existing
-
pre-commit— block secrets before they are committed. Scan the staged diff for real credential patterns (Stripesk_/pk_, AWSAKIA…, GitHub tokens,APP_KEY=base64:…, private keys) and abort if any appear.Terminal window # $HOOK_DIR/pre-commit — abort the commit if a real credential value is stagedPATTERN='sk_(live|test)_[0-9A-Za-z]{16,}|pk_live_[0-9A-Za-z]{16,}|AKIA[0-9A-Z]{16}|gh[pousr]_[0-9A-Za-z]{36}|APP_KEY=base64:[A-Za-z0-9+/=]{30,}|-----BEGIN [A-Z ]+PRIVATE KEY-----'files=$(git diff --cached --name-only --diff-filter=ACM | grep -vE '(^|/)\.env\.example$|^vendor/|^node_modules/')for f in $files; doif git diff --cached -U0 -- "$f" | grep -E '^\+' | grep -qE "$PATTERN"; thenecho "BLOCKED: a real credential value is in the staged diff ($f). Move it to an env var; rotate it if it was real." >&2exit 1fidone- ✅ Staging a fake
sk_live_…then committing is rejected; a clean commit passes.
- ✅ Staging a fake
-
pre-push— lint before the push. Run Pint on changed files and requireactionlintwhen workflows exist.Terminal window # $HOOK_DIR/pre-push[ -x vendor/bin/pint ] && { ./vendor/bin/pint --test --dirty || { echo "Pint style failed — run: vendor/bin/pint --dirty" >&2; exit 1; }; }if compgen -G '.github/workflows/*.yml' > /dev/null; thencommand -v actionlint >/dev/null || { echo "actionlint required — brew install actionlint" >&2; exit 1; }actionlint -shellcheck=fi- ✅ A push with a Pint violation or a broken workflow is blocked locally; missing
actionlintfails the hook when workflows are present.
- ✅ A push with a Pint violation or a broken workflow is blocked locally; missing
-
post-merge— warn when a pull brings new migrations. Aftergit pull, if new migration files arrived, remind you to run your local migrate task — and that migrations reach servers via the deploy pipeline only, never a manual SSHartisan migrate.Terminal window # $HOOK_DIR/post-mergechanged=$(git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD | grep -E 'database/[Mm]igrations/.*\.php$') || exit 0[ -n "$changed" ] && echo "New migrations pulled — run your local migrate task. Servers migrate via the deploy pipeline only."- ✅ Pulling a branch with a new migration prints the reminder.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Cause | Fix |
|---|---|---|
| Workflow not listed in Actions | File missing from develop (the default branch), or bad YAML | Keep the workflow on develop and run actionlint |
Permission denied (publickey) | SSH key/secret mismatch | Re-add public key to server; recheck DEPLOYER_SSH_KEY |
| ServerSync push 403 | PAT scope/expiry | Reissue PAT with contents + pull-request write scopes, update secret; verify with feedback_api_scope_verification.md |
| Files deleted after sync | clear_paths/GIT_ONLY_PATHS asymmetry | Reconcile the two lists |
Checklist
Section titled “Checklist”Do not mark this step done until every box below is checked.
- 🤖 Workflow customized —
.github/workflows/deploy.ymlcustomized (host, branch, PHP version) and passesactionlint -shellcheck=. - 🔀 Secrets set — SSH key, ServerSync PAT (min scope), known_hosts — secret names verified, no values echoed in logs.
- 🤖 Paths symmetric —
clear_pathsandGIT_ONLY_PATHSmirror each other. - 🤖 Workflows visible —
gh workflow listshows Deploy and ServerSync; first live run proof is assigned to Phase 5 / Phase 12. - 🔀 Git hooks active — hooks are installed in the active
core.hooksPath(or.husky/if new);pre-commitblocks a staged secret,pre-pushruns Pint +actionlint -shellcheck=,post-mergewarns on new migrations.