{"record":{"id":"67993a3a0757c844","repo":"gitbutlerapp/gitbutler","slug":"failed-to-migrate-gitlab-token-for-project-proje","errorCode":null,"errorMessage":"Failed to migrate GitLab token for project ${projectId}:","messagePattern":"Failed to migrate GitLab token for project (.+?):","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"apps/desktop/src/lib/forge/gitlab/gitlabUserService.svelte.ts","lineNumber":115,"sourceCode":"\tconstructor(\n\t\tbackendApi: BackendApi,\n\t\tprivate secretsService: SecretsService,\n\t) {\n\t\tthis.backendApi = injectBackendEndpoints(backendApi);\n\t}\n\n\t/**\n\t * Migrate the access token for the given project from the old storage location (if it exists) to the new one.\n\t */\n\tasync migrate(projectId: string): Promise<void> {\n\t\ttry {\n\t\t\tconst gitlabToken = await this.secretsService.get(`git-lab-token:${projectId}`);\n\t\t\tif (!gitlabToken) return;\n\t\t\tawait this.backendApi.endpoints.storeGitLabPat.initiate({ accessToken: gitlabToken });\n\t\t\tawait this.secretsService.delete(`git-lab-token:${projectId}`);\n\t\t} catch (error) {\n\t\t\t// Fail should not explote. Log instead.\n\t\t\tconsole.warn(`Failed to migrate GitLab token for project ${projectId}:`, error);\n\t\t}\n\t}\n\n\tget storeGitLabPat() {\n\t\treturn this.backendApi.endpoints.storeGitLabPat.useMutation();\n\t}\n\n\tget storeGitLabEnterprisePat() {\n\t\treturn this.backendApi.endpoints.storeGitLabEnterprisePat.useMutation();\n\t}\n\n\tget forgetGitLabAccount() {\n\t\treturn this.backendApi.endpoints.forgetGitLabAccount.useMutation();\n\t}\n\n\tauthenticatedUser<T = GitlabAuthenticatedUserSensitive | null>(\n\t\taccount: GitlabAccountIdentifier,\n\t\toptions?: { transform?: (result: GitlabAuthenticatedUserSensitive | null) => T },","sourceCodeStart":97,"sourceCodeEnd":133,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/apps/desktop/src/lib/forge/gitlab/gitlabUserService.svelte.ts#L97-L133","documentation":"migrate() lifts a project's GitLab PAT out of the legacy per-project secret key (git-lab-token:{projectId} in the OS secret store) into the backend via the storeGitLabPat endpoint, then deletes the old key. Any failure in that chain is caught and warned so migration never breaks startup. The store-before-delete ordering makes the operation idempotent: a failed run leaves the old key in place and later retries converge.","triggerScenarios":"Calling migrate(projectId) when secretsService.get or .delete throws (secret store unavailable, keychain locked) or when the backendApi storeGitLabPat mutation rejects (backend down, auth failure, token rejected).","commonSituations":"Backend not ready when migration runs at app start; macOS keychain access denied; GitLab token revoked server-side; a crash between store and delete leaving copies in both places.","solutions":["Do nothing — the next launch re-runs migrate() and converges because the old key is deleted only after a successful store","If the backend rejects the token, re-enter the PAT in settings so the new store receives a valid one","If the secret store errors, unlock the keychain or verify libsecret on Linux, then restart","If stale duplicates cause issues, manually clear the old git-lab-token:{projectId} secret"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// Only touch the backend when there is actually something to migrate\nconst token = await this.secretsService.get(`git-lab-token:${projectId}`);\nif (!token) return;","typeGuard":"function isKeychainError(error: unknown): boolean {\n\treturn error instanceof Error && /keychain|secret|access denied/i.test(error.message);\n}","tryCatchPattern":"try {\n\tawait this.backendApi.endpoints.storeGitLabPat.initiate({ accessToken: token });\n\tawait this.secretsService.delete(`git-lab-token:${projectId}`);\n} catch (error) {\n\tconsole.warn(`Failed to migrate GitLab token for ${projectId}:`, error);\n\t// safe to retry next launch: the old key still exists\n}","preventionTips":["Keep store-then-delete ordering so migrations stay retryable","Run migrations after the backend-ready signal, not at import time","Log projectId plus error class for triage","Alert on repeated migration failures for the same project"],"tags":["typescript","gitlab","token","migration","secrets"],"backgroundTag":"credential-migration-failed","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}