Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions content_docs/tld-directory/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,20 @@ Record a `checked_at` per row and show it. A directory that does not say when it
6. What DNSSEC at the TLD level does and does not mean.
7. Sources and refresh cadence, stated plainly.

## Refresh

A homelab timer runs `scripts/domain-tools/tld-directory-monthly.mjs` on the 1st of each month at 04:30 UTC. It refreshes `tlds.json`, `rdap-bootstrap.json`, `rdap-overrides.json`, `whois-servers.json`, `whois-reviewed.json`, and `tld-directory.json` in a throwaway checkout of `main`. If a fact changed, it opens one pull request on `chore/tld-directory-refresh`. If only dates and version numbers moved, it does nothing. A failed run shows in the automation monitor.

The job doesn't add RDAP or WHOIS servers. A new ending that needs one still gets a manual review through `rdap-overrides.json` or `whois-reviewed.json`.

Install on homelab from the main checkout:

```bash
cp scripts/domain-tools/systemd/stack127-tld-directory-refresh.* ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now stack127-tld-directory-refresh.timer
```

## Placement

The canonical route is `/tld-directory`. It can appear in the domain collection and any future reference collection without changing its URL or ownership folder.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[Unit]
Description=Check the TLD directory against IANA and open a pull request if facts changed
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
WorkingDirectory=/srv/data/projects/stack127
Environment=PATH=/home/teja/.local/bin:/usr/local/bin:/usr/bin:/bin
Environment=NODE_NO_WARNINGS=1
ExecStart=/usr/bin/node scripts/domain-tools/tld-directory-monthly.mjs
TimeoutStartSec=1h
Nice=15
MemoryMax=1G
10 changes: 10 additions & 0 deletions scripts/domain-tools/systemd/stack127-tld-directory-refresh.timer
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[Unit]
Description=Monthly TLD directory check on homelab

[Timer]
OnCalendar=*-*-01 04:30:00 UTC
Persistent=true
Unit=stack127-tld-directory-refresh.service

[Install]
WantedBy=timers.target
113 changes: 113 additions & 0 deletions scripts/domain-tools/tld-directory-monthly.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#!/usr/bin/env node

/**
* Monthly check behind /tld-directory. Run by a homelab timer; see
* scripts/domain-tools/systemd/.
*
* Refreshes the datasets the directory reads in a throwaway checkout of main.
* If a real fact changed, it opens one pull request for review. If only dates
* and version numbers moved, it does nothing. Any failure exits non-zero, which
* the automation monitor shows.
*
* --dry-run stops before the commit. --base <ref> checks out another ref.
*/

import { execFileSync } from "node:child_process";
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";

const BRANCH = "chore/tld-directory-refresh";
const DATA_DIR = "lib/domain/data";
const DATA_FILES = ["tlds.json", "rdap-bootstrap.json", "rdap-overrides.json", "whois-servers.json", "whois-reviewed.json", "tld-directory.json"];
// These move on most runs without any fact changing.
const VOLATILE_KEYS = new Set(["fetchedAt", "version", "publication", "tldListVersion"]);

const args = process.argv.slice(2);
const dryRun = args.includes("--dry-run");
const baseIndex = args.indexOf("--base");
const base = baseIndex === -1 ? "origin/main" : args[baseIndex + 1];

const run = (command, commandArgs, cwd) =>
execFileSync(command, commandArgs, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "inherit"], maxBuffer: 1 << 28 });

const facts = (text) =>
JSON.stringify(Object.fromEntries(Object.entries(JSON.parse(text)).filter(([key]) => !VOLATILE_KEYS.has(key))));

function listChange(before, after) {
const was = new Set(JSON.parse(before).tlds);
const now = new Set(JSON.parse(after).tlds);
return { added: [...now].filter((tld) => !was.has(tld)), removed: [...was].filter((tld) => !now.has(tld)) };
}

function refresh(tree) {
const script = (name) => path.join("scripts/domain-tools", name);
const node = process.execPath;
run(node, ["--experimental-strip-types", script("refresh-datasets.mjs")], tree);
// That script also refreshes data the directory doesn't read. Leave it for its own release.
const others = run("git", ["diff", "--name-only", "--", DATA_DIR], tree)
.split("\n")
.filter((file) => file && !DATA_FILES.includes(path.basename(file)));
if (others.length) run("git", ["checkout", "--", ...others], tree);
run(node, ["--experimental-strip-types", script("refresh-tld-directory.mjs")], tree);
}

function changedFiles(tree) {
return DATA_FILES.filter((file) => {
const relative = path.posix.join(DATA_DIR, file);
const before = run("git", ["show", `HEAD:${relative}`], tree);
return facts(before) !== facts(readFileSync(path.join(tree, relative), "utf8"));
});
}

function pullRequestBody(tree, changed) {
const tlds = path.posix.join(DATA_DIR, "tlds.json");
const { added, removed } = listChange(run("git", ["show", `HEAD:${tlds}`], tree), readFileSync(path.join(tree, tlds), "utf8"));
const lines = ["The monthly TLD directory check found changes in IANA's data.", "", "Changed files:", ...changed.map((file) => `- \`${file}\``)];
if (added.length) lines.push("", `New endings: ${added.map((tld) => `\`.${tld}\``).join(", ")}. Check whether they need reviewed RDAP or WHOIS servers.`);
if (removed.length) lines.push("", `Removed endings: ${removed.map((tld) => `\`.${tld}\``).join(", ")}.`);
lines.push("", "Review the data diff and merge to update /tld-directory. Merging deploys.");
return lines.join("\n");
}

function publish(tree, changed) {
// Read the old list for the description before the commit moves HEAD.
const body = pullRequestBody(tree, changed);
const title = `Update TLD directory data (${new Date().toISOString().slice(0, 7)})`;
run("git", ["add", "--", ...DATA_FILES.map((file) => path.posix.join(DATA_DIR, file))], tree);
run("git", ["commit", "--quiet", "-m", title], tree);
// Stay detached: a local branch would outlive this checkout and block next month's run.
run("git", ["push", "--quiet", "--force", "origin", `HEAD:refs/heads/${BRANCH}`], tree);
const open = run("gh", ["pr", "list", "--head", BRANCH, "--state", "open", "--json", "number", "--jq", "length"], tree).trim();
if (open !== "0") {
run("gh", ["pr", "edit", BRANCH, "--title", title, "--body", body], tree);
console.log(`Updated the open pull request on ${BRANCH}.`);
return;
}
const url = run("gh", ["pr", "create", "--base", "main", "--head", BRANCH, "--title", title, "--body", body], tree).trim();
console.log(`Opened ${url}`);
}

const repo = run("git", ["rev-parse", "--show-toplevel"], process.cwd()).trim();
const parent = mkdtempSync(path.join(tmpdir(), "stack127-tld-"));
const tree = path.join(parent, "tree");

try {
if (baseIndex === -1) run("git", ["fetch", "--quiet", "origin", "main"], repo);
run("git", ["worktree", "add", "--quiet", "--detach", tree, base], repo);
refresh(tree);
const changed = changedFiles(tree);
if (!changed.length) {
console.log("No TLD directory facts changed. Nothing to do.");
} else if (dryRun) {
console.log(`Dry run. Would open a pull request:\n${pullRequestBody(tree, changed)}`);
} else {
publish(tree, changed);
}
} finally {
try {
run("git", ["worktree", "remove", "--force", tree], repo);
} finally {
rmSync(parent, { recursive: true, force: true });
}
}
3 changes: 2 additions & 1 deletion stack127_docs/monitoring.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
{"key": "bloxodes-vps/basebuddy-demo-reset", "group": "Basebuddy", "name": "Basebuddy demo reset", "about": "Every night, wipes and re-seeds the public Basebuddy demo so visitors start from clean sample data."},
{"key": "bloxodes-vps/collector", "group": "Basebuddy", "name": "Basebuddy GitHub traffic", "about": "Every 6 hours, saves GitHub traffic numbers for the Basebuddy repository, because GitHub only keeps 14 days."},
{"key": "bloxodes-vps/umami-selfhost-svdgnp-github-collector-1", "group": "Basebuddy", "name": "Umami GitHub collector", "about": "A container that sends GitHub traffic numbers to Umami analytics. It has been restarting in a loop."},
{"key": "homelab/stack127-shared-env", "group": "Developer setup", "name": "Share env files with worktrees", "about": "Every 30 seconds, keeps stack127's private env files linked into every worktree on the homelab."}
{"key": "homelab/stack127-shared-env", "group": "Developer setup", "name": "Share env files with worktrees", "about": "Every 30 seconds, keeps stack127's private env files linked into every worktree on the homelab."},
{"key": "homelab/stack127-tld-directory-refresh", "group": "Releases and checks", "name": "TLD directory check", "about": "On the 1st of each month at 04:30 UTC (10:00 IST), checks the TLD directory against IANA's data and opens a pull request only if a fact changed.", "impact": "New, removed, or changed domain endings stay out of /tld-directory until someone refreshes it by hand."}
]
}
Loading