Build and Deploy Verso / deploy (push) Has been cancelled
- Icon: change rail tab from integration_instructions to merge - Auto-push on compile: toggle stored in localStorage, watches compile context; when compiling goes true→false with no error, triggers a push automatically (including the latest build's PDF if configured) - PDF destination path: new gitSyncPdfPath field; backend fetches the compiled PDF from CLSI (buildId + clsiServerId passed from frontend) and writes it at the configured path in the repo; silently skipped if no recent compile or field is blank - Push now always sends current buildId/clsiServerId so PDF is included without needing a separate save step Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
122 lines
4.2 KiB
JavaScript
122 lines
4.2 KiB
JavaScript
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'
|
|
import { createWriteStream } from 'node:fs'
|
|
import { join, dirname } from 'node:path'
|
|
import { tmpdir } from 'node:os'
|
|
import { spawn } from 'node:child_process'
|
|
import { pipeline } from 'node:stream/promises'
|
|
import logger from '@overleaf/logger'
|
|
import ProjectEntityHandler from '../Project/ProjectEntityHandler.mjs'
|
|
import HistoryManager from '../History/HistoryManager.mjs'
|
|
import ClsiManager from '../Compile/ClsiManager.mjs'
|
|
import { Project } from '../../models/Project.mjs'
|
|
import { ObjectId } from '../../infrastructure/mongodb.mjs'
|
|
|
|
async function spawnGit(args, cwd) {
|
|
return new Promise((resolve, reject) => {
|
|
const proc = spawn('git', args, {
|
|
cwd,
|
|
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
})
|
|
let stderr = ''
|
|
proc.stderr?.on('data', d => (stderr += d.toString()))
|
|
proc.on('close', code => {
|
|
if (code !== 0) {
|
|
reject(new Error(`git ${args[0]} exited ${code}: ${stderr.trim()}`))
|
|
} else {
|
|
resolve()
|
|
}
|
|
})
|
|
proc.on('error', reject)
|
|
})
|
|
}
|
|
|
|
async function setConfig(projectId, { remoteUrl, subPath, pdfPath }) {
|
|
await Project.updateOne(
|
|
{ _id: new ObjectId(projectId) },
|
|
{ $set: { gitRemote: remoteUrl, gitSyncPath: subPath, gitSyncPdfPath: pdfPath } }
|
|
)
|
|
}
|
|
|
|
async function getConfig(projectId) {
|
|
const project = await Project.findById(projectId, {
|
|
gitRemote: 1,
|
|
gitSyncPath: 1,
|
|
gitSyncPdfPath: 1,
|
|
}).lean()
|
|
return {
|
|
remoteUrl: project?.gitRemote ?? null,
|
|
subPath: project?.gitSyncPath ?? '',
|
|
pdfPath: project?.gitSyncPdfPath ?? '',
|
|
}
|
|
}
|
|
|
|
async function pushToRemote(
|
|
projectId,
|
|
remoteUrl,
|
|
subPath,
|
|
{ pdfPath, pdfBuildId, pdfClsiServerId, userId } = {}
|
|
) {
|
|
const tmpDir = await mkdtemp(join(tmpdir(), 'verso-git-'))
|
|
logger.debug({ projectId, tmpDir, subPath }, 'git sync: writing project to temp dir')
|
|
try {
|
|
const fileRoot = subPath ? join(tmpDir, subPath) : tmpDir
|
|
|
|
// Fetch all text documents
|
|
const docs = await ProjectEntityHandler.promises.getAllDocs(projectId)
|
|
for (const [path, doc] of Object.entries(docs)) {
|
|
const rel = path.startsWith('/') ? path.slice(1) : path
|
|
const dest = join(fileRoot, rel)
|
|
await mkdir(dirname(dest), { recursive: true })
|
|
await writeFile(dest, doc.lines.join('\n'))
|
|
}
|
|
|
|
// Fetch all binary files
|
|
const files = await ProjectEntityHandler.promises.getAllFiles(projectId)
|
|
for (const [path, file] of Object.entries(files)) {
|
|
const rel = path.startsWith('/') ? path.slice(1) : path
|
|
const dest = join(fileRoot, rel)
|
|
await mkdir(dirname(dest), { recursive: true })
|
|
const { stream } =
|
|
await HistoryManager.promises.requestBlobWithProjectId(
|
|
projectId,
|
|
file.hash
|
|
)
|
|
await pipeline(stream, createWriteStream(dest))
|
|
}
|
|
|
|
// Optionally include the compiled PDF at a configured path
|
|
if (pdfPath && pdfBuildId && pdfClsiServerId && userId) {
|
|
try {
|
|
const pdfStream = await ClsiManager.promises.getOutputFileStream(
|
|
projectId,
|
|
userId,
|
|
pdfClsiServerId,
|
|
pdfBuildId,
|
|
'output.pdf'
|
|
)
|
|
const pdfDest = join(fileRoot, pdfPath)
|
|
await mkdir(dirname(pdfDest), { recursive: true })
|
|
await pipeline(pdfStream, createWriteStream(pdfDest))
|
|
logger.debug({ projectId }, 'git sync: PDF included')
|
|
} catch (err) {
|
|
logger.warn({ err, projectId }, 'git sync: could not fetch PDF, skipping')
|
|
}
|
|
}
|
|
|
|
// Git operations — force-push a single commit so the remote always
|
|
// reflects the current Verso state exactly.
|
|
await spawnGit(['init', '-b', 'main'], tmpDir)
|
|
await spawnGit(['config', 'user.email', 'verso-sync@localhost'], tmpDir)
|
|
await spawnGit(['config', 'user.name', 'Verso Sync'], tmpDir)
|
|
await spawnGit(['add', '-A'], tmpDir)
|
|
await spawnGit(['commit', '--allow-empty', '-m', 'Verso sync'], tmpDir)
|
|
await spawnGit(['push', '--force', remoteUrl, 'HEAD:main'], tmpDir)
|
|
|
|
logger.debug({ projectId }, 'git sync: push complete')
|
|
} finally {
|
|
await rm(tmpDir, { recursive: true, force: true })
|
|
}
|
|
}
|
|
|
|
export default { setConfig, getConfig, pushToRemote }
|