Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
384 changes: 384 additions & 0 deletions .github/workflows/git-ape-deck-build.yml

Large diffs are not rendered by default.

135 changes: 135 additions & 0 deletions .github/workflows/git-ape-workshop-sync.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
name: "Git-Ape: Workshop Sync"

on:
push:
branches: [main]
paths:
- '.github/agents/**'
- '.github/skills/**'
- '.github/workflows/git-ape-plan.yml'
- '.github/workflows/git-ape-deploy.yml'
- '.github/workflows/git-ape-destroy.yml'
- '.github/workflows/git-ape-verify.yml'

permissions:
contents: read
issues: write

jobs:
detect-and-issue:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 2

- name: Detect changed feature files
id: changes
run: |
CHANGED=$(git diff HEAD~1 --name-only -- \
'.github/agents/' \
'.github/skills/' \
'.github/workflows/git-ape-plan.yml' \
'.github/workflows/git-ape-deploy.yml' \
'.github/workflows/git-ape-destroy.yml' \
'.github/workflows/git-ape-verify.yml' \
)

if [[ -z "$CHANGED" ]]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi

echo "skip=false" >> "$GITHUB_OUTPUT"

# Categorize changes
AGENTS_CHANGED=$(echo "$CHANGED" | grep -c '\.github/agents/' || true)
SKILLS_CHANGED=$(echo "$CHANGED" | grep -c '\.github/skills/' || true)
WORKFLOWS_CHANGED=$(echo "$CHANGED" | grep -c '\.github/workflows/' || true)

# Map to impacted tracks
TRACKS=""
if [[ "$AGENTS_CHANGED" -gt 0 ]]; then
TRACKS="Track 1 (Zero to Deploy), Track 2 (Deploy Like a Pro)"
fi
if [[ "$SKILLS_CHANGED" -gt 0 ]]; then
TRACKS="${TRACKS:+$TRACKS, }Track 2 (Deploy Like a Pro), Track 4 (Executive Briefing)"
fi
if [[ "$WORKFLOWS_CHANGED" -gt 0 ]]; then
TRACKS="${TRACKS:+$TRACKS, }Track 3 (Platform Engineering)"
fi

# Deduplicate track list
TRACKS=$(echo "$TRACKS" | tr ',' '\n' | sort -u | tr '\n' ',' | sed 's/,$//' | sed 's/^,//')

{
echo "file_list<<EOF"
echo "$CHANGED"
echo "EOF"
echo "agents_changed=$AGENTS_CHANGED"
echo "skills_changed=$SKILLS_CHANGED"
echo "workflows_changed=$WORKFLOWS_CHANGED"
echo "tracks=$TRACKS"
} >> "$GITHUB_OUTPUT"

- name: Check for existing open issue
if: steps.changes.outputs.skip != 'true'
id: existing
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
OPEN_COUNT=$(gh issue list --label workshop-sync --state open --json number --jq 'length')
echo "has_open=$([[ "$OPEN_COUNT" -gt 0 ]] && echo true || echo false)" >> "$GITHUB_OUTPUT"

- name: Create workshop sync issue
if: steps.changes.outputs.skip != 'true' && steps.existing.outputs.has_open != 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
FILE_LIST: ${{ steps.changes.outputs.file_list }}
AGENTS_CHANGED: ${{ steps.changes.outputs.agents_changed }}
SKILLS_CHANGED: ${{ steps.changes.outputs.skills_changed }}
WORKFLOWS_CHANGED: ${{ steps.changes.outputs.workflows_changed }}
TRACKS: ${{ steps.changes.outputs.tracks }}
COMMIT_SHA: ${{ github.sha }}
run: |
# Build change summary
SUMMARY=""
if [[ "$AGENTS_CHANGED" -gt 0 ]]; then
SUMMARY="$AGENTS_CHANGED agent(s)"
fi
if [[ "$SKILLS_CHANGED" -gt 0 ]]; then
SUMMARY="${SUMMARY:+$SUMMARY, }$SKILLS_CHANGED skill(s)"
fi
if [[ "$WORKFLOWS_CHANGED" -gt 0 ]]; then
SUMMARY="${SUMMARY:+$SUMMARY, }$WORKFLOWS_CHANGED workflow(s)"
fi

# Read issue template and substitute variables
BODY=$(cat .github/workshop-sync-issue-template.md)
BODY="${BODY//\{\{SUMMARY\}\}/$SUMMARY}"
BODY="${BODY//\{\{FILE_LIST\}\}/$FILE_LIST}"
BODY="${BODY//\{\{TRACKS\}\}/$TRACKS}"
BODY="${BODY//\{\{COMMIT_SHA\}\}/$COMMIT_SHA}"

gh issue create \
--title "[Workshop Sync] Update workshops for: $SUMMARY" \
--body "$BODY" \
--label "workshop-sync"

- name: Append to existing issue
if: steps.changes.outputs.skip != 'true' && steps.existing.outputs.has_open == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
FILE_LIST: ${{ steps.changes.outputs.file_list }}
COMMIT_SHA: ${{ github.sha }}
run: |
ISSUE_NUM=$(gh issue list --label workshop-sync --state open --json number --jq '.[0].number')
gh issue comment "$ISSUE_NUM" \
--body "**Additional changes detected** (commit $COMMIT_SHA):

