Skip to content

feat(core): store the Mergify credential in the OS keychain - #1803

Draft
sileht wants to merge 1 commit into
devs/sileht/mrgfy-8703-cli-auth-commands/split-mergify-github-token-resolution--32c37b21from
devs/sileht/mrgfy-8703-cli-auth-commands/store-mergify-credential-os-keychain--bd91cd6d
Draft

feat(core): store the Mergify credential in the OS keychain#1803
sileht wants to merge 1 commit into
devs/sileht/mrgfy-8703-cli-auth-commands/split-mergify-github-token-resolution--32c37b21from
devs/sileht/mrgfy-8703-cli-auth-commands/store-mergify-credential-os-keychain--bd91cd6d

Conversation

@sileht

@sileht sileht commented Sep 4, 2026

Copy link
Copy Markdown
Member

mergify auth login has to put the token it mints somewhere. This adds
that somewhere, keyed by API URL and nothing else -- one machine can
legitimately hold a credential for the hosted service and one for an
on-premise install, and each has to survive the other's logout.

Two backends. The OS keychain first: macOS Keychain, the Windows
credential manager, or a freedesktop Secret Service. Then a 0600 JSON
file under the platform configuration directory, used whenever the
keychain is absent or refuses -- a container, a CI runner, or an SSH
session on a headless box has no D-Bus session at all, and auth login
still has to work there. So no keychain failure is fatal; each one is a
debug line and a fallback.

Two details worth the reviewer's attention:

  • keyring's default feature set resolves to
    zbus-secret-service-keyring-store on Linux, which speaks D-Bus in
    pure Rust. The libsecret-backed alternative would have added a C
    build dependency inside the manylinux container and a runtime .so
    on the user's machine, for the same functionality. The bill for
    avoiding that is 63 transitive crates, nearly all of them zbus and
    its async-io runtime, and nearly all of them Linux-only.
  • keyring 4.2's MSRV is 1.88.0, which is exactly this workspace's
    floor. A patch release that raises it reddens the msrv job.

discover() is infallible on purpose. A machine with no configuration
directory has no fallback, which is not the same as having no store --
and that machine, a Windows service account or a systemd unit with
ProtectHome=, is often exactly the one whose OS credential manager
works. Failing there would refuse to read an entry sitting right in
front of it. Writing when neither backend is available is the one case
that errors, and it says which of the two is missing.

Two backends mean two ways to disagree, and both are handled by
reading back rather than trusting a write. A keychain that refuses a
delete looks exactly like one that had nothing, so delete checks
whether the entry is still there and says so if it is -- logout
promising a clean machine cannot report success it did not achieve.
Same check before the fallback write: an older keychain entry that
cannot be removed would shadow the file copy forever, since get
reads the keychain first.

A write goes to a sibling temp file and is renamed, so a crash cannot
truncate a store holding a second deployment's credential, and the
backend that did not take the credential is cleared, so logging in once
without a keychain and once with it leaves no readable copy behind. An
emptied store is deleted rather than left as {}: an empty object on
disk reads, to anyone auditing the machine, like a credential that
failed to load.

Nothing is wired to this yet.

Fixes MRGFY-8703

Depends-On: #1802

@mergify
mergify Bot had a problem deploying to Mergify Merge Protections September 4, 2026 23:20 Failure
@sileht

sileht commented Sep 4, 2026

Copy link
Copy Markdown
Member Author

This pull request is part of a Mergify stack:

# Pull Request Link
1 refactor(core): split Mergify and GitHub token resolution #1802
2 feat(core): store the Mergify credential in the OS keychain #1803 👈
3 feat(core): let a caller read an endpoint's rejection body #1804
4 feat(auth): the device authorization grant, client side #1805
5 feat(cli): mergify auth login, logout and status #1806
6 feat(core): use the stored credential, deprecate GITHUB_TOKEN #1807
7 fix(core): never send a Mergify user token to GitHub #1808

@mergify

mergify Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Merge Protections

🔴 3 of 7 protections blocking · waiting on 👀 reviews and ⛓️ dependency

Protection Waiting on
🔴 ⛓️ Depends-On Requirements ⛓️ dependency
🔴 👀 Review Requirements 👀 reviews
🔴 🔎 Reviews 👀 reviews
🟢 🤖 Continuous Integration
🟢 Enforce conventional commit
🟢 📕 PR description
🟢 🚦 Auto-queue

🔴 ⛓️ Depends-On Requirements

Waiting for

