Merge branch 'master' of github.com:spotify/backstage into shmidt-i/github-actions-frontend-list-builds
This commit is contained in:
@@ -4,6 +4,7 @@ on:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/techdocs.yml'
|
||||
- 'packages/techdocs-container/**'
|
||||
- 'packages/techdocs-cli/**'
|
||||
- 'plugins/techdocs/**'
|
||||
- 'plugins/techdocs-backend/**'
|
||||
@@ -18,23 +19,18 @@ jobs:
|
||||
python-version: [3.7]
|
||||
|
||||
env:
|
||||
TECHDOCS_CORE_PATH: ./plugins/techdocs/mkdocs/container/techdocs-core
|
||||
TECHDOCS_CORE_PATH: ./packages/techdocs-container/techdocs-core
|
||||
|
||||
name: Python ${{ matrix.node-version }} on ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
# Build Docker Image
|
||||
- name: Build and push Docker images
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@v1.1.0
|
||||
with:
|
||||
path: plugins/techdocs/mkdocs/container
|
||||
registry: docker.pkg.github.com
|
||||
repository: ${{ github.repository }}/mkdocs
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ github.token }}
|
||||
tag_with_ref: true
|
||||
push: true
|
||||
path: packages/techdocs-container
|
||||
push: false
|
||||
|
||||
# Lint Python code for techdocs-core package
|
||||
- name: prepare python environment
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "root",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
"node": "12"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "yarn workspace example-app start",
|
||||
|
||||
@@ -58,6 +58,10 @@ import {
|
||||
import { scaffolderApiRef, ScaffolderApi } from '@backstage/plugin-scaffolder';
|
||||
|
||||
import { rollbarApiRef, RollbarClient } from '@backstage/plugin-rollbar';
|
||||
import {
|
||||
GithubActionsClient,
|
||||
githubActionsApiRef,
|
||||
} from '@backstage/plugin-github-actions';
|
||||
|
||||
export const apis = (config: ConfigApi) => {
|
||||
// eslint-disable-next-line no-console
|
||||
@@ -75,6 +79,7 @@ export const apis = (config: ConfigApi) => {
|
||||
|
||||
builder.add(storageApiRef, WebStorage.create({ errorApi }));
|
||||
builder.add(circleCIApiRef, new CircleCIApi());
|
||||
builder.add(githubActionsApiRef, new GithubActionsClient());
|
||||
builder.add(featureFlagsApiRef, new FeatureFlags());
|
||||
|
||||
builder.add(lighthouseApiRef, new LighthouseRestApi('http://localhost:3003'));
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": "12"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "backstage-cli backend:build",
|
||||
@@ -29,6 +29,7 @@
|
||||
"@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.13",
|
||||
"@backstage/plugin-sentry-backend": "^0.1.1-alpha.13",
|
||||
"@backstage/plugin-techdocs-backend": "^0.1.1-alpha.13",
|
||||
"@octokit/rest": "^18.0.0",
|
||||
"dockerode": "^3.2.0",
|
||||
"express": "^4.17.1",
|
||||
"knex": "^0.21.1",
|
||||
|
||||
@@ -20,19 +20,34 @@ import {
|
||||
FilePreparer,
|
||||
GithubPreparer,
|
||||
Preparers,
|
||||
GithubPublisher,
|
||||
Templaters,
|
||||
} from '@backstage/plugin-scaffolder-backend';
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import type { PluginEnvironment } from '../types';
|
||||
import Docker from 'dockerode';
|
||||
|
||||
export default async function createPlugin({ logger }: PluginEnvironment) {
|
||||
const templater = new CookieCutter();
|
||||
const cookiecutterTemplater = new CookieCutter();
|
||||
const templaters = new Templaters();
|
||||
templaters.register('cookiecutter', cookiecutterTemplater);
|
||||
|
||||
const filePreparer = new FilePreparer();
|
||||
const githubPreparer = new GithubPreparer();
|
||||
const preparers = new Preparers();
|
||||
const dockerClient = new Docker();
|
||||
|
||||
preparers.register('file', filePreparer);
|
||||
preparers.register('github', githubPreparer);
|
||||
|
||||
return await createRouter({ preparers, templater, logger, dockerClient });
|
||||
const githubClient = new Octokit({ auth: process.env.GITHUB_ACCESS_TOKEN });
|
||||
const publisher = new GithubPublisher({ client: githubClient });
|
||||
|
||||
const dockerClient = new Docker();
|
||||
return await createRouter({
|
||||
preparers,
|
||||
templaters,
|
||||
publisher,
|
||||
logger,
|
||||
dockerClient,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -32,7 +32,8 @@ describe('TemplateEntityV1alpah1', () => {
|
||||
name: 'test',
|
||||
},
|
||||
spec: {
|
||||
type: 'cookiecutter',
|
||||
type: 'website',
|
||||
templater: 'cookiecutter',
|
||||
schema: {
|
||||
$schema: 'http://json-schema.org/draft-07/schema#',
|
||||
required: ['storePath', 'owner'],
|
||||
@@ -78,7 +79,7 @@ describe('TemplateEntityV1alpah1', () => {
|
||||
await expect(policy.enforce(entity)).rejects.toThrow(/type/);
|
||||
});
|
||||
|
||||
it('acceptps any other type', async () => {
|
||||
it('accepts any other type', async () => {
|
||||
(entity as any).spec.type = 'hallo';
|
||||
await expect(policy.enforce(entity)).resolves.toBe(entity);
|
||||
});
|
||||
@@ -87,4 +88,9 @@ describe('TemplateEntityV1alpah1', () => {
|
||||
(entity as any).spec.type = '';
|
||||
await expect(policy.enforce(entity)).rejects.toThrow(/type/);
|
||||
});
|
||||
|
||||
it('rejects missing templater', async () => {
|
||||
(entity as any).spec.templater = '';
|
||||
await expect(policy.enforce(entity)).rejects.toThrow(/templater/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface TemplateEntityV1alpha1 extends Entity {
|
||||
kind: typeof KIND;
|
||||
spec: {
|
||||
type: string;
|
||||
templater: string;
|
||||
path?: string;
|
||||
schema: JSONSchema;
|
||||
};
|
||||
@@ -43,6 +44,7 @@ export class TemplateEntityV1alpha1Policy implements EntityPolicy {
|
||||
type: yup.string().required().min(1),
|
||||
path: yup.string(),
|
||||
schema: yup.object().required(),
|
||||
templater: yup.string().required(),
|
||||
})
|
||||
.required(),
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
"node": "12"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "yarn workspace app start",
|
||||
|
||||
@@ -4,46 +4,42 @@ Check out the [TechDocs README](https://github.com/spotify/backstage/blob/master
|
||||
|
||||
**WIP: This cli is a work in progress. It is not ready for use yet. Follow our progress on [the Backstage Discord](https://discord.gg/MUpMjP2) under #docs-like-code or on [our GitHub Milestone](https://github.com/spotify/backstage/milestone/15).**
|
||||
|
||||
## Getting Started
|
||||
|
||||
You'll need Docker installed and running to use this. You will also need to build the container located at `plugins/techdocs/mkdocs/container` under the tag `mkdocs:local-dev`, as you can see in the commands from below:
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
docker build plugins/techdocs/mkdocs/container -t mkdocs:local-dev
|
||||
# Serve localhost:3000 (and localhost:8000)
|
||||
yarn serve
|
||||
|
||||
# Serve localhost:8000 containing your Mkdocs documentation.
|
||||
yarn serve:mkdocs
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
|
||||
You'll need Docker installed and running to use this. You will also need to build the container located at `/packages/techdocs-container` under the tag `mkdocs:local-dev`, as you can see in the commands from below:
|
||||
|
||||
```bash
|
||||
docker build packages/techdocs-container -t mkdocs:local-dev
|
||||
```
|
||||
|
||||
From that point, you can invoke the CLI from any project with a docs folder. Try out our example!
|
||||
|
||||
```bash
|
||||
cd plugins/techdocs/mkdocs/mock-docs
|
||||
cd packages/techdocs-container/mock-docs
|
||||
npx @techdocs/cli serve
|
||||
```
|
||||
|
||||
## Local Development
|
||||
|
||||
You'll need Docker installed and running to use this. You will also need to build the container located at `plugins/techdocs/mkdocs/container` under the tag `mkdocs:local-dev`, as you can see in the commands from below:
|
||||
You'll need Docker installed and running to use this. You will also need to build the container located at `packages/techdocs-container` under the tag `mkdocs:local-dev` (for now until we deploy the container to a centralized Docker registry), as you can see in the commands from below:
|
||||
|
||||
```bash
|
||||
docker build plugins/techdocs/mkdocs/container -t mkdocs:local-dev
|
||||
docker build packages/techdocs-container -t mkdocs:local-dev
|
||||
```
|
||||
|
||||
Once that is built, you'll need to manually create an `alias` for running the CLI locally:
|
||||
|
||||
```bash
|
||||
cd packages/techdocs-cli
|
||||
echo "$(pwd)/bin/techdocs"
|
||||
|
||||
# Copy the value from above and add it in [HERE] below
|
||||
# For more convenience, add it to your ~/.zshrc or ~/.bash_profile
|
||||
# otherwise you'll lose it when you open a new Terminal
|
||||
alias techdocs="[HERE]"
|
||||
```
|
||||
|
||||
From that point, you can invoke `techdocs` from any project with a docs folder. Try out our example!
|
||||
|
||||
```bash
|
||||
cd plugins/techdocs/mkdocs/mock-docs
|
||||
techdocs serve
|
||||
cd packages/techdocs-container/mock-docs
|
||||
npx techdocs serve
|
||||
```
|
||||
|
||||
You should have a `localhost:3000` serving TechDocs in Backstage, as well as `localhost:8000` serving Mkdocs (which won't open up and be exposed to the user).
|
||||
|
||||
@@ -22,7 +22,7 @@ TECHDOCS_PREVIEW_DEST=$ROOT_DIR/packages/techdocs-cli/dist/techdocs-preview-bund
|
||||
backstage-cli build --outputs cjs
|
||||
|
||||
# Create export of the TechDocs plugin
|
||||
yarn workspace @backstage/plugin-techdocs export
|
||||
APP_CONFIG_techdocs_storageUrl='"http://localhost:3000/api"' yarn workspace @backstage/plugin-techdocs export
|
||||
|
||||
# Copy over export to techdocs-cli dist/ folder
|
||||
cp -r $TECHDOCS_PREVIEW_SOURCE $TECHDOCS_PREVIEW_DEST
|
||||
|
||||
@@ -45,10 +45,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.13",
|
||||
"@backstage/plugin-techdocs": "^0.1.1-alpha.13",
|
||||
"chalk": "^4.1.0",
|
||||
"commander": "^5.1.0",
|
||||
"fs-extra": "^9.0.1",
|
||||
"http-proxy": "^1.18.1",
|
||||
"react-dev-utils": "^10.2.1",
|
||||
"serve-handler": "^6.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,17 +13,28 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { spawn, ChildProcess } from 'child_process';
|
||||
import program from 'commander';
|
||||
import { version } from './lib/version';
|
||||
// import chalk from 'chalk';
|
||||
import { spawn } from 'child_process';
|
||||
import path from 'path';
|
||||
// import HTTPServer from './lib/httpServer';
|
||||
import HTTPServer from './lib/httpServer';
|
||||
import openBrowser from 'react-dev-utils/openBrowser';
|
||||
|
||||
const run = (workingDirectory: string, name: string, args: string[] = []) => {
|
||||
const child = spawn(name, args, {
|
||||
const run = (
|
||||
workingDirectory: string,
|
||||
name: string,
|
||||
args: string[] = [],
|
||||
): ChildProcess => {
|
||||
const [stdin, stdout, stderr] = [
|
||||
'inherit' as const,
|
||||
'pipe' as const,
|
||||
'inherit' as const,
|
||||
];
|
||||
|
||||
const childProcess = spawn(name, args, {
|
||||
cwd: workingDirectory,
|
||||
stdio: ['inherit', 'inherit', 'inherit'],
|
||||
stdio: [stdin, stdout, stderr],
|
||||
shell: true,
|
||||
env: {
|
||||
...process.env,
|
||||
@@ -31,56 +42,87 @@ const run = (workingDirectory: string, name: string, args: string[] = []) => {
|
||||
},
|
||||
});
|
||||
|
||||
child.once('error', error => {
|
||||
childProcess.once('error', error => {
|
||||
console.error(error);
|
||||
childProcess.kill();
|
||||
});
|
||||
child.once('exit', code => {
|
||||
console.log('exited!', code);
|
||||
|
||||
childProcess.once('exit', () => {
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
return childProcess;
|
||||
};
|
||||
|
||||
const runMkdocsServer = (options?: {
|
||||
devAddr: string;
|
||||
}): Promise<ChildProcess> => {
|
||||
const devAddr = options?.devAddr ?? '0.0.0.0:8000';
|
||||
|
||||
return new Promise(resolve => {
|
||||
const childProcess = run(process.env.PWD!, 'docker', [
|
||||
'run',
|
||||
'-it',
|
||||
'-w',
|
||||
'/content',
|
||||
'-v',
|
||||
'$(pwd):/content',
|
||||
'-p',
|
||||
'8000:8000',
|
||||
'mkdocs:local-dev',
|
||||
'serve',
|
||||
'-a',
|
||||
devAddr,
|
||||
]);
|
||||
|
||||
childProcess.stdout?.on('data', rawData => {
|
||||
const data = rawData.toString().split('\n')[0];
|
||||
console.log('[mkdocs] ', data);
|
||||
|
||||
if (data.includes(`Serving on http://${devAddr}`)) {
|
||||
resolve(childProcess);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const main = (argv: string[]) => {
|
||||
program.name('techdocs-cli').version(version);
|
||||
|
||||
program
|
||||
.command('serve:mkdocs')
|
||||
.description('Serve a documentation project locally')
|
||||
.action(() => {
|
||||
runMkdocsServer().then(() => {
|
||||
openBrowser('http://localhost:8000');
|
||||
});
|
||||
});
|
||||
|
||||
program
|
||||
.command('serve')
|
||||
.description('Serve a documentation project locally')
|
||||
.action(() => {
|
||||
// const techdocsPreviewBundlePath = path.join(
|
||||
// __dirname,
|
||||
// '..',
|
||||
// 'dist',
|
||||
// 'techdocs-preview-bundle',
|
||||
// );
|
||||
// Mkdocs server
|
||||
const mkdocsServer = runMkdocsServer();
|
||||
|
||||
// new HTTPServer(techdocsPreviewBundlePath, 3000).serve();
|
||||
|
||||
run(process.env.PWD!, 'docker', [
|
||||
'run',
|
||||
'-it',
|
||||
'-w',
|
||||
'/content',
|
||||
'-v',
|
||||
'$(pwd):/content',
|
||||
'-p',
|
||||
'8000:8000',
|
||||
'mkdocs:local-dev',
|
||||
'serve',
|
||||
'-a',
|
||||
'0.0.0.0:8000',
|
||||
]);
|
||||
|
||||
const pluginPath = path.join(
|
||||
require.resolve('@backstage/plugin-techdocs'),
|
||||
'..',
|
||||
// Local Backstage Preview
|
||||
const techdocsPreviewBundlePath = path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'dist',
|
||||
'techdocs-preview-bundle',
|
||||
);
|
||||
|
||||
run(
|
||||
pluginPath,
|
||||
path.join(require.resolve('@backstage/cli'), '../../bin/backstage-cli'),
|
||||
['plugin:serve'],
|
||||
);
|
||||
const httpServer = new HTTPServer(techdocsPreviewBundlePath, 3000)
|
||||
.serve()
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
mkdocsServer.then(childProcess => childProcess.kill());
|
||||
});
|
||||
|
||||
Promise.all([mkdocsServer, httpServer]).then(() => {
|
||||
openBrowser('http://localhost:3000/docs/local-dev/');
|
||||
});
|
||||
});
|
||||
|
||||
program.parse(argv);
|
||||
|
||||
@@ -16,20 +16,64 @@
|
||||
|
||||
import serveHandler from 'serve-handler';
|
||||
import http from 'http';
|
||||
import httpProxy from 'http-proxy';
|
||||
|
||||
export default class HTTPServer {
|
||||
constructor(public dir: string, public port: number) {}
|
||||
proxyEndpoint: string;
|
||||
|
||||
serve() {
|
||||
const server = http.createServer((request, response) => {
|
||||
return serveHandler(request, response, {
|
||||
public: this.dir,
|
||||
trailingSlash: true,
|
||||
});
|
||||
constructor(public dir: string, public port: number) {
|
||||
this.proxyEndpoint = '/api/';
|
||||
}
|
||||
|
||||
private createProxy() {
|
||||
const proxy = httpProxy.createProxyServer({
|
||||
target: 'http://localhost:8000',
|
||||
});
|
||||
|
||||
server.listen(this.port, () => {
|
||||
console.log('Running at http://localhost:3000');
|
||||
return (request: http.IncomingMessage): [httpProxy, string] => {
|
||||
const [, ...pathChunks] =
|
||||
request.url?.substring(this.proxyEndpoint.length).split('/') ?? [];
|
||||
const forwardPath = pathChunks.join('/');
|
||||
|
||||
return [proxy, forwardPath];
|
||||
};
|
||||
}
|
||||
|
||||
public async serve(): Promise<http.Server> {
|
||||
return new Promise<http.Server>((resolve, reject) => {
|
||||
const proxyHandler = this.createProxy();
|
||||
|
||||
const server = http.createServer(
|
||||
(request: http.IncomingMessage, response: http.ServerResponse) => {
|
||||
if (request.url?.startsWith(this.proxyEndpoint)) {
|
||||
const [proxy, forwardPath] = proxyHandler(request);
|
||||
|
||||
proxy.on('error', (error: Error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
request.url = forwardPath;
|
||||
return proxy.web(request, response);
|
||||
}
|
||||
|
||||
return serveHandler(request, response, {
|
||||
public: this.dir,
|
||||
trailingSlash: true,
|
||||
rewrites: [{ source: '**', destination: 'index.html' }],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
server.listen(this.port, () => {
|
||||
console.log(
|
||||
'[techdocs-preview-bundle] Running local version of Backstage at http://localhost:3000',
|
||||
);
|
||||
resolve(server);
|
||||
});
|
||||
|
||||
server.on('error', (error: Error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# techdocs-container
|
||||
|
||||
This is the Docker container that powers the creation of static documentation sites that are supported by [TechDocs](https://github.com/spotify/backstage/blob/master/plugins/techdocs).
|
||||
|
||||
**WIP: This is a work in progress. It is not ready for use yet. Follow our progress on [the Backstage Discord](https://discord.gg/MUpMjP2) under #docs-like-code or on [our GitHub Milestone](https://github.com/spotify/backstage/milestone/15).**
|
||||
|
||||
## Getting Started
|
||||
|
||||
Using the TechDocs CLI, we can invoke the latest version of `techdocs-container` via Docker Hub:
|
||||
|
||||
```bash
|
||||
npx @techdocs/cli serve:container
|
||||
```
|
||||
|
||||
## Local Development
|
||||
|
||||
```bash
|
||||
docker build ./container -t techdocs-container
|
||||
|
||||
docker run -w /content -v $(pwd)/mock-docs:/content -p 8000:8000 -it techdocs-container serve -a 0.0.0.0:8000
|
||||
```
|
||||
|
||||
Then open up `http://localhost:8000` on your local machine.
|
||||
+1
-1
@@ -33,7 +33,7 @@ You'll then have the `techdocs-core` package available to use in Mkdocs and `pip
|
||||
|
||||
In the parent `Dockerfile` we add this folder to the build and install the package locally in the container. In the future, we'll probably move away from this approach and have it download directly from a Python registry (and this folder will publish to one).
|
||||
|
||||
See the `README.md` located in the `mkdocs/` folder for more details on how to build and run the Docker container.
|
||||
See the `README.md` located in the `techdocs-container/` folder for more details on how to build and run the Docker container.
|
||||
|
||||
## Linting
|
||||
|
||||
@@ -21,7 +21,9 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.13",
|
||||
"@backstage/core": "^0.1.1-alpha.13",
|
||||
"@backstage/core-api": "^0.1.1-alpha.13",
|
||||
"@backstage/theme": "^0.1.1-alpha.13",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
|
||||
+17
-19
@@ -14,25 +14,23 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export enum BuildStatus {
|
||||
Null,
|
||||
Success,
|
||||
Failure,
|
||||
Pending,
|
||||
Running,
|
||||
}
|
||||
import { createApiRef } from '@backstage/core';
|
||||
import { Build, BuildDetails } from './types';
|
||||
|
||||
export type Build = {
|
||||
commitId: string;
|
||||
message: string;
|
||||
branch: string;
|
||||
status: BuildStatus;
|
||||
uri: string;
|
||||
};
|
||||
export const githubActionsApiRef = createApiRef<GithubActionsApi>({
|
||||
id: 'plugin.githubactions.service',
|
||||
description: 'Used by the Github Actions plugin to make requests',
|
||||
});
|
||||
|
||||
export type BuildDetails = {
|
||||
build: Build;
|
||||
author: string;
|
||||
logUrl: string;
|
||||
overviewUrl: string;
|
||||
export type GithubActionsApi = {
|
||||
listBuilds: ({
|
||||
owner,
|
||||
repo,
|
||||
token,
|
||||
}: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
token: string;
|
||||
}) => Promise<Build[]>;
|
||||
getBuild: (buildUri: string, token: Promise<string>) => Promise<BuildDetails>;
|
||||
};
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { GithubActionsApi } from './GithubActionsApi';
|
||||
import { Build, BuildDetails, BuildStatus, WorkflowRun } from './types';
|
||||
|
||||
const statusToBuildStatus: { [status: string]: BuildStatus } = {
|
||||
success: BuildStatus.Success,
|
||||
failure: BuildStatus.Failure,
|
||||
pending: BuildStatus.Pending,
|
||||
running: BuildStatus.Running,
|
||||
in_progress: BuildStatus.Running,
|
||||
completed: BuildStatus.Success,
|
||||
};
|
||||
|
||||
const conclusionToStatus = (conslusion: string): BuildStatus =>
|
||||
statusToBuildStatus[conslusion] ?? BuildStatus.Null;
|
||||
|
||||
export class GithubActionsClient implements GithubActionsApi {
|
||||
async listBuilds({
|
||||
owner,
|
||||
repo,
|
||||
token,
|
||||
}: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
token: string;
|
||||
}): Promise<Build[]> {
|
||||
const url = `https://api.github.com/repos/${owner}/${repo}/actions/runs`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: new Headers({
|
||||
Authorization: `Bearer ${token}`,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return [
|
||||
{
|
||||
commitId: 'Error',
|
||||
message: 'Response status is not OK',
|
||||
branch: 'Error',
|
||||
status: BuildStatus.Failure,
|
||||
uri: 'Error',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const newData: WorkflowRun[] = data.workflow_runs;
|
||||
|
||||
const endData: Build[] = [];
|
||||
|
||||
newData.forEach((element, index) => {
|
||||
const transData: Build = {
|
||||
commitId: '',
|
||||
message: '',
|
||||
branch: '',
|
||||
status: BuildStatus.Null,
|
||||
uri: '',
|
||||
};
|
||||
transData.commitId = String(element.head_commit.id);
|
||||
transData.branch = element.head_branch;
|
||||
transData.status = conclusionToStatus(element.conclusion);
|
||||
transData.message = element.head_commit.message;
|
||||
transData.uri = element.url;
|
||||
endData[index] = transData;
|
||||
});
|
||||
|
||||
return endData;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async getBuild(
|
||||
buildUri: string,
|
||||
token: Promise<string>,
|
||||
): Promise<BuildDetails> {
|
||||
const response = await fetch(buildUri, {
|
||||
headers: new Headers({
|
||||
Authorization: `Bearer ${await token}`,
|
||||
}),
|
||||
});
|
||||
const buildBlank: Build = {
|
||||
commitId: '',
|
||||
message: '',
|
||||
branch: '',
|
||||
status: BuildStatus.Null,
|
||||
uri: '',
|
||||
};
|
||||
|
||||
const dataBlank: BuildDetails = {
|
||||
build: buildBlank,
|
||||
author: '',
|
||||
logUrl: '',
|
||||
overviewUrl: '',
|
||||
};
|
||||
|
||||
if (!response.ok) {
|
||||
return dataBlank;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const newData: WorkflowRun = data;
|
||||
|
||||
dataBlank.author = newData.head_commit.author.name;
|
||||
dataBlank.build.branch = newData.head_branch;
|
||||
dataBlank.build.commitId = newData.head_commit.id;
|
||||
dataBlank.build.message = newData.head_commit.message;
|
||||
dataBlank.build.status = conclusionToStatus(newData.status);
|
||||
dataBlank.build.uri = newData.url;
|
||||
dataBlank.logUrl = newData.logs_url;
|
||||
dataBlank.overviewUrl = newData.html_url;
|
||||
|
||||
return dataBlank;
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -14,5 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { BuildsClient } from './BuildsClient';
|
||||
export * from './GithubActionsApi';
|
||||
export * from './GithubActionsClient';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export enum BuildStatus {
|
||||
Null,
|
||||
Success,
|
||||
Failure,
|
||||
Pending,
|
||||
Running,
|
||||
}
|
||||
|
||||
export type Build = {
|
||||
commitId: string;
|
||||
message: string;
|
||||
branch: string;
|
||||
status: BuildStatus;
|
||||
uri: string;
|
||||
};
|
||||
|
||||
export type BuildDetails = {
|
||||
build: Build;
|
||||
author: string;
|
||||
logUrl: string;
|
||||
overviewUrl: string;
|
||||
};
|
||||
|
||||
export interface Author {
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface Committer {
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface HeadCommit {
|
||||
id: string;
|
||||
tree_id: string;
|
||||
message: string;
|
||||
timestamp: Date;
|
||||
author: Author;
|
||||
committer: Committer;
|
||||
}
|
||||
|
||||
export interface Owner {
|
||||
login: string;
|
||||
id: number;
|
||||
node_id: string;
|
||||
avatar_url: string;
|
||||
gravatar_id: string;
|
||||
url: string;
|
||||
html_url: string;
|
||||
followers_url: string;
|
||||
following_url: string;
|
||||
gists_url: string;
|
||||
starred_url: string;
|
||||
subscriptions_url: string;
|
||||
organizations_url: string;
|
||||
repos_url: string;
|
||||
events_url: string;
|
||||
received_events_url: string;
|
||||
type: string;
|
||||
site_admin: boolean;
|
||||
}
|
||||
|
||||
export interface Repository {
|
||||
id: number;
|
||||
node_id: string;
|
||||
name: string;
|
||||
full_name: string;
|
||||
private: boolean;
|
||||
owner: Owner;
|
||||
html_url: string;
|
||||
description?: any;
|
||||
fork: boolean;
|
||||
url: string;
|
||||
forks_url: string;
|
||||
keys_url: string;
|
||||
collaborators_url: string;
|
||||
teams_url: string;
|
||||
hooks_url: string;
|
||||
issue_events_url: string;
|
||||
events_url: string;
|
||||
assignees_url: string;
|
||||
branches_url: string;
|
||||
tags_url: string;
|
||||
blobs_url: string;
|
||||
git_tags_url: string;
|
||||
git_refs_url: string;
|
||||
trees_url: string;
|
||||
statuses_url: string;
|
||||
languages_url: string;
|
||||
stargazers_url: string;
|
||||
contributors_url: string;
|
||||
subscribers_url: string;
|
||||
subscription_url: string;
|
||||
commits_url: string;
|
||||
git_commits_url: string;
|
||||
comments_url: string;
|
||||
issue_comment_url: string;
|
||||
contents_url: string;
|
||||
compare_url: string;
|
||||
merges_url: string;
|
||||
archive_url: string;
|
||||
downloads_url: string;
|
||||
issues_url: string;
|
||||
pulls_url: string;
|
||||
milestones_url: string;
|
||||
notifications_url: string;
|
||||
labels_url: string;
|
||||
releases_url: string;
|
||||
deployments_url: string;
|
||||
}
|
||||
|
||||
export interface Owner2 {
|
||||
login: string;
|
||||
id: number;
|
||||
node_id: string;
|
||||
avatar_url: string;
|
||||
gravatar_id: string;
|
||||
url: string;
|
||||
html_url: string;
|
||||
followers_url: string;
|
||||
following_url: string;
|
||||
gists_url: string;
|
||||
starred_url: string;
|
||||
subscriptions_url: string;
|
||||
organizations_url: string;
|
||||
repos_url: string;
|
||||
events_url: string;
|
||||
received_events_url: string;
|
||||
type: string;
|
||||
site_admin: boolean;
|
||||
}
|
||||
|
||||
export interface HeadRepository {
|
||||
id: number;
|
||||
node_id: string;
|
||||
name: string;
|
||||
full_name: string;
|
||||
private: boolean;
|
||||
owner: Owner2;
|
||||
html_url: string;
|
||||
description?: any;
|
||||
fork: boolean;
|
||||
url: string;
|
||||
forks_url: string;
|
||||
keys_url: string;
|
||||
collaborators_url: string;
|
||||
teams_url: string;
|
||||
hooks_url: string;
|
||||
issue_events_url: string;
|
||||
events_url: string;
|
||||
assignees_url: string;
|
||||
branches_url: string;
|
||||
tags_url: string;
|
||||
blobs_url: string;
|
||||
git_tags_url: string;
|
||||
git_refs_url: string;
|
||||
trees_url: string;
|
||||
statuses_url: string;
|
||||
languages_url: string;
|
||||
stargazers_url: string;
|
||||
contributors_url: string;
|
||||
subscribers_url: string;
|
||||
subscription_url: string;
|
||||
commits_url: string;
|
||||
git_commits_url: string;
|
||||
comments_url: string;
|
||||
issue_comment_url: string;
|
||||
contents_url: string;
|
||||
compare_url: string;
|
||||
merges_url: string;
|
||||
archive_url: string;
|
||||
downloads_url: string;
|
||||
issues_url: string;
|
||||
pulls_url: string;
|
||||
milestones_url: string;
|
||||
notifications_url: string;
|
||||
labels_url: string;
|
||||
releases_url: string;
|
||||
deployments_url: string;
|
||||
}
|
||||
|
||||
export interface WorkflowRun {
|
||||
id: number;
|
||||
node_id: string;
|
||||
head_branch: string;
|
||||
head_sha: string;
|
||||
run_number: number;
|
||||
event: string;
|
||||
status: string;
|
||||
conclusion: string;
|
||||
workflow_id: number;
|
||||
url: string;
|
||||
html_url: string;
|
||||
pull_requests: any[];
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
jobs_url: string;
|
||||
logs_url: string;
|
||||
check_suite_url: string;
|
||||
artifacts_url: string;
|
||||
cancel_url: string;
|
||||
rerun_url: string;
|
||||
workflow_url: string;
|
||||
head_commit: HeadCommit;
|
||||
repository: Repository;
|
||||
head_repository: HeadRepository;
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { Build, BuildDetails, BuildStatus } from './types';
|
||||
|
||||
export class BuildsClient {
|
||||
static create(): BuildsClient {
|
||||
return new BuildsClient();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async listBuilds(_entityUri: string): Promise<Build[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async getBuild(_buildUri: string): Promise<BuildDetails> {
|
||||
return {
|
||||
build: {
|
||||
commitId: 'TODO',
|
||||
branch: 'TODO',
|
||||
uri: 'TODO',
|
||||
status: BuildStatus.Running,
|
||||
message: 'TODO',
|
||||
},
|
||||
author: 'TODO',
|
||||
logUrl: 'TODO',
|
||||
overviewUrl: 'TODO',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Link } from '@backstage/core';
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
@@ -30,10 +29,11 @@ import {
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useAsync } from 'react-use';
|
||||
import { BuildsClient } from '../../apis/builds';
|
||||
import { BuildStatusIndicator } from '../BuildStatusIndicator';
|
||||
import { Link, useApi, githubAuthApiRef } from '@backstage/core';
|
||||
import { githubActionsApiRef } from '../../api';
|
||||
|
||||
const useStyles = makeStyles<Theme>(theme => ({
|
||||
root: {
|
||||
@@ -48,12 +48,18 @@ const useStyles = makeStyles<Theme>(theme => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const client = BuildsClient.create();
|
||||
|
||||
export const BuildDetailsPage = () => {
|
||||
const api = useApi(githubActionsApiRef);
|
||||
const githubApi = useApi(githubAuthApiRef);
|
||||
const token = githubApi.getAccessToken('repo');
|
||||
|
||||
const classes = useStyles();
|
||||
const { buildUri } = useParams();
|
||||
const status = useAsync(() => client.getBuild(buildUri), [buildUri]);
|
||||
const location = useLocation();
|
||||
const status = useAsync(
|
||||
() =>
|
||||
api.getBuild(decodeURIComponent(location.search.split('uri=')[1]), token),
|
||||
[location.search],
|
||||
);
|
||||
|
||||
if (status.loading) {
|
||||
return <LinearProgress />;
|
||||
@@ -70,7 +76,7 @@ export const BuildDetailsPage = () => {
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Typography className={classes.title} variant="h3">
|
||||
<Link to="/builds">
|
||||
<Link to="/github-actions">
|
||||
<Typography component="span" variant="h3" color="primary">
|
||||
<
|
||||
</Typography>
|
||||
@@ -124,12 +130,12 @@ export const BuildDetailsPage = () => {
|
||||
>
|
||||
{details?.overviewUrl && (
|
||||
<Button>
|
||||
<Link to={details.overviewUrl}>GitHub</Link>
|
||||
<a href={details.overviewUrl}>GitHub</a>
|
||||
</Button>
|
||||
)}
|
||||
{details?.logUrl && (
|
||||
<Button>
|
||||
<Link to={details.logUrl}>Logs</Link>
|
||||
<a href={details.logUrl}>Logs</a>
|
||||
</Button>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Link } from '@backstage/core';
|
||||
import {
|
||||
LinearProgress,
|
||||
makeStyles,
|
||||
@@ -27,10 +26,9 @@ import {
|
||||
} from '@material-ui/core';
|
||||
import React from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
import { BuildsClient } from '../../apis/builds';
|
||||
import { BuildStatusIndicator } from '../BuildStatusIndicator';
|
||||
|
||||
const client = BuildsClient.create();
|
||||
import { githubActionsApiRef } from '../../api';
|
||||
import { Link, useApi, githubAuthApiRef } from '@backstage/core';
|
||||
|
||||
const useStyles = makeStyles<Theme>(theme => ({
|
||||
root: {
|
||||
@@ -41,62 +39,69 @@ const useStyles = makeStyles<Theme>(theme => ({
|
||||
},
|
||||
}));
|
||||
|
||||
export const BuildInfoCard = () => {
|
||||
const classes = useStyles();
|
||||
const status = useAsync(() => client.listBuilds('entity:spotify:backstage'));
|
||||
const BuildInfoCardContent = () => {
|
||||
const api = useApi(githubActionsApiRef);
|
||||
const githubApi = useApi(githubAuthApiRef);
|
||||
|
||||
let content: JSX.Element;
|
||||
const status = useAsync(async () => {
|
||||
const token = await githubApi.getAccessToken('repo');
|
||||
return api.listBuilds({ owner: 'spotify', repo: 'backstage', token });
|
||||
});
|
||||
|
||||
if (status.loading) {
|
||||
content = <LinearProgress />;
|
||||
return <LinearProgress />;
|
||||
} else if (status.error) {
|
||||
content = (
|
||||
return (
|
||||
<Typography variant="h2" color="error">
|
||||
Failed to load builds, {status.error.message}
|
||||
</Typography>
|
||||
);
|
||||
} else {
|
||||
const [build] =
|
||||
status.value?.filter(({ branch }) => branch === 'master') ?? [];
|
||||
|
||||
content = (
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Message</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Link to={`builds/${encodeURIComponent(build?.uri || '')}`}>
|
||||
<Typography color="primary">{build?.message}</Typography>
|
||||
</Link>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Commit ID</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{build?.commitId}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Status</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<BuildStatusIndicator status={build?.status} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
const [build] =
|
||||
status.value?.filter(({ branch }) => branch === 'master') ?? [];
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Message</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Link to={`builds/${encodeURIComponent(build?.uri || '')}`}>
|
||||
<Typography color="primary">{build?.message}</Typography>
|
||||
</Link>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Commit ID</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{build?.commitId}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Status</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<BuildStatusIndicator status={build?.status} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
export const BuildInfoCard = () => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Typography variant="h2" className={classes.title}>
|
||||
Master Build
|
||||
</Typography>
|
||||
{content}
|
||||
<BuildInfoCardContent />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ import SuccessIcon from '@material-ui/icons/CheckCircle';
|
||||
import FailureIcon from '@material-ui/icons/Error';
|
||||
import UnknownIcon from '@material-ui/icons/Help';
|
||||
import React from 'react';
|
||||
import { BuildStatus } from '../../apis/builds';
|
||||
import { BuildStatus } from '../../api/types';
|
||||
|
||||
type Props = {
|
||||
status?: BuildStatus;
|
||||
|
||||
@@ -15,3 +15,4 @@
|
||||
*/
|
||||
|
||||
export { plugin } from './plugin';
|
||||
export * from './api';
|
||||
|
||||
@@ -24,7 +24,7 @@ export const rootRouteRef = createRouteRef({
|
||||
title: 'GitHub Actions',
|
||||
});
|
||||
export const buildRouteRef = createRouteRef({
|
||||
path: '/github-actions/builds/:buildUri',
|
||||
path: '/github-actions/builds',
|
||||
title: 'GitHub Actions Build',
|
||||
});
|
||||
|
||||
|
||||
@@ -30,20 +30,6 @@ describe('ComponentIdValidators', () => {
|
||||
expect(ComponentIdValidators.httpsValidator(arg)).toBe(expected);
|
||||
});
|
||||
});
|
||||
describe('masterValidator', () => {
|
||||
const errorMessage = 'Must reference a file on the master branch.';
|
||||
test.each([
|
||||
[true, '/blob/master/'],
|
||||
[true, 'http://example.com/blob/master/'],
|
||||
[errorMessage, 'blob/master/'],
|
||||
[errorMessage, '/blob/master'],
|
||||
[errorMessage, '/master/'],
|
||||
[errorMessage, ''],
|
||||
[errorMessage, undefined],
|
||||
])('should return %p for %s', (expected: string | boolean, arg: any) => {
|
||||
expect(ComponentIdValidators.masterValidator(arg)).toBe(expected);
|
||||
});
|
||||
});
|
||||
describe('yamlValidator', () => {
|
||||
const errorMessage = "Must end with '.yaml'.";
|
||||
test.each([
|
||||
|
||||
@@ -18,9 +18,6 @@ export const ComponentIdValidators = {
|
||||
httpsValidator: (value: any) =>
|
||||
(typeof value === 'string' && value.match(/^https:\/\//) !== null) ||
|
||||
'Must start with https://.',
|
||||
masterValidator: (value: any) =>
|
||||
(typeof value === 'string' && value.match(/\/blob\/master\//) !== null) ||
|
||||
'Must reference a file on the master branch.',
|
||||
yamlValidator: (value: any) =>
|
||||
(typeof value === 'string' && value.match(/.yaml$/) !== null) ||
|
||||
"Must end with '.yaml'.",
|
||||
|
||||
@@ -8,7 +8,7 @@ metadata:
|
||||
- Recommended
|
||||
- React
|
||||
spec:
|
||||
processor: cookiecutter
|
||||
templater: cookiecutter
|
||||
type: website
|
||||
path: '.'
|
||||
schema:
|
||||
|
||||
@@ -8,7 +8,7 @@ metadata:
|
||||
- Recommended
|
||||
- Java
|
||||
spec:
|
||||
processor: cookiecutter
|
||||
templater: cookiecutter
|
||||
type: service
|
||||
path: '.'
|
||||
schema:
|
||||
|
||||
@@ -37,7 +37,8 @@ describe('JobProcessor', () => {
|
||||
generation: 1,
|
||||
},
|
||||
spec: {
|
||||
type: 'cookiecutter',
|
||||
type: 'website',
|
||||
templater: 'cookiecutter',
|
||||
path: './template',
|
||||
schema: {
|
||||
$schema: 'http://json-schema.org/draft-07/schema#',
|
||||
|
||||
@@ -47,7 +47,8 @@ describe('GitHubPreparer', () => {
|
||||
generation: 1,
|
||||
},
|
||||
spec: {
|
||||
type: 'cookiecutter',
|
||||
type: 'website',
|
||||
templater: 'cookiecutter',
|
||||
path: './template',
|
||||
schema: {
|
||||
$schema: 'http://json-schema.org/draft-07/schema#',
|
||||
|
||||
@@ -39,7 +39,8 @@ describe('Helpers', () => {
|
||||
generation: 1,
|
||||
},
|
||||
spec: {
|
||||
type: 'cookiecutter',
|
||||
type: 'website',
|
||||
templater: 'cookiecutter',
|
||||
path: './template',
|
||||
schema: {
|
||||
$schema: 'http://json-schema.org/draft-07/schema#',
|
||||
@@ -86,7 +87,8 @@ describe('Helpers', () => {
|
||||
generation: 1,
|
||||
},
|
||||
spec: {
|
||||
type: 'cookiecutter',
|
||||
type: 'website',
|
||||
templater: 'cookiecutter',
|
||||
path: './template',
|
||||
schema: {
|
||||
$schema: 'http://json-schema.org/draft-07/schema#',
|
||||
@@ -131,7 +133,8 @@ describe('Helpers', () => {
|
||||
generation: 1,
|
||||
},
|
||||
spec: {
|
||||
type: 'cookiecutter',
|
||||
type: 'website',
|
||||
templater: 'cookiecutter',
|
||||
path: './template',
|
||||
schema: {
|
||||
$schema: 'http://json-schema.org/draft-07/schema#',
|
||||
@@ -177,7 +180,8 @@ describe('Helpers', () => {
|
||||
generation: 1,
|
||||
},
|
||||
spec: {
|
||||
type: 'cookiecutter',
|
||||
type: 'website',
|
||||
templater: 'cookiecutter',
|
||||
path: './template',
|
||||
schema: {
|
||||
$schema: 'http://json-schema.org/draft-07/schema#',
|
||||
@@ -221,7 +225,8 @@ describe('Helpers', () => {
|
||||
generation: 1,
|
||||
},
|
||||
spec: {
|
||||
type: 'cookiecutter',
|
||||
type: 'website',
|
||||
templater: 'cookiecutter',
|
||||
path: './template',
|
||||
schema: {
|
||||
$schema: 'http://json-schema.org/draft-07/schema#',
|
||||
|
||||
@@ -24,7 +24,7 @@ describe('Preparers', () => {
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location':
|
||||
'file:/Users/blam/dev/spotify/backstage/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml',
|
||||
'file:/Users/bingo/spotify/backstage/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml',
|
||||
},
|
||||
name: 'react-ssr-template',
|
||||
title: 'React SSR Template',
|
||||
@@ -35,8 +35,9 @@ describe('Preparers', () => {
|
||||
generation: 1,
|
||||
},
|
||||
spec: {
|
||||
type: 'cookiecutter',
|
||||
templater: 'cookiecutter',
|
||||
path: '.',
|
||||
type: 'website',
|
||||
schema: {
|
||||
$schema: 'http://json-schema.org/draft-07/schema#',
|
||||
required: ['storePath', 'owner'],
|
||||
@@ -88,7 +89,8 @@ describe('Preparers', () => {
|
||||
generation: 1,
|
||||
},
|
||||
spec: {
|
||||
type: 'cookiecutter',
|
||||
type: 'website',
|
||||
templater: 'cookiecutter',
|
||||
path: '.',
|
||||
schema: {
|
||||
$schema: 'http://json-schema.org/draft-07/schema#',
|
||||
|
||||
@@ -14,3 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
export * from './github';
|
||||
export * from './types';
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
import { Writable, PassThrough } from 'stream';
|
||||
import Docker from 'dockerode';
|
||||
import fs from 'fs';
|
||||
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
|
||||
import { InputError } from '@backstage/backend-common';
|
||||
|
||||
export type RunDockerContainerOptions = {
|
||||
imageName: string;
|
||||
@@ -26,6 +28,20 @@ export type RunDockerContainerOptions = {
|
||||
dockerClient: Docker;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the templater key to use for templating from the entity
|
||||
* @param entity Template entity
|
||||
*/
|
||||
export const getTemplaterKey = (entity: TemplateEntityV1alpha1): string => {
|
||||
const { templater } = entity.spec;
|
||||
|
||||
if (!templater) {
|
||||
throw new InputError('Template does not have a required templating key');
|
||||
}
|
||||
|
||||
return templater;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param options the options object
|
||||
|
||||
@@ -16,3 +16,4 @@
|
||||
export * from './cookiecutter';
|
||||
export * from './types';
|
||||
export * from './helpers';
|
||||
export * from './templaters';
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { Templaters } from '.';
|
||||
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
|
||||
import { CookieCutter } from './cookiecutter';
|
||||
|
||||
describe('Templaters', () => {
|
||||
const mockTemplate: TemplateEntityV1alpha1 = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Template',
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location':
|
||||
'file:/Users/bingo/spotify/backstage/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml',
|
||||
},
|
||||
name: 'react-ssr-template',
|
||||
title: 'React SSR Template',
|
||||
description:
|
||||
'Next.js application skeleton for creating isomorphic web applications.',
|
||||
uid: '7357f4c5-aa58-4a1e-9670-18931eef771f',
|
||||
etag: 'YWUxZWQyY2EtZDkxMC00MDM0LWI0ODAtMDgwMWY0YzdlMWIw',
|
||||
generation: 1,
|
||||
},
|
||||
spec: {
|
||||
templater: 'cookiecutter',
|
||||
path: '.',
|
||||
type: 'website',
|
||||
schema: {
|
||||
$schema: 'http://json-schema.org/draft-07/schema#',
|
||||
required: ['storePath', 'owner'],
|
||||
properties: {
|
||||
owner: {
|
||||
type: 'string',
|
||||
title: 'Owner',
|
||||
description: 'Who is going to own this component',
|
||||
},
|
||||
storePath: {
|
||||
type: 'string',
|
||||
title: 'Store path',
|
||||
description: 'GitHub store path in org/repo format',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
it('should throw an error when the templater is not registered', () => {
|
||||
const templaters = new Templaters();
|
||||
|
||||
expect(() => templaters.get(mockTemplate)).toThrow(
|
||||
expect.objectContaining({
|
||||
message: 'No templater registered for template: "cookiecutter"',
|
||||
}),
|
||||
);
|
||||
});
|
||||
it('should return the correct templater when the templater matches', () => {
|
||||
const templaters = new Templaters();
|
||||
const templater = new CookieCutter();
|
||||
|
||||
templaters.register('cookiecutter', templater);
|
||||
|
||||
expect(templaters.get(mockTemplate)).toBe(templater);
|
||||
});
|
||||
|
||||
it('should throw an error if the templater does not exist in the entity', () => {
|
||||
const brokenTemplate: TemplateEntityV1alpha1 = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Template',
|
||||
metadata: {
|
||||
annotations: {},
|
||||
name: 'react-ssr-template',
|
||||
title: 'React SSR Template',
|
||||
description:
|
||||
'Next.js application skeleton for creating isomorphic web applications.',
|
||||
uid: '7357f4c5-aa58-4a1e-9670-18931eef771f',
|
||||
etag: 'YWUxZWQyY2EtZDkxMC00MDM0LWI0ODAtMDgwMWY0YzdlMWIw',
|
||||
generation: 1,
|
||||
},
|
||||
spec: {
|
||||
type: 'website',
|
||||
path: '.',
|
||||
templater: '',
|
||||
schema: {
|
||||
$schema: 'http://json-schema.org/draft-07/schema#',
|
||||
required: ['storePath', 'owner'],
|
||||
properties: {
|
||||
owner: {
|
||||
type: 'string',
|
||||
title: 'Owner',
|
||||
description: 'Who is going to own this component',
|
||||
},
|
||||
storePath: {
|
||||
type: 'string',
|
||||
title: 'Store path',
|
||||
description: 'GitHub store path in org/repo format',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const templaters = new Templaters();
|
||||
|
||||
expect(() => templaters.get(brokenTemplate)).toThrow(
|
||||
expect.objectContaining({
|
||||
name: 'InputError',
|
||||
message: expect.stringContaining(
|
||||
'Template does not have a required templating key',
|
||||
),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 {
|
||||
TemplaterBase,
|
||||
SupportedTemplatingKey,
|
||||
TemplaterBuilder,
|
||||
} from './types';
|
||||
|
||||
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
|
||||
import { getTemplaterKey } from './helpers';
|
||||
|
||||
export class Templaters implements TemplaterBuilder {
|
||||
private preparerMap = new Map<SupportedTemplatingKey, TemplaterBase>();
|
||||
|
||||
register(templaterKey: SupportedTemplatingKey, templater: TemplaterBase) {
|
||||
this.preparerMap.set(templaterKey, templater);
|
||||
}
|
||||
|
||||
get(template: TemplateEntityV1alpha1): TemplaterBase {
|
||||
const templaterKey = getTemplaterKey(template);
|
||||
const preparer = this.preparerMap.get(templaterKey);
|
||||
|
||||
if (!preparer) {
|
||||
throw new Error(
|
||||
`No templater registered for template: "${templaterKey}"`,
|
||||
);
|
||||
}
|
||||
|
||||
return preparer;
|
||||
}
|
||||
}
|
||||
@@ -13,10 +13,10 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Writable } from 'stream';
|
||||
import Docker from 'dockerode';
|
||||
import { JsonValue } from '@backstage/config';
|
||||
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
|
||||
|
||||
/**
|
||||
* Currently the required template values. The owner
|
||||
@@ -55,3 +55,16 @@ export type TemplaterBase = {
|
||||
export type TemplaterConfig = {
|
||||
templater?: TemplaterBase;
|
||||
};
|
||||
|
||||
/**
|
||||
* List of supported templating options
|
||||
*/
|
||||
export type SupportedTemplatingKey = 'cookiecutter' | string;
|
||||
|
||||
/**
|
||||
* The templater builder holds the templaters ready for run time
|
||||
*/
|
||||
export type TemplaterBuilder = {
|
||||
register(protocol: SupportedTemplatingKey, templater: TemplaterBase): void;
|
||||
get(template: TemplateEntityV1alpha1): TemplaterBase;
|
||||
};
|
||||
|
||||
@@ -16,23 +16,24 @@
|
||||
|
||||
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
|
||||
import { JsonValue } from '@backstage/config';
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import Docker from 'dockerode';
|
||||
import express from 'express';
|
||||
import Router from 'express-promise-router';
|
||||
import { Logger } from 'winston';
|
||||
import {
|
||||
GithubPublisher,
|
||||
JobProcessor,
|
||||
PreparerBuilder,
|
||||
RequiredTemplateValues,
|
||||
StageContext,
|
||||
TemplaterBase,
|
||||
TemplaterBuilder,
|
||||
Publisher,
|
||||
} from '../scaffolder';
|
||||
|
||||
export interface RouterOptions {
|
||||
preparers: PreparerBuilder;
|
||||
templater: TemplaterBase;
|
||||
templaters: TemplaterBuilder;
|
||||
publisher: Publisher;
|
||||
|
||||
logger: Logger;
|
||||
dockerClient: Docker;
|
||||
}
|
||||
@@ -42,9 +43,14 @@ export async function createRouter(
|
||||
): Promise<express.Router> {
|
||||
const router = Router();
|
||||
|
||||
const githubClient = new Octokit({ auth: process.env.GITHUB_ACCESS_TOKEN });
|
||||
const { preparers, templater, logger: parentLogger, dockerClient } = options;
|
||||
const githubPulisher = new GithubPublisher({ client: githubClient });
|
||||
const {
|
||||
preparers,
|
||||
templaters,
|
||||
publisher,
|
||||
logger: parentLogger,
|
||||
dockerClient,
|
||||
} = options;
|
||||
|
||||
const logger = parentLogger.child({ plugin: 'scaffolder' });
|
||||
const jobProcessor = new JobProcessor();
|
||||
|
||||
@@ -106,6 +112,7 @@ export async function createRouter(
|
||||
{
|
||||
name: 'Run the templater',
|
||||
handler: async (ctx: StageContext<{ skeletonDir: string }>) => {
|
||||
const templater = templaters.get(ctx.entity);
|
||||
const { resultDir } = await templater.run({
|
||||
directory: ctx.skeletonDir,
|
||||
dockerClient,
|
||||
@@ -120,7 +127,8 @@ export async function createRouter(
|
||||
name: 'Publish template',
|
||||
handler: async (ctx: StageContext<{ resultDir: string }>) => {
|
||||
ctx.logger.info('Should not store the template');
|
||||
const { remoteUrl } = await githubPulisher.publish({
|
||||
const { remoteUrl } = await publisher.publish({
|
||||
entity: ctx.entity,
|
||||
values: ctx.values,
|
||||
directory: ctx.resultDir,
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ It is only meant for local development, and the setup for it can be found inside
|
||||
|
||||
### Custom Storage URL
|
||||
|
||||
TechDocs currently reads a static HTML file, generated by Mkdocs (see our `plugins/techdocs/mkdocs/container` folder for more documentation) and stored on an external server, and loads that into Backstage. By default, we have set up a mock server with some example documentation sites over in Google Cloud Storage:
|
||||
TechDocs currently reads a static HTML file, generated by Mkdocs (see our `packages/techdocs-container` folder for more documentation) and stored on an external server, and loads that into Backstage. By default, we have set up a mock server with some example documentation sites over in Google Cloud Storage:
|
||||
|
||||
```md
|
||||
# Base URL
|
||||
@@ -36,7 +36,7 @@ https://techdocs-mock-sites.storage.googleapis.com/mkdocs/index.html
|
||||
https://techdocs-mock-sites.storage.googleapis.com/backstage-microsite/index.html
|
||||
```
|
||||
|
||||
Using your own setup (or ours which is being worked on as of Q3 2020), you can point it to your own server with your own hosted documentation sites. The only requirement is that it the output is from [Mkdocs](https://mkdocs.org) with the Material theme. You can always use our documentation generation tool located at `plugins/techdocs/mkdocs/container` for easy setup.
|
||||
Using your own setup (or ours which is being worked on as of Q3 2020), you can point it to your own server with your own hosted documentation sites. The only requirement is that it the output is from [Mkdocs](https://mkdocs.org) with the Material theme. You can always use our documentation generation tool located at `packages/techdocs-container` for easy setup.
|
||||
|
||||
To point TechDocs to your own server, simply update the `techdocs.storageUrl` value in your `app-config.yaml` file or set the environment variable `APP_CONFIG_techdocs_storageUrl` in your application:
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
# MkDocs
|
||||
|
||||
Welcome to MkDocs. This is the TechDocs implementation of MkDocs.
|
||||
|
||||
**WIP: This is a work in progress. It is not ready for use yet. Follow our progress on [the Backstage Discord](https://discord.gg/MUpMjP2) under #docs-like-code or on [our GitHub Milestone](https://github.com/spotify/backstage/milestone/15).**
|
||||
|
||||
## Getting started
|
||||
|
||||
```bash
|
||||
docker build ./container -t mkdocs-container
|
||||
|
||||
docker run -w /content -v $(pwd)/mock-docs:/content -p 8000:8000 -it mkdocs-container serve -a 0.0.0.0:8000
|
||||
```
|
||||
|
||||
Then open up `http://localhost:8000` on your local machine.
|
||||
@@ -4,7 +4,7 @@
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.esm.js",
|
||||
|
||||
@@ -10182,6 +10182,15 @@ http-proxy@^1.17.0:
|
||||
follow-redirects "^1.0.0"
|
||||
requires-port "^1.0.0"
|
||||
|
||||
http-proxy@^1.18.1:
|
||||
version "1.18.1"
|
||||
resolved "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549"
|
||||
integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==
|
||||
dependencies:
|
||||
eventemitter3 "^4.0.0"
|
||||
follow-redirects "^1.0.0"
|
||||
requires-port "^1.0.0"
|
||||
|
||||
http-signature@~1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
|
||||
|
||||
Reference in New Issue
Block a user