diff --git a/.github/workflows/validate-issue-title.yaml b/.github/workflows/validate-issue-title.yaml new file mode 100644 index 0000000..b021a37 --- /dev/null +++ b/.github/workflows/validate-issue-title.yaml @@ -0,0 +1,50 @@ +name: Validate issue title + +on: + issues: + types: [opened, edited] + workflow_dispatch: + +jobs: + validate-and-label: + runs-on: ubuntu-latest + name: Validate issue title + permissions: + issues: write + steps: + - uses: actions/checkout@v4 + - uses: jdx/mise-action@v2 + - name: Validate issue title + env: + NAME: ${{ github.event.issue.title }} + run: | + deno install + { + echo 'VALERR<&1 >/dev/null)" + echo EOF + } >> "$GITHUB_ENV" + - name: In case of an invalid issue title, comment about the error and label the issue + if: ${{ env.VALERR != '' && contains(github.event.issue.labels.*.name, 'checks/invalid-issue-title') != true }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + NUMBER: ${{ github.event.issue.number }} + VALERR: ${{ env.VALERR }} + run: | + gh issue comment "${NUMBER}" --body "$(cat < + gh issue edit "${NUMBER}" --remove-label 'checks/invalid-issue-title' diff --git a/deno.json b/deno.json index 8fa9936..96a7384 100644 --- a/deno.json +++ b/deno.json @@ -3,5 +3,8 @@ "build": "deno run -R -W ./src/build.js", "lint": "deno fmt --check ./src ./test", "tests": "deno test --allow-read" + }, + "imports": { + "tldts": "npm:tldts@^6.1.71" } } diff --git a/deno.lock b/deno.lock index 5232f93..f89d598 100644 --- a/deno.lock +++ b/deno.lock @@ -5,7 +5,9 @@ "jsr:@std/expect@*": "1.0.9", "jsr:@std/internal@^1.0.5": "1.0.5", "npm:@ghostery/adblocker@*": "2.3.1", - "npm:@types/node@*": "22.5.4" + "npm:@types/node@*": "22.5.4", + "npm:tldts@*": "6.1.71", + "npm:tldts@^6.1.71": "6.1.71" }, "jsr": { "@std/assert@1.0.9": { @@ -80,14 +82,28 @@ "tldts-core@6.1.67": { "integrity": "sha512-12K5O4m3uUW6YM5v45Z7wc6NTSmAYj4Tq3de7eXghZkp879IlfPJrUWeWFwu1FS94U5t2vwETgJ1asu8UGNKVQ==" }, + "tldts-core@6.1.71": { + "integrity": "sha512-LRbChn2YRpic1KxY+ldL1pGXN/oVvKfCVufwfVzEQdFYNo39uF7AJa/WXdo+gYO7PTvdfkCPCed6Hkvz/kR7jg==" + }, "tldts-experimental@6.1.67": { "integrity": "sha512-M5HZFMmtfxXcTQSTHu4Pn51CJdOK4hFw+y9KCj6bRRNaJRffNJIpTqSLixol+P+0v3NkXYxb1Mm90goJksCSrw==", "dependencies": [ - "tldts-core" + "tldts-core@6.1.67" + ] + }, + "tldts@6.1.71": { + "integrity": "sha512-LQIHmHnuzfZgZWAf2HzL83TIIrD8NhhI0DVxqo9/FdOd4ilec+NTNZOlDZf7EwrTNoutccbsHjvWHYXLAtvxjw==", + "dependencies": [ + "tldts-core@6.1.71" ] }, "undici-types@6.19.8": { "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" } + }, + "workspace": { + "dependencies": [ + "npm:tldts@^6.1.71" + ] } } diff --git a/src/validate-issue-title.ts b/src/validate-issue-title.ts new file mode 100644 index 0000000..944c363 --- /dev/null +++ b/src/validate-issue-title.ts @@ -0,0 +1,46 @@ +import * as process from "node:process"; +import { parse } from "npm:tldts"; + +// Convert decimal notation of ip address into 32 bit unsigned integer +function addr(decimalNotation: string): number { + return decimalNotation.split(".").reduce( + (state, part, index) => state | (parseInt(part, 10) << ((3 - index) * 8)), + 0, + ) >>> 0; +} + +const title = process.argv.slice(2)[0]; + +if (/[A-Z]/.test(title)) { + console.error("Given string includes an upper-case character!"); + process.exit(1); +} + +const parsed = parse(title); + +if (parsed.hostname === null) { + console.error("Given string is not a valid domain name or IP address!"); + process.exit(1); +} + +if (parsed.hostname !== title) { + console.error("Given string is not a pure hostname!"); + process.exit(1); +} + +if (parsed.isIp) { + const ip = addr(title); + + // https://datatracker.ietf.org/doc/html/rfc1918#section-3 + if ( + (ip >= addr("10.0.0.0") && ip <= addr("10.255.255.255")) || + (ip >= addr("172.16.0.0") && ip <= addr("172.31.255.255")) || + (ip >= addr("192.168.0.0") && ip <= addr("192.168.255.255")) + ) { + console.error("IP address is not in the public address space!"); + process.exit(1); + } +} else if (!parsed.isIcann) { + console.error("TLD is not registered to ICANN list!"); + process.exit(1); +}