From a8bf9a8d24e6b7e9ee08401490ae4121e9ebf94f Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Thu, 23 Apr 2026 18:38:49 +0300 Subject: [PATCH 1/2] Open upstream release docs PR as draft Keep the auto-generated release docs PR as a draft until the full augmentation job (skill, review, autofix, body update, reviewer assignment) finishes. Prevents accidental approve/merge on a half- written PR and consolidates reviewer notifications to a single coherent signal when the PR is actually reviewable. - Bootstrap PR now created with --draft - New "Convert PR to draft" step flips Renovate-opened PRs at the start of the job (idempotent via isDraft check) - Reviewer assignment moved from pre-skill to post-commit-push so the review-requested email fires alongside the body augmentation - New "Flip PR to ready for review" step at the end, gated on success() so failed runs leave the PR as draft for a human Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/upstream-release-docs.yml | 205 ++++++++++++-------- 1 file changed, 128 insertions(+), 77 deletions(-) diff --git a/.github/workflows/upstream-release-docs.yml b/.github/workflows/upstream-release-docs.yml index 535345fb..cc8695bc 100644 --- a/.github/workflows/upstream-release-docs.yml +++ b/.github/workflows/upstream-release-docs.yml @@ -204,7 +204,11 @@ jobs: EOF sed -i 's/^ //' /tmp/bootstrap-body.md + # Opens as draft; the "Flip PR to ready for review" step at + # the end of the job flips it once the body has been augmented + # and reviewers assigned. gh pr create \ + --draft \ --base "$BASE_REF" \ --head "$BRANCH" \ --title "Update $PROJECT_ID to $NEW_TAG (manual dispatch)" \ @@ -253,6 +257,27 @@ jobs: echo "base_ref=$BASE" } >> "$GITHUB_OUTPUT" + # Flip the PR to draft for the duration of the augmentation job. + # A Renovate-opened PR arrives non-draft with the default bump- + # only body -- reviewers could approve / merge it before the + # skill has written any content. Bootstrap PRs are already + # --draft from the create call above, but we still run this for + # idempotency on workflow_dispatch retries. The "Flip PR to + # ready for review" step at the end reverses this once the body + # is augmented and reviewers are assigned. + - name: Convert PR to draft + if: steps.eff.outputs.number != '' + env: + PR_NUMBER: ${{ steps.eff.outputs.number }} + run: | + IS_DRAFT=$(gh pr view "$PR_NUMBER" --json isDraft --jq .isDraft) + if [ "$IS_DRAFT" = "false" ]; then + gh pr ready "$PR_NUMBER" --undo + echo "Converted PR #$PR_NUMBER to draft." + else + echo "PR #$PR_NUMBER is already a draft; nothing to do." + fi + - name: Checkout PR branch uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: @@ -394,83 +419,6 @@ jobs: id: pre_skill run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - - name: Assign reviewers and prepare contributor mentions - id: reviewers - env: - REPO: ${{ steps.detect.outputs.repo }} - PREV: ${{ steps.detect.outputs.prev_tag }} - NEW: ${{ steps.detect.outputs.new_tag }} - REVIEW_REPO: ${{ github.repository }} - PR_NUMBER: ${{ steps.eff.outputs.number }} - run: | - # Get non-bot commit authors in the release range. - if COMPARE=$(gh api "repos/$REPO/compare/$PREV...$NEW" \ - --jq '[.commits[].author.login? // empty] | unique | .[]' 2>/dev/null); then - echo "compare_ok=true" >> "$GITHUB_OUTPUT" - else - COMPARE="" - echo "compare_ok=false" >> "$GITHUB_OUTPUT" - fi - - # Filter out bot accounts. - CANDIDATES=$(echo "$COMPARE" | - grep -Ev '(\[bot\]$|^github-actions|^stacklokbot$|^dependabot|^renovate|^copilot)' || true) - - # Attempt to assign each candidate as a reviewer individually, - # rather than filtering upfront and batching. Rationale: - # - `gh pr edit --add-reviewer "a,b,c"` is atomic. A single - # 422 on any name aborts the whole call, dropping valid - # names alongside invalid ones. - # - `gh api repos/X/collaborators/Y` as a pre-filter is - # unreliable from a GITHUB_TOKEN in Actions: on PR #759 - # the check returned 404 for Stacklok employees who ARE - # collaborators via the `stackers` team (push perm on - # this repo), and only `rdimitrov` slipped through. We - # suspect the collaborator endpoint treats team-based - # access differently for GITHUB_TOKEN vs PATs with - # read:org, but haven't nailed down the exact rule. - # Per-user attempts sidestep both issues: the authoritative - # answer is "does GitHub accept this as a reviewer right now" - # and we ask the API that question directly. - # - # Cap attempts at 5 to avoid review fatigue on big releases. - TRIED=0 - ASSIGN_LIST="" - MENTION_LIST="" - while IFS= read -r login; do - [ -z "$login" ] && continue - if [ "$TRIED" -ge 5 ]; then - # Over the review-fatigue cap -- mention any additional - # contributors instead of trying to assign them. - MENTION_LIST="${MENTION_LIST:+$MENTION_LIST }@$login" - continue - fi - TRIED=$((TRIED + 1)) - if gh pr edit "$PR_NUMBER" --add-reviewer "$login" 2>/dev/null; then - ASSIGN_LIST="${ASSIGN_LIST:+$ASSIGN_LIST,}$login" - echo "Assigned: $login" - else - MENTION_LIST="${MENTION_LIST:+$MENTION_LIST }@$login" - echo "Mention (assignment rejected by GitHub): $login" - fi - done <<< "$CANDIDATES" - - # Exposed for diagnostic visibility in the PR body (e.g., - # "Auto-assigned: @alice @bob") and for the next workflow_ - # dispatch retry to know what was attempted. - echo "list=$ASSIGN_LIST" >> "$GITHUB_OUTPUT" - { - echo "mention_block<> "$GITHUB_OUTPUT" - echo "Auto-assigned: ${ASSIGN_LIST:-}" - echo "Mentioned: ${MENTION_LIST:-}" - - name: Read docs_paths hint id: hints env: @@ -1042,6 +990,91 @@ jobs: git push origin "HEAD:$HEAD_REF" fi + # Reviewer assignment is deliberately at the end of the job (not + # at skill-start). Placement rationale: GitHub fires the + # "review-requested" email the moment `gh pr edit --add-reviewer` + # runs. If we assign early, reviewers land on a half-written, + # draft PR whose body is still the default Renovate bump-only + # text. Running this right before the body augmentation and + # flip-to-ready gives reviewers a single coherent notification + # on a PR that's actually reviewable. + - name: Assign reviewers and prepare contributor mentions + id: reviewers + env: + REPO: ${{ steps.detect.outputs.repo }} + PREV: ${{ steps.detect.outputs.prev_tag }} + NEW: ${{ steps.detect.outputs.new_tag }} + REVIEW_REPO: ${{ github.repository }} + PR_NUMBER: ${{ steps.eff.outputs.number }} + run: | + # Get non-bot commit authors in the release range. + if COMPARE=$(gh api "repos/$REPO/compare/$PREV...$NEW" \ + --jq '[.commits[].author.login? // empty] | unique | .[]' 2>/dev/null); then + echo "compare_ok=true" >> "$GITHUB_OUTPUT" + else + COMPARE="" + echo "compare_ok=false" >> "$GITHUB_OUTPUT" + fi + + # Filter out bot accounts. + CANDIDATES=$(echo "$COMPARE" | + grep -Ev '(\[bot\]$|^github-actions|^stacklokbot$|^dependabot|^renovate|^copilot)' || true) + + # Attempt to assign each candidate as a reviewer individually, + # rather than filtering upfront and batching. Rationale: + # - `gh pr edit --add-reviewer "a,b,c"` is atomic. A single + # 422 on any name aborts the whole call, dropping valid + # names alongside invalid ones. + # - `gh api repos/X/collaborators/Y` as a pre-filter is + # unreliable from a GITHUB_TOKEN in Actions: on PR #759 + # the check returned 404 for Stacklok employees who ARE + # collaborators via the `stackers` team (push perm on + # this repo), and only `rdimitrov` slipped through. We + # suspect the collaborator endpoint treats team-based + # access differently for GITHUB_TOKEN vs PATs with + # read:org, but haven't nailed down the exact rule. + # Per-user attempts sidestep both issues: the authoritative + # answer is "does GitHub accept this as a reviewer right now" + # and we ask the API that question directly. + # + # Cap attempts at 5 to avoid review fatigue on big releases. + TRIED=0 + ASSIGN_LIST="" + MENTION_LIST="" + while IFS= read -r login; do + [ -z "$login" ] && continue + if [ "$TRIED" -ge 5 ]; then + # Over the review-fatigue cap -- mention any additional + # contributors instead of trying to assign them. + MENTION_LIST="${MENTION_LIST:+$MENTION_LIST }@$login" + continue + fi + TRIED=$((TRIED + 1)) + if gh pr edit "$PR_NUMBER" --add-reviewer "$login" 2>/dev/null; then + ASSIGN_LIST="${ASSIGN_LIST:+$ASSIGN_LIST,}$login" + echo "Assigned: $login" + else + MENTION_LIST="${MENTION_LIST:+$MENTION_LIST }@$login" + echo "Mention (assignment rejected by GitHub): $login" + fi + done <<< "$CANDIDATES" + + # Exposed for diagnostic visibility in the PR body (e.g., + # "Auto-assigned: @alice @bob") and for the next workflow_ + # dispatch retry to know what was attempted. + echo "list=$ASSIGN_LIST" >> "$GITHUB_OUTPUT" + { + echo "mention_block<> "$GITHUB_OUTPUT" + echo "Auto-assigned: ${ASSIGN_LIST:-}" + echo "Mentioned: ${MENTION_LIST:-}" + - name: Augment PR body (marker-delimited section) # Runs even if earlier steps soft-failed so the augmentation # survives partial failures; a subsequent workflow_dispatch @@ -1317,6 +1350,24 @@ jobs: gh pr edit "$PR_NUMBER" --body-file /tmp/pr-body.md + # Counterpart to the "Convert PR to draft" step at the top of + # the job. Uses success() (not always()) on purpose: a failed + # run leaves the PR as draft so nobody merges a half-written + # augmentation. The existing failure-comment step at the end of + # the job points reviewers to the retry command. + - name: Flip PR to ready for review + if: success() && steps.eff.outputs.number != '' + env: + PR_NUMBER: ${{ steps.eff.outputs.number }} + run: | + IS_DRAFT=$(gh pr view "$PR_NUMBER" --json isDraft --jq .isDraft) + if [ "$IS_DRAFT" = "true" ]; then + gh pr ready "$PR_NUMBER" + echo "Flipped PR #$PR_NUMBER to ready for review." + else + echo "PR #$PR_NUMBER is already ready for review; nothing to do." + fi + # Post-run summary for workflow_dispatch, mirroring the pre-run # placeholder comment above. Gives reviewers a single point in # the PR timeline that says "here's what happened, and where to From 8cd343dda50bbb8134a1181731c2c904d4686bb6 Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Thu, 23 Apr 2026 18:51:36 +0300 Subject: [PATCH 2/2] Address Copilot review: --repo flag and COMPARE_OK tri-state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two issues surfaced in Copilot's review of the initial commit: - "Convert PR to draft" runs before the PR branch is checked out, so `gh pr view` / `gh pr ready` must pass --repo or they fail with "not a git repository". The existing pre-checkout step at line 121 already calls this out; apply the same fix here. - Moving reviewer assignment to the end of the job introduces a new state: the augment-PR-body step is `if: always()`, but reviewers is not, so on upstream skill/autofix failures the augment step runs with `COMPARE_OK` unset. The previous `!= "true"` branch rendered that as "Compare failed โ€” pinned tag missing upstream", which is misleading because the compare was never attempted. Handle empty as a distinct "Not attempted" branch. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/upstream-release-docs.yml | 24 ++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/.github/workflows/upstream-release-docs.yml b/.github/workflows/upstream-release-docs.yml index cc8695bc..77d10525 100644 --- a/.github/workflows/upstream-release-docs.yml +++ b/.github/workflows/upstream-release-docs.yml @@ -265,14 +265,20 @@ jobs: # idempotency on workflow_dispatch retries. The "Flip PR to # ready for review" step at the end reverses this once the body # is augmented and reviewers are assigned. + # --repo is required on `gh pr view` / `gh pr ready` here because + # this step runs before the "Checkout PR branch" step below; gh + # otherwise looks for local .git context and fails with "not a + # git repository" on pull_request and workflow_dispatch retries. + # (Bootstrap has already checked out the dispatching branch, but + # the flag is harmless there and keeps this step path-agnostic.) - name: Convert PR to draft if: steps.eff.outputs.number != '' env: PR_NUMBER: ${{ steps.eff.outputs.number }} run: | - IS_DRAFT=$(gh pr view "$PR_NUMBER" --json isDraft --jq .isDraft) + IS_DRAFT=$(gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json isDraft --jq .isDraft) if [ "$IS_DRAFT" = "false" ]; then - gh pr ready "$PR_NUMBER" --undo + gh pr ready "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --undo echo "Converted PR #$PR_NUMBER to draft." else echo "PR #$PR_NUMBER is already a draft; nothing to do." @@ -1187,7 +1193,19 @@ jobs: MENTION_COUNT=0 fi - if [ "$COMPARE_OK" != "true" ]; then + # COMPARE_OK tri-state: + # "true" -> compare succeeded, contributor list is populated + # "false" -> compare was attempted and failed (pinned prev_tag + # missing upstream) + # "" -> reviewers step never ran (earlier step in the job + # failed and short-circuited before we got there). + # The augment step itself is `if: always()`, so it + # still renders the PR body -- but the contributor + # cell must say "not attempted" rather than claim + # a compare failure that never happened. + if [ -z "$COMPARE_OK" ]; then + CONTRIB_CELL="**Not attempted** โ€” run failed before reviewer assignment" + elif [ "$COMPARE_OK" != "true" ]; then CONTRIB_CELL="**Compare failed** โ€” pinned \`$PREV_TAG\` missing upstream, no auto-assignment" elif [ "$ASSIGN_COUNT" -gt 0 ] && [ "$MENTION_COUNT" -gt 0 ]; then CONTRIB_CELL="$ASSIGN_COUNT auto-assigned ยท $MENTION_COUNT mentioned below"