From 8ad674e7b1bd3f0bd491c6206dd57d42376eaa3e Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Tue, 14 Apr 2026 15:55:12 +0000 Subject: [PATCH] fix: postmortem pipeline uses Vault for SSH key (not Woodpecker secrets) Pipeline authenticates to Vault via K8s SA JWT, fetches devvm_ssh_key from secret/ci/infra, SSHes to DevVM to run Claude Code headlessly. Co-Authored-By: Claude Opus 4.6 (1M context) --- .woodpecker/postmortem-todos.yml | 68 ++++++++++++++++---------------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/.woodpecker/postmortem-todos.yml b/.woodpecker/postmortem-todos.yml index 28a6880f..56fbd603 100644 --- a/.woodpecker/postmortem-todos.yml +++ b/.woodpecker/postmortem-todos.yml @@ -8,72 +8,74 @@ when: - '.woodpecker/**' steps: - - name: parse-todos + - name: parse-and-implement image: python:3.12-alpine commands: - - apk add --no-cache jq git openssh-client + - apk add --no-cache jq curl git openssh-client # Find which post-mortem changed - PM_FILE=$(git diff HEAD~1 --name-only | grep 'docs/post-mortems/.*\.md' | head -1) - | if [ -z "$PM_FILE" ]; then echo "No post-mortem markdown changes detected" - echo '{"skip": true}' > /tmp/todos.json exit 0 fi - echo "Post-mortem changed: $PM_FILE" # Check if there are new TODOs (not just TODO→Done updates) - | - if ! git diff HEAD~1 -- "$PM_FILE" | grep -q '+.*| TODO |'; then - echo "No new TODOs added (only status updates)" - echo '{"skip": true}' > /tmp/todos.json + if ! git diff HEAD~1 -- "$PM_FILE" | grep -q '+.*TODO'; then + echo "No new TODOs added (only status updates) — skipping" exit 0 fi # Parse TODOs - - python3 scripts/parse-postmortem-todos.sh "$PM_FILE" > /tmp/todos.json || bash scripts/parse-postmortem-todos.sh "$PM_FILE" > /tmp/todos.json + - bash scripts/parse-postmortem-todos.sh "$PM_FILE" > /tmp/todos.json - cat /tmp/todos.json - TODO_COUNT=$(jq '.safe_todos' /tmp/todos.json) - - echo "$TODO_COUNT auto-implementable TODO(s) found" - | if [ "$TODO_COUNT" -eq 0 ]; then - echo "No auto-implementable TODOs (all are Architecture/Investigation/Migration type)" - echo '{"skip": true}' > /tmp/todos.json - fi - - - name: implement-todos - image: alpine - commands: - - | - if [ "$(jq -r '.skip // empty' /tmp/todos.json 2>/dev/null)" = "true" ]; then - echo "Skipping — no TODOs to implement" + echo "No auto-implementable TODOs — skipping" exit 0 fi - - apk add --no-cache openssh-client jq - - PM_FILE=$(jq -r '.file' /tmp/todos.json) - - PM_DATE=$(echo "$PM_FILE" | grep -oP '\d{4}-\d{2}-\d{2}') - - TODOS=$(cat /tmp/todos.json) - # SSH to DevVM and run Claude Code in headless mode + - echo "$TODO_COUNT safe TODO(s) to implement" + # Vault: authenticate via K8s SA JWT and fetch DevVM SSH key - | - ssh -o StrictHostKeyChecking=no wizard@10.0.10.10 \ + SA_TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) + VAULT_TOKEN=$(curl -sf -X POST http://vault-active.vault.svc.cluster.local:8200/v1/auth/kubernetes/login \ + -d "{\"role\":\"ci\",\"jwt\":\"$SA_TOKEN\"}" | jq -r .auth.client_token) + if [ -z "$VAULT_TOKEN" ] || [ "$VAULT_TOKEN" = "null" ]; then + echo "ERROR: Vault authentication failed" + exit 1 + fi + - | + curl -sf -H "X-Vault-Token: $VAULT_TOKEN" \ + http://vault-active.vault.svc.cluster.local:8200/v1/secret/data/ci/infra | \ + jq -r '.data.data.devvm_ssh_key' > /tmp/devvm-key + chmod 600 /tmp/devvm-key + if [ ! -s /tmp/devvm-key ]; then + echo "ERROR: Failed to fetch DevVM SSH key from Vault" + exit 1 + fi + echo "SSH key fetched from Vault" + # SSH to DevVM and run Claude Code headless + - | + TODOS=$(cat /tmp/todos.json | tr '\n' ' ') + ssh -i /tmp/devvm-key -o StrictHostKeyChecking=no wizard@10.0.10.10 \ "cd ~/code/infra && git pull && claude -p \ --agent postmortem-todo-resolver \ --dangerously-skip-permissions \ --max-budget-usd 5 \ - 'Implement the auto-implementable TODOs from $PM_FILE. Here is the parsed TODO list: $TODOS'" - secrets: - - ssh_deploy_key + 'Implement the auto-implementable TODOs from $PM_FILE. Parsed TODOs: $TODOS'" + - rm -f /tmp/devvm-key - name: notify-slack image: alpine commands: - - apk add --no-cache curl jq + - apk add --no-cache curl - | - PM_FILE=$(jq -r '.file // "unknown"' /tmp/todos.json 2>/dev/null) - SAFE=$(jq -r '.safe_todos // 0' /tmp/todos.json 2>/dev/null) - SKIPPED=$(jq -r '.skipped_todos // 0' /tmp/todos.json 2>/dev/null) + PM_FILE=$(git diff HEAD~1 --name-only | grep 'docs/post-mortems/.*\.md' | head -1) STATUS="${CI_PIPELINE_STATUS:-unknown}" - curl -sf -X POST "$SLACK_WEBHOOK_URL" \ + curl -sf -X POST "https://hooks.slack.com/services/$${SLACK_WEBHOOK}" \ -H "Content-Type: application/json" \ - -d "{\"text\": \"*Post-mortem TODO resolver* ($STATUS)\\n• File: \`$PM_FILE\`\\n• Safe TODOs processed: $SAFE\\n• Skipped (needs human): $SKIPPED\\n• Pipeline: ${CI_PIPELINE_URL:-N/A}\"}" || true + -d "{\"text\": \"*Post-mortem TODO resolver* ($STATUS)\\nFile: \`$PM_FILE\`\\nPipeline: ${CI_PIPELINE_URL:-N/A}\"}" || true secrets: - slack_webhook when: