Skip to content

Commit

Permalink
Merge pull request #838 from zapier/zapier-pull-cmd
Browse files Browse the repository at this point in the history
feat(cli): Introduce zapier pull command
  • Loading branch information
standielpls authored Aug 16, 2024
2 parents 776d866 + 28aca73 commit 27fd7eb
Show file tree
Hide file tree
Showing 8 changed files with 235 additions and 93 deletions.
4 changes: 2 additions & 2 deletions packages/cli/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const BUILD_DIR = 'build';
const DEFINITION_PATH = `${BUILD_DIR}/definition.json`;
const BUILD_PATH = `${BUILD_DIR}/build.zip`;
const SOURCE_PATH = `${BUILD_DIR}/source.zip`;
const BLACKLISTED_PATHS = [
const BLOCKLISTED_PATHS = [
// Will be excluded from build.zip and source.zip
'.git',
'.env',
Expand Down Expand Up @@ -91,7 +91,7 @@ module.exports = {
AUTH_LOCATION,
AUTH_LOCATION_RAW,
BASE_ENDPOINT,
BLACKLISTED_PATHS,
BLOCKLISTED_PATHS,
BUILD_DIR,
BUILD_PATH,
CHECK_REF_DOC_LINK,
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/generators/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const prettier = require('gulp-prettier');

const { PACKAGE_VERSION, PLATFORM_PACKAGE } = require('../constants');
const authFilesCodegen = require('../utils/auth-files-codegen');
const { PullGenerator } = require('./pull');

const writeGenericReadme = (gen) => {
gen.fs.copyTpl(
Expand Down Expand Up @@ -207,5 +208,6 @@ class ProjectGenerator extends Generator {

module.exports = {
TEMPLATE_CHOICES,
PullGenerator,
ProjectGenerator,
};
49 changes: 49 additions & 0 deletions packages/cli/src/generators/pull.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const colors = require('colors/safe');
const debug = require('debug')('zapier:pull');
const inquirer = require('inquirer');
const path = require('path');
const Generator = require('yeoman-generator');

const maybeOverwriteFiles = async (gen) => {
const dstDir = gen.options.dstDir;
const srcDir = gen.options.srcDir;
for (const file of gen.options.sourceFiles) {
gen.fs.copy(path.join(srcDir, file), path.join(dstDir, file), gen.options);
}
};

module.exports = class PullGenerator extends Generator {
initializing() {
debug('SRC', this.options.sourceFiles);
}

prompting() {
const prompts = [
{
type: 'confirm',
name: 'confirm',
message: `Warning: You are about to overwrite existing files.
Before proceeding, please make sure you have saved your work. Consider creating a backup or saving your current state in a git branch. During the process, you may abort anytime by pressing 'x'.
Do you want to continue?`,
default: false,
},
];

return inquirer.prompt(prompts).then((answers) => {
if (!answers.confirm) {
this.log(colors.green('zapier pull cancelled'));
process.exit(1);
}
});
}

writing() {
maybeOverwriteFiles(this);
}

end() {
this.log(colors.green('zapier pull completed successfully'));
}
};
52 changes: 52 additions & 0 deletions packages/cli/src/oclif/commands/pull.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
const AdmZip = require('adm-zip');
const { ensureFileSync } = require('fs-extra');
const path = require('path');
const yeoman = require('yeoman-environment');

const ZapierBaseCommand = require('../ZapierBaseCommand');
const { downloadSourceZip } = require('../../utils/api');
const { ensureDir, makeTempDir, removeDirSync } = require('../../utils/files');
const { listFiles } = require('../../utils/build');
const { buildFlags } = require('../buildFlags');
const PullGenerator = require('../../generators/pull');

class PullCommand extends ZapierBaseCommand {
async perform() {
// Fetch the source zip from API
const tmpDir = makeTempDir();
const srcZipDst = path.join(tmpDir, 'download', 'source.zip');

try {
ensureFileSync(srcZipDst);
await downloadSourceZip(srcZipDst);

// Write source zip to tmp dir
const srcDst = path.join(tmpDir, 'source');
await ensureDir(srcDst);
const zip = new AdmZip(srcZipDst);
zip.extractAllTo(srcDst, true);

// Prompt user to confirm overwrite
const currentDir = process.cwd();
const sourceFiles = await listFiles(srcDst);

const env = yeoman.createEnv();
const namespace = 'zapier:pull';
env.registerStub(PullGenerator, namespace);
await env.run(namespace, {
sourceFiles,
srcDir: srcDst,
dstDir: currentDir,
});
} finally {
removeDirSync(tmpDir);
}
}
}

PullCommand.flags = buildFlags();
PullCommand.description =
'Pull the source code of the latest version of your integration from Zapier, overwriting your local integration files.';
PullCommand.skipValidInstallCheck = true;

module.exports = PullCommand;
2 changes: 1 addition & 1 deletion packages/cli/src/tests/utils/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ describe('build (runs slowly)', function () {
});
});

it('list should not include blacklisted files', () => {
it('list should not include blocklisted files', () => {
const tmpProjectDir = getNewTempDirPath();

[
Expand Down
154 changes: 89 additions & 65 deletions packages/cli/src/utils/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const _ = require('lodash');
const colors = require('colors/safe');
const debug = require('debug')('zapier:api');
const { pipeline } = require('stream/promises');

const constants = require('../constants');

Expand Down Expand Up @@ -48,11 +49,12 @@ const readCredentials = (explodeIfMissing = true) => {
};

// Calls the underlying platform REST API with proper authentication.
const callAPI = (
const callAPI = async (
route,
options,
rawError = false,
credentialsRequired = true
credentialsRequired = true,
returnStreamBody = false
) => {
// temp manual enable while we're not all the way moved over
if (_.get(global, ['argOpts', 'debug'])) {
Expand All @@ -64,7 +66,6 @@ const callAPI = (

const requestOptions = {
method: options.method || 'GET',
url,
body: options.body ? JSON.stringify(options.body) : null,
headers: {
Accept: 'application/json',
Expand All @@ -73,73 +74,65 @@ const callAPI = (
'X-Requested-With': 'XMLHttpRequest',
},
};
return Promise.resolve(requestOptions)
.then((_requestOptions) => {
// requestOptions === _requestOptions side step for linting
if (options.skipDeployKey) {
return _requestOptions;

if (!options.skipDeployKey) {
const credentials = await readCredentials(credentialsRequired);
requestOptions.headers['X-Deploy-Key'] = credentials[constants.AUTH_KEY];
}

const res = await fetch(url, requestOptions);

let errorMessage = '';
let text = '';
const hitError = res.status >= 400;
if (hitError) {
try {
text = await res.text();
errorMessage = JSON.parse(text).errors.join(', ');
} catch (err) {
console.log('text', text);
errorMessage = (text || 'Unknown error').slice(0, 250);
}
}

debug(`>> ${requestOptions.method} ${requestOptions.url}`);

if (requestOptions.body) {
const replacementStr = 'raw zip removed in logs';
const requestBody = JSON.parse(requestOptions.body);
const cleanedBody = {};
for (const k in requestBody) {
if (k.includes('zip_file')) {
cleanedBody[k] = replacementStr;
} else {
return readCredentials(credentialsRequired).then((credentials) => {
_requestOptions.headers['X-Deploy-Key'] =
credentials[constants.AUTH_KEY];
return _requestOptions;
});
}
})
.then((_requestOptions) => {
return fetch(_requestOptions.url, _requestOptions);
})
.then((res) => {
return Promise.all([res, res.text()]);
})
.then(([res, text]) => {
let errors;
const hitError = res.status >= 400;
if (hitError) {
try {
errors = JSON.parse(text).errors.join(', ');
} catch (err) {
errors = (text || 'Unknown error').slice(0, 250);
}
cleanedBody[k] = requestBody[k];
}
}
debug(`>> ${JSON.stringify(cleanedBody)}`);
}

debug(`>> ${requestOptions.method} ${requestOptions.url}`);
if (requestOptions.body) {
const replacementStr = 'raw zip removed in logs';
const requestBody = JSON.parse(requestOptions.body);
const cleanedBody = {};
for (const k in requestBody) {
if (k.includes('zip_file')) {
cleanedBody[k] = replacementStr;
} else {
cleanedBody[k] = requestBody[k];
}
}
debug(`>> ${JSON.stringify(cleanedBody)}`);
}
debug(`<< ${res.status}`);
debug(`<< ${(text || '').substring(0, 2500)}`);
debug('------------'); // to help differentiate request from each other

if (hitError) {
const niceMessage = `"${requestOptions.url}" returned "${res.status}" saying "${errors}"`;

if (rawError) {
res.text = text;
try {
res.json = JSON.parse(text);
} catch (e) {
res.json = {};
}
res.errText = niceMessage;
return Promise.reject(res);
} else {
throw new Error(niceMessage);
}
debug(`<< ${res.status}`);
debug(`<< ${(text || '').substring(0, 2500)}`);
debug('------------'); // to help differentiate request from each other

if (hitError) {
const niceMessage = `"${url}" returned "${res.status}" saying "${errorMessage}"`;

if (rawError) {
res.text = text;
try {
res.json = JSON.parse(text);
} catch (e) {
res.json = {};
}
res.errText = niceMessage;
throw res;
} else {
throw new Error(niceMessage);
}
}

return JSON.parse(text);
});
return returnStreamBody ? res.body : res.json();
};

// Given a valid username and password - create a new deploy key.
Expand Down Expand Up @@ -364,6 +357,36 @@ const validateApp = async (definition) => {
});
};

const downloadSourceZip = async (dst) => {
const linkedAppConfig = await getLinkedAppConfig(undefined, false);
if (!linkedAppConfig.id) {
throw new Error(
`This project hasn't yet been associated with an existing Zapier integration.\n\nIf it's a brand new integration, run \`${colors.cyan(
'zapier register'
)}\`.\n\nIf this project already exists in your Zapier account, run \`${colors.cyan(
'zapier link'
)}\` instead.`
);
}

const url = `/apps/${linkedAppConfig.id}/latest/pull`;

try {
const resBody = await callAPI(url, undefined, true, true, true);

const writeStream = fs.createWriteStream(dst);

startSpinner('Downloading most recent source.zip file...');

// use pipeline to handle the download stream
await pipeline(resBody, writeStream);
} catch (err) {
throw new Error(`Failed to download source.zip: ${err.errText}`);
} finally {
endSpinner();
}
};

const upload = async (app, { skipValidation = false } = {}) => {
const zipPath = constants.BUILD_PATH;
const sourceZipPath = constants.SOURCE_PATH;
Expand Down Expand Up @@ -408,6 +431,7 @@ module.exports = {
callAPI,
checkCredentials,
createCredentials,
downloadSourceZip,
getLinkedAppConfig,
getWritableApp,
getVersionInfo,
Expand Down
Loading

0 comments on commit 27fd7eb

Please sign in to comment.