Generate: enhancement for issue #3115
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: "Impl: Generate" | |
| 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 | |
| DEPS_matplotlib: "matplotlib>=3.9.0 numpy>=1.26.0" | |
| DEPS_seaborn: "seaborn>=0.13.0 matplotlib>=3.9.0 numpy>=1.26.0" | |
| DEPS_plotly: "plotly>=5.18.0 kaleido>=0.2.1 numpy>=1.26.0" | |
| DEPS_bokeh: "bokeh>=3.4.0 numpy>=1.26.0 selenium>=4.15.0 webdriver-manager>=4.0.0" | |
| DEPS_altair: "altair>=5.2.0 vl-convert-python>=1.3.0 numpy>=1.26.0" | |
| DEPS_plotnine: "plotnine>=0.13.0 numpy>=1.26.0" | |
| DEPS_pygal: "pygal>=3.0.0 cairosvg>=2.7.0" | |
| DEPS_highcharts: "highcharts-core>=1.10.0 numpy>=1.26.0 selenium>=4.15.0 webdriver-manager>=4.0.0" | |
| DEPS_letsplot: "lets-plot>=4.5.0" | |
| 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 | |
| # Get library dependencies | |
| DEPS_VAR="DEPS_${LIBRARY}" | |
| DEPS="${!DEPS_VAR}" | |
| echo "specification_id=$SPEC_ID" >> $GITHUB_OUTPUT | |
| echo "library=$LIBRARY" >> $GITHUB_OUTPUT | |
| echo "issue_number=$ISSUE" >> $GITHUB_OUTPUT | |
| echo "deps=$DEPS" >> $GITHUB_OUTPUT | |
| echo "::notice::Generating $LIBRARY for $SPEC_ID (issue: ${ISSUE:-none})" | |
| - name: Checkout repository | |
| uses: actions/checkout@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@v6 | |
| with: | |
| python-version: '3.13' | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| - name: Install system dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y pngquant | |
| - name: Install dependencies | |
| env: | |
| DEPS: ${{ steps.inputs.outputs.deps }} | |
| run: | | |
| uv venv .venv | |
| source .venv/bin/activate | |
| uv pip install $DEPS pandas>=2.2.0 ruff Pillow>=10.0.0 pyyaml>=6.0 | |
| - name: Setup Chrome for Highcharts | |
| if: steps.inputs.outputs.library == 'highcharts' | |
| uses: browser-actions/setup-chrome@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 }} | |
| LIBRARY: ${{ steps.inputs.outputs.library }} | |
| run: | | |
| METADATA_FILE="plots/${SPEC_ID}/metadata/${LIBRARY}.yaml" | |
| IMPL_FILE="plots/${SPEC_ID}/implementations/${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: Run Claude Code to generate implementation | |
| id: claude | |
| continue-on-error: true | |
| timeout-minutes: 60 | |
| uses: anthropics/claude-code-action@v1 | |
| with: | |
| claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} | |
| claude_args: "--model opus" | |
| prompt: | | |
| ## Task: Generate ${{ steps.inputs.outputs.library }} Implementation | |
| You are generating the **${{ steps.inputs.outputs.library }}** implementation for **${{ steps.inputs.outputs.specification_id }}**. | |
| **Regeneration:** ${{ steps.existing.outputs.is_regeneration }} | |
| ### Step 1: Read required files | |
| 1. `prompts/plot-generator.md` - Base generation rules (IMPORTANT: Read the "Regeneration" section!) | |
| 2. `prompts/default-style-guide.md` - Visual style requirements | |
| 3. `prompts/quality-criteria.md` - Quality requirements | |
| 4. `prompts/library/${{ steps.inputs.outputs.library }}.md` - Library-specific rules | |
| 5. `plots/${{ steps.inputs.outputs.specification_id }}/specification.md` - The specification | |
| ### Step 1b: If Regeneration, read previous feedback | |
| If this is a regeneration (${{ steps.existing.outputs.is_regeneration }} == true): | |
| 1. Read `plots/${{ steps.inputs.outputs.specification_id }}/metadata/${{ steps.inputs.outputs.library }}.yaml` | |
| - Look at `review.strengths` (keep these aspects!) | |
| - Look at `review.weaknesses` (fix these problems - decide HOW yourself) | |
| - Look at `review.image_description` (understand what was generated visually) | |
| - Look at `review.criteria_checklist` (see exactly which criteria failed) | |
| - Focus on categories with low scores (e.g., visual_quality.score < visual_quality.max) | |
| - Check items with `passed: false` - these need fixing | |
| - VQ-XX items for visual issues | |
| - SC-XX items for spec compliance | |
| - CQ-XX items for code quality | |
| 2. Read `plots/${{ steps.inputs.outputs.specification_id }}/implementations/${{ steps.inputs.outputs.library }}.py` | |
| - Understand what was done before | |
| - Keep what worked, fix what didn't | |
| ### Step 2: Generate implementation | |
| Create: `plots/${{ steps.inputs.outputs.specification_id }}/implementations/${{ steps.inputs.outputs.library }}.py` | |
| The script MUST: | |
| - Save as `plot.png` in the current directory | |
| - For interactive libraries (plotly, bokeh, altair, highcharts, pygal, letsplot): also save `plot.html` | |
| ### Step 3: Test and fix (up to 3 attempts) | |
| Run the implementation: | |
| ```bash | |
| source .venv/bin/activate | |
| cd plots/${{ steps.inputs.outputs.specification_id }}/implementations | |
| MPLBACKEND=Agg python ${{ steps.inputs.outputs.library }}.py | |
| ``` | |
| If it fails, fix and try again (max 3 attempts). | |
| ### Step 4: Visual self-check | |
| Look at the generated `plot.png`: | |
| - Does it match the specification? | |
| - Are axes labeled correctly? | |
| - Is the visualization clear? | |
| ### Step 5: Format the code | |
| ```bash | |
| source .venv/bin/activate | |
| ruff format plots/${{ steps.inputs.outputs.specification_id }}/implementations/${{ steps.inputs.outputs.library }}.py | |
| ruff check --fix plots/${{ steps.inputs.outputs.specification_id }}/implementations/${{ steps.inputs.outputs.library }}.py | |
| ``` | |
| ### Step 6: Commit | |
| ```bash | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add plots/${{ steps.inputs.outputs.specification_id }}/implementations/${{ steps.inputs.outputs.library }}.py | |
| git commit -m "feat(${{ steps.inputs.outputs.library }}): implement ${{ steps.inputs.outputs.specification_id }}" | |
| git push -u origin implementation/${{ steps.inputs.outputs.specification_id }}/${{ steps.inputs.outputs.library }} | |
| ``` | |
| ### Report result | |
| Print exactly one line: | |
| - `GENERATION_SUCCESS` - if everything worked | |
| - `GENERATION_FAILED: <reason>` - if it failed | |
| - name: Retry Claude (on failure) | |
| if: steps.claude.outcome == 'failure' | |
| id: claude_retry | |
| timeout-minutes: 60 | |
| uses: anthropics/claude-code-action@v1 | |
| with: | |
| claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} | |
| claude_args: "--model opus" | |
| prompt: | | |
| ## Task: Generate ${{ steps.inputs.outputs.library }} Implementation | |
| You are generating the **${{ steps.inputs.outputs.library }}** implementation for **${{ steps.inputs.outputs.specification_id }}**. | |
| **Regeneration:** ${{ steps.existing.outputs.is_regeneration }} | |
| ### Step 1: Read required files | |
| 1. `prompts/plot-generator.md` - Base generation rules (IMPORTANT: Read the "Regeneration" section!) | |
| 2. `prompts/default-style-guide.md` - Visual style requirements | |
| 3. `prompts/quality-criteria.md` - Quality requirements | |
| 4. `prompts/library/${{ steps.inputs.outputs.library }}.md` - Library-specific rules | |
| 5. `plots/${{ steps.inputs.outputs.specification_id }}/specification.md` - The specification | |
| ### Step 1b: If Regeneration, read previous feedback | |
| If this is a regeneration (${{ steps.existing.outputs.is_regeneration }} == true): | |
| 1. Read `plots/${{ steps.inputs.outputs.specification_id }}/metadata/${{ steps.inputs.outputs.library }}.yaml` | |
| - Look at `review.strengths` (keep these aspects!) | |
| - Look at `review.weaknesses` (fix these problems - decide HOW yourself) | |
| - Look at `review.image_description` (understand what was generated visually) | |
| - Look at `review.criteria_checklist` (see exactly which criteria failed) | |
| - Focus on categories with low scores (e.g., visual_quality.score < visual_quality.max) | |
| - Check items with `passed: false` - these need fixing | |
| - VQ-XX items for visual issues | |
| - SC-XX items for spec compliance | |
| - CQ-XX items for code quality | |
| 2. Read `plots/${{ steps.inputs.outputs.specification_id }}/implementations/${{ steps.inputs.outputs.library }}.py` | |
| - Understand what was done before | |
| - Keep what worked, fix what didn't | |
| ### Step 2: Generate implementation | |
| Create: `plots/${{ steps.inputs.outputs.specification_id }}/implementations/${{ steps.inputs.outputs.library }}.py` | |
| The script MUST: | |
| - Save as `plot.png` in the current directory | |
| - For interactive libraries (plotly, bokeh, altair, highcharts, pygal, letsplot): also save `plot.html` | |
| ### Step 3: Test and fix (up to 3 attempts) | |
| Run the implementation: | |
| ```bash | |
| source .venv/bin/activate | |
| cd plots/${{ steps.inputs.outputs.specification_id }}/implementations | |
| MPLBACKEND=Agg python ${{ steps.inputs.outputs.library }}.py | |
| ``` | |
| If it fails, fix and try again (max 3 attempts). | |
| ### Step 4: Visual self-check | |
| Look at the generated `plot.png`: | |
| - Does it match the specification? | |
| - Are axes labeled correctly? | |
| - Is the visualization clear? | |
| ### Step 5: Format the code | |
| ```bash | |
| source .venv/bin/activate | |
| ruff format plots/${{ steps.inputs.outputs.specification_id }}/implementations/${{ steps.inputs.outputs.library }}.py | |
| ruff check --fix plots/${{ steps.inputs.outputs.specification_id }}/implementations/${{ steps.inputs.outputs.library }}.py | |
| ``` | |
| ### Step 6: Commit | |
| ```bash | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add plots/${{ steps.inputs.outputs.specification_id }}/implementations/${{ steps.inputs.outputs.library }}.py | |
| git commit -m "feat(${{ steps.inputs.outputs.library }}): implement ${{ steps.inputs.outputs.specification_id }}" | |
| git push -u origin implementation/${{ steps.inputs.outputs.specification_id }}/${{ steps.inputs.outputs.library }} | |
| ``` | |
| ### Report result | |
| Print exactly one line: | |
| - `GENERATION_SUCCESS` - if everything worked | |
| - `GENERATION_FAILED: <reason>` - if it failed | |
| # ======================================================================== | |
| # Create metadata file (before PR) | |
| # ======================================================================== | |
| - name: Create library metadata file | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| SPEC_ID: ${{ steps.inputs.outputs.specification_id }} | |
| LIBRARY: ${{ steps.inputs.outputs.library }} | |
| ISSUE: ${{ steps.issue.outputs.number }} | |
| BRANCH: ${{ steps.branch.outputs.branch }} | |
| run: | | |
| # Save plot files before git operations (they are not committed and would be lost) | |
| cp "plots/${SPEC_ID}/implementations/plot.png" "/tmp/plot.png" 2>/dev/null || true | |
| cp "plots/${SPEC_ID}/implementations/plot.html" "/tmp/plot.html" 2>/dev/null || true | |
| # 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 | |
| cp "/tmp/plot.png" "plots/${SPEC_ID}/implementations/plot.png" 2>/dev/null || true | |
| cp "/tmp/plot.html" "plots/${SPEC_ID}/implementations/plot.html" 2>/dev/null || true | |
| # Now create metadata file | |
| METADATA_DIR="plots/${SPEC_ID}/metadata" | |
| 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" | |
| # Determine preview_html for interactive libraries | |
| PREVIEW_HTML="null" | |
| case "$LIBRARY" in | |
| plotly|bokeh|altair|highcharts|pygal|letsplot) | |
| PREVIEW_HTML="https://storage.googleapis.com/pyplots-images/plots/${SPEC_ID}/${LIBRARY}/plot.html" | |
| ;; | |
| esac | |
| # Write metadata file using Python for proper YAML formatting | |
| # Pass all variables inline to avoid export/env issues | |
| .venv/bin/python3 -c " | |
| import yaml | |
| lib = '$LIBRARY' | |
| spec = '$SPEC_ID' | |
| ts = '$TIMESTAMP' | |
| run_id = ${{ github.run_id }} | |
| issue = int('$ISSUE' or '0') | |
| py_ver = '$PYTHON_VERSION' | |
| lib_ver = '$LIBRARY_VERSION' | |
| preview_html = '$PREVIEW_HTML' | |
| metadata_file = '$METADATA_FILE' | |
| data = { | |
| 'library': lib, | |
| 'specification_id': spec, | |
| 'created': ts, | |
| 'updated': ts, | |
| 'generated_by': 'claude-opus-4-5-20251101', | |
| 'workflow_run': run_id, | |
| 'issue': issue, | |
| 'python_version': py_ver, | |
| 'library_version': lib_ver, | |
| 'preview_url': f'https://storage.googleapis.com/pyplots-images/plots/{spec}/{lib}/plot.png', | |
| 'preview_thumb': f'https://storage.googleapis.com/pyplots-images/plots/{spec}/{lib}/plot_thumb.png', | |
| 'preview_html': preview_html if preview_html != 'null' 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 | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add "$METADATA_FILE" | |
| 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})" | |
| # ======================================================================== | |
| # Process images: optimize + thumbnail | |
| # ======================================================================== | |
| - name: Process plot images | |
| env: | |
| SPEC_ID: ${{ steps.inputs.outputs.specification_id }} | |
| run: | | |
| IMPL_DIR="plots/${SPEC_ID}/implementations" | |
| if [ ! -f "$IMPL_DIR/plot.png" ]; then | |
| echo "::error::No plot.png found - implementation failed to generate image" | |
| exit 1 | |
| fi | |
| source .venv/bin/activate | |
| # Process PNG: optimize and create thumbnail | |
| python -m core.images process \ | |
| "$IMPL_DIR/plot.png" \ | |
| "$IMPL_DIR/plot.png" \ | |
| "$IMPL_DIR/plot_thumb.png" | |
| echo "::notice::Processed images: optimized + thumbnail created" | |
| 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 }} | |
| 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}\` - ${LIBRARY} | |
| Implements the **${LIBRARY}** version of \`${SPEC_ID}\`. | |
| **File:** \`plots/${SPEC_ID}/implementations/${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: Upload to GCS Staging | |
| id: gcs | |
| if: steps.pr.outputs.pr_exists == 'true' | |
| env: | |
| GCS_CREDENTIALS: ${{ secrets.GCS_CREDENTIALS }} | |
| SPEC_ID: ${{ steps.inputs.outputs.specification_id }} | |
| LIBRARY: ${{ steps.inputs.outputs.library }} | |
| run: | | |
| IMPL_DIR="plots/${SPEC_ID}/implementations" | |
| STAGING_PATH="gs://pyplots-images/staging/${SPEC_ID}/${LIBRARY}" | |
| PUBLIC_URL="https://storage.googleapis.com/pyplots-images/staging/${SPEC_ID}/${LIBRARY}" | |
| if [ -z "$GCS_CREDENTIALS" ]; then | |
| echo "::warning::GCS_CREDENTIALS not configured - skipping upload" | |
| echo "uploaded=false" >> $GITHUB_OUTPUT | |
| exit 0 | |
| fi | |
| # Authenticate | |
| echo "$GCS_CREDENTIALS" > /tmp/gcs-key.json | |
| gcloud auth activate-service-account --key-file=/tmp/gcs-key.json | |
| # Upload PNG (with watermark) | |
| if [ -f "$IMPL_DIR/plot.png" ]; then | |
| gsutil cp "$IMPL_DIR/plot.png" "${STAGING_PATH}/plot.png" | |
| gsutil acl ch -u AllUsers:R "${STAGING_PATH}/plot.png" 2>/dev/null || true | |
| echo "png_url=${PUBLIC_URL}/plot.png" >> $GITHUB_OUTPUT | |
| echo "uploaded=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "::error::No plot.png found - cannot continue without image" | |
| exit 1 | |
| fi | |
| # Upload thumbnail | |
| if [ -f "$IMPL_DIR/plot_thumb.png" ]; then | |
| gsutil cp "$IMPL_DIR/plot_thumb.png" "${STAGING_PATH}/plot_thumb.png" | |
| gsutil acl ch -u AllUsers:R "${STAGING_PATH}/plot_thumb.png" 2>/dev/null || true | |
| echo "thumb_url=${PUBLIC_URL}/plot_thumb.png" >> $GITHUB_OUTPUT | |
| echo "::notice::Uploaded thumbnail" | |
| fi | |
| # Upload HTML (interactive libraries) | |
| if [ -f "$IMPL_DIR/plot.html" ]; then | |
| gsutil cp "$IMPL_DIR/plot.html" "${STAGING_PATH}/plot.html" | |
| gsutil acl ch -u AllUsers:R "${STAGING_PATH}/plot.html" 2>/dev/null || true | |
| echo "html_url=${PUBLIC_URL}/plot.html" >> $GITHUB_OUTPUT | |
| fi | |
| 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 }} | |
| 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) | |
| PNG_URL="https://storage.googleapis.com/pyplots-images/staging/${SPEC_ID}/${LIBRARY}/plot.png" | |
| HTML_URL="" | |
| # Set HTML_URL for interactive libraries | |
| case "$LIBRARY" in | |
| plotly|bokeh|altair|highcharts|pygal|letsplot) | |
| HTML_URL="https://storage.googleapis.com/pyplots-images/staging/${SPEC_ID}/${LIBRARY}/plot.html" | |
| ;; | |
| esac | |
| BODY="## :art: ${LIBRARY} Preview | |
|  | |
| **PR:** #${PR_NUMBER}" | |
| if [ -n "$HTML_URL" ]; then | |
| BODY="${BODY} | |
| **Interactive:** [Open HTML](${HTML_URL})" | |
| 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 and clean up labels | |
| # ======================================================================== | |
| - 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 failed generation runs for this spec/library | |
| # Filter by both library AND spec ID to avoid counting unrelated failures | |
| FAILURE_COUNT=$(gh run list \ | |
| --workflow=impl-generate.yml \ | |
| --limit 20 \ | |
| --json conclusion,displayTitle,createdAt \ | |
| -q "[.[] | select(.conclusion == \"failure\") | select(.displayTitle | (contains(\"$LIBRARY\") and contains(\"$SPEC_ID\")))] | length" \ | |
| 2>/dev/null || echo "0") | |
| echo "::notice::Previous failures: $FAILURE_COUNT" | |
| # After 3 failures, mark as failed (current run is the 3rd) | |
| if [ "$FAILURE_COUNT" -ge 2 ]; then | |
| echo "::warning::Marking $LIBRARY as failed after multiple generation failures" | |
| # 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 (Fix #2: clean up duplicates) | |
| gh issue edit "$ISSUE" \ | |
| --remove-label "generate:${LIBRARY},impl:${LIBRARY}:pending" \ | |
| --add-label "impl:${LIBRARY}:failed" 2>/dev/null || true | |
| # Post failure comment | |
| gh issue comment "$ISSUE" --body "## :x: ${LIBRARY} Failed | |
| The **${LIBRARY}** implementation for \`${SPEC_ID}\` failed after multiple attempts. | |
| This may indicate that the library cannot implement this plot type natively. | |
| --- | |
| :robot: *[impl-generate](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})*" | |
| else | |
| # Not enough failures yet - just clean up stale generate label | |
| # Keep pending label for retry | |
| gh issue edit "$ISSUE" --remove-label "generate:${LIBRARY}" 2>/dev/null || true | |
| echo "::notice::Cleaned up generate label, keeping pending for retry" | |
| fi |