Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: add new endpoint for backfill #2601

Merged
merged 34 commits into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
be99c17
- Modal added for backfilling and disconnecting subscription
krazziekay Dec 5, 2023
150023e
- Fixing eslint in a test case
krazziekay Dec 5, 2023
4dd1eed
- WIP - backfill modal
krazziekay Dec 5, 2023
7377807
- WIP - backfill modal
krazziekay Dec 5, 2023
e5d872b
Merge branch 'main' of github.com:atlassian/github-for-jira into ARC-…
krazziekay Dec 5, 2023
9379076
Merge branch 'main' into ARC-adding-modals-for-backfill-page
krazziekay Dec 5, 2023
e940786
Merge branch 'main' of github.com:atlassian/github-for-jira into ARC-…
krazziekay Dec 5, 2023
54747ff
- WIP - backfill modal
krazziekay Dec 5, 2023
ef67e5a
Merge branch 'main' into ARC-adding-modals-for-backfill-page
krazziekay Dec 6, 2023
85c0ec9
chore: add new endpoint for backfill
kamaksheeAtl Dec 6, 2023
75bf2cd
Merge branch 'main' into ARC-2714-Kamakshee
kamaksheeAtl Dec 6, 2023
b8a0402
chore: add new endpoint for backfill
kamaksheeAtl Dec 6, 2023
d2b2ce1
chore: add new endpoint for backfill
kamaksheeAtl Dec 6, 2023
2a2fcac
chore: add test cases
kamaksheeAtl Dec 6, 2023
8675da7
chore: add test cases
kamaksheeAtl Dec 6, 2023
1ed4f9b
chore: PR comments
kamaksheeAtl Dec 6, 2023
d077876
chore: PR comments
kamaksheeAtl Dec 6, 2023
4da351c
chore: PR comments
kamaksheeAtl Dec 6, 2023
b12fe93
chore: PR comments
kamaksheeAtl Dec 6, 2023
2ea4d5d
chore: PR comments
kamaksheeAtl Dec 6, 2023
ae84f00
chore: PR comments
kamaksheeAtl Dec 6, 2023
0b4d49f
- Using the corrected datepicker
krazziekay Dec 7, 2023
e2bf47a
Merge branch 'ARC-adding-modals-for-backfill-page' into ARC-2714-Kama…
kamaksheeAtl Dec 7, 2023
3726cea
chore: merge main
kamaksheeAtl Dec 7, 2023
a4ea01a
chore: PR comment
kamaksheeAtl Dec 7, 2023
263b5dc
chore: revoke unnecessary changes
kamaksheeAtl Dec 7, 2023
1f6533f
chore: revoke unnecessary changes
kamaksheeAtl Dec 7, 2023
b772dad
Merge branch 'main' into ARC-2714-Kamakshee
kamaksheeAtl Dec 7, 2023
7888cd0
chore: revoke unnecessary changes
kamaksheeAtl Dec 7, 2023
f1a77b2
chore: revoke unnecessary changes
kamaksheeAtl Dec 7, 2023
f478afc
chore: revoke unnecessary changes
kamaksheeAtl Dec 7, 2023
c40b726
chore: revoke unnecessary changes
kamaksheeAtl Dec 7, 2023
93183ab
chore: revoke unnecessary changes
kamaksheeAtl Dec 7, 2023
2519132
chore: revoke unnecessary changes
kamaksheeAtl Dec 7, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion spa/src/api/subscriptions/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { RestSyncReqBody } from "~/src/rest-interfaces";
import { axiosRest } from "../axiosInstance";

export default {
getSubscriptions: () => axiosRest.get("/rest/subscriptions")
getSubscriptions: () => axiosRest.get("/rest/subscriptions"),
syncSubscriptions: (data: RestSyncReqBody) => axiosRest.post(`/rest/app/cloud/sync`, data),
kamaksheeAtl marked this conversation as resolved.
Show resolved Hide resolved
};
6 changes: 6 additions & 0 deletions src/rest-interfaces/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
export type RestSyncReqBody = {
installationId: number;
syncType: string;
source: string;
commitsFromDate: string;
}

export type GetRedirectUrlResponse = {
redirectUrl: string;
Expand Down
10 changes: 10 additions & 0 deletions src/rest/rest-router.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Router } from "express";
import { JwtHandler } from "./middleware/jwt/jwt-handler";
import { body } from "express-validator";
import { OAuthRouter } from "./routes/oauth";
import { OAuthCallbackHandler, OrgsInstalledHandler, OrgsInstallRequestedHandler } from "./routes/github-callback";
import { GitHubOrgsRouter } from "./routes/github-orgs";
Expand All @@ -10,6 +11,7 @@ import { RestErrorHandler } from "./middleware/error";
import { JiraAdminEnforceMiddleware } from "./middleware/jira-admin/jira-admin-check";
import { AnalyticsProxyHandler } from "./routes/analytics-proxy";
import { SubscriptionsRouter } from "./routes/subscriptions";
import { SyncRouterHandler } from "./routes/sync";
import { DeferredRouter } from "./routes/deferred";

