Fixes silent timeouts and partial-tree rendering when loading large git repositories into the in-app viewer (Tony Hoang report: 400+ host_vars files, 32 roles). Three coordinated fixes: 1. Frontend (GitRepoViewer.svelte): drop the 60s clone timeout. Long-poll getJobUpdates until the job completes, with a 30 min hard cap and a user-cancel button. Stream live job logs into the viewer with a link to the full job page. After success, verify the .windmill_clone_complete marker before flipping pathExists, so a partial S3 directory is no longer rendered as a complete tree. 2. Backend (check_s3_folder_exists, EE): new optional marker_file query param. When set, the handler short-circuits to head() on the marker object instead of "any object under prefix exists". The frontend now always passes marker_file=.windmill_clone_complete. 3. Hub script: cloneRepoToS3forGitRepoViewer points at hub/28216, which uploads files via a bounded-concurrency pool (16 workers), emits throttled progress logs, and writes .windmill_clone_complete as its last action. docs/clone_repo_and_upload_to_instance_storage.bun.ts is the source for that hub publish; docs/git-repo-viewer-hub-script.md explains the change. Also drops three unused legacy hubPaths entries (cloneRepoToS3forGitRepoViewer_0..2) — none were referenced from anywhere in the codebase. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4.4 KiB
Git repo viewer — hub script patch
This documents the changes that need to be applied to the hub script
clone_repo_and_upload_to_instance_storage (currently published as
hub/28182/clone_repo_and_upload_to_instance_storage and referenced from
frontend/src/lib/hubPaths.json as cloneRepoToS3forGitRepoViewer).
The repo viewer in the Windmill app expects the hub script to:
- Upload files in parallel with bounded concurrency (sequential per-file uploads of 400+ files easily blow past any reasonable client-side timeout).
- Log progress so the streaming logs visible in the viewer are useful.
- Write a completion marker as the very last action of a successful run, so the API and frontend can distinguish a fully-populated S3 directory from a partial / interrupted upload.
The marker file the frontend looks for is .windmill_clone_complete at the
root of the per-commit directory:
gitrepos/{workspace}/{resource_path}/{commit_hash}/.windmill_clone_complete
The frontend passes markerFile=.windmill_clone_complete to the
checkS3FolderExists API, which only reports the folder as existing when this
exact file is present.
Replacement uploadDirectoryToS3 implementation
Replace the recursive sequential uploadDirectoryToS3 function with a
batched-concurrent implementation, and write the marker after the walk
completes:
const UPLOAD_CONCURRENCY = 16
const CLONE_MARKER_FILE = ".windmill_clone_complete"
async function uploadDirectoryToS3(
directoryPath: string,
s3BasePath: string,
workspace: string,
) {
console.log(`Uploading ${directoryPath} -> ${s3BasePath}`)
// Walk the directory once, producing a flat list of (localPath, s3Key) pairs.
const tasks: { localPath: string; s3Key: string }[] = []
function walk(dir: string, s3Path: string) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const fullPath = join(dir, entry.name)
const s3Key = s3Path ? `${s3Path}/${entry.name}` : entry.name
if (entry.isDirectory()) {
walk(fullPath, s3Key)
} else if (entry.isFile()) {
tasks.push({ localPath: fullPath, s3Key })
}
}
}
walk(directoryPath, s3BasePath)
console.log(`Discovered ${tasks.length} files to upload`)
// Bounded-concurrency upload pool.
let nextIndex = 0
let uploaded = 0
let lastReport = 0
async function worker() {
while (true) {
const idx = nextIndex++
if (idx >= tasks.length) return
const { localPath, s3Key } = tasks[idx]
const fileContent = fs.readFileSync(localPath)
const blob = new Blob([fileContent], {
type: "application/octet-stream",
})
await wmillclient.HelpersService.gitRepoViewerFileUpload({
workspace,
fileKey: s3Key,
requestBody: blob,
})
uploaded++
// Throttled progress log so 400+ files don't drown the log.
if (uploaded - lastReport >= 25 || uploaded === tasks.length) {
lastReport = uploaded
console.log(`Uploaded ${uploaded} / ${tasks.length} files`)
}
}
}
const workers = Array.from(
{ length: Math.min(UPLOAD_CONCURRENCY, tasks.length) },
() => worker(),
)
await Promise.all(workers)
// Write the completion marker LAST. Until this exists, the viewer treats the
// directory as not-yet-cloned.
const markerKey = `${s3BasePath}/${CLONE_MARKER_FILE}`
const markerBody = JSON.stringify({
completed_at: new Date().toISOString(),
file_count: tasks.length,
})
await wmillclient.HelpersService.gitRepoViewerFileUpload({
workspace,
fileKey: markerKey,
requestBody: new Blob([markerBody], { type: "application/json" }),
})
console.log(`Wrote completion marker: ${markerKey}`)
}
Notes for review
- Concurrency level: 16 is a starting point; tune based on instance storage backend. Too high will overwhelm the API; too low won't help.
- Marker is the last write: if any upload fails, the marker is never written and the viewer correctly shows the state as incomplete.
- No deletion of stale partials: the script overwrites the same per-commit paths on retry, so a partial upload + retry naturally heals. Old commit directories from before this patch are unreachable through the UI but still consume storage; an instance admin can prune them manually if desired.
- Error propagation: keep the existing
try/catchinmainso an upload failure surfaces in the job result and is shown in the new viewer error banner.