Skip to content

Resolve the git credential when each remote command runs - #2124

Merged
ppXD merged 1 commit into
mainfrom
fix/resolve-the-git-credential-at-each-push
Oct 18, 2026
Merged

ppXD merged 1 commit into
mainfrom
fix/resolve-the-git-credential-at-each-push

Conversation

@ppXD

@ppXD ppXD commented Oct 18, 2026

Copy link
Copy Markdown
Owner

Summary

  • Resolve the git credential immediately before each command that reaches the remote (clone fetch, LFS upload, push, readback, every revise re-push) through a narrow IWorkspaceTokenSource backed by the same resolver and provider-auth layer that provisioned the workspace. A GitLab OAuth access token lives two hours and is revoked the moment any caller refreshes it, so a token captured at run start was dead by the end-of-run push in long runs, or whenever a sibling run refreshed the credential.
  • WorkspaceRequest gains additive RepositoryId / TeamId (data only, in Messages). A request without a repository id, or a provider without a token source, keeps the captured token exactly as before.
  • A push the remote refuses is retried once with a freshly resolved credential, only when it differs from the refused one. Refused again, or resolved to the same one, it ends as a typed GitCredentialRejectedException (git.credential_rejected, not retried) instead of presenting the same refusal three times against the account.
  • The run keeps Succeeded so every caller that reads that status is unchanged. The failure is recorded as PublishFailureCode and PublishError on the run result, in the publish manifest and on the timeline. A run whose acceptance contract finds nothing to grade because of it now fails closed with credential-rejected (infra) instead of no-branch-or-repo.
  • The push-retry doc comments in AgentRunExecutor now describe this behaviour: a refused credential is classified by the workspace provider and never retried, while other WorkspaceExceptions still get the bounded blind retry. LocalWorkspaceHandle takes its collaborators as one private HandleDependencies record, so its constructor stays within five parameters.

Test plan

  • Unit: LocalGitWorkspaceProviderCredentialTests, RepositoryWorkspaceTokenSourceTests, AgentRunExecutorPushTests, AgentRunExecutorAcceptanceTests, FailureTaxonomyTests (442 passed with the workspace slice)
  • Unit: Architecture, AgentRunExecutor and Sandbox suites (560 passed)
  • Integration against loopback git/GitLab stubs and Postgres: token expires before the push (red on main, green here), stale-then-fresh credential, refused credential, disconnected credential, long-run publish, resolver (33 passed)
  • Non-breaking: a null RepositoryId takes the captured-token path unchanged; no migration; no env toggle; new fields are additive and nullable, so rolling deploy and rollback are safe

@ppXD
ppXD force-pushed the fix/resolve-the-git-credential-at-each-push branch from 79cdf48 to 707475b Compare October 18, 2026 11:20
@ppXD
ppXD force-pushed the fix/show-build-and-credential-plumbing-health branch from 9412742 to 9c3a330 Compare October 18, 2026 11:23
@ppXD
ppXD changed the base branch from fix/show-build-and-credential-plumbing-health to main October 18, 2026 12:08
A run's workspace was provisioned with the access token resolved once at
its start, and the end-of-run push, its LFS upload, the readback and
every revise re-push reused that string. A GitLab OAuth access token
lives two hours and is revoked the moment any caller refreshes it,
while the refresher only refreshes under two minutes left, so a long
run, or one whose credential a sibling run refreshed, pushed with a
dead token. The push retried that token three times, the failure was
swallowed into a Warning, and the run stayed Succeeded with no branch.

WorkspaceRequest now names the repository and team its token was
resolved for. The workspace provider asks a narrow IWorkspaceTokenSource
for the credential immediately before each command that reaches the
remote; the source re-resolves through the same resolver and provider
auth layer that provisioned the clone, so it reads the credential as it
stands now, refreshes it if due and adopts a pair a sibling stored.
A request without a repository id, or a provider without a source,
keeps the captured token exactly as before.

A push the remote refuses is tried once more with a freshly resolved
credential when that differs from the refused one. Refused again, or
resolved to the same one, it ends as a typed GitCredentialRejected
failure instead of presenting the same refusal a third time. The run
keeps its Succeeded status, so every caller that reads Succeeded is
unchanged; the failure is recorded on the run result (code and
message), the publish manifest and the timeline. A contract that finds
nothing to grade because of it now says credential-rejected, classed as
infra, instead of no-branch-or-repo.
@ppXD
ppXD force-pushed the fix/resolve-the-git-credential-at-each-push branch from 707475b to 420f458 Compare October 18, 2026 12:08
@ppXD
ppXD merged commit 6144f8b into main Oct 18, 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.

1 participant