Toolkit: Remove deprecated plugin:github-publish command (#67471)

This commit is contained in:
Esteban Beltran
2023-04-28 09:54:16 +02:00
committed by GitHub
parent a1b9eb1eea
commit 82838a2176
12 changed files with 2 additions and 854 deletions

View File

@ -4,7 +4,7 @@ import { program } from 'commander';
import { nodeVersionCheckerTask } from './tasks/nodeVersionChecker';
import { buildPackageTask } from './tasks/package.build';
import { pluginBuildTask } from './tasks/plugin.build';
import { getToolkitVersion, githubPublishTask } from './tasks/plugin.utils';
import { getToolkitVersion } from './tasks/plugin.utils';
import { templateTask } from './tasks/template';
import { toolkitBuildTask } from './tasks/toolkit.build';
import { execTask } from './utils/execTask';
@ -127,26 +127,6 @@ export const run = (includeInternalScripts = false) => {
process.exit(1);
});
program
.command('plugin:github-publish')
.option('--dryrun', 'Do a dry run only', false)
.option('--verbose', 'Print verbose', false)
.option('--commitHash <hashKey>', 'Specify the commit hash')
.description('[Deprecated] Publish to github')
.action(async (cmd) => {
console.log(
chalk.yellow.bold(`⚠️ This command is deprecated and will be removed . No further support will be provided. ⚠️`)
);
console.log(
'We recommend using github actions directly for plugin releasing. You can find an example here: https://github.com/grafana/plugin-tools/tree/main/packages/create-plugin/templates/github/ci/.github/workflows'
);
await execTask(githubPublishTask)({
dryrun: cmd.dryrun,
verbose: cmd.verbose,
commitHash: cmd.commitHash,
});
});
program.on('command:*', () => {
console.error('Invalid command: %s\nSee --help for a list of available commands.', program.args.join(' '));
process.exit(1);

View File

@ -1,147 +1,4 @@
import execa = require('execa');
import { readFileSync } from 'fs';
import path = require('path');
import { getPluginId } from '../../config/utils/getPluginId';
import { getPluginJson } from '../../config/utils/pluginValidation';
import { getCiFolder } from '../../plugins/env';
import { GitHubRelease } from '../utils/githubRelease';
import { useSpinner } from '../utils/useSpinner';
import { Task, TaskRunner } from './task';
interface Command extends Array<any> {}
const DEFAULT_EMAIL_ADDRESS = 'eng@grafana.com';
const DEFAULT_USERNAME = 'CircleCI Automation';
const releaseNotes = async (): Promise<string> => {
const { stdout } = await execa(`awk 'BEGIN {FS="##"; RS="##"} FNR==3 {print "##" $1; exit}' CHANGELOG.md`, {
shell: true,
});
return stdout;
};
const checkoutBranch = async (branchName: string): Promise<Command> => {
const currentBranch = await execa(`git rev-parse --abbrev-ref HEAD`, { shell: true });
const branchesAvailable = await execa(
`(git branch -a | grep "${branchName}$" | grep -v remote) || echo 'No release found'`,
{ shell: true }
);
if (currentBranch.stdout !== branchName) {
console.log('available', branchesAvailable.stdout.trim());
if (branchesAvailable.stdout.trim() === branchName) {
return ['git', ['checkout', branchName]];
} else {
return ['git', ['checkout', '-b', branchName]];
}
}
return [];
};
const gitUrlParse = (url: string): { owner: string; name: string } => {
let matchResult: RegExpMatchArray | null = [];
if (url.match(/^git@github.com/)) {
// We have an ssh style url.
matchResult = url.match(/^git@github.com:(.*?)\/(.*?)\.git/);
}
if (url.match(/^https:\/\/github.com\//)) {
// We have an https style url
matchResult = url.match(/^https:\/\/github.com\/(.*?)\/(.*?)\/.git/);
}
if (matchResult && matchResult.length > 2) {
return {
owner: matchResult[1],
name: matchResult[2],
};
}
throw `Could not find a suitable git repository. Received [${url}]`;
};
const prepareRelease = ({ dryrun, verbose }: any) =>
useSpinner('Preparing release', async () => {
const ciDir = getCiFolder();
const distDir = path.resolve(ciDir, 'dist');
const distContentDir = path.resolve(distDir, getPluginId());
const pluginJsonFile = path.resolve(distContentDir, 'plugin.json');
const pluginJson = getPluginJson(pluginJsonFile);
const githubPublishScript: Command = [
['git', ['config', 'user.email', DEFAULT_EMAIL_ADDRESS]],
['git', ['config', 'user.name', DEFAULT_USERNAME]],
await checkoutBranch(`release-${pluginJson.info.version}`),
['/bin/rm', ['-rf', 'dist'], { dryrun }],
['mv', ['-v', distContentDir, 'dist']],
['git', ['add', '--force', 'dist'], { dryrun }],
['/bin/rm', ['-rf', 'src'], { enterprise: true }],
['git', ['rm', '-rf', 'src'], { enterprise: true }],
[
'git',
['commit', '-m', `automated release ${pluginJson.info.version} [skip ci]`],
{
dryrun,
okOnError: [/nothing to commit/g, /nothing added to commit/g, /no changes added to commit/g],
},
],
['git', ['push', '-f', 'origin', `release-${pluginJson.info.version}`], { dryrun }],
['git', ['tag', '-f', `v${pluginJson.info.version}`]],
['git', ['push', '-f', 'origin', `v${pluginJson.info.version}`]],
];
for (let line of githubPublishScript) {
const opts = line.length === 3 ? line[2] : {};
const command = line[0];
const args = line[1];
try {
if (verbose) {
console.log('executing >>', line);
}
if (line.length > 0 && line[0].length > 0) {
if (opts['dryrun']) {
line[1].push('--dry-run');
}
// Exit if the plugin is NOT an enterprise plugin
if (pluginJson.enterprise && !opts['enterprise']) {
continue;
}
const { stdout } = await execa(command, args);
if (verbose) {
console.log(stdout);
}
} else {
if (verbose) {
console.log('skipping empty line');
}
}
} catch (ex: any) {
const err: string = ex.message;
if (opts['okOnError'] && Array.isArray(opts['okOnError'])) {
let trueError = true;
for (let regex of opts['okOnError']) {
if (err.match(regex)) {
trueError = false;
break;
}
}
if (!trueError) {
// This is not an error
continue;
}
}
console.error(err);
process.exit(-1);
}
}
});
export const getToolkitVersion = () => {
const pkg = readFileSync(`${__dirname}/../../../package.json`, 'utf8');
@ -151,73 +8,3 @@ export const getToolkitVersion = () => {
}
return version;
};
interface GithubPublishReleaseOptions {
commitHash?: string;
githubToken: string;
githubUser: string;
gitRepoName: string;
}
const createRelease = ({ commitHash, githubUser, githubToken, gitRepoName }: GithubPublishReleaseOptions) =>
useSpinner('Creating release', async () => {
const gitRelease = new GitHubRelease(githubToken, githubUser, gitRepoName, await releaseNotes(), commitHash);
return gitRelease.release();
});
export interface GithubPublishOptions {
dryrun?: boolean;
verbose?: boolean;
commitHash?: string;
dev?: boolean;
}
const githubPublishRunner: TaskRunner<GithubPublishOptions> = async ({ dryrun, verbose, commitHash }) => {
let repoUrl: string | undefined = process.env.DRONE_REPO_LINK || process.env.CIRCLE_REPOSITORY_URL;
if (!repoUrl) {
// Try and figure it out
const repo = await execa('git', ['config', '--local', 'remote.origin.url']);
if (repo && repo.stdout) {
repoUrl = repo.stdout;
} else {
throw new Error(
'The release plugin requires you specify the repository url as environment variable DRONE_REPO_LINK or ' +
'CIRCLE_REPOSITORY_URL'
);
}
}
if (!process.env['GITHUB_ACCESS_TOKEN']) {
// Try to use GITHUB_TOKEN, which may be set.
if (process.env['GITHUB_TOKEN']) {
process.env['GITHUB_ACCESS_TOKEN'] = process.env['GITHUB_TOKEN'];
} else {
throw new Error(
`GitHub publish requires that you set the environment variable GITHUB_ACCESS_TOKEN to a valid github api token.
See: https://github.com/settings/tokens for more details.`
);
}
}
if (!process.env['GITHUB_USERNAME']) {
// We can default this one
process.env['GITHUB_USERNAME'] = DEFAULT_EMAIL_ADDRESS;
}
const parsedUrl = gitUrlParse(repoUrl);
const githubToken = process.env['GITHUB_ACCESS_TOKEN'];
const githubUser = parsedUrl.owner;
await prepareRelease({
dryrun,
verbose,
});
await createRelease({
commitHash,
githubUser,
githubToken,
gitRepoName: parsedUrl.name,
});
};
export const githubPublishTask = new Task<GithubPublishOptions>('GitHub Publish', githubPublishRunner);

View File

@ -1,107 +0,0 @@
import GithubClient from './githubClient';
const fakeClient = jest.fn();
beforeEach(() => {
delete process.env.GITHUB_USERNAME;
delete process.env.GITHUB_ACCESS_TOKEN;
});
afterEach(() => {
delete process.env.GITHUB_USERNAME;
delete process.env.GITHUB_ACCESS_TOKEN;
});
describe('GithubClient', () => {
it('should initialise a GithubClient', () => {
const github = new GithubClient();
const githubEnterprise = new GithubClient({ enterprise: true });
expect(github).toBeInstanceOf(GithubClient);
expect(githubEnterprise).toBeInstanceOf(GithubClient);
});
describe('#client', () => {
it('it should contain a grafana client', () => {
// @ts-ignore
const spy = jest.spyOn(GithubClient.prototype, 'createClient').mockImplementation(() => fakeClient);
const github = new GithubClient();
const client = github.client;
expect(spy).toHaveBeenCalledWith({
baseURL: 'https://api.github.com/repos/grafana/grafana',
timeout: 10000,
});
expect(client).toEqual(fakeClient);
});
it('it should contain a grafana enterprise client', () => {
// @ts-ignore
const spy = jest.spyOn(GithubClient.prototype, 'createClient').mockImplementation(() => fakeClient);
const github = new GithubClient({ enterprise: true });
const client = github.client;
expect(spy).toHaveBeenCalledWith({
baseURL: 'https://api.github.com/repos/grafana/grafana-enterprise',
timeout: 10000,
});
expect(client).toEqual(fakeClient);
});
describe('when the credentials are required', () => {
it('should create the client when the credentials are defined', () => {
const username = 'grafana';
const token = 'averysecureaccesstoken';
process.env.GITHUB_USERNAME = username;
process.env.GITHUB_ACCESS_TOKEN = token;
// @ts-ignore
const spy = jest.spyOn(GithubClient.prototype, 'createClient').mockImplementation(() => fakeClient);
const github = new GithubClient({ required: true });
const client = github.client;
expect(spy).toHaveBeenCalledWith({
baseURL: 'https://api.github.com/repos/grafana/grafana',
timeout: 10000,
auth: { username, password: token },
});
expect(client).toEqual(fakeClient);
});
it('should create the enterprise client when the credentials are defined', () => {
const username = 'grafana';
const token = 'averysecureaccesstoken';
process.env.GITHUB_USERNAME = username;
process.env.GITHUB_ACCESS_TOKEN = token;
// @ts-ignore
const spy = jest.spyOn(GithubClient.prototype, 'createClient').mockImplementation(() => fakeClient);
const github = new GithubClient({ required: true, enterprise: true });
const client = github.client;
expect(spy).toHaveBeenCalledWith({
baseURL: 'https://api.github.com/repos/grafana/grafana-enterprise',
timeout: 10000,
auth: { username, password: token },
});
expect(client).toEqual(fakeClient);
});
describe('when the credentials are not defined', () => {
it('should throw an error', () => {
expect(() => {
// eslint-disable-next-line
new GithubClient({ required: true });
}).toThrow(/operation needs a GITHUB_USERNAME and GITHUB_ACCESS_TOKEN environment variables/);
});
});
});
});
});

View File

@ -1,49 +0,0 @@
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
const grafanaURL = (owner: string, repo: string) => `https://api.github.com/repos/${owner}/${repo}`;
const enterpriseURL = 'https://api.github.com/repos/grafana/grafana-enterprise';
// Encapsulates the creation of a client for the GitHub API
//
// Two key things:
// 1. You can specify whenever you want the credentials to be required or not when imported.
// 2. If the credentials are available as part of the environment, even if
// they're not required - the library will use them. This allows us to overcome
// any API rate limiting imposed without authentication.
interface GithubClientProps {
required?: boolean;
enterprise?: boolean;
owner?: string;
repo?: string;
}
class GithubClient {
client: AxiosInstance;
constructor({ required = false, enterprise = false, owner = 'grafana', repo = 'grafana' }: GithubClientProps = {}) {
const username = process.env.GITHUB_USERNAME;
const token = process.env.GITHUB_ACCESS_TOKEN;
const clientConfig: AxiosRequestConfig = {
baseURL: enterprise ? enterpriseURL : grafanaURL(owner, repo),
timeout: 10000,
};
if (required && !username && !token) {
throw new Error('operation needs a GITHUB_USERNAME and GITHUB_ACCESS_TOKEN environment variables');
}
if (username && token) {
clientConfig.auth = { username: username, password: token };
}
this.client = this.createClient(clientConfig);
}
private createClient(clientConfig: AxiosRequestConfig) {
return axios.create(clientConfig);
}
}
export default GithubClient;

View File

@ -1,10 +0,0 @@
import { GitHubRelease } from './githubRelease';
describe('GithubRelease', () => {
it('should initialise a GithubRelease', () => {
process.env.GITHUB_ACCESS_TOKEN = '12345';
process.env.GITHUB_USERNAME = 'test@grafana.com';
const github = new GitHubRelease('A token', 'A username', 'A repo', 'Some release notes');
expect(github).toBeInstanceOf(GitHubRelease);
});
});

View File

@ -1,113 +0,0 @@
import { AxiosResponse } from 'axios';
import fs = require('fs');
import path = require('path');
import { getPluginId } from '../../config/utils/getPluginId';
import { getPluginJson } from '../../config/utils/pluginValidation';
import { getCiFolder } from '../../plugins/env';
import GithubClient from './githubClient';
const resolveContentType = (extension: string): string => {
if (extension.startsWith('.')) {
extension = extension.slice(1);
}
switch (extension) {
case 'zip':
return 'application/zip';
case 'json':
return 'application/json';
case 'sha1':
return 'text/plain';
default:
return 'application/octet-stream';
}
};
class GitHubRelease {
token: string;
username: string;
repository: string;
releaseNotes: string;
commitHash?: string;
git: GithubClient;
constructor(token: string, username: string, repository: string, releaseNotes: string, commitHash?: string) {
this.token = token;
this.username = username;
this.repository = repository;
this.releaseNotes = releaseNotes;
this.commitHash = commitHash;
this.git = new GithubClient({
required: true,
owner: username,
repo: repository,
});
}
publishAssets(srcLocation: string, destUrl: string) {
// Add the assets. Loop through files in the ci/dist folder and upload each asset.
const files = fs.readdirSync(srcLocation);
return files.map(async (file: string) => {
const fileStat = fs.statSync(`${srcLocation}/${file}`);
const fileData = fs.readFileSync(`${srcLocation}/${file}`);
return this.git.client.post(`${destUrl}?name=${file}`, fileData, {
headers: {
'Content-Type': resolveContentType(path.extname(file)),
'Content-Length': fileStat.size,
},
maxContentLength: fileStat.size * 2 * 1024 * 1024,
});
});
}
async release() {
const ciDir = getCiFolder();
const distDir = path.resolve(ciDir, 'dist');
const distContentDir = path.resolve(distDir, getPluginId());
const pluginJsonFile = path.resolve(distContentDir, 'plugin.json');
const pluginInfo = getPluginJson(pluginJsonFile).info;
const PUBLISH_DIR = path.resolve(getCiFolder(), 'packages');
const commitHash = this.commitHash || pluginInfo.build?.hash;
try {
const latestRelease: AxiosResponse<any> = await this.git.client.get(`releases/tags/v${pluginInfo.version}`);
// Re-release if the version is the same as an existing release
if (latestRelease.data.tag_name === `v${pluginInfo.version}`) {
await this.git.client.delete(`releases/${latestRelease.data.id}`);
}
} catch (reason: any) {
if (reason.response.status !== 404) {
// 404 just means no release found. Not an error. Anything else though, re throw the error
throw reason;
}
}
try {
// Now make the release
const newReleaseResponse = await this.git.client.post('releases', {
tag_name: `v${pluginInfo.version}`,
target_commitish: commitHash,
name: `v${pluginInfo.version}`,
body: this.releaseNotes,
draft: false,
prerelease: false,
});
const publishPromises = this.publishAssets(
PUBLISH_DIR,
`https://uploads.github.com/repos/${this.username}/${this.repository}/releases/${newReleaseResponse.data.id}/assets`
);
await Promise.all(publishPromises);
} catch (reason: any) {
console.error(reason.data?.message ?? reason.response.data ?? reason);
// Rethrow the error so that we can trigger a non-zero exit code to circle-ci
throw reason;
}
}
}
export { GitHubRelease };