feat(oauth): migrate GitLab token from JSON file to operator_oauth_tokens #824

Merged
reviewer merged 1 commit from feat/gitlab-oauth-to-db into main 2026-05-04 12:49:02 +00:00
Collaborator

Closes one of three slices of #823.

Summary

oauth-gitlab.ts now reads via getOperatorOAuth("gitlab") and writes via upsertOperatorOAuth({ forge_type: "gitlab", … }) — same pattern as the Forgejo / GitHub flows. ~/.local/state/claude-hooks/gitlab-oauth-token.json retired. Idempotent migration 012 ports any existing JSON file into the DB on first boot, then unlinks the file.

The callback now also fetches the operator profile from /api/v4/user so account_login / account_id land with real values instead of sentinels (mirrors forgejo / github).

Changes

  • apps/server/src/http/handlers/oauth-gitlab.ts — DB-backed; dropped gitlabOAuthTokenPath, mkdir, readFile, writeFile, writeGitLabToken. Added profile fetch.
  • apps/server/src/http/handlers/oauth-gitlab.test.ts (new) — 11 tests covering token read / refresh / no-row, init redirect, callback happy path + profile failure fallback + missing state + exchange failure.
  • apps/server/src/infrastructure/database/migrations/012-migrate-gitlab-oauth-to-db.ts (new) — file-existence + row-existence guards; AEAD-encrypts via encryptToken.
  • apps/server/src/infrastructure/database/migrations/012-migrate-gitlab-oauth-to-db.test.ts (new) — 5 tests: happy migrate, idempotent re-run, file-present-row-present (file unlinked, row preserved), no-file no-op, malformed JSON (file kept, no throw).
  • apps/server/src/infrastructure/database/db.ts — wired runMigrateGitlabOauthToDbMigration into ensureSchema() after runCollapseBuiltinRowsMigration.

Notes

  • Migration backfills account_login: "unknown" / base_url: "https://gitlab.com" when the legacy file lacked them; the next callback overwrites with real values from /api/v4/user.
  • Did NOT add session-cookie issuance / setActiveForge('gitlab') to the GitLab callback — those weren't in the original handler and this PR is scoped to persistence shape. Bringing GitLab to operator-login parity is a separate cut-over.

Test plan

  • 16 new tests pass; broader test suite has the same 242 pre-existing fails as main (TDZ in agents-health.test.ts unrelated to this branch)
  • Verify existing GitLab callback flow still completes end-to-end on staging
  • Run migration 012 against an instance that has gitlab-oauth-token.json and confirm row + file removal

🤖 Generated with Claude Code

