Skip to content

GIT-869c63k1e: enforce ClickUp branch/PR/commit linking guardrails (#89) #4

GIT-869c63k1e: enforce ClickUp branch/PR/commit linking guardrails (#89)

GIT-869c63k1e: enforce ClickUp branch/PR/commit linking guardrails (#89) #4

Workflow file for this run

name: PR Governance

Check failure on line 1 in .github/workflows/pr-governance.yml

View workflow run for this annotation

GitHub Actions / .github/workflows/pr-governance.yml

Invalid workflow file

(Line: 20, Col: 30): Unrecognized function: 'replace'. Located at position 27 within expression: vars.SONAR_PROJECT_KEY || replace(github.repository, '/', '_')
on:
pull_request:
types: [opened, edited, synchronize, reopened, ready_for_review]
branches: [main]
permissions:
contents: read
pull-requests: read
jobs:
enforce-governance:
runs-on: ubuntu-latest
steps:
- name: Enforce Sonar quality gate and new issues policy
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
SONAR_ORGANIZATION: ${{ vars.SONAR_ORGANIZATION || github.repository_owner }}
SONAR_PROJECT_KEY: ${{ vars.SONAR_PROJECT_KEY || replace(github.repository, '/', '_') }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_MAX_POLL_ITERATIONS: "30"
SONAR_POLL_INTERVAL_SECONDS: "10"
run: |
set -euo pipefail
MAX_POLL_ITERATIONS="${SONAR_MAX_POLL_ITERATIONS:-30}"
POLL_INTERVAL_SECONDS="${SONAR_POLL_INTERVAL_SECONDS:-10}"
SONAR_AUTH_ARGS=()
if [[ -n "${SONAR_TOKEN:-}" ]]; then
SONAR_AUTH_ARGS=(-u "${SONAR_TOKEN}:")
fi
quality_status=""
attempt=1
while (( attempt <= MAX_POLL_ITERATIONS )); do
response="$(
curl -sS "${SONAR_AUTH_ARGS[@]}" \
"https://sonarcloud.io/api/project_pull_requests/list?organization=${SONAR_ORGANIZATION}&project=${SONAR_PROJECT_KEY}"
)"
quality_status="$(jq -r --arg pr "${PR_NUMBER}" '.pullRequests[]? | select(.key == $pr) | .status.qualityGateStatus' <<<"$response")"
if [[ -n "$quality_status" && "$quality_status" != "NONE" ]]; then
break
fi
sleep "${POLL_INTERVAL_SECONDS}"
attempt=$((attempt + 1))
done
if [[ -z "$quality_status" || "$quality_status" == "NONE" ]]; then
echo "Sonar result not ready for PR #${PR_NUMBER}." >&2
exit 1
fi
if [[ "$quality_status" != "OK" ]]; then
echo "Sonar quality gate must pass. Current status: ${quality_status}" >&2
exit 1
fi
issues_response="$(
curl -sS "${SONAR_AUTH_ARGS[@]}" \
"https://sonarcloud.io/api/issues/search?organization=${SONAR_ORGANIZATION}&componentKeys=${SONAR_PROJECT_KEY}&pullRequest=${PR_NUMBER}&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true&ps=1"
)"
new_issues="$(jq -r '.total // 0' <<<"$issues_response")"
if [[ "$new_issues" != "0" ]]; then
echo "PR introduces ${new_issues} Sonar new issue(s). New issues must be 0." >&2
exit 1
fi
echo "Sonar checks passed: quality gate OK and 0 new issues."
- name: Enforce inline review replies and resolved threads
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO_OWNER: ${{ github.repository_owner }}
REPO_NAME: ${{ github.event.repository.name }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
query='
query($owner:String!, $repo:String!, $number:Int!) {
repository(owner:$owner, name:$repo) {
pullRequest(number:$number) {
author { login }
reviewThreads(first:100) {
pageInfo {
hasNextPage
}
nodes {
id
isResolved
isOutdated
comments(first:100) {
pageInfo {
hasNextPage
}
nodes {
author { login }
}
}
}
}
}
}
}'
response="$(gh api graphql -f query="$query" -F owner="$REPO_OWNER" -F repo="$REPO_NAME" -F number="$PR_NUMBER")"
pr_author="$(jq -r '.data.repository.pullRequest.author.login' <<<"$response")"
has_more_threads="$(jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.hasNextPage' <<<"$response")"
has_more_comments="$(jq -r '
.data.repository.pullRequest.reviewThreads.nodes
| any(.comments.pageInfo.hasNextPage == true)
' <<<"$response")"
if [[ "$has_more_threads" == "true" || "$has_more_comments" == "true" ]]; then
echo "Review thread pagination limit reached; increase pagination handling before enforcing this check." >&2
exit 1
fi
unresolved_count="$(jq -r '
.data.repository.pullRequest.reviewThreads.nodes
| map(select(.isOutdated | not))
| map(select(.isResolved | not))
| length
' <<<"$response")"
missing_inline_reply_count="$(jq -r --arg author "$pr_author" '
.data.repository.pullRequest.reviewThreads.nodes
| map(select(.isOutdated | not))
| map(select(([.comments.nodes[]?.author.login] | index($author)) | not))
| length
' <<<"$response")"
if [[ "$missing_inline_reply_count" != "0" ]]; then
echo "Each active review thread must include an inline reply from PR author (${pr_author})." >&2
exit 1
fi
if [[ "$unresolved_count" != "0" ]]; then
echo "All active review threads must be resolved before merge." >&2
exit 1
fi
echo "Review thread checks passed."