
Brewtools:Deploy
- 6 installs
- 29 repo stars
- Updated August 2, 2026
- kochetkov-ma/claude-brewcode
Helps with devops & ci/cd tasks.
About
brewtools:deploy is a Claude Code skill for devops & ci/cd. It helps solo builders move faster with AI-assisted development.
- brewtools:deploy
- DevOps & CI/CD
- AI-coding skill
Brewtools:Deploy by the numbers
- 6 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #1,067 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/kochetkov-ma/claude-brewcode --skill brewtoolsdeployAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 6 |
|---|---|
| repo stars | ★ 29 |
| Last updated | August 2, 2026 |
| Repository | kochetkov-ma/claude-brewcode ↗ |
What it does
Helps with devops & ci/cd tasks.
Files
[DICT: P=Phase, EXEC=EXECUTE using Bash tool, AUQ=AskUserQuestion, WF=workflow, CFG=config, REF=references, GH=gh CLI, TPL=template, LOPS=deploy-local-ops.sh]
GitHub Actions Deployment
Manage GitHub Actions — WFs, releases, GHCR, CI/CD with safety gates + persistent CFG.
<instructions>
Robustness Rules (MANDATORY — ALL phases)
Fail-Fast
| Rule | Scope |
|---|---|
| Every Bash call: `&& echo "OK ..." \ | \ |
| 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
| Op | Timeout | On timeout |
|---|---|---|
| GH CLI cmds | timeout 30 gh ... | report "gh timed out", stop |
gh run watch | timeout 300 gh run watch ... | switch to polling |
| Entire invocation | max 15 GH calls total | stop, report progress, suggest manual |
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 |
| LOPS | 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">Silent failures = bugs.
---
P0: Mode Detection (MANDATORY FIRST STEP)
EXEC:
bash "${CLAUDE_SKILL_DIR}/scripts/detect-mode.sh" "$ARGUMENTS"Output: ARGS: [...] MODE: [...]
| Keyword | MODE |
|---|---|
| setup, check, prerequisites, init | setup |
| create, new workflow, add workflow | create |
| release, bump, version, tag, publish | release |
| deploy, trigger, dispatch, run workflow | deploy |
| monitor, watch, status, check runs, logs | monitor |
| update agent, refresh, rescan | update-agent |
| (empty, no GH CFG) | setup |
| (empty, GH CFG exists) | monitor |
---
P1: Environment + CFG Check (ALL modes before branching)
EXEC:
bash "${CLAUDE_SKILL_DIR}/scripts/gh-env-check.sh" && echo "OK env-check" || echo "FAILED env-check"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 "${CLAUDE_SKILL_DIR}/scripts/${LOPS}" 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) |
| 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:
gh auth status 2>&1 && echo "OK auth" || echo "FAILED auth"If FAILED → instruct: gh auth login
Step 2: Detect Repo
EXEC:
gh repo view --json owner,name,url,defaultBranchRef,visibility 2>/dev/null && echo "OK repo" || echo "FAILED repo"Step 3: Check Secrets
EXEC:
gh secret list 2>/dev/null && echo "OK secrets" || echo "FAILED secrets"Step 4: Check SSH Integration
EXEC:
grep -q "^## SSH Servers" CLAUDE.local.md 2>/dev/null && echo "SSH_SERVERS=exists" || echo "SSH_SERVERS=missing"Step 5: Discover WFs
EXEC:
bash "${CLAUDE_SKILL_DIR}/scripts/workflow-discover.sh" && echo "OK discovery" || echo "FAILED discovery"Step 6: Persist CFG
EXEC:
bash "${CLAUDE_SKILL_DIR}/scripts/${LOPS}" add-github "OWNER" "REPO" "ghcr.io" && echo "OK add-github" || echo "FAILED add-github"Replace OWNER + REPO with values from Step 2. EXEC:
bash "${CLAUDE_SKILL_DIR}/scripts/${LOPS}" add-workflows && echo "OK add-workflows" || echo "FAILED add-workflows"Step 7: Gitignore
EXEC:
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:
cat "${CLAUDE_SKILL_DIR}/templates/deploy-admin-agent.md.template"Replace placeholders: {{GITHUB_CONFIG}}=GH CFG table | {{WORKFLOW_INVENTORY}}=WFs table | {{SERVER_TARGETS}}=SSH Servers (or "No SSH servers CFG") | {{SECRETS_LIST}}=secret names | {{LAST_UPDATED}}=current ISO timestamp. Write to .claude/agents/deploy-admin.md.
---
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:
mkdir -p .github/workflows && echo "OK dir" || echo "FAILED dir"Write WF file via Write tool.
Step 4: Update CFG
EXEC:
bash "${CLAUDE_SKILL_DIR}/scripts/${LOPS}" update-workflows && echo "OK update" || echo "FAILED update"---
P4: Release (CRITICAL)
Read REF/safety-rules.md + REF/release-best-practices.md first.
Step 1: Determine Version
EXEC:
git describe --tags --abbrev=0 2>/dev/null || echo "NO_TAGS"EXEC:
git log --oneline $(git describe --tags --abbrev=0 2>/dev/null || echo "HEAD~10")..HEAD 2>/dev/null | head -20Suggest semver bump (patch/minor/major) based on commits.
Step 2: Bump Version
EXEC:
bash .claude/scripts/bump-version.sh X.Y.Z && echo "OK bump" || echo "FAILED bump"Step 3: Auto-generate Changelog
Analyze commits since last tag. Group by type (Added/Changed/Fixed).
Step 4: Update RELEASE-NOTES.md
Add at top:
## vX.Y.Z (YYYY-MM-DD)
> Docs: [page](https://doc-claude.brewcode.app/plugin/path/) | [page2](...)
### brewcode
#### Added / Changed / Fixed
- **category:** descriptionStep 5: Confirmation Gate
AUQ: "Ready to release vX.Y.Z:\n\n[changelog preview]\n\nThis will:\n1. Commit version bump + changelog\n2. Create tag vX.Y.Z\n3. Push to remote (triggers CI)\n4. Run update-plugin.sh\n\nProceed?" Options: "Yes, release" | "Edit changelog first" | "Cancel"
Step 6: Commit + Tag + Push
EXEC:
git add -A && git commit -m "vX.Y.Z: <summary>" && echo "OK commit" || echo "FAILED commit"EXEC:
git tag vX.Y.Z && echo "OK tag" || echo "FAILED tag"EXEC:
git push && git push --tags && echo "OK push" || echo "FAILED push"Step 7: Post-Release
EXEC:
bash .claude/scripts/update-plugin.sh && echo "OK update-plugin" || echo "FAILED update-plugin"Step 8: Monitor CI
EXEC:
timeout 60 gh run list -L 3 --json workflowName,status,conclusion,createdAt 2>/dev/null && echo "OK runs" || echo "FAILED runs"Wait for runs triggered by tag push. Report status.
Step 9: Verify Release
EXEC:
gh release view vX.Y.Z --json tagName,name,isDraft,createdAt 2>/dev/null && echo "OK release" || echo "FAILED release"EXEC:
grep '"matcher"' ~/.claude/plugins/cache/claude-brewcode/brewcode/X.Y.Z/hooks/hooks.json 2>/dev/null && echo "OK cache" || echo "FAILED cache"---
P5: Deploy
Step 1: Load Safety Rules
Read REF/safety-rules.md.
Step 2: List Deployable WFs
EXEC:
gh workflow list --json name,state,id --jq '.[] | select(.state == "active")' 2>/dev/null && echo "OK list" || echo "FAILED list"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
EXEC:
timeout 30 gh workflow run "WORKFLOW_FILE" --ref BRANCH && echo "OK trigger" || echo "FAILED trigger"Step 6: Watch Run
EXEC:
sleep 5 && timeout 300 gh run list -w "WORKFLOW_FILE" -L 1 --json databaseId,status,conclusion --jq '.[0]' 2>/dev/null && echo "OK run" || echo "FAILED run"Poll until complete or timeout.
Step 7: VPS Health Check (if deploy target is VPS + CLAUDE.local.md has SSH CFG)
EXEC:
curl -sf -o /dev/null -w "%{http_code}" "HEALTH_URL" && echo "OK health" || echo "FAILED health"---
P6: Monitor
Step 1: WF Runs
EXEC:
timeout 30 gh run list -L 10 --json workflowName,status,conclusion,createdAt,headBranch,event 2>/dev/null && echo "OK runs" || echo "FAILED runs"Step 2: WF Status
EXEC:
timeout 30 gh workflow list --json name,state,id 2>/dev/null && echo "OK workflows" || echo "FAILED workflows"Step 3: Releases
EXEC:
timeout 30 gh release list -L 5 2>/dev/null && echo "OK releases" || echo "FAILED releases"Step 4: Failed Run Logs (if conclusion=failure found)
EXEC:
timeout 30 gh run view RUN_ID --log-failed 2>/dev/null | tail -50 && echo "OK logs" || echo "FAILED logs"Replace RUN_ID with failed run's databaseId.
Step 5: Update CFG
EXEC:
bash "${CLAUDE_SKILL_DIR}/scripts/${LOPS}" update-workflows && echo "OK update" || echo "FAILED update"---
Mode: update-agent
Re-discover all WFs + refresh deploy-admin agent.
Step 1: Discover
EXEC:
bash "${CLAUDE_SKILL_DIR}/scripts/workflow-discover.sh" && echo "OK discovery" || echo "FAILED discovery"Step 2: Update CFG
EXEC:
bash "${CLAUDE_SKILL_DIR}/scripts/${LOPS}" update-workflows && echo "OK update" || echo "FAILED update"Step 3: Re-read CFG
EXEC:
bash "${CLAUDE_SKILL_DIR}/scripts/${LOPS}" read-github 2>/dev/nullStep 4: Regenerate Agent
Read TPL, replace placeholders with fresh data, write to .claude/agents/deploy-admin.md. Set {{LAST_UPDATED}} = current timestamp. Report what changed.
</instructions>
---
Output Format
# 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]Release Best Practices
Release flow, semver rules, changelog conventions for this project.
Semver Rules
| Bump | When | Examples |
|---|---|---|
| patch (0.0.X) | Bug fixes, typos, minor adjustments | Fix hook, fix script, update docs |
| minor (0.X.0) | New features, new skills, new agents | Add deploy skill, add image-gen |
| major (X.0.0) | Breaking changes, incompatible API | Restructure plugins, rename skills |
Release Flow (this project)
1. bump-version.sh X.Y.Z → Updates ALL 6 version files
2. Update RELEASE-NOTES.md → Add changelog section
3. git add + commit → "vX.Y.Z: <summary>"
4. git tag vX.Y.Z → Create tag
5. git push && push --tags → Push to remote (triggers CI)
6. update-plugin.sh → Refresh local plugin cache
7. Verify CI → gh run list -L 3 (all green)
8. Verify cache → grep matcher in hooks.jsonVersion Files (CRITICAL — ALL must match)
| File | Path |
|---|---|
| brewcode plugin.json | brewcode/.claude-plugin/plugin.json |
| brewcode marketplace.json | brewcode/.claude-plugin/marketplace.json |
| brewdoc plugin.json | brewdoc/.claude-plugin/plugin.json |
| brewdoc marketplace.json | brewdoc/.claude-plugin/marketplace.json |
| brewtools plugin.json | brewtools/.claude-plugin/plugin.json |
| brewtools marketplace.json | brewtools/.claude-plugin/marketplace.json |
NEVER edit versions manually. ALWAYS use bash .claude/scripts/bump-version.sh X.Y.ZRELEASE-NOTES.md Format
## vX.Y.Z (YYYY-MM-DD)
> Docs: [page](https://doc-claude.brewcode.app/plugin/path/) | [page2](...)
### brewcode
#### Added
- **skill:** deploy skill — GitHub Actions deployment with safety gates
#### Changed
- **hook:** improved pre-compact knowledge extraction
#### Fixed
- **script:** bump-version.sh handles missing files gracefullyRules
| Rule | Details |
|---|---|
> Docs: line | MUST list doc pages for ALL affected skills/agents/hooks |
| URL pattern | `https://doc-claude.brewcode.app/{plugin}/{skills\ |
| Group by plugin | Separate ### brewcode, ### brewdoc, ### brewtools, ### brewui |
| Group by type | #### Added, #### Changed, #### Fixed under each plugin |
| Category prefix | Bold: **skill:**, **hook:**, **agent:**, **script:** |
Changelog Generation
From Commits
Analyze commits since last tag:
git log --oneline $(git describe --tags --abbrev=0)..HEADType Mapping
| Commit prefix | Changelog type |
|---|---|
feat:, add:, new file | Added |
fix:, bugfix: | Fixed |
refactor:, update:, improve: | Changed |
docs: | Changed (docs) |
test: | Usually skip unless significant |
Plugin Detection
Detect which plugin is affected from file paths:
| Path prefix | Plugin |
|---|---|
brewcode/ | brewcode |
brewdoc/ | brewdoc |
brewtools/ | brewtools |
brewui/ | brewui |
.claude/, .github/ | infrastructure (under brewcode) |
Tag Conventions
| Pattern | Meaning |
|---|---|
vX.Y.Z | Release tag (triggers CI) |
| No pre-release tags | This project uses simple semver only |
CI Triggers
| Event | Workflows triggered |
|---|---|
Tag v*.*.* push | Docs (GHCR build), Release (GitHub Release) |
| Release workflow completes | Deploy Docs (VPS deploy) via workflow_run |
| Branch push (non-main) | Docs (GHCR build, branch tag) |
Post-Release Verification
| Check | Command | Expected |
|---|---|---|
| CI runs | gh run list -L 3 | All green |
| Release created | gh release view vX.Y.Z | Exists, not draft |
| Plugin cache | grep '"matcher"' ~/.claude/plugins/cache/claude-brewcode/brewcode/X.Y.Z/hooks/hooks.json | Matchers present |
| Docs deployed | curl -sf https://doc-claude.brewcode.app/getting-started/ | HTTP 200 |
Emergency Rollback
If release has critical issues:
1. Do NOT delete the tag (breaks references) 2. Fix forward: create patch release vX.Y.(Z+1) 3. If CI broken: gh workflow run "Deploy Docs" -f image_tag=PREVIOUS_VERSION
GitHub Command Safety Classification
Reference for command classification and confirmation gates.
Classification Levels
| Level | Gate | Description |
|---|---|---|
| READ | free | Observe CI/CD state, no changes |
| CREATE | free | Create new resources (workflow files, draft releases) |
| MODIFY | confirm | Change existing workflows, secrets, config |
| SERVICE | confirm | Trigger workflows, rerun jobs, push code/tags |
| DELETE | always confirm | Delete releases, cancel runs, remove workflow files |
| PRIVILEGE | always confirm | Delete secrets, modify branch protection, disable workflows |
READ Commands (free)
| Category | Commands |
|---|---|
| Runs | gh run list, gh run view, gh run view --log, gh run view --log-failed |
| Workflows | gh workflow list, gh workflow view |
| Releases | gh release list, gh release view |
| Secrets | gh secret list (names only, no values) |
| Repo | gh repo view, gh api repos/... (GET) |
| Git | git status, git log, git diff, git describe, git tag -l |
| Docker | docker images, docker manifest inspect |
CREATE Commands (free)
| Category | Commands |
|---|---|
| Workflows | Write new .github/workflows/*.yml file |
| Releases | gh release create --draft (draft only) |
| Git | git branch, git stash |
| Directories | mkdir -p .github/workflows |
MODIFY Commands (confirm)
| Category | Commands | Risk |
|---|---|---|
| Secrets | gh secret set | Credential changes |
| Workflows | Edit existing .github/workflows/*.yml | CI behavior changes |
| Release notes | Edit RELEASE-NOTES.md | Documentation changes |
| Git | git commit, git tag | History changes |
| Config | Edit .github/dependabot.yml, CODEOWNERS | Repo config |
SERVICE Commands (confirm)
| Category | Commands | Risk |
|---|---|---|
| Workflows | gh workflow run, gh run rerun | Trigger CI execution |
| Git | git push, git push --tags | Remote state changes |
| Releases | gh release create (non-draft), gh release edit | Public release |
| Docker | docker push, GHCR operations | Registry changes |
DELETE Commands (always confirm)
| Category | Commands | Risk |
|---|---|---|
| Releases | gh release delete | Release removal |
| Runs | gh run cancel | Job interruption |
| Files | Remove workflow file from .github/workflows/ | CI pipeline removal |
| Git | git tag -d, git push --delete | Tag/branch removal |
| Docker | docker rmi, GHCR package deletion | Image removal |
PRIVILEGE Commands (always confirm)
| Category | Commands | Risk |
|---|---|---|
| Secrets | gh secret delete | Credential removal |
| Workflows | gh workflow disable | CI pipeline disable |
| Branch protection | gh api -X PUT repos/.../branches/.../protection | Security bypass |
| Force push | git push --force | History rewrite |
| Admin | gh repo edit --visibility, gh repo delete | Repository control |
Compound Command Rules
| Pattern | Classification | Why |
|---|---|---|
git commit && git push | SERVICE (highest) | Push changes remote |
git tag && git push --tags | SERVICE | Triggers CI pipelines |
gh release create && gh workflow run | SERVICE | Multiple side effects |
git push --force | PRIVILEGE (overrides SERVICE) | History rewrite |
bump-version.sh && git commit && git tag && git push | SERVICE | Release chain |
Confirmation Message Format
MODIFY/SERVICE
About to execute:
[command 1]
[command 2]
Classification: MODIFY/SERVICE
Repo: [owner/name]
Branch: [branch]
Proceed?DELETE/PRIVILEGE
WARNING: DESTRUCTIVE action on [owner/name]:
[command 1] -- [what it deletes/changes]
Classification: DELETE/PRIVILEGE
This cannot be undone.
Proceed?Emergency Stop
If any command returns unexpected output suggesting:
- Wrong repository (owner/name mismatch)
- Production branch when expecting staging/dev
- Force push to main/master
- Deleting a non-draft release with downloads
- Tag already exists on remote with different commit
STOP immediately. Report findings. Ask user to confirm before continuing.
GitHub Actions Workflow Templates
Based on real workflows from this repository. Replace {{PLACEHOLDERS}} with project values.Template 1: Build + Push to GHCR
Based on: docs.yml -- builds Docker image, pushes to GitHub Container Registry.Trigger: Tag push v*.*.* + branch pushes (except main). Key steps: Checkout, compute tags, Docker Buildx, GHCR login, build+push, summary.
name: {{WORKFLOW_NAME}}
on:
push:
tags:
- "v*.*.*"
branches-ignore:
- main
permissions:
contents: read
packages: write
concurrency:
group: {{CONCURRENCY_GROUP}}-${{ github.ref }}
cancel-in-progress: true
env:
IMAGE: ghcr.io/{{OWNER}}/{{IMAGE_NAME}}
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Compute image tags
id: meta
run: |
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
VERSION="${GITHUB_REF_NAME#v}"
echo "tags=${IMAGE}:${VERSION},${IMAGE}:latest" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
else
BRANCH="${GITHUB_REF_NAME}"
BRANCH_SAFE=$(echo "$BRANCH" | sed 's/[^a-zA-Z0-9._-]/-/g')
DESC=$(git describe --tags --long --match "v*.*.*" 2>/dev/null || echo "0.0.0-0-g$(git rev-parse --short HEAD)")
BASE_VERSION=$(echo "$DESC" | sed 's/^v//' | sed 's/-.*//')
COMMITS_AFTER=$(echo "$DESC" | sed 's/.*-\([0-9]*\)-g.*/\1/')
TAG="${BASE_VERSION}-${BRANCH_SAFE}-${COMMITS_AFTER}"
echo "tags=${IMAGE}:${TAG}" >> "$GITHUB_OUTPUT"
echo "version=${TAG}" >> "$GITHUB_OUTPUT"
fi
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v7
with:
context: {{DOCKER_CONTEXT}}
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}
build-args: VERSION=${{ steps.meta.outputs.version }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Summary
run: |
echo "### Image pushed" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "**Version:** \`${{ steps.meta.outputs.version }}\`" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "**Tags:**" >> "$GITHUB_STEP_SUMMARY"
IFS=',' read -ra TAGS <<< "${{ steps.meta.outputs.tags }}"
for tag in "${TAGS[@]}"; do
echo "- \`${tag}\`" >> "$GITHUB_STEP_SUMMARY"
donePlaceholders:
| Placeholder | Description | Example |
|---|---|---|
{{WORKFLOW_NAME}} | Display name | Docs |
{{CONCURRENCY_GROUP}} | Concurrency group prefix | docs |
{{OWNER}} | GitHub owner/org | kochetkov-ma |
{{IMAGE_NAME}} | Docker image name | claude-brewcode-docs |
{{DOCKER_CONTEXT}} | Docker build context path | web/docs |
---
Template 2: Deploy to VPS
Based on: deploy-docs.yml -- deploys via SSH after upstream build completes.Trigger: workflow_run (after build) + workflow_dispatch (manual). Key steps: Compute tag, SCP deploy files, SSH deploy script, health check, rollback.
name: {{WORKFLOW_NAME}}
on:
workflow_run:
workflows: ["{{UPSTREAM_WORKFLOW}}"]
types: [completed]
workflow_dispatch:
inputs:
image_tag:
description: "Docker image tag to deploy"
required: true
default: "latest"
concurrency:
group: {{CONCURRENCY_GROUP}}
cancel-in-progress: false
env:
HEALTH_URL: {{HEALTH_CHECK_URL}}
permissions:
contents: read
deployments: write
jobs:
deploy:
runs-on: ubuntu-latest
if: >
github.event_name == 'workflow_dispatch' ||
github.event.workflow_run.conclusion == 'success'
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
fetch-depth: 0
- name: Compute image tag
id: tag
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
echo "value=${{ inputs.image_tag }}" >> "$GITHUB_OUTPUT"
else
REF="${{ github.event.workflow_run.head_branch || github.ref_name }}"
if [[ "$REF" =~ ^v[0-9]+\.[0-9]+\.[0-9]+ ]]; then
echo "value=${REF#v}" >> "$GITHUB_OUTPUT"
else
BRANCH_SAFE=$(echo "$REF" | sed 's/[^a-zA-Z0-9._-]/-/g')
DESC=$(git describe --tags --long --match "v*.*.*" 2>/dev/null || echo "0.0.0-0-g$(git rev-parse --short HEAD)")
BASE_VERSION=$(echo "$DESC" | sed 's/^v//' | sed 's/-.*//')
COMMITS_AFTER=$(echo "$DESC" | sed 's/.*-\([0-9]*\)-g.*/\1/')
echo "value=${BASE_VERSION}-${BRANCH_SAFE}-${COMMITS_AFTER}" >> "$GITHUB_OUTPUT"
fi
fi
- name: Create deployment
id: deployment
uses: actions/github-script@v8
with:
script: |
const deployment = await github.rest.repos.createDeployment({
owner: context.repo.owner,
repo: context.repo.repo,
ref: context.sha,
environment: '{{ENVIRONMENT}}',
auto_merge: false,
required_contexts: [],
description: `Deploy {{SERVICE}} v${{ steps.tag.outputs.value }}`,
});
await github.rest.repos.createDeploymentStatus({
owner: context.repo.owner,
repo: context.repo.repo,
deployment_id: deployment.data.id,
state: 'in_progress',
log_url: `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`,
});
return deployment.data.id;
- name: Copy deploy files to VPS
uses: appleboy/scp-action@v1
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
source: "{{DEPLOY_FILES_SOURCE}}"
target: /tmp/{{DEPLOY_SYNC_DIR}}
strip_components: {{STRIP_COMPONENTS}}
- name: Deploy service
uses: appleboy/ssh-action@v1
env:
TAG: ${{ steps.tag.outputs.value }}
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
envs: TAG
script: |
set -euo pipefail
DEPLOY_PATH={{VPS_DEPLOY_PATH}}
cp /tmp/{{DEPLOY_SYNC_DIR}}/* "$DEPLOY_PATH/" 2>/dev/null || true
rm -rf /tmp/{{DEPLOY_SYNC_DIR}}
cd "$DEPLOY_PATH"
[ -f .env ] && cp .env .env.bak || touch .env.bak
if grep -q "^{{TAG_VAR}}=" .env 2>/dev/null; then
sed -i "s/^{{TAG_VAR}}=.*/{{TAG_VAR}}=${TAG}/" .env
else
echo "{{TAG_VAR}}=${TAG}" >> .env
fi
for attempt in $(seq 1 10); do
if docker compose pull {{SERVICE_NAME}} 2>&1; then
break
fi
if [ "$attempt" -eq 10 ]; then
echo "Failed to pull image after 10 attempts"
exit 1
fi
echo "Image not available yet, retrying in 15s... (attempt $attempt/10)"
sleep 15
done
docker compose up -d --no-deps --force-recreate {{SERVICE_NAME}}
for i in $(seq 1 10); do
if curl -sf -o /dev/null "{{INTERNAL_HEALTH_URL}}"; then
echo "Health check passed (attempt $i)"
exit 0
fi
echo "Waiting for service... (attempt $i/10)"
sleep 5
done
echo "Health check failed, rolling back..."
cp .env.bak .env
docker compose pull {{SERVICE_NAME}}
docker compose up -d --no-deps --force-recreate {{SERVICE_NAME}}
exit 1
- name: Verify from runner
run: |
for i in $(seq 1 5); do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$HEALTH_URL" || true)
if [[ "$STATUS" == "200" ]]; then
echo "External health check passed (HTTP $STATUS)"
exit 0
fi
echo "Waiting... (attempt $i/5, HTTP $STATUS)"
sleep 5
done
echo "::warning::External health check did not return 200"
- name: Update deployment (success)
if: success()
uses: actions/github-script@v8
with:
script: |
await github.rest.repos.createDeploymentStatus({
owner: context.repo.owner,
repo: context.repo.repo,
deployment_id: ${{ steps.deployment.outputs.result }},
state: 'success',
environment_url: '{{PUBLIC_URL}}',
log_url: `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`,
});
- name: Update deployment (failure)
if: failure()
uses: actions/github-script@v8
with:
script: |
const id = ${{ steps.deployment.outputs.result || 0 }};
if (!id) return;
await github.rest.repos.createDeploymentStatus({
owner: context.repo.owner,
repo: context.repo.repo,
deployment_id: id,
state: 'failure',
log_url: `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`,
});
- name: Summary
if: always()
run: |
echo "### Deploy {{SERVICE}}" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "**Tag:** \`${{ steps.tag.outputs.value }}\`" >> "$GITHUB_STEP_SUMMARY"
echo "**Health:** $HEALTH_URL" >> "$GITHUB_STEP_SUMMARY"Placeholders:
| Placeholder | Description | Example |
|---|---|---|
{{UPSTREAM_WORKFLOW}} | Build workflow name | Docs |
{{HEALTH_CHECK_URL}} | External health check URL | https://doc-claude.brewcode.app/getting-started/ |
{{ENVIRONMENT}} | GitHub environment name | docs |
{{SERVICE}} | Service display name | docs |
{{DEPLOY_FILES_SOURCE}} | Files to SCP | web/docs/deploy/* |
{{DEPLOY_SYNC_DIR}} | Temp dir on VPS | brewcode-docs-sync |
{{STRIP_COMPONENTS}} | SCP strip level | 3 |
{{VPS_DEPLOY_PATH}} | Deploy path on VPS | /opt/brewcode-docs |
{{TAG_VAR}} | .env tag variable | DOCS_TAG |
{{SERVICE_NAME}} | Docker Compose service | docs |
{{INTERNAL_HEALTH_URL}} | Health URL inside VPS | same as HEALTH_CHECK_URL |
{{PUBLIC_URL}} | Public URL for deployment | https://doc-claude.brewcode.app |
Required secrets: VPS_HOST, VPS_USER, VPS_SSH_KEY
---
Template 3: Release
Based on: release.yml -- creates GitHub Release from tag push, extracts changelog.Trigger: Tag push v*.*.* Key steps: Extract changelog from RELEASE-NOTES.md, create GitHub Release.
name: Release
on:
push:
tags:
- "v*.*.*"
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
sparse-checkout: RELEASE-NOTES.md
- name: Extract changelog for tag version
id: changelog
run: |
TAG="${GITHUB_REF_NAME}"
VERSION="${TAG#v}"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
RELEASE_NOTES="RELEASE-NOTES.md"
if [ ! -f "$RELEASE_NOTES" ]; then
echo "::error::${RELEASE_NOTES} not found"
exit 1
fi
BODY=$(awk -v ver="$VERSION" '
BEGIN { found=0 }
$0 ~ "^## (\\[" ver "\\]|v?" ver ")([^0-9.]|$)" {
found=1
print
next
}
found && $0 ~ "^## (\\[|v?[0-9])" { exit }
found && /^---[[:space:]]*$/ { exit }
found { print }
' "$RELEASE_NOTES")
if [ -z "$BODY" ]; then
echo "::error::No changelog section found for version ${VERSION} in ${RELEASE_NOTES}"
exit 1
fi
echo "$BODY" > /tmp/release-body.md
# Append install instructions
printf '\n---\n\n## Quick Install\n\n```bash\n# Add marketplace\nclaude plugin marketplace add https://github.com/{{OWNER}}/{{REPO}}\n\n# Install plugins\n{{INSTALL_COMMANDS}}\n```\n\n## Already installed? Update\n\n```bash\nclaude plugin marketplace update {{REPO}}\n{{UPDATE_COMMANDS}}\n```\n' >> /tmp/release-body.md
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.changelog.outputs.tag }}
name: ${{ steps.changelog.outputs.tag }}
body_path: /tmp/release-body.md
draft: false
prerelease: falsePlaceholders:
| Placeholder | Description | Example |
|---|---|---|
{{OWNER}} | GitHub owner | kochetkov-ma |
{{REPO}} | Repository name | claude-brewcode |
{{INSTALL_COMMANDS}} | Plugin install commands | claude plugin install brewcode@claude-brewcode |
{{UPDATE_COMMANDS}} | Plugin update commands | claude plugin update brewcode@claude-brewcode |
---
Template 4: Security Scan
Generic template for dependency/code scanning.
Trigger: Push to main + PRs + weekly schedule. Key steps: Checkout, run scanner, upload SARIF, summary.
name: Security Scan
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: "0 6 * * 1"
permissions:
contents: read
security-events: write
concurrency:
group: security-${{ github.ref }}
cancel-in-progress: true
jobs:
scan:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Run {{SCANNER_NAME}}
uses: {{SCANNER_ACTION}}
with:
{{SCANNER_INPUTS}}
- name: Upload SARIF
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: {{SARIF_PATH}}
- name: Summary
if: always()
run: |
echo "### Security Scan" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "**Scanner:** {{SCANNER_NAME}}" >> "$GITHUB_STEP_SUMMARY"
echo "**Branch:** ${{ github.ref_name }}" >> "$GITHUB_STEP_SUMMARY"Placeholders:
| Placeholder | Description | Example |
|---|---|---|
{{SCANNER_NAME}} | Scanner display name | Trivy |
{{SCANNER_ACTION}} | GitHub Action for scanner | aquasecurity/trivy-action@master |
{{SCANNER_INPUTS}} | Action inputs block | scan-type: 'fs' |
{{SARIF_PATH}} | SARIF output path | trivy-results.sarif |
#!/bin/bash
set -euo pipefail
# Manage CLAUDE.local.md GitHub/Workflow entries
# Usage: deploy-local-ops.sh <subcommand> [args...]
# Subcommands: read-github, add-github, add-workflows, update-workflows, list
# IMPORTANT: Never touches SSH sections (## SSH Servers, ## Server:)
LOCAL_FILE="CLAUDE.local.md"
SUBCMD="${1:?Usage: deploy-local-ops.sh <read-github|add-github|add-workflows|update-workflows|list> [args...]}"
shift
# Initialize file if missing (preserve SSH sections if they exist)
init_file() {
if [[ ! -f "$LOCAL_FILE" ]]; then
cat > "$LOCAL_FILE" << 'HEREDOC'
# Local Configuration
> This file is gitignored. Do not commit.
HEREDOC
fi
}
# Check if GitHub Config section exists
has_github_config() {
grep -q "^## GitHub Config" "$LOCAL_FILE" 2>/dev/null
}
case "$SUBCMD" in
read-github)
if [[ ! -f "$LOCAL_FILE" ]] || ! has_github_config; then
echo "GITHUB_CONFIG=missing"
exit 0
fi
echo "GITHUB_CONFIG=exists"
# Parse GitHub Config table
IN_SECTION=false
while IFS= read -r line; do
if [[ "$line" == "## GitHub Config"* ]]; then
IN_SECTION=true
continue
fi
if [[ "$IN_SECTION" == true ]] && [[ "$line" == "## "* ]]; then
break
fi
if [[ "$IN_SECTION" == true ]] && [[ "$line" == "| "* ]] && [[ "$line" != "| Property"* ]] && [[ "$line" != "|---"* ]]; then
PROP=$(echo "$line" | awk -F'|' '{gsub(/^[ \t]+|[ \t]+$/, "", $2); print $2}')
VAL=$(echo "$line" | awk -F'|' '{gsub(/^[ \t]+|[ \t]+$/, "", $3); print $3}')
echo "${PROP}=${VAL}"
fi
done < "$LOCAL_FILE"
;;
add-github)
OWNER="${1:?add-github requires: owner repo [registry]}"
REPO="${2:?add-github requires: repo}"
REGISTRY="${3:-ghcr.io}"
init_file
if has_github_config; then
echo "ERROR: GitHub Config already exists. Use update-workflows to modify."
exit 1
fi
cat >> "$LOCAL_FILE" << HEREDOC
## GitHub Config
| Property | Value |
|----------|-------|
| Owner | $OWNER |
| Auth | gh CLI (token) |
| Registry | $REGISTRY |
| Default repo | $REPO |
HEREDOC
echo "ADDED=github-config"
;;
add-workflows)
REPO="${1:-}"
if [[ ! -f "$LOCAL_FILE" ]]; then
echo "ERROR: $LOCAL_FILE not found. Run add-github first."
exit 1
fi
# Auto-detect repo name if not provided
if [[ -z "$REPO" ]]; then
REPO=$(gh repo view --json name --jq '.name' 2>/dev/null || basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null || echo "unknown")
fi
SECTION_HEADER="## Workflows: $REPO"
# Remove existing workflows section if present (preserve everything else)
if grep -q "^$SECTION_HEADER" "$LOCAL_FILE" 2>/dev/null; then
TMPF=$(mktemp)
IN_SECTION=false
while IFS= read -r line; do
if [[ "$line" == "$SECTION_HEADER" ]]; then
IN_SECTION=true
continue
fi
if [[ "$IN_SECTION" == true ]] && [[ "$line" == "## "* ]]; then
IN_SECTION=false
fi
if [[ "$IN_SECTION" == false ]]; then
echo "$line" >> "$TMPF"
fi
done < "$LOCAL_FILE"
mv "$TMPF" "$LOCAL_FILE"
fi
# Build workflows table
TABLE="$SECTION_HEADER
| Name | File | Trigger | Status | Last Run |
|------|------|---------|--------|----------|"
WORKFLOW_DIR=".github/workflows"
if [[ -d "$WORKFLOW_DIR" ]]; then
for wf_file in "$WORKFLOW_DIR"/*.yml "$WORKFLOW_DIR"/*.yaml; do
[[ -f "$wf_file" ]] || continue
BASENAME=$(basename "$wf_file")
WF_NAME=$(grep -m1 '^name:' "$wf_file" 2>/dev/null | sed 's/^name:[[:space:]]*//' | tr -d '"'"'" || echo "$BASENAME")
WF_TRIGGER=$(grep -A5 '^on:' "$wf_file" 2>/dev/null | grep -oE '(push|pull_request|workflow_dispatch|workflow_run|schedule|release)' | sort -u | tr '\n' ',' | sed 's/,$//' || echo "unknown")
# Get last run status
LAST_RUN=$(timeout 15 gh run list -w "$BASENAME" -L 1 --json conclusion,createdAt --jq '.[0] | "\(.conclusion // "pending") (\(.createdAt | split("T")[0]))"' 2>/dev/null || echo "unknown")
# Get workflow state
WF_STATE=$(timeout 15 gh workflow view "$BASENAME" --json state --jq '.state' 2>/dev/null || echo "unknown")
TABLE="$TABLE
| $WF_NAME | $BASENAME | $WF_TRIGGER | $WF_STATE | $LAST_RUN |"
done
fi
echo "" >> "$LOCAL_FILE"
echo "$TABLE" >> "$LOCAL_FILE"
echo "ADDED=workflows"
echo "REPO=$REPO"
;;
update-workflows)
# Same as add-workflows but explicitly replaces
exec "$0" add-workflows "$@"
;;
list)
if [[ ! -f "$LOCAL_FILE" ]]; then
echo "NO_CONFIG"
exit 0
fi
if has_github_config; then
echo "GITHUB_CONFIG=exists"
else
echo "GITHUB_CONFIG=missing"
fi
# Count workflow sections
WF_SECTIONS=$(grep -c "^## Workflows:" "$LOCAL_FILE" 2>/dev/null || echo "0")
echo "WORKFLOW_SECTIONS=$WF_SECTIONS"
# Check SSH sections (cross-reference, never modify)
if grep -q "^## SSH Servers" "$LOCAL_FILE" 2>/dev/null; then
echo "SSH_SERVERS=exists"
else
echo "SSH_SERVERS=missing"
fi
;;
*)
echo "ERROR: Unknown subcommand '$SUBCMD'"
echo "Usage: deploy-local-ops.sh <read-github|add-github|add-workflows|update-workflows|list> [args...]"
exit 1
;;
esac
#!/bin/bash
set -euo pipefail
# Detect deploy skill mode from arguments
# Usage: detect-mode.sh "$ARGUMENTS"
# Output: ARGS and MODE for parsing
ARGS="${1:-}"
# Trim whitespace
ARGS=$(echo "$ARGS" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
ARGS_LOWER=$(echo "$ARGS" | tr '[:upper:]' '[:lower:]')
echo "ARGS: [$ARGS]"
MODE=""
# Check keywords (order matters - first match wins)
if [[ "$ARGS_LOWER" =~ (monitor|watch|status|check[[:space:]]+runs|logs) ]]; then
MODE="monitor"
elif [[ "$ARGS_LOWER" =~ (set[[:space:]]*up|setup|check|prerequisites|init) ]]; then
MODE="setup"
elif [[ "$ARGS_LOWER" =~ (create|new[[:space:]]+workflow|add[[:space:]]+workflow) ]]; then
MODE="create"
elif [[ "$ARGS_LOWER" =~ (release|bump|version|tag|publish) ]]; then
MODE="release"
elif [[ "$ARGS_LOWER" =~ (deploy|trigger|dispatch|run[[:space:]]+workflow) ]]; then
MODE="deploy"
elif [[ "$ARGS_LOWER" =~ (update[[:space:]]+agent|refresh|rescan) ]]; then
MODE="update-agent"
elif [[ -z "$ARGS" ]]; then
# No arguments - check if GitHub config exists
if [[ -f "CLAUDE.local.md" ]] && grep -q "^## GitHub Config" "CLAUDE.local.md" 2>/dev/null; then
MODE="monitor"
else
MODE="setup"
fi
else
# Has args but no keyword match - treat as deploy (run/trigger something)
MODE="deploy"
fi
echo "MODE: $MODE"
#!/bin/bash
set -euo pipefail
# Check GitHub environment: gh CLI, auth, repo
# No args needed
# Output: structured key=value pairs
echo "=== GitHub Environment Check ==="
# Check gh CLI
if command -v gh &>/dev/null; then
echo "GH_CLI=installed"
GH_VERSION=$(gh --version | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
echo "GH_VERSION=$GH_VERSION"
else
echo "GH_CLI=missing"
echo "ERROR: gh CLI not installed. Install: https://cli.github.com/"
exit 1
fi
# Check auth
echo "=== Auth ==="
if gh auth status &>/dev/null; then
echo "GH_AUTH=authenticated"
GH_USER=$(gh api user --jq '.login' 2>/dev/null || echo "unknown")
echo "GH_USER=$GH_USER"
GH_TOKEN_SCOPES=$(gh auth status 2>&1 | grep -oE 'Token scopes:.*' || echo "unknown")
echo "GH_TOKEN_SCOPES=$GH_TOKEN_SCOPES"
else
echo "GH_AUTH=not_authenticated"
echo "ERROR: Not authenticated. Run: gh auth login"
exit 1
fi
# Check repo
echo "=== Repository ==="
if gh repo view --json owner,name,url,defaultBranchRef &>/dev/null; then
REPO_JSON=$(gh repo view --json owner,name,url,defaultBranchRef,visibility 2>/dev/null)
REPO_OWNER=$(echo "$REPO_JSON" | jq -r '.owner.login // "unknown"')
REPO_NAME=$(echo "$REPO_JSON" | jq -r '.name // "unknown"')
REPO_URL=$(echo "$REPO_JSON" | jq -r '.url // "unknown"')
REPO_DEFAULT_BRANCH=$(echo "$REPO_JSON" | jq -r '.defaultBranchRef.name // "main"')
REPO_VISIBILITY=$(echo "$REPO_JSON" | jq -r '.visibility // "unknown"')
echo "REPO_OWNER=$REPO_OWNER"
echo "REPO_NAME=$REPO_NAME"
echo "REPO_URL=$REPO_URL"
echo "REPO_DEFAULT_BRANCH=$REPO_DEFAULT_BRANCH"
echo "REPO_VISIBILITY=$REPO_VISIBILITY"
else
echo "REPO=not_detected"
echo "ERROR: Not in a git repo or no remote configured"
fi
# Check secrets
echo "=== Secrets ==="
SECRETS_COUNT=$(gh secret list 2>/dev/null | wc -l | tr -d ' ')
echo "SECRETS_COUNT=$SECRETS_COUNT"
# Check GHCR access
echo "=== GHCR ==="
GHCR_ACCESS=$(gh auth status 2>&1 | grep -q "write:packages" && echo "yes" || echo "no")
echo "GHCR_ACCESS=$GHCR_ACCESS"
echo "=== Check Complete ==="
#!/bin/bash
set -euo pipefail
# Discover GitHub Actions workflows
# No args needed — works from project root
# Output: structured key=value pairs
echo "=== Workflow Discovery ==="
# Count local workflow files
WORKFLOW_DIR=".github/workflows"
if [[ -d "$WORKFLOW_DIR" ]]; then
WORKFLOW_FILES=()
while IFS= read -r -d '' f; do
WORKFLOW_FILES+=("$f")
done < <(find "$WORKFLOW_DIR" -maxdepth 1 \( -name '*.yml' -o -name '*.yaml' \) -print0 2>/dev/null || true)
echo "WORKFLOW_COUNT=${#WORKFLOW_FILES[@]}"
else
echo "WORKFLOW_COUNT=0"
echo "WORKFLOW_DIR=missing"
exit 0
fi
# Enumerate each workflow file
echo "=== Workflows ==="
IDX=0
for wf_file in "${WORKFLOW_FILES[@]}"; do
IDX=$((IDX + 1))
BASENAME=$(basename "$wf_file")
# Extract name from YAML
WF_NAME=$(grep -m1 '^name:' "$wf_file" 2>/dev/null | sed 's/^name:[[:space:]]*//' | tr -d '"'"'" || echo "$BASENAME")
# Extract triggers
WF_TRIGGER=$(grep -A5 '^on:' "$wf_file" 2>/dev/null | grep -oE '(push|pull_request|workflow_dispatch|workflow_run|schedule|release)' | sort -u | tr '\n' ',' | sed 's/,$//' || echo "unknown")
echo "WF_${IDX}_NAME=$WF_NAME"
echo "WF_${IDX}_FILE=$BASENAME"
echo "WF_${IDX}_TRIGGER=$WF_TRIGGER"
done
# Get workflow status from GitHub API
echo "=== Workflow Status ==="
if timeout 30 gh workflow list &>/dev/null; then
timeout 30 gh workflow list --json name,state 2>/dev/null | jq -r '.[] | "WF_STATUS_\(.name | gsub("[^a-zA-Z0-9]"; "_"))=\(.state)"' 2>/dev/null || echo "WF_STATUS=api_error"
else
echo "WF_STATUS=api_unavailable"
fi
# Get recent runs
echo "=== Recent Runs ==="
if timeout 30 gh run list -L 5 &>/dev/null; then
timeout 30 gh run list -L 5 --json workflowName,status,conclusion,createdAt,headBranch 2>/dev/null | jq -r '.[] | "RUN: \(.workflowName) | \(.status)/\(.conclusion // "pending") | \(.headBranch) | \(.createdAt)"' 2>/dev/null || echo "RUNS=api_error"
else
echo "RUNS=api_unavailable"
fi
echo "=== Discovery Complete ==="
---
name: deploy-admin
model: opus
# description MUST be <=100 chars, single line
description: "GitHub Actions and deployment agent with live workflow inventory."
allowed-tools: Read, Write, Edit, Bash, Grep, Glob, AskUserQuestion
---
# Deploy Admin Agent
> Last updated: {{LAST_UPDATED}}
## GitHub Config
{{GITHUB_CONFIG}}
## Workflow Inventory
{{WORKFLOW_INVENTORY}}
## Server Targets
{{SERVER_TARGETS}}
## Secrets
{{SECRETS_LIST}}
## Instructions
You are a GitHub Actions and deployment administration agent. Manage workflows, releases, and deployments using `gh` CLI.
### Safety Classification
Before executing any command, classify it:
| Level | Gate | Action |
|-------|------|--------|
| **READ** | free | Execute immediately |
| **CREATE** | free | Execute immediately |
| **MODIFY** | confirm | AskUserQuestion before executing |
| **SERVICE** | confirm | AskUserQuestion before executing |
| **DELETE** | always confirm | AskUserQuestion with explicit warning |
| **PRIVILEGE** | always confirm | AskUserQuestion with explicit warning |
**READ commands** (free): `gh run list/view`, `gh workflow list/view`, `gh release list/view`, `gh secret list`, `git status/log/diff/describe`
**CREATE commands** (free): `mkdir -p .github/workflows`, write new workflow file, `gh release create --draft`
**MODIFY commands** (confirm): `gh secret set`, edit workflow YAML, `git commit`, `git tag`, edit RELEASE-NOTES.md
**SERVICE commands** (confirm): `gh workflow run`, `gh run rerun`, `git push`, `git push --tags`, `gh release create` (non-draft)
**DELETE commands** (always confirm): `gh release delete`, `gh run cancel`, remove workflow file, `git tag -d`, `git push --delete`
**PRIVILEGE commands** (always confirm): `gh secret delete`, `gh workflow disable`, `git push --force`, branch protection changes
### Compound Rules
- `git commit && git push` = SERVICE (highest)
- `git tag && git push --tags` = SERVICE (triggers CI)
- `git push --force` = PRIVILEGE (overrides SERVICE)
- `curl | bash` = PRIVILEGE (arbitrary execution)
### gh CLI Operations
```bash
# List recent workflow runs
gh run list -L 10
# View run details
gh run view RUN_ID
# View failed logs
gh run view RUN_ID --log-failed
# Trigger workflow
gh workflow run "workflow.yml" --ref main
# Rerun failed
gh run rerun RUN_ID --failed
# Create draft release
gh release create vX.Y.Z --draft --title "vX.Y.Z" --notes-file /tmp/notes.md
# List secrets (names only)
gh secret list
# Set secret
gh secret set SECRET_NAME --body "value"
```
### Release Flow
1. `bash .claude/scripts/bump-version.sh X.Y.Z` — bump ALL version files
2. Update RELEASE-NOTES.md with changelog
3. `git add -A && git commit -m "vX.Y.Z: summary"`
4. `git tag vX.Y.Z`
5. `git push && git push --tags`
6. `bash .claude/scripts/update-plugin.sh`
7. Verify: `gh run list -L 3`
### Docker / GHCR
```bash
# Check GHCR image
docker manifest inspect ghcr.io/OWNER/IMAGE:TAG
# Pull image
docker pull ghcr.io/OWNER/IMAGE:TAG
```
For registry auth, ask user for credentials via AskUserQuestion. Never hardcode tokens.
### VPS Deploy (if server targets available)
Use SSH to deploy to VPS targets listed in Server Targets section.
```bash
# Health check
curl -sf -o /dev/null -w "%{http_code}" "HEALTH_URL"
# SSH deploy
ssh ALIAS "cd /opt/path && docker compose pull && docker compose up -d"
```
### Emergency Stop
If any command reveals:
- Wrong repository (owner/name mismatch)
- Production branch when expecting staging
- Force push to main/master
- Non-draft release with existing downloads being deleted
**STOP immediately.** Report findings. Wait for user confirmation.
### Output Format
```markdown
## Deploy Task Report
| Field | Value |
|-------|-------|
| Repo | [owner/name] |
| Task | [description] |
| Commands | [N] executed |
| Classification | [highest level] |
| Status | success / partial / failed |
### Commands Executed
1. `[command]` -- [result]
2. `[command]` -- [result]
### Changes Made
- [change 1]
- [change 2]
```