Files
windmill/frontend/src/lib/components/git_sync/setup/repoConnection.ts
T
Diego ImbertandClaude Opus 5 504d01a563 feat: rework the git sync setup into a guided flow (#11308)
* feat: rework git sync settings into a flat repo list and setup modal

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat: add the connect step for gitlab and github in git sync setup

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat: open the github app install page from the git sync setup next step

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat: let git sync setup set the repository folder

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat: add the configure sync step to git sync setup

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* style: even out the git sync filter settings columns

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* style: pin the filter input to the bottom of the filter list

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* style: frame the git sync path filters in a box with an empty hint

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor: use a multiselect for the git sync path filters

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat: grey out the skipped connect step in git sync setup

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* style: move the check job line to the bottom of the setup modal

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: hide the check job line once the repository check resolved

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* style: center the repository check loader in the setup modal

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: stop the setup modal shifting when the check job id arrives

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* style: move the existing resource hint to the setup modal footer

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat: always show the existing resource option when one exists

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* style: drop the card and collapse from the git sync filter settings

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* style: use a success alert for the pull from git notice

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat: make initializing the repository a step of the git sync setup

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* style: hide the push job lines once they succeed

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* style: reword the push step intro

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* style: copy button on the cli snippet and a taller changes list

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* style: match the push step loading to the check step

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: stop the push step shifting when a job id arrives

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* style: let the changes list fill the push step

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat: end the git sync setup on a done step instead of a modal

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat: delete a git sync repository from a row menu with a confirmation

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* style: lift the git sync done message off the notices

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat: restore github app installation transfer between instances

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: address regressions from the git sync setup rework

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: keep the overlay z-index floor in step with the ai chat

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: hold the push state in the setup dialog, not in the step it destroys

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: block every dismissal while setup work is in flight and keep sync reachable

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: regenerate the resource path when the repository changes

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: configure the resource the connect step actually created

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: keep a modal open when a drawer above it takes the click

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: snapshot the topmost surface for keyboard clicks too

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* style: size the add repository buttons and explain git promotion

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* revert: drop the promotion-first action from the empty state

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* style: shrink the test connection result line

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat: offer the fork sync and fork pull request toggles during setup

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* style: group promotion repositories under their own label

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat: offer the deploy branch pull request toggle during setup

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: wait for the repository credential before saving the setup

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: ignore a superseded credential load and drop the bindable default

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: key setup readiness to the repository it was loaded for

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: tie setup readiness to the attempt that asked for it

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat: link to where the access token is created

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* style: say plainly how each provider connects

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat: sync resources, variables, schedules and triggers by default

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* style: use the accent colour for the token help link

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* style: move the CI/CD deploy hint next to the pull toggle

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: keep setup gated when the connection could not be read

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: only explain pull-from-git where the workspace can turn it on

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: offer a save retry once the repository is initialized

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: say when pulling fell back to polling on the last setup step

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: clear the initialized flag when a new draft starts

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-24 11:54:25 +02:00

118 lines
3.5 KiB
TypeScript

import { GitSyncService, ResourceService, VariableService } from '$lib/gen'
/** What a provider form hands back once the user has picked a repository. */
export type RepoConnection = {
/** The repository URL, never carrying a secret. */
url: string
isGithubApp?: boolean
/** A token for Windmill to store and renew server-side (EE GitLab). */
heldToken?: string
/** `url` with the token embedded, filed as a secret variable because the
* resource value itself is readable by anyone who can read the resource. */
tokenUrl?: string
}
/** Username git sends alongside a token: GitHub ignores it for PATs, GitLab
* requires a non-empty one and documents `oauth2`. */
const TOKEN_USERNAME = { github: 'x-access-token', gitlab: 'oauth2' } as const
export type TokenProvider = keyof typeof TOKEN_USERNAME
/** Parses a pasted repository URL, dropping any credentials it carried. */
export function parseRepoUrl(raw: string): URL | undefined {
let u: URL
try {
u = new URL(raw.trim())
} catch {
return undefined
}
// https only: the token is embedded in this URL, and plain http would put it on the wire.
if (u.protocol !== 'https:') return undefined
if (u.pathname.replace(/\/+$/, '').split('/').filter(Boolean).length < 2) return undefined
u.username = ''
u.password = ''
u.search = ''
u.hash = ''
return u
}
export function withToken(url: URL, token: string, provider: TokenProvider): string {
const u = new URL(url.toString())
u.username = TOKEN_USERNAME[provider]
u.password = token.trim()
return u.toString()
}
/** A resource name derived from the repository, e.g. `acme/infra.git` → `infra`. */
export function repoSlug(url: string): string {
const last =
url
.replace(/\.git\/?$/, '')
.replace(/\/+$/, '')
.split('/')
.pop() ?? ''
return last.replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase() || 'git_repository'
}
/**
* Writes everything a connection needs and creates the `git_repository` resource at
* `path`. A token-bearing URL goes to a secret variable at the same path, which the
* backend renames along with the resource.
*/
export async function createRepositoryResource(
workspace: string,
path: string,
conn: RepoConnection,
location: { branch?: string; folder?: string }
): Promise<void> {
const branch = location.branch?.trim()
// Relative to the repository root, which is how the sync script joins it.
const folder = location.folder?.trim().replace(/^\/+|\/+$/g, '')
let url = conn.url
let createdVariable = false
if (conn.tokenUrl) {
if (await VariableService.existsVariable({ workspace, path })) {
throw new Error(`A variable already exists at ${path}. Pick another path.`)
}
await VariableService.createVariable({
workspace,
requestBody: {
path,
value: conn.tokenUrl,
is_secret: true,
description: `Authenticated URL of the git repository ${conn.url}`
}
})
createdVariable = true
url = `$var:${path}`
}
try {
if (conn.heldToken) {
await GitSyncService.setGitCredential({
workspace,
requestBody: { repo_url: conn.url, token: conn.heldToken }
})
}
await ResourceService.createResource({
workspace,
requestBody: {
path,
resource_type: 'git_repository',
description: `Git repository ${conn.url}`,
value: {
url,
...(branch ? { branch } : {}),
...(folder ? { folder } : {}),
is_github_app: !!conn.isGithubApp
}
}
})
} catch (e) {
// Otherwise a retry at the same path is refused over this run's own secret.
if (createdVariable) {
await VariableService.deleteVariable({ workspace, path }).catch(() => {})
}
throw e
}
}