Skip to content

Commit 62d30ec

Browse files
authored
Create a very simple CLI wrapper (#2)
* Move heavy lifting parts into a dedicated module That way, you should be able to reuse most of the action-code, but replace the GitHub Actions specific part with other wrappers. For example, a simple CLI command could be used to trigger deployments as well. * Add a very simple CLI wrapper to trigger deployments * Add a package version * Catch missing appspec.yml files early on * Pass the "core" module * Avoid querying for parameters that should have sane defaults * Improve exception handling * Remove commented-out code, bump dependencies
1 parent 38eda93 commit 62d30ec

File tree

7 files changed

+39462
-39497
lines changed

7 files changed

+39462
-39497
lines changed

Diff for: README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ The IAM User that is used to run the action requires the following IAM permissio
165165
As a note to my future self, in order to work on this repo:
166166

167167
* Clone it
168-
* Run `npm install` to fetch dependencies
168+
* Run `yarn install` to fetch dependencies
169169
* _hack hack hack_
170170
* Run `npm run build` to update `dist/*`, which holds the files actually run
171171
* Read https://help.github.com/en/articles/creating-a-javascript-action if unsure.

Diff for: cli.js

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
'use strict';
2+
3+
(async function () {
4+
var core = require('@actions/core');
5+
core.setOutput = function () {};
6+
core.setFailed = function (message) {
7+
console.log(message instanceof Error ? message.toString() : message);
8+
process.exitCode = 1;
9+
}
10+
11+
const fs = require('fs');
12+
if (!fs.existsSync('./appspec.yml')) {
13+
core.setFailed("❓ ./appspec.yml does not exist. Make sure you are in the project's top level directory.");
14+
process.exit();
15+
}
16+
17+
const simpleGit = require('simple-git');
18+
const git = simpleGit();
19+
var branchName, commitId;
20+
21+
try {
22+
await git.init();
23+
const remotes = await git.getRemotes(true);
24+
var applicationName, fullRepositoryName;
25+
26+
for (const remote of remotes) {
27+
if (remote.name !== 'origin') {
28+
continue;
29+
}
30+
31+
const url = remote.refs.push;
32+
33+
var matches
34+
35+
if (matches = url.match(/git@github.com:([a-z0-9_-]+)\/([a-z0-9_-]+).git/)) {
36+
applicationName = matches[2];
37+
fullRepositoryName = `${matches[1]}/${matches[2]}`;
38+
}
39+
}
40+
41+
branchName = await git.revparse(['--abbrev-ref', 'HEAD']);
42+
commitId = await git.revparse(['HEAD']);
43+
} catch (e) {
44+
core.setFailed('🌩 Failed to parse git information. Are you sure this is a git repo?')
45+
process.exit();
46+
}
47+
48+
if (!applicationName || !fullRepositoryName) {
49+
core.setFailed("❓ Unable to parse GitHub repository name from the 'origin' remote.");
50+
process.exit();
51+
}
52+
53+
console.log("🚂 OK, let's ship this...");
54+
console.log(`GitHub 💎 repository '${fullRepositoryName}'`);
55+
console.log(`Branch 🎋 ${branchName}`);
56+
console.log(`Commit ⚙️ ${commitId}`);
57+
58+
const prompt = require('prompt');
59+
60+
prompt.message = '';
61+
prompt.start();
62+
63+
try {
64+
await prompt.get({
65+
properties: {
66+
confirm: {
67+
name: 'yes',
68+
message: 'Type "yes" to create deployment',
69+
validator: /yes/,
70+
warning: 'Must respond yes to continue',
71+
default: ''
72+
}
73+
}
74+
});
75+
} catch (e) {
76+
core.setFailed('🙈 Aborted.');
77+
process.exit();
78+
}
79+
80+
const action = require('./create-deployment');
81+
try {
82+
await action.createDeployment(applicationName, fullRepositoryName, branchName, commitId, core);
83+
} catch (e) {
84+
console.log(`👉🏻 ${e.message}`);
85+
process.exit(1);
86+
}
87+
})();

Diff for: create-deployment.js

+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
'use strict';
2+
3+
function fetchBranchConfig(branchName) {
4+
const fs = require('fs');
5+
const yaml = require('js-yaml');
6+
7+
let fileContents = fs.readFileSync('./appspec.yml', 'utf8');
8+
let data = yaml.safeLoad(fileContents);
9+
10+
for (var prop in data.branch_config) {
11+
var regex = new RegExp('^' + prop + '$', 'i');
12+
if (branchName.match(regex)) {
13+
if (data.branch_config[prop] == null) {
14+
console.log(`🤷🏻‍♂️ Found an empty appspec.yml -> branch_config for '${branchName}' – skipping deployment`);
15+
process.exit();
16+
}
17+
console.log(`💡 Using appspec.yml -> branch_config '${prop}' for branch '${branchName}'`);
18+
return data.branch_config[prop];
19+
}
20+
}
21+
22+
console.log(`❓ Found no matching appspec.yml -> branch_config for '${branchName}' – skipping deployment`);
23+
process.exit();
24+
}
25+
26+
exports.createDeployment = async function(applicationName, fullRepositoryName, branchName, commitId, core) {
27+
const branchConfig = fetchBranchConfig(branchName);
28+
const safeBranchName = branchName.replace(/[^a-z0-9-/]+/gi, '-').replace(/\/+/, '--');
29+
const deploymentGroupName = branchConfig.deploymentGroupName ? branchConfig.deploymentGroupName.replace('$BRANCH', safeBranchName) : safeBranchName;
30+
const deploymentGroupConfig = branchConfig.deploymentGroupConfig;
31+
const deploymentConfig = branchConfig.deploymentConfig;
32+
33+
console.log(`🎳 Using deployment group '${deploymentGroupName}'`);
34+
35+
const client = require('aws-sdk/clients/codedeploy');
36+
const codeDeploy = new client();
37+
38+
try {
39+
await codeDeploy.updateDeploymentGroup({
40+
...deploymentGroupConfig,
41+
...{
42+
applicationName: applicationName,
43+
currentDeploymentGroupName: deploymentGroupName
44+
}
45+
}).promise();
46+
console.log(`⚙️ Updated deployment group '${deploymentGroupName}'`);
47+
core.setOutput('deploymentGroupCreated', false);
48+
} catch (e) {
49+
if (e.code == 'DeploymentGroupDoesNotExistException') {
50+
await codeDeploy.createDeploymentGroup({
51+
...deploymentGroupConfig,
52+
...{
53+
applicationName: applicationName,
54+
deploymentGroupName: deploymentGroupName,
55+
}
56+
}).promise();
57+
console.log(`🎯 Created deployment group '${deploymentGroupName}'`);
58+
core.setOutput('deploymentGroupCreated', true);
59+
} else {
60+
core.setFailed(`🌩 Unhandled exception`);
61+
throw e;
62+
}
63+
}
64+
65+
let tries = 0;
66+
while (true) {
67+
68+
if (++tries > 5) {
69+
core.setFailed('🤥 Unable to create a new deployment (too much concurrency?)');
70+
return;
71+
}
72+
73+
try {
74+
var {deploymentId: deploymentId} = await codeDeploy.createDeployment({
75+
...deploymentConfig,
76+
...{
77+
applicationName: applicationName,
78+
deploymentGroupName: deploymentGroupName,
79+
revision: {
80+
revisionType: 'GitHub',
81+
gitHubLocation: {
82+
commitId: commitId,
83+
repository: fullRepositoryName
84+
}
85+
}
86+
}
87+
}).promise();
88+
console.log(`🚚️ Created deployment ${deploymentId} – https://console.aws.amazon.com/codesuite/codedeploy/deployments/${deploymentId}?region=${codeDeploy.config.region}`);
89+
core.setOutput('deploymentId', deploymentId);
90+
core.setOutput('deploymentGroupName', deploymentGroupName);
91+
break;
92+
} catch (e) {
93+
if (e.code == 'DeploymentLimitExceededException') {
94+
var [, otherDeployment] = e.message.toString().match(/is already deploying deployment \'(d-\w+)\'/);
95+
console.log(`😶 Waiting for another pending deployment ${otherDeployment}`);
96+
try {
97+
await codeDeploy.waitFor('deploymentSuccessful', {deploymentId: otherDeployment}).promise();
98+
console.log(`🙂 The pending deployment ${otherDeployment} sucessfully finished.`);
99+
} catch (e) {
100+
console.log(`🤔 The other pending deployment ${otherDeployment} seems to have failed.`);
101+
}
102+
continue;
103+
} else {
104+
core.setFailed(`🌩 Unhandled exception`);
105+
throw e;
106+
}
107+
}
108+
}
109+
110+
console.log(`⏲ Waiting for deployment ${deploymentId} to finish`);
111+
112+
try {
113+
await codeDeploy.waitFor('deploymentSuccessful', {deploymentId: deploymentId}).promise();
114+
console.log('🥳 Deployment successful');
115+
} catch (e) {
116+
core.setFailed(`😱 The deployment ${deploymentId} seems to have failed.`);
117+
throw e;
118+
}
119+
}

0 commit comments

Comments
 (0)