Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
11 changes: 8 additions & 3 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@ on:
push:
branches:
- main
pull_request_target:
types: [labeled]
pull_request:

jobs:
build:
if: ${{ github.event_name != 'pull_request_target' || github.event.label.name == 'e2e:approve' }}
runs-on: blacksmith-2vcpu-ubuntu-2404
# The `e2e` environment holds CLOUDFLARE_API_KEY / CLOUDFLARE_ZONE_ID.
# Environment secrets (unlike repo secrets) are also passed to fork PR
# runs, which is what allows external contributions to run e2e. Fork runs
# are gated by the repo's "require approval for all outside collaborators"
# Actions setting — approving such a run hands the PR's code these
# secrets, so review the diff first.
environment: e2e
steps:
- uses: actions/checkout@v6
- uses: denoland/setup-deno@v2
Expand Down
2 changes: 1 addition & 1 deletion e2e/create-certificate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import {
AcmeOrder,
DnsUtils,
} from "../src/mod.ts";
import { resolveDns } from "../src/resolveDns.deno.ts";
import { expect, it } from "../test_deps.ts";
import { CloudflareZone } from "./utils/cloudflare.ts";
import { resolveDns } from "./utils/resolveDns.ts";
import { expectToBeDefined } from "./utils/expectToBeDefined.ts";
import { randomFishballTestingSubdomain } from "./utils/randomFishballTestingSubdomain.ts";

Expand Down
60 changes: 60 additions & 0 deletions e2e/utils/resolveDns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { ResolveDnsFunction } from "../../src/DnsUtils/resolveDns.ts";
import { createResolveDns } from "../../src/resolveDns.doh.ts";
import { PUBLIC_DNS } from "../../src/resolveDns.nameServers.ts";

// CI runners (currently Blacksmith) sit behind their own DNS infrastructure,
// where freshly created TXT records can stay invisible to the system resolver
// for longer than our polling timeout. Resolving over DoH (plain HTTPS)
// bypasses the runner's DNS path entirely, so propagation polling behaves the
// same on any runner.
const dohResolveDns = createResolveDns({
endpoint: PUBLIC_DNS.cloudflare.doh[0],
});

/**
* Flushes a record from 1.1.1.1's cache via the endpoint behind
* https://one.one.one.one/purge-cache/.
*
* Unofficial and undocumented — Cloudflare may gate or change it without
* notice. If e2e starts timing out on DNS polling with purge warnings in the
* log, this endpoint is the first thing to check.
*/
const purgeCloudflareDnsCache = async (
domain: string,
recordType: string,
): Promise<void> => {
const url = new URL("https://one.one.one.one/api/v1/purge");
url.searchParams.set("domain", domain.replace(/\.$/, ""));
url.searchParams.set("type", recordType);

const res = await fetch(url, {
method: "POST",
// This endpoint is unofficial, so don't let a stalled request block the
// polling loop indefinitely.
signal: AbortSignal.timeout(10_000),
});
await res.text(); // consume the body to avoid leaking the connection

if (!res.ok) {
throw new Error(`unexpected response: ${res.status} ${res.statusText}`);
}
};

export const resolveDns: ResolveDnsFunction = async (domain, recordType) => {
// Purge 1.1.1.1's cache before every lookup so polling keeps re-querying
// the authoritative servers: a lookup that races record propagation would
// otherwise cache NXDOMAIN for the zone's full negative TTL, which outlives
// the polling budget (observed in e2e runs 78 and 86). The purge applies
// asynchronously on Cloudflare's side, so a lookup may still see the cache
// as it was one attempt ago — polling converges an attempt later.
try {
console.log(`🧹 Purging 1.1.1.1 cache for ${domain} (${recordType})...`);
await purgeCloudflareDnsCache(domain, recordType);
} catch (error) {
console.warn(
`⚠️ Failed to purge 1.1.1.1 cache for ${domain} (${recordType}): ${error}`,
);
}

return await dohResolveDns(domain, recordType);
};
2 changes: 1 addition & 1 deletion e2e/wildcard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import {
AcmeOrder,
DnsUtils,
} from "../src/mod.ts";
import { resolveDns } from "../src/resolveDns.deno.ts";
import { expect, it } from "../test_deps.ts";
import { CloudflareZone } from "./utils/cloudflare.ts";
import { expectToBeDefined } from "./utils/expectToBeDefined.ts";
import { resolveDns } from "./utils/resolveDns.ts";
import { randomFishballTestingSubdomain } from "./utils/randomFishballTestingSubdomain.ts";

const EMAIL = "e2e-wildcard@test.acme.pkg.fishball.dev";
Expand Down
2 changes: 1 addition & 1 deletion e2e/workflows.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import {
AcmeOrder,
AcmeWorkflows,
} from "@fishballpkg/acme";
import { resolveDns } from "@fishballpkg/acme/resolveDns.deno";
import { describe, expect, it } from "../test_deps.ts";
import { CloudflareZone } from "./utils/cloudflare.ts";
import { resolveDns } from "./utils/resolveDns.ts";
import { randomFishballTestingSubdomain } from "./utils/randomFishballTestingSubdomain.ts";

const EMAIL = "e2e@test.acme.pkg.fishball.dev";
Expand Down
Loading