Skip to content
Back to skills

Deploy

ASecurity

GitHub Actions deployment: workflows, releases, GHCR, CI/CD with safety gates. Triggers: deploy, release, workflow.

  • 32 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added May 27, 2026
data-aigoshellbashdockergitapidatabasebackendci/cdsecurity

Works with

  • claude code
  • terminal
  • cli
  • api
  • mcp

Security analysis

A92/100
  • mediumUses curl or wget to download content

Pro scans all 9 files and shows the line behind each finding

Scanned October 1, 2026

npx -y skills add kochetkov-ma/claude-brewcode --skill deploy --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Deploy?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Deploy
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/kochetkov-ma-deploy/badge)](https://www.skillsdirectory.com/skills/kochetkov-ma-deploy)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: deploy
description: "GitHub Actions deployment: workflows, releases, GHCR, CI/CD with safety gates. Triggers: deploy, release, workflow."
user-invocable: true
disable-model-invocation: true
argument-hint: "[prompt] [setup|create|release|deploy|monitor|update-agent]"
allowed-tools: [Read, Write, Edit, Bash, Agent, AskUserQuestion, Glob, Grep]
model: opus
---

[DICT: P=Phase, EXEC=EXECUTE using Bash tool, AUQ=AskUserQuestion, WF=workflow, CFG=config, REF=references, GH=gh CLI, TPL=template]

# GitHub Actions Deployment

> Manage GitHub Actions — WFs, releases, GHCR, CI/CD with safety gates + persistent CFG.

## Prompt contract

Position 1 of `$ARGUMENTS` is a **free-form prompt** (RU/EN) — modes and flags are optional and may
follow in any order. Nobody types keys: resolve mode + scope FROM the prompt.

1. Strip flags. An explicit mode token anywhere wins outright, no scoring.
2. Else score modes by distinct whole-word keyword hits (table in P0). Highest unique score wins.
   Scored tie with monitor -> read-only monitor; other ties -> ask before actions.
   All zero -> `setup` (no GH CFG) or `monitor` (GH CFG exists).
3. Empty arguments -> `setup`/`monitor` per the rule above; ask ONE scoping `AskUserQuestion` only
   when the answer changes what gets written. `monitor`/`check` ask nothing.
4. Outcome-changing ambiguity (e.g. `release` vs `deploy`) -> ONE `AskUserQuestion` (max 4
   questions) BEFORE any work — P4/P5 confirmation gates cover the destructive cases separately.
5. Prose that is not a mode/id/path is still input: extract the id, path or target from it.

Then print this block ONCE, before the first mutation (P0 is its home for mutating modes;
`monitor` prints it immediately before its P6 report):

```
PLAN — brewtools:deploy
INPUT:  <arguments verbatim, or "(empty)">
MODE:   <resolved> — <explicit | matched keyword: X | default>
SCOPE:  <resolved paths / target / level / flags>
DO:     <2-5 imperative bullets>
RESULT: <what the user ends up holding>
```

Labels/plan values are English; INPUT remains verbatim.

<instructions>

## Robustness Rules (MANDATORY — ALL phases)

### Fail-Fast
| Rule | Scope |
|------|-------|
| Every Bash call reports OK/FAILED and preserves non-zero status; `&& echo "OK ..." \|\| { rc=$?; echo "FAILED ..." >&2; exit "$rc"; }` | ALL scripts; probe/no-data exceptions stay explicit |
| On FAILED: stop phase, report error, !=retry same command blindly | ALL |
| Max 2 retries per failed op. After 2nd — report + stop | ALL |
| Script exits non-zero: read stderr, diagnose, fix root cause, retry ONCE | Scripts |

### Loop Protection
| Rule | Limit |
|------|-------|
| `gh auth` attempts | max 2, then AUQ |
| GH commands per phase | max 5 |
| AUQ per phase | max 3 |
| update-agent mode WFs per run | max 5 |

### Timeouts — always via `ght`, never bare `timeout`

GNU `timeout` is Homebrew-only on macOS, this skill's primary local platform. `timeout 30 gh ...`
on a stock Mac exits **127 before gh ever runs**, which used to be reported as "API unavailable" or
`FAILED trigger` for a dispatch that was never attempted. Every bounded call therefore sources the
helper first — it enforces the bound with `timeout`, `gtimeout` or a built-in bash watchdog:

```bash
. "${CLAUDE_SKILL_DIR}/scripts/lib/deploy-common.sh"   # provides ght, ght_backend, ght_reason
```

| Op | Bound | On non-zero |
|----|-------|-------------|
| GH CLI cmds | `ght 30 gh ...` | classify with `ght_reason $?`: `timeout` / `no_tool` / `failed` — never one sentinel for all three |
| `gh run watch` | `ght 900 gh run watch <id> --exit-status` | report the run URL + the last failing job |
| Entire invocation | max 15 GH calls total | stop, report progress, suggest manual |

> `ght_reason 127` = `no_tool` means **gh itself is missing** — say that, !=report a GitHub outage.
> `ght_backend` prints which watchdog is in use; include it when a bound is what failed.

### Fallback Strategy
1. Report exact error: script name, exit code, stderr
2. Attempt same op manually (inline Bash) — scripts are helpers, not gatekeepers
3. If manual also fails → report both + AUQ what to do
4. !=silently swallow errors or continue with stale/missing data

| Failed script | Manual alternative |
|---------------|--------------------|
| detect-mode.sh | parse $ARGUMENTS (keyword match) |
| gh-env-check.sh | `gh auth status`, `gh repo view --json name`, `gh secret list` |
| workflow-discover.sh | `ls .github/workflows/`, `gh workflow list`, `gh run list -L 5` |
| deploy-local-ops.sh | Read/Edit CLAUDE.local.md directly |

### Error Reporting (MANDATORY)
On ANY failure — before stopping or AUQ:
```
SCRIPT_ERROR: <name>
EXIT_CODE: <code>
STDERR: <message>
PHASE: <current>
ACTION: <attempted>
FALLBACK: <next OR "asking user">
```

### Delegation (any `Agent` spawn, e.g. `deploy-admin`)

Main owns every spawn/acceptance; delegates never nest and return unresolved decisions. One SA =
ONE deliverable/~<=5 files/~<=10 steps, ONE repo/ONE environment. Bigger MUST split into N tasks
(per repo/environment), spawned in ONE message.

Every spawn prompt MUST carry:

| Field | Content |
|-------|---------|
| GOAL | overall task/purpose beyond editing |
| ROLE | owned responsibility + forbidden changes |
| SCOPE | exact paths/commands in/out of bounds |
| CONTEXT | prior work/owners, parallel work; relevant to this agent only |
| CONSUMER | next consumer + required result shape |
| DONE | acceptance criteria + exact return format |

A bare one-line task is never enough.

**Safety gates are NOT delegable.** Ordinary SAs lose `AskUserQuestion` even if declared;
conversation forks skip filters, skill `context: fork` does not. Capability never bypasses this
role's exact approval contract: P4 Step 3/P5 Step 4 stay in main. Delegates finish non-destructive
work, execute no unapproved mutation and return:

```markdown
## APPROVAL REQUIRED
### A1
COMMAND:      <exact command, one line>
HOST:         <local | user@host>
EFFECT:       <what changes, irreversibly or remotely>
ROLLBACK:     <exact reverse command, or NONE>
EVIDENCE:     <why this is the right command — file:line / run URL / probe output>
PRECONDITION: <what must still hold at execution time>
```

One envelope per destructive operation, executing none of them. This skill shows the envelopes to
the user, and re-spawns with `APPROVED: A1 A3` in the prompt. **An explicit approval token in the
incoming prompt is the only authorization a subagent may act on.** Destructive = irreversible or
touching a remote/shared system: force-push, tag delete, deploy/rollback, service restart,
`docker system prune`, remote `ssh` mutations, secret rotation.
Recheck PRECONDITION/exact id+command at execution; no file/agent-message approval or changed target.
Checkpoint each completed step; CC 2.1.246+ maxTurns returns partial output, external effects persist.
Main checks that marker and checkpoint before accepting completion/continuing.

## P0: Mode Detection (MANDATORY FIRST STEP)

EXEC: replace PROMPT_HERE with the whole rendered prompt as ONE safely shell-quoted data
argument; never eval/interpolate it as code or rely on shell `$ARGUMENTS` in the Bash fence.
```bash
bash "${CLAUDE_SKILL_DIR}/scripts/detect-mode.sh" "PROMPT_HERE"
```
Optional second argument is a previously resolved canonical mode. Parse exactly one MODE line;
conflicting explicit modes/mutating highest-score ties return `MODE: ask`, which main resolves
with its existing material-choice question before actions, then passes the selected mode on rerun.
Flags/values, paths and key=value scopes remain ARGS data. Unknown nouns never select deploy:
zero hits/empty -> setup without GitHub Config, monitor when that actual section exists. A scored
tie involving read-only monitor selects monitor (e.g. check runs), never automatic setup writes.
Output: `ARGS: [...] MODE: [...]`

| Mode | EN keywords | RU keywords | Mutates? |
|------|-------------|--------------|----------|
| `setup` | *(empty, no GH CFG)*, `setup`, `set up`, `check`, `prerequisites`, `init` | `настройка`, `подготовь`, `проверь настройку` | yes |
| `create` | `create`, `new workflow`, `add workflow` | `создай workflow`, `новый workflow`, `добавь workflow` | yes |
| `release` | `release`, `bump`, `version`, `tag`, `publish` | `релиз`, `версия`, `тег`, `опубликуй` | yes |
| `deploy` | `deploy`, `trigger`, `dispatch`, `run workflow` | `деплой`, `разверни`, `запусти workflow` | yes |
| `monitor` | *(empty, GH CFG exists)*, `monitor`, `watch`, `status`, `check runs`, `logs` | `статус`, `мониторь`, `посмотри логи` | no |
| `update-agent` | `update agent`, `refresh`, `rescan` | `обнови агента`, `пересканируй` | yes |

Print the PLAN block from `## Prompt contract` here (`monitor` prints it before its report
instead), then proceed to P1.

## P1: Environment + CFG Check (ALL modes before branching)

EXEC:
```bash
bash "${CLAUDE_SKILL_DIR}/scripts/gh-env-check.sh" && echo "OK env-check" || { rc=$?; echo "FAILED env-check" >&2; exit "$rc"; }
```
> STOP if FAILED — fix GH env before continuing.

Parse key=value: GH CLI version, auth status, repo info, secrets count.

### Load Existing CFG
EXEC:
```bash
bash "${CLAUDE_SKILL_DIR}/scripts/deploy-local-ops.sh" list 2>/dev/null || echo "NO_CONFIG"
```
Read CLAUDE.local.md — check `## GitHub Config` + `## Workflows:` sections.

| Condition | Action |
|-----------|--------|
| NO_CONFIG + mode=setup | GOTO P2 |
| NO_CONFIG + mode=create/release/deploy | GOTO P2 (need CFG first) |
| NO_CONFIG + mode=monitor | report missing CFG + setup next step; remain read-only, never auto-run setup |
| CFG exists + mode=setup | report existing CFG, AUQ re-setup? |
| CFG exists + mode=create | GOTO P3 |
| CFG exists + mode=release | GOTO P4 |
| CFG exists + mode=deploy | GOTO P5 |
| CFG exists + mode=monitor | GOTO P6 |
| mode=update-agent | GOTO Mode: update-agent |

## P2: Setup

### Step 1: Verify GH Auth
EXEC:
```bash
gh auth status 2>&1 && echo "OK auth" || { rc=$?; echo "FAILED auth" >&2; exit "$rc"; }
```
If FAILED → instruct: `gh auth login`

### Step 2: Detect Repo
EXEC:
```bash
gh repo view --json owner,name,url,defaultBranchRef,visibility 2>/dev/null && echo "OK repo" || { rc=$?; echo "FAILED repo" >&2; exit "$rc"; }
```

### Step 3: Check Secrets
EXEC:
```bash
gh secret list 2>/dev/null && echo "OK secrets" || { rc=$?; echo "FAILED secrets" >&2; exit "$rc"; }
```

### Step 4: Check SSH Integration
EXEC:
```bash
grep -q "^## SSH Servers" CLAUDE.local.md 2>/dev/null && echo "SSH_SERVERS=exists" || echo "SSH_SERVERS=missing"
```

### Step 5: Discover WFs
EXEC:
```bash
bash "${CLAUDE_SKILL_DIR}/scripts/workflow-discover.sh" && echo "OK discovery" || { rc=$?; echo "FAILED discovery" >&2; exit "$rc"; }
```

### Step 6: Persist CFG
EXEC:
```bash
bash "${CLAUDE_SKILL_DIR}/scripts/deploy-local-ops.sh" add-github "OWNER" "REPO" "ghcr.io" && echo "OK add-github" || { rc=$?; echo "FAILED add-github" >&2; exit "$rc"; }
```
Replace OWNER + REPO with values from Step 2.
EXEC:
```bash
bash "${CLAUDE_SKILL_DIR}/scripts/deploy-local-ops.sh" add-workflows && echo "OK add-workflows" || { rc=$?; echo "FAILED add-workflows" >&2; exit "$rc"; }
```

### Step 7: Gitignore
EXEC:
```bash
grep -q "CLAUDE.local.md" .gitignore 2>/dev/null && echo "EXISTS" || (echo "CLAUDE.local.md" >> .gitignore && echo "ADDED")
```

### Step 8: Generate deploy-admin Agent
EXEC:
```bash
cat "${CLAUDE_SKILL_DIR}/templates/deploy-admin-agent.md.template"
```
Resolve the metadata stamp (never hardcode a version). EXEC:
```bash
SD="${CLAUDE_SKILL_DIR}"
if [ -n "$SD" ] && [ -f "$SD/../../.claude-plugin/plugin.json" ]; then BT_ROOT=$(cd "$SD/../.." && pwd); else BT_ROOT=$(ls -d ~/.claude/plugins/cache/claude-brewcode/brewtools/*/ 2>/dev/null | sort -V | tail -1 | sed 's:/*$::'); fi
[ -n "$BT_ROOT" ] || { echo "ERROR: cannot locate brewtools plugin root -- install/update brewtools first."; exit 1; }
PV=$(jq -r '.version // empty' "$BT_ROOT/.claude-plugin/plugin.json" 2>/dev/null || true)
PV=${PV:-$(basename "$BT_ROOT")}
echo "PLUGIN_VERSION=$PV LAST_UPDATED=$(date +%F)"
```
> Use bare `${CLAUDE_SKILL_DIR}`: prompt substitution, not a promised shell env var. Historical
> 2.1.226 used `replace(/\$\{CLAUDE_SKILL_DIR\}/g, dirname(skillPath))`/`replaceAll`; brace modifiers
> such as `:-fallback` are not recognized and reach the shell (fallback wins when unset).
> `CLAUDE_PLUGIN_ROOT` is supplied to hook/MCP processes and substituted in plugin prompt text,
> not guaranteed as a Bash-tool env var. CSD works in cache installs AND `--plugin-dir` dev runs;
> the cache glob is last resort and names the INSTALLED plugin, never the source checkout.

Replace placeholders: `{{GITHUB_CONFIG}}`=GH CFG table | `{{WORKFLOW_INVENTORY}}`=WFs table | `{{SERVER_TARGETS}}`=SSH Servers (or "No SSH servers CFG") | `{{SECRETS_LIST}}`=secret names | `{PLUGIN_VERSION}`=`PV` above | `{LAST_UPDATED}`=`date +%F` (`YYYY-MM-DD`, quoted in the frontmatter).
Write to `.claude/agents/deploy-admin.md`.

Leftover-token gate -- BOTH brace families (this skill's `{{...}}` tokens and the single-brace metadata ones). **EXECUTE** using Bash tool:
```bash
F="$PWD/.claude/agents/deploy-admin.md"
test -f "$F" || { echo "❌ FAILED -- $F not written"; exit 1; }
LEFT="$(grep -nE '\{\{|\{(PLUGIN_VERSION|GENERATED_BY|LAST_UPDATED)\}' "$F" || true)"
test -z "$LEFT" && echo "✅ no leftover placeholders" || { echo "❌ FAILED -- leftover placeholders:"; echo "$LEFT"; }
```
> **STOP if ❌** -- re-substitute before continuing.

## P3: Create WF

### Step 1: Load TPLs
Read `REF/workflow-templates.md` for WF patterns.

### Step 2: Determine Type
AUQ: "What type of GitHub Actions WF?"
- "Build + Push to GHCR" — Docker image → GHCR
- "Deploy to VPS" — SSH to remote server
- "Release" — GitHub Release from tag push
- "Security Scan" — dependency/code scan with SARIF
- "Custom" — describe needs

### Step 3: Generate YAML
1. Generate WF YAML with project-specific values
2. Write to `.github/workflows/<name>.yml`
3. Validate YAML structure

EXEC:
```bash
mkdir -p .github/workflows && echo "OK dir" || { rc=$?; echo "FAILED dir" >&2; exit "$rc"; }
```
Write WF file via Write tool.

### Step 4: Update CFG
EXEC:
```bash
bash "${CLAUDE_SKILL_DIR}/scripts/deploy-local-ops.sh" update-workflows && echo "OK update" || { rc=$?; echo "FAILED update" >&2; exit "$rc"; }
```

## P4: Release (CRITICAL)

Read `REF/safety-rules.md` first. `REF/release-best-practices.md` is a WORKED EXAMPLE from one
multi-package repo — a pattern to adapt, !=commands to run in the current project.

### Step 0: Probe Project Release Tooling (MANDATORY before Steps 2/7/9)

This skill ships to arbitrary repos. It knows NOTHING about the current project's release
scripts until it looks.

EXEC:
```bash
ls .claude/scripts/*.sh 2>/dev/null; ls scripts/ 2>/dev/null | head -20; jq -r '.scripts // {} | keys[]' package.json 2>/dev/null; ls Makefile 2>/dev/null
```
Record: BUMP_SCRIPT (a bump/version script, or `none`) | POST_SCRIPT (a post-release/publish
script, or `none`) | CHANGELOG (`CHANGELOG.md` / `RELEASE-NOTES.md` / `none`).

> A `none` is NOT a failure. It means the step is skipped or done by hand — say so in the report.

### Step 1: Determine Version
EXEC:
```bash
git describe --tags --abbrev=0 2>/dev/null || echo "NO_TAGS"
```
EXEC:
```bash
git log --oneline $(git describe --tags --abbrev=0 2>/dev/null || echo "HEAD~10")..HEAD 2>/dev/null | head -20
```
Suggest semver bump (patch/minor/major) based on commits.

### Step 2: Build the Release PLAN — NO WRITES YET

Nothing is edited before the gate. "Cancel" must leave the tree byte-identical to how it was found.

EXEC:
```bash
git status --porcelain; echo "--- local tags ---"; git tag --list 'v*' | tail -5; echo "--- unpushed ---"; git log --oneline @{u}..HEAD 2>/dev/null | head -10
```

Record, WITHOUT writing anything:

| Plan field | Content |
|------------|---------|
| VERSION | the exact `X.Y.Z` |
| TAG | `vX.Y.Z` — MUST NOT already exist locally or on the remote |
| OWNED_PATHS | the exact list of files THIS release will change (version files + changelog). Nothing else is ever staged |
| PRE_EXISTING_DIRTY | files already modified before this run — they stay unstaged and unpushed |
| PRE_EXISTING_TAGS | local tags not on the remote — they stay unpushed |
| CHANGELOG_PREVIEW | the section text generated from `git log` since the last tag, grouped Added/Changed/Fixed |

Changelog preview shape when the file is new/empty (otherwise match the file's existing headings):
```markdown
## vX.Y.Z (YYYY-MM-DD)
#### Added / Changed / Fixed
- **category:** description
```

> A dirty tree is NOT a blocker — it is a reason the plan must name OWNED_PATHS explicitly.
> `git add -A` is banned in this skill: it publishes whatever the user happened to be editing.

### Step 3: Confirmation Gate (BEFORE the first write)
AUQ: "Ready to release vX.Y.Z:\n\n[CHANGELOG_PREVIEW]\n\nWill WRITE: [OWNED_PATHS]\nWill NOT touch: [PRE_EXISTING_DIRTY]\nWill push: HEAD + refs/tags/vX.Y.Z only (not [PRE_EXISTING_TAGS])\nThen: [POST_SCRIPT from Step 0, or 'no post-release script']\n\nProceed?"
Options: "Yes, release" | "Change version/scope" | "Cancel"

> **Cancel here costs nothing** — no file has been touched yet. Everything below runs only after "Yes".

### Step 4: Bump Version (first write)

| BUMP_SCRIPT (Step 0) | Action |
|----------------------|--------|
| found | `bash "<BUMP_SCRIPT>" X.Y.Z && echo "OK bump" \|\| { rc=$?; echo "FAILED bump" >&2; exit "$rc"; }` |
| `none`, version files obvious | Edit every version file the repo has (`package.json`, `pyproject.toml`, `gradle.properties`, `*/plugin.json`, `Cargo.toml`, ...) to the SAME X.Y.Z |
| `none`, unclear | Back to Step 2 — an unknown file set cannot be approved. AUQ: "Which files carry the version?" then re-run the gate |

> Never invent a script path. `bash .claude/scripts/bump-version.sh` exists in SOME repos, not this one by default.
> Every file written here MUST already be in OWNED_PATHS. A write outside that list voids the approval — stop and re-gate.

### Step 5: Update Changelog
Write to CHANGELOG from Step 0, matching the heading style already in that file. If CHANGELOG
is `none` — skip this step, put the summary in the tag/release body instead.

### Step 6: Release Transaction (ONE chain, stop-on-error)

One `&&` chain with an explicit failure handler preserves the failing status; `set -e` alone
does not stop the caller after a failed non-final link. Emit RELEASED only after the whole chain succeeds.

EXEC:
```bash
set -euo pipefail
VER="X.Y.Z"                      # from the approved plan
PATHS=(package.json CHANGELOG.md)  # EXACTLY the approved OWNED_PATHS, nothing else
git rev-parse -q --verify "refs/tags/v${VER}" >/dev/null && { echo "ABORT: tag v${VER} already exists"; exit 1; }
BEFORE=$(git tag --list | wc -l | tr -d ' ')
git add -- "${PATHS[@]}" \
  && git commit -m "v${VER}: <summary>" \
  && git push origin HEAD \
  && git tag "v${VER}" \
  && [ "$(git tag --list | wc -l | tr -d ' ')" -eq "$((BEFORE + 1))" ] \
  && git push origin "refs/tags/v${VER}" \
  || { rc=$?; echo "FAILED release (exit $rc)" >&2; exit "$rc"; }
echo "RELEASED v${VER}"
```

| Banned | Required | Why |
|--------|----------|-----|
| `git add -A` | `git add -- <OWNED_PATHS>` | stages unrelated user work |
| `git push --tags` | `git push origin refs/tags/vX.Y.Z` | publishes every unpushed local tag |
| `... \|\| echo "FAILED"` | a real non-zero exit | a masked failure reads as success |
| three separate EXEC blocks | one `&&` chain | a mid-sequence failure leaves partial remote state |

> Non-zero exit → report which link failed and the recovery command (`git reset --soft HEAD~1`,
> `git tag -d vX.Y.Z`). Both are DELETE-level: propose them, !=run them unasked.
>
> Both recover a LOCAL failure only before HEAD is pushed; a successful HEAD push already makes the
> commit public. Once `git push origin refs/tags/vX.Y.Z` has succeeded, deleting
> or force-moving that tag is irreversible for anyone who already fetched it — their clone keeps the
> old object and the tag name then means two different commits. The non-destructive escape past that
> point is always the next patch version.

### Step 7: Post-Release
Only if POST_SCRIPT was found in Step 0. Otherwise SKIP and report "no post-release script".
EXEC:
```bash
POST_SCRIPT="<absolute path to the post-release script recorded in Step 0>"
bash "$POST_SCRIPT" && echo "OK post-release" || { rc=$?; echo "FAILED post-release" >&2; exit "$rc"; }
```

### Step 8: Monitor CI — correlated to THIS release, never `gh run list -L 3`

A bare `gh run list` shows whatever ran most recently. Correlate by the pushed SHA, then watch that
run to a terminal state. EXEC:
```bash
. "${CLAUDE_SKILL_DIR}/scripts/lib/deploy-common.sh"
SHA=$(git rev-parse HEAD)
RUN_ID=$(ght 30 gh run list -L 20 --json databaseId,headSha,workflowName --jq "[.[] | select(.headSha == \"$SHA\")] | .[0].databaseId // empty")
[ -n "$RUN_ID" ] || { echo "NO_RUN_FOR_SHA=$SHA (CI may not have registered yet — re-check, !=claim success)"; exit 1; }
echo "RUN_URL=$(ght 30 gh run view "$RUN_ID" --json url --jq .url)"
ght 900 gh run watch "$RUN_ID" --exit-status
RC=$?; echo "CI_RESULT=$(ght_reason $RC)"
```
`CI_RESULT` other than `ok` → the release is NOT verified. Report the run URL + `gh run view $RUN_ID --log-failed | tail -30`.

### Step 9: Verify Release
EXEC:
```bash
. "${CLAUDE_SKILL_DIR}/scripts/lib/deploy-common.sh"
ght 30 gh release view vX.Y.Z --json tagName,name,isDraft,createdAt 2>/dev/null && echo "OK release" || { rc=$?; echo "FAILED release" >&2; exit "$rc"; }
```
Then verify whatever THIS project actually publishes — pick what applies, skip the rest:

| Artifact | Check |
|----------|-------|
| Container image | `docker manifest inspect <registry>/<image>:vX.Y.Z >/dev/null && echo "OK image" \|\| echo "FAILED image"` |
| npm / PyPI package | `npm view <pkg>@X.Y.Z version` / `curl -sf https://pypi.org/pypi/<pkg>/X.Y.Z/json >/dev/null` |
| Live service | `curl -sf <base>/version` — MUST equal `X.Y.Z` (version gate, not just health) |
| Claude Code plugin | `grep '"version"' ~/.claude/plugins/cache/<marketplace>/<plugin>/X.Y.Z/.claude-plugin/plugin.json` |

> Nothing published → report "no external artifact to verify", !=FAILED.

## P5: Deploy

### Step 1: Load Safety Rules
Read `REF/safety-rules.md`.

### Step 2: List Deployable WFs
EXEC:
```bash
. "${CLAUDE_SKILL_DIR}/scripts/lib/deploy-common.sh"
ght 30 gh workflow list --json name,state,id --jq '.[] | select(.state == "active")' 2>/dev/null && echo "OK list" || { rc=$?; echo "FAILED list" >&2; exit "$rc"; }
```

### Step 3: Select WF
If multiple: AUQ to select. If $ARGUMENTS specifies WF → use that.

### Step 4: Confirmation Gate
AUQ: "About to trigger WF:\n\n  WF: [name]\n  Branch: [branch]\n  Inputs: [if any]\n\nClassification: SERVICE\nProceed?"
Options: "Yes, deploy" | "Cancel"

### Step 5: Trigger + Correlate the Dispatched Run
Snapshot the newest run id BEFORE dispatching, so the run that is watched is provably the one just
triggered — not a neighbouring run that happened to start. EXEC:
```bash
. "${CLAUDE_SKILL_DIR}/scripts/lib/deploy-common.sh"
WF="WORKFLOW_FILE"; BR="BRANCH"
BEFORE=$(ght 30 gh run list -w "$WF" -L 1 --json databaseId --jq '.[0].databaseId // 0')
ght 30 gh workflow run "$WF" --ref "$BR"
RC=$?; [ "$RC" -eq 0 ] || { echo "TRIGGER=$(ght_reason $RC)"; exit 1; }
RUN_ID=""
for _ in 1 2 3 4 5 6 7 8 9 10; do
  RUN_ID=$(ght 30 gh run list -w "$WF" -L 10 --json databaseId,event --jq "[.[] | select(.event == \"workflow_dispatch\" and .databaseId > $BEFORE)] | .[0].databaseId // empty")
  [ -n "$RUN_ID" ] && break
  sleep 3
done
[ -n "$RUN_ID" ] || { echo "DISPATCH_NOT_OBSERVED (triggered, run id not found — check manually, !=claim success)"; exit 1; }
echo "RUN_ID=$RUN_ID"
```
> `TRIGGER=no_tool` means gh is not installed — !=report a failed deployment for a dispatch that
> was never attempted.

### Step 6: Watch That Run to a Terminal State
EXEC:
```bash
. "${CLAUDE_SKILL_DIR}/scripts/lib/deploy-common.sh"
ght 900 gh run watch "$RUN_ID" --exit-status
echo "RUN_RESULT=$(ght_reason $?)"
```
Only `RUN_RESULT=ok` is a green deployment. Anything else → report the run URL + `--log-failed`.

### Step 7: VPS Health + Version Gate (if deploy target is VPS + CLAUDE.local.md has SSH CFG)
Health alone proves the box is up, not that the new build is live. EXEC:
```bash
CODE=$(curl -sf -o /dev/null -w "%{http_code}" "HEALTH_URL" || true)
LIVE=$(curl -sf "VERSION_URL" || true)
[ "$CODE" = "200" ] && [ "$LIVE" = "EXPECTED_VERSION" ] && echo "OK health+version" || { echo "FAILED health=$CODE version=$LIVE"; exit 1; }
```
> No `/version` endpoint → say "no version gate available", !=silently downgrade to health-only success.

## P6: Monitor

All four steps share one sourced helper — `ght`, never bare `timeout` (see Robustness Rules).

### Step 1: WF Runs
EXEC:
```bash
. "${CLAUDE_SKILL_DIR}/scripts/lib/deploy-common.sh"
ght 30 gh run list -L 10 --json workflowName,status,conclusion,createdAt,headBranch,event 2>/dev/null && echo "OK runs" || { rc=$?; echo "FAILED runs (reason=$(ght_reason "$rc") watchdog=$(ght_backend))" >&2; exit "$rc"; }
```

### Step 2: WF Status
EXEC:
```bash
. "${CLAUDE_SKILL_DIR}/scripts/lib/deploy-common.sh"
ght 30 gh workflow list --json name,state,id 2>/dev/null && echo "OK workflows" || { rc=$?; echo "FAILED workflows" >&2; exit "$rc"; }
```

### Step 3: Releases
EXEC:
```bash
. "${CLAUDE_SKILL_DIR}/scripts/lib/deploy-common.sh"
ght 30 gh release list -L 5 2>/dev/null && echo "OK releases" || { rc=$?; echo "FAILED releases" >&2; exit "$rc"; }
```

### Step 4: Failed Run Logs (if conclusion=failure found)
EXEC:
```bash
. "${CLAUDE_SKILL_DIR}/scripts/lib/deploy-common.sh"
ght 30 gh run view RUN_ID --log-failed 2>/dev/null | tail -50 && echo "OK logs" || { rc=$?; echo "FAILED logs" >&2; exit "$rc"; }
```
Replace RUN_ID with failed run's databaseId.

### Step 5: CFG Refresh Proposal (read-only)
Report stale workflow inventory; actual `deploy-local-ops.sh update-workflows` writes belong to
`setup` or `update-agent`, not read-only monitor. Monitor never silently mutates CLAUDE.local.md.

## Mode: update-agent

Re-discover all WFs + refresh deploy-admin agent.

### Step 1: Discover
EXEC:
```bash
bash "${CLAUDE_SKILL_DIR}/scripts/workflow-discover.sh" && echo "OK discovery" || { rc=$?; echo "FAILED discovery" >&2; exit "$rc"; }
```

### Step 2: Update CFG
EXEC:
```bash
bash "${CLAUDE_SKILL_DIR}/scripts/deploy-local-ops.sh" update-workflows && echo "OK update" || { rc=$?; echo "FAILED update" >&2; exit "$rc"; }
```

### Step 3: Re-read CFG
EXEC:
```bash
bash "${CLAUDE_SKILL_DIR}/scripts/deploy-local-ops.sh" read-github 2>/dev/null
```

### Step 4: Regenerate Agent
Read TPL, replace placeholders with fresh data, write to `.claude/agents/deploy-admin.md`.
Re-resolve `{PLUGIN_VERSION}` + `{LAST_UPDATED}` exactly as in P2 Step 8 -- a regeneration is a new write, so the stamp is refreshed, never carried over. Report what changed.

</instructions>

## Output Format

```markdown
# Deploy [MODE]

## Detection
| Field | Value |
|-------|-------|
| Arguments | `$ARGUMENTS` |
| Mode | `[detected mode]` |

## Environment
| Component | Status |
|-----------|--------|
| gh CLI | [version] |
| Auth | [user] |
| Repo | [owner/name] |
| Secrets | [N CFG] |
| WFs | [N found] |

## Actions Taken
- [action 1]
- [action 2]

## Status
[success / partial / failed]
```

Files in this skill

  • SKILL.md14.5 KB
  • references/release-best-practices.md4.1 KB
  • references/safety-rules.md4.2 KB
  • references/workflow-templates.md15.1 KB
  • scripts/deploy-local-ops.sh5.8 KB
  • scripts/detect-mode.sh1.3 KB
  • scripts/gh-env-check.sh2.1 KB
  • scripts/workflow-discover.sh2 KB
  • templates/deploy-admin-agent.md.template3.9 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.