Address review feedback from freben
- Use cli-defaults instead of listing individual CLI modules in create-app template and root package.json - Move nodeTransform config files from cli-module-build to cli-node to avoid cross-module direct imports - Rename cli-module-create-github-app to cli-module-github - Start createCliModule init chain with Promise.resolve() - Deduplicate exitWithError in runCliModule.ts - Extract shared isCommandNodeHidden to @internal/cli - Add explanatory comment for fromArray deduplication field - Restore error for cli role packages missing bin in runCliExtraction Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com> Made-with: Cursor
This commit is contained in:
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,14 @@
|
||||
# @backstage/cli-module-github
|
||||
|
||||
CLI module that provides the `create-github-app` command for the Backstage CLI, used to create a new GitHub App in your organization for use with Backstage.
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
| :------------------ | :----------------------------------------- |
|
||||
| `create-github-app` | Create new GitHub App in your organization |
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md)
|
||||
- [Backstage Documentation](https://backstage.io/docs)
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env node
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const path = require('node:path');
|
||||
|
||||
/* eslint-disable-next-line no-restricted-syntax */
|
||||
const isLocal = require('node:fs').existsSync(
|
||||
path.resolve(__dirname, '../src'),
|
||||
);
|
||||
|
||||
if (!isLocal) {
|
||||
const { runCliModule } = require('@backstage/cli-node');
|
||||
const cliModule = require('..').default;
|
||||
const pkg = require('../package.json');
|
||||
runCliModule({ module: cliModule, name: pkg.name, version: pkg.version });
|
||||
} else {
|
||||
require('@backstage/cli-node/config/nodeTransform.cjs');
|
||||
require('../src/cli');
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: backstage-cli-module-github
|
||||
title: '@backstage/cli-module-github'
|
||||
description: CLI module for Backstage CLI
|
||||
spec:
|
||||
lifecycle: experimental
|
||||
type: backstage-cli-module
|
||||
owner: tooling-maintainers
|
||||
@@ -0,0 +1,26 @@
|
||||
## CLI Report file for "@backstage/cli-module-github"
|
||||
|
||||
> Do not edit this file. It is a report generated by `yarn build:api-reports`
|
||||
|
||||
### `backstage-cli-module-github`
|
||||
|
||||
```
|
||||
Usage: @backstage/cli-module-github [options] [command]
|
||||
|
||||
Options:
|
||||
-V, --version
|
||||
-h, --help
|
||||
|
||||
Commands:
|
||||
create-github-app
|
||||
help [command]
|
||||
```
|
||||
|
||||
### `backstage-cli-module-github create-github-app`
|
||||
|
||||
```
|
||||
Usage: @backstage/cli-module-github create-github-app <github-org>
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
```
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "@backstage/cli-module-github",
|
||||
"version": "0.0.0",
|
||||
"description": "CLI module for Backstage CLI",
|
||||
"backstage": {
|
||||
"role": "cli-module"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"homepage": "https://backstage.io",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/backstage/backstage",
|
||||
"directory": "packages/cli-module-github"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"bin"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "backstage-cli package build",
|
||||
"clean": "backstage-cli package clean",
|
||||
"lint": "backstage-cli package lint",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack",
|
||||
"test": "backstage-cli package test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/cli-common": "workspace:^",
|
||||
"@backstage/cli-node": "workspace:^",
|
||||
"@octokit/request": "^8.0.0",
|
||||
"chalk": "^4.0.0",
|
||||
"cleye": "^2.3.0",
|
||||
"express": "^4.22.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"inquirer": "^8.2.0",
|
||||
"react-dev-utils": "^12.0.0-next.60",
|
||||
"yaml": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/fs-extra": "^11.0.0"
|
||||
},
|
||||
"bin": "bin/backstage-cli-module-github"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
## API Report File for "@backstage/cli-module-github"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { CliModule } from '@backstage/cli-node';
|
||||
|
||||
// @public (undocumented)
|
||||
const _default: CliModule;
|
||||
export default _default;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { runCliModule } from '@backstage/cli-node';
|
||||
import cliModule from './index';
|
||||
|
||||
const pkg = require('../package.json') as { name: string; version: string };
|
||||
|
||||
runCliModule({
|
||||
module: cliModule,
|
||||
name: pkg.name,
|
||||
version: pkg.version,
|
||||
});
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import crypto from 'node:crypto';
|
||||
import openBrowser from 'react-dev-utils/openBrowser';
|
||||
import { request } from '@octokit/request';
|
||||
import express, { Express, Request, Response } from 'express';
|
||||
|
||||
const FORM_PAGE = `
|
||||
<html>
|
||||
<body>
|
||||
<form id="form" action="ACTION_URL" method="post">
|
||||
<input type="hidden" name="manifest" value="MANIFEST_JSON">
|
||||
<input type="submit" value="Continue">
|
||||
</form>
|
||||
<script>
|
||||
document.getElementById("form").submit()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
type GithubAppConfig = {
|
||||
appId: number;
|
||||
slug?: string;
|
||||
name?: string;
|
||||
webhookUrl?: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
webhookSecret: string;
|
||||
privateKey: string;
|
||||
};
|
||||
|
||||
export class GithubCreateAppServer {
|
||||
private baseUrl?: string;
|
||||
private webhookUrl?: string;
|
||||
|
||||
static async run(options: {
|
||||
org: string;
|
||||
permissions: string[];
|
||||
}): Promise<GithubAppConfig> {
|
||||
const encodedOrg = encodeURIComponent(options.org);
|
||||
const actionUrl = `https://github.com/organizations/${encodedOrg}/settings/apps/new`;
|
||||
const server = new GithubCreateAppServer(actionUrl, options.permissions);
|
||||
return server.start();
|
||||
}
|
||||
|
||||
private readonly actionUrl: string;
|
||||
private readonly permissions: string[];
|
||||
|
||||
private constructor(actionUrl: string, permissions: string[]) {
|
||||
this.actionUrl = actionUrl;
|
||||
this.permissions = permissions;
|
||||
const webhookId = crypto
|
||||
.randomBytes(15)
|
||||
.toString('base64')
|
||||
.replace(/[\+\/]/g, '');
|
||||
|
||||
this.webhookUrl = `https://smee.io/${webhookId}`;
|
||||
}
|
||||
|
||||
private async start(): Promise<GithubAppConfig> {
|
||||
const app = express();
|
||||
|
||||
app.get('/', this.formHandler);
|
||||
|
||||
const callPromise = new Promise<GithubAppConfig>((resolve, reject) => {
|
||||
app.get('/callback', (req, res) => {
|
||||
request(
|
||||
`POST /app-manifests/${encodeURIComponent(
|
||||
req.query.code as string,
|
||||
)}/conversions`,
|
||||
).then(({ data }) => {
|
||||
resolve({
|
||||
name: data.name,
|
||||
slug: data.slug,
|
||||
appId: data.id,
|
||||
webhookUrl: this.webhookUrl,
|
||||
clientId: data.client_id,
|
||||
clientSecret: data.client_secret,
|
||||
webhookSecret: data.webhook_secret,
|
||||
privateKey: data.pem,
|
||||
});
|
||||
res.redirect(302, `${data.html_url}/installations/new`);
|
||||
}, reject);
|
||||
});
|
||||
});
|
||||
|
||||
this.baseUrl = await this.listen(app);
|
||||
|
||||
openBrowser(this.baseUrl);
|
||||
|
||||
return callPromise;
|
||||
}
|
||||
|
||||
private formHandler = (_req: Request, res: Response) => {
|
||||
const baseUrl = this.baseUrl;
|
||||
if (!baseUrl) {
|
||||
throw new Error('baseUrl is not set');
|
||||
}
|
||||
|
||||
const manifest = {
|
||||
default_events: ['create', 'delete', 'push', 'repository'],
|
||||
default_permissions: {
|
||||
metadata: 'read',
|
||||
...(this.permissions.includes('members') && {
|
||||
members: 'read',
|
||||
}),
|
||||
...(this.permissions.includes('read') && {
|
||||
contents: 'read',
|
||||
checks: 'read',
|
||||
}),
|
||||
...(this.permissions.includes('write') && {
|
||||
contents: 'write',
|
||||
checks: 'read',
|
||||
actions: 'write',
|
||||
}),
|
||||
},
|
||||
name: 'Backstage-<changeme>',
|
||||
url: 'https://backstage.io',
|
||||
description: 'GitHub App for Backstage',
|
||||
public: false,
|
||||
redirect_url: `${baseUrl}/callback`,
|
||||
hook_attributes: {
|
||||
url: this.webhookUrl,
|
||||
active: false,
|
||||
},
|
||||
};
|
||||
|
||||
const manifestJson = JSON.stringify(manifest).replace(/\"/g, '"');
|
||||
|
||||
let body = FORM_PAGE;
|
||||
body = body.replace('MANIFEST_JSON', manifestJson);
|
||||
body = body.replace('ACTION_URL', this.actionUrl);
|
||||
|
||||
res.setHeader('content-type', 'text/html');
|
||||
res.send(body);
|
||||
};
|
||||
|
||||
private async listen(app: Express) {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const listener = app.listen(0, () => {
|
||||
const info = listener.address();
|
||||
if (typeof info !== 'object' || info === null) {
|
||||
reject(new Error(`Unexpected listener info '${info}'`));
|
||||
return;
|
||||
}
|
||||
const { port } = info;
|
||||
resolve(`http://localhost:${port}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import chalk from 'chalk';
|
||||
import { stringify as stringifyYaml } from 'yaml';
|
||||
import inquirer, { Question, Answers } from 'inquirer';
|
||||
import { targetPaths } from '@backstage/cli-common';
|
||||
import { cli } from 'cleye';
|
||||
|
||||
import { GithubCreateAppServer } from './GithubCreateAppServer';
|
||||
import openBrowser from 'react-dev-utils/openBrowser';
|
||||
import type { CliCommandContext } from '@backstage/cli-node';
|
||||
|
||||
// This is an experimental command that at this point does not support GitHub Enterprise
|
||||
// due to lacking support for creating apps from manifests.
|
||||
// https://docs.github.com/en/free-pro-team@latest/developers/apps/creating-a-github-app-from-a-manifest
|
||||
export default async ({ args, info }: CliCommandContext) => {
|
||||
const { _: positionals } = cli(
|
||||
{
|
||||
help: { ...info, usage: `${info.usage} <github-org>` },
|
||||
booleanFlagNegation: true,
|
||||
parameters: ['<github-org>'],
|
||||
},
|
||||
undefined,
|
||||
args,
|
||||
);
|
||||
|
||||
const org = positionals[0];
|
||||
|
||||
const answers: Answers = await inquirer.prompt({
|
||||
name: 'appType',
|
||||
type: 'checkbox',
|
||||
message:
|
||||
'Select permissions [required] (these can be changed later but then require approvals in all installations)',
|
||||
choices: [
|
||||
{
|
||||
name: 'Read access to content (required by Software Catalog to ingest data from repositories)',
|
||||
value: 'read',
|
||||
checked: true,
|
||||
},
|
||||
{
|
||||
name: 'Read access to members (required by Software Catalog to ingest GitHub teams)',
|
||||
value: 'members',
|
||||
checked: true,
|
||||
},
|
||||
{
|
||||
name: 'Read and Write to content and actions (required by Software Templates to create new repositories)',
|
||||
value: 'write',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (answers.appType.length === 0) {
|
||||
console.log(chalk.red('You must select at least one permission'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await verifyGithubOrg(org);
|
||||
const { slug, name, ...config } = await GithubCreateAppServer.run({
|
||||
org,
|
||||
permissions: answers.appType,
|
||||
});
|
||||
|
||||
const fileName = `github-app-${slug}-credentials.yaml`;
|
||||
const content = `# Name: ${name}\n${stringifyYaml(config)}`;
|
||||
await fs.writeFile(targetPaths.resolveRoot(fileName), content);
|
||||
console.log(`GitHub App configuration written to ${chalk.cyan(fileName)}`);
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'This file contains sensitive credentials, it should not be committed to version control and handled with care!',
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
"Here's an example on how to update the integrations section in app-config.yaml",
|
||||
);
|
||||
console.log(
|
||||
chalk.green(`
|
||||
integrations:
|
||||
github:
|
||||
- host: github.com
|
||||
apps:
|
||||
- $include: ${fileName}`),
|
||||
);
|
||||
};
|
||||
|
||||
async function verifyGithubOrg(org: string): Promise<void> {
|
||||
let response;
|
||||
|
||||
try {
|
||||
response = await fetch(
|
||||
`https://api.github.com/orgs/${encodeURIComponent(org)}`,
|
||||
);
|
||||
} catch (e) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'Warning: Unable to verify existence of GitHub organization. ',
|
||||
e,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (response?.status === 404) {
|
||||
const questions: Question[] = [
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'shouldCreateOrg',
|
||||
message: `GitHub organization ${chalk.cyan(
|
||||
org,
|
||||
)} does not exist. Would you like to create a new Organization instead?`,
|
||||
},
|
||||
];
|
||||
|
||||
const answers = await inquirer.prompt(questions);
|
||||
|
||||
if (!answers.shouldCreateOrg) {
|
||||
console.log(
|
||||
chalk.yellow('GitHub organization must exist to create GitHub app'),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
openBrowser('https://github.com/account/organizations/new');
|
||||
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'Please re-run this command when you have created your new organization',
|
||||
),
|
||||
);
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { createCliModule } from '@backstage/cli-node';
|
||||
import packageJson from '../package.json';
|
||||
|
||||
export default createCliModule({
|
||||
packageJson,
|
||||
init: async reg => {
|
||||
reg.addCommand({
|
||||
path: ['create-github-app'],
|
||||
description: 'Create new GitHub App in your organization.',
|
||||
execute: { loader: () => import('./commands/create-github-app') },
|
||||
});
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user