Merge: 2823 #1919
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: Merge" | |
| run-name: "Merge: ${{ github.event.inputs.pr_number || github.event.pull_request.head.ref }}" | |
| # Auto-merge implementation PRs when ai-approved label is added | |
| # Creates per-library metadata file and promotes GCS images | |
| on: | |
| pull_request: | |
| types: [labeled] | |
| workflow_dispatch: | |
| inputs: | |
| pr_number: | |
| description: 'PR number to auto-merge' | |
| required: true | |
| type: number | |
| jobs: | |
| merge: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| issues: write | |
| actions: write # Required for gh workflow run sync-postgres.yml | |
| steps: | |
| - name: Check conditions | |
| id: check | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then | |
| PR_NUM="${{ github.event.inputs.pr_number }}" | |
| PR_DATA=$(gh pr view "$PR_NUM" --repo ${{ github.repository }} --json headRefName,labels) | |
| BRANCH=$(echo "$PR_DATA" | jq -r '.headRefName') | |
| HAS_APPROVED=$(echo "$PR_DATA" | jq -r '[.labels[].name] | any(. == "ai-approved")') | |
| else | |
| ACTION="${{ github.event.action }}" | |
| LABEL="${{ github.event.label.name }}" | |
| BRANCH="${{ github.event.pull_request.head.ref }}" | |
| PR_NUM="${{ github.event.pull_request.number }}" | |
| if [[ "$ACTION" != "labeled" || "$LABEL" != "ai-approved" ]]; then | |
| echo "should_run=false" >> $GITHUB_OUTPUT | |
| exit 0 | |
| fi | |
| HAS_APPROVED="true" | |
| fi | |
| # Only process implementation/* branches | |
| if [[ ! "$BRANCH" =~ ^implementation/ ]]; then | |
| echo "::notice::Skipping: Branch '$BRANCH' is not implementation/*" | |
| echo "should_run=false" >> $GITHUB_OUTPUT | |
| exit 0 | |
| fi | |
| if [[ "$HAS_APPROVED" != "true" ]]; then | |
| echo "::notice::Skipping: PR does not have ai-approved label" | |
| echo "should_run=false" >> $GITHUB_OUTPUT | |
| exit 0 | |
| fi | |
| echo "pr_number=$PR_NUM" >> $GITHUB_OUTPUT | |
| echo "branch=$BRANCH" >> $GITHUB_OUTPUT | |
| echo "should_run=true" >> $GITHUB_OUTPUT | |
| - name: Checkout repository | |
| if: steps.check.outputs.should_run == 'true' | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - name: Extract info from branch | |
| if: steps.check.outputs.should_run == 'true' | |
| id: extract | |
| run: | | |
| BRANCH="${{ steps.check.outputs.branch }}" | |
| # Format: implementation/{specification-id}/{library} | |
| SPEC_ID=$(echo "$BRANCH" | cut -d'/' -f2) | |
| LIBRARY=$(echo "$BRANCH" | cut -d'/' -f3) | |
| echo "specification_id=$SPEC_ID" >> $GITHUB_OUTPUT | |
| echo "library=$LIBRARY" >> $GITHUB_OUTPUT | |
| - name: Get parent issue from PR body | |
| if: steps.check.outputs.should_run == 'true' | |
| id: issue | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| PR_NUM="${{ steps.check.outputs.pr_number }}" | |
| PR_BODY=$(gh pr view "$PR_NUM" --json body -q '.body' 2>/dev/null || echo "") | |
| ISSUE=$(echo "$PR_BODY" | grep -oP '\*\*Parent Issue:\*\* #\K\d+' | head -1 || echo "") | |
| echo "number=$ISSUE" >> $GITHUB_OUTPUT | |
| - name: Extract quality score from PR labels | |
| if: steps.check.outputs.should_run == 'true' | |
| id: quality | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| PR_NUM="${{ steps.check.outputs.pr_number }}" | |
| LABELS=$(gh pr view "$PR_NUM" --json labels -q '.labels[].name' 2>/dev/null || echo "") | |
| SCORE=$(echo "$LABELS" | grep -oP 'quality:\K\d+' | head -1 || echo "") | |
| echo "score=$SCORE" >> $GITHUB_OUTPUT | |
| - name: React with rocket emoji | |
| if: steps.check.outputs.should_run == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| gh api repos/${{ github.repository }}/issues/${{ steps.check.outputs.pr_number }}/reactions \ | |
| -f content=rocket | |
| - name: Merge PR to main (with retry) | |
| if: steps.check.outputs.should_run == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| PR_NUM="${{ steps.check.outputs.pr_number }}" | |
| MAX_ATTEMPTS=5 | |
| for attempt in $(seq 1 $MAX_ATTEMPTS); do | |
| echo "::notice::Merge attempt $attempt/$MAX_ATTEMPTS" | |
| # Update branch before merge attempt | |
| gh pr update-branch "$PR_NUM" --repo ${{ github.repository }} 2>/dev/null || true | |
| sleep 2 | |
| if gh pr merge "$PR_NUM" \ | |
| --repo ${{ github.repository }} \ | |
| --squash \ | |
| --delete-branch; then | |
| echo "::notice::Merge successful on attempt $attempt" | |
| exit 0 | |
| fi | |
| if [ $attempt -lt $MAX_ATTEMPTS ]; then | |
| DELAY=$((attempt * 10)) | |
| echo "::warning::Merge failed, retrying in ${DELAY}s..." | |
| sleep $DELAY | |
| fi | |
| done | |
| echo "::error::Merge failed after $MAX_ATTEMPTS attempts" | |
| exit 1 | |
| # Note: quality_score is now set by impl-review.yml before merge | |
| # The PR already contains the updated metadata when merged | |
| - name: Promote GCS images to production | |
| if: steps.check.outputs.should_run == 'true' | |
| env: | |
| GCS_CREDENTIALS: ${{ secrets.GCS_CREDENTIALS }} | |
| SPEC_ID: ${{ steps.extract.outputs.specification_id }} | |
| LIBRARY: ${{ steps.extract.outputs.library }} | |
| run: | | |
| if [ -z "$GCS_CREDENTIALS" ]; then | |
| echo "::warning::GCS_CREDENTIALS not configured - skipping promotion" | |
| exit 0 | |
| fi | |
| echo "$GCS_CREDENTIALS" > /tmp/gcs-key.json | |
| gcloud auth activate-service-account --key-file=/tmp/gcs-key.json | |
| STAGING="gs://pyplots-images/staging/${SPEC_ID}/${LIBRARY}" | |
| PRODUCTION="gs://pyplots-images/plots/${SPEC_ID}/${LIBRARY}" | |
| HISTORY="${PRODUCTION}/history" | |
| # Get created date from metadata for version naming (YYYY-MM-DD) | |
| CREATED_DATE=$(yq '.created' "plots/${SPEC_ID}/metadata/${LIBRARY}.yaml" 2>/dev/null | cut -d'T' -f1 || echo "$(date +%Y-%m-%d)") | |
| echo "Created date: ${CREATED_DATE}" | |
| # 1. Copy from staging to production (current) | |
| gsutil -m cp -r "${STAGING}/*" "${PRODUCTION}/" 2>/dev/null || echo "No staging files to promote" | |
| # 2. Copy versioned to history/ folder (using date instead of version number) | |
| gsutil cp "${STAGING}/plot.png" "${HISTORY}/${CREATED_DATE}.png" 2>/dev/null || true | |
| gsutil cp "${STAGING}/plot.html" "${HISTORY}/${CREATED_DATE}.html" 2>/dev/null || true | |
| # Make production files public | |
| gsutil -m acl ch -r -u AllUsers:R "${PRODUCTION}/" 2>/dev/null || true | |
| # Clean up staging | |
| gsutil -m rm -r "${STAGING}/" 2>/dev/null || true | |
| rm -f /tmp/gcs-key.json | |
| echo "::notice::Promoted images to production + history/${CREATED_DATE}" | |
| - name: Update issue labels | |
| if: steps.check.outputs.should_run == 'true' && steps.issue.outputs.number != '' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| ISSUE: ${{ steps.issue.outputs.number }} | |
| LIBRARY: ${{ steps.extract.outputs.library }} | |
| run: | | |
| # Create labels if they don't exist | |
| gh label create "impl:${LIBRARY}:done" --color "0e8a16" --description "${LIBRARY} implementation merged" 2>/dev/null || true | |
| # Remove trigger and pending, add done | |
| gh issue edit "$ISSUE" --remove-label "generate:${LIBRARY}" 2>/dev/null || true | |
| gh issue edit "$ISSUE" --remove-label "impl:${LIBRARY}:pending" 2>/dev/null || true | |
| gh issue edit "$ISSUE" --add-label "impl:${LIBRARY}:done" | |
| - name: Post success to issue | |
| if: steps.check.outputs.should_run == 'true' && steps.issue.outputs.number != '' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| ISSUE: ${{ steps.issue.outputs.number }} | |
| LIBRARY: ${{ steps.extract.outputs.library }} | |
| SPEC_ID: ${{ steps.extract.outputs.specification_id }} | |
| PR_NUM: ${{ steps.check.outputs.pr_number }} | |
| QUALITY_SCORE: ${{ steps.quality.outputs.score }} | |
| run: | | |
| BODY="## :white_check_mark: ${LIBRARY} Complete | |
| **${LIBRARY}** implementation for \`${SPEC_ID}\` has been merged to main. | |
| **PR:** #${PR_NUM}" | |
| if [ -n "$QUALITY_SCORE" ]; then | |
| BODY="${BODY} | |
| **Quality Score:** ${QUALITY_SCORE}/100" | |
| fi | |
| BODY="${BODY} | |
| **Preview:** [View image](https://storage.googleapis.com/pyplots-images/plots/${SPEC_ID}/${LIBRARY}/plot.png) | |
| --- | |
| :robot: *[impl-merge](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})*" | |
| gh issue comment "$ISSUE" --body "$BODY" | |
| - name: Close issue if all libraries done | |
| if: steps.check.outputs.should_run == 'true' && steps.issue.outputs.number != '' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| ISSUE: ${{ steps.issue.outputs.number }} | |
| SPEC_ID: ${{ steps.extract.outputs.specification_id }} | |
| run: | | |
| # All 9 supported libraries | |
| LIBRARIES="matplotlib seaborn plotly bokeh altair plotnine pygal highcharts letsplot" | |
| # Get current labels on the issue | |
| LABELS=$(gh issue view "$ISSUE" --json labels -q '.labels[].name' 2>/dev/null || echo "") | |
| # Count done and failed implementations | |
| DONE_COUNT=0 | |
| FAILED_COUNT=0 | |
| DONE_LIBS="" | |
| FAILED_LIBS="" | |
| for lib in $LIBRARIES; do | |
| if echo "$LABELS" | grep -q "^impl:${lib}:done$"; then | |
| DONE_COUNT=$((DONE_COUNT + 1)) | |
| DONE_LIBS="$DONE_LIBS $lib" | |
| elif echo "$LABELS" | grep -q "^impl:${lib}:failed$"; then | |
| FAILED_COUNT=$((FAILED_COUNT + 1)) | |
| FAILED_LIBS="$FAILED_LIBS $lib" | |
| fi | |
| done | |
| TOTAL=$((DONE_COUNT + FAILED_COUNT)) | |
| echo "::notice::Libraries: $DONE_COUNT done, $FAILED_COUNT failed, $TOTAL/9 total" | |
| # Close issue if all 9 libraries are done OR done+failed=9 | |
| if [ "$TOTAL" -eq 9 ]; then | |
| # Build status table | |
| TABLE="| Library | Status |\n|---------|--------|" | |
| for lib in $LIBRARIES; do | |
| if echo "$DONE_LIBS" | grep -w -q "$lib"; then | |
| TABLE="$TABLE\n| $lib | :white_check_mark: |" | |
| elif echo "$FAILED_LIBS" | grep -w -q "$lib"; then | |
| TABLE="$TABLE\n| $lib | :x: (not supported) |" | |
| fi | |
| done | |
| if [ "$FAILED_COUNT" -eq 0 ]; then | |
| TITLE=":tada: All Implementations Complete!" | |
| SUMMARY="All 9 library implementations for \`${SPEC_ID}\` have been successfully merged." | |
| else | |
| TITLE=":white_check_mark: Implementations Complete" | |
| SUMMARY="${DONE_COUNT}/9 implementations merged, ${FAILED_COUNT} libraries could not implement this plot type." | |
| fi | |
| gh issue comment "$ISSUE" --body "## ${TITLE} | |
| ${SUMMARY} | |
| $(echo -e "$TABLE") | |
| --- | |
| :robot: *[impl-merge](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})*" | |
| gh issue close "$ISSUE" | |
| echo "::notice::Closed issue #$ISSUE - all implementations complete ($DONE_COUNT done, $FAILED_COUNT failed)" | |
| fi | |
| - name: Trigger database sync | |
| if: steps.check.outputs.should_run == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| gh workflow run sync-postgres.yml | |
| echo "::notice::Triggered sync-postgres.yml" |