This rule is failing.

Requirement based on the presence of Depends-On in the body of the pull request

🔴 👀 Review Requirements

Waiting for

  • #approved-reviews-by>=2
This rule is failing.
  • any of:
    • #approved-reviews-by>=2
    • author = dependabot[bot]
    • author = mergify-ci-bot
    • author = renovate[bot]

🔴 🔎 Reviews

Waiting for

  • #review-requested = 0
This rule is failing.
  • #review-requested = 0
  • #changes-requested-reviews-by = 0
  • #review-threads-unresolved = 0

Show 4 satisfied protections

🟢 🤖 Continuous Integration

  • all of:
    • check-success=ci-gate

🟢 Enforce conventional commit

Make sure that we follow https://www.conventionalcommits.org/en/v1.0.0/

  • title ~= ^(fix|feat|internal|docs|style|refactor|perf|test|build|ci|chore|revert|ui)(?:\(.+\))?!?:

🟢 📕 PR description

  • body ~= (?ms:.{48,})

🟢 🚦 Auto-queue

When all merge protections are satisfied, this pull request will be queued automatically.

`mergify auth login` has to put the token it mints somewhere. This adds
that somewhere, keyed by API URL and nothing else -- one machine can
legitimately hold a credential for the hosted service and one for an
on-premise install, and each has to survive the other's `logout`.

Two backends. The OS keychain first: macOS Keychain, the Windows
credential manager, or a freedesktop Secret Service. Then a `0600` JSON
file under the platform configuration directory, used whenever the
keychain is absent or refuses -- a container, a CI runner, or an SSH
session on a headless box has no D-Bus session at all, and `auth login`
still has to work there. So no keychain failure is fatal; each one is a
debug line and a fallback.

Two details worth the reviewer's attention:

- `keyring`'s default feature set resolves to
  `zbus-secret-service-keyring-store` on Linux, which speaks D-Bus in
  pure Rust. The libsecret-backed alternative would have added a C
  build dependency inside the manylinux container and a runtime `.so`
  on the user's machine, for the same functionality. The bill for
  avoiding that is 63 transitive crates, nearly all of them zbus and
  its async-io runtime, and nearly all of them Linux-only.
- `keyring` 4.2's MSRV is 1.88.0, which is exactly this workspace's
  floor. A patch release that raises it reddens the msrv job.

`discover()` is infallible on purpose. A machine with no configuration
directory has no *fallback*, which is not the same as having no store --
and that machine, a Windows service account or a systemd unit with
`ProtectHome=`, is often exactly the one whose OS credential manager
works. Failing there would refuse to read an entry sitting right in
front of it. Writing when neither backend is available is the one case
that errors, and it says which of the two is missing.

Two backends mean two ways to disagree, and both are handled by
reading back rather than trusting a write. A keychain that refuses a
delete looks exactly like one that had nothing, so `delete` checks
whether the entry is still there and says so if it is -- `logout`
promising a clean machine cannot report success it did not achieve.
Same check before the fallback write: an older keychain entry that
cannot be removed would shadow the file copy forever, since `get`
reads the keychain first.

A write goes to a sibling temp file and is renamed, so a crash cannot
truncate a store holding a second deployment's credential, and the
backend that did not take the credential is cleared, so logging in once
without a keychain and once with it leaves no readable copy behind. An
emptied store is deleted rather than left as `{}`: an empty object on
disk reads, to anyone auditing the machine, like a credential that
failed to load.

Nothing is wired to this yet.

Fixes MRGFY-8703

Change-Id: Ibd91cd6db5a6c3c5863e40538d9641f97c0b5162
@sileht
sileht force-pushed the devs/sileht/mrgfy-8703-cli-auth-commands/split-mergify-github-token-resolution--32c37b21 branch from 3d3e49d to a02e23a Compare September 4, 2026 23:40
@sileht
sileht force-pushed the devs/sileht/mrgfy-8703-cli-auth-commands/store-mergify-credential-os-keychain--bd91cd6d branch from 7273861 to e0609e3 Compare September 4, 2026 23:40
@sileht

sileht commented Sep 4, 2026

Copy link
Copy Markdown
Member Author

Revision history

# Type Changes Reason Date
1 initial 7273861 2026-09-04 23:40 UTC
2 rebase 7273861 → e0609e3 (rebase only) 2026-09-04 23:40 UTC

@mergify
mergify Bot had a problem deploying to Mergify Merge Protections September 4, 2026 23:41 Failure
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant