From bc17404f7ca085aed78f7c4cc8f05cd6e15ca8c8 Mon Sep 17 00:00:00 2001 From: William Harrison <87287585+wdhdev@users.noreply.github.com> Date: Wed, 19 Aug 2026 11:48:15 +0800 Subject: [PATCH 1/5] Create check-pr-template.js --- util/check-pr-template.js | 76 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 util/check-pr-template.js diff --git a/util/check-pr-template.js b/util/check-pr-template.js new file mode 100644 index 0000000000..c05db0f978 --- /dev/null +++ b/util/check-pr-template.js @@ -0,0 +1,76 @@ +const body = process.env.PR_BODY || ""; + +const REQUIRED_CHECKBOXES = [ + "I agree to the [Terms of Service]", + "My file is following the [domain structure]", + "My website is reachable and completed.", + "My website is software development related.", + "My website is not for commercial use.", + "I have provided sufficient contact information in the `owner` key.", + "I have provided a link to my website below.", +]; + +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +function isRequiredCheckboxChecked(text) { + const regex = new RegExp( + `-\\s*\\[[xX]\\]\\s*(?:\\s*)?${escapeRegExp(text)}`, + "i", + ); + + return regex.test(body); +} + +function getSection(startMarker, endMarker) { + const regex = new RegExp( + `${escapeRegExp(startMarker)}([\\s\\S]*?)${escapeRegExp(endMarker)}`, + "i", + ); + + const match = body.match(regex); + + return match ? match[1].trim() : ""; +} + +const errors = []; + +const uncheckedRequirements = REQUIRED_CHECKBOXES.filter( + (requirement) => !isRequiredCheckboxChecked(requirement), +); + +if (uncheckedRequirements.length > 0) { + errors.push( + "The following required checkboxes have not been checked:", + ...uncheckedRequirements.map((requirement) => `- ${requirement}`), + ); +} + +const websitePreview = getSection( + "", + "", +); + +if (!websitePreview) { + errors.push("The **Website Preview** section has not been filled out."); +} + +const websitePurpose = getSection( + "", + "", +); + +if (!websitePurpose) { + errors.push("The **Website Purpose** section has not been filled out."); +} + +if (errors.length > 0) { + console.error("PR template validation failed."); + console.error(""); + console.error(errors.join("\n")); + + process.exit(1); +} + +console.log("PR template validation passed."); From dc8f6e9099330c94d9450acf8346160f21d9c1bc Mon Sep 17 00:00:00 2001 From: William Harrison <87287585+wdhdev@users.noreply.github.com> Date: Wed, 19 Aug 2026 11:53:40 +0800 Subject: [PATCH 2/5] Create pr-template.yml --- .github/workflows/pr-template.yml | 142 ++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 .github/workflows/pr-template.yml diff --git a/.github/workflows/pr-template.yml b/.github/workflows/pr-template.yml new file mode 100644 index 0000000000..cf4f83152a --- /dev/null +++ b/.github/workflows/pr-template.yml @@ -0,0 +1,142 @@ +name: PR Template + +on: + pull_request_target: + types: + - opened + - edited + - synchronize + +permissions: + contents: read + pull-requests: write + issues: write + +jobs: + validate: + name: Validate + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + ref: ${{ github.event.repository.default_branch }} + + - name: Validate PR template + id: validate + continue-on-error: true + env: + PR_BODY: ${{ github.event.pull_request.body }} + run: node scripts/validate-pr-template.js + + - name: Handle incomplete PR + if: steps.validate.outcome == 'failure' + uses: actions/github-script@v8 + with: + github-token: ${{ secrets.BOT }} + script: | + const label = "reason: incomplete pr"; + + try { + await github.rest.issues.getLabel({ + owner: context.repo.owner, + repo: context.repo.name, + name: label + }); + } catch (error) { + if (error.status === 404) { + await github.rest.issues.createLabel({ + owner: context.repo.owner, + repo: context.repo.name, + name: label, + color: "D93F0B", + description: "Your PR template is not complete. (e.g. missing the requirements form, missing website preview)" + }); + } else { + throw error; + } + } + + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.name, + issue_number: context.issue.number, + labels: [label] + }); + + const body = [ + "## ❌ Incomplete PR template", + "", + "This pull request is missing required information from the PR template.", + "", + "Please make sure that:", + "", + "- Every required checkbox is checked.", + "- **Website Preview** is filled out.", + "- **Website Purpose** is filled out.", + "", + "Once the template has been completed, this check will run again automatically." + ].join("\n"); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.name, + issue_number: context.issue.number, + body + }); + + - name: Remove incomplete label + if: steps.validate.outcome == 'success' + uses: actions/github-script@v8 + with: + github-token: ${{ secrets.BOT }} + script: | + const label = "reason: incomplete pr"; + + try { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.name, + issue_number: context.issue.number, + name: label + }); + } catch (error) { + if (error.status !== 404) { + throw error; + } + + tests: + name: Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - run: npm ci + + - name: Fetch PR information + if: github.event_name == 'pull_request' + run: | + echo "PR_AUTHOR=${{ github.event.pull_request.user.login }}" >> $GITHUB_ENV + echo "PR_AUTHOR_ID=${{ github.event.pull_request.user.id }}" >> $GITHUB_ENV + + LABELS=$(gh api --jq '[.labels[].name]' /repos/{owner}/{repo}/pulls/${{ github.event.number }}) + echo "PR_LABELS=$LABELS" >> $GITHUB_ENV + + FILES=$(gh api --jq '[.[] | select(.status != "removed") | .filename]' /repos/{owner}/{repo}/pulls/${{ github.event.number }}/files) + echo "CHANGED_FILES=$FILES" >> $GITHUB_ENV + + REMOVED_FILE_DATA=$(gh api --jq '[.[] | select(.status == "removed") | {name: .filename, data: .patch}]' /repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files) + echo "DELETED_FILES=$REMOVED_FILE_DATA" >> $GITHUB_ENV + env: + GH_TOKEN: ${{ github.token }} + + - uses: gacts/install-dnscontrol@890ba3249712c51203ccb7aba63307c0091e921b # v1.3.4 + if: github.event_name == 'pull_request' && contains(fromJson(env.CHANGED_FILES), 'dnsconfig.js') + with: + version: 4.14.0 + + - name: Run DNSControl checks + run: dnscontrol check + if: github.event_name == 'pull_request' && contains(fromJson(env.CHANGED_FILES), 'dnsconfig.js') + + - name: Run tests + run: npx ava tests/*.test.js --timeout=1m From 120bf3a8eeb3cff954ee6ebda1a693d7c9af4328 Mon Sep 17 00:00:00 2001 From: William Harrison <87287585+wdhdev@users.noreply.github.com> Date: Wed, 19 Aug 2026 11:54:35 +0800 Subject: [PATCH 3/5] Update check-pr-template.js --- util/check-pr-template.js | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/util/check-pr-template.js b/util/check-pr-template.js index c05db0f978..1b20233213 100644 --- a/util/check-pr-template.js +++ b/util/check-pr-template.js @@ -47,21 +47,21 @@ if (uncheckedRequirements.length > 0) { ); } -const websitePreview = getSection( - "", - "", -); - -if (!websitePreview) { +if ( + !getSection( + "", + "", + ) +) { errors.push("The **Website Preview** section has not been filled out."); } -const websitePurpose = getSection( - "", - "", -); - -if (!websitePurpose) { +if ( + !getSection( + "", + "", + ) +) { errors.push("The **Website Purpose** section has not been filled out."); } @@ -69,7 +69,6 @@ if (errors.length > 0) { console.error("PR template validation failed."); console.error(""); console.error(errors.join("\n")); - process.exit(1); } From 4b926a8f12871a84eb692630039aabcc8f23996d Mon Sep 17 00:00:00 2001 From: William Harrison <87287585+wdhdev@users.noreply.github.com> Date: Wed, 19 Aug 2026 11:55:31 +0800 Subject: [PATCH 4/5] Update pr-template.yml --- .github/workflows/pr-template.yml | 123 ++++++++++++++---------------- 1 file changed, 58 insertions(+), 65 deletions(-) diff --git a/.github/workflows/pr-template.yml b/.github/workflows/pr-template.yml index cf4f83152a..6bf51a6c08 100644 --- a/.github/workflows/pr-template.yml +++ b/.github/workflows/pr-template.yml @@ -16,6 +16,7 @@ jobs: validate: name: Validate runs-on: ubuntu-latest + steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: @@ -39,50 +40,78 @@ jobs: try { await github.rest.issues.getLabel({ owner: context.repo.owner, - repo: context.repo.name, + repo: context.repo.repo, name: label }); } catch (error) { if (error.status === 404) { await github.rest.issues.createLabel({ owner: context.repo.owner, - repo: context.repo.name, + repo: context.repo.repo, name: label, - color: "D93F0B", - description: "Your PR template is not complete. (e.g. missing the requirements form, missing website preview)" + color: "d73a4a", + description: "The PR template has not been completely filled out." }); } else { throw error; } } - await github.rest.issues.addLabels({ + const labels = await github.rest.issues.listLabelsOnIssue({ owner: context.repo.owner, - repo: context.repo.name, - issue_number: context.issue.number, - labels: [label] + repo: context.repo.repo, + issue_number: context.issue.number }); - const body = [ - "## ❌ Incomplete PR template", - "", - "This pull request is missing required information from the PR template.", - "", - "Please make sure that:", - "", - "- Every required checkbox is checked.", - "- **Website Preview** is filled out.", - "- **Website Purpose** is filled out.", - "", - "Once the template has been completed, this check will run again automatically." - ].join("\n"); + if (!labels.data.some((item) => item.name === label)) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + labels: [label] + }); + } - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.name, - issue_number: context.issue.number, - body - }); + const marker = ""; + + const comments = await github.paginate( + github.rest.issues.listComments, + { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number + } + ); + + const alreadyCommented = comments.some( + (comment) => + comment.user?.login === "is-a-dev-bot" && + comment.body?.includes(marker) + ); + + if (!alreadyCommented) { + const body = [ + marker, + "## ❌ Incomplete PR template", + "", + "This pull request is missing required information from the PR template.", + "", + "Please make sure that:", + "", + "- Every required checkbox is checked.", + "- **Website Preview** is filled out.", + "- **Website Purpose** is filled out.", + "", + "Once the template has been completed, this check will run again automatically." + ].join("\n"); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body + }); + } - name: Remove incomplete label if: steps.validate.outcome == 'success' @@ -95,7 +124,7 @@ jobs: try { await github.rest.issues.removeLabel({ owner: context.repo.owner, - repo: context.repo.name, + repo: context.repo.repo, issue_number: context.issue.number, name: label }); @@ -103,40 +132,4 @@ jobs: if (error.status !== 404) { throw error; } - - tests: - name: Tests - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - - run: npm ci - - - name: Fetch PR information - if: github.event_name == 'pull_request' - run: | - echo "PR_AUTHOR=${{ github.event.pull_request.user.login }}" >> $GITHUB_ENV - echo "PR_AUTHOR_ID=${{ github.event.pull_request.user.id }}" >> $GITHUB_ENV - - LABELS=$(gh api --jq '[.labels[].name]' /repos/{owner}/{repo}/pulls/${{ github.event.number }}) - echo "PR_LABELS=$LABELS" >> $GITHUB_ENV - - FILES=$(gh api --jq '[.[] | select(.status != "removed") | .filename]' /repos/{owner}/{repo}/pulls/${{ github.event.number }}/files) - echo "CHANGED_FILES=$FILES" >> $GITHUB_ENV - - REMOVED_FILE_DATA=$(gh api --jq '[.[] | select(.status == "removed") | {name: .filename, data: .patch}]' /repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files) - echo "DELETED_FILES=$REMOVED_FILE_DATA" >> $GITHUB_ENV - env: - GH_TOKEN: ${{ github.token }} - - - uses: gacts/install-dnscontrol@890ba3249712c51203ccb7aba63307c0091e921b # v1.3.4 - if: github.event_name == 'pull_request' && contains(fromJson(env.CHANGED_FILES), 'dnsconfig.js') - with: - version: 4.14.0 - - - name: Run DNSControl checks - run: dnscontrol check - if: github.event_name == 'pull_request' && contains(fromJson(env.CHANGED_FILES), 'dnsconfig.js') - - - name: Run tests - run: npx ava tests/*.test.js --timeout=1m + } From 954e741333c11e11359850cf4aa9c0e4e8e63314 Mon Sep 17 00:00:00 2001 From: William Harrison <87287585+wdhdev@users.noreply.github.com> Date: Wed, 19 Aug 2026 11:56:27 +0800 Subject: [PATCH 5/5] Update pr-template.yml --- .github/workflows/pr-template.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-template.yml b/.github/workflows/pr-template.yml index 6bf51a6c08..d67c475249 100644 --- a/.github/workflows/pr-template.yml +++ b/.github/workflows/pr-template.yml @@ -49,8 +49,8 @@ jobs: owner: context.repo.owner, repo: context.repo.repo, name: label, - color: "d73a4a", - description: "The PR template has not been completely filled out." + color: "D93F0B", + description: "Your PR template is not complete. (e.g. missing the requirements form, missing website preview)" }); } else { throw error;