# Maintaining Mailglass

This document covers the release flow and maintenance protocols for Mailglass.

## Release Flow

Mailglass uses [Release Please](https://github.com/googleapis/release-please) to automate versioning and changelogs.

1. Merge feature branches into `main` using Conventional Commits.
2. Release Please will open a "Release PR" with the version bump and updated `CHANGELOG.md`.
3. Merging the Release PR should trigger the `publish-hex` workflow from the
   published GitHub Release. If downstream workflow fan-out does not happen,
   `workflow_dispatch` with the core release tag (`mailglass-v<version>`) is the canonical maintainer
   fallback.
4. The `publish-hex` workflow is environment-gated and requires manual approval in the GitHub Actions UI.

## Snapshot Update Protocol

When the installer output or golden files change:

1. Run `mix verify.installer.golden`.
2. If the failure is expected, update the golden files in `test/fixtures/`.
3. Commit the updated fixtures with a `chore: update installer golden files` message.

## Required Checks

Before merging any PR, ensure:
- `mix verify.stability_contract`
- `scripts/verify_support_contract.sh`
- `Support Contract Core`
- `Support Contract Admin`
- `Compile No Optional Deps`
- `mix credo --strict`
- `mix dialyzer`
- `mix docs --warnings-as-errors`

The honest repo-root entrypoint is `mix verify.stability_contract` or
`scripts/verify_support_contract.sh`. They run the three required
branch-protection buckets plus the inbound sibling-package docs lane in sequence:
- `Support Contract Core`
- `Support Contract Admin`
- `mailglass_inbound` docs contract (`mailglass_inbound/test/mailglass_inbound/docs_contract_test.exs`)
- `Compile No Optional Deps`

When those checks pass, they prove the current compatibility contract described
in [`guides/compatibility-and-deprecations.md`](guides/compatibility-and-deprecations.md):
runtime floors, matched sibling-package docs wiring for `mailglass_inbound`,
matched `mailglass_admin` release truth, and the required-vs-advisory split
below. Do not claim broader support than those repo artifacts prove.

The following checks are advisory signal, not branch-protection truth:
- `Core Full Suite Advisory`
- `Provider Compatibility Advisory`
- `Provider Live Advisory`

`Provider Live Advisory` remains a cron and `workflow_dispatch` canary. It is not a merge blocker.

## Bus Factor & Continuity

Mailglass is single-maintainer at v0.1. The release pipeline is gated on a GitHub
Environment (`hex-publish`) with a single required reviewer (`szTheory`). When a
GitHub Environment has only one reviewer, GitHub silently disables the
`prevent_self_review` setting — the gate is effectively a one-eye pause, not a
two-eyes review. This is documented honestly here rather than presented as a
stronger control than it is. Multi-owner Hex transition is deferred to v0.5,
when production adopters exist (D-26 rationale: at v0.1 the asymmetry of a
co-owner being able to `mix hex.publish` from their own machine bypassing
GitHub governance is a worse footgun than the bus-factor risk it solves).

If `szTheory` is unreachable for more than 30 days, the community can request a
Hex.pm package transfer by opening a public issue titled
`Maintainer-unreachable: requesting Hex transfer` on
https://github.com/szTheory/mailglass/issues — Hex.pm's public maintainer-transfer
process can be initiated from there.

## Retract Decision Tree

Five rules. Bias toward patch over retract — three retractions in your first six
months tells evaluators "don't bet on this lib."

1. **Data-loss / security / signature bypass / fails to compile.**
   Run `mix hex.retire <pkg> <ver> security|invalid --message "<140 chars>"`
   AND ship `<ver+1>` immediately.
2. **User-visible breakage with workaround.**
   Do NOT retire. Patch within 7 days. Add a CHANGELOG entry.
   If the fix changes a documented compatibility bridge or support claim, update
   `guides/compatibility-and-deprecations.md` in the same patch.
3. **Cosmetic / docs / non-runtime.**
   Do NOT retire. Roll into next planned patch.
4. **Published less than 60 minutes ago AND zero downloads.**
   Run `mix hex.publish --revert <ver>` (only window where unpublish works —
   also bounded by Hex.pm's 24-hour initial-release window).
5. **Already retired and false alarm.**
   Run `mix hex.retire <pkg> <ver> --unretire`.

## Security Response SLA

Single-maintainer numbers, written to be kept rather than aspired to.

- **Acknowledgement of report:** within 72 hours.
- **Mitigation or workaround for critical issues:** within 14 days.
- **Public security advisory:** published alongside the fix.

Critical issue classes are listed in `SECURITY.md` (`## Critical Classes`).
Reports go through the disclosure address documented there or via GitHub
Private Vulnerability Reporting if no email is reachable.

## Release Runbook

Five steps. Step 4 has a literal 60-minute timer — that is the last revert
window before the published artifact becomes permanent.

Use the Phase 38 release-day proof forms while running these steps:
- `.planning/phases/38-release-rehearsal-and-proof-artifacts/38-03-RELEASE-CHECKLIST.md`
- `.planning/phases/38-release-rehearsal-and-proof-artifacts/38-03-RELEASE-RECORD.md`

The checklist separates repo-proved gates from manual/external proof and forces
explicit capture of the tag, workflow run URLs, approver identity, fallback
usage, Hex/HexDocs checks, branch-protection result, and 60-minute outcome.

1. **Verify CI green on `main` for the SHA to be released.**
   Check `actions/workflows/ci.yml` — required because publish-hex.yml gates
   on this SHA via the `gate-ci-green` job (per Plan 08, D-16).
   The required release-truth buckets are:
   - `Support Contract Core (Elixir 1.18 / OTP 27)`
   - `Support Contract Admin (Elixir 1.18 / OTP 27)`
   - `Compile No Optional Deps (Elixir 1.18 / OTP 27)`
   - Phase 38 prepublish proof/export bundle (`38-01-PREPUBLISH-PROOF.md`)
   - Phase 38 install/upgrade rehearsal artifact (`38-02-REHEARSAL-EVIDENCE.md`)
2. **Merge the release-please PR.**
   Squash-merge keeps the changelog history linear.
   Review the release PR diff before merge. This repo uses a custom
   mailglass_admin dep-pin sync step, so the generated PR is load-bearing.
   The current release path emits package tags such as `mailglass-v<version>`
   and `mailglass_admin-v<version>`.
   If a broad milestone PR was squash-merged under a non-releasable subject
   and release-please skips the cut, recover with a tiny follow-up commit that
   carries a `Release-As: <intended-version>` footer. Do not hand-edit
   `.release-please-manifest.json` to force the version.
3. **Approve the `hex-publish` deployment in the GitHub Environment UI.**
   Review the pre-publish summary in the workflow run page (rendered by the
   `prepublish-summary` job per D-15) BEFORE clicking Approve. Verify the
   file count, total size, CHANGELOG excerpt, and top files all match
   expectations.
   Record the tag, publish workflow run URL, approver identity, and approval
   timestamp in `38-03-RELEASE-RECORD.md`.
   - **Package order:** The workflow guarantees `mailglass` (core) publishes first; once core is indexed, `mailglass_admin` and `mailglass_inbound` publish in parallel against the newly live core.
   - **Idempotency:** All three publish steps check `mix hex.info` first and skip the publish command if the version is already live, making the workflow safe to retry.
   - **Fallback path:** If the Release Please tag/release exists but `publish-hex` did not fan out, dispatch `.github/workflows/publish-hex.yml` manually (with `package=all` and `dry_run=false`). **Do not dispatch from `main`**. Always use the reviewed release tag (for `1.0.0`: `mailglass-v1.0.0`) so the publish run is pinned to the exact commit Release Please tagged.
4. **Within 60 minutes of publish: smoke-install in a fresh Phoenix app.**
   Set a literal timer when approving the deployment.
   Run:

       mix archive.install hex phx_new --force
       mix phx.new sandbox --no-ecto --no-mailer --install
       cd sandbox
       # add {:mailglass, "== 1.0.0"}, {:mailglass_admin, "== 1.0.0"}, {:mailglass_inbound, "== 0.1.0"} to deps
       mix deps.get && mix mailglass.install && mix compile --warnings-as-errors
       mix phx.server  # visit http://localhost:4000/dev/mail/

   If anything fails AND the publish was less than 60 minutes ago AND zero
   downloads have happened, the Retract Decision Tree rule 4
   (`mix hex.publish --revert`) is reachable. After 60 minutes the only
   options are retire-then-patch (rule 1) or patch-only (rule 2).
Keep the published support story honest: if the smoke or support-contract
   checks reveal a mismatch with the documented matrix or upgrade posture, fix
   the guide and package metadata together rather than carrying split truth.
   For inbound-slice changes, rerun `mix verify.stability_contract` so the
   repo-root lane proves the canonical `mailglass_inbound` docs and support
   posture before you publish.
   If you need to reproduce the v0.2 codemod or rollback story during this
   window, do it in a disposable fixture or git-clean worktree only. The
   public rollback contract is git-based review/revert of the upgrade diff,
   not cleanup of arbitrary dirty repositories.

   The post-publish-smoke workflow (`.github/workflows/post-publish-smoke.yml`,
   Plan 09) runs the same smoke automatically — but it does not respect the
   60-minute window. Run the manual smoke during the window regardless.
   If publish succeeds but smoke does not fan out, use `workflow_dispatch` on
   `.github/workflows/post-publish-smoke.yml` with that same core tag.
   Record the post-publish smoke run URL, whether fallback dispatch was used,
   Hex/HexDocs URLs, and the final 60-minute decision in the Phase 38 release
   record.
5. **Post the release link to Elixir Forum #libraries section** (post-publish, optional
   — performed by maintainer on their own cadence; not gated by Phase 07.1's
   milestone-shipped marker per CONTEXT line 14 / line 351).
   Body equals the GitHub Release narrative (CHANGELOG entry verbatim plus
   one framing paragraph for 0.x.0 minor bumps; verbatim CHANGELOG only for
   patches).
