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
20 changes: 20 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,26 @@ jobs:
disable_search: true
fail_ci_if_error: true

# Annotate each email address a pull request adds to an existing register entry, so a
# reviewer checks it on the entity's own pages before users send requests to it.
register-emails:
if: github.event_name == 'pull_request'
timeout-minutes: 20
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Check out the base branch's register
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.base.sha }}
path: base
sparse-checkout: registry
persist-credentials: false
- run: rustup toolchain install
- run: cargo run --locked -p xtask -- email-changes base/registry/au registry/au

audit:
timeout-minutes: 20
runs-on: ubuntu-latest
Expand Down
4 changes: 3 additions & 1 deletion registry/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ A pack has `id` (equal to the file name), `name`, `description` and `entries`, a

Each `[[evidence]]` table records a source page's state when the entry was verified. A scheduled workflow (`.github/workflows/evidence-drift.yml`) re-fetches every evidence page on the first of each month and opens or updates one issue, 'Register evidence drift', listing pages that changed or could not be read within 30 seconds, and closes that issue once every page matches again. It never commits. When a page changes, recheck the entry against it and update the hash and `checked_at`.

The same run checks the contact channels of every entry that is not obsolete. It looks up each email address's domain through Google Public DNS (`dns.google`) and reports one that does not exist, has no mail or address record, or publishes a null MX record saying it accepts no mail. It also requests each web-form address and reports an HTTP error or a host public DNS says does not exist. A 401, 403, 405, 429 or 5xx, a dropped connection or a failed DNS lookup proves nothing, because sites also answer scripts that way when the form works and such failures pass, so those go to the workflow log and not the issue: a form or domain behind one of them is not confirmed to work. These checks never send an email or submit a form, and a passing domain does not prove the mailbox exists.
The same run checks the contact channels of every entry that is not obsolete. It looks up each email address's domain through Google Public DNS (`dns.google`) and reports one that does not exist, has no mail or address record, or publishes a null MX record saying it accepts no mail. It also requests each web-form address and reports an HTTP error or a host public DNS says does not exist. A 401, 403, 405, 429 or 5xx, a dropped connection or a failed DNS lookup proves nothing, because sites also answer scripts that way when the form works and such failures pass, so those go to the workflow log and not the issue: a form or domain behind one of them is not confirmed to work. It also reports an email address that appears on none of its entry's evidence pages, searching each page's raw HTML and decoded main text in any letter case; a missing address goes to the workflow log instead when its entry has no evidence page, or has one that failed or is not HTML. These checks never send an email or submit a form, and a passing domain does not prove the mailbox exists.

On a pull request, CI compares the register with the base branch's and adds a warning annotation for each email address an entry gains, and for every address of an entry whose id is new, so a renamed entry is flagged too (`cargo run -p xtask -- email-changes <base registry dir> registry/au`). Domains compare case-insensitively; local parts do not. The check passes either way, but fails on an entry that does not parse: check each flagged address on the entity's own pages before merging.

`cargo run -p xtask -- evidence-hash <url>` prints a page's hash, computed as follows:

Expand Down
29 changes: 29 additions & 0 deletions src/core/register.rs
Original file line number Diff line number Diff line change
Expand Up @@ -885,6 +885,7 @@ entries = ["example-au"]
"88000014675",
"88 000 014 67",
"8a 000 014 675",
"880 00 014 675",
] {
let report = check_one(|t| t.replace("88 000 014 675", abn));
assert_error(&report, "is malformed");
Expand Down Expand Up @@ -1042,6 +1043,7 @@ entries = ["example-au"]
"https://example.com:bad/path",
"https://-bad.example/path",
"https://example.com:+443/path",
"https://example.com:0/path",
&format!("https://{}.com/", "a".repeat(64)),
] {
let report = check_one(|t| t.replace("https://example.com.au/privacy", url));
Expand Down Expand Up @@ -1073,6 +1075,32 @@ entries = ["example-au"]
assert_error(&report, "unknown field `drivers_licence`");
let report = check_one(|t| t.replace("privacy@example.com.au", "privacy"));
assert_error(&report, "is not an email address");
let channel = |kind: &str, address: &str| {
format!("\n[[channels]]\nkind = \"{kind}\"\naddress = \"{address}\"\nlevers = []\n")
};
let report = check_one(|t| format!("{t}{}", channel("web-form", "http://x.com.au/")));
assert_error(&report, "web form `http://x.com.au/` is not an https URL");
let report = check_one(|t| format!("{t}{}", channel("post", " ")));
assert_error(&report, "a channel address must not be empty");
}

#[test]
fn rejects_an_empty_name_or_legal_entity() {
let report = check_one(|t| t.replace("name = \"Example\"", "name = \"\""));
assert_error(&report, "name and legal_entity must not be empty");
let report = check_one(|t| t.replace("\"Example Pty Ltd\"", "\" \""));
assert_error(&report, "name and legal_entity must not be empty");
}

#[test]
fn rejects_an_obsolete_entry_in_a_pack() {
let obsolete = format!("{VALID}\n[obsolete]\nreason = \"closed\"\n");
let report = check(
&[file("example-au.toml", &obsolete)],
&[file("marketing.toml", PACK)],
today(),
);
assert_error(&report, "pack lists obsolete entry `example-au`");
}

#[test]
Expand All @@ -1083,6 +1111,7 @@ entries = ["example-au"]
assert!(messages(&report, Severity::Warning)[0].contains("more than 12 months old"));
let report = check_one(|t| t.replace("2026-10-24", "2025-10-24"));
assert_eq!(report.findings, []);
assert_eq!(report.warnings(), 0);
}

#[test]
Expand Down
Loading
Loading