Skip to content

feat: reconcile Forward Email domain settings and aliases from git - #1

Merged
srgvg merged 7 commits into
mainfrom
feat/forward-email
Aug 30, 2026
Merged

feat: reconcile Forward Email domain settings and aliases from git#1
srgvg merged 7 commits into
mainfrom
feat/forward-email

Conversation

@srgvg

@srgvg srgvg commented Aug 30, 2026

Copy link
Copy Markdown
Member

Opt-in GitOps for the Forward Email account side of a dns-zones repo, shaped like delegation: / octodns-gitops-delegate.

  • forward_email: block in config.yaml claims a domain list (the ownership and prune boundary); one mail/forward-email/<domain>.yaml per claimed domain with settings: (the 13 API-writable fields only), expect: (read-only fields, drift-reported) and aliases:. Package defaults are the only layer shared across repos.
  • octodns-gitops-forwardemail: dry-run by default, --doit writes, --domain scopes. Never creates or deletes domains. --prune is off by default, scoped to claimed domains, re-lists before deleting, and refuses to delete any alias with has_imap or stored mail. --export bootstraps/re-baselines the files; --drift checks FE's generated DNS records (DKIM, fe-bounces, verification TXT, DMARC rua, MX) against the zone file.
  • Makefile template: mail-plan, mail-apply, mail-drift, mail-export (DOMAIN=, PRUNE=1).
  • README section documents the contract.

Tests: 181 new (tests/forward_email/, tests/cli/test_forward_email.py, Makefile template), full suite 448 passing (base main: 267), no network in tests (transport injected; an autouse fixture fails any call that reaches the real one).

Follow-up, not in this PR: the CalVer tag, then the per-repo rollout (autops first, ginsys second) with a zero-diff first plan as the acceptance test.

