UNPKG

@flowfuse/flowfuse

Version:

An open source low-code development platform

191 lines (172 loc) • 7.89 kB
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 } }