export const RestRouter = Router({ mergeParams: true });
Expand All @@ -27,6 +29,14 @@ RestRouter.use("/subscriptions", JwtHandler, JiraAdminEnforceMiddleware, Subscri
*/
RestRouter.use("/app/:cloudOrUUID", subRouter);

subRouter.post(
kamaksheeAtl marked this conversation as resolved.
Show resolved Hide resolved
"/sync",
body("commitsFromDate").optional().isISO8601(),
JwtHandler,
JiraAdminEnforceMiddleware,
SyncRouterHandler
);

subRouter.get("/github-callback", OAuthCallbackHandler);
subRouter.get("/github-installed", OrgsInstalledHandler);
subRouter.get("/github-requested", OrgsInstallRequestedHandler);
Expand Down
155 changes: 155 additions & 0 deletions src/rest/routes/sync/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { getFrontendApp } from "~/src/app";
import { Installation } from "models/installation";
import { Subscription } from "models/subscription";
import express, { Express } from "express";
import { RootRouter } from "routes/router";
import supertest from "supertest";
import { encodeSymmetric } from "atlassian-jwt";
import { GitHubServerApp } from "models/github-server-app";
import { v4 as newUUID } from "uuid";
import { sqsQueues } from "~/src/sqs/queues";

jest.mock("~/src/sqs/queues");
jest.mock("config/feature-flags");

describe("Checking the deferred request parsing route", () => {
let app: Express;
let installation: Installation;
const installationIdForCloud = 1;
const installationIdForServer = 2;
let gitHubServerApp: GitHubServerApp;
// let jwt: string;
const testSharedSecret = "test-secret";
const clientKey = "jira-client-key";
const getToken = ({
secret = testSharedSecret,
iss = clientKey,
exp = Date.now() / 1000 + 10000,
qsh = "context-qsh",
sub = "myAccount" } = {}): string => {
return encodeSymmetric({
qsh,
iss,
exp,
sub
}, secret);
};
beforeEach(async () => {
app = getFrontendApp();
installation = await Installation.install({
host: jiraHost,
sharedSecret: testSharedSecret,
clientKey: clientKey
});
await Subscription.install({
installationId: installationIdForCloud,
host: jiraHost,
hashedClientKey: installation.clientKey,
gitHubAppId: undefined
});
gitHubServerApp = await GitHubServerApp.install({
uuid: newUUID(),
appId: 123,
gitHubAppName: "My GitHub Server App",
gitHubBaseUrl: gheUrl,
gitHubClientId: "lvl.1234",
gitHubClientSecret: "myghsecret",
webhookSecret: "mywebhooksecret",
privateKey: "myprivatekey",
installationId: installation.id
}, jiraHost);
await Subscription.install({
installationId: installationIdForServer,
host: jiraHost,
hashedClientKey: installation.clientKey,
gitHubAppId: gitHubServerApp.id
});
app = express();
app.use(RootRouter);
});

describe("cloud", () => {
it("should throw 401 error when no github token is passed", async () => {
const resp = await supertest(app)
.get("/rest/app/cloud/sync");

expect(resp.status).toEqual(401);
});

it("should return 202 on correct post for /rest/app/cloud/sync one for Cloud app", async () => {
return supertest(app)
.post("/rest/app/cloud/sync")
.set("authorization", `${getToken()}`)
.send({
installationId: installationIdForCloud,
jiraHost
})
.expect(202)
.then(() => {
expect(sqsQueues.backfill.sendMessage).toBeCalledWith(expect.objectContaining({
installationId: installationIdForCloud,
jiraHost,
startTime: expect.anything(),
gitHubAppConfig: expect.objectContaining({ gitHubAppId: undefined, uuid: undefined })
}), expect.anything(), expect.anything());
});
});

it("should run incremental sync", async() => {
const commitsFromDate = new Date(new Date().getTime() - 2000);
const backfillSince = new Date(new Date().getTime() - 1000);
const subscription = await Subscription.getSingleInstallation(
jiraHost,
installationIdForServer,
gitHubServerApp.id
);
await subscription?.update({
syncStatus: "COMPLETE",
backfillSince
});
return supertest(app)
.post(`/rest/app/${gitHubServerApp.id}/sync`)
.set("authorization", `${getToken()}`)
.send({
installationId: installationIdForServer,
jiraHost,
commitsFromDate
})
.expect(202)
.then(() => {
expect(sqsQueues.backfill.sendMessage).toBeCalledWith(expect.objectContaining({
syncType: "partial",
installationId: installationIdForServer,
jiraHost,
commitsFromDate: commitsFromDate.toISOString(),
targetTasks: ["pull", "branch", "commit", "build", "deployment", "dependabotAlert", "secretScanningAlert", "codeScanningAlert"],
gitHubAppConfig: expect.objectContaining({ gitHubAppId: gitHubServerApp.id, uuid: gitHubServerApp.uuid })
}), expect.anything(), expect.anything());
});
});

it("should run full sync if explicitly selected by user", async () => {
const commitsFromDate = new Date(new Date().getTime() - 2000);
return supertest(app)
.post(`/rest/app/${gitHubServerApp.id}/sync`)
.set("authorization", `${getToken()}`)
.send({
installationId: installationIdForServer,
jiraHost,
commitsFromDate,
syncType: "full"
})
.expect(202)
.then(() => {
expect(sqsQueues.backfill.sendMessage).toBeCalledWith(expect.objectContaining({
syncType: "full",
installationId: installationIdForServer,
jiraHost,
commitsFromDate: commitsFromDate.toISOString(),
targetTasks: undefined,
gitHubAppConfig: expect.objectContaining({ gitHubAppId: gitHubServerApp.id, uuid: gitHubServerApp.uuid })
}), expect.anything(), expect.anything());
});
});
});
});
74 changes: 74 additions & 0 deletions src/rest/routes/sync/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Request, Response } from "express";
import { ParamsDictionary } from "express-serve-static-core";
import { errorWrapper } from "../../helper";
import { Subscription } from "models/subscription";
import { findOrStartSync } from "~/src/sync/sync-utils";
import { determineSyncTypeAndTargetTasks } from "~/src/util/github-sync-helper";
import { BaseLocals } from "..";
import { RestApiError } from "~/src/config/errors";
import { RestSyncReqBody } from "~/src/rest-interfaces";


