Skip to content

chore: implement audit quick wins and architectural doc updates #5617

chore: implement audit quick wins and architectural doc updates

chore: implement audit quick wins and architectural doc updates #5617

Workflow file for this run

name: "Impl: Generate"

Check failure on line 1 in .github/workflows/impl-generate.yml

View workflow run for this annotation

GitHub Actions / .github/workflows/impl-generate.yml

Invalid workflow file

(Line: 42, Col: 5): Unexpected value ''
run-name: "Generate: ${{ inputs.library || github.event.label.name }} for ${{ inputs.specification_id || 'issue' }}"
# Generates single library implementation
# Triggers:
# - generate:{library} label on spec-ready issue
# - workflow_dispatch with specification_id + library
on:
issues:
types: [labeled]
workflow_dispatch:
inputs:
specification_id:
description: "Specification ID (e.g., scatter-basic)"
required: true
type: string
library:
description: "Library to generate"
required: true
type: choice
options:
- matplotlib
- seaborn
- plotly
- bokeh
- altair
- plotnine
- pygal
- highcharts
- letsplot
issue_number:
description: "Issue number (optional, for tracking)"
required: false
type: string
# Global concurrency: max 3 concurrent implementation workflows
concurrency:
group: impl-generate-${{ inputs.specification_id || github.event.issue.number }}-${{ inputs.library || github.event.label.name }}
cancel-in-progress: false
env:
# Library dependencies mapping
# Per-library deps now come from `pyproject.toml` `lib-{library}` extras —
# see [project.optional-dependencies] there. Keeping this list in sync was
# painful and silently drifted (this env block lagged the pyproject floors
# by a full major version on plotly/altair). Single source of truth wins.
jobs:
generate:
# Run on label trigger OR workflow_dispatch
if: >
(github.event_name == 'workflow_dispatch') ||
(github.event_name == 'issues' && startsWith(github.event.label.name, 'generate:'))
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
actions: write
id-token: write
outputs:
success: ${{ steps.result.outputs.success }}
pr_number: ${{ steps.pr.outputs.pr_number }}
steps:
# ========================================================================
# Setup: Extract inputs and validate
# ========================================================================
- name: Extract inputs
id: inputs
env:
LABEL_NAME: ${{ github.event.label.name }}
ISSUE_TITLE: ${{ github.event.issue.title }}
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
# From workflow_dispatch
SPEC_ID="${{ inputs.specification_id }}"
LIBRARY="${{ inputs.library }}"
ISSUE="${{ inputs.issue_number }}"
else
# From label trigger: generate:{library}
LIBRARY=$(echo "$LABEL_NAME" | sed 's/^generate://')
# Extract spec ID from issue title: [spec-id] ...
SPEC_ID=$(echo "$ISSUE_TITLE" | sed -n 's/^\[\([a-z0-9-]*\)\].*/\1/p')
ISSUE="${{ github.event.issue.number }}"
fi
if [ -z "$SPEC_ID" ]; then
echo "::error::Could not determine specification ID"
exit 1
fi
if [ -z "$LIBRARY" ]; then
echo "::error::Could not determine library"
exit 1
fi
# Language: only python supported today. Future multi-language work will derive this per spec.
LANGUAGE="python"
echo "specification_id=$SPEC_ID" >> $GITHUB_OUTPUT
echo "library=$LIBRARY" >> $GITHUB_OUTPUT
echo "language=$LANGUAGE" >> $GITHUB_OUTPUT
echo "issue_number=$ISSUE" >> $GITHUB_OUTPUT
echo "::notice::Generating $LANGUAGE/$LIBRARY for $SPEC_ID (issue: ${ISSUE:-none})"
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Read issue number from specification.yaml (fallback)
id: spec_issue
if: steps.inputs.outputs.issue_number == ''
env:
SPEC_ID: ${{ steps.inputs.outputs.specification_id }}
run: |
SPEC_YAML="plots/${SPEC_ID}/specification.yaml"
if [ -f "$SPEC_YAML" ]; then
# Extract issue number using yq for robust YAML parsing
ISSUE=$(yq '.issue' "$SPEC_YAML" 2>/dev/null || echo "")
if [ -n "$ISSUE" ] && [ "$ISSUE" != "null" ]; then
echo "issue_number=$ISSUE" >> $GITHUB_OUTPUT
echo "::notice::Found issue #$ISSUE from specification.yaml"
else
echo "issue_number=" >> $GITHUB_OUTPUT
echo "::warning::No issue number in specification.yaml"
fi
else
echo "issue_number=" >> $GITHUB_OUTPUT
echo "::warning::specification.yaml not found"
fi
# Consolidate issue number from inputs or fallback
- name: Set final issue number
id: issue
run: |
ISSUE="${{ steps.inputs.outputs.issue_number || steps.spec_issue.outputs.issue_number }}"
echo "number=$ISSUE" >> $GITHUB_OUTPUT
if [ -n "$ISSUE" ]; then
echo "::notice::Using issue #$ISSUE for tracking"
else
echo "::warning::No issue number available - PR will not have Parent Issue link"
fi
- name: Validate specification exists
env:
SPEC_ID: ${{ steps.inputs.outputs.specification_id }}
run: |
if [ ! -f "plots/${SPEC_ID}/specification.md" ]; then
echo "::error::Specification not found: plots/${SPEC_ID}/specification.md"
exit 1
fi
- name: Reopen issue if closed
if: steps.issue.outputs.number != ''
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ISSUE: ${{ steps.issue.outputs.number }}
run: |
# Reopen issue so it's visible during implementation
gh issue reopen "$ISSUE" 2>/dev/null || true
- name: Add pending label
if: steps.issue.outputs.number != ''
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
LIBRARY: ${{ steps.inputs.outputs.library }}
ISSUE: ${{ steps.issue.outputs.number }}
run: |
gh issue edit "$ISSUE" --add-label "impl:${LIBRARY}:pending" 2>/dev/null || true
# ========================================================================
# Setup: Python and dependencies
# ========================================================================
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: '3.14'
- name: Install uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y pngquant
- name: Install dependencies
env:
LIBRARY: ${{ steps.inputs.outputs.library }}
run: |
uv venv .venv
source .venv/bin/activate
# Source of truth: pyproject.toml lib-${LIBRARY} extras
uv pip install -e ".[lib-${LIBRARY}]" ruff pillow pyyaml
- name: Setup Chrome for Highcharts
if: steps.inputs.outputs.library == 'highcharts'
uses: browser-actions/setup-chrome@4f8e94349a351df0f048634f25fec36c3c91eded # v2
with:
chrome-version: stable
# ========================================================================
# Generate: Create implementation branch and code
# ========================================================================
- name: Create implementation branch
id: branch
env:
SPEC_ID: ${{ steps.inputs.outputs.specification_id }}
LIBRARY: ${{ steps.inputs.outputs.library }}
run: |
BRANCH="implementation/${SPEC_ID}/${LIBRARY}"
echo "branch=$BRANCH" >> $GITHUB_OUTPUT
# Delete branch if exists (regeneration)
git push origin --delete "$BRANCH" 2>/dev/null || true
# Create fresh branch from main
git checkout -b "$BRANCH" origin/main
echo "::notice::Created branch: $BRANCH"
- name: Check for existing implementation (regeneration)
id: existing
env:
SPEC_ID: ${{ steps.inputs.outputs.specification_id }}
LANGUAGE: ${{ steps.inputs.outputs.language }}
LIBRARY: ${{ steps.inputs.outputs.library }}
run: |
METADATA_FILE="plots/${SPEC_ID}/metadata/${LANGUAGE}/${LIBRARY}.yaml"
IMPL_FILE="plots/${SPEC_ID}/implementations/${LANGUAGE}/${LIBRARY}.py"
if [ -f "$METADATA_FILE" ] && [ -f "$IMPL_FILE" ]; then
echo "is_regeneration=true" >> $GITHUB_OUTPUT
echo "::notice::Regeneration detected - will read previous review feedback"
else
echo "is_regeneration=false" >> $GITHUB_OUTPUT
fi
- name: Extract previous review feedback (regeneration)
id: prev_review
if: steps.existing.outputs.is_regeneration == 'true'
env:
SPEC_ID: ${{ steps.inputs.outputs.specification_id }}
LANGUAGE: ${{ steps.inputs.outputs.language }}
LIBRARY: ${{ steps.inputs.outputs.library }}
run: |
.venv/bin/python3 - <<'PY'
import os, pathlib, yaml
spec = os.environ["SPEC_ID"]
lang = os.environ["LANGUAGE"]
lib = os.environ["LIBRARY"]
meta_path = pathlib.Path(f"plots/{spec}/metadata/{lang}/{lib}.yaml")
data = yaml.safe_load(meta_path.read_text(encoding="utf-8")) or {}
review = data.get("review") or {}
quality = data.get("quality_score")
lines = [f"# Previous Review for {spec} / {lang} / {lib}", ""]
lines.append(f"**Previous quality score:** {quality if quality is not None else 'n/a'}")
lines.append("")
desc = review.get("image_description")
if desc:
lines += ["## Previous image description", str(desc).strip(), ""]
strengths = review.get("strengths") or []
if strengths:
lines.append("## Strengths (KEEP these)")
lines += [f"- {s}" for s in strengths]
lines.append("")
weaknesses = review.get("weaknesses") or []
if weaknesses:
lines.append("## Weaknesses (FIX these)")
lines += [f"- {w}" for w in weaknesses]
lines.append("")
checklist = review.get("criteria_checklist") or {}
if checklist:
lines.append("## Criteria checklist (focus on items that failed)")
for cat, payload in checklist.items():
payload = payload or {}
score = payload.get("score", "?")
max_score = payload.get("max", "?")
lines.append(f"### {cat} ({score}/{max_score})")
for item in payload.get("items") or []:
item = item or {}
mark = "✅" if item.get("passed") else "❌"
lines.append(
f"- {mark} {item.get('id', '?')} {item.get('name', '')}: {item.get('comment', '')}"
)
lines.append("")
pathlib.Path("/tmp/anyplot-prev-review.md").write_text("\n".join(lines), encoding="utf-8")
print(f"::notice::Extracted previous review (score={quality}) to /tmp/anyplot-prev-review.md")
PY
- name: Ensure implementation directories exist
env:
SPEC_ID: ${{ steps.inputs.outputs.specification_id }}
LANGUAGE: ${{ steps.inputs.outputs.language }}
run: |
mkdir -p "plots/${SPEC_ID}/implementations/${LANGUAGE}"
mkdir -p "plots/${SPEC_ID}/metadata/${LANGUAGE}"
echo "::notice::Ensured implementation + metadata directories exist for language '${LANGUAGE}'"
- name: Run Claude Code to generate implementation
id: claude
continue-on-error: true
timeout-minutes: 60
uses: anthropics/claude-code-action@567fe954a4527e81f132d87d1bdbcc94f7737434 # v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
claude_args: "--model sonnet"
# bulk-generate dispatches us from the github-actions bot; explicitly allow it.
allowed_bots: '*'
prompt: |
Read `prompts/workflow-prompts/impl-generate-claude.md` and follow those instructions.
Variables for this run:
- LANGUAGE: ${{ steps.inputs.outputs.language }}
- LIBRARY: ${{ steps.inputs.outputs.library }}
- SPEC_ID: ${{ steps.inputs.outputs.specification_id }}
- IS_REGENERATION: ${{ steps.existing.outputs.is_regeneration }}
- name: Retry Claude (on failure)
if: steps.claude.outcome == 'failure'
id: claude_retry
timeout-minutes: 60
uses: anthropics/claude-code-action@567fe954a4527e81f132d87d1bdbcc94f7737434 # v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
claude_args: "--model sonnet"
# bulk-generate dispatches us from the github-actions bot; explicitly allow it.
allowed_bots: '*'
prompt: |
Read `prompts/workflow-prompts/impl-generate-claude.md` and follow those instructions.
Variables for this run:
- LANGUAGE: ${{ steps.inputs.outputs.language }}
- LIBRARY: ${{ steps.inputs.outputs.library }}
- SPEC_ID: ${{ steps.inputs.outputs.specification_id }}
- IS_REGENERATION: ${{ steps.existing.outputs.is_regeneration }}
# ========================================================================
# Create metadata file (before PR)
# ========================================================================
- name: Create library metadata file
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SPEC_ID: ${{ steps.inputs.outputs.specification_id }}
LANGUAGE: ${{ steps.inputs.outputs.language }}
LIBRARY: ${{ steps.inputs.outputs.library }}
ISSUE: ${{ steps.issue.outputs.number }}
BRANCH: ${{ steps.branch.outputs.branch }}
run: |
IMPL_DIR="plots/${SPEC_ID}/implementations/${LANGUAGE}"
# Save plot files before git operations (they are not committed and would be lost).
# Phase C emits plot-light.png + plot-dark.png (+ plot-light.html + plot-dark.html
# for interactive libs); legacy plot.png/plot.html is preserved during the transition.
mkdir -p /tmp/anyplot-plot-cache
rm -rf /tmp/anyplot-plot-cache/*
for f in plot-light.png plot-dark.png plot-light.html plot-dark.html plot.png plot.html; do
cp "${IMPL_DIR}/${f}" "/tmp/anyplot-plot-cache/${f}" 2>/dev/null || true
done
# Configure git auth (Claude's action configured it, but it's gone now)
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git"
# Sync with remote (Claude already pushed the code)
git fetch origin
# Check if remote branch exists before checkout (fixes branch-not-found error)
if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then
git checkout -B "$BRANCH" "origin/$BRANCH"
else
# Branch doesn't exist on remote - create fresh from main
echo "::warning::Remote branch $BRANCH not found, creating fresh from main"
git checkout -B "$BRANCH" origin/main
fi
# Restore plot files after git operations
mkdir -p "${IMPL_DIR}"
for f in plot-light.png plot-dark.png plot-light.html plot-dark.html plot.png plot.html; do
cp "/tmp/anyplot-plot-cache/${f}" "${IMPL_DIR}/${f}" 2>/dev/null || true
done
# Now create metadata file
METADATA_DIR="plots/${SPEC_ID}/metadata/${LANGUAGE}"
mkdir -p "${METADATA_DIR}"
METADATA_FILE="${METADATA_DIR}/${LIBRARY}.yaml"
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
mkdir -p "$METADATA_DIR"
# Get Python version (e.g., "3.13.1" from "Python 3.13.1")
PYTHON_VERSION=$(python3 --version 2>&1 | awk '{print $2}')
# Get library version from pip (use explicit venv path)
get_pip_version() {
.venv/bin/pip show "$1" 2>/dev/null | grep -i "^Version:" | awk '{print $2}'
}
case "$LIBRARY" in
letsplot)
LIBRARY_VERSION=$(get_pip_version "lets-plot")
;;
highcharts)
LIBRARY_VERSION=$(get_pip_version "highcharts-core")
;;
*)
LIBRARY_VERSION=$(get_pip_version "$LIBRARY")
;;
esac
# Fallback if version is empty
if [ -z "$LIBRARY_VERSION" ]; then
echo "::warning::Could not get version for $LIBRARY, trying alternative method"
# Map library names to Python module names
case "$LIBRARY" in
letsplot) PYTHON_MODULE="lets_plot" ;;
plotnine) PYTHON_MODULE="plotnine" ;;
highcharts) PYTHON_MODULE="highcharts_core" ;;
*) PYTHON_MODULE="$LIBRARY" ;;
esac
LIBRARY_VERSION=$(.venv/bin/python -c "import $PYTHON_MODULE; print(getattr($PYTHON_MODULE, '__version__', 'unknown'))" 2>/dev/null || echo "unknown")
fi
echo "::notice::Library version: $LIBRARY = $LIBRARY_VERSION"
# Interactive libraries additionally produce HTML previews (one per theme)
HAS_HTML="false"
case "$LIBRARY" in
plotly|bokeh|altair|highcharts|pygal|letsplot) HAS_HTML="true" ;;
esac
# Write metadata file using Python for proper YAML formatting
# Pass all variables inline to avoid export/env issues
.venv/bin/python3 -c "
import os, yaml
lib = '$LIBRARY'
spec = '$SPEC_ID'
language = '$LANGUAGE'
ts = '$TIMESTAMP'
run_id = ${{ github.run_id }}
issue = int('$ISSUE' or '0')
py_ver = '$PYTHON_VERSION'
lib_ver = '$LIBRARY_VERSION'
has_html = '$HAS_HTML' == 'true'
metadata_file = '$METADATA_FILE'
base_url = f'https://storage.googleapis.com/anyplot-images/plots/{spec}/{language}/{lib}'
# Preserve the original 'created' timestamp on regenerations.
# 'created' is the first-generation date — it must never be overwritten.
# Only 'updated' moves forward on every regen.
created_ts = ts
if os.path.exists(metadata_file):
try:
with open(metadata_file) as f:
existing = yaml.safe_load(f) or {}
if existing.get('created'):
created_ts = existing['created']
except Exception:
pass
data = {
'library': lib,
'language': language,
'specification_id': spec,
'created': created_ts,
'updated': ts,
# Reflects what claude_args=`--model sonnet` actually runs: whatever
# Claude Code's current "sonnet" alias resolves to.
# Use the family name instead of a frozen version string so the
# metadata doesn't go stale every model release.
'generated_by': 'claude-sonnet',
'workflow_run': run_id,
'issue': issue,
'python_version': py_ver,
'library_version': lib_ver,
# Theme-aware preview URLs (Phase C). Both PNG variants are always emitted.
'preview_url_light': f'{base_url}/plot-light.png',
'preview_url_dark': f'{base_url}/plot-dark.png',
'preview_html_light': f'{base_url}/plot-light.html' if has_html else None,
'preview_html_dark': f'{base_url}/plot-dark.html' if has_html else None,
'quality_score': None,
'review': {'strengths': [], 'weaknesses': []}
}
with open(metadata_file, 'w') as f:
f.write(f'# Per-library metadata for {lib} implementation of {spec}\n')
f.write('# Auto-generated by impl-generate.yml\n\n')
yaml.dump(data, f, default_flow_style=False, sort_keys=False, allow_unicode=True)
"
# Commit and push metadata (implementation already committed by Claude)
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
IMPL_FILE="plots/${SPEC_ID}/implementations/${LANGUAGE}/${LIBRARY}.py"
# Verify implementation file exists in the repository (Claude should have committed it)
if ! git ls-files --error-unmatch "$IMPL_FILE" >/dev/null 2>&1; then
echo "::error::Implementation file not found in repository - cannot commit"
echo "::error::Expected implementation at: $IMPL_FILE"
echo "::error::This indicates Claude failed to create the implementation file"
exit 1
fi
# Add metadata file
git add "$METADATA_FILE"
# Verify metadata file is staged
if ! git diff --cached --name-only | grep -q "$(basename "$METADATA_FILE")"; then
echo "::error::Metadata file not staged - cannot commit"
echo "::error::This indicates the metadata file was not added correctly"
exit 1
fi
git commit -m "chore(${LIBRARY}): add metadata for ${SPEC_ID}"
git push origin "$BRANCH"
echo "::notice::Created metadata file: $METADATA_FILE (py${PYTHON_VERSION}, ${LIBRARY}==${LIBRARY_VERSION})"
echo "::notice::Implementation verified present, metadata committed"
# ========================================================================
# Process images: optimize + thumbnail
# ========================================================================
- name: Process plot images (light + dark)
env:
SPEC_ID: ${{ steps.inputs.outputs.specification_id }}
LANGUAGE: ${{ steps.inputs.outputs.language }}
run: |
IMPL_DIR="plots/${SPEC_ID}/implementations/${LANGUAGE}"
if [ ! -f "$IMPL_DIR/plot-light.png" ] || [ ! -f "$IMPL_DIR/plot-dark.png" ]; then
echo "::error::Missing plot-{light,dark}.png — implementation failed to produce both theme renders"
echo "::error::Expected both ${IMPL_DIR}/plot-light.png and ${IMPL_DIR}/plot-dark.png"
ls -la "$IMPL_DIR/" || true
exit 1
fi
source .venv/bin/activate
# Optimize each theme PNG in place, then generate responsive variants
# (400/800/1200 x png/webp + full webp) named plot-{theme}_*.{png,webp}.
for theme in light dark; do
python -m core.images process \
"$IMPL_DIR/plot-${theme}.png" \
"$IMPL_DIR/plot-${theme}.png"
python -m core.images responsive \
"$IMPL_DIR/plot-${theme}.png" \
"$IMPL_DIR/"
done
echo "::notice::Processed both themes: plot-light + plot-dark (optimized + responsive variants)"
ls -la "$IMPL_DIR/"
# ========================================================================
# Create PR
# ========================================================================
- name: Create Pull Request
id: pr
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SPEC_ID: ${{ steps.inputs.outputs.specification_id }}
LANGUAGE: ${{ steps.inputs.outputs.language }}
LIBRARY: ${{ steps.inputs.outputs.library }}
ISSUE: ${{ steps.issue.outputs.number }}
BRANCH: ${{ steps.branch.outputs.branch }}
run: |
# Check if PR already exists
EXISTING_PR=$(gh pr list --head "$BRANCH" --base main --json number -q '.[0].number' 2>/dev/null || echo "")
if [ -n "$EXISTING_PR" ]; then
echo "pr_number=$EXISTING_PR" >> $GITHUB_OUTPUT
echo "pr_exists=true" >> $GITHUB_OUTPUT
echo "::notice::Using existing PR #$EXISTING_PR"
exit 0
fi
# Create PR body
BODY="## Implementation: \`${SPEC_ID}\` - ${LANGUAGE}/${LIBRARY}
Implements the **${LANGUAGE}/${LIBRARY}** version of \`${SPEC_ID}\`.
**File:** \`plots/${SPEC_ID}/implementations/${LANGUAGE}/${LIBRARY}.py\`"
if [ -n "$ISSUE" ]; then
BODY="${BODY}
**Parent Issue:** #${ISSUE}"
fi
BODY="${BODY}
---
:robot: *[impl-generate workflow](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})*"
# Create PR
PR_URL=$(gh pr create --base main --head "$BRANCH" \
--title "feat(${LIBRARY}): implement ${SPEC_ID}" \
--body "$BODY")
PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$')
echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
echo "pr_exists=true" >> $GITHUB_OUTPUT
echo "::notice::Created PR #$PR_NUMBER"
# ========================================================================
# Upload to GCS Staging
# ========================================================================
- name: Authenticate to GCP
if: steps.pr.outputs.pr_exists == 'true'
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3
with:
project_id: anyplot
workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
- name: Set up Cloud SDK
if: steps.pr.outputs.pr_exists == 'true'
uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3
- name: Upload to GCS Staging
id: gcs
if: steps.pr.outputs.pr_exists == 'true'
env:
SPEC_ID: ${{ steps.inputs.outputs.specification_id }}
LANGUAGE: ${{ steps.inputs.outputs.language }}
LIBRARY: ${{ steps.inputs.outputs.library }}
run: |
IMPL_DIR="plots/${SPEC_ID}/implementations/${LANGUAGE}"
STAGING_PATH="gs://anyplot-images/staging/${SPEC_ID}/${LANGUAGE}/${LIBRARY}"
PUBLIC_URL="https://storage.googleapis.com/anyplot-images/staging/${SPEC_ID}/${LANGUAGE}/${LIBRARY}"
# Require both theme renders
if [ ! -f "$IMPL_DIR/plot-light.png" ] || [ ! -f "$IMPL_DIR/plot-dark.png" ]; then
echo "::error::Missing plot-light.png and/or plot-dark.png — cannot upload"
ls -la "$IMPL_DIR/" || true
exit 1
fi
# Upload all plot images for both themes (originals + responsive variants)
gsutil -m -h "Cache-Control:public, max-age=604800" cp \
"$IMPL_DIR"/plot-light*.png "$IMPL_DIR"/plot-light*.webp \
"$IMPL_DIR"/plot-dark*.png "$IMPL_DIR"/plot-dark*.webp \
"${STAGING_PATH}/"
gsutil -m acl ch -u AllUsers:R "${STAGING_PATH}/plot-light*" "${STAGING_PATH}/plot-dark*" 2>/dev/null || true
echo "png_url_light=${PUBLIC_URL}/plot-light.png" >> $GITHUB_OUTPUT
echo "png_url_dark=${PUBLIC_URL}/plot-dark.png" >> $GITHUB_OUTPUT
echo "uploaded=true" >> $GITHUB_OUTPUT
echo "::notice::Uploaded plot-light + plot-dark (+responsive variants)"
# Upload HTML for interactive libraries (one per theme)
for theme in light dark; do
if [ -f "$IMPL_DIR/plot-${theme}.html" ]; then
gsutil -h "Cache-Control:public, max-age=604800" cp "$IMPL_DIR/plot-${theme}.html" "${STAGING_PATH}/plot-${theme}.html"
gsutil acl ch -u AllUsers:R "${STAGING_PATH}/plot-${theme}.html" 2>/dev/null || true
echo "html_url_${theme}=${PUBLIC_URL}/plot-${theme}.html" >> $GITHUB_OUTPUT
fi
done
rm -f /tmp/gcs-key.json
# ========================================================================
# Post preview and trigger review
# ========================================================================
- name: Post preview to issue
if: steps.issue.outputs.number != '' && steps.gcs.outputs.uploaded == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SPEC_ID: ${{ steps.inputs.outputs.specification_id }}
LANGUAGE: ${{ steps.inputs.outputs.language }}
LIBRARY: ${{ steps.inputs.outputs.library }}
ISSUE: ${{ steps.issue.outputs.number }}
PR_NUMBER: ${{ steps.pr.outputs.pr_number }}
run: |
# Use staging URLs (available immediately after upload) — show both themes side by side.
BASE="https://storage.googleapis.com/anyplot-images/staging/${SPEC_ID}/${LANGUAGE}/${LIBRARY}"
PNG_LIGHT="${BASE}/plot-light.png"
PNG_DARK="${BASE}/plot-dark.png"
HTML_LIGHT=""
HTML_DARK=""
case "$LIBRARY" in
plotly|bokeh|altair|highcharts|pygal|letsplot)
HTML_LIGHT="${BASE}/plot-light.html"
HTML_DARK="${BASE}/plot-dark.html"
;;
esac
BODY="## :art: ${LANGUAGE}/${LIBRARY} Preview
| Light | Dark |
|-------|------|
| ![${LIBRARY} light](${PNG_LIGHT}) | ![${LIBRARY} dark](${PNG_DARK}) |
**PR:** #${PR_NUMBER}"
if [ -n "$HTML_LIGHT" ]; then
BODY="${BODY}
**Interactive:** [light](${HTML_LIGHT}) · [dark](${HTML_DARK})"
fi
BODY="${BODY}
---
:robot: *[impl-generate](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})*"
gh issue comment "$ISSUE" --body "$BODY"
- name: Trigger review workflow
if: steps.pr.outputs.pr_exists == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ steps.pr.outputs.pr_number }}
run: |
# Use repository_dispatch as workaround for workflow_dispatch caching issue
gh api repos/${{ github.repository }}/dispatches \
-f event_type=review-pr \
-f 'client_payload[pr_number]='"$PR_NUMBER"
echo "::notice::Triggered impl-review.yml via repository_dispatch for PR #$PR_NUMBER"
- name: Determine result
id: result
run: |
if [ "${{ steps.pr.outputs.pr_exists }}" == "true" ]; then
echo "success=true" >> $GITHUB_OUTPUT
else
echo "success=false" >> $GITHUB_OUTPUT
fi
# ========================================================================
# Failure handling: Track failures via comments and auto-retry
# ========================================================================
- name: Handle generation failure
if: failure() && steps.issue.outputs.number != ''
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SPEC_ID: ${{ steps.inputs.outputs.specification_id }}
LIBRARY: ${{ steps.inputs.outputs.library }}
ISSUE: ${{ steps.issue.outputs.number }}
run: |
echo "::notice::Handling generation failure for $LIBRARY/$SPEC_ID"
# Count previous failures via hidden marker comments (more reliable than workflow runs).
# Paginate so the marker is found even on issues with >30 comments
# (which is common because all 9 library impls land on the same issue).
MARKER="<!-- impl-fail:${SPEC_ID}:${LIBRARY} -->"
FAILURE_COUNT=$(gh api --paginate "repos/${{ github.repository }}/issues/${ISSUE}/comments?per_page=100" \
--jq "[.[] | select(.body != null and (.body | contains(\"$MARKER\")))] | length" 2>/dev/null || echo "0")
echo "::notice::Previous failures for ${LIBRARY}/${SPEC_ID}: $FAILURE_COUNT"
# FAILURE_COUNT counts marker comments BEFORE this run.
# 0 → this is attempt 1 fail, 1 → attempt 2 fail, 2 → attempt 3 fail.
ATTEMPT=$((FAILURE_COUNT + 1))
# After 2 previous failures (= this is attempt 3) → mark as failed
if [ "$FAILURE_COUNT" -ge 2 ]; then
echo "::warning::Marking $LIBRARY as failed after 3 generation attempts"
# Create failed label if needed
gh label create "impl:${LIBRARY}:failed" --color "d73a4a" \
--description "${LIBRARY} implementation failed" 2>/dev/null || true
# Remove stale labels and add failed
gh issue edit "$ISSUE" \
--remove-label "generate:${LIBRARY},impl:${LIBRARY}:pending" \
--add-label "impl:${LIBRARY}:failed" 2>/dev/null || true
# Post final failure comment with marker
gh issue comment "$ISSUE" --body "${MARKER}
## :x: ${LIBRARY} Failed (Attempt 3/3)
The **${LIBRARY}** implementation for \`${SPEC_ID}\` failed after 3 attempts.
**Reason:** Claude Code failed to create the implementation file.
To retry manually:
\`\`\`
gh workflow run impl-generate.yml -f specification_id=${SPEC_ID} -f library=${LIBRARY} -f issue_number=${ISSUE}
\`\`\`
---
:robot: *[impl-generate](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})*"
else
# Attempt 1 or 2 failed → post comment with marker and auto-retry
gh issue comment "$ISSUE" --body "${MARKER}
## :warning: ${LIBRARY} Generation Failed (Attempt ${ATTEMPT}/3)
Attempt ${ATTEMPT} failed. Automatically retrying...
---
:robot: *[impl-generate](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})*"
# Clean up generate label before retry
gh issue edit "$ISSUE" --remove-label "generate:${LIBRARY}" 2>/dev/null || true
# Automatic retry via workflow_dispatch
gh workflow run impl-generate.yml \
-f specification_id="${SPEC_ID}" \
-f library="${LIBRARY}" \
-f issue_number="${ISSUE}"
echo "::notice::Triggered automatic retry for ${LIBRARY}/${SPEC_ID} (attempt $((ATTEMPT + 1)))"
fi