Skip to content

Commit

Permalink
Merge pull request #450 from middlewarehq/GROW-1674
Browse files Browse the repository at this point in the history
Support Github actions as a deployment source
  • Loading branch information
shivam-bit authored Jul 1, 2024
2 parents 30c2d19 + 24f819b commit d7f866b
Show file tree
Hide file tree
Showing 15 changed files with 570 additions and 176 deletions.
2 changes: 1 addition & 1 deletion web-server/pages/api/integrations/github/orgs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as yup from 'yup';
import { Endpoint, nullSchema } from '@/api-helpers/global';
import { Row, Table } from '@/constants/db';
import { selectedDBReposMock } from '@/mocks/github';
import { DB_OrgRepo } from '@/types/api/org_repo';
import { DB_OrgRepo } from '@/types/resources';
import { db } from '@/utils/db';

const patchSchema = yup.object().shape({
Expand Down
3 changes: 1 addition & 2 deletions web-server/pages/api/integrations/orgs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ import {
WorkflowType
} from '@/constants/integrations';
import { selectedDBReposMock } from '@/mocks/github';
import { DB_OrgRepo } from '@/types/api/org_repo';
import { ReqOrgRepo, ReqRepo } from '@/types/resources';
import { DB_OrgRepo, ReqOrgRepo, ReqRepo } from '@/types/resources';
import { db } from '@/utils/db';
import groupBy from '@/utils/objectArray';

Expand Down
52 changes: 37 additions & 15 deletions web-server/pages/api/integrations/selected.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { groupBy, mapObjIndexed, prop } from 'ramda';
import { groupBy, mapObjIndexed, prop, uniqBy } from 'ramda';
import * as yup from 'yup';

import { Endpoint, nullSchema } from '@/api-helpers/global';
import { Table } from '@/constants/db';
import { Table, Row } from '@/constants/db';
import { Integration } from '@/constants/integrations';
import { selectedDBReposMock } from '@/mocks/github';
import {
Expand Down Expand Up @@ -40,28 +40,50 @@ export const getSelectedReposForOrg = async (
dbRaw.raw(true)
);
})
.leftJoin({ tr: Table.TeamRepos }, function () {
this.on('OrgRepo.id', 'tr.org_repo_id');
})
.select('OrgRepo.*')
.select(dbRaw.raw('to_json(rw) as repo_workflow'))
.select('tr.deployment_type', 'tr.team_id')
.from('OrgRepo')
.where({ org_id, 'OrgRepo.provider': provider })
.andWhereNot('OrgRepo.is_active', false);

const reposGroupedById = mapObjIndexed((repos: RepoWithSingleWorkflow[]) => {
return repos.reduce((map, repo) => {
const updatedRepo = {
...repo,
repo_workflows: [
...(map.repo_workflows || []),
repo.repo_workflow
].filter(Boolean)
const repoToWorkflowMap = dbRepos.reduce(
(map, repo) => {
return {
...map,
[repo.id]: uniqBy(
prop('id'),
[...(map[repo.id] || []), repo.repo_workflow].filter(Boolean)
)
};
},
{} as Record<ID, Row<'RepoWorkflow'>[]>
);

delete updatedRepo.repo_workflow;

return updatedRepo;
}, {} as RepoWithMultipleWorkflows);
}, groupBy(prop('id'), dbRepos));
const reposGroupedById = mapObjIndexed(
(repos: RepoWithSingleWorkflow[]) => {
return repos.reduce((_, repo) => {
const updatedRepo = {
...repo,
repo_workflows: repoToWorkflowMap[repo.id]
.map((workflow) => {
return {
name: workflow.name,
value: workflow.provider_workflow_id
};
})
.filter((workflow) => workflow.value)
};
delete updatedRepo.repo_workflow;

return updatedRepo as any as RepoWithMultipleWorkflows;
}, {} as RepoWithMultipleWorkflows);
},
groupBy(prop('id'), dbRepos)
);
return Object.values(reposGroupedById);
};

Expand Down
157 changes: 117 additions & 40 deletions web-server/pages/api/resources/orgs/[org_id]/teams/v2.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,43 @@
import { groupBy, prop, mapObjIndexed, forEachObjIndexed } from 'ramda';
import {
groupBy as ramdaGroupBy,
prop,
mapObjIndexed,
forEachObjIndexed
} from 'ramda';
import * as yup from 'yup';

import { getSelectedReposForOrg } from '@/api/integrations/selected';
import { syncReposForOrg } from '@/api/internal/[org_id]/sync_repos';
import {
getOnBoardingState,
updateOnBoardingState
} from '@/api/resources/orgs/[org_id]/onboarding';
import { getTeamRepos } from '@/api/resources/team_repos';
import { handleRequest } from '@/api-helpers/axios';
import { Endpoint } from '@/api-helpers/global';
import { Row } from '@/constants/db';
import { Integration } from '@/constants/integrations';
import { Row, Table } from '@/constants/db';
import {
CIProvider,
Integration,
WorkflowType
} from '@/constants/integrations';
import { getTeamV2Mock } from '@/mocks/teams';
import { BaseTeam } from '@/types/api/teams';
import { OnboardingStep, ReqRepoWithProvider } from '@/types/resources';
import { db, getFirstRow } from '@/utils/db';
import groupBy from '@/utils/objectArray';

const repoSchema = yup.object().shape({
idempotency_key: yup.string().required(),
deployment_type: yup.string().required(),
slug: yup.string().required(),
name: yup.string().required(),
repo_workflows: yup.array().of(
yup.object().shape({
name: yup.string().required(),
value: yup.string().required()
})
)
});

const getSchema = yup.object().shape({
provider: yup.string().oneOf(Object.values(Integration)).required()
Expand All @@ -24,19 +47,7 @@ const postSchema = yup.object().shape({
name: yup.string().required(),
provider: yup.string().oneOf(Object.values(Integration)).required(),
org_repos: yup.lazy((obj) =>
yup.object(
mapObjIndexed(
() =>
yup.array().of(
yup.object().shape({
idempotency_key: yup.string().required(),
slug: yup.string().required(),
name: yup.string().required()
})
),
obj
)
)
yup.object(mapObjIndexed(() => yup.array().of(repoSchema), obj))
)
});

Expand All @@ -45,19 +56,7 @@ const patchSchema = yup.object().shape({
name: yup.string().nullable().optional(),
provider: yup.string().oneOf(Object.values(Integration)).required(),
org_repos: yup.lazy((obj) =>
yup.object(
mapObjIndexed(
() =>
yup.array().of(
yup.object().shape({
idempotency_key: yup.string().required(),
slug: yup.string().required(),
name: yup.string().required()
})
),
obj
)
)
yup.object(mapObjIndexed(() => yup.array().of(repoSchema), obj))
)
});

Expand All @@ -84,14 +83,14 @@ endpoint.handle.GET(getSchema, async (req, res) => {
.orderBy('name', 'asc');

const teams = await getQuery;

const repos = (
await Promise.all(teams.map((team) => getTeamRepos(team.id)))
).flat();

const reposWithWorkflows = await getSelectedReposForOrg(
org_id,
provider as Integration
);
res.send({
teams: teams,
teamReposMap: groupBy(prop('team_id'), repos)
teamReposMap: ramdaGroupBy(prop('team_id'), reposWithWorkflows),
reposWithWorkflows
});
});

Expand Down Expand Up @@ -132,15 +131,18 @@ endpoint.handle.POST(postSchema, async (req, res) => {
updateOnBoardingState(org_id, updatedOnboardingState);
syncReposForOrg();

res.send({ team, teamReposMap: groupBy(prop('team_id'), teamRepos) });
res.send({
team,
teamReposMap: ramdaGroupBy(prop('team_id'), teamRepos)
});
});

endpoint.handle.PATCH(patchSchema, async (req, res) => {
if (req.meta?.features?.use_mock_data) {
return res.send(getTeamV2Mock);
}

const { id, name, org_repos, provider } = req.payload;
const { org_id, id, name, org_repos, provider } = req.payload;
const orgReposList: ReqRepoWithProvider[] = [];
forEachObjIndexed((repos, org) => {
repos.forEach((repo) => {
Expand All @@ -159,10 +161,15 @@ endpoint.handle.PATCH(patchSchema, async (req, res) => {
data: {
repos: orgReposList
}
}).then((repos) => repos.map((r) => ({ ...r, team_id: id })))
}).then((repos) => repos.map((r) => ({ ...r, team_id: id }))),
updateReposWorkflows(org_id, provider as Integration, orgReposList)
]);

syncReposForOrg();
res.send({ team, teamReposMap: groupBy(prop('team_id'), teamRepos) });
res.send({
team,
teamReposMap: ramdaGroupBy(prop('team_id'), teamRepos)
});
});

endpoint.handle.DELETE(deleteSchema, async (req, res) => {
Expand Down Expand Up @@ -209,3 +216,73 @@ const createTeam = async (
}
});
};

const updateReposWorkflows = async (
org_id: ID,
provider: Integration,
orgReposList: ReqRepoWithProvider[]
) => {
const repoWorkflows = orgReposList.reduce(
(prev, curr) => ({
...prev,
[curr.name]:
curr.repo_workflows?.map((w) => ({
value: String(w.value),
name: w.name
})) || []
}),
{} as Record<string, { name: string; value: string }[]>
);

const reposForWorkflows = Object.keys(repoWorkflows);

if (
reposForWorkflows.length &&
(provider === Integration.GITHUB || provider === Integration.BITBUCKET)
) {
// Step 1: Get all repos for the workflows
const dbReposForWorkflows = await db(Table.OrgRepo)
.select('*')
.whereIn('name', reposForWorkflows)
.where('org_id', org_id)
.andWhere('is_active', true)
.andWhere('provider', provider);

const groupedRepos = groupBy(dbReposForWorkflows, 'name');

// Step 2: Disable all workflows for the above db repos
await db('RepoWorkflow')
.update('is_active', false)
.whereIn(
'org_repo_id',
dbReposForWorkflows.map((r) => r.id)
)
.andWhere('type', WorkflowType.DEPLOYMENT);

const newWorkflows = Object.entries(repoWorkflows)
.filter(([repoName]) => groupedRepos[repoName]?.id)
.flatMap(([repoName, workflows]) =>
workflows.map((workflow) => ({
is_active: true,
name: workflow.name,
provider:
provider === Integration.GITHUB
? CIProvider.GITHUB_ACTIONS
: provider === Integration.BITBUCKET
? CIProvider.CIRCLE_CI
: null,
provider_workflow_id: String(workflow.value),
type: WorkflowType.DEPLOYMENT,
org_repo_id: groupedRepos[repoName]?.id
}))
);

if (newWorkflows.length) {
await db('RepoWorkflow')
.insert(newWorkflows)
.onConflict(['org_repo_id', 'provider_workflow_id'])
.merge()
.returning('*');
}
}
};
3 changes: 1 addition & 2 deletions web-server/src/components/OverlayComponents/TeamEdit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@ import { FlexBox } from '../FlexBox';
import { useOverlayPage } from '../OverlayPageContext';
import { CreateEditTeams } from '../Teams/CreateTeams';

export const TeamEdit: FC<CRUDProps> = ({ teamId, hideCardComponents }) => {
export const TeamEdit: FC<CRUDProps> = ({ teamId }) => {
const { removeAll } = useOverlayPage();
const pageRefreshCallback = usePageRefreshCallback();

return (
<FlexBox>
<CreateEditTeams
teamId={teamId}
hideCardComponents={hideCardComponents}
onDiscard={removeAll}
onSave={() => {
removeAll();
Expand Down
2 changes: 1 addition & 1 deletion web-server/src/components/RepoCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { DataObjectRounded } from '@mui/icons-material';
import { Typography, styled } from '@mui/material';

import GitBranch from '@/assets/git-merge-line.svg';
import { DB_OrgRepo } from '@/types/api/org_repo';
import { Repo } from '@/types/github';
import { DB_OrgRepo } from '@/types/resources';

export const RepoTitle = styled(Typography)(() => ({
textOverflow: 'ellipsis',
Expand Down
3 changes: 1 addition & 2 deletions web-server/src/components/TeamSelector/TeamPopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -281,8 +281,7 @@ export const TeamPopover: FC<{
title: 'Edit team',
ui: 'team_edit',
props: {
teamId: apiTeam.id,
hideCardComponents: true
teamId: apiTeam.id
}
}
});
Expand Down
Loading

0 comments on commit d7f866b

Please sign in to comment.