Closes one of three slices of #823. ## Summary `oauth-gitlab.ts` now reads via `getOperatorOAuth("gitlab")` and writes via `upsertOperatorOAuth({ forge_type: "gitlab", … })` — same pattern as the Forgejo / GitHub flows. `~/.local/state/claude-hooks/gitlab-oauth-token.json` retired. Idempotent migration `012` ports any existing JSON file into the DB on first boot, then `unlink`s the file. The callback now also fetches the operator profile from `/api/v4/user` so `account_login` / `account_id` land with real values instead of sentinels (mirrors forgejo / github). ## Changes - `apps/server/src/http/handlers/oauth-gitlab.ts` — DB-backed; dropped `gitlabOAuthTokenPath`, `mkdir`, `readFile`, `writeFile`, `writeGitLabToken`. Added profile fetch. - `apps/server/src/http/handlers/oauth-gitlab.test.ts` (new) — 11 tests covering token read / refresh / no-row, init redirect, callback happy path + profile failure fallback + missing state + exchange failure. - `apps/server/src/infrastructure/database/migrations/012-migrate-gitlab-oauth-to-db.ts` (new) — file-existence + row-existence guards; AEAD-encrypts via `encryptToken`. - `apps/server/src/infrastructure/database/migrations/012-migrate-gitlab-oauth-to-db.test.ts` (new) — 5 tests: happy migrate, idempotent re-run, file-present-row-present (file unlinked, row preserved), no-file no-op, malformed JSON (file kept, no throw). - `apps/server/src/infrastructure/database/db.ts` — wired `runMigrateGitlabOauthToDbMigration` into `ensureSchema()` after `runCollapseBuiltinRowsMigration`. ## Notes - Migration backfills `account_login: "unknown"` / `base_url: "https://gitlab.com"` when the legacy file lacked them; the next callback overwrites with real values from `/api/v4/user`. - Did NOT add session-cookie issuance / `setActiveForge('gitlab')` to the GitLab callback — those weren't in the original handler and this PR is scoped to persistence shape. Bringing GitLab to operator-login parity is a separate cut-over. ## Test plan - [x] 16 new tests pass; broader test suite has the same 242 pre-existing fails as main (TDZ in agents-health.test.ts unrelated to this branch) - [ ] Verify existing GitLab callback flow still completes end-to-end on staging - [ ] Run migration `012` against an instance that has `gitlab-oauth-token.json` and confirm row + file removal 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(oauth): migrate GitLab token from JSON file to operator_oauth_tokens
Some checks failed
qa / dockerfile (pull_request) Successful in 19s
qa / qa-1 (pull_request) Has been cancelled
qa / qa (pull_request) Has been cancelled
38025dc7bb
Brings the GitLab OAuth handler in line with Forgejo + GitHub by writing
the operator's access + refresh tokens into `operator_oauth_tokens`
(forge_type='gitlab') instead of `<stateRoot>/gitlab-oauth-token.json`.
Tokens are now AEAD-encrypted at rest like the other forges.

Migration 012 one-shots existing operators: when the legacy file exists
and no `gitlab` row is present, the file is parsed, inserted, and
unlinked. `account_login` and `base_url` (NOT NULL columns the legacy
file shape didn't carry) are backfilled with sentinels — the next
OAuth callback overwrites them with real values fetched from
`/api/v4/user`. Idempotent across re-runs.

The handler now also fetches the operator profile during the callback so
fresh logins land the real account_login + account_id, matching the
Forgejo + GitHub flow.

Refs #823
charles force-pushed feat/gitlab-oauth-to-db from 38025dc7bb
Some checks failed
qa / dockerfile (pull_request) Successful in 19s
qa / qa-1 (pull_request) Has been cancelled
qa / qa (pull_request) Has been cancelled
to 32004da955
All checks were successful
qa / dockerfile (pull_request) Successful in 19s
qa / qa-1 (pull_request) Successful in 5m25s
qa / qa (pull_request) Successful in 0s
2026-05-04 12:33:42 +00:00
Compare
reviewer approved these changes 2026-05-04 12:48:58 +00:00
reviewer left a comment

All operator_oauth_tokens ACs from #823 met.

  • oauth-gitlab.ts: JSON file I/O cleanly replaced with getOperatorOAuth / upsertOperatorOAuth / updateOAuthTokenAfterRefresh; gitlabOAuthTokenPath fully removed (no dangling references).
  • Migration 012: idempotent, handles all four file/row states correctly, encrypts tokens via encryptToken, falls back to sentinels for missing account_login/base_url.
  • Tests cover happy path, idempotency, malformed JSON, and missing-file no-op.
  • CI green.
All `operator_oauth_tokens` ACs from #823 met. - `oauth-gitlab.ts`: JSON file I/O cleanly replaced with `getOperatorOAuth` / `upsertOperatorOAuth` / `updateOAuthTokenAfterRefresh`; `gitlabOAuthTokenPath` fully removed (no dangling references). - Migration 012: idempotent, handles all four file/row states correctly, encrypts tokens via `encryptToken`, falls back to sentinels for missing `account_login`/`base_url`. - Tests cover happy path, idempotency, malformed JSON, and missing-file no-op. - CI green.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/claude-hooks!824
No description provided.