Generate: generate:highcharts for issue #5228
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.14' | |
| - 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: Ensure implementations directory exists | |
| env: | |
| SPEC_ID: ${{ steps.inputs.outputs.specification_id }} | |
| run: | | |
| # Create implementations directory if it doesn't exist (for new specs) | |
| mkdir -p "plots/${SPEC_ID}/implementations" | |
| echo "::notice::Ensured implementations directory exists" | |
| - 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: | | |
| Read `prompts/workflow-prompts/impl-generate-claude.md` and follow those instructions. | |
| Variables for this run: | |
| - 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@v1 | |
| with: | |
| claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} | |
| claude_args: "--model opus" | |
| prompt: | | |
| Read `prompts/workflow-prompts/impl-generate-claude.md` and follow those instructions. | |
| Variables for this run: | |
| - 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 }} | |
| 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_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 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/${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 | |
| 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 | |
| # Optimize PNG and generate responsive variants (400/800/1200 x png/webp + full webp) | |
| python -m core.images process \ | |
| "$IMPL_DIR/plot.png" \ | |
| "$IMPL_DIR/plot.png" | |
| python -m core.images responsive \ | |
| "$IMPL_DIR/plot.png" \ | |
| "$IMPL_DIR/" | |
| echo "::notice::Processed images: optimized + responsive variants 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 all plot images (original + responsive variants) | |
| if [ -f "$IMPL_DIR/plot.png" ]; then | |
| gsutil -m -h "Cache-Control:public, max-age=604800" cp "$IMPL_DIR"/plot*.png "$IMPL_DIR"/plot*.webp "${STAGING_PATH}/" | |
| gsutil -m acl ch -u AllUsers:R "${STAGING_PATH}/plot*" 2>/dev/null || true | |
| echo "png_url=${PUBLIC_URL}/plot.png" >> $GITHUB_OUTPUT | |
| echo "uploaded=true" >> $GITHUB_OUTPUT | |
| echo "::notice::Uploaded plot.png + responsive variants (PNG + WebP)" | |
| else | |
| echo "::error::No plot.png found - cannot continue without image" | |
| exit 1 | |
| fi | |
| # Upload HTML (interactive libraries) | |
| if [ -f "$IMPL_DIR/plot.html" ]; then | |
| gsutil -h "Cache-Control:public, max-age=604800" 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 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) | |
| MARKER="<!-- impl-fail:${SPEC_ID}:${LIBRARY} -->" | |
| FAILURE_COUNT=$(gh api "repos/${{ github.repository }}/issues/${ISSUE}/comments" \ | |
| --jq "[.[] | select(.body | contains(\"$MARKER\"))] | length" 2>/dev/null || echo "0") | |
| echo "::notice::Previous failures for ${LIBRARY}/${SPEC_ID}: $FAILURE_COUNT" | |
| # After 1 previous failure (= this is attempt 2) → mark as failed | |
| if [ "$FAILURE_COUNT" -ge 1 ]; then | |
| echo "::warning::Marking $LIBRARY as failed after 2 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 2/2) | |
| The **${LIBRARY}** implementation for \`${SPEC_ID}\` failed after 2 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 | |
| # First failure → post comment with marker and auto-retry | |
| gh issue comment "$ISSUE" --body "${MARKER} | |
| ## :warning: ${LIBRARY} Generation Failed (Attempt 1/2) | |
| First 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}" | |
| fi |