@flowfuse/flowfuse
Version:
An open source low-code development platform
191 lines (172 loc) • 7.89 kB
JavaScript
const { exec } = require('node:child_process')
const { existsSync } = require('node:fs')
const fs = require('node:fs/promises')
const os = require('node:os')
const path = require('node:path')
const { promisify } = require('node:util')
const execPromised = promisify(exec)
const axios = require('axios')
const { encryptValue, decryptValue } = require('../../../../db/utils')
const { cloneRepository } = require('./utils')
module.exports.init = async function (app) {
/**
* Push a snapshot to a git repository
* @param {Object} repoOptions
* @param {String} repoOptions.token
* @param {String} repoOptions.url
* @param {String} repoOptions.branch
* @param {Object} snapshot
* @param {Object} options
* @param {Object} options.sourceObject what produced the snapshot
* @param {Object} options.user who triggered the pipeline
* @param {Object} options.pipeline details of the pipeline
*/
async function pushToRepository (repoOptions, snapshot, options) {
let workingDir
try {
const token = repoOptions.token
const branch = repoOptions.branch || 'main'
if (!/^https:\/\/github.com/i.test(repoOptions.url)) {
throw new Error('Only GitHub repositories are supported')
}
const url = new URL(repoOptions.url)
url.username = 'x-access-token'
url.password = token
// 2. get user details so we can properly attribute the commit
let userDetails
try {
userDetails = await axios.get('https://api.github.com/user', {
headers: {
Accept: 'application/vnd.github+json',
Authorization: `Bearer ${token}`,
'X-GitHub-Api-Version': '2022-11-28'
}
})
} catch (err) {
const result = new Error('Invalid git token')
result.code = 'invalid_token'
result.cause = err
throw result
}
const userGitName = userDetails.data.login
const userGitEmail = `${userDetails.data.id}+${userDetails.data.login}@users.noreply.github.com`
const author = `${userGitName} <${userGitEmail}>`.replace(/"/g, '\\"')
workingDir = await fs.mkdtemp(path.join(os.tmpdir(), 'flowfuse-git-repo-'))
// 3. clone repo
await cloneRepository(url, branch, workingDir)
// 4. set username/email
await execPromised('git config user.email "no-reply@flowfuse.com"', { cwd: workingDir })
await execPromised('git config user.name "FlowFuse"', { cwd: workingDir })
// For local dev - disable gpg signing in case its set in global config
await execPromised('git config commit.gpgsign false', { cwd: workingDir })
// 5. export snapshot
const exportOptions = {
credentialSecret: repoOptions.credentialSecret,
components: {
flows: true,
credentials: true
}
}
const result = await app.db.controllers.Snapshot.exportSnapshot(snapshot, exportOptions)
const snapshotExport = app.db.views.ProjectSnapshot.snapshotExport(result)
if (snapshotExport.settings?.settings?.palette?.npmrc) {
const enc = encryptValue(repoOptions.credentialSecret, snapshotExport.settings.settings?.palette?.npmrc)
snapshotExport.settings.settings.palette.npmrc = { $: enc }
}
const snapshotFile = path.join(workingDir, repoOptions.path || 'snapshot.json').replace(/"/g, '')
await fs.writeFile(snapshotFile, JSON.stringify(snapshotExport, null, 4))
// 6. stage file
await execPromised(`git add "${snapshotFile}"`, { cwd: workingDir })
// 7. commit
await execPromised(`git commit -m "Update snapshot\n\nSnapshot updated by FlowFuse Pipeline '${options.pipeline.name.replace(/"/g, '')}', triggered by ${options.user.username.replace(/"/g, '')}" --author="${author}"`, { cwd: workingDir })
try {
// 8. push
await execPromised('git push', { cwd: workingDir })
} catch (err) {
const output = err.stdout + err.stderr
if (/unable to access/.test(output)) {
const result = new Error('Permission denied')
result.code = 'invalid_token'
result.cause = err
throw result
}
let error
const m = /fatal: (.*)/.exec(output)
if (m) {
error = new Error('Failed to push repository: ' + m[1])
} else {
error = Error('Failed to push repository')
}
error.cause = err
throw error
}
} finally {
if (workingDir) {
try {
await fs.rm(workingDir, { recursive: true, force: true })
} catch (err) {}
}
}
}
/**
* Push a snapshot to a git repository
* @param {Object} repoOptions
* @param {String} repoOptions.token
* @param {String} repoOptions.url
* @param {String} repoOptions.branch
*/
async function pullFromRepository (repoOptions) {
let workingDir
try {
const token = repoOptions.token
const branch = repoOptions.branch || 'main'
if (!/^https:\/\/github.com/i.test(repoOptions.url)) {
throw new Error('Only GitHub repositories are supported')
}
const url = new URL(repoOptions.url)
url.username = 'x-access-token'
url.password = token
workingDir = await fs.mkdtemp(path.join(os.tmpdir(), 'flowfuse-git-repo-'))
// 3. clone repo
await cloneRepository(url, branch, workingDir)
const snapshotFile = path.join(workingDir, repoOptions.path || 'snapshot.json').replace(/"/g, '')
if (!existsSync(snapshotFile)) {
throw new Error('Snapshot file not found in repository')
}
try {
const snapshotContent = await fs.readFile(snapshotFile, 'utf8')
const snapshot = JSON.parse(snapshotContent)
if (snapshot.settings?.env) {
const keys = Object.keys(snapshot.settings.env)
keys.forEach((key) => {
const env = snapshot.settings.env[key]
if (env.hidden && env.$) {
// Decrypt the value if it is encrypted
env.value = decryptValue(repoOptions.credentialSecret, env.$)
delete env.$
}
})
}
if (snapshot.settings?.settings?.palette?.npmrc) {
const npmrc = snapshot.settings.settings.palette.npmrc
if (typeof npmrc === 'object' && npmrc.$) {
snapshot.settings.settings.palette.npmrc = decryptValue(repoOptions.credentialSecret, npmrc.$)
}
}
return snapshot
} catch (err) {
throw new Error('Failed to read snapshot file: ' + err.message)
}
} finally {
if (workingDir) {
try {
await fs.rm(workingDir, { recursive: true, force: true })
} catch (err) {}
}
}
}
return {
pushToRepository,
pullFromRepository
}
}