const restSyncPost = async (
req: Request<ParamsDictionary, unknown, RestSyncReqBody>,
res: Response<unknown, BaseLocals>
kamaksheeAtl marked this conversation as resolved.
Show resolved Hide resolved
) => {
//TODO: We are yet to handle enterprise backfill
const cloudOrUUID = req.params.cloudOrUUID;
const gheUUID = cloudOrUUID === "cloud" ? undefined : req.params.cloudOrUUID;
let gitHubAppId : number | undefined = undefined;
if (gheUUID) {
gitHubAppId = parseFloat(gheUUID);
kamaksheeAtl marked this conversation as resolved.
Show resolved Hide resolved
}

const {
installationId: gitHubInstallationId,
syncType: syncTypeFromReq,
source
} = req.body;
// A date to start fetching commit history(main and branch) from.
const commitsFromDate = req.body.commitsFromDate
? new Date(req.body.commitsFromDate)
: undefined;

try {
const subscription = await Subscription.getSingleInstallation(
res.locals.installation.jiraHost,
gitHubInstallationId,
gitHubAppId
);
if (!subscription) {
req.log.info(
{
jiraHost: res.locals.installation.jiraHost,
installationId: gitHubInstallationId
},
"Subscription not found when retrying sync."
);
throw new RestApiError(400, "INVALID_OR_MISSING_ARG", "Subscription not found, cannot resync.");
}

if (commitsFromDate && commitsFromDate.valueOf() > Date.now()) {
throw new RestApiError(400, "INVALID_OR_MISSING_ARG", "Invalid date value, cannot select a future date");
}

const { syncType, targetTasks } = determineSyncTypeAndTargetTasks(
syncTypeFromReq,
subscription
);
await findOrStartSync(
subscription,
req.log,
syncType,
commitsFromDate || subscription.backfillSince,
targetTasks,
{ source }
);

res.sendStatus(202);
} catch (error: unknown) {
throw new RestApiError(500, "UNKNOWN", "Something went wrong");
kamaksheeAtl marked this conversation as resolved.
Show resolved Hide resolved
}
};

