agentsclimarketplace

Solo engineer pipeline

Skill OmarEltak/legacy-prod-survival-kit/skills/solo-engineer-pipeline

Claude Code skills for the solo engineer who just inherited a 17-year-old production system. Audit, deploy, monitor, and report — without a DevOps team.

Install
npx -y skills add OmarEltak/legacy-prod-survival-kit --skill solo-engineer-pipeline

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Use when scaffolding a deployment pipeline for a constrained shared-hosting environment (cPanel, Plesk, etc.) with no shell access and disabled PHP exec functions. Generates a complete pipeline; PHP-based deploy endpoint, smoke tests, GitHub Actions for lint/deploy/monitoring, with auto-rollback. All free-tier. Designed for the solo engineer who needs a production-grade pipeline without a DevOps team.

SKILL.md

18.4 KB, ~4.7k tokens by cl100k_base, as published. Nobody here has run it

Solo-engineer pipeline

When to use this

You need to ship code to a production system, but:

  • The hosting is shared cPanel / Plesk / similar — no SSH, no shell access from PHP.
  • You're alone. No DevOps team to ask, no QA to write tests, no SRE to set up monitoring.
  • Standard tools (Capistrano, Deployer, GitHub Actions over SSH) don't work because of the constraints above.
  • The site is real production — students, customers, payments. Not a hobby project.
  • You need this working today, on free tools, with safety nets (rollback, smoke tests, monitoring).

This skill produces the full pipeline as deployable artifacts. You'll have a working CI/CD setup in about 30 minutes.

Run shared-hosting-recon first — its output tells you which deployment options are even possible on your specific host.

Architecture

┌──────────────────────┐        ┌──────────────────────┐
│  Local dev           │  push  │  GitHub Actions      │
│  reg-mishkah-prod    ├───────►│  ┌────────────────┐  │
└──────────────────────┘        │  │ 1. PHP lint    │  │
                                │  │ 2. Deploy      │  │
                                │  │ 3. Smoke tests │  │
                                │  │ 4. Auto-       │  │
                                │  │    rollback    │  │
                                │  └────────────────┘  │
                                └──────────┬───────────┘
                                           │ HTTPS curl
                                           ▼
                          ┌──────────────────────────────┐
                          │  yourdomain.com              │
                          │  /_deploy.php?token=...      │
                          │  ┌────────────────────────┐  │
                          │  │ Fetch zipball (GitHub) │  │
                          │  │ ZipArchive extract     │  │
                          │  │ md5-skip + exclude     │  │
                          │  │ State: SHAs persisted  │  │
                          │  └────────────────────────┘  │
                          └──────────────┬───────────────┘
                                         ▼
                              /home/user/public_html
                              (live site)

Six artifacts to produce:

  1. _deploy.php — token-gated PHP endpoint that fetches a zipball from GitHub, extracts, and md5-copies to public_html.
  2. _deploy_config.php — secrets file (NOT in git), holds deploy token + GitHub PAT.
  3. scripts/smoke.sh — HTTP smoke tests against critical paths.
  4. .github/workflows/deploy.yml — push-to-main → lint → deploy → smoke → auto-rollback.
  5. .github/workflows/lint.yml — PHP syntax check on push/PR.
  6. .github/workflows/monitoring.yml — every 10 min, smoke test, open issue on failure.

Pre-requisites

  • A GitHub repository, private or public, with the code you want to deploy.
  • A GitHub fine-grained PAT scoped to that one repo with Contents: Read-only.
  • A deploy token: 32+ random hex bytes (openssl rand -hex 32 or node -e "console.log(require('crypto').randomBytes(32).toString('hex'))").
  • Confirmed via shared-hosting-recon:
    • allow_url_fopen: 1
    • ZipArchive: AVAILABLE
    • PHP memory_limit ≥ 256MB (will set 512 in script)
    • /home/<user>/public_html writable from PHP
    • Some safe directory (e.g. /home/<user>/.deploy_staging) writable from PHP

Skill questions

