Merge branch 'master' of github.com:spotify/backstage into shmidt-i/create-app-backend
This commit is contained in:
@@ -21,8 +21,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.1.1-alpha.16",
|
||||
"@backstage/config": "^0.1.1-alpha.13",
|
||||
"@backstage/config-loader": "^0.1.1-alpha.13",
|
||||
"@backstage/config": "^0.1.1-alpha.16",
|
||||
"@backstage/config-loader": "^0.1.1-alpha.16",
|
||||
"@types/express": "^4.17.6",
|
||||
"body-parser": "^1.19.0",
|
||||
"compression": "^1.7.4",
|
||||
|
||||
@@ -24,6 +24,11 @@ import { createOAuth2Provider } from './oauth2';
|
||||
import { createOktaProvider } from './okta';
|
||||
import { createSamlProvider } from './saml';
|
||||
import { AuthProviderConfig, AuthProviderFactory } from './types';
|
||||
import { Config } from '@backstage/config';
|
||||
import {
|
||||
EnvironmentHandlers,
|
||||
EnvironmentHandler,
|
||||
} from '../lib/EnvironmentHandler';
|
||||
|
||||
const factories: { [providerId: string]: AuthProviderFactory } = {
|
||||
google: createGoogleProvider,
|
||||
@@ -37,7 +42,7 @@ const factories: { [providerId: string]: AuthProviderFactory } = {
|
||||
export const createAuthProviderRouter = (
|
||||
providerId: string,
|
||||
globalConfig: AuthProviderConfig,
|
||||
providerConfig: any, // TODO: make this a config reader object of sorts
|
||||
providerConfig: Config,
|
||||
logger: Logger,
|
||||
issuer: TokenIssuer,
|
||||
) => {
|
||||
@@ -46,17 +51,28 @@ export const createAuthProviderRouter = (
|
||||
throw Error(`No auth provider available for '${providerId}'`);
|
||||
}
|
||||
|
||||
const provider = factory(globalConfig, providerConfig, logger, issuer);
|
||||
|
||||
const router = Router();
|
||||
router.get('/start', provider.start.bind(provider));
|
||||
router.get('/handler/frame', provider.frameHandler.bind(provider));
|
||||
router.post('/handler/frame', provider.frameHandler.bind(provider));
|
||||
if (provider.logout) {
|
||||
router.post('/logout', provider.logout.bind(provider));
|
||||
const envs = providerConfig.keys();
|
||||
const envProviders: EnvironmentHandlers = {};
|
||||
|
||||
for (const env of envs) {
|
||||
const envConfig = providerConfig.getConfig(env);
|
||||
const provider = factory(globalConfig, env, envConfig, logger, issuer);
|
||||
if (provider) {
|
||||
envProviders[env] = provider;
|
||||
}
|
||||
}
|
||||
if (provider.refresh) {
|
||||
router.get('/refresh', provider.refresh.bind(provider));
|
||||
|
||||
const handler = new EnvironmentHandler(providerId, envProviders);
|
||||
|
||||
router.get('/start', handler.start.bind(handler));
|
||||
router.get('/handler/frame', handler.frameHandler.bind(handler));
|
||||
router.post('/handler/frame', handler.frameHandler.bind(handler));
|
||||
if (handler.logout) {
|
||||
router.post('/logout', handler.logout.bind(handler));
|
||||
}
|
||||
if (handler.refresh) {
|
||||
router.get('/refresh', handler.refresh.bind(handler));
|
||||
}
|
||||
|
||||
return router;
|
||||
|
||||
@@ -25,20 +25,15 @@ import {
|
||||
OAuthProviderHandlers,
|
||||
AuthProviderConfig,
|
||||
RedirectInfo,
|
||||
EnvironmentProviderConfig,
|
||||
OAuthProviderOptions,
|
||||
OAuthProviderConfig,
|
||||
OAuthResponse,
|
||||
PassportDoneCallback,
|
||||
} from '../types';
|
||||
import { OAuthProvider } from '../../lib/OAuthProvider';
|
||||
import {
|
||||
EnvironmentHandlers,
|
||||
EnvironmentHandler,
|
||||
} from '../../lib/EnvironmentHandler';
|
||||
import { Logger } from 'winston';
|
||||
import { TokenIssuer } from '../../identity';
|
||||
import passport from 'passport';
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
export class GithubAuthProvider implements OAuthProviderHandlers {
|
||||
private readonly _strategy: GithubStrategy;
|
||||
@@ -131,44 +126,43 @@ export class GithubAuthProvider implements OAuthProviderHandlers {
|
||||
|
||||
export function createGithubProvider(
|
||||
{ baseUrl }: AuthProviderConfig,
|
||||
providerConfig: EnvironmentProviderConfig,
|
||||
env: string,
|
||||
envConfig: Config,
|
||||
logger: Logger,
|
||||
tokenIssuer: TokenIssuer,
|
||||
) {
|
||||
const providerId = 'github';
|
||||
const envProviders: EnvironmentHandlers = {};
|
||||
const secure = envConfig.getBoolean('secure');
|
||||
const appOrigin = envConfig.getString('appOrigin');
|
||||
const clientID = envConfig.getString('clientId');
|
||||
const clientSecret = envConfig.getString('clientSecret');
|
||||
const callbackURL = `${baseUrl}/${providerId}/handler/frame?env=${env}`;
|
||||
|
||||
for (const [env, envConfig] of Object.entries(providerConfig)) {
|
||||
const config = (envConfig as unknown) as OAuthProviderConfig;
|
||||
const { secure, appOrigin } = config;
|
||||
const opts = {
|
||||
clientID: config.clientId,
|
||||
clientSecret: config.clientSecret,
|
||||
callbackURL: `${baseUrl}/${providerId}/handler/frame?env=${env}`,
|
||||
};
|
||||
const opts = {
|
||||
clientID,
|
||||
clientSecret,
|
||||
callbackURL,
|
||||
};
|
||||
|
||||
if (!opts.clientID || !opts.clientSecret) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
throw new Error(
|
||||
'Failed to initialize Github auth provider, set AUTH_GITHUB_CLIENT_ID and AUTH_GITHUB_CLIENT_SECRET env vars',
|
||||
);
|
||||
}
|
||||
|
||||
logger.warn(
|
||||
'Github auth provider disabled, set AUTH_GITHUB_CLIENT_ID and AUTH_GITHUB_CLIENT_SECRET env vars to enable',
|
||||
if (!opts.clientID || !opts.clientSecret) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
throw new Error(
|
||||
'Failed to initialize Github auth provider, set AUTH_GITHUB_CLIENT_ID and AUTH_GITHUB_CLIENT_SECRET env vars',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
envProviders[env] = new OAuthProvider(new GithubAuthProvider(opts), {
|
||||
disableRefresh: true,
|
||||
persistScopes: true,
|
||||
providerId,
|
||||
secure,
|
||||
baseUrl,
|
||||
appOrigin,
|
||||
tokenIssuer,
|
||||
});
|
||||
logger.warn(
|
||||
'Github auth provider disabled, set AUTH_GITHUB_CLIENT_ID and AUTH_GITHUB_CLIENT_SECRET env vars to enable',
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
return new EnvironmentHandler(providerId, envProviders);
|
||||
return new OAuthProvider(new GithubAuthProvider(opts), {
|
||||
disableRefresh: true,
|
||||
persistScopes: true,
|
||||
providerId,
|
||||
secure,
|
||||
baseUrl,
|
||||
appOrigin,
|
||||
tokenIssuer,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -25,20 +25,15 @@ import {
|
||||
OAuthProviderHandlers,
|
||||
AuthProviderConfig,
|
||||
RedirectInfo,
|
||||
EnvironmentProviderConfig,
|
||||
OAuthProviderOptions,
|
||||
OAuthProviderConfig,
|
||||
OAuthResponse,
|
||||
PassportDoneCallback,
|
||||
} from '../types';
|
||||
import { OAuthProvider } from '../../lib/OAuthProvider';
|
||||
import {
|
||||
EnvironmentHandlers,
|
||||
EnvironmentHandler,
|
||||
} from '../../lib/EnvironmentHandler';
|
||||
import { Logger } from 'winston';
|
||||
import { TokenIssuer } from '../../identity';
|
||||
import passport from 'passport';
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
export class GitlabAuthProvider implements OAuthProviderHandlers {
|
||||
private readonly _strategy: GitlabStrategy;
|
||||
@@ -138,49 +133,45 @@ export class GitlabAuthProvider implements OAuthProviderHandlers {
|
||||
|
||||
export function createGitlabProvider(
|
||||
{ baseUrl }: AuthProviderConfig,
|
||||
providerConfig: EnvironmentProviderConfig,
|
||||
env: string,
|
||||
envConfig: Config,
|
||||
logger: Logger,
|
||||
tokenIssuer: TokenIssuer,
|
||||
) {
|
||||
const providerId = 'gitlab';
|
||||
const envProviders: EnvironmentHandlers = {};
|
||||
const secure = envConfig.getBoolean('secure');
|
||||
const appOrigin = envConfig.getString('appOrigin');
|
||||
const clientID = envConfig.getString('clientId');
|
||||
const clientSecret = envConfig.getString('clientSecret');
|
||||
const audience = envConfig.getString('audience');
|
||||
const baseURL = audience || 'https://gitlab.com';
|
||||
const callbackURL = `${baseUrl}/${providerId}/handler/frame?env=${env}`;
|
||||
|
||||
for (const [env, envConfig] of Object.entries(providerConfig)) {
|
||||
const {
|
||||
secure,
|
||||
appOrigin,
|
||||
clientId,
|
||||
clientSecret,
|
||||
audience,
|
||||
} = (envConfig as unknown) as OAuthProviderConfig;
|
||||
const opts = {
|
||||
clientID: clientId,
|
||||
clientSecret: clientSecret,
|
||||
callbackURL: `${baseUrl}/${providerId}/handler/frame?env=${env}`,
|
||||
baseURL: audience,
|
||||
};
|
||||
const opts = {
|
||||
clientID,
|
||||
clientSecret,
|
||||
callbackURL,
|
||||
baseURL,
|
||||
};
|
||||
|
||||
if (!opts.clientID || !opts.clientSecret) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
throw new Error(
|
||||
'Failed to initialize Gitlab auth provider, set AUTH_GITLAB_CLIENT_ID and AUTH_GITLAB_CLIENT_SECRET env vars',
|
||||
);
|
||||
}
|
||||
|
||||
logger.warn(
|
||||
'Gitlab auth provider disabled, set AUTH_GITLAB_CLIENT_ID and AUTH_GITLAB_CLIENT_SECRET env vars to enable',
|
||||
if (!opts.clientID || !opts.clientSecret) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
throw new Error(
|
||||
'Failed to initialize Gitlab auth provider, set AUTH_GITLAB_CLIENT_ID and AUTH_GITLAB_CLIENT_SECRET env vars',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
envProviders[env] = new OAuthProvider(new GitlabAuthProvider(opts), {
|
||||
disableRefresh: true,
|
||||
providerId,
|
||||
secure,
|
||||
baseUrl,
|
||||
appOrigin,
|
||||
tokenIssuer,
|
||||
});
|
||||
logger.warn(
|
||||
'Gitlab auth provider disabled, set AUTH_GITLAB_CLIENT_ID and AUTH_GITLAB_CLIENT_SECRET env vars to enable',
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
return new EnvironmentHandler(providerId, envProviders);
|
||||
return new OAuthProvider(new GitlabAuthProvider(opts), {
|
||||
disableRefresh: true,
|
||||
providerId,
|
||||
secure,
|
||||
baseUrl,
|
||||
appOrigin,
|
||||
tokenIssuer,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -27,20 +27,15 @@ import {
|
||||
OAuthProviderHandlers,
|
||||
RedirectInfo,
|
||||
AuthProviderConfig,
|
||||
EnvironmentProviderConfig,
|
||||
OAuthProviderOptions,
|
||||
OAuthProviderConfig,
|
||||
OAuthResponse,
|
||||
PassportDoneCallback,
|
||||
} from '../types';
|
||||
import { OAuthProvider } from '../../lib/OAuthProvider';
|
||||
import passport from 'passport';
|
||||
import {
|
||||
EnvironmentHandler,
|
||||
EnvironmentHandlers,
|
||||
} from '../../lib/EnvironmentHandler';
|
||||
import { Logger } from 'winston';
|
||||
import { TokenIssuer } from '../../identity';
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
type PrivateInfo = {
|
||||
refreshToken: string;
|
||||
@@ -150,43 +145,42 @@ export class GoogleAuthProvider implements OAuthProviderHandlers {
|
||||
|
||||
export function createGoogleProvider(
|
||||
{ baseUrl }: AuthProviderConfig,
|
||||
providerConfig: EnvironmentProviderConfig,
|
||||
env: string,
|
||||
envConfig: Config,
|
||||
logger: Logger,
|
||||
tokenIssuer: TokenIssuer,
|
||||
) {
|
||||
const providerId = 'google';
|
||||
const envProviders: EnvironmentHandlers = {};
|
||||
const secure = envConfig.getBoolean('secure');
|
||||
const appOrigin = envConfig.getString('appOrigin');
|
||||
const clientID = envConfig.getString('clientId');
|
||||
const clientSecret = envConfig.getString('clientSecret');
|
||||
const callbackURL = `${baseUrl}/${providerId}/handler/frame?env=${env}`;
|
||||
|
||||
for (const [env, envConfig] of Object.entries(providerConfig)) {
|
||||
const config = (envConfig as unknown) as OAuthProviderConfig;
|
||||
const { secure, appOrigin } = config;
|
||||
const opts = {
|
||||
clientID: config.clientId,
|
||||
clientSecret: config.clientSecret,
|
||||
callbackURL: `${baseUrl}/${providerId}/handler/frame?env=${env}`,
|
||||
};
|
||||
const opts = {
|
||||
clientID,
|
||||
clientSecret,
|
||||
callbackURL,
|
||||
};
|
||||
|
||||
if (!opts.clientID || !opts.clientSecret) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
throw new Error(
|
||||
'Failed to initialize Google auth provider, set AUTH_GOOGLE_CLIENT_ID and AUTH_GOOGLE_CLIENT_SECRET env vars',
|
||||
);
|
||||
}
|
||||
|
||||
logger.warn(
|
||||
'Google auth provider disabled, set AUTH_GOOGLE_CLIENT_ID and AUTH_GOOGLE_CLIENT_SECRET env vars to enable',
|
||||
if (!opts.clientID || !opts.clientSecret) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
throw new Error(
|
||||
'Failed to initialize Google auth provider, set AUTH_GOOGLE_CLIENT_ID and AUTH_GOOGLE_CLIENT_SECRET env vars',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
envProviders[env] = new OAuthProvider(new GoogleAuthProvider(opts), {
|
||||
disableRefresh: false,
|
||||
providerId,
|
||||
secure,
|
||||
baseUrl,
|
||||
appOrigin,
|
||||
tokenIssuer,
|
||||
});
|
||||
logger.warn(
|
||||
'Google auth provider disabled, set AUTH_GOOGLE_CLIENT_ID and AUTH_GOOGLE_CLIENT_SECRET env vars to enable',
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
return new EnvironmentHandler(providerId, envProviders);
|
||||
return new OAuthProvider(new GoogleAuthProvider(opts), {
|
||||
disableRefresh: false,
|
||||
providerId,
|
||||
secure,
|
||||
baseUrl,
|
||||
appOrigin,
|
||||
tokenIssuer,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -19,10 +19,6 @@ import passport from 'passport';
|
||||
import { Strategy as OAuth2Strategy } from 'passport-oauth2';
|
||||
import { Logger } from 'winston';
|
||||
import { TokenIssuer } from '../../identity';
|
||||
import {
|
||||
EnvironmentHandler,
|
||||
EnvironmentHandlers,
|
||||
} from '../../lib/EnvironmentHandler';
|
||||
import { OAuthProvider } from '../../lib/OAuthProvider';
|
||||
import {
|
||||
executeFetchUserProfileStrategy,
|
||||
@@ -33,14 +29,13 @@ import {
|
||||
} from '../../lib/PassportStrategyHelper';
|
||||
import {
|
||||
AuthProviderConfig,
|
||||
EnvironmentProviderConfig,
|
||||
GenericOAuth2ProviderConfig,
|
||||
GenericOAuth2ProviderOptions,
|
||||
OAuthProviderHandlers,
|
||||
OAuthResponse,
|
||||
PassportDoneCallback,
|
||||
RedirectInfo,
|
||||
} from '../types';
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
type PrivateInfo = {
|
||||
refreshToken: string;
|
||||
@@ -148,51 +143,51 @@ export class OAuth2AuthProvider implements OAuthProviderHandlers {
|
||||
|
||||
export function createOAuth2Provider(
|
||||
{ baseUrl }: AuthProviderConfig,
|
||||
providerConfig: EnvironmentProviderConfig,
|
||||
env: string,
|
||||
envConfig: Config,
|
||||
logger: Logger,
|
||||
tokenIssuer: TokenIssuer,
|
||||
) {
|
||||
const providerId = 'oauth2';
|
||||
const envProviders: EnvironmentHandlers = {};
|
||||
const secure = envConfig.getBoolean('secure');
|
||||
const appOrigin = envConfig.getString('appOrigin');
|
||||
const clientID = envConfig.getString('clientId');
|
||||
const clientSecret = envConfig.getString('clientSecret');
|
||||
const callbackURL = `${baseUrl}/${providerId}/handler/frame?env=${env}`;
|
||||
const authorizationURL = envConfig.getString('authorizationURL');
|
||||
const tokenURL = envConfig.getString('tokenURL');
|
||||
|
||||
for (const [env, envConfig] of Object.entries(providerConfig)) {
|
||||
const config = (envConfig as unknown) as GenericOAuth2ProviderConfig;
|
||||
const { secure, appOrigin } = config;
|
||||
const opts = {
|
||||
clientID: config.clientId,
|
||||
clientSecret: config.clientSecret,
|
||||
callbackURL: `${baseUrl}/${providerId}/handler/frame?env=${env}`,
|
||||
authorizationURL: config.authorizationURL,
|
||||
tokenURL: config.tokenURL,
|
||||
};
|
||||
const opts = {
|
||||
clientID,
|
||||
clientSecret,
|
||||
callbackURL,
|
||||
authorizationURL,
|
||||
tokenURL,
|
||||
};
|
||||
|
||||
if (
|
||||
!opts.clientID ||
|
||||
!opts.clientSecret ||
|
||||
!opts.authorizationURL ||
|
||||
!opts.tokenURL
|
||||
) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
throw new Error(
|
||||
'Failed to initialize OAuth2 auth provider, set AUTH_OAUTH2_CLIENT_ID, AUTH_OAUTH2_CLIENT_SECRET, AUTH_OAUTH2_AUTH_URL, and AUTH_OAUTH2_TOKEN_URL env vars',
|
||||
);
|
||||
}
|
||||
|
||||
logger.warn(
|
||||
'OAuth2 auth provider disabled, set AUTH_OAUTH2_CLIENT_ID, AUTH_OAUTH2_CLIENT_SECRET, AUTH_OAUTH2_AUTH_URL, and AUTH_OAUTH2_TOKEN_URL env vars to enable',
|
||||
if (
|
||||
!opts.clientID ||
|
||||
!opts.clientSecret ||
|
||||
!opts.authorizationURL ||
|
||||
!opts.tokenURL
|
||||
) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
throw new Error(
|
||||
'Failed to initialize OAuth2 auth provider, set AUTH_OAUTH2_CLIENT_ID, AUTH_OAUTH2_CLIENT_SECRET, AUTH_OAUTH2_AUTH_URL, and AUTH_OAUTH2_TOKEN_URL env vars',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
envProviders[env] = new OAuthProvider(new OAuth2AuthProvider(opts), {
|
||||
disableRefresh: false,
|
||||
providerId,
|
||||
secure,
|
||||
baseUrl,
|
||||
appOrigin,
|
||||
tokenIssuer,
|
||||
});
|
||||
logger.warn(
|
||||
'OAuth2 auth provider disabled, set AUTH_OAUTH2_CLIENT_ID, AUTH_OAUTH2_CLIENT_SECRET, AUTH_OAUTH2_AUTH_URL, and AUTH_OAUTH2_TOKEN_URL env vars to enable',
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return new EnvironmentHandler(providerId, envProviders);
|
||||
return new OAuthProvider(new OAuth2AuthProvider(opts), {
|
||||
disableRefresh: false,
|
||||
providerId,
|
||||
secure,
|
||||
baseUrl,
|
||||
appOrigin,
|
||||
tokenIssuer,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -28,19 +28,14 @@ import {
|
||||
OAuthProviderHandlers,
|
||||
RedirectInfo,
|
||||
AuthProviderConfig,
|
||||
EnvironmentProviderConfig,
|
||||
OAuthProviderOptions,
|
||||
OAuthProviderConfig,
|
||||
OAuthResponse,
|
||||
PassportDoneCallback,
|
||||
} from '../types';
|
||||
import {
|
||||
EnvironmentHandler,
|
||||
EnvironmentHandlers,
|
||||
} from '../../lib/EnvironmentHandler';
|
||||
import { Logger } from 'winston';
|
||||
import { StateStore } from 'passport-oauth2';
|
||||
import { TokenIssuer } from '../../identity';
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
type PrivateInfo = {
|
||||
refreshToken: string;
|
||||
@@ -170,45 +165,44 @@ export class OktaAuthProvider implements OAuthProviderHandlers {
|
||||
|
||||
export function createOktaProvider(
|
||||
{ baseUrl }: AuthProviderConfig,
|
||||
providerConfig: EnvironmentProviderConfig,
|
||||
env: string,
|
||||
envConfig: Config,
|
||||
logger: Logger,
|
||||
tokenIssuer: TokenIssuer,
|
||||
) {
|
||||
const providerId = 'okta';
|
||||
const envProviders: EnvironmentHandlers = {};
|
||||
const secure = envConfig.getBoolean('secure');
|
||||
const appOrigin = envConfig.getString('appOrigin');
|
||||
const clientID = envConfig.getString('clientId');
|
||||
const clientSecret = envConfig.getString('clientSecret');
|
||||
const audience = envConfig.getString('audience');
|
||||
const callbackURL = `${baseUrl}/${providerId}/handler/frame?env=${env}`;
|
||||
|
||||
for (const [env, envConfig] of Object.entries(providerConfig)) {
|
||||
const config = (envConfig as unknown) as OAuthProviderConfig;
|
||||
const { secure, appOrigin } = config;
|
||||
const opts = {
|
||||
audience: config.audience,
|
||||
clientID: config.clientId,
|
||||
clientSecret: config.clientSecret,
|
||||
callbackURL: `${baseUrl}/${providerId}/handler/frame?env=${env}`,
|
||||
};
|
||||
const opts = {
|
||||
audience,
|
||||
clientID,
|
||||
clientSecret,
|
||||
callbackURL,
|
||||
};
|
||||
|
||||
if (!opts.clientID || !opts.clientSecret || !opts.audience) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
throw new Error(
|
||||
'Failed to initialize Okta auth provider, set AUTH_OKTA_CLIENT_ID, AUTH_OKTA_CLIENT_SECRET, and AUTH_OKTA_AUDIENCE env vars',
|
||||
);
|
||||
}
|
||||
|
||||
logger.warn(
|
||||
'Okta auth provider disabled, set AUTH_OKTA_CLIENT_ID, AUTH_OKTA_CLIENT_SECRET, and AUTH_OKTA_AUDIENCE env vars to enable',
|
||||
if (!opts.clientID || !opts.clientSecret || !opts.audience) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
throw new Error(
|
||||
'Failed to initialize Okta auth provider, set AUTH_OKTA_CLIENT_ID, AUTH_OKTA_CLIENT_SECRET, and AUTH_OKTA_AUDIENCE env vars',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
envProviders[env] = new OAuthProvider(new OktaAuthProvider(opts), {
|
||||
disableRefresh: false,
|
||||
providerId,
|
||||
secure,
|
||||
baseUrl,
|
||||
appOrigin,
|
||||
tokenIssuer,
|
||||
});
|
||||
logger.warn(
|
||||
'Okta auth provider disabled, set AUTH_OKTA_CLIENT_ID, AUTH_OKTA_CLIENT_SECRET, and AUTH_OKTA_AUDIENCE env vars to enable',
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return new EnvironmentHandler(providerId, envProviders);
|
||||
return new OAuthProvider(new OktaAuthProvider(opts), {
|
||||
disableRefresh: false,
|
||||
providerId,
|
||||
secure,
|
||||
baseUrl,
|
||||
appOrigin,
|
||||
tokenIssuer,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -27,18 +27,13 @@ import {
|
||||
import {
|
||||
AuthProviderConfig,
|
||||
AuthProviderRouteHandlers,
|
||||
EnvironmentProviderConfig,
|
||||
SAMLProviderConfig,
|
||||
PassportDoneCallback,
|
||||
ProfileInfo,
|
||||
} from '../types';
|
||||
import { postMessageResponse } from '../../lib/OAuthProvider';
|
||||
import {
|
||||
EnvironmentHandlers,
|
||||
EnvironmentHandler,
|
||||
} from '../../lib/EnvironmentHandler';
|
||||
import { Logger } from 'winston';
|
||||
import { TokenIssuer } from '../../identity';
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
type SamlInfo = {
|
||||
userId: string;
|
||||
@@ -122,30 +117,25 @@ type SAMLProviderOptions = {
|
||||
|
||||
export function createSamlProvider(
|
||||
_authProviderConfig: AuthProviderConfig,
|
||||
providerConfig: EnvironmentProviderConfig,
|
||||
_env: string,
|
||||
envConfig: Config,
|
||||
logger: Logger,
|
||||
tokenIssuer: TokenIssuer,
|
||||
) {
|
||||
const envProviders: EnvironmentHandlers = {};
|
||||
const entryPoint = envConfig.getString('entryPoint');
|
||||
const issuer = envConfig.getString('issuer');
|
||||
const opts = {
|
||||
entryPoint,
|
||||
issuer,
|
||||
path: '/auth/saml/handler/frame',
|
||||
tokenIssuer,
|
||||
};
|
||||
|
||||
for (const [env, envConfig] of Object.entries(providerConfig)) {
|
||||
const config = (envConfig as unknown) as SAMLProviderConfig;
|
||||
const opts = {
|
||||
entryPoint: config.entryPoint,
|
||||
issuer: config.issuer,
|
||||
path: '/auth/saml/handler/frame',
|
||||
tokenIssuer,
|
||||
};
|
||||
|
||||
if (!opts.entryPoint || !opts.issuer) {
|
||||
logger.warn(
|
||||
'SAML auth provider disabled, set entryPoint and entryPoint in saml auth config to enable',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
envProviders[env] = new SamlAuthProvider(opts);
|
||||
if (!opts.entryPoint || !opts.issuer) {
|
||||
logger.warn(
|
||||
'SAML auth provider disabled, set entryPoint and entryPoint in saml auth config to enable',
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return new EnvironmentHandler('saml', envProviders);
|
||||
return new SamlAuthProvider(opts);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
import express from 'express';
|
||||
import { Logger } from 'winston';
|
||||
import { TokenIssuer } from '../identity';
|
||||
import { Config } from '@backstage/config';
|
||||
import { OAuthProvider } from '../lib/OAuthProvider';
|
||||
import { SamlAuthProvider } from './saml/provider';
|
||||
|
||||
export type OAuthProviderOptions = {
|
||||
/**
|
||||
@@ -204,10 +207,11 @@ export interface AuthProviderRouteHandlers {
|
||||
|
||||
export type AuthProviderFactory = (
|
||||
globalConfig: AuthProviderConfig,
|
||||
providerConfig: EnvironmentProviderConfig,
|
||||
env: string,
|
||||
envConfig: Config,
|
||||
logger: Logger,
|
||||
issuer: TokenIssuer,
|
||||
) => AuthProviderRouteHandlers;
|
||||
) => OAuthProvider | SamlAuthProvider | undefined;
|
||||
|
||||
export type AuthResponse<ProviderInfo> = {
|
||||
providerInfo: ProviderInfo;
|
||||
|
||||
@@ -36,8 +36,6 @@ export async function createRouter(
|
||||
const router = Router();
|
||||
const logger = options.logger.child({ plugin: 'auth' });
|
||||
|
||||
const appUrl = new URL(options.config.getString('app.baseUrl'));
|
||||
const appOrigin = appUrl.origin;
|
||||
const backendUrl = options.config.getString('backend.baseUrl');
|
||||
const authUrl = `${backendUrl}/auth`;
|
||||
|
||||
@@ -57,71 +55,13 @@ export async function createRouter(
|
||||
router.use(bodyParser.urlencoded({ extended: false }));
|
||||
router.use(bodyParser.json());
|
||||
|
||||
const config = {
|
||||
backend: {
|
||||
baseUrl: backendUrl,
|
||||
},
|
||||
auth: {
|
||||
providers: {
|
||||
google: {
|
||||
development: {
|
||||
appOrigin,
|
||||
secure: false,
|
||||
clientId: process.env.AUTH_GOOGLE_CLIENT_ID!,
|
||||
clientSecret: process.env.AUTH_GOOGLE_CLIENT_SECRET!,
|
||||
},
|
||||
},
|
||||
github: {
|
||||
development: {
|
||||
appOrigin,
|
||||
secure: false,
|
||||
clientId: process.env.AUTH_GITHUB_CLIENT_ID!,
|
||||
clientSecret: process.env.AUTH_GITHUB_CLIENT_SECRET!,
|
||||
},
|
||||
},
|
||||
gitlab: {
|
||||
development: {
|
||||
appOrigin,
|
||||
secure: false,
|
||||
clientId: process.env.AUTH_GITLAB_CLIENT_ID!,
|
||||
clientSecret: process.env.AUTH_GITLAB_CLIENT_SECRET!,
|
||||
audience: process.env.GITLAB_BASE_URL! || 'https://gitlab.com',
|
||||
},
|
||||
},
|
||||
saml: {
|
||||
development: {
|
||||
entryPoint: 'http://localhost:7001/',
|
||||
issuer: 'passport-saml',
|
||||
},
|
||||
},
|
||||
okta: {
|
||||
development: {
|
||||
appOrigin,
|
||||
secure: false,
|
||||
clientId: process.env.AUTH_OKTA_CLIENT_ID!,
|
||||
clientSecret: process.env.AUTH_OKTA_CLIENT_SECRET!,
|
||||
audience: process.env.AUTH_OKTA_AUDIENCE,
|
||||
},
|
||||
},
|
||||
oauth2: {
|
||||
development: {
|
||||
appOrigin,
|
||||
secure: false,
|
||||
clientId: process.env.AUTH_OAUTH2_CLIENT_ID!,
|
||||
clientSecret: process.env.AUTH_OAUTH2_CLIENT_SECRET!,
|
||||
authorizationURL: process.env.AUTH_OAUTH2_AUTH_URL!,
|
||||
tokenURL: process.env.AUTH_OAUTH2_TOKEN_URL!,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const providersConfig = options.config.getConfig('auth.providers');
|
||||
const providers = providersConfig.keys();
|
||||
|
||||
const providerConfigs = config.auth.providers;
|
||||
|
||||
for (const [providerId, providerConfig] of Object.entries(providerConfigs)) {
|
||||
for (const providerId of providers) {
|
||||
logger.info(`Configuring provider, ${providerId}`);
|
||||
try {
|
||||
const providerConfig = providersConfig.getConfig(providerId);
|
||||
const providerRouter = createAuthProviderRouter(
|
||||
providerId,
|
||||
{ baseUrl: authUrl },
|
||||
|
||||
@@ -27,6 +27,7 @@ import { EntityPolicyProcessor } from './processors/EntityPolicyProcessor';
|
||||
import { FileReaderProcessor } from './processors/FileReaderProcessor';
|
||||
import { GithubReaderProcessor } from './processors/GithubReaderProcessor';
|
||||
import { GithubApiReaderProcessor } from './processors/GithubApiReaderProcessor';
|
||||
import { GitlabApiReaderProcessor } from './processors/GitlabApiReaderProcessor';
|
||||
import { GitlabReaderProcessor } from './processors/GitlabReaderProcessor';
|
||||
import { LocationRefProcessor } from './processors/LocationEntityProcessor';
|
||||
import * as result from './processors/results';
|
||||
@@ -59,6 +60,7 @@ export class LocationReaders implements LocationReader {
|
||||
new FileReaderProcessor(),
|
||||
new GithubReaderProcessor(),
|
||||
new GithubApiReaderProcessor(),
|
||||
new GitlabApiReaderProcessor(),
|
||||
new GitlabReaderProcessor(),
|
||||
new YamlProcessor(),
|
||||
new EntityPolicyProcessor(entityPolicy),
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* 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 { GitlabApiReaderProcessor } from './GitlabApiReaderProcessor';
|
||||
|
||||
describe('GitlabApiReaderProcessor', () => {
|
||||
it('should build raw api', () => {
|
||||
const processor = new GitlabApiReaderProcessor();
|
||||
|
||||
const tests = [
|
||||
{
|
||||
target:
|
||||
'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml',
|
||||
url: new URL(
|
||||
'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml?ref=branch',
|
||||
),
|
||||
err: undefined,
|
||||
},
|
||||
{
|
||||
target:
|
||||
'https://gitlab.example.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml',
|
||||
url: new URL(
|
||||
'https://gitlab.example.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml?ref=branch',
|
||||
),
|
||||
err: undefined,
|
||||
},
|
||||
{
|
||||
target:
|
||||
'https://gitlab.com/groupA/teams/teamA/repoA/-/blob/branch/my/path/to/file.yaml', // Repo not in subgroup
|
||||
url: new URL(
|
||||
'https://gitlab.example.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml?ref=branch',
|
||||
),
|
||||
err: undefined,
|
||||
},
|
||||
{
|
||||
target:
|
||||
'https://gitlab.com/groupA/teams/teamA/repoA/-/blob/branch/my/path/',
|
||||
url: null,
|
||||
err:
|
||||
'Incorrect url: https://gitlab.com/groupA/teams/teamA/repoA/-/blob/branch/my/path/, Error: Gitlab url does not end in .ya?ml',
|
||||
},
|
||||
];
|
||||
|
||||
for (const test of tests) {
|
||||
if (test.err) {
|
||||
expect(() => processor.buildRawUrl(test.target, 12345)).toThrowError(
|
||||
test.err,
|
||||
);
|
||||
} else {
|
||||
expect(processor.buildRawUrl(test.target, 12345)).toEqual(test.url);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should return request options', () => {
|
||||
const tests = [
|
||||
{
|
||||
token: '0123456789',
|
||||
expect: {
|
||||
headers: {
|
||||
'PRIVATE-TOKEN': '0123456789',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
token: '',
|
||||
expect: {
|
||||
headers: {
|
||||
'PRIVATE-TOKEN': '',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
for (const test of tests) {
|
||||
process.env.GITLAB_PRIVATE_TOKEN = test.token;
|
||||
const processor = new GitlabApiReaderProcessor();
|
||||
expect(processor.getRequestOptions()).toEqual(test.expect);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* 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 { LocationSpec } from '@backstage/catalog-model';
|
||||
import fetch, { RequestInit, HeadersInit } from 'node-fetch';
|
||||
import * as result from './results';
|
||||
import { LocationProcessor, LocationProcessorEmit } from './types';
|
||||
|
||||
export class GitlabApiReaderProcessor implements LocationProcessor {
|
||||
private privateToken: string = process.env.GITLAB_PRIVATE_TOKEN || '';
|
||||
|
||||
getRequestOptions(): RequestInit {
|
||||
const headers: HeadersInit = { 'PRIVATE-TOKEN': '' };
|
||||
if (this.privateToken !== '') {
|
||||
headers['PRIVATE-TOKEN'] = this.privateToken;
|
||||
}
|
||||
|
||||
const requestOptions: RequestInit = {
|
||||
headers,
|
||||
};
|
||||
|
||||
return requestOptions;
|
||||
}
|
||||
|
||||
async readLocation(
|
||||
location: LocationSpec,
|
||||
optional: boolean,
|
||||
emit: LocationProcessorEmit,
|
||||
): Promise<boolean> {
|
||||
if (location.type !== 'gitlab/api') {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const projectID = await this.getProjectID(location.target);
|
||||
const url = this.buildRawUrl(location.target, projectID);
|
||||
const response = await fetch(url.toString(), this.getRequestOptions());
|
||||
if (response.ok) {
|
||||
const data = await response.buffer();
|
||||
emit(result.data(location, data));
|
||||
} else {
|
||||
const message = `${location.target} could not be read as ${url}, ${response.status} ${response.statusText}`;
|
||||
if (response.status === 404) {
|
||||
if (!optional) {
|
||||
emit(result.notFoundError(location, message));
|
||||
}
|
||||
} else {
|
||||
emit(result.generalError(location, message));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
const message = `Unable to read ${location.type} ${location.target}, ${e}`;
|
||||
emit(result.generalError(location, message));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// convert https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath
|
||||
// to https://gitlab.com/api/v4/projects/<PROJECTID>/repository/files/filepath?ref=branch
|
||||
buildRawUrl(target: string, projectID: Number): URL {
|
||||
try {
|
||||
const url = new URL(target);
|
||||
|
||||
const branchAndfilePath = url.pathname.split('/-/blob/')[1];
|
||||
|
||||
if (!branchAndfilePath.match(/\.ya?ml$/)) {
|
||||
throw new Error('Gitlab url does not end in .ya?ml');
|
||||
}
|
||||
|
||||
const [branch, ...filePath] = branchAndfilePath.split('/');
|
||||
|
||||
url.pathname = [
|
||||
'/api/v4/projects',
|
||||
projectID,
|
||||
'repository/files',
|
||||
encodeURIComponent(filePath.join('/')),
|
||||
'raw',
|
||||
].join('/');
|
||||
url.search = `?ref=${branch}`;
|
||||
|
||||
return url;
|
||||
} catch (e) {
|
||||
throw new Error(`Incorrect url: ${target}, ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
async getProjectID(target: string): Promise<Number> {
|
||||
const url = new URL(target);
|
||||
|
||||
if (
|
||||
// absPaths to gitlab files should contain /-/blob
|
||||
// ex: https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath
|
||||
!url.pathname.match(/\/\-\/blob\//)
|
||||
) {
|
||||
throw new Error('Please provide full path to yaml file from Gitlab');
|
||||
}
|
||||
try {
|
||||
const repo = url.pathname.split('/-/blob/')[0];
|
||||
|
||||
// Find ProjectID from url
|
||||
// convert 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath'
|
||||
// to 'https://gitlab.com/api/v4/projects/groupA%2Fteams%2FsubgroupA%2FteamA%2Frepo'
|
||||
const repoIDLookup = new URL(
|
||||
`${url.protocol + url.hostname}/api/v4/projects/${encodeURIComponent(
|
||||
repo.replace(/^\//, ''),
|
||||
)}`,
|
||||
);
|
||||
const response = await fetch(
|
||||
repoIDLookup.toString(),
|
||||
this.getRequestOptions(),
|
||||
);
|
||||
const projectIDJson = await response.json();
|
||||
const projectID: Number = projectIDJson.id;
|
||||
|
||||
return projectID;
|
||||
} catch (e) {
|
||||
throw new Error(`Could not get Gitlab ProjectID for: ${target}, ${e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"msw": "^0.19.0",
|
||||
"react-test-renderer": "^16.13.1",
|
||||
"whatwg-fetch": "^3.0.0"
|
||||
"whatwg-fetch": "^2.0.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,13 +1,75 @@
|
||||
# github-actions
|
||||
# GitHub Actions Plugin
|
||||
|
||||
Welcome to the github-actions plugin!
|
||||
Website: [https://github.com/actions](https://github.com/actions)
|
||||
|
||||
_This plugin was created through the Backstage CLI_
|
||||
## Screenshots
|
||||
|
||||
## Getting started
|
||||
TBD
|
||||
|
||||
Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/github-actions](http://localhost:3000/github-actions).
|
||||
## Setup
|
||||
|
||||
You can also serve the plugin in isolation by running `yarn start` in the plugin directory.
|
||||
This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads.
|
||||
It is only meant for local development, and the setup for it can be found inside the [/dev](/dev) directory.
|
||||
### Generic Requirements
|
||||
|
||||
1. Provide OAuth credentials:
|
||||
1. [Create an OAuth App](https://developer.github.com/apps/building-oauth-apps/creating-an-oauth-app/) with callback URL set to `https://localhost:3000/auth/github`.
|
||||
2. Take Client ID and Client Secret from the newly created app's settings page and put them into `AUTH_GITHUB_CLIENT_ID` and `AUTH_GITHUB_CLIENT_SECRET` env variables.
|
||||
2. Annotate your component with a correct GitHub Actions repository and owner:
|
||||
|
||||
The annotation key is `backstage.io/github-actions-id`.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: backstage
|
||||
description: backstage.io
|
||||
annotations:
|
||||
backstage.io/github-actions-id: 'spotify/backstage'
|
||||
spec:
|
||||
type: website
|
||||
lifecycle: production
|
||||
owner: guest
|
||||
```
|
||||
|
||||
### Standalone app requirements
|
||||
|
||||
If you didn't clone this repo you have to do some extra work.
|
||||
|
||||
1. Add plugin API to your Backstage instance:
|
||||
|
||||
```bash
|
||||
yarn add @backstage/plugin-github-actions
|
||||
```
|
||||
|
||||
```js
|
||||
// packages/app/src/api.ts
|
||||
import { ApiRegistry } from '@backstage/core';
|
||||
import { GithubActionsClient, githubActionsApiRef } from '@backstage/plugin-github-actions';
|
||||
|
||||
const builder = ApiRegistry.builder();
|
||||
builder.add(githubActionsApiRef, new GithubActionsClient());
|
||||
|
||||
export default builder.build() as ApiHolder;
|
||||
```
|
||||
|
||||
2. Add plugin itself:
|
||||
|
||||
```js
|
||||
// packages/app/src/plugins.ts
|
||||
export { plugin as GithubActions } from '@backstage/plugin-github-actions';
|
||||
```
|
||||
|
||||
3. Run the app with `yarn start` and the backend with `yarn --cwd packages/backend start`, navigate to `/github-actions/`.
|
||||
|
||||
## Features
|
||||
|
||||
- List workflow runs for a project
|
||||
- Dive into one run to see a job steps
|
||||
- Retry runs
|
||||
- Pagination for runs
|
||||
|
||||
## Limitations
|
||||
|
||||
- There is a limit of 100 apps for one OAuth client/token pair
|
||||
|
||||
@@ -18,18 +18,21 @@
|
||||
"diff": "backstage-cli plugin:diff",
|
||||
"prepack": "backstage-cli prepack",
|
||||
"postpack": "backstage-cli postpack",
|
||||
"clean": "backstage-cli clean"
|
||||
"clean": "backstage-cli clean",
|
||||
"mock-data": "./scripts/mock-data.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.16",
|
||||
"@backstage/core": "^0.1.1-alpha.16",
|
||||
"@backstage/core-api": "^0.1.1-alpha.16",
|
||||
"@backstage/theme": "^0.1.1-alpha.16",
|
||||
"@backstage/plugin-catalog": "^0.1.1-alpha.16",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
"@octokit/rest": "^18.0.0",
|
||||
"@octokit/types": "^5.0.1",
|
||||
"moment": "^2.27.0",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-router-dom": "6.0.0-beta.0",
|
||||
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
curl \
|
||||
--location \
|
||||
--request POST 'localhost:7000/catalog/locations' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw "{\"type\": \"file\", \"target\": \"$(pwd)/scripts/sample.yaml\"}"
|
||||
echo
|
||||
@@ -0,0 +1,11 @@
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: backstage
|
||||
description: backstage.io
|
||||
annotations:
|
||||
backstage.io/github-actions-id: 'spotify/backstage'
|
||||
spec:
|
||||
type: website
|
||||
lifecycle: production
|
||||
owner: guest
|
||||
@@ -72,5 +72,5 @@ export type GithubActionsApi = {
|
||||
owner: string;
|
||||
repo: string;
|
||||
runId: number;
|
||||
}) => void;
|
||||
}) => Promise<any>;
|
||||
};
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
} from '@octokit/types';
|
||||
|
||||
export class GithubActionsClient implements GithubActionsApi {
|
||||
reRunWorkflow({
|
||||
async reRunWorkflow({
|
||||
token,
|
||||
owner,
|
||||
repo,
|
||||
@@ -33,8 +33,8 @@ export class GithubActionsClient implements GithubActionsApi {
|
||||
owner: string;
|
||||
repo: string;
|
||||
runId: number;
|
||||
}) {
|
||||
new Octokit({ auth: token }).actions.reRunWorkflow({
|
||||
}): Promise<any> {
|
||||
return new Octokit({ auth: token }).actions.reRunWorkflow({
|
||||
owner,
|
||||
repo,
|
||||
run_id: runId,
|
||||
|
||||
@@ -14,211 +14,33 @@
|
||||
* 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 {
|
||||
export type Step = {
|
||||
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;
|
||||
number: number; // starts from 1
|
||||
started_at: string;
|
||||
completed_at: string;
|
||||
};
|
||||
|
||||
export type Job = {
|
||||
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;
|
||||
status: string;
|
||||
conclusion: string;
|
||||
started_at: string;
|
||||
completed_at: string;
|
||||
name: string;
|
||||
steps: Step[];
|
||||
};
|
||||
|
||||
export type Jobs = {
|
||||
total_count: number;
|
||||
jobs: Job[];
|
||||
};
|
||||
|
||||
export enum BuildStatus {
|
||||
'success',
|
||||
'failure',
|
||||
'pending',
|
||||
'running',
|
||||
}
|
||||
|
||||
@@ -1,143 +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 {
|
||||
Button,
|
||||
LinearProgress,
|
||||
makeStyles,
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableRow,
|
||||
Theme,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useAsync } from 'react-use';
|
||||
import { Link, useApi, githubAuthApiRef } from '@backstage/core';
|
||||
import { githubActionsApiRef } from '../../api';
|
||||
|
||||
const useStyles = makeStyles<Theme>(theme => ({
|
||||
root: {
|
||||
maxWidth: 720,
|
||||
margin: theme.spacing(2),
|
||||
},
|
||||
title: {
|
||||
padding: theme.spacing(1, 0, 2, 0),
|
||||
},
|
||||
table: {
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
export const BuildDetailsPage = () => {
|
||||
const repo = 'try-ssr';
|
||||
const owner = 'CircleCITest3';
|
||||
const api = useApi(githubActionsApiRef);
|
||||
const auth = useApi(githubAuthApiRef);
|
||||
|
||||
const classes = useStyles();
|
||||
const { id } = useParams();
|
||||
const status = useAsync(async () => {
|
||||
const token = await auth.getAccessToken(['repo', 'user']);
|
||||
return api
|
||||
.getWorkflowRun({
|
||||
token,
|
||||
owner,
|
||||
repo,
|
||||
id: parseInt(id, 10),
|
||||
})
|
||||
.then(data => {
|
||||
return data;
|
||||
});
|
||||
}, [location.search]);
|
||||
|
||||
if (status.loading) {
|
||||
return <LinearProgress />;
|
||||
} else if (status.error) {
|
||||
return (
|
||||
<Typography variant="h6" color="error">
|
||||
Failed to load build, {status.error.message}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
const details = status.value;
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Typography className={classes.title} variant="h3">
|
||||
<Link to="/github-actions">
|
||||
<Typography component="span" variant="h3" color="primary">
|
||||
<
|
||||
</Typography>
|
||||
</Link>
|
||||
Build Details
|
||||
</Typography>
|
||||
<TableContainer component={Paper} className={classes.table}>
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Branch</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{details?.head_branch}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Message</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{details?.head_commit.message}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Commit ID</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{details?.head_commit.id}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Status</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{details?.status}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Author</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{`${details?.head_commit.author.name} (${details?.head_commit.author.email})`}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Links</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{details?.html_url && (
|
||||
<Button>
|
||||
<a href={details.html_url}>GitHub</a>
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,107 +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 {
|
||||
LinearProgress,
|
||||
makeStyles,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableRow,
|
||||
Theme,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import React from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
import { BuildStatusIndicator } from '../BuildStatusIndicator';
|
||||
import { githubActionsApiRef, BuildStatus } from '../../api';
|
||||
import { Link, useApi, githubAuthApiRef } from '@backstage/core';
|
||||
|
||||
const useStyles = makeStyles<Theme>(theme => ({
|
||||
root: {
|
||||
// height: 400,
|
||||
},
|
||||
title: {
|
||||
paddingBottom: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
const BuildInfoCardContent = () => {
|
||||
const api = useApi(githubActionsApiRef);
|
||||
const auth = useApi(githubAuthApiRef);
|
||||
|
||||
const status = useAsync(async () => {
|
||||
const token = await auth.getAccessToken(['repo', 'user']);
|
||||
return api.listWorkflowRuns({ token, owner: 'spotify', repo: 'backstage' });
|
||||
});
|
||||
|
||||
if (status.loading) {
|
||||
return <LinearProgress />;
|
||||
} else if (status.error) {
|
||||
return (
|
||||
<Typography variant="h2" color="error">
|
||||
Failed to load builds, {status.error.message}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
// const [build] =
|
||||
// status.value?.filter(({ branch }) => branch === 'master') ?? [];
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Message</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Link to="builds/123">
|
||||
<Typography color="primary">build message</Typography>
|
||||
</Link>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Commit ID</Typography>
|
||||
</TableCell>
|
||||
<TableCell>build commit id</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Status</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<BuildStatusIndicator status={BuildStatus.Success} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
export const BuildInfoCard = () => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Typography variant="h2" className={classes.title}>
|
||||
Master Build
|
||||
</Typography>
|
||||
<BuildInfoCardContent />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,74 +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 { IconComponent } from '@backstage/core';
|
||||
import { makeStyles, Theme } from '@material-ui/core';
|
||||
import ProgressIcon from '@material-ui/icons/Autorenew';
|
||||
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 '../../api/types';
|
||||
|
||||
type Props = {
|
||||
status?: BuildStatus;
|
||||
};
|
||||
|
||||
type StatusStyle = {
|
||||
icon: IconComponent;
|
||||
color: string;
|
||||
};
|
||||
|
||||
const styles: { [key in BuildStatus]: StatusStyle } = {
|
||||
[BuildStatus.Null]: {
|
||||
icon: UnknownIcon,
|
||||
color: '#f49b20',
|
||||
},
|
||||
[BuildStatus.Success]: {
|
||||
icon: SuccessIcon,
|
||||
color: '#1db855',
|
||||
},
|
||||
[BuildStatus.Failure]: {
|
||||
icon: FailureIcon,
|
||||
color: '#CA001B',
|
||||
},
|
||||
[BuildStatus.Pending]: {
|
||||
icon: UnknownIcon,
|
||||
color: '#5BC0DE',
|
||||
},
|
||||
[BuildStatus.Running]: {
|
||||
icon: ProgressIcon,
|
||||
color: '#BEBEBE',
|
||||
},
|
||||
};
|
||||
|
||||
const useStyles = makeStyles<Theme, StatusStyle>({
|
||||
icon: style => ({
|
||||
color: style.color,
|
||||
}),
|
||||
});
|
||||
|
||||
export const BuildStatusIndicator = ({ status }: Props) => {
|
||||
const style = (status && styles[status]) || styles[BuildStatus.Null];
|
||||
const classes = useStyles(style);
|
||||
const Icon = style.icon;
|
||||
|
||||
return (
|
||||
<div className={classes.icon}>
|
||||
<Icon />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
/*
|
||||
* 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 {
|
||||
Button,
|
||||
LinearProgress,
|
||||
makeStyles,
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableRow,
|
||||
Theme,
|
||||
Typography,
|
||||
Box,
|
||||
ExpansionPanelDetails,
|
||||
ExpansionPanel,
|
||||
ExpansionPanelSummary,
|
||||
ListItemText,
|
||||
CircularProgress,
|
||||
Grid,
|
||||
Breadcrumbs,
|
||||
} from '@material-ui/core';
|
||||
import moment from 'moment';
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Link,
|
||||
Page,
|
||||
Header,
|
||||
HeaderLabel,
|
||||
Content,
|
||||
ContentHeader,
|
||||
SupportButton,
|
||||
pageTheme,
|
||||
} from '@backstage/core';
|
||||
import { Job, Step, Jobs } from '../../api/types';
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import { useWorkflowRunsDetails } from './useWorkflowRunsDetails';
|
||||
import { useWorkflowRunJobs } from './useWorkflowRunJobs';
|
||||
import { WorkflowRunStatusIcon } from '../WorkflowRunStatusIcon/WorkflowRunStatusIcon';
|
||||
import { useProjectName } from '../useProjectName';
|
||||
import GitHubIcon from '@material-ui/icons/GitHub';
|
||||
|
||||
const useStyles = makeStyles<Theme>(theme => ({
|
||||
root: {
|
||||
maxWidth: 720,
|
||||
margin: theme.spacing(2),
|
||||
},
|
||||
title: {
|
||||
padding: theme.spacing(1, 0, 2, 0),
|
||||
},
|
||||
table: {
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
expansionPanelDetails: {
|
||||
padding: 0,
|
||||
},
|
||||
button: {
|
||||
order: -1,
|
||||
marginRight: 0,
|
||||
marginLeft: '-20px',
|
||||
},
|
||||
}));
|
||||
|
||||
const JobsList = ({ jobs }: { jobs?: Jobs }) => {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<Box>
|
||||
{jobs &&
|
||||
jobs.total_count > 0 &&
|
||||
jobs.jobs.map((job: Job) => (
|
||||
<JobListItem
|
||||
job={job}
|
||||
className={
|
||||
job.status !== 'success' ? classes.failed : classes.success
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const getElapsedTime = (start: string, end: string) => {
|
||||
const diff = moment(moment(end || moment()).diff(moment(start)));
|
||||
const timeElapsed = diff.format('m [minutes] s [seconds]');
|
||||
return timeElapsed;
|
||||
};
|
||||
|
||||
const StepView = ({ step }: { step: Step }) => {
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<ListItemText
|
||||
primary={step.name}
|
||||
secondary={getElapsedTime(step.started_at, step.completed_at)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<WorkflowRunStatusIcon status={step.status.toUpperCase()} />
|
||||
{step.status}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
|
||||
const JobListItem = ({ job, className }: { job: Job; className: string }) => {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<ExpansionPanel
|
||||
TransitionProps={{ unmountOnExit: true }}
|
||||
className={className}
|
||||
>
|
||||
<ExpansionPanelSummary
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
aria-controls={`panel-${name}-content`}
|
||||
id={`panel-${name}-header`}
|
||||
IconButtonProps={{
|
||||
className: classes.button,
|
||||
}}
|
||||
>
|
||||
<Typography variant="button">
|
||||
{job.name} ({getElapsedTime(job.started_at, job.completed_at)})
|
||||
</Typography>
|
||||
</ExpansionPanelSummary>
|
||||
<ExpansionPanelDetails className={classes.expansionPanelDetails}>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
{job.steps.map((step: Step) => (
|
||||
<StepView step={step} />
|
||||
))}
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</ExpansionPanelDetails>
|
||||
</ExpansionPanel>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* A component for Jobs visualization. Jobs are a property of a Workflow Run.
|
||||
*/
|
||||
export const WorkflowRunDetailsPage = () => {
|
||||
const [owner, repo] = (
|
||||
useProjectName({
|
||||
kind: 'Component',
|
||||
name: 'backstage',
|
||||
}) ?? '/'
|
||||
).split('/');
|
||||
const details = useWorkflowRunsDetails(repo, owner);
|
||||
const jobs = useWorkflowRunJobs(details.value?.jobs_url);
|
||||
|
||||
const classes = useStyles();
|
||||
|
||||
if (details.loading) {
|
||||
return <LinearProgress />;
|
||||
} else if (details.error) {
|
||||
return (
|
||||
<Typography variant="h6" color="error">
|
||||
Failed to load build, {details.error.message}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Page theme={pageTheme.tool}>
|
||||
<Header
|
||||
title="GitHub Actions"
|
||||
subtitle="See recent workflow runs and their status"
|
||||
>
|
||||
<HeaderLabel label="Owner" value="Spotify" />
|
||||
<HeaderLabel label="Lifecycle" value="Alpha" />
|
||||
</Header>
|
||||
<Content>
|
||||
<ContentHeader title="Workflow run details">
|
||||
<SupportButton>
|
||||
This plugin allows you to view and interact with your builds within
|
||||
the GitHub Actions environment.
|
||||
</SupportButton>
|
||||
</ContentHeader>
|
||||
<Breadcrumbs aria-label="breadcrumb">
|
||||
<Link to="/github-actions">Workflow runs</Link>
|
||||
<Typography>Workflow run details</Typography>
|
||||
</Breadcrumbs>
|
||||
<Grid container spacing={3} direction="column">
|
||||
<Grid item>
|
||||
<div className={classes.root}>
|
||||
<TableContainer component={Paper} className={classes.table}>
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Branch</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{details.value?.head_branch}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Message</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{details.value?.head_commit.message}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Commit ID</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{details.value?.head_commit.id}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Status</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<WorkflowRunStatusIcon status={details.value?.status} />{' '}
|
||||
{details.value?.status.toUpperCase()}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Author</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{`${details.value?.head_commit.author.name} (${details.value?.head_commit.author.email})`}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Links</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{details.value?.html_url && (
|
||||
<a href={details.value.html_url}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="default"
|
||||
startIcon={<GitHubIcon />}
|
||||
>
|
||||
Workflow runs on GitHub
|
||||
</Button>
|
||||
</a>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={2}>
|
||||
<Typography noWrap>Jobs</Typography>
|
||||
{jobs.loading ? (
|
||||
<CircularProgress />
|
||||
) : (
|
||||
<JobsList jobs={jobs.value} />
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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 { WorkflowRunDetailsPage } from './WorkflowRunDetailsPage';
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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 { useAsync } from 'react-use';
|
||||
import { Jobs } from '../../api/types';
|
||||
|
||||
export const useWorkflowRunJobs = (jobsUrl?: string) => {
|
||||
const jobs = useAsync<Jobs>(async () => {
|
||||
if (jobsUrl === undefined) return [];
|
||||
const data = await fetch(jobsUrl).then(d => d.json());
|
||||
return data;
|
||||
}, [jobsUrl]);
|
||||
return jobs;
|
||||
};
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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 { useApi, githubAuthApiRef } from '@backstage/core';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useAsync } from 'react-use';
|
||||
import { githubActionsApiRef } from '../../api';
|
||||
|
||||
export const useWorkflowRunsDetails = (repo: string, owner: string) => {
|
||||
const api = useApi(githubActionsApiRef);
|
||||
const auth = useApi(githubAuthApiRef);
|
||||
const { id } = useParams();
|
||||
const details = useAsync(async () => {
|
||||
const token = await auth.getAccessToken(['repo']);
|
||||
return api.getWorkflowRun({
|
||||
token,
|
||||
owner,
|
||||
repo,
|
||||
id: parseInt(id, 10),
|
||||
});
|
||||
}, [repo, owner, id]);
|
||||
return details;
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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 { StatusPending, StatusRunning, StatusOK } from '@backstage/core';
|
||||
import React from 'react';
|
||||
|
||||
export const WorkflowRunStatusIcon = ({
|
||||
status,
|
||||
}: {
|
||||
status: string | undefined;
|
||||
}) => {
|
||||
if (status === undefined) return null;
|
||||
switch (status.toLowerCase()) {
|
||||
case 'queued':
|
||||
return <StatusPending />;
|
||||
case 'in_progress':
|
||||
return <StatusRunning />;
|
||||
case 'completed':
|
||||
return <StatusOK />;
|
||||
default:
|
||||
return <StatusPending />;
|
||||
}
|
||||
};
|
||||
+1
-1
@@ -14,4 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { BuildStatusIndicator } from './BuildStatusIndicator';
|
||||
export { WorkflowRunStatusIcon } from './WorkflowRunStatusIcon';
|
||||
+5
-5
@@ -26,20 +26,20 @@ import {
|
||||
import { Grid } from '@material-ui/core';
|
||||
import React from 'react';
|
||||
|
||||
import { BuildListTable } from '../BuildListTable';
|
||||
import { WorkflowRunsTable } from '../WorkflowRunsTable';
|
||||
|
||||
export const BuildListPage = () => {
|
||||
export const WorkflowRunsPage = () => {
|
||||
return (
|
||||
<Page theme={pageTheme.tool}>
|
||||
<Header
|
||||
title="GitHub Actions"
|
||||
subtitle="See recent builds and their status"
|
||||
subtitle="See recent workflow runs and their status"
|
||||
>
|
||||
<HeaderLabel label="Owner" value="Spotify" />
|
||||
<HeaderLabel label="Lifecycle" value="Alpha" />
|
||||
</Header>
|
||||
<Content>
|
||||
<ContentHeader title="All builds">
|
||||
<ContentHeader title="Workflow runs">
|
||||
<SupportButton>
|
||||
This plugin allows you to view and interact with your builds within
|
||||
the GitHub Actions environment.
|
||||
@@ -47,7 +47,7 @@ export const BuildListPage = () => {
|
||||
</ContentHeader>
|
||||
<Grid container spacing={3} direction="column">
|
||||
<Grid item>
|
||||
<BuildListTable repo="try-ssr" owner="CircleCITest3" />
|
||||
<WorkflowRunsTable />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Content>
|
||||
+1
-1
@@ -14,4 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { BuildDetailsPage } from './BuildDetailsPage';
|
||||
export { WorkflowRunsPage } from './WorkflowRunsPage';
|
||||
+40
-66
@@ -14,25 +14,19 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React, { FC } from 'react';
|
||||
import { Link, Typography, Box, IconButton } from '@material-ui/core';
|
||||
import { Link, Typography, Box, IconButton, Tooltip } from '@material-ui/core';
|
||||
import RetryIcon from '@material-ui/icons/Replay';
|
||||
import GitHubIcon from '@material-ui/icons/GitHub';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import {
|
||||
StatusError,
|
||||
StatusWarning,
|
||||
StatusOK,
|
||||
StatusPending,
|
||||
StatusRunning,
|
||||
Table,
|
||||
TableColumn,
|
||||
} from '@backstage/core';
|
||||
import { useBuilds } from './useBuilds';
|
||||
import { Table, TableColumn } from '@backstage/core';
|
||||
import { useWorkflowRuns } from './useWorkflowRuns';
|
||||
import { WorkflowRunStatusIcon } from '../WorkflowRunStatusIcon';
|
||||
import SyncIcon from '@material-ui/icons/Sync';
|
||||
|
||||
export type Build = {
|
||||
export type WorkflowRun = {
|
||||
id: string;
|
||||
buildName: string;
|
||||
buildUrl?: string;
|
||||
message: string;
|
||||
url?: string;
|
||||
source: {
|
||||
branchName: string;
|
||||
commit: {
|
||||
@@ -41,25 +35,7 @@ export type Build = {
|
||||
};
|
||||
};
|
||||
status: string;
|
||||
onRestartClick: () => void;
|
||||
};
|
||||
|
||||
// retried, canceled, infrastructure_fail, timedout, not_run, running, failed, queued, scheduled, not_running, no_tests, fixed, success
|
||||
const getStatusComponent = (status: string | undefined = '') => {
|
||||
switch (status.toLowerCase()) {
|
||||
case 'queued':
|
||||
case 'scheduled':
|
||||
return <StatusPending />;
|
||||
case 'running':
|
||||
return <StatusRunning />;
|
||||
case 'failed':
|
||||
return <StatusError />;
|
||||
case 'success':
|
||||
return <StatusOK />;
|
||||
case 'canceled':
|
||||
default:
|
||||
return <StatusWarning />;
|
||||
}
|
||||
onReRunClick: () => void;
|
||||
};
|
||||
|
||||
const generatedColumns: TableColumn[] = [
|
||||
@@ -70,40 +46,47 @@ const generatedColumns: TableColumn[] = [
|
||||
width: '150px',
|
||||
},
|
||||
{
|
||||
title: 'Build',
|
||||
field: 'buildName',
|
||||
title: 'Message',
|
||||
field: 'message',
|
||||
highlight: true,
|
||||
render: (row: Partial<Build>) => (
|
||||
<Link component={RouterLink} to={`/github-actions/build/${row.id}`}>
|
||||
{row.buildName}
|
||||
render: (row: Partial<WorkflowRun>) => (
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={`/github-actions/workflow-run/${row.id}`}
|
||||
>
|
||||
{row.message}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Source',
|
||||
render: (row: Partial<Build>) => (
|
||||
<>
|
||||
render: (row: Partial<WorkflowRun>) => (
|
||||
<Typography variant="body2" noWrap>
|
||||
<p>{row.source?.branchName}</p>
|
||||
<p>{row.source?.commit.hash}</p>
|
||||
</>
|
||||
</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
render: (row: Partial<Build>) => (
|
||||
render: (row: Partial<WorkflowRun>) => (
|
||||
<Box display="flex" alignItems="center">
|
||||
{getStatusComponent(row.status)}
|
||||
<WorkflowRunStatusIcon status={row.status} />
|
||||
<Box mr={1} />
|
||||
<Typography variant="button">{row.status}</Typography>
|
||||
<Typography variant="button" noWrap>
|
||||
{row.status}
|
||||
</Typography>
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Actions',
|
||||
render: (row: Partial<Build>) => (
|
||||
<IconButton onClick={row.onRestartClick}>
|
||||
<RetryIcon />
|
||||
</IconButton>
|
||||
render: (row: Partial<WorkflowRun>) => (
|
||||
<Tooltip title="Rerun workflow">
|
||||
<IconButton onClick={row.onReRunClick}>
|
||||
<RetryIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
),
|
||||
width: '10%',
|
||||
},
|
||||
@@ -112,7 +95,7 @@ const generatedColumns: TableColumn[] = [
|
||||
type Props = {
|
||||
loading: boolean;
|
||||
retry: () => void;
|
||||
builds?: Build[];
|
||||
runs?: WorkflowRun[];
|
||||
projectName: string;
|
||||
page: number;
|
||||
onChangePage: (page: number) => void;
|
||||
@@ -121,13 +104,13 @@ type Props = {
|
||||
onChangePageSize: (pageSize: number) => void;
|
||||
};
|
||||
|
||||
const BuildListTableView: FC<Props> = ({
|
||||
const WorkflowRunsTableView: FC<Props> = ({
|
||||
projectName,
|
||||
loading,
|
||||
pageSize,
|
||||
page,
|
||||
retry,
|
||||
builds,
|
||||
runs,
|
||||
onChangePage,
|
||||
onChangePageSize,
|
||||
total,
|
||||
@@ -140,13 +123,13 @@ const BuildListTableView: FC<Props> = ({
|
||||
page={page}
|
||||
actions={[
|
||||
{
|
||||
icon: () => <RetryIcon />,
|
||||
tooltip: 'Refresh Data',
|
||||
icon: () => <SyncIcon />,
|
||||
tooltip: 'Reload workflow runs',
|
||||
isFreeAction: true,
|
||||
onClick: () => retry(),
|
||||
},
|
||||
]}
|
||||
data={builds ?? []}
|
||||
data={runs ?? []}
|
||||
onChangePage={onChangePage}
|
||||
onChangeRowsPerPage={onChangePageSize}
|
||||
title={
|
||||
@@ -161,19 +144,10 @@ const BuildListTableView: FC<Props> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const BuildListTable = ({
|
||||
repo,
|
||||
owner,
|
||||
}: {
|
||||
repo: string;
|
||||
owner: string;
|
||||
}) => {
|
||||
const [tableProps, { retry, setPage, setPageSize }] = useBuilds({
|
||||
repo,
|
||||
owner,
|
||||
});
|
||||
export const WorkflowRunsTable = () => {
|
||||
const [tableProps, { retry, setPage, setPageSize }] = useWorkflowRuns();
|
||||
return (
|
||||
<BuildListTableView
|
||||
<WorkflowRunsTableView
|
||||
{...tableProps}
|
||||
retry={retry}
|
||||
onChangePageSize={setPageSize}
|
||||
+2
-2
@@ -13,5 +13,5 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { BuildListTable } from './BuildListTable';
|
||||
export type { Build } from './BuildListTable';
|
||||
export { WorkflowRunsTable } from './WorkflowRunsTable';
|
||||
export type { WorkflowRun } from './WorkflowRunsTable';
|
||||
+37
-26
@@ -15,42 +15,55 @@
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { useAsyncRetry } from 'react-use';
|
||||
import { Build } from './BuildListTable';
|
||||
import { WorkflowRun } from './WorkflowRunsTable';
|
||||
import { githubActionsApiRef } from '../../api/GithubActionsApi';
|
||||
import { useApi, githubAuthApiRef } from '@backstage/core';
|
||||
import { useApi, githubAuthApiRef, errorApiRef } from '@backstage/core';
|
||||
import { ActionsListWorkflowRunsForRepoResponseData } from '@octokit/types';
|
||||
import { useProjectName } from '../useProjectName';
|
||||
|
||||
export function useBuilds({ repo, owner }: { repo: string; owner: string }) {
|
||||
export function useWorkflowRuns() {
|
||||
const api = useApi(githubActionsApiRef);
|
||||
const auth = useApi(githubAuthApiRef);
|
||||
|
||||
const errorApi = useApi(errorApiRef);
|
||||
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(5);
|
||||
|
||||
const restartBuild = async () => {};
|
||||
|
||||
const { loading, value: builds, retry } = useAsyncRetry<Build[]>(async () => {
|
||||
const token = await auth.getAccessToken(['repo', 'user']);
|
||||
|
||||
const projectName = useProjectName({
|
||||
kind: 'Component',
|
||||
name: 'backstage',
|
||||
});
|
||||
const { loading, value: runs, retry } = useAsyncRetry<
|
||||
WorkflowRun[]
|
||||
>(async () => {
|
||||
const token = await auth.getAccessToken(['repo']);
|
||||
const [owner, repo] = (projectName ?? '/').split('/');
|
||||
return (
|
||||
api
|
||||
// GitHub API pagination count starts from 1
|
||||
.listWorkflowRuns({ token, owner, repo, pageSize, page: page + 1 })
|
||||
.then(
|
||||
(allBuilds: ActionsListWorkflowRunsForRepoResponseData): Build[] => {
|
||||
setTotal(allBuilds.total_count);
|
||||
(
|
||||
workflowRunsData: ActionsListWorkflowRunsForRepoResponseData,
|
||||
): WorkflowRun[] => {
|
||||
setTotal(workflowRunsData.total_count);
|
||||
// Transformation here
|
||||
return allBuilds.workflow_runs.map(run => ({
|
||||
buildName: run.head_commit.message,
|
||||
return workflowRunsData.workflow_runs.map(run => ({
|
||||
message: run.head_commit.message,
|
||||
id: `${run.id}`,
|
||||
onRestartClick: () => {
|
||||
api.reRunWorkflow({
|
||||
token,
|
||||
owner,
|
||||
repo,
|
||||
runId: run.id,
|
||||
});
|
||||
onReRunClick: async () => {
|
||||
try {
|
||||
await api.reRunWorkflow({
|
||||
token,
|
||||
owner,
|
||||
repo,
|
||||
runId: run.id,
|
||||
});
|
||||
} catch (e) {
|
||||
errorApi.post(e);
|
||||
}
|
||||
},
|
||||
source: {
|
||||
branchName: run.head_branch,
|
||||
@@ -63,28 +76,26 @@ export function useBuilds({ repo, owner }: { repo: string; owner: string }) {
|
||||
},
|
||||
},
|
||||
status: run.status,
|
||||
buildUrl: run.url,
|
||||
url: run.url,
|
||||
}));
|
||||
},
|
||||
)
|
||||
);
|
||||
}, [page, pageSize]);
|
||||
}, [page, pageSize, projectName]);
|
||||
|
||||
const projectName = `${owner}/${repo}`;
|
||||
return [
|
||||
{
|
||||
page,
|
||||
pageSize,
|
||||
loading,
|
||||
builds,
|
||||
projectName,
|
||||
runs,
|
||||
projectName: projectName ?? '',
|
||||
total,
|
||||
},
|
||||
{
|
||||
builds,
|
||||
runs,
|
||||
setPage,
|
||||
setPageSize,
|
||||
restartBuild,
|
||||
retry,
|
||||
},
|
||||
] as const;
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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 { useAsync } from 'react-use';
|
||||
import { catalogApiRef, EntityCompoundName } from '@backstage/plugin-catalog';
|
||||
import { useApi } from '@backstage/core';
|
||||
|
||||
export const useProjectName = (name: EntityCompoundName) => {
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
|
||||
const { value } = useAsync<string>(async () => {
|
||||
const entity = await catalogApi.getEntityByName(name);
|
||||
return (
|
||||
entity?.metadata.annotations?.['backstage.io/github-actions-id'] ?? ''
|
||||
);
|
||||
});
|
||||
return value;
|
||||
};
|
||||
@@ -15,8 +15,8 @@
|
||||
*/
|
||||
|
||||
import { createPlugin, createRouteRef } from '@backstage/core';
|
||||
import { BuildDetailsPage } from './components/BuildDetailsPage';
|
||||
import { BuildListPage } from './components/BuildListPage';
|
||||
import { WorkflowRunDetailsPage } from './components/WorkflowRunDetailsPage';
|
||||
import { WorkflowRunsPage } from './components/WorkflowRunsPage';
|
||||
|
||||
// TODO(freben): This is just a demo route for now
|
||||
export const rootRouteRef = createRouteRef({
|
||||
@@ -24,14 +24,14 @@ export const rootRouteRef = createRouteRef({
|
||||
title: 'GitHub Actions',
|
||||
});
|
||||
export const buildRouteRef = createRouteRef({
|
||||
path: '/github-actions/build/:id',
|
||||
title: 'GitHub Actions Build',
|
||||
path: '/github-actions/workflow-run/:id',
|
||||
title: 'GitHub Actions Workflow Run',
|
||||
});
|
||||
|
||||
export const plugin = createPlugin({
|
||||
id: 'github-actions',
|
||||
register({ router }) {
|
||||
router.addRoute(rootRouteRef, BuildListPage);
|
||||
router.addRoute(buildRouteRef, BuildDetailsPage);
|
||||
router.addRoute(rootRouteRef, WorkflowRunsPage);
|
||||
router.addRoute(buildRouteRef, WorkflowRunDetailsPage);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
# graphql
|
||||
|
||||
Welcome to the graphql backend plugin!
|
||||
|
||||
_This plugin was created through the Backstage CLI_
|
||||
|
||||
## Getting started
|
||||
|
||||
Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/graphql](http://localhost:3000/graphql).
|
||||
|
||||
You can also serve the plugin in isolation by running `yarn start` in the plugin directory.
|
||||
This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads.
|
||||
It is only meant for local development, and the setup for it can be found inside the [/dev](/dev) directory.
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@backstage/plugin-graphql-backend",
|
||||
"version": "0.1.1-alpha.16",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "backstage-cli backend:dev",
|
||||
"build": "backstage-cli backend:build",
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test",
|
||||
"prepack": "backstage-cli prepack",
|
||||
"postpack": "backstage-cli postpack",
|
||||
"clean": "backstage-cli clean",
|
||||
"mock-data": "./scripts/mock-data.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.1.1-alpha.16",
|
||||
"@types/express": "^4.17.6",
|
||||
"apollo-server": "^2.16.0",
|
||||
"apollo-server-express": "^2.16.0",
|
||||
"express": "^4.17.1",
|
||||
"express-promise-router": "^3.0.3",
|
||||
"graphql": "^15.3.0",
|
||||
"whatwg-fetch": "^2.0.0",
|
||||
"winston": "^3.2.1",
|
||||
"yn": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.16",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"eslint-plugin-graphql": "^4.0.0",
|
||||
"msw": "^0.19.5",
|
||||
"supertest": "^4.0.2"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
Executable
+2
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env bash
|
||||
echo "use this script to load your service with some mock data if needed!"
|
||||
+3
-1
@@ -14,4 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { BuildInfoCard } from './BuildInfoCard';
|
||||
require('whatwg-fetch');
|
||||
|
||||
export * from './service/router';
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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 { getRootLogger } from '@backstage/backend-common';
|
||||
import yn from 'yn';
|
||||
import { startStandaloneServer } from './service/standaloneServer';
|
||||
|
||||
const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7000;
|
||||
const enableCors = yn(process.env.PLUGIN_CORS, { default: false });
|
||||
const logger = getRootLogger();
|
||||
|
||||
startStandaloneServer({ port, enableCors, logger }).catch(err => {
|
||||
logger.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
logger.info('CTRL+C pressed; exiting.');
|
||||
process.exit(0);
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
type CatalogEntity {
|
||||
id: String
|
||||
}
|
||||
|
||||
type CatalogQuery {
|
||||
list: [CatalogEntity!]!
|
||||
}
|
||||
|
||||
type Query {
|
||||
catalog: CatalogQuery!
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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 './router';
|
||||
|
||||
describe('Router', () => {
|
||||
it('should pass the test', () => {
|
||||
expect(true).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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 { errorHandler } from '@backstage/backend-common';
|
||||
import express from 'express';
|
||||
import Router from 'express-promise-router';
|
||||
import { Logger } from 'winston';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { ApolloServer } from 'apollo-server-express';
|
||||
|
||||
export interface RouterOptions {
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export async function createRouter(
|
||||
options: RouterOptions,
|
||||
): Promise<express.Router> {
|
||||
const typeDefs = await fs.promises.readFile(
|
||||
path.resolve(__dirname, '..', 'schema.gql'),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const server = new ApolloServer({ typeDefs, logger: options.logger });
|
||||
const router = Router();
|
||||
|
||||
const apolloMiddlware = server.getMiddleware({ path: '/' });
|
||||
router.use(apolloMiddlware);
|
||||
|
||||
router.get('/health', (_, response) => {
|
||||
response.send({ status: 'ok' });
|
||||
});
|
||||
|
||||
router.use(errorHandler());
|
||||
|
||||
return router;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
/*
|
||||
* 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 { createServiceBuilder } from '@backstage/backend-common';
|
||||
import { Server } from 'http';
|
||||
import { Logger } from 'winston';
|
||||
import { createRouter } from './router';
|
||||
|
||||
export interface ServerOptions {
|
||||
port: number;
|
||||
enableCors: boolean;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export async function startStandaloneServer(
|
||||
options: ServerOptions,
|
||||
): Promise<Server> {
|
||||
const logger = options.logger.child({ service: 'graphql-backend' });
|
||||
logger.debug('Starting application server...');
|
||||
const router = await createRouter({
|
||||
logger,
|
||||
});
|
||||
|
||||
const service = createServiceBuilder(module)
|
||||
.enableCors({ origin: 'http://localhost:3000' })
|
||||
.addRouter('/graphql', router);
|
||||
|
||||
return await service.start().catch(err => {
|
||||
logger.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.hot?.accept();
|
||||
+2
-1
@@ -13,5 +13,6 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
require('whatwg-fetch');
|
||||
|
||||
export { BuildListPage } from './BuildListPage';
|
||||
export {};
|
||||
@@ -20,8 +20,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.1.1-alpha.16",
|
||||
"@backstage/config": "^0.1.1-alpha.13",
|
||||
"@backstage/config-loader": "^0.1.1-alpha.13",
|
||||
"@backstage/config": "^0.1.1-alpha.16",
|
||||
"@backstage/config-loader": "^0.1.1-alpha.16",
|
||||
"@types/express": "^4.17.6",
|
||||
"express": "^4.17.1",
|
||||
"express-promise-router": "^3.0.3",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.1.1-alpha.16",
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.16",
|
||||
"@backstage/config": "^0.1.1-alpha.13",
|
||||
"@backstage/config": "^0.1.1-alpha.16",
|
||||
"@octokit/rest": "^18.0.0",
|
||||
"@types/dockerode": "^2.5.32",
|
||||
"@types/express": "^4.17.6",
|
||||
|
||||
@@ -13,7 +13,7 @@ spec:
|
||||
type: website
|
||||
path: '.'
|
||||
schema:
|
||||
required:
|
||||
required:
|
||||
- component_id
|
||||
- description
|
||||
properties:
|
||||
@@ -22,7 +22,6 @@ spec:
|
||||
type: string
|
||||
description: Unique name of the component
|
||||
description:
|
||||
title: Description
|
||||
title: Description
|
||||
type: string
|
||||
description: Description of the component
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core": "^0.1.1-alpha.16",
|
||||
"@backstage/test-utils-core": "^0.1.1-alpha.13",
|
||||
"@backstage/test-utils-core": "^0.1.1-alpha.16",
|
||||
"@backstage/theme": "^0.1.1-alpha.16",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# example docs
|
||||
|
||||
This is a basic example of documentation.
|
||||
@@ -0,0 +1,11 @@
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: documented-component
|
||||
description: A Service with TechDocs documentation
|
||||
annotations:
|
||||
backstage.io/techdocs-ref: 'dir:./'
|
||||
spec:
|
||||
type: service
|
||||
lifecycle: experimental
|
||||
owner: documented@example.com
|
||||
@@ -0,0 +1,7 @@
|
||||
site_name: 'example-docs'
|
||||
|
||||
nav:
|
||||
- Home: index.md
|
||||
|
||||
plugins:
|
||||
- techdocs-core
|
||||
@@ -17,7 +17,8 @@
|
||||
"test": "backstage-cli test",
|
||||
"prepack": "backstage-cli prepack",
|
||||
"postpack": "backstage-cli postpack",
|
||||
"clean": "backstage-cli clean"
|
||||
"clean": "backstage-cli clean",
|
||||
"mock-data": "./scripts/mock-data.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.1.1-alpha.16",
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
for URL in \
|
||||
'documented-component/documented-component.yaml' \
|
||||
; do \
|
||||
curl \
|
||||
--location \
|
||||
--request POST 'localhost:7000/catalog/locations' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw "{\"type\": \"file\", \"target\": \"$(pwd)/examples/${URL}\"}"
|
||||
echo
|
||||
done
|
||||
@@ -91,6 +91,10 @@ export const Reader = () => {
|
||||
return; // Page isn't ready
|
||||
}
|
||||
|
||||
if (state.value instanceof Error) {
|
||||
return; // Docs not found
|
||||
}
|
||||
|
||||
// Pre-render
|
||||
const transformedElement = transformer(state.value as string, [
|
||||
sanitizeDOM(),
|
||||
|
||||
Reference in New Issue
Block a user