name: Check PRs on: workflow_dispatch: schedule: - cron: '0 */2 * * *' concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: validate-prs: runs-on: ubuntu-latest timeout-minutes: 45 permissions: pull-requests: write steps: - name: Install deps run: | pip install 'publicsuffixlist==1.0.2.20250802' - name: Fetch recent PR numbers run: | CUTOFF_DATE="2025-05-25T00:00:00Z" prs_raw=$(gh pr list --base "new-pr" -L 50 --state open --json number,createdAt,updatedAt,isDraft,labels \ | jq -r --arg cutoff "$CUTOFF_DATE" '[. [] | select( .isDraft == false and .createdAt >= $cutoff and .updatedAt >= (now - (2 * 24 * 60 * 60) | todate) and (all(.labels[].name; . != "Stale") or (.labels == [])) ) | .number ] | .[]') prs_list=$(printf '%s\n' $prs_raw | head -30 | paste -sd "," -) echo "PR_LIST=$prs_list" >> "$GITHUB_ENV" draft_prs_raw=$(gh pr list --base "new-pr" -L 50 --state open --json number,createdAt,updatedAt,isDraft,labels \ | jq -r --arg cutoff "$CUTOFF_DATE" '[. [] | select( .isDraft == true and .createdAt >= $cutoff and .updatedAt >= (now - (2 * 24 * 60 * 60) | todate) and (all(.labels[].name; . != "Stale") or (.labels == [])) ) | .number ] | .[]') draft_prs_list=$(printf '%s\n' $draft_prs_raw | head -30 | paste -sd "," -) echo "DRAFT_PR_LIST=$draft_prs_list" >> "$GITHUB_ENV" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPO: ${{ github.repository }} - name: Label draft PRs env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPO: ${{ github.repository }} run: | IFS=',' read -ra PR_NUMBERS <<< "${{ env.DRAFT_PR_LIST }}" if [ ${#PR_NUMBERS[@]} -eq 0 ] || [ -z "${PR_NUMBERS[0]}" ]; then echo "No PRs found, exiting." exit 0 fi for PR_NUM in "${PR_NUMBERS[@]}"; do echo "Checking PR https://github.com/flathub/flathub/pull/$PR_NUM" gh pr edit "$PR_NUM" --add-label "work-in-progress" >/dev/null 2>&1 || true done - name: Validate PRs run: | IFS=',' read -ra PR_NUMBERS <<< "${{ env.PR_LIST }}" if [ ${#PR_NUMBERS[@]} -eq 0 ] || [ -z "${PR_NUMBERS[0]}" ]; then echo "No PRs found, exiting." exit 0 fi for PR_NUM in "${PR_NUMBERS[@]}"; do echo "===================== Checking PR https://github.com/flathub/flathub/pull/$PR_NUM =====================" json=$(gh pr view "$PR_NUM" --json body,title -q '{body: .body, title: .title}') PR_BODY=$(echo "$json" | jq -r .body) PR_TITLE=$(echo "$json" | jq -r .title) export PR_TITLE="$PR_TITLE" NORMALIZED_PR_BODY=$(printf "%s\n" "$PR_BODY" | tr -d '\r') EXIT_CODE=$(curl -sSL "$SCRIPT_URL" | python3 | grep 'EXIT_CODE=' | cut -d= -f2) echo "EXIT_CODE is $EXIT_CODE" PR_COMMENTS="$(gh pr view "$PR_NUM" --json comments -q '.comments[] | select(.author.login == "flathubbot" or .author.login == "github-actions") | .body')" PR_LABELS="$(gh pr view "$PR_NUM" --json labels -q '.labels[].name')" get_unresolved_review_threads() { owner=$1 repo=$2 pr_number=$3 gh api graphql -f query=' query($owner: String!, $repo: String!, $number: Int!) { repository(owner: $owner, name: $repo) { pullRequest(number: $number) { reviewThreads(first: 100) { nodes { isResolved comments(first: 1) { nodes { body path } } } } } } }' \ -F owner="$owner" \ -F repo="$repo" \ -F number="$pr_number" \ --jq '[.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false)] | length' 2>/dev/null } PR_THREAD_COUNT=$(get_unresolved_review_threads flathub flathub $PR_NUM) PR_THREAD_COUNT=${PR_THREAD_COUNT:-0} PR_FILES=$(gh pr view "$PR_NUM" --json files --jq '.files[].path') COMMIT_COUNT="$(gh api repos/$GH_REPO/pulls/$PR_NUM --jq '.commits' 2>/dev/null)" CONTAINS_MASTER=$(gh api repos/$GH_REPO/pulls/$PR_NUM/commits --jq '.[1] | (.commit.author.email == "mclasen@redhat.com" and .commit.message == "Add some instructions")' 2>/dev/null) BLOCKED=0 CHECKLIST_FAILED=0 REVIEW_COMMENT_LINES=() gh pr view $PR_NUM --json isDraft --jq '.isDraft' | grep -q false && \ gh pr edit $PR_NUM --remove-label "work-in-progress" >/dev/null 2>&1 || true latest_build_succeeded() { printf '%s\n' "$PR_COMMENTS" \ | grep 'Test build' \ | tail -n 1 \ | grep -Fq "$BUILD_SUCCESS_COMMENT" } comment_exists() { local comment="$1" echo "$PR_COMMENTS" | grep -Fq "$comment" } comment_exists_full() { local comment="$1" echo "$PR_COMMENTS" | grep -Fxq "$comment" } comment_contains() { local substr="$1" echo "$PR_COMMENTS" | grep -Fq "$substr" } comment_exists_any() { for comment in "$@"; do if echo "$PR_COMMENTS" | grep -Fq "$comment"; then return 0 fi done return 1 } label_exists_any() { for label in "$@"; do if echo "$PR_LABELS" | grep -Fxq "$label"; then return 0 fi done return 1 } start_build() { if ! label_exists_any "pr-check-blocked" "blocked" \ && ! comment_exists_any "$BUILD_START_COMMENT_PARTIAL" \ && ! latest_build_succeeded then echo "PR is not blocked and no active or successful build exists. Starting a build" gh pr comment "$PR_NUM" --body "$BUILD_START_COMMENT" >/dev/null 2>&1 || true fi } if [ "${CONTAINS_MASTER:-false}" = "true" ]; then echo "PR contains commits from master branch" REVIEW_COMMENT_LINES+=("- PR does not contain commits from the [master branch](https://github.com/flathub/flathub/commits/master/)") BLOCKED=1 fi if [ "$EXIT_CODE" -ne 0 ]; then echo "PR title validation failed" REVIEW_COMMENT_LINES+=('- PR title is "Add $FLATPAK_ID"') BLOCKED=1 fi if echo "$PR_FILES" | grep -qE '.*/flathub\.json$'; then echo "flathub.json not at toplevel" BLOCKED=1 REVIEW_COMMENT_LINES+=("- flathub.json file is at toplevel") fi if ! echo "$PR_FILES" | grep -qE '^[^/]+\.(ya?ml|json)$'; then echo "No JSON/YAML files at toplevel" BLOCKED=1 REVIEW_COMMENT_LINES+=("- Flatpak manifest is at toplevel") fi CHECKLIST_PRESENT=$(printf "%s\n" "$PR_BODY" | grep -cE '^- ?\[[xX]\] [A-Za-z]+' || true) echo "Total checklists found: $CHECKLIST_PRESENT" UNCHECKED=$(printf "%s\n" "$PR_BODY" | grep -Ec '^- \[ \] [A-Za-z]+' || true) echo "Unchecked checklists found: $UNCHECKED" if [ "$CHECKLIST_PRESENT" -eq 0 ]; then echo "No checklist present in PR body" CHECKLIST_FAILED=1 BLOCKED=1 elif [ "$UNCHECKED" -gt 0 ]; then echo "Checklist incomplete in PR body" CHECKLIST_FAILED=1 BLOCKED=1 fi REQUIRED_ITEM="I have read and followed all the [Submission requirements]" REQUIRED_CHECKED=$(printf "%s\n" "$NORMALIZED_PR_BODY" | grep -Fi "[x] $REQUIRED_ITEM" || true) if [ -z "$REQUIRED_CHECKED" ]; then echo "Required checklist not found in PR body" CHECKLIST_FAILED=1 BLOCKED=1 fi if [ "$CHECKLIST_FAILED" -eq 1 ]; then REVIEW_COMMENT_LINES+=("- All [checklists](https://github.com/flathub/flathub/blob/master/.github/pull_request_template.md?plain=1) are present in PR body and are completed") fi if [ "$BLOCKED" -eq 1 ]; then REVIEW_COMMENT_LINES+=("- The [requirements](https://docs.flathub.org/docs/for-app-authors/requirements) and [submission process](https://docs.flathub.org/docs/for-app-authors/submission) have been followed") fi echo "BLOCKED is set to $BLOCKED" if [ "$BLOCKED" -eq 0 ]; then if ! label_exists_any "blocked"; then DOMAIN=$(curl -sSL "$DOMAIN_SCRIPT_URL" | python3 - "$PR_TITLE") if [ -n "$DOMAIN" ] && [ "$DOMAIN" != "None" ]; then echo "Domain is: $DOMAIN" VERIF_URL="https://$DOMAIN/.well-known/org.flathub.VerifiedApps.txt" VERIF_COMMENT_1="If you intend to [verify](https://docs.flathub.org/docs/for-app-authors/verification) this submission, please" VERIF_COMMENT_2="confirm by uploading an empty \`org.flathub.VerifiedApps.txt\` file to $VERIF_URL. Otherwise, ignore this" VERIF_COMMENT="$VERIF_COMMENT_1 $VERIF_COMMENT_2" DOMAIN_COMMENT="$DOMAIN_COMMENT_PARTIAL $DOMAIN. $VERIF_COMMENT. Please comment if this incorrect." if ! comment_contains "$VERIF_URL"; then echo "Did not find domain comment, commenting" gh pr comment "$PR_NUM" --body "$DOMAIN_COMMENT" >/dev/null 2>&1 || true fi fi fi if label_exists_any "pr-check-blocked"; then echo "Removing pr-check-blocked label" gh pr edit "$PR_NUM" --remove-label "pr-check-blocked" >/dev/null 2>&1 || true fi if ! label_exists_any "awaiting-changes" "awaiting-upstream" "blocked" "reviewed-waiting"; then echo "Marking as awaiting-review" gh pr edit "$PR_NUM" --add-label "awaiting-review" --remove-label "pr-check-blocked" >/dev/null 2>&1 || true fi start_build elif [ "$BLOCKED" -eq 1 ]; then echo "Marking as blocked" gh pr edit "$PR_NUM" --add-label "pr-check-blocked" --remove-label "awaiting-review" >/dev/null 2>&1 || true if ! comment_exists "$REVIEW_COMMENT_PARTIAL"; then echo "Did not find comment, commenting" REVIEW_COMMENT="$BASE_REVIEW_COMMENT" for line in "${REVIEW_COMMENT_LINES[@]}"; do REVIEW_COMMENT="$REVIEW_COMMENT"$'\n'"$line" done gh pr comment "$PR_NUM" --body "$REVIEW_COMMENT" >/dev/null 2>&1 || true else echo "Found comment, skipping commenting" fi else echo "Nothing to do" start_build fi if label_exists_any "pr-check-blocked" "blocked" \ && ! comment_exists_any "$BUILD_SUCCESS_COMMENT" \ && [ "$(echo "$PR_COMMENTS" | grep -E -c 'Test build.*failed')" -gt 5 ]; then echo "PR is blocked and too many failing builds. Locking" gh pr comment "$PR_NUM" --body "$LOCKED_COMMENT" >/dev/null 2>&1 || true gh pr lock "$PR_NUM" >/dev/null 2>&1 || true fi if label_exists_any "awaiting-review" && [ "$PR_THREAD_COUNT" -gt 0 ]; then echo "Has awaiting-review label and review comments, marking as awaiting-changes" gh pr edit "$PR_NUM" \ --add-label "awaiting-changes" \ --remove-label "awaiting-review" >/dev/null 2>&1 || true elif label_exists_any "awaiting-changes" \ && ! label_exists_any \ "awaiting-upstream" \ "work-in-progress" \ "pr-check-blocked" \ "blocked" \ && ! latest_build_succeeded \ && [ "$PR_THREAD_COUNT" -eq 0 ]; then echo "No review comments, ensuring awaiting-review label" gh pr edit "$PR_NUM" \ --add-label "awaiting-review" \ --remove-label "awaiting-changes" >/dev/null 2>&1 || true fi echo "====================================== Finished checking PR =======================================" done env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPO: ${{ github.repository }} SCRIPT_URL: https://raw.githubusercontent.com/flathub/flathub/refs/heads/master/.github/scripts/validate.py DOMAIN_SCRIPT_URL: https://raw.githubusercontent.com/flathub/flathub/refs/heads/master/.github/scripts/domain_from_appid.py BUILD_SUCCESS_COMMENT: "[Test build succeeded]" BUILD_START_COMMENT: | Starting a test build of the submission. Please fix any issues reported in the build log. You can restart the build once the issue is fixed by commenting the phrase below. bot, build BUILD_START_COMMENT_PARTIAL: "Starting a test build of the submission" BASE_REVIEW_COMMENT: | This pull request is temporarily marked as blocked as some automated checks failed on it. Please make sure the following items are done: LOCKED_COMMENT: > This pull request is marked as blocked and has too many builds failing. Locking this PR temporarily. Please build it locally following the instructions and push the relevant changes first. REVIEW_COMMENT_PARTIAL: "This pull request is temporarily marked as blocked as some" DOMAIN_COMMENT_PARTIAL: "The domain to be used for verification is"