export const SyncRouterHandler = errorWrapper("AnalyticsProxyHandler",restSyncPost);
29 changes: 3 additions & 26 deletions src/routes/jira/sync/jira-sync-post.ts
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic for restSyncPost and this endpoint is the same. Would be better to pull this common logic out into another method and reuse it in both places.
As for the analytics, can add an extra attribute spa: true | false.

Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Subscription, SyncStatus } from "models/subscription";
import { Subscription } from "models/subscription";
import * as Sentry from "@sentry/node";
import { NextFunction, Request, Response } from "express";
import { findOrStartSync } from "~/src/sync/sync-utils";
import { sendAnalytics } from "utils/analytics-client";
import { AnalyticsEventTypes, AnalyticsTrackEventsEnum, AnalyticsTrackSource } from "interfaces/common";
import { TaskType, SyncType } from "~/src/sync/sync.types";
import { booleanFlag, BooleanFlags } from "config/feature-flags";
import { determineSyncTypeAndTargetTasks, getStartTimeInDaysAgo } from "../../../util/github-sync-helper";

export const JiraSyncPost = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
const { installationId: gitHubInstallationId, appId: gitHubAppId, syncType: syncTypeFromReq, source } = req.body;
Expand Down Expand Up @@ -35,7 +35,7 @@ export const JiraSyncPost = async (req: Request, res: Response, next: NextFuncti
return;
}

const { syncType, targetTasks } = await determineSyncTypeAndTargetTasks(syncTypeFromReq, subscription);
const { syncType, targetTasks } = determineSyncTypeAndTargetTasks(syncTypeFromReq, subscription);
await findOrStartSync(subscription, req.log, syncType, commitsFromDate || subscription.backfillSince, targetTasks, { source });

await sendAnalytics(res.locals.jiraHost, AnalyticsEventTypes.TrackEvent, {
Expand Down Expand Up @@ -64,26 +64,3 @@ export const JiraSyncPost = async (req: Request, res: Response, next: NextFuncti
next(new Error("Unauthorized"));
}
};

const MILLISECONDS_IN_ONE_DAY = 24 * 60 * 60 * 1000;
const getStartTimeInDaysAgo = (commitsFromDate: Date | undefined) => {
if (commitsFromDate === undefined) return undefined;
return Math.floor((Date.now() - commitsFromDate?.getTime()) / MILLISECONDS_IN_ONE_DAY);
};

type SyncTypeAndTargetTasks = {
syncType: SyncType,
targetTasks: TaskType[] | undefined,
};

const determineSyncTypeAndTargetTasks = async (syncTypeFromReq: string, subscription: Subscription): Promise<SyncTypeAndTargetTasks> => {
if (syncTypeFromReq === "full") {
return { syncType: "full", targetTasks: undefined };
}

if (subscription.syncStatus === SyncStatus.FAILED) {
return { syncType: "full", targetTasks: undefined };
}

return { syncType: "partial", targetTasks: ["pull", "branch", "commit", "build", "deployment", "dependabotAlert", "secretScanningAlert", "codeScanningAlert"] };
};
26 changes: 26 additions & 0 deletions src/util/github-sync-helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Subscription, SyncStatus } from "models/subscription";
import { TaskType, SyncType } from "~/src/sync/sync.types";


const MILLISECONDS_IN_ONE_DAY = 24 * 60 * 60 * 1000;
export const getStartTimeInDaysAgo = (commitsFromDate: Date | undefined) => {
if (commitsFromDate === undefined) return undefined;
return Math.floor((Date.now() - commitsFromDate.getTime()) / MILLISECONDS_IN_ONE_DAY);
};

type SyncTypeAndTargetTasks = {
syncType: SyncType,
targetTasks: TaskType[] | undefined,
};

export const determineSyncTypeAndTargetTasks = (syncTypeFromReq: string, subscription: Subscription): SyncTypeAndTargetTasks => {
if (syncTypeFromReq === "full") {
return { syncType: "full", targetTasks: undefined };
}

if (subscription.syncStatus === SyncStatus.FAILED) {
return { syncType: "full", targetTasks: undefined };
}

return { syncType: "partial", targetTasks: ["pull", "branch", "commit", "build", "deployment", "dependabotAlert", "secretScanningAlert", "codeScanningAlert"] };
};
2 changes: 2 additions & 0 deletions test/snapshots/app.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ exports[`app getFrontendApp please review routes and update snapshot when adding
query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,serveStatic
:GET ^/?(?=/|$)^/rest/?(?=/|$)^/subscriptions/?(?=/|$)^/?$
query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,SubscriptionsGet
:POST ^/?(?=/|$)^/rest/?(?=/|$)^/app/(?:([^/]+?))/?(?=/|$)^/sync/?$
query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,middleware,JwtHandler,jiraAdminEnforceMiddleware,AnalyticsProxyHandler
:GET ^/?(?=/|$)^/rest/?(?=/|$)^/app/(?:([^/]+?))/?(?=/|$)^/github-callback/?$
query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,OAuthCallbackHandler
:GET ^/?(?=/|$)^/rest/?(?=/|$)^/app/(?:([^/]+?))/?(?=/|$)^/github-installed/?$
Expand Down
Loading