-
Notifications
You must be signed in to change notification settings - Fork 2
/
index.js
317 lines (284 loc) · 9.68 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
const Heroku = require('heroku-client');
const core = require('@actions/core');
const github = require('@actions/github');
const VALID_EVENT = 'pull_request';
async function run() {
try {
const githubToken = core.getInput('github_token', { required: true });
const prLabel = core.getInput('github_label', {
required: false,
default: 'Review App',
});
const shouldCommentPR = core.getInput('should_comment_pull_request', {
required: false,
default: false,
});
const herokuApiToken = core.getInput('heroku_api_token', {
required: true,
});
const herokuPipelineId = core.getInput('heroku_pipeline_id', {
required: true,
});
const octokit = new github.getOctokit(githubToken);
const heroku = new Heroku({ token: herokuApiToken });
const {
action,
eventName,
payload: {
pull_request: {
head: {
ref: branch,
sha: version,
repo: {
id: repoId,
fork: forkRepo,
html_url: repoHtmlUrl,
},
},
number: prNumber,
// updated_at: prUpdatedAtRaw,
},
},
issue: {
number: issueNumber,
},
repo,
} = github.context;
const {
owner: repoOwner,
} = repo;
if (eventName !== VALID_EVENT) {
throw new Error(`Unexpected github event trigger: ${eventName}`);
}
// const prUpdatedAt = DateTime.fromISO(prUpdatedAtRaw);
const sourceUrl = `${repoHtmlUrl}/tarball/${version}`;
const forkRepoId = forkRepo ? repoId : undefined;
const getAppDetails = async (id) => {
const url = `/apps/${id}`;
core.debug(`Getting app details for app ID ${id} (${url})`);
const appDetails = await heroku.get(url);
core.info(`Got app details for app ID ${id} OK: ${JSON.stringify(appDetails)}`);
return appDetails;
};
const outputAppDetails = (app) => {
core.startGroup('Output app details');
const {
id: appId,
web_url: webUrl,
} = app;
core.info(`Review app ID: "${appId}"`);
core.setOutput('app_id', appId);
core.info(`Review app Web URL: "${webUrl}"`);
core.setOutput('app_web_url', webUrl);
core.endGroup();
};
const findReviewApp = async () => {
const apiUrl = `/pipelines/${herokuPipelineId}/review-apps`;
core.debug(`Listing review apps: "${apiUrl}"`);
const reviewApps = await heroku.get(apiUrl);
core.info(`Listed ${reviewApps.length} review apps OK: ${reviewApps.length} apps found.`);
core.debug(`Finding review app for PR #${prNumber}...`);
const app = reviewApps.find(app => app.pr_number === prNumber);
if (app) {
const { status } = app;
if ('errored' === status) {
core.notice(`Found review app for PR #${prNumber} OK, but status is "${status}"`);
return null;
}
core.info(`Found review app for PR #${prNumber} OK: ${JSON.stringify(app)}`);
} else {
core.info(`No review app found for PR #${prNumber}`);
}
return app;
};
const waitReviewAppUpdated = async () => {
core.startGroup('Ensure review app is up to date');
const waitSeconds = secs => new Promise(resolve => setTimeout(resolve, secs * 1000));
const checkBuildStatusForReviewApp = async (app) => {
core.debug(`Checking build status for app: ${JSON.stringify(app)}`);
if ('pending' === app.status || 'creating' === app.status) {
return false;
}
if ('deleting' === app.status) {
throw new Error(`Unexpected app status: "${app.status}" - ${app.message} (error status: ${app.error_status})`);
}
if (!app.app) {
throw new Error(`Unexpected app status: "${app.status}"`);
}
const {
app: {
id: appId,
},
status,
error_status: errorStatus,
} = app;
core.debug(`Fetching latest builds for app ${appId}...`);
const latestBuilds = await heroku.get(`/apps/${appId}/builds`);
core.debug(`Fetched latest builds for pipeline ${appId} OK: ${latestBuilds.length} builds found.`);
core.debug(`Finding build matching version ${version}...`);
const build = await latestBuilds.find(build => version === build.source_blob.version);
if (!build) {
core.error(`Could not find build matching version ${version}.`);
core.setFailed(`No existing build for app ID ${appId} matches version ${version}`);
throw new Error(`Unexpected build status: "${status}" yet no matching build found`);
}
core.info(`Found build matching version ${version} OK: ${JSON.stringify(build)}`);
switch (build.status) {
case 'succeeded':
return true;
case 'pending':
return false;
default:
throw new Error(`Unexpected build status: "${status}": ${errorStatus || 'no error provided'}`);
}
};
let reviewApp;
let isFinished;
do {
reviewApp = await findReviewApp();
isFinished = await checkBuildStatusForReviewApp(reviewApp);
await waitSeconds(5);
} while (!isFinished);
core.endGroup();
return getAppDetails(reviewApp.app.id);
};
const createReviewApp = async () => {
try {
core.startGroup('Create review app');
const archiveBody = {
owner: repoOwner,
repo: repo.repo,
ref: version,
};
core.debug(`Fetching archive: ${JSON.stringify(archiveBody)}`);
const { url: archiveUrl } = await octokit.rest.repos.downloadTarballArchive(archiveBody);
core.info(`Fetched archive OK: ${JSON.stringify(archiveUrl)}`);
const body = {
branch,
pipeline: herokuPipelineId,
source_blob: {
url: archiveUrl,
version,
},
fork_repo_id: forkRepoId,
pr_number: prNumber,
environment: {
GIT_REPO_URL: repoHtmlUrl,
},
};
core.debug(`Creating heroku review app: ${JSON.stringify(body)}`);
const app = await heroku.post('/review-apps', { body });
core.info('Created review app OK:', app);
core.endGroup();
return app;
} catch (err) {
// 409 indicates duplicate; anything else is unexpected
if (err.statusCode !== 409) {
throw err;
}
// possibly build kicked off after this PR action began running
core.warning('Review app now seems to exist after previously not...');
core.endGroup();
// just some sanity checking
const app = await findReviewApp();
if (!app) {
throw new Error('Previously got status 409 but no app found');
}
return app;
}
};
core.debug(`Deploy info: ${JSON.stringify({
branch,
version,
repoId,
forkRepo,
forkRepoId,
repoHtmlUrl,
prNumber,
issueNumber,
repoOwner,
sourceUrl,
})}`);
if (forkRepo) {
core.notice('No secrets are available for PRs in forked repos.');
return;
}
if ('labeled' === action) {
core.startGroup('PR labelled');
core.debug('Checking PR label...');
const {
payload: {
label: {
name: newLabelAddedName,
},
},
} = github.context;
if (newLabelAddedName === prLabel) {
core.info(`Checked PR label: "${newLabelAddedName}", so need to create review app...`);
await createReviewApp();
const app = await waitReviewAppUpdated();
outputAppDetails(app);
} else {
core.info('Checked PR label OK: "${newLabelAddedName}", no action required.');
}
core.endGroup();
return;
}
// Only people that can close PRs are maintainers or the author
// hence can safely delete review app without being collaborator
if ('closed' === action) {
core.debug('PR closed, deleting review app...');
const app = await findReviewApp();
if (app) {
await heroku.delete(`/review-apps/${app.id}`);
core.info('PR closed, deleted review app OK');
core.endGroup();
} else {
core.error(`Could not find review app for PR #${prNumber}`);
core.setFailed(`Action "closed", yet no existing review app for PR #${prNumber}`);
}
return;
}
// TODO: ensure we have permission
// const perms = await tools.github.repos.getCollaboratorPermissionLevel({
// ...tools.context.repo,
// username: tools.context.actor,
// });
const app = await findReviewApp();
if (!app) {
await createReviewApp();
}
const updatedApp = await waitReviewAppUpdated();
outputAppDetails(updatedApp);
if (prLabel) {
core.startGroup('Label PR');
core.debug(`Adding label "${prLabel}" to PR...`);
await octokit.rest.issues.addLabels({
...repo,
labels: [prLabel],
issue_number: prNumber,
});
core.info(`Added label "${prLabel}" to PR... OK`);
core.endGroup();
} else {
core.debug('No label specified; will not label PR');
}
if (shouldCommentPR) {
core.startGroup('Comment on PR');
core.debug('Adding comment to PR...');
await octokit.rest.issues.createComment({
...repo,
issue_number: prNumber,
body: `Review app deployed to ${updatedApp.web_url}`,
});
core.info('Added comment to PR... OK');
core.endGroup();
} else {
core.debug('should_comment_pull_request is not set; will not comment on PR');
}
} catch (err) {
core.error(err);
core.setFailed(err.message);
}
}
run();