Skip to content
prod 352bb92
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 workflows, set the secrets, keep clear_pathsGIT_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:

  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.
  2. Configure GitHub Secrets — Set these under Repo → Settings → Secrets and variables → Actions:.
  3. Keep clear_pathsGIT_ONLY_PATHS symmetric — ServerSync pushes server-side changes back to Git. Two lists must mirror each other or you get drift or accidental deletion:.
  4. Validate the YAML before pushing — Catch syntax and expression errors locally so a bad workflow never lands on develop (the default branch).
  5. Commit and publish the workflow source — Put the workflow files on develop so GitHub can see them. Do not require a live deploy run in Phase 4; Phase 5 triggers and verifies the first non-production run.
  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.
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 "$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.yml exists with every <PLACEHOLDER> (host, branch, PHP version, deploy command) replaced; the PHP version matches bin/php in 1 · Deployer. Phase 5 proves the selected non-production command live.

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.

SecretPurposeHow to set safely
DEPLOYER_SSH_KEYPrivate SSH key the runner uses to reach the servergh secret set DEPLOYER_SSH_KEY --body-file <key-file> or browser paste
SERVERSYNC_PAT / GIT_AUTO_COMMIT_PATFine-grained PAT so ServerSync can push/open PRsBrowser UI or secure prompt only; never -b "<pat>" in shell history
KNOWN_HOSTSServer host key, to avoid interactive promptsssh-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_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.

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:

Terminal window
Admin-Local Tools .cursor .claude .agent .ai .cursorignore
.github tests .editorconfig phpunit.xml .phpunit.result.cache
README.md Zaj-PROJECT.md Zaj-PROGRESS.md Zaj-BACKLOG.md Zaj-CHANGELOG.md Zaj-CUSTOMIZATIONS.md
CHANGELOG.md _CHANGELOG.md _CHANGELOG-PUBLIC.md
CLAUDE.md CLAUDE.local.md GEMINI.md AGENTS.md ONBOARDING.md DECISIONS.md
deploy.php .env.tpl .env.example .mcp.json boost.json
.temp assets

Zaj-* files and 90-DevLog/ stay in Git for humans and agents, but they are not production runtime files.

  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 develop (the default branch).

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

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.

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

    Terminal window
    git add .github/workflows/deploy.yml deploy.php
    git 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.
  2. 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.

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.

  1. Detect or create the active hook directory.

    Terminal window
    HOOK_DIR="$(git config core.hooksPath || true)"
    if [ -z "$HOOK_DIR" ]; then
    npm install --save-dev husky
    npx husky init
    HOOK_DIR=".husky"
    fi
    mkdir -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.
  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
    # $HOOK_DIR/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.

    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; then
    command -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 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.

    Terminal window
    # $HOOK_DIR/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 missing from develop (the default branch), or bad YAMLKeep the workflow on develop and 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 + pull-request write scopes, update secret; verify with feedback_api_scope_verification.md
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 -shellcheck=.
  • 🔀 Secrets set — SSH key, ServerSync PAT (min scope), known_hosts — secret names verified, no values echoed in logs.
  • 🤖 Paths symmetricclear_paths and GIT_ONLY_PATHS mirror each other.
  • 🤖 Workflows visiblegh workflow list shows 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-commit blocks a staged secret, pre-push runs Pint + actionlint -shellcheck=, post-merge warns on new migrations.