Skip to content
prod e051e98
Browse

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_pathsGIT_ONLY_PATHS symmetric, lint, and confirm the run.

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 --> Push

Create .github/workflows/deploy.yml. The CodeCanyon kit ships a template with placeholders — customize hostnames, branches, and paths rather than writing from scratch.

  1. Write the deploy workflow from the kit template.

    name: Deploy
    on:
    push:
    branches: [main] # production
    # add 'staging' for a staging deploy job
    jobs:
    deploy:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: shivammathur/setup-php@v2
    with:
    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.0
    with:
    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 key
    run: |
    mkdir -p ~/.ssh && chmod 700 ~/.ssh
    echo "${{ secrets.KNOWN_HOSTS }}" >> ~/.ssh/known_hosts
    chmod 600 ~/.ssh/known_hosts
    - run: composer install --no-dev --optimize-autoloader
    - name: Deploy
    run: vendor/bin/dep deploy production
    env:
    # ServerSync pushes generated artifacts back to the repo with this PAT.
    SERVERSYNC_PAT: ${{ secrets.SERVERSYNC_PAT }}
    • .github/workflows/deploy.yml exists with every <PLACEHOLDER> (host, branch, PHP version) replaced; the PHP version matches bin/php in 1 · Deployer and composer.json.

Set these under Repo → Settings → Secrets and variables → Actions:

SecretPurpose
DEPLOYER_SSH_KEYPrivate SSH key the runner uses to reach the server
SERVERSYNC_PATFine-grained PAT (repo contents: write) so ServerSync can push back
KNOWN_HOSTSServer 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_pathsGIT_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 (in deploy.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.

  1. Audit the two lists side by side.

    Terminal window
    grep -A20 "clear_paths" deploy.php
    grep "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_paths also appears in GIT_ONLY_PATHS, and vice-versa.
  2. Patch a protected security hook without bypassing the secret guard.

    Do not use perl -i -pe or 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.patch
    chmod +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.

Catch syntax and expression errors locally so a bad workflow never lands on the default branch.

  1. 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"
    • actionlint passes (or the YAML parse check prints OK).

Push to the default branch and watch the Actions run reach the deploy step.

  1. Commit and push the workflow + deploy.php.

    Terminal window
    git add .github/workflows/deploy.yml deploy.php
    git 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.
  2. Confirm the run in Repo → Actions — verify the workflow appears and the run reaches the deploy step. A green run that flips the current symlink means CI deploy works end-to-end.

    • ✅ A green Actions run reaches the deploy/symlink step.

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.

  1. 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 install re-activates the hooks on any clone.
  2. pre-commit — block secrets before they are committed. Scan the staged diff for real credential patterns (Stripe sk_/pk_, AWS AKIA…, 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 staged
    PATTERN='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; do
    if git diff --cached -U0 -- "$f" | grep -E '^\+' | grep -qE "$PATTERN"; then
    echo "BLOCKED: a real credential value is in the staged diff ($f). Move it to an env var; rotate it if it was real." >&2
    exit 1
    fi
    done
    • ✅ Staging a fake sk_live_… then committing is rejected; a clean commit passes.
  3. pre-push — lint before the push. Run Pint on changed files and require actionlint when 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; then
    command -v actionlint >/dev/null || { echo "actionlint required — brew install actionlint" >&2; exit 1; }
    actionlint
    fi
    • ✅ A push with a Pint violation or a broken workflow is blocked locally; missing actionlint fails the hook when workflows are present.
  4. post-merge — warn when a pull brings new migrations. After git 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 SSH artisan 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.
SymptomCauseFix
Workflow not listed in ActionsFile not on the default branch / bad YAMLPush to default branch; run actionlint
Permission denied (publickey)SSH key/secret mismatchRe-add public key to server; recheck DEPLOYER_SSH_KEY
ServerSync push 403PAT scope/expiryReissue PAT with contents: write, update secret
Files deleted after syncclear_paths/GIT_ONLY_PATHS asymmetryReconcile the two lists

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

  • 🤖 Workflow customized.github/workflows/deploy.yml customized (host, branch, PHP version) and passes actionlint.
  • 👤 Secrets set — SSH key, ServerSync PAT (min scope), known_hosts — none echoed in logs.
  • 🤖 Paths symmetricclear_paths and GIT_ONLY_PATHS mirror each other.
  • 🔀 Green run — a pushed commit triggers a green Actions run that reaches the deploy/symlink step.
  • 🔀 Git hooks active — Husky installed; pre-commit blocks a staged secret, pre-push runs Pint + actionlint, post-merge warns on new migrations.