mirror of
https://github.com/stan220/flathub.git
synced 2026-09-08 16:28:52 +00:00
350 lines
15 KiB
YAML
350 lines
15 KiB
YAML
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 <<< "$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 <<< "$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" \
|
|
&& ! comment_exists_any "$LOCKED_COMMENT_PARTIAL" \
|
|
&& [ "$(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. Please build it locally following the
|
|
instructions and push the relevant changes first.
|
|
LOCKED_COMMENT_PARTIAL: "has too many builds failing"
|
|
REVIEW_COMMENT_PARTIAL: "This pull request is temporarily marked as blocked as some"
|
|
DOMAIN_COMMENT_PARTIAL: "The domain to be used for verification is"
|