\`\`\`
$FILE_LIST
\`\`\`

Please include these changes in the workshop update."
90 changes: 90 additions & 0 deletions .github/workflows/workshop-quality-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
name: Workshop Quality Check

on:
pull_request:
paths:
- 'workshops/track-*/*_deck.md'
- 'workshops/track-*/lab-*.md'
- 'workshops/shared/**'
- 'scripts/render-workshop-decks.js'
- 'scripts/verify-workshop-decks.js'

permissions:
contents: read
pull-requests: write

concurrency:
group: workshop-quality
cancel-in-progress: true

jobs:
check:
runs-on: ubuntu-latest
if: github.head_ref != 'auto/deck-rebuild'

steps:
- name: Checkout PR
uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '24'

- name: Prereq script syntax check
run: |
set -e
for s in workshops/shared/check-track-*-prereqs.sh; do
bash -n "$s"
done
echo OK

- name: Lab transcript presence check
id: presence
run: |
set -e
OUT=$(mktemp)
MISSING=0
for lab in workshops/track-*/lab-*.md; do
base=$(basename "$lab" .md)
dir=$(dirname "$lab")
transcript="$dir/evidence/${base}-transcript.md"
if [[ -f "$transcript" ]]; then
echo "- ok: $transcript" >> "$OUT"
else
echo "- missing: $transcript" >> "$OUT"
MISSING=$((MISSING+1))
fi
done
echo "missing=$MISSING" >> "$GITHUB_OUTPUT"
cat "$OUT"

- name: Post advisory comment
uses: actions/github-script@v9
with:
script: |
const marker = '<!-- workshop-quality-check -->';
const body = `${marker}\n## Workshop quality check (advisory)\n\nThis advisory checks: prereq script syntax, lab transcript presence, deck render scripts.\n\nSee \`workshops/CUSTOMER-READINESS-CHECKLIST.md\` for the full pre-customer checklist.\n\n_Non-blocking._`;
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const existing = comments.find(c => (c.body || '').includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});
}
54 changes: 54 additions & 0 deletions scripts/_extract-capture-cmds.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""_extract-capture-cmds.py — extract and run bash commands tagged with
`# capture-evidence` from a lab markdown file. Emits transcript-format
output to stdout.

Used by scripts/capture-lab-evidence.sh.
"""
import re
import subprocess
import sys

def main():
if len(sys.argv) != 2:
print("usage: _extract-capture-cmds.py <lab.md>", file=sys.stderr)
sys.exit(1)
path = sys.argv[1]
text = open(path).read()

# Find ```bash blocks
blocks = re.findall(r'```bash\s*\n(.*?)```', text, flags=re.S)
n_captured = 0
for block in blocks:
lines = block.splitlines()
# Only run if the block has the marker comment
if not any('# capture-evidence' in ln for ln in lines):
continue
for ln in lines:
stripped = ln.strip()
if not stripped or stripped.startswith('#'):
continue
# Run the command, capture stdout+stderr
print(f"$ {stripped}")
try:
r = subprocess.run(
stripped,
shell=True,
capture_output=True,
text=True,
timeout=60,
)
out = (r.stdout or '') + (r.stderr or '')
print(out.rstrip())
except subprocess.TimeoutExpired:
print("(timeout)")
except Exception as e:
print(f"(error: {e})")
print()
n_captured += 1
if n_captured == 0:
print("> No commands tagged with `# capture-evidence` in this lab.")
print("> Add the comment inside a ```bash block to capture its output.")

if __name__ == '__main__':
main()
50 changes: 50 additions & 0 deletions scripts/capture-lab-evidence.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
# capture-lab-evidence.sh -- record commands from a lab into a transcript.
#
# Usage:
# bash scripts/capture-lab-evidence.sh <lab-md-file>
#
# Scans the lab markdown for ```bash blocks that contain a # capture-evidence
# directive in a comment line. Runs each such command and writes the literal
# output to evidence/<lab-basename>-transcript.md alongside the lab. Pipes
# through scripts/redact-evidence.js.
#
# Does NOT run @git-ape chat invocations or any command needing interactive
# approval; those are facilitator-captured during dry-run.

set -uo pipefail

LAB="${1:-}"
if [[ -z "$LAB" || ! -f "$LAB" ]]; then
echo "usage: $0 <lab-md-file>" >&2
exit 1
fi

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
LAB_DIR=$(dirname "$LAB")
LAB_BASE=$(basename "$LAB" .md)
OUT_DIR="$LAB_DIR/evidence"
OUT_FILE="$OUT_DIR/${LAB_BASE}-transcript.md"
mkdir -p "$OUT_DIR"

TMP_RAW=$(mktemp)
TMP_REDACT=$(mktemp)

{
echo "# ${LAB_BASE} Transcript"
echo ""
echo "> Captured: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "> Environment: $(uname -s) ${USER:-unknown}"
echo "> Subscription: <REDACTED-SUB>"
echo ""
echo "## Captured commands"
echo ""
} > "$TMP_RAW"

python3 "$REPO_ROOT/scripts/_extract-capture-cmds.py" "$LAB" >> "$TMP_RAW"

node "$REPO_ROOT/scripts/redact-evidence.js" < "$TMP_RAW" > "$TMP_REDACT"
mv "$TMP_REDACT" "$OUT_FILE"
rm -f "$TMP_RAW"

echo "Wrote: $OUT_FILE"
Loading
Loading