Skip to content

thirdshift email-test through Resend - #119

Merged
JacobStephens2 merged 3 commits into
mainfrom
issue-113
Sep 29, 2026
Merged

JacobStephens2 merged 3 commits into
mainfrom
issue-113

Conversation

@JacobStephens2

Copy link
Copy Markdown
Owner

Summary

thirdshift email-test [<address>] sends one test email through Resend's HTTP API, so the email setup can be checked without a Run. The checks and the send live in email::Resend, which Run notifications are to reuse (ADR 0005).

email-test [<address>]
  UserConfig::load            # new [email] section: to, from; unknown keys still rejected
  email::send_test
    Resend::new               # local checks only: address (arg, else email.to) and RESEND_API_KEY
    Resend::send              # one POST {url}/emails, Bearer key, JSON {from,to,subject,text}, 30 s timeout
  → "accepted by Resend; check your inbox" (exit 0) | Resend's error text (exit 1)
 src/
+├── email.rs     # Resend: checks, send, the test email
+├── host.rs      # hostname, moved out of failed_run.rs to share it
 ├── config.rs    # + [email] to/from
 ├── args.rs      # + email-test [<address>]
 └── main.rs      # + help text; update and email-test end through one helper
 tests/
+├── email_test.rs
+└── support/resend.rs   # std-only Resend stand-in that records method, path, auth, JSON body

reqwest becomes a direct dependency with rustls, blocking and json. It was already in the tree through axoupdater. A new packaging test fails if openssl, openssl-sys or native-tls ever joins the tree (ADR 0003).

Evidence

  • Before: tests/email_test.rs failed 9 of 9, because email-test was parsed as an Issue URL (exit 2).
    After: 9 of 9 pass. They cover the request shape, email.to and the argument winning over it, email.from, a missing address, an unset or empty key (no request sent in either case), a 401 JSON error and a 500 text error printed word for word, an unknown [email] key, and an extra argument.
  • The full cargo test passes and cargo clippy --all-targets is clean. packaging::the_binary_never_links_openssl passes.

Merge Danger

Door: two-way

A new command and a new optional config section. Nothing existing changes behaviour, apart from update's ending now going through a shared helper, with the same output.

Blast Radius: small

The binary gains reqwest's blocking client, which adds a few futures crates to Cargo.lock. The failed-run commit message still says host unknown when the OS gives no hostname.

Unaddressed findings

Standards

  • "Accept dependencies, don't create them": Resend::new reads the environment itself. The issue specifies the environment variables RESEND_API_KEY and THIRDSHIFT_RESEND_URL as the seam, and poll.rs already uses the same pattern.
  • Speculative generality: Resend is public but has one caller. The issue requires email-test to use the same checks and sending code as Run notifications, so this seam is what the issue asked for.
  • Duplicated host fallback: "unknown" vs "unknown host". The issue fixes "unknown host" for the email. The failed-run commit reads host unknown, which would become host unknown host with one shared fallback, so each caller keeps its own.
  • Repeated switch in config.rs: sections are matched twice. The outer match only checks each section's shape. The inner (section, key) match is the single table of settings, and splitting it further adds code without removing any.
  • Mysterious name: Resend names the vendor. ADR 0005 picks Resend on purpose, and the type speaks Resend's API. A second provider would be the time to name the role.
  • Primitive obsession: email addresses are plain Strings. There is no validation to hold (Resend validates), so a newtype would add nothing yet.

Spec

  • A second address is an argument error (exit 2). This wasn't asked for. It matches how the Run treats an extra argument.
  • email.to/email.from must be strings. This wasn't asked for. It matches how merge.always is type-checked.
  • The hostname helper moved to host.rs. It was moved so both callers could share it. Failed-run behaviour is unchanged, except that an empty hostname now reads unknown.
  • "Word for word" prints Resend's message field, not the whole JSON. message is Resend's error text. Any reply without one, such as a plain-text 500, is printed whole.
  • Success prints to stderr. This follows the Run's rule, which update also follows: stdout stays empty and messages go to stderr.

Closes #113

🤖 Generated with Claude Code

JacobStephens2 and others added 3 commits September 29, 2026 19:32
… API

The User config gains an [email] section with to and from, and the key
comes only from RESEND_API_KEY. The tests point THIRDSHIFT_RESEND_URL at a
local Resend stand-in that records each request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A refusal with an empty reply prints only the status, an empty
THIRDSHIFT_RESEND_URL falls back to Resend, and the test email no longer
claims it reached anyone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Combines main's logs.dir setting (#117) with this branch's [email]
section: the User config now knows the merge, logs and email sections,
holds both the resolved logs directory and the email settings, and the
README and help text document both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@JacobStephens2
JacobStephens2 merged commit 994bea9 into main Sep 29, 2026
13 checks passed
@JacobStephens2
JacobStephens2 deleted the issue-113 branch September 29, 2026 19:46
@JacobStephens2 JacobStephens2 mentioned this pull request Sep 30, 2026
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.

thirdshift email-test through Resend

1 participant