validate-domain-alive.ts 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. import fs from 'node:fs';
  2. import process from 'node:process';
  3. import { getMethods } from './lib/is-domain-alive';
  4. import { enumerateSourceDomains } from './lib/enumerate-source-domains';
  5. import { getShardConfigFromEnv, isInShard } from './lib/shard';
  6. import { getRunnerGeoIP } from './lib/get-runner-geoip';
  7. import type { RunnerGeoIP } from './lib/get-runner-geoip';
  8. import cliProgress from 'cli-progress';
  9. import { newQueue } from '@henrygd/queue';
  10. const queue = newQueue(32);
  11. const deadDomains: string[] = [];
  12. /**
  13. * Append this shard's result to the GitHub Actions job summary, if running in
  14. * CI. Each shard writes its own summary (no dedicated merge job) — the union
  15. * of all shards' summaries is the full dead-domain list.
  16. */
  17. function formatGeo(geo: RunnerGeoIP | null): string {
  18. if (!geo) return 'unknown (geoip lookup failed)';
  19. return `${geo.ip} — ${geo.city}, ${geo.region}, ${geo.country} (AS${geo.asn} ${geo.asOrg})`;
  20. }
  21. function writeJobSummary(shardLabel: string, dead: string[], geo: RunnerGeoIP | null) {
  22. const summaryPath = process.env.GITHUB_STEP_SUMMARY;
  23. if (!summaryPath) return;
  24. let summary = `## Dead domains — shard ${shardLabel}\n\n`
  25. + `Runner egress: \`${formatGeo(geo)}\`\n\n`
  26. + `Found **${dead.length}** dead domain(s) in this shard.\n\n`;
  27. if (dead.length > 0) {
  28. summary += '<details><summary>Show list</summary>\n\n'
  29. + '```\n'
  30. + dead.join('\n') + '\n'
  31. + '```\n\n'
  32. + '</details>\n\n'
  33. // Machine-recoverable copy so the union can be scraped from summaries.
  34. + '```json\n'
  35. + JSON.stringify(dead) + '\n'
  36. + '```\n\n';
  37. }
  38. fs.appendFileSync(summaryPath, summary);
  39. }
  40. (async () => {
  41. const shard = getShardConfigFromEnv();
  42. const shardLabel = `${shard.index + 1}/${shard.total}`;
  43. const [
  44. { isDomainAlive, isRegisterableDomainAlive },
  45. allDomains,
  46. geo
  47. ] = await Promise.all([
  48. getMethods(),
  49. enumerateSourceDomains(),
  50. getRunnerGeoIP()
  51. ]);
  52. console.log(`[shard ${shardLabel}] runner egress: ${formatGeo(geo)}`);
  53. const shardDomains = allDomains.filter(({ domain }) => isInShard(domain, shard));
  54. console.log(
  55. `[shard ${shardLabel}] checking ${shardDomains.length} of ${allDomains.length} domain(s)`
  56. );
  57. const bar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic);
  58. bar.start(shardDomains.length, 0);
  59. for (const { domain, includeAllSubdomain } of shardDomains) {
  60. queue.add(async () => {
  61. let registerableDomainAlive, registerableDomain, alive: boolean | undefined;
  62. if (includeAllSubdomain) {
  63. // we only need to check apex domain, because we don't know if there is any stripped subdomain
  64. ({ alive: registerableDomainAlive, registerableDomain } = await isRegisterableDomainAlive(domain));
  65. } else {
  66. ({ alive, registerableDomainAlive, registerableDomain } = await isDomainAlive(domain));
  67. }
  68. bar.increment();
  69. if (!registerableDomainAlive) {
  70. if (registerableDomain) {
  71. deadDomains.push('.' + registerableDomain);
  72. }
  73. } else if (!includeAllSubdomain && alive != null && !alive) {
  74. deadDomains.push(domain);
  75. }
  76. });
  77. }
  78. await queue.done();
  79. bar.stop();
  80. console.log();
  81. console.log();
  82. console.log(`[shard ${shardLabel}]`, JSON.stringify(deadDomains));
  83. writeJobSummary(shardLabel, deadDomains, geo);
  84. })();