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 workflow, set the secrets, keep clear_paths ↔ GIT_ONLY_PATHS symmetric, lint, and confirm the run.
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 productionenv:# 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) replaced; the PHP version matchesbin/phpin 1 · Deployer andcomposer.json.
- ✅
2. Configure GitHub Secrets
Section titled “2. Configure GitHub Secrets”Set these under Repo → Settings → Secrets and variables → Actions:
| Secret | Purpose |
|---|---|
DEPLOYER_SSH_KEY | Private SSH key the runner uses to reach the server |
SERVERSYNC_PAT | Fine-grained PAT (repo contents: write) so ServerSync can push back |
KNOWN_HOSTS | Server host key, to avoid interactive prompts |
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.
-
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 the default branch.
-
Lint (or parse) the workflow.
Terminal window actionlint .github/workflows/deploy.yml # lints syntax + expressions# 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, push, and confirm the run
Section titled “5. Commit, push, and confirm the run”Push to the default branch and watch the Actions run reach the deploy step.
-
Commit and push the workflow +
deploy.php.Terminal window git add .github/workflows/deploy.yml deploy.phpgit commit -m "ci: GitHub Actions deploy + ServerSync"git push origin main# Expected: the commit pushes to the default branch and triggers the Deploy workflow- ✅ The push lands on the default branch and triggers a workflow run.
-
Confirm the run in Repo → Actions — verify the workflow appears and the run reaches the deploy step. A green run that flips the
currentsymlink means CI deploy works end-to-end.- ✅ A green Actions run reaches the deploy/symlink step.
6. Mirror the gates locally — git hooks
Section titled “6. 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. Three hooks pull their weight.
-
Install Husky and enable hooks.
Terminal window npm install --save-dev husky && npx husky init# Expected: a .husky/ directory + a "prepare": "husky" script added to package.json- ✅
.husky/exists;npm installre-activates the hooks on any clone.
- ✅
-
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 # .husky/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..husky/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; }actionlintfi- ✅ 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..husky/post-merge changed=$(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 not on the default branch / bad YAML | Push to default branch; 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: write, update secret |
| 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. - 👤 Secrets set — SSH key, ServerSync PAT (min scope), known_hosts — none echoed in logs.
- 🤖 Paths symmetric —
clear_pathsandGIT_ONLY_PATHSmirror each other. - 🔀 Green run — a pushed commit triggers a green Actions run that reaches the deploy/symlink step.
- 🔀 Git hooks active — Husky installed;
pre-commitblocks a staged secret,pre-pushruns Pint +actionlint,post-mergewarns on new migrations.