|
| 1 | +on: |
| 2 | + pull_request_target: |
| 3 | + types: [opened, reopened, edited, synchronize] |
| 4 | + |
| 5 | +jobs: |
| 6 | + validate-issue: |
| 7 | + runs-on: ubuntu-latest |
| 8 | + permissions: |
| 9 | + pull-requests: write |
| 10 | + issues: read |
| 11 | + |
| 12 | + steps: |
| 13 | + - name: Scan PR description for Issue URL(s) |
| 14 | + id: scan |
| 15 | + env: |
| 16 | + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 17 | + PR_NUMBER: ${{ github.event.pull_request.number }} |
| 18 | + REPO_FULL: ${{ github.repository }} |
| 19 | + run: | |
| 20 | + # Fetch PR body safely (handles null -> empty string) |
| 21 | + BODY=$(gh api repos/$REPO_FULL/pulls/$PR_NUMBER --jq '.body // ""') |
| 22 | +
|
| 23 | + # Look for direct GitHub Issue links (any org/repo) |
| 24 | + URLS=$(printf '%s\n' "$BODY" \ |
| 25 | + | grep -Eo 'https://github\.com/airbnb/viaduct/issues/[0-9]+' \ |
| 26 | + | sort -u) |
| 27 | +
|
| 28 | + if [ -z "$URLS" ]; then |
| 29 | + echo "none=true" >> "$GITHUB_OUTPUT" |
| 30 | + else |
| 31 | + echo "none=false" >> "$GITHUB_OUTPUT" |
| 32 | + { |
| 33 | + echo "list<<EOF" |
| 34 | + echo "$URLS" |
| 35 | + echo "EOF" |
| 36 | + } >> "$GITHUB_OUTPUT" |
| 37 | + fi |
| 38 | +
|
| 39 | + - name: Status — PR has Issue URL(s) |
| 40 | + if: steps.scan.outputs.none == 'false' |
| 41 | + run: | |
| 42 | + echo "✅ PR description contains Issue URL(s):" |
| 43 | + echo "${{ steps.scan.outputs.list }}" |
| 44 | +
|
| 45 | + - name: Status — PR has no Issue URL |
| 46 | + if: steps.scan.outputs.none == 'true' |
| 47 | + run: | |
| 48 | + echo "⚠️ PR description contains no direct Issue URL." |
| 49 | +
|
| 50 | + - name: Comment on PR if missing Issue URL |
| 51 | + if: steps.scan.outputs.none == 'true' |
| 52 | + env: |
| 53 | + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 54 | + PR_NUMBER: ${{ github.event.pull_request.number }} |
| 55 | + REPO_FULL: ${{ github.repository }} |
| 56 | + run: | |
| 57 | + gh api repos/$REPO_FULL/issues/$PR_NUMBER/comments \ |
| 58 | + -f body=$'⚠️ This Pull Request does not include a direct Issue link in its description.\n\nPlease add a URL to a GitHub Issue, for example:\nhttps://github.com/owner/repo/issues/123\n\n(Optional) You can also use keywords like **Closes #123**, but this check only looks for direct links.' |
| 59 | +
|
| 60 | + - name: Manage labels (issue-linked/review me/create patch + synchronize behavior) |
| 61 | + env: |
| 62 | + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 63 | + PR_NUMBER: ${{ github.event.pull_request.number }} |
| 64 | + REPO_FULL: ${{ github.repository }} |
| 65 | + ACTION: ${{ github.event.action }} |
| 66 | + LABEL_MISSING: "needs-issue ❗" |
| 67 | + LABEL_REVIEW: "ready-to-go" |
| 68 | + LABEL_CREATE: "create-patch" |
| 69 | + LABEL_INREVIEW: "in-review" |
| 70 | + run: | |
| 71 | + set -e |
| 72 | +
|
| 73 | + # URL-encode for DELETE endpoints |
| 74 | + uri() { jq -rn --arg s "$1" '$s|@uri'; } |
| 75 | +
|
| 76 | + # Ensure labels exist (best-effort) |
| 77 | + ensure_label() { |
| 78 | + local name="$1" color="$2" |
| 79 | + if ! gh api repos/$REPO_FULL/labels --paginate --jq '.[].name' | grep -Fxq "$name"; then |
| 80 | + gh api -X POST "repos/$REPO_FULL/labels" -f name="$name" -f color="$color" >/dev/null 2>&1 || true |
| 81 | + fi |
| 82 | + } |
| 83 | + ensure_label "$LABEL_MISSING" "d73a4a" # red |
| 84 | + ensure_label "$LABEL_REVIEW" "0366d6" # blue |
| 85 | + ensure_label "$LABEL_CREATE" "6f42c1" # purple |
| 86 | +
|
| 87 | + # Helpers |
| 88 | + refresh() { CURRENT=$(gh api "repos/$REPO_FULL/issues/$PR_NUMBER/labels" --jq '.[].name'); } |
| 89 | + has() { grep -Fxq "$1" <<< "$CURRENT"; } |
| 90 | + add() { gh api "repos/$REPO_FULL/issues/$PR_NUMBER/labels" -f labels[]="$1" >/dev/null; } |
| 91 | + del() { gh api -X DELETE "repos/$REPO_FULL/issues/$PR_NUMBER/labels/$(uri "$1")" >/dev/null 2>&1 || true; } |
| 92 | +
|
| 93 | + refresh |
| 94 | +
|
| 95 | + # 1) Base rule according to the presence of a link |
| 96 | + if [ "${{ steps.scan.outputs.none }}" = "false" ]; then |
| 97 | + # Link present → remove needs-issue |
| 98 | +
|
| 99 | + if has "$LABEL_MISSING"; then del "$LABEL_MISSING"; fi |
| 100 | + refresh |
| 101 | +
|
| 102 | + # Exclusion with create patch: if create patch exists → do not put review me (and remove if it was there) |
| 103 | + if has "$LABEL_CREATE"; then |
| 104 | + if has "$LABEL_REVIEW"; then del "$LABEL_REVIEW"; fi |
| 105 | + else |
| 106 | + # create patch does not exist |
| 107 | + if ! has "$LABEL_REVIEW"; then add "$LABEL_REVIEW"; fi |
| 108 | + fi |
| 109 | + else |
| 110 | + # No link → needs-issue; remove review me / create patch |
| 111 | + has "$LABEL_MISSING" || add "$LABEL_MISSING" |
| 112 | + if has "$LABEL_REVIEW"; then del "$LABEL_REVIEW"; fi |
| 113 | + if has "$LABEL_CREATE"; then del "$LABEL_CREATE"; fi |
| 114 | + fi |
| 115 | +
|
| 116 | + refresh |
| 117 | +
|
| 118 | + # 2) Extra behavior in synchronize: in review -> create patch, and remove in review |
| 119 | + if [ "$ACTION" = "synchronize" ] && has "$LABEL_INREVIEW"; then |
| 120 | + has "$LABEL_CREATE" || add "$LABEL_CREATE" |
| 121 | + del "$LABEL_INREVIEW" |
| 122 | + refresh |
| 123 | + if has "$LABEL_CREATE" && has "$LABEL_REVIEW"; then del "$LABEL_REVIEW"; fi |
| 124 | + fi |
0 commit comments