When invoked, ask the user:

  1. GitHub repo URL (e.g., OmarEltak/reg-mishkah-production)
  2. Domain / base URL (e.g., https://reg.mishkahuniversity.com)
  3. Server-side document root path (e.g., /home/reg/public_html)
  4. Server-side staging dir for deploy temp files (e.g., /home/reg/.deploy_staging — outside web root)
  5. Server-side config path (e.g., /home/reg/_deploy_config.php — outside web root)
  6. PHP version (5.6, 7.x, 8.x — affects syntax in deploy script)
  7. Exclude list — directories to keep out of deploy. At minimum: .git, the deploy script itself, large media dirs, user uploads.
  8. Critical URLs for smoke tests (homepage, login, admin, checkout, etc.)

Then generate the artifacts.

Artifact 1 — _deploy.php

PHP 5.6 compatible. Goes at <doc_root>/_deploy.php.

Key behaviour:

  • Loads config from outside web root.
  • Gates on token via hash_equals (constant-time compare).
  • Supports ?a=deploy (default), ?a=status, ?a=info (JSON), ?a=rollback.
  • On deploy: fetches https://api.github.com/repos/<owner>/<repo>/zipball/<ref> with PAT in Authorization: Bearer header, via file_get_contents + stream_context_create (no curl needed).
  • Extracts via ZipArchive. GitHub zipballs nest under one top-level dir; auto-detect.
  • Recursive copy to doc root, skipping files in exclude list, md5-skipping unchanged files.
  • Records current_sha and previous_sha in a JSON state file. On rollback, redeploys previous_sha.
  • Logs every step to a per-run log file. Keeps last 5 logs.
  • File-locks via flock to prevent concurrent deploys.
  • Self-excludes — does not overwrite itself during deploy (avoids the "I'm rewriting myself mid-execution" problem).

Full template: see examples/_deploy.php in this kit. About 280 lines.

Critical implementation details (commonly missed):

// PHP 5.6 — no null coalescing operator
$token = isset($_GET['token']) ? $_GET['token'] : '';

// Constant-time token compare (prevents timing attacks)
if (!hash_equals($DEPLOY_TOKEN, $token)) {
    http_response_code(403);
    die("forbidden\n");
}

// HTTP context with PAT, NO curl_exec
$ctx = stream_context_create(array(
    'http' => array(
        'method' => 'GET',
        'header' => "Authorization: Bearer $GITHUB_PAT\r\n" .
                    "User-Agent: deploy/1.0\r\n" .
                    "Accept: application/vnd.github+json\r\n",
        'follow_location' => 1,    // critical — GitHub redirects to AWS
        'max_redirects'   => 5,
        'timeout'         => 60,
        'ignore_errors'   => true,  // capture body even on non-2xx
    ),
    'ssl' => array(
        'verify_peer'      => true,
        'verify_peer_name' => true,
    ),
));
$zip_data = @file_get_contents($apiUrl, false, $ctx);

// Inspect status from $http_response_header (set automatically by file_get_contents)
$http_status = 0;
foreach ($http_response_header as $h) {
    if (preg_match('#^HTTP/\S+\s+(\d+)#', $h, $m)) $http_status = (int)$m[1];
}

// Extract via ZipArchive (PharData also works for tar.gz — pick one)
$zip = new ZipArchive();
$zip->open($zip_path);
$zip->extractTo($extract_dir);
$zip->close();

// GitHub zipballs are nested in one dir
$tops = glob($extract_dir . '/*', GLOB_ONLYDIR);
$src_root = $tops[0];

// Capture commit SHA from the dir name (format: <owner>-<repo>-<sha>)
preg_match('/-([0-9a-f]{7,40})$/', basename($src_root), $m);
$deployed_sha = $m[1];

// Recursive copy with md5 skip
$it = new RecursiveIteratorIterator(
    new RecursiveDirectoryIterator($src_root, RecursiveDirectoryIterator::SKIP_DOTS),
    RecursiveIteratorIterator::SELF_FIRST
);
foreach ($it as $info) {
    $rel = str_replace('\\', '/', substr($info->getPathname(), strlen($src_root) + 1));
    if (path_excluded($rel)) continue;
    $dest = $deploy_path . '/' . $rel;
    if ($info->isDir()) {
        if (!is_dir($dest)) @mkdir($dest, 0755, true);
        continue;
    }
    // md5 skip — only copy if content actually different
    if (is_file($dest) &&
        filesize($dest) === filesize($info->getPathname()) &&
        @md5_file($dest) === @md5_file($info->getPathname())) {
        continue;
    }
    @mkdir(dirname($dest), 0755, true);
    @copy($info->getPathname(), $dest);
    @chmod($dest, 0644);
}

Artifact 2 — _deploy_config.php

NOT in git. Lives outside web root. Generated locally and uploaded once via cPanel File Manager.

<?php
// /home/<user>/_deploy_config.php — secrets, NOT in git
$DEPLOY_TOKEN = '<32-char hex>';
$GITHUB_PAT   = 'github_pat_...';
$GITHUB_OWNER = '<owner>';
$GITHUB_REPO  = '<repo>';
$DEPLOY_PATH  = '/home/<user>/public_html';
$STAGING_DIR  = '/home/<user>/.deploy_staging';

Artifact 3 — scripts/smoke.sh

Bash. Curls a list of URLs, checks expected HTTP status. Exits non-zero if any fail.

#!/usr/bin/env bash
set -u

BASE="${1:-https://yourdomain.com}"
PASS=0
FAIL=0

check() {
  local path="$1" expected="$2" desc="${3:-}"
  local actual
  actual="$(curl -sS -o /dev/null -w '%{http_code}' --max-time 15 "$BASE$path" 2>&1 || echo CURL_ERR)"
  if [[ "$actual" == "$expected" ]]; then
    printf '  [PASS] %-40s %s%s\n' "$path" "$actual" "${desc:+ ($desc)}"
    PASS=$((PASS + 1))
  else
    printf '  [FAIL] %-40s expected=%s got=%s%s\n' "$path" "$expected" "$actual" "${desc:+ ($desc)}"
    FAIL=$((FAIL + 1))
  fi
}

echo "Smoke tests against: $BASE"
check "/"           "302" "homepage redirects"
check "/login.php"  "200" "login page"
check "/admin/"     "401" "admin auth-required"
# ... add 5–8 more for your critical paths

echo "Results: $PASS passed, $FAIL failed"
[[ $FAIL -gt 0 ]] && exit 1 || exit 0

Aim for 6–10 checks. Each one < 15 seconds. Total budget: under 2 minutes.

Artifact 4 — .github/workflows/deploy.yml

name: Deploy to Production
on:
  push:
    branches: [main]
  workflow_dispatch:
    inputs:
      ref: {description: 'Git ref', required: false, default: 'main'}

concurrency:
  group: prod-deploy
  cancel-in-progress: false

jobs:
  deploy:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    env:
      BASE_URL: https://yourdomain.com
      DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }

      - uses: shivammathur/setup-php@v2
        with: { php-version: '5.6', tools: none }

      - name: PHP lint
        run: |
          set -e
          while IFS= read -r f; do
            out="$(php -l -- "$f" 2>&1)"
            case "$out" in
              "No syntax errors detected"*) ;;
              *) echo "SYNTAX ERROR in $f"; echo "$out"; exit 1 ;;
            esac
          done < <(git ls-files '*.php')

      - name: Trigger deploy
        id: deploy
        run: |
          curl -sS --max-time 300 \
            "$BASE_URL/_deploy.php?token=$DEPLOY_TOKEN&ref=${{ github.event.inputs.ref || 'main' }}" \
            | tee deploy.log

      - name: Wait
        run: sleep 5

      - name: Smoke tests
        id: smoke
        run: bash scripts/smoke.sh "$BASE_URL"

      - name: AUTO-ROLLBACK on smoke failure
        if: failure() && steps.smoke.outcome == 'failure'
        run: |
          echo "::error::Smoke failed. Rolling back."
          curl -sS --max-time 300 "$BASE_URL/_deploy.php?token=$DEPLOY_TOKEN&a=rollback" | tee rollback.log
          sleep 5
          bash scripts/smoke.sh "$BASE_URL" || {
            echo "::error::CRITICAL: rollback didn't fix it. Manual intervention required."
            exit 1
          }
          exit 1   # mark run failed even though we recovered, so you notice

      - if: always()
        uses: actions/upload-artifact@v4
        with:
          name: deploy-logs-${{ github.run_id }}
          path: |
            deploy.log
            rollback.log
          if-no-files-found: ignore
          retention-days: 14