srgvg added 2 commits August 30, 2026 16:24
New opt-in feature, shaped like `delegation:`/`octodns-gitops-delegate`:
a `forward_email:` block in config.yaml claims a domain list, and one
`mail/forward-email/<domain>.yaml` per claimed domain holds the desired
settings, read-only expectations and aliases. Package defaults are the
only layer shared across repos (each repo's CI sees only itself).

`octodns-gitops-forwardemail`:
- dry-run by default, `--doit` writes; `--domain` scopes
- never creates or deletes domains; a claimed domain missing from the
  account is an error
- `--prune` (off by default) deletes aliases absent from git inside
  claimed domains only, after a second listing agrees with the first;
  an alias with `has_imap` or stored mail blocks the run instead
- `--export` writes the files from live state (defaults omitted, regex
  alias names single-quoted); a fresh export must plan as zero changes
- `--drift` checks FE's generated DKIM/return-path/verification/DMARC
  `rua`/MX records against the repo's zone file plus the read-only
  expectations; `has_strict_dmarc` is derived from the zone's own `p=`

Only the 13 fields Forward Email's update controller reads are
declarable as settings; `max_quota_per_alias`/`bounce_webhook` are
write-only and ride along with real changes. Alias `max_quota` is
compared in bytes (bytes on GET, human string on PUT, blank = reset).

Makefile template gains mail-plan, mail-apply, mail-drift, mail-export
(DOMAIN=, PRUNE=1). 81 new tests; suite 348 passing.
…+ live dry run

Codex review of #1 (6 findings, all confirmed) plus a
read-only run against the real account (24 domains: export -> plan is a
zero diff on every domain; drift clean on 23, traceback on 1):

- prune relist: besides the id set, every planned delete must keep its
  name and stay a non-mailbox on the second listing
- alias create/update bodies carry every resolved flag explicitly, so a
  repo default such as `is_enabled: false` reaches the API; per-alias-only
  fields (max_quota, public_key, vacation_responder) are rejected as
  repo defaults instead of silently ignored
- scalar type validation for settings, expectations and alias fields
  (strict bool; `"false"` or `retention_days: forever` never reach a PUT);
  `domains:` entries and recipients/labels must be strings
- export: digit-leading strings (dates, "25", 1e3) are quoted; strings
  with newlines are emitted as JSON so YAML reads them back unchanged
- drift: a `value:` holding a list (vanginderachter.name apex TXT) no
  longer raises; a missing smtp_dns_records key is reported as
  unverifiable rather than clean
- CLI: any exception while listing domains or handling one domain is
  reported with rc 1 instead of a traceback; the run continues

Suite 368 passing.
@srgvg
srgvg marked this pull request as ready for review August 30, 2026 14:45
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 30, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-08-30T16:36:45.182648Z eeaf3d7 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 552754841c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/octodns_gitops/cli/forward_email.py Outdated
Comment thread src/octodns_gitops/forward_email/reconcile.py Outdated
Comment thread src/octodns_gitops/forward_email/export.py Outdated
Comment thread src/octodns_gitops/cli/forward_email.py
Comment thread src/octodns_gitops/forward_email/config.py Outdated
Comment thread src/octodns_gitops/forward_email/drift.py Outdated
Comment thread src/octodns_gitops/cli/forward_email.py Outdated
Comment thread src/octodns_gitops/forward_email/config.py Outdated
Comment thread src/octodns_gitops/forward_email/drift.py Outdated
Comment thread src/octodns_gitops/forward_email/export.py Outdated
- `--doit` and `--dry-run` are mutually exclusive (argparse rejects both, exit 2)
- `--export` over an existing file preserves its write-only `bounce_webhook` /
  `max_quota_per_alias`; the API cannot return them and the next domain PUT would
  otherwise send the repo default. Alias quotas are exported against that
  preserved per-domain default, or an omitted alias quota reloads as a reset
- apply order: settings PUT, prune re-list, creates/updates, deletes — a create
  changed the id set, so a create+delete plan always aborted half-applied
- alias create sends a non-empty resolved description (a repo default was dropped)
- export writes `description: ''` when the live value is blank under a non-empty
  repo default, so the export still plans as zero diff
- `labels` is server-managed: rejected in alias entries and `defaults.alias`
  with a dedicated error instead of being accepted and never reconciled
- drift: a missing/empty `verification_record` is an "unverifiable" finding,
  mirroring the `smtp_dns_records` handling
- `forward_email: {}` / bare key / list is a config error, not an opt-out; the
  same for a non-mapping `settings:` / `expect:` / `defaults*:` section, which
  used to collapse to "nothing declared" and enforce the package defaults
- unreadable or malformed YAML and a non-string `directory` surface as config
  errors (rc 2 / per-domain rc 1) instead of tracebacks
- DMARC `rua` compared as complete URIs (case-insensitive mailto, `!size`
  stripped) instead of substring
- export emits every key of an enabled vacation_responder, nested values as
  JSON flow style; known keys first, then sorted
@srgvg

srgvg commented Aug 30, 2026

Copy link
Copy Markdown
Member Author

/gmr round 1 — reviewed 5527548, pushed d473257

CI at d473257: green — runs 33319189862 and 33319190902, test (3.10–3.14) 10/10 pass. Local verify: python -m pytest tests/ -q393 passed (368 → 393, 25 regression tests added, each written first and watched fail).

This loop pushed without the pre-commit human review pause; review happens on this evidenced PR.

Disposition — 13 of 13 applied (10 review threads + 3 from a local Codex lens on the fix)

# Thread Claim Class Status Fix at d473257
1 P1 cli/forward_email.py:229 --doit --dry-run ignores dry-run blocking fixed --doit/--dry-run are an argparse mutually exclusive group; both → exit 2 before any config is read
2 P1 cli/forward_email.py:152 export drops per-domain write-only settings blocking fixed --export reads the existing <domain>.yaml, carries bounce_webhook/max_quota_per_alias into the new file and prints what it preserved; alias quotas are exported against that preserved per-domain default so the export still plans as zero diff
3 P2 cli/forward_email.py:79 creates run before the prune re-list blocking fixed apply order: settings PUT → prune re-list/validation → creates/updates → deletes; test uses a create+delete plan against a fake whose create_alias changes the id set
4 P2 reconcile.py:121 repo-default description not sent on create blocking fixed _alias_body sends description whenever the resolved value is non-empty
5 P2 export.py:64 blank live description dropped under non-empty default blocking fixed export writes description: '' when the normalized live value differs from the resolved default; round-trip test for "", null, absent
6 P2 config.py:345 labels accepted but never reconciled blocking fixed labels is server-managed: rejected in alias entries and defaults.alias with a dedicated error, removed from the accepted field sets and DesiredAlias
7 P2 drift.py:82 missing verification_record skips the check blocking fixed missing/empty token is a verification "cannot verify" finding (rc 1), like smtp_dns_records
8 P2 config.py:247 forward_email: {} treated as absent blocking fixed only an absent key means "not configured"; {}, null or a non-mapping is a config error (rc 2)
9 P2 drift.py:92 DMARC rua substring match blocking fixed rua split on ,, compared as whole URIs (mailto lowercased, !size suffix stripped)
10 P2 export.py:77 vacation_responder export hard-codes 3 keys blocking fixed every key of an enabled vacation_responder is exported (known keys first, rest sorted)
C1 local Codex lens fix 2 incomplete: alias quotas compared to the repo default, not the preserved per-domain one → reload planned a max_quota reset blocking fixed export.py uses the preserved max_quota_per_alias as the alias quota baseline
C2 local Codex lens settings: [], defaults.alias: [], false collapsed to {} (same class as 8) blocking fixed non-mapping settings:/expect:/defaults*: sections and a non-string directory are config errors
C3 local Codex lens malformed YAML / unreadable config escaped main() as a traceback blocking fixed wrapped into ForwardEmailConfigError → rc 2

0 rejected, 0 deferred. Round 2 (full-scope confirm) follows.

@srgvg

srgvg commented Aug 30, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d4732574e0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/octodns_gitops/forward_email/config.py
Comment thread src/octodns_gitops/forward_email/config.py
Comment thread src/octodns_gitops/cli/forward_email.py Outdated
Comment thread src/octodns_gitops/forward_email/drift.py Outdated
Comment thread src/octodns_gitops/forward_email/config.py Outdated
…d its defaults

A misspelled `defualts:` (or `defaults: {setings: ...}`) was silently ignored, so the
package defaults were enforced instead of the operator's override — e.g. a declared
`retention_days: 0` became a PUT of 30. The per-domain file already rejected unknown
top-level keys; the config block and its `defaults:` sub-block now do the same, through
the existing `_check_keys`.

Also documents the CLI exit codes in the module docstring (0 clean, 1 any per-domain
error or finding, 2 an invalid invocation); the split was only implicit in the tests.

Round-2 Codex lens on #1, full-scope confirm: one blocking finding fixed (this), one
rejected (per-domain config errors keep rc 1 — nothing documents rc 2 for them and the
suite asserts 1).
@srgvg

srgvg commented Aug 30, 2026

Copy link
Copy Markdown
Member Author

/gmr round 2 — full-scope confirm at d473257, pushed f76b8ee

CI at f76b8ee: green — runs 33319772290 and 33319772695, test (3.10–3.14) 10/10 pass. Local verify: python -m pytest tests/ -q396 passed (393 → 396).

Disposition — local Codex full-scope lens at d473257: 2 of 3 applied, 1 rejected

# Claim Class Status Detail
R2-1 unknown keys in the forward_email: block or under defaults: were silently ignored (a defualts: typo enforced package defaults over declared values) blocking fixed @ f76b8ee config.py BLOCK_FIELDS/DEFAULTS_FIELDS key checks, 3 tests
R2-2 per-domain config errors return rc 1 "instead of the contracted rc 2" blocking as claimed rejected no such contract: README.md documents "exit 1 on any finding"; rc 2 is reserved for an invalid invocation before any domain runs; per-domain errors aggregate to 1 because the loop continues (tests/cli/test_forward_email.py asserts this deliberately). The "0/1/2" phrasing Codex quoted was the loop prompt's own paraphrase.
W1 exit-code contract undocumented in the module wording fixed @ f76b8ee cli/forward_email.py module docstring

Fold-in audit of the 13 round-1 fixes: 12 complete; C2's sibling gap (unknown keys, not just non-mapping sections) is R2-1.

New review threads at d473257 (the @codex review re-run, 17:24)

# Claim Class Status
G2 config.py:304 unknown keys under defaults: ignored blocking already fixed @ f76b8ee (R2-1) — resolving
G1 config.py:111 nested vacation_responder.is_enabled: "false" is truthy blocking confirmed → round 3
G3 cli/forward_email.py:46 zone_directory takes the first YamlProvider globally blocking (latent — both consuming repos have exactly one) confirmed → round 3
G4 drift.py:129 MX check ignores preference and competing exchangers blocking confirmed → round 3
G5 config.py:351 missing/null aliases:[]--prune deletes every alias blocking (P1) confirmed → round 3

Lite cap (2 rounds) reached with 4 open blocking threads → the loop escalated to full mode (cap 3). Round 3 fixes G1/G3/G4/G5 and runs a targeted confirm on the diff since d473257.

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create an environment for this repo.

…ct MX, explicit aliases

Four review threads on the round-1 push (GitHub Codex review at d473257):

- vacation_responder was only checked as a mapping, so `is_enabled: "false"` (a truthy
  string) enabled the responder on the API. The nested mapping now admits exactly
  Forward Email's three fields (is_enabled required and strictly bool, subject, message);
  export writes only those keys and the reconciler compares only those keys, so the three
  stay consistent and an extra live key is neither written nor a perpetual diff.
- --drift resolved the zone directory as "the first YamlProvider" globally; a domain sourced
  from a second one was checked in the wrong directory and reported "no zone file; not
  checked" with rc 0. ZoneLookup resolves per domain from zones.<domain>.sources (or "*"),
  falls back to the only YamlProvider, and with several and no naming entry reports an
  explicit "ambiguous" finding (rc 1) instead of silence.
- The MX check only required the two FE hosts to appear. It now requires both at exactly
  one shared integer preference — every entry's preference is kept, so a duplicate host at
  another preference, a missing preference or a malformed entry is reported — and no other exchanger at all
  (lower preference routes mail away, higher is a backup FE does not know about);
  ignore_mx_check remains the opt-out for split routing. Legacy octoDNS `value`/`priority`
  spellings are accepted.
- A missing or null `aliases:` loaded as [], so --prune would have deleted every
  non-mailbox alias of a domain whose file merely forgot the key. It is now an error that
  names `aliases: []` as the explicit form; export already writes that. Same class, same
  fix: an alias without `recipients:` no longer reads as [] (which would PUT no
  recipients), and `domains:` null in the block is covered by the existing non-empty check.

Verify: 423 passed (396 before; 27 tests added, 1 renamed).
@srgvg

srgvg commented Aug 30, 2026

Copy link
Copy Markdown
Member Author

@codex review

@srgvg

srgvg commented Aug 30, 2026

Copy link
Copy Markdown
Member Author

/gmr round 3 — targeted confirm on the diff since d473257, pushed ec06620

CI at ec06620: green — runs 33320795022 and 33320796339, test (3.10–3.14) 10/10 pass. Local verify: python -m pytest tests/ -q423 passed (396 → 423, 27 regression tests added, each watched failing first).

Disposition — 7 of 7 applied (5 review threads at d473257 + 2 from the local Codex confirm passes)

# Thread Claim Class Status Fix at ec06620
G1 P2 config.py:111 vacation_responder.is_enabled: "false" is truthy blocking fixed strict schema: exactly is_enabled (required bool), subject, message; export and reconcile use the same three keys — this supersedes round 1's "export every key" (thread 10), as that thread's own alternative suggested
G2 P2 config.py:304 unknown keys under defaults: ignored blocking fixed @ f76b8ee BLOCK_FIELDS/DEFAULTS_FIELDS key checks; test covers defualts:, defaults.setings:, tokn:
G3 P2 cli/forward_email.py:46 first YamlProvider used for every domain blocking (latent) fixed ZoneLookup resolves per domain from zones.<domain>.sources (or '*'), falls back to the only YamlProvider; several providers with no naming entry → an "ambiguous" drift finding (rc 1), never "not checked"
G4 P2 drift.py:129 MX check ignores preference and competing exchangers blocking fixed unless ignore_mx_check: apex MX must be exactly mx1/mx2.forwardemail.net at one shared integer preference; any other exchanger, duplicate FE entry at another preference, missing preference or malformed entry is a finding (8-case test); all 24 live zones stay clean
G5 P1 config.py:351 absent/null aliases:[]--prune deletes every alias blocking fixed absent/null aliases: is a config error naming aliases: [] (which export already writes; round trip tested). Same class: an alias without recipients: is rejected instead of reading as []
R3-1 local Codex lens G4 fix: both FE hosts without a preference read clean; duplicate host overwritten blocking fixed per-host preference sets; the FE union must be one int
R3-2 local Codex lens G4 fix: non-dict / host-less MX entries skipped → FE pair + malformed extra read clean blocking fixed malformed entries collected and reported

Fold-in audit at ec06620 (all 26 adjudicated findings across three rounds): 26/26 complete. Local Codex confirm pass at ec06620: 0 findings. Diff vs main: 18 files, +3146/−3 (+12.5% this round, under the 20% stop).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ec06620126

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/octodns_gitops/cli/forward_email.py Outdated
Comment thread src/octodns_gitops/forward_email/drift.py Outdated
Comment thread src/octodns_gitops/forward_email/config.py Outdated
Comment thread src/octodns_gitops/forward_email/config.py
Comment thread src/octodns_gitops/bootstrap/makefile.py Outdated
…ct duplicate YAML keys, merge zone sources

- reconcile: refuse an update that would turn IMAP off on a live mailbox only
  because the file omits `has_imap` (an explicit `has_imap: false` is intent);
  refuse an alias with no recipients and no mailbox; the prune error now says
  "add it to git with `has_imap: true`"
- config: reject duplicate YAML mapping keys (PyYAML kept the last block
  silently; with --prune that deleted the first block's aliases); used for
  config.yaml, the per-domain files, the export-preserve read and zone files
- drift: read and merge every YamlProvider a zone's `sources:` names; compare
  the return-path CNAME and record names case-insensitively
- cli: `--prune` with `--export`/`--drift` is a usage error (rc 2); the exit
  code docstring scopes "a finding" to --drift
- makefile: PRUNE=1 means the literal 1 (`$(filter 1,$(PRUNE))`); 0/false no
  longer prune
- README: the mailbox remedy names `has_imap: true`; mail-drift proves nothing
  for a domain without a zone file; `recipients: []` only for a mailbox;
  duplicate keys are an error

Addresses the code-reviewer termination pass (CR1, CR3-CR5) and the GitHub
Codex threads H1-H5 posted at ec06620.
@srgvg

srgvg commented Aug 30, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2072407243

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/octodns_gitops/cli/forward_email.py
Comment thread src/octodns_gitops/cli/forward_email.py
Comment thread src/octodns_gitops/forward_email/config.py
@srgvg

srgvg commented Aug 30, 2026

Copy link
Copy Markdown
Member Author

/gmr round 3 (second targeted re-confirm) — pushed 2072407

CI at 2072407: green — runs 33321954879 and 33321956602, test (3.10–3.14) 10/10 pass. Local verify: python -m pytest tests/ -q441 passed (423 → 441, 18 tests added). PR body test counts corrected (174 new, 441 total, base 267).

Disposition — 10 of 10 applied (5 review threads at ec06620 + 5 from the full-mode termination lens)

# Source Claim Class Status Fix at 2072407
H3 P1 config.py:218 duplicate YAML mapping keys silently kept the last block → --prune deletes the aliases from the discarded one blocking fixed strict SafeLoader subclass rejects duplicate keys ("duplicate key 'aliases' at line N (first at line M)"); used for config.yaml, per-domain files, the export-preserve read and zone-file reads
H5 P1 makefile.py:158 PRUNE=0 / PRUNE=false still passed --prune blocking fixed $(if $(filter 1,$(PRUNE)),--prune,) on mail-plan and mail-apply; only the literal PRUNE=1 prunes
H1 P2 cli/forward_email.py:67 a zone sourced from several YamlProviders was checked against the first only blocking fixed ZoneLookup.directories() returns every named YamlProvider; drift loads each existing <domain>.yaml and merges same-name entries
H2 P2 drift.py:97 return-path CNAME compared case-sensitively blocking fixed lowercased after rstrip("."); record-name lookups case-insensitive
H4 P2 config.py:349 recipients: [] with has_imap: false passed validation → alias with no delivery target blocking fixed plan error after defaults resolve ("give it recipients or has_imap: true"); the account's only zero-recipient alias is a mailbox, export→plan stays zero-diff
CR1 termination lens a live mailbox declared in git without has_imap planned a has_imap: false PUT — the prune guard's own "add it to git" remedy led into it blocking fixed update-path guard: live has_imap: true + undeclared has_imap in the file → plan error naming has_imap: true; an explicit has_imap: false is intent and still applies. Prune remedy text and README updated
CR3 termination lens README implied mail-drift covers a domain with no zone file wording fixed README states drift proves nothing about such a domain
CR4 termination lens --prune silently ignored with --export/--drift wording fixed parser.error on the combination
CR5 termination lens exit-code docstring ambiguous between plan and drift wording fixed docstring scoped: findings → rc 1 in drift only; plan/apply rc 1 only on errors/aborts
CR2 termination lens PR body test counts stale wording fixed PR body edited

Local Codex targeted re-confirm at 2072407: 0 findings; fold-in audit of all 33 prior findings: complete. Diff vs main: 18 files, +3426/−3 (+8.9% this pass).

…rs in drift, reject non-string keys

- cli: `--dry-run --export` is a usage error (rc 2) — export always writes
  the domain files, so a "dry run" of it overwrote every selected file;
  `--dry-run --drift` stays allowed (drift writes nothing)
- cli: the drift branch now reports every `plan.errors` entry as an `alias`
  finding (rc 1) instead of reading "clean" for a file plan/apply refuses
- config: `_check_keys` rejects non-string mapping keys with a clear
  message; an unquoted `on:` loads as True and made the unknown-key join
  raise TypeError (traceback instead of rc 2/1)

Addresses GitHub Codex threads I1-I3 posted at 2072407.
@srgvg

srgvg commented Aug 30, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: eeaf3d7703

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@srgvg

srgvg commented Aug 30, 2026

Copy link
Copy Markdown
Member Author

/gmr round 3 (third targeted re-confirm) — pushed eeaf3d7

CI at eeaf3d7: green — runs 33322781330 and 33322783740, test (3.10–3.14) 10/10 pass. Local verify: python -m pytest tests/ -q448 passed (441 → 448, 7 tests added). PR body counts updated (181 new, 448 total).

Disposition — 4 of 4 applied (3 review threads at 2072407 + 1 wording from the local confirm pass)

# Source Claim Class Status Fix at eeaf3d7
I1 P2 cli/forward_email.py:287 --dry-run --export still wrote every selected domain file blocking fixed usage error (rc 2, before config/network) — export always writes, there is no preview of it; --dry-run --drift stays allowed
I2 P2 cli/forward_email.py:236 drift discarded plan.errors → a file plan/apply rejects read clean, rc 0 blocking fixed every plan.errors entry is an alias: drift finding (rc 1); guards needing live alias state (mailbox update, prune) remain plan/apply-only and the docstring says so
I3 P3 config.py:270 non-string YAML key (on:True) crashed _check_keys with a TypeError traceback blocking (crash instead of a config error) fixed non-string keys rejected first with a ForwardEmailConfigError naming the key and suggesting quoting
W local Codex lens docstring overstated which guards drift surfaces wording fixed docstring scoped

Local Codex targeted re-confirm at eeaf3d7: 0 blocking. Diff vs main: 18 files, +3481/−3.

@srgvg

srgvg commented Aug 30, 2026

Copy link
Copy Markdown
Member Author

/gmr — MR-READY: review converged at eeaf3d7

Evidence, not conclusion:

  • Converged OID: eeaf3d770372da8fcff07eb4dd53846ddb66b9bf (7 commits on feat/forward-email over main 81a6537: ee59526 feature, 5527548, d473257, f76b8ee, ec06620, 2072407, eeaf3d7 fixes).
  • CI at eeaf3d7: green — runs 33322781330 (push) and 33322783740 (pull_request), test (3.10–3.14) 10/10 pass. No flake, no retry, at any of the 5 pushed OIDs.
  • Verify: python -m pytest tests/ -q (the same command CI's make test runs) → 448 passed at eeaf3d7 (base main: 267; 181 new, every one written before its fix and watched failing). This repo has no gate-registry row and no linter in CI; ruff on the touched files reports only the pre-existing EXE001/I001 findings present on main.
  • Review lenses: GitHub Codex (chatgpt-codex-connector) at 5527548, d473257, ec06620, 2072407 and eeaf3d7 (clean: "Didn't find any major issues", comment 5469924731); local read-only Codex lens (gpt-5.6-sol) 7 passes — full-scope at d3fef40/d473257, targeted confirms at 4de082c, b7cd5ea, ec06620, 2072407, fd3d482; full-mode termination lens (claude:code-reviewer, forge-network) at ec06620.
  • Findings, all rounds: 37 — 31 blocking, 0 citation, 6 wording. 36 fixed / 1 rejected / 0 deferred. The rejection: "per-domain config errors should exit 2" — rc 1 aggregation is the documented design; tests/cli/test_forward_email.py asserts it. Every one of the 23 review threads on this PR has a fix commit named in an iteration comment above and is resolved.
  • Safety contract, traced by the termination lens: no write to Forward Email without --doit (the four mutating client calls sit only inside _apply, reachable only from plan mode with --doit); a prune never deletes a has_imap/stored-mail alias (guard + pre-write re-list); an update never clears has_imap on a live mailbox unless the file says has_imap: false explicitly; domains are never created or deleted; only the literal PRUNE=1 prunes from the Makefile.
  • Iterations: 3 rounds (lite rounds 1–2, escalated to full mode at round 2's cap when 5 blocking threads landed post-round; round 3 with three targeted re-confirm passes), 1 invocation, 0 human interventions in the loop. Run log: 6 rows.
  • Deviation, stated: this loop pushed each round without a pre-commit human review pause; review happened on this PR.

The PR was already marked ready by the author; nothing to undraft. Not merged — merging and the CalVer tag 2608.1 are the author's actions. Live dry run of the resulting behaviour against the real account (export → plan = zero diff on 24/24 domains) was done at 5527548 and re-done by the round-3 reviewer's export→load→plan round-trip with a mixed fixture at ec06620.

@srgvg
srgvg merged commit 27ef757 into main Aug 30, 2026
10 checks passed
@srgvg
srgvg deleted the feat/forward-email branch August 31, 2026 04:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant