heroku
Version:
CLI to interact with Heroku
168 lines (167 loc) • 8.56 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
const color_1 = require("@heroku-cli/color");
const command_1 = require("@heroku-cli/command");
const core_1 = require("@oclif/core");
const heroku_cli_util_1 = require("@heroku/heroku-cli-util");
const http_call_1 = require("@heroku/http-call");
const api_1 = require("../../lib/api");
const kolkrabbi_api_1 = require("../../lib/pipelines/kolkrabbi-api");
const generation_1 = require("../../lib/apps/generation");
const PROMOTION_ORDER = ['development', 'staging', 'production'];
async function diff(targetApp, downstreamApp, githubToken, herokuUserAgent) {
if (!downstreamApp.repo) {
return core_1.ux.log(`\n${color_1.default.app(targetApp.name)} was not compared to ${color_1.default.app(downstreamApp.name)} as ${color_1.default.app(downstreamApp.name)} is not connected to GitHub`);
}
if (downstreamApp.repo !== targetApp.repo) {
return core_1.ux.log(`\n${color_1.default.app(targetApp.name)} was not compared to ${color_1.default.app(downstreamApp.name)} as ${color_1.default.app(downstreamApp.name)} is not connected to the same GitHub repo as ${color_1.default.app(targetApp.name)}`);
}
if (!downstreamApp.hash) {
return core_1.ux.log(`\n${color_1.default.app(targetApp.name)} was not compared to ${color_1.default.app(downstreamApp.name)} as ${color_1.default.app(downstreamApp.name)} does not have any releases`);
}
if (downstreamApp.hash === targetApp.hash) {
return core_1.ux.log(`\n${color_1.default.app(targetApp.name)} is up to date with ${color_1.default.app(downstreamApp.name)}`);
}
// Do the actual GitHub diff
try {
const path = `${targetApp.repo}/compare/${downstreamApp.hash}...${targetApp.hash}`;
const headers = {
authorization: 'token ' + githubToken,
'Content-Type': 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
};
if (herokuUserAgent) {
Reflect.set(headers, 'user-agent', herokuUserAgent);
}
const { body: githubDiff } = await http_call_1.default.get(`https://api.github.com/repos/${path}`, { headers });
core_1.ux.log('');
heroku_cli_util_1.hux.styledHeader(`${color_1.default.app(targetApp.name)} is ahead of ${color_1.default.app(downstreamApp.name)} by ${githubDiff.ahead_by} commit${githubDiff.ahead_by === 1 ? '' : 's'}`);
const mapped = githubDiff.commits.map((commit) => {
return {
sha: commit.sha.slice(0, 7),
date: commit.commit.author.date,
author: commit.commit.author.name,
message: commit.commit.message.split('\n')[0],
};
}).reverse();
heroku_cli_util_1.hux.table(mapped, {
sha: {
header: 'SHA',
},
date: {},
author: {},
message: {},
});
core_1.ux.log(`\nhttps://github.com/${path}`);
}
catch (_a) {
core_1.ux.log(`\n${color_1.default.app(targetApp.name)} was not compared to ${color_1.default.app(downstreamApp.name)} because we were unable to perform a diff`);
core_1.ux.log('are you sure you have pushed your latest commits to GitHub?');
}
}
class PipelinesDiff extends command_1.Command {
constructor() {
super(...arguments);
this.kolkrabbi = new kolkrabbi_api_1.default(this.config.userAgent, () => this.heroku.auth);
this.getAppInfo = async (appName, appId, generation) => {
var _a;
// Find GitHub connection for the app
const githubApp = await this.kolkrabbi.getAppLink(appId)
.catch(() => {
return { name: appName, repo: null, hash: null };
});
// Find the commit hash of the latest release for this app
let slug;
let ociImages;
let commit;
try {
const { body: releases } = await (0, api_1.getReleases)(this.heroku, appId);
const release = releases.find(r => r.status === 'succeeded');
if (!release || !(release.slug || release.oci_image)) {
throw new Error(`no release found for ${appName}`);
}
if (generation === 'cedar' && release.slug) {
slug = await this.heroku.get(`/apps/${appId}/slugs/${release.slug.id}`, {
headers: { Accept: api_1.SDK_HEADER },
}).then(res => res.body);
commit = slug.commit;
}
else if (generation === 'fir' && release.oci_image) {
ociImages = await this.heroku.get(`/apps/${appId}/oci-images/${release.oci_image.id}`, {
headers: { Accept: api_1.SDK_HEADER },
}).then(res => res.body);
commit = (_a = ociImages[0]) === null || _a === void 0 ? void 0 : _a.commit;
}
}
catch (_b) {
return { name: appName, repo: githubApp.repo, hash: undefined };
}
return { name: appName, repo: githubApp.repo, hash: commit };
};
}
async run() {
const { flags } = await this.parse(PipelinesDiff);
const targetAppName = flags.app;
let coupling;
try {
({ body: coupling } = await (0, api_1.getCoupling)(this.heroku, targetAppName));
}
catch (_a) {
core_1.ux.error(`This app (${targetAppName}) does not seem to be a part of any pipeline`);
return;
}
const { body: pipeline } = await (0, api_1.getPipeline)(this.heroku, coupling.pipeline.id);
const targetAppId = coupling.app.id;
const generation = (0, generation_1.getGeneration)(pipeline);
core_1.ux.action.start('Fetching apps from pipeline');
const allApps = await (0, api_1.listPipelineApps)(this.heroku, coupling.pipeline.id);
core_1.ux.action.stop();
const sourceStage = coupling.stage;
if (!sourceStage) {
return core_1.ux.error(`Unable to diff ${targetAppName}`);
}
const downstreamStage = PROMOTION_ORDER[PROMOTION_ORDER.indexOf(sourceStage) + 1];
if (!downstreamStage || !PROMOTION_ORDER.includes(sourceStage)) {
return core_1.ux.error(`Unable to diff ${targetAppName}`);
}
const downstreamApps = allApps.filter(app => app.pipelineCoupling.stage === downstreamStage);
if (downstreamApps.length === 0) {
return core_1.ux.error(`Cannot diff ${targetAppName} as there are no downstream apps configured`);
}
// Fetch GitHub repo/latest release hash for [target, downstream[0], .., downstream[n]] apps
const appInfoPromises = [this.getAppInfo(targetAppName, targetAppId, generation)];
downstreamApps.forEach(app => {
if (app.name && app.id) {
appInfoPromises.push(this.getAppInfo(app.name, app.id, generation));
}
});
core_1.ux.action.start('Fetching release info for all apps');
const appInfo = await Promise.all(appInfoPromises);
core_1.ux.action.stop();
// Verify the target app
const targetAppInfo = appInfo[0];
if (!targetAppInfo.repo) {
const command = `heroku pipelines:open ${coupling.pipeline.name}`;
return core_1.ux.error(`${targetAppName} does not seem to be connected to GitHub!\nRun ${color_1.default.cyan(command)} and "Connect to GitHub".`);
}
if (!targetAppInfo.hash) {
return core_1.ux.error(`No release was found for ${targetAppName}, unable to diff`);
}
// Fetch GitHub token for the user
const githubAccount = await this.kolkrabbi.getAccount();
// Diff [{target, downstream[0]}, {target, downstream[1]}, .., {target, downstream[n]}]
const downstreamAppsInfo = appInfo.slice(1);
for (const downstreamAppInfo of downstreamAppsInfo) {
await diff(targetAppInfo, downstreamAppInfo, githubAccount.github.token, this.config.userAgent);
}
}
}
exports.default = PipelinesDiff;
PipelinesDiff.description = 'compares the latest release of this app to its downstream app(s)';
PipelinesDiff.examples = [
'$ heroku pipelines:diff -a my-app-staging',
];
PipelinesDiff.flags = {
app: command_1.flags.app({ required: true }),
remote: command_1.flags.remote(),
};
;