Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Project support #537

Merged
merged 17 commits into from
Aug 17, 2021
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions packages/cli-lib/api/dfs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
const http = require('../http');
const fs = require('fs');

const DEVELOPER_FILE_SYSTEM_API_PATH = 'dfs/v1/projects';

/**
* Fetch projects
*
* @async
* @returns {Promise}
*/
async function fetchProjects(portalId) {
return http.get(portalId, {
uri: DEVELOPER_FILE_SYSTEM_API_PATH,
});
}
// TODO: paging?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did we want to add paging, or is this a reminder for later?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll need to add paging but I'll remove the TODO for now


/**
* Create project
*
* @async
* @param {string} name
* @returns {Promise}
*/
async function createProject(portalId, name) {
return http.post(portalId, {
uri: DEVELOPER_FILE_SYSTEM_API_PATH,
body: {
name,
},
});
}

/**
* Upload project
*
* @async
* @param {string} projectName
* @param {string} projectFile
* @returns {Promise}
*/
async function uploadProject(accountId, projectName, projectFile) {
return http.post(accountId, {
uri: `${DEVELOPER_FILE_SYSTEM_API_PATH}/upload/${projectName}`,
timeout: 60000,
formData: {
file: fs.createReadStream(projectFile),
},
});
}

/**
* Fetch project
*
* @async
* @param {string} name
* @returns {Promise}
*/
async function fetchProject(portalId, name) {
return http.get(portalId, {
uri: `${DEVELOPER_FILE_SYSTEM_API_PATH}/${name}`,
});
}

/**
* Delete project
*
* @async
* @param {string} name
* @returns {Promise}
*/
async function deleteProject(portalId, name) {
return http.delete(portalId, {
uri: `${DEVELOPER_FILE_SYSTEM_API_PATH}/${name}`,
});
}

module.exports = {
fetchProjects,
createProject,
uploadProject,
fetchProject,
deleteProject,
};
7 changes: 6 additions & 1 deletion packages/cli/commands/projects.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,21 @@ const {
addOverwriteOptions,
} = require('../lib/commonOpts');
const deploy = require('./projects/deploy');
const init = require('./projects/init');
const upload = require('./projects/upload');

exports.command = 'projects';
exports.command = 'project';
exports.describe = false; //'Commands for working with projects';

exports.builder = yargs => {
addOverwriteOptions(yargs, true);
addConfigOptions(yargs, true);
addAccountOptions(yargs, true);

// TODO: deploy must be updated
yargs.command(deploy).demandCommand(1, '');
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not currently used in the flow. Will need to be adjusted eventually, but will mostly stay the same so keeping this for now.

yargs.command(init).demandCommand(0, '');
yargs.command(upload).demandCommand(0, '');

return yargs;
};
2 changes: 1 addition & 1 deletion packages/cli/commands/projects/deploy.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ exports.builder = yargs => {

yargs.example([
[
'$0 projects deploy myProjectFolder',
'$0 project deploy myProjectFolder',
'Deploy a project within the myProjectFolder folder',
],
]);
Expand Down
118 changes: 118 additions & 0 deletions packages/cli/commands/projects/init.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
const {
addAccountOptions,
addConfigOptions,
setLogLevel,
getAccountId,
addUseEnvironmentOptions,
} = require('../../lib/commonOpts');
// const { trackCommandUsage } = require('../../lib/usageTracking');
const { logDebugInfo } = require('../../lib/debugInfo');
const {
loadConfig,
validateConfig,
checkAndWarnGitInclusion,
} = require('@hubspot/cli-lib');
const {
logApiErrorInstance,
ApiErrorContext,
} = require('@hubspot/cli-lib/errorHandlers');
const { logger } = require('@hubspot/cli-lib/logger');
const { createProject } = require('@hubspot/cli-lib/api/dfs');
const { validateAccount } = require('../../lib/validation');
const { getCwd } = require('../../../cli-lib/path');
const path = require('path');
const { getOrCreateProjectConfig } = require('../../lib/projects');

const loadAndValidateOptions = async options => {
setLogLevel(options);
logDebugInfo(options);
const { config: configPath } = options;
loadConfig(configPath, options);
checkAndWarnGitInclusion();

if (!(validateConfig() && (await validateAccount(options)))) {
process.exit(1);
}
};

exports.command = 'init [path]';
exports.describe = false;

exports.handler = async options => {
loadAndValidateOptions(options);

const { path: projectPath } = options;
const accountId = getAccountId(options);

// TODO:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we enable this now?

// trackCommandUsage('projects-init', { projectPath }, accountId);

const cwd = projectPath ? path.resolve(getCwd(), projectPath) : getCwd();
const projectConfig = await getOrCreateProjectConfig(cwd);

logger.log(`Initializing project: ${projectConfig.name}`);

try {
await createProject(accountId, projectConfig.name);

logger.success(
`"${projectConfig.name}" creation succeeded in account ${accountId}`
);
} catch (e) {
if (e.statusCode === 409) {
logger.log(
`Project ${projectConfig.name} already exists in ${accountId}.`
);
} else {
return logApiErrorInstance(
accountId,
e,
new ApiErrorContext({ accountId, projectPath })
);
}
}
logger.log('');
logger.log('> Welcome to HubSpot Developer Projects!');
logger.log('-------------------------------------------------------------');
logger.log('Getting Started');
logger.log('1. hs project upload');
logger.log(
' Upload your project files to HubSpot. Upload action adds your files to a build.'
);
logger.log();
logger.log('2. View your changes on the preview build url');
logger.log();
logger.log('Use `hs project --help` to learn more about the command.');
logger.log('-------------------------------------------------------------');
};
Comment on lines +73 to +86
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


exports.builder = yargs => {
yargs.positional('path', {
describe: 'Path to a project folder',
type: 'string',
});
// TODO:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was previously accounting for these to be passed as options. Not exactly sure how that should work with the new config file. Thinking of keeping these and getting some UX feedback.

yargs.options({
name: {
describe: 'Project name (cannot be changed)',
type: 'string',
},
srcDir: {
describe: 'Directory of project',
type: 'string',
},
});

yargs.example([
[
'$0 project init myProjectFolder',
'Initialize a project within the myProjectFolder folder',
],
]);

addConfigOptions(yargs, true);
addAccountOptions(yargs, true);
addUseEnvironmentOptions(yargs, true);

return yargs;
};
128 changes: 128 additions & 0 deletions packages/cli/commands/projects/upload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
const {
addAccountOptions,
addConfigOptions,
setLogLevel,
getAccountId,
addUseEnvironmentOptions,
} = require('../../lib/commonOpts');
// const { trackCommandUsage } = require('../../lib/usageTracking');
const { logDebugInfo } = require('../../lib/debugInfo');
const {
loadConfig,
validateConfig,
checkAndWarnGitInclusion,
} = require('@hubspot/cli-lib');
const {
logApiErrorInstance,
ApiErrorContext,
} = require('@hubspot/cli-lib/errorHandlers');
const { logger } = require('@hubspot/cli-lib/logger');
const { uploadProject } = require('@hubspot/cli-lib/api/dfs');
const { validateAccount } = require('../../lib/validation');
const fs = require('fs');
const { getCwd } = require('../../../cli-lib/path');
const path = require('path');
const archiver = require('archiver');
const tmp = require('tmp');
const {
getProjectConfig,
validateProjectConfig,
} = require('../../lib/projects');
const { shouldIgnoreFile } = require('@hubspot/cli-lib/ignoreRules');

const loadAndValidateOptions = async options => {
setLogLevel(options);
logDebugInfo(options);
const { config: configPath } = options;
loadConfig(configPath, options);
checkAndWarnGitInclusion();

if (!(validateConfig() && (await validateAccount(options)))) {
process.exit(1);
}
};

exports.command = 'upload [path]';
exports.describe = false;

const uploadProjectFiles = async (accountId, projectName, filePath) => {
logger.log(`Uploading project '${projectName}'...`);
try {
const upload = await uploadProject(accountId, projectName, filePath);
logger.log(`Project uploaded and build #${upload.buildId} created`);
} catch (err) {
if (err.statusCode === 404) {
return logger.error(
`Project '${projectName}' does not exist. Try running 'hs project init' first.`
);
}
logApiErrorInstance(err, {
context: new ApiErrorContext({
accountId,
projectName,
}),
});
}
};

exports.handler = async options => {
loadAndValidateOptions(options);

const { path: projectPath } = options;
const accountId = getAccountId(options);

// TODO:
// trackCommandUsage('projects-upload', { projectPath }, accountId);

const cwd = projectPath ? path.resolve(getCwd(), projectPath) : getCwd();
const projectConfig = await getProjectConfig(cwd);

validateProjectConfig(projectConfig);

const tempFile = tmp.fileSync({ postfix: '.zip' });

logger.log(`Compressing build files to '${tempFile.name}'`);

const output = fs.createWriteStream(tempFile.name);
const archive = archiver('zip');

output.on('close', async function() {
logger.log(`Project files compressed: ${archive.pointer()} bytes`);

await uploadProjectFiles(accountId, projectConfig.name, tempFile.name);

try {
tempFile.removeCallback();
logger.debug(`Cleaned up temporary file ${tempFile.name}`);
} catch (e) {
logger.error(e);
}
});

archive.on('error', function(err) {
throw err;
});

archive.pipe(output);

archive.directory(path.resolve(cwd, projectConfig.srcDir), false, file =>
shouldIgnoreFile(file.name) ? false : file
);

archive.finalize();
};

exports.builder = yargs => {
yargs.positional('path', {
describe: 'Path to a project folder',
type: 'string',
});

yargs.example([['$0 project upload myProjectFolder', 'Upload a project']]);

addConfigOptions(yargs, true);
addAccountOptions(yargs, true);
addUseEnvironmentOptions(yargs, true);

return yargs;
};
Loading