Artifact 5 — .github/workflows/lint.yml

Standalone lint that also runs on PR / dev branch (catches errors before merge):

name: PHP Lint
on:
  push: { branches: [main, dev] }
  pull_request: { branches: [main] }

jobs:
  php-syntax-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with: { php-version: '5.6', tools: none }
      - name: Lint all .php files
        run: |
          set -e
          fail=0
          while IFS= read -r f; do
            out="$(php -l -- "$f" 2>&1)"
            case "$out" in
              "No syntax errors detected"*) ;;
              *) echo "SYNTAX ERROR in $f"; echo "$out"; fail=1 ;;
            esac
          done < <(git ls-files '*.php')
          [[ $fail -eq 0 ]] || exit 1

Artifact 6 — .github/workflows/monitoring.yml

name: Production Monitoring
on:
  schedule: [{cron: '*/10 * * * *'}]   # every 10 min (5 is GH minimum, 10 saves Actions minutes)
  workflow_dispatch:

concurrency:
  group: monitoring
  cancel-in-progress: false

jobs:
  smoke:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    steps:
      - uses: actions/checkout@v4
      - run: bash scripts/smoke.sh   # uses production URL hardcoded in script

      - name: Open issue on failure
        if: failure()
        uses: actions/github-script@v7
        with:
          script: |
            const open = await github.rest.issues.listForRepo({
              owner: context.repo.owner, repo: context.repo.repo,
              state: 'open', labels: 'monitoring', per_page: 5,
            });
            if (open.data.length > 0) {
              await github.rest.issues.createComment({
                owner: context.repo.owner, repo: context.repo.repo,
                issue_number: open.data[0].number,
                body: `Smoke tests failed at ${new Date().toISOString()}.\nRun: ${context.payload.repository.html_url}/actions/runs/${context.runId}`,
              });
            } else {
              await github.rest.issues.create({
                owner: context.repo.owner, repo: context.repo.repo,
                title: `Site monitoring: smoke failing (${new Date().toISOString()})`,
                labels: ['monitoring', 'urgent'],
                body: `Production smoke tests are failing.\n\nRun: ${context.payload.repository.html_url}/actions/runs/${context.runId}\n\nQuick triage:\n1. Check the site in a browser.\n2. Check ${{ inputs.deploy_url || 'deploy info' }} (\`?a=info\`).\n3. If a recent deploy is the cause: \`?a=rollback\`.`,
              });
            }

