solo-engineer-pipeline — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited solo-engineer-pipeline (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
You need to ship code to a production system, but:
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.
┌──────────────────────┐ ┌──────────────────────┐
│ 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:
_deploy.php — token-gated PHP endpoint that fetches a zipball from GitHub, extracts, and md5-copies to public_html._deploy_config.php — secrets file (NOT in git), holds deploy token + GitHub PAT.scripts/smoke.sh — HTTP smoke tests against critical paths..github/workflows/deploy.yml — push-to-main → lint → deploy → smoke → auto-rollback..github/workflows/lint.yml — PHP syntax check on push/PR..github/workflows/monitoring.yml — every 10 min, smoke test, open issue on failure.Contents: Read-only.openssl rand -hex 32 or node -e "console.log(require('crypto').randomBytes(32).toString('hex'))").shared-hosting-recon:allow_url_fopen: 1ZipArchive: AVAILABLEmemory_limit ≥ 256MB (will set 512 in script)/home/<user>/public_html writable from PHP/home/<user>/.deploy_staging) writable from PHPWhen invoked, ask the user:
OmarEltak/reg-mishkah-production)https://reg.mishkahuniversity.com)/home/reg/public_html)/home/reg/.deploy_staging — outside web root)/home/reg/_deploy_config.php — outside web root).git, the deploy script itself, large media dirs, user uploads.Then generate the artifacts.
_deploy.phpPHP 5.6 compatible. Goes at <doc_root>/_deploy.php.
Key behaviour:
hash_equals (constant-time compare).?a=deploy (default), ?a=status, ?a=info (JSON), ?a=rollback.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).ZipArchive. GitHub zipballs nest under one top-level dir; auto-detect.current_sha and previous_sha in a JSON state file. On rollback, redeploys previous_sha.flock to prevent concurrent deploys.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);
}_deploy_config.phpNOT 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';scripts/smoke.shBash. 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 0Aim for 6–10 checks. Each one < 15 seconds. Total budget: under 2 minutes.
.github/workflows/deploy.ymlname: 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.github/workflows/lint.ymlStandalone 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.github/workflows/monitoring.ymlname: 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.
/home/<user>/_deploy_config.php (outside web root).curl "https://yourdomain.com/_deploy.php?token=<TOKEN>&a=status" — should return env info, not 403, not 500.gh secret set DEPLOY_TOKEN --repo <owner>/<repo>..github/workflows/ files._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:
_deploy.php from GitHub and writes it. The bootstrap self-deletes after running.<?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";rsync, no git pull, no native parallelism. Sufficient for codebases under ~50 MB.Building this pipeline from scratch took ~3 hours of trial-and-error. The rabbit holes:
/tmp perms.~/.git-credentials.passthru('tar ...') — disabled. Need pure PHP.curl_exec — disabled. Need file_get_contents with stream context._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.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.