Skip to content

Commit

Permalink
feat: add support for GitHub Actions (#62)
Browse files Browse the repository at this point in the history
* feat: add support for GitHub Actions

Power by OIDC exclusively instead of using log based proof. OIDC is the Cool Thing that didn't exist when CFA was originally written, relying on it for the GHA implementation makes sense.

* chore: run prettier

* chore: add helpful logging

* logging

* Revert "logging"

This reverts commit 8dd7f39.

* booleans are hard

* set npm_token if we have it

* tada

* chore: run prettier
  • Loading branch information
MarshallOfSound authored Aug 14, 2024
1 parent dbaa7fa commit 63893af
Show file tree
Hide file tree
Showing 29 changed files with 764 additions and 51 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"prettier:check": "prettier --check \"src/**/*.{ts,tsx}\"",
"prettier:write": "prettier --write \"src/**/*.{ts,tsx}\"",
"start": "npm run start:prod",
"start:dev:server": "cross-env DEBUG=cfa* REDIS_URL=redis://localhost NO_DB_SSL=true DATABASE_URL=postgresql://cfa-user:cfa-pass@localhost:5433/cfa node lib/server",
"start:dev:server": "cross-env DEBUG=cfa* REDIS_URL=redis://127.0.0.1 NO_DB_SSL=true DATABASE_URL=postgresql://cfa-user:cfa-pass@localhost:5433/cfa node lib/server",
"start:prod": "cross-env DEBUG=cfa* node lib/server",
"test": "rm -rf lib && jest --coverage",
"test:ci": "jest --ci --coverage --reporters=default --reporters=jest-junit",
Expand All @@ -37,6 +37,7 @@
"@types/jest": "^29.0.0",
"@types/joi": "^14.3.4",
"@types/jwk-to-pem": "^2.0.1",
"@types/libsodium-wrappers": "^0.7.14",
"@types/morgan": "^1.7.37",
"@types/node": "^20.0.0",
"@types/passport": "^1.0.0",
Expand Down Expand Up @@ -106,6 +107,7 @@
"joi": "^14.3.1",
"jsonwebtoken": "^9.0.2",
"jwk-to-pem": "^2.0.5",
"libsodium-wrappers": "^0.7.13",
"morgan": "^1.9.1",
"openid-client": "^5.1.10",
"passport": "^0.6.0",
Expand Down
1 change: 1 addition & 0 deletions src/__mocks__/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export const mockProject = (): FullProject => ({
secret: 'my_secret',
enabled: true,
requester_circleCI: null,
requester_gitHub: null,
responder_slack: null,
id: '123',
repoName: 'my-repo',
Expand Down
4 changes: 4 additions & 0 deletions src/client/__tests__/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ describe('projectHasAnyConfig', () => {
expect(projectHasAnyConfig({ requester_circleCI: true } as any)).toBe(true);
});

it('should return true if the project has github configured', () => {
expect(projectHasAnyConfig({ requester_gitHub: true } as any)).toBe(true);
});

it('should return true if the project has slack configured', () => {
expect(projectHasAnyConfig({ responder_slack: true } as any)).toBe(true);
});
Expand Down
10 changes: 10 additions & 0 deletions src/client/components/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { CircleCILogo } from './icons/CircleCI';
import { SlackLogo } from './icons/Slack';
import { Link } from 'react-router-dom';
import { cx, projectHasAnyConfig, defaultFetchInit, defaultBodyReader } from '../utils';
import { GitHubLogo } from './icons/GitHub';

export function Dashboard() {
const reposFetch = useFetch<ReposResponse>('/api/repos', defaultFetchInit, defaultBodyReader);
Expand Down Expand Up @@ -146,6 +147,15 @@ export function Dashboard() {
<span>CircleCI</span>
</Pane>
) : null}
{repo.requester_gitHub ? (
<Pane className={styles.configRow}>
<Tooltip content="CFA Requester" position={Position.LEFT}>
<CircleArrowRightIcon color="success" />
</Tooltip>
<GitHubLogo className={styles.configIcon} />
<span>GitHub Actions</span>
</Pane>
) : null}
{repo.responder_slack ? (
<Pane className={styles.configRow}>
<Tooltip content="CFA Responder" position={Position.LEFT}>
Expand Down
1 change: 0 additions & 1 deletion src/client/components/MenuHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ export function MenuHeaderInner() {
<Avatar
name={user ? user.displayName : '?'}
size={24}
isSolid
src={user ? `https://github.com/${user.username}.png` : undefined}
/>
<span>{user ? user.username : '?'}</span>
Expand Down
13 changes: 13 additions & 0 deletions src/client/components/RequesterConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { FullProject } from '../../common/types';
import styles from './ReqResConfig.scss';
import { CircleCILogo } from './icons/CircleCI';
import { CircleCIRequesterConfig } from './configurators/CircleCIRequesterConfig';
import { GitHubLogo } from './icons/GitHub';
import { GitHubActionsRequesterConfig } from './configurators/GitHubActionsRequesterConfig';
export interface Props {
project: FullProject;
setProject: (newProject: FullProject) => void;
Expand All @@ -14,10 +16,12 @@ export interface Props {
enum RequesterTab {
NOTHING_YET,
CIRCLE_CI,
GITHUB_ACTIONS,
}

const defaultTabForProject = (project: FullProject) => {
if (project.requester_circleCI) return RequesterTab.CIRCLE_CI;
if (project.requester_gitHub) return RequesterTab.GITHUB_ACTIONS;
return RequesterTab.NOTHING_YET;
};

Expand Down Expand Up @@ -61,6 +65,13 @@ export function RequesterConfig({ project, setProject }: Props) {
>
<CircleCILogo className={styles.tabIcon} /> Circle CI
</Tab>
<Tab
onSelect={() => setActiveTab(RequesterTab.GITHUB_ACTIONS)}
isSelected={activeTab === RequesterTab.GITHUB_ACTIONS}
style={{ paddingLeft: 28, position: 'relative' }}
>
<GitHubLogo className={styles.tabIcon} /> GitHub Actions
</Tab>
<Tab disabled>More Coming Soon...</Tab>
</Tablist>
</Pane>
Expand All @@ -69,6 +80,8 @@ export function RequesterConfig({ project, setProject }: Props) {
<Paragraph>No Requester has been configured, choose one to get started!</Paragraph>
) : activeTab === RequesterTab.CIRCLE_CI ? (
<CircleCIRequesterConfig project={project} setProject={setProject} />
) : activeTab === RequesterTab.GITHUB_ACTIONS ? (
<GitHubActionsRequesterConfig project={project} setProject={setProject} />
) : null}
</Pane>
</Pane>
Expand Down
8 changes: 8 additions & 0 deletions src/client/components/__tests__/RequesterConfig.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,12 @@ describe('<RequesterConfig />', () => {
const mounted = mount(<RequesterConfig project={project} setProject={setProject} />);
expect(mounted.find('CircleCIRequesterConfig')).toHaveLength(1);
});

it('Should show the github configurator when github has been configured on the provided project', () => {
const setProject = jest.fn();
const project = mockProject();
project.requester_gitHub = {};
const mounted = mount(<RequesterConfig project={project} setProject={setProject} />);
expect(mounted.find('GitHubActionsRequesterConfig')).toHaveLength(1);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ exports[`<MenuHeader /> Should render a logo 1`] = `
className="right"
>
<Memo(ForwardRef(Avatar))
isSolid={true}
name="?"
size={24}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,21 @@ exports[`<RequesterConfig /> Should render 1`] = `
/>
Circle CI
</Memo(ForwardRef(Tab))>
<Memo(ForwardRef(Tab))
isSelected={false}
onSelect={[Function]}
style={
{
"paddingLeft": 28,
"position": "relative",
}
}
>
<GitHubLogo
className="tabIcon"
/>
GitHub Actions
</Memo(ForwardRef(Tab))>
<Memo(ForwardRef(Tab))
disabled={true}
>
Expand Down
112 changes: 112 additions & 0 deletions src/client/components/configurators/GitHubActionsRequesterConfig.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import * as React from 'react';
import { Alert, Button, Code, Heading, Pane, Paragraph, toaster } from 'evergreen-ui';

import { FullProject } from '../../../common/types';
import { useAsyncTaskFetch } from 'react-hooks-async';
import { defaultBodyReader } from '../../utils';

export interface Props {
project: FullProject;
setProject: (newProject: FullProject) => void;
}

export function GitHubActionsRequesterConfig({ project, setProject }: Props) {
const [showInstallButton, setShowInstallButton] = React.useState(false);
const options = React.useMemo(
() => ({
method: 'POST',
headers: new Headers({
'Content-Type': 'application/json',
}),
body: JSON.stringify({}),
}),
[project],
);

const installGitHubApp = React.useCallback(() => {
window.open('https://github.com/apps/continuous-auth/installations/new', '_blank');
setShowInstallButton(false);
}, []);

const createRequesterTask = useAsyncTaskFetch<FullProject>(
`/api/project/${project.id}/config/requesters/github`,
options,
defaultBodyReader,
);

const projectSlug = `${project.repoOwner}/${project.repoName}`;

React.useEffect(() => {
if (createRequesterTask.error) {
if (createRequesterTask.error.message.includes('412')) {
toaster.notify(`Continuous Auth not installed in ${projectSlug}`);
setShowInstallButton(true);
} else {
toaster.danger(`Failed to create the GitHub Requester, please try again later.`);
}
}
}, [createRequesterTask.error, projectSlug]);

React.useEffect(() => {
if (createRequesterTask.result) {
toaster.success(`Successfully created the GitHub Requester.`);
setProject(createRequesterTask.result);
}
}, [createRequesterTask.result]);

const saving = createRequesterTask.started && createRequesterTask.pending;

return (
<Pane>
<Pane>
<Heading size={400} marginBottom={8}>
GitHub Actions Secrets
</Heading>
{showInstallButton ? (
<>
<Alert marginBottom={4} intent="warning">
You need to install the Continuous Auth github app before we can set this project up.
<br />
<Button marginTop={12} onClick={installGitHubApp}>
Install
</Button>
</Alert>
</>
) : null}
{project.requester_gitHub ? (
<>
<Paragraph marginBottom={4}>
ContinuousAuth is fully set up, if you're having issues with secrets you can use the
"Fix" button below.
</Paragraph>
<Button
appearance="default"
marginLeft={8}
isLoading={saving}
disabled={showInstallButton}
onClick={() => createRequesterTask.start()}
>
Fix GitHub Actions for {project.repoOwner}/{project.repoName}
</Button>
</>
) : (
<>
<Paragraph marginBottom={4}>
ContinuousAuth needs to make some secrets in GitHub Actions in order to publish.
</Paragraph>
<Button
appearance="primary"
intent="success"
marginLeft={8}
isLoading={saving}
disabled={showInstallButton}
onClick={() => createRequesterTask.start()}
>
Use GitHub Actions for {project.repoOwner}/{project.repoName}
</Button>
</>
)}
</Pane>
</Pane>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ exports[`<CircleCIRequesterConfig /> Should render a GenericAccessTokenRequester
"repoName": "my-repo",
"repoOwner": "my-owner",
"requester_circleCI": null,
"requester_gitHub": null,
"responder_slack": null,
"secret": "my_secret",
}
Expand Down
17 changes: 17 additions & 0 deletions src/client/components/icons/GitHub.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import * as React from 'react';

import { Props } from './icon-props';

export function GitHubLogo(props: Props) {
return (
<svg {...props} viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.73 6.56 11.53C4.78 11.33 2.92 10.64 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.33 9.47 11.53C9.76 11.78 10.01 12.26 10.01 13.01C10.01 14.08 10 14.94 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z"
transform="scale(64)"
fill="#1B1F23"
/>
</svg>
);
}
11 changes: 11 additions & 0 deletions src/client/components/icons/__tests__/GitHub.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import { GitHubLogo } from '../GitHub';

describe('GitHubLogo Icon', () => {
it('Should render with a className', () => {
const wrapper = shallow(<GitHubLogo className="test_class_name" />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.prop('className')).toBe('test_class_name');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`GitHubLogo Icon Should render with a className 1`] = `
<svg
className="test_class_name"
fill="none"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
>
<path
clipRule="evenodd"
d="M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.73 6.56 11.53C4.78 11.33 2.92 10.64 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.33 9.47 11.53C9.76 11.78 10.01 12.26 10.01 13.01C10.01 14.08 10 14.94 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z"
fill="#1B1F23"
fillRule="evenodd"
transform="scale(64)"
/>
</svg>
`;
2 changes: 1 addition & 1 deletion src/client/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { SimpleProject } from '../common/types';
export const cx = (...args: (string | null | undefined)[]) => args.filter((a) => a).join(' ');

export const projectHasAnyConfig = (project: SimpleProject): boolean => {
return Boolean(project.requester_circleCI || project.responder_slack);
return Boolean(project.requester_circleCI || project.requester_gitHub || project.responder_slack);
};

export const defaultBodyReader = (body: any) => body.json();
Expand Down
4 changes: 4 additions & 0 deletions src/common/__tests__/types.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ describe('projectIsMissingConfig', () => {
projectIsMissingConfig({
responder_slack: {},
requester_circleCI: {},
requester_gitHub: null,
}),
).toBe(false);
});
Expand All @@ -15,6 +16,7 @@ describe('projectIsMissingConfig', () => {
projectIsMissingConfig({
responder_slack: {},
requester_circleCI: null,
requester_gitHub: null,
}),
).toBe(true);
});
Expand All @@ -24,6 +26,7 @@ describe('projectIsMissingConfig', () => {
projectIsMissingConfig({
responder_slack: null,
requester_circleCI: {},
requester_gitHub: null,
}),
).toBe(true);
});
Expand All @@ -33,6 +36,7 @@ describe('projectIsMissingConfig', () => {
projectIsMissingConfig({
responder_slack: null,
requester_circleCI: null,
requester_gitHub: null,
}),
).toBe(true);
});
Expand Down
4 changes: 3 additions & 1 deletion src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface SimpleRepo {

export interface SimpleProject extends SimpleRepo {
requester_circleCI: boolean;
requester_gitHub: boolean;
responder_slack: {
team: string;
channel: string;
Expand All @@ -26,6 +27,7 @@ export interface FullProject extends SimpleRepo {
requester_circleCI: {
accessToken: string;
} | null;
requester_gitHub: {} | null;
responder_slack: {
teamName: string;
channelName: string;
Expand Down Expand Up @@ -54,7 +56,7 @@ export const projectIsMissingConfig = (
>
>,
) => {
const hasRequester: boolean = !!project.requester_circleCI;
const hasRequester: boolean = !!project.requester_circleCI || !!project.requester_gitHub;
const hasResponder: boolean = !!project.responder_slack;
return !hasRequester || !hasResponder;
};
Loading

0 comments on commit 63893af

Please sign in to comment.