GitHub Actions free tier on private repos: 2,000 min/month. This monitoring uses ~25 min/day = ~750/month. Comfortable headroom.

Setup steps (after artifacts are generated)

  1. Generate token & PAT.
  2. Create _deploy_config.php with real values; upload via cPanel File Manager to /home/<user>/_deploy_config.php (outside web root).
  3. Upload _deploy.php to web root via cPanel.
  4. Test status: curl "https://yourdomain.com/_deploy.php?token=<TOKEN>&a=status" — should return env info, not 403, not 500.
  5. Add token to GitHub Actions secrets: gh secret set DEPLOY_TOKEN --repo <owner>/<repo>.
  6. Commit & push the .github/workflows/ files.
  7. First test deploy: push a tiny change. Watch the action run. Verify on prod.
  8. Set up rotation reminder: the GitHub PAT expires in 90 days. Calendar reminder for day 80.

Self-update problem (and the bootstrap pattern)

_deploy.php is in your git repo, but excluded from the deploy operation (otherwise the script overwrites itself mid-execution — usually fine but occasionally weird).

To update _deploy.php itself:

  1. Push the new version to the repo.
  2. Upload a tiny bootstrap PHP that fetches the latest _deploy.php from GitHub and writes it. The bootstrap self-deletes after running.
  3. Run the bootstrap once. Future deploys benefit from the new logic.
<?php
// _deploy_bootstrap.php — one-time self-updater. Self-deletes.
$config = '/home/<user>/_deploy_config.php';
require $config;
$k = isset($_GET['k']) ? $_GET['k'] : '';
if (!hash_equals($DEPLOY_TOKEN, $k)) { http_response_code(403); die('forbidden'); }
header('Content-Type: text/plain');

$url = "https://api.github.com/repos/$GITHUB_OWNER/$GITHUB_REPO/contents/_deploy.php?ref=main";
$ctx = stream_context_create(array(
    'http' => array(
        'method' => 'GET',
        'header' => "Authorization: Bearer $GITHUB_PAT\r\nUser-Agent: bootstrap\r\nAccept: application/vnd.github.v3.raw\r\n",
        'follow_location' => 1, 'max_redirects' => 5, 'timeout' => 30,
    ),
    'ssl' => array('verify_peer' => true, 'verify_peer_name' => true),
));
$content = @file_get_contents($url, false, $ctx);
if ($content === false || strlen($content) < 500) { die("fetch failed\n"); }
file_put_contents('/home/<user>/public_html/_deploy.php', $content);
@chmod('/home/<user>/public_html/_deploy.php', 0644);
echo "updated\n";
@unlink(__FILE__);
echo "self-deleted\n";

Limitations

  • PHP-only deploy — no shell-out, so no rsync, no git pull, no native parallelism. Sufficient for codebases under ~50 MB.
  • No --delete semantics — files removed from git stay on prod until manually deleted. This is intentional (safety) but means orphan cleanup is a separate manual step.
  • Sequential, single-process — one deploy at a time (file lock). Multiple developers pushing rapidly will queue.
  • GitHub-specific — uses the GitHub zipball API. Adapting to GitLab / Bitbucket / Gitea requires changing one URL and the auth header format.

Why this skill exists

Building this pipeline from scratch took ~3 hours of trial-and-error. The rabbit holes:

  1. cPanel native deploy — broken on Codero by /tmp perms.
  2. SSH key with passphrase — cPanel forces passphrase, can't be entered non-interactively.
  3. HTTPS in clone URL — cPanel rejects credentials in URL. Workaround: write ~/.git-credentials.
  4. passthru('tar ...') — disabled. Need pure PHP.
  5. curl_exec — disabled. Need file_get_contents with stream context.
  6. _deploy.php overwriting itself during deploy — needed exclude-self logic.

This skill compresses all of that into a 30-minute setup. The pain is real and recurring — every solo engineer on shared hosting hits the same walls.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.