-
-
Notifications
You must be signed in to change notification settings - Fork 111
/
startVercelDeployment.ts
198 lines (171 loc) · 8.11 KB
/
startVercelDeployment.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
import redirect from '@/common/utils/redirect';
import { logEvent } from '@/modules/core/amplitude/amplitudeServerClient';
import {
AMPLITUDE_API_ENDPOINTS,
AMPLITUDE_EVENTS,
} from '@/modules/core/amplitude/events';
import dispatchWorkflowByPath from '@/modules/core/githubActions/dispatchWorkflowByPath';
import { createLogger } from '@/modules/core/logging/logger';
import { ALERT_TYPES } from '@/modules/core/sentry/config';
import { configureReq } from '@/modules/core/sentry/server';
import { flushSafe } from '@/modules/core/sentry/universal';
import * as Sentry from '@sentry/node';
import size from 'lodash.size';
import {
NextApiRequest,
NextApiResponse,
} from 'next';
const fileLabel = 'api/startVercelDeployment';
const logger = createLogger({
fileLabel,
});
type EndpointRequestQuery = {
/**
* Customer authentication token. (security)
*
* Used to make sure "naked" calls to the endpoint won't trigger a production deployment.
* E.g: A bot calling "/api/startVercelDeployment" will not trigger a deployment, because no token is provided.
*
* Used to make sure the request is authenticated, by using a token that belongs to the current customer.
* E.g: A customer A might call the "/api/startVercelDeployment" endpoint of another customer B, using the token of customer A will not work.
*
* @example ?customerAuthToken=customer1 Token for customer1
* @example ?customerAuthToken=customer2 Token for customer2
*/
customerAuthToken: string;
/**
* Release reference of the platform.
* Basically, a Git commit hash, branch name, or tag.
*
* The ref used will be used to locate what version of the source code should be used for the deployment.
*
* XXX By design, should use the same ref as the one used by the staging environment, by default.
* This way, a customer who deploys a new version always use the same source code version as the staging version they have tested upon.
*
* @example ?platformReleaseRef=main
* @example ?platformReleaseRef=nrn-v2-mst-aptd-gcms-lcz-sty-c1
* @example ?platformReleaseRef=my-git-branch
* @example ?platformReleaseRef=my-git-tag
* @example ?platformReleaseRef=252b76314184fbeaa236c336c70ea42ca89e0e87
*/
platformReleaseRef?: string;
/**
* Url to redirect to, once the deployment has been triggered.
*
* Will not wait for the actual deployment to be done, will not return whether the trigger was successful either.
*
* XXX We can't wait for the deployment to be performed by Vercel, as it'd definitely be longer than the maximum allowed serverless function running time (10-60sec depending on your Vercel plan).
* Thus, we redirect as early as possible and don't wait for any kind of feedback.
*
* XXX You'll need to implement your own business logic if you want to subscribe to the GitHub Action.
* Implementing a dedicated GitHub Action workflow, which in turn will calls your own API to update the status of each steps might be the way to go.
*
* @default "/"
* @example ?redirectTo=/
* @example ?redirectTo=https://google.com
*/
redirectTo?: string;
/**
* Force option to avoid being redirected.
*
* Meant to be used when debugging, to avoid being redirected all the time, but stay on the page instead.
* XXX Using any non-empty value will enable this option. (prefer using "true")
*
* @example ?forceNoRedirect=true Will not redirect
* @example ?forceNoRedirect=1 Will not redirect
* @example ?forceNoRedirect=false Will not redirect
*/
forceNoRedirect?: string;
};
type EndpointRequest = NextApiRequest & {
query: EndpointRequestQuery;
};
const GITHUB_ACTION_WORKFLOW_FILE_PATH_PRODUCTION = '.github/workflows/deploy-vercel-production.yml';
const GITHUB_ACTION_WORKFLOW_FILE_PATH_STAGING = '.github/workflows/deploy-vercel-staging.yml';
/**
* Starts a new Vercel deployment, for the current customer.
*
* Meant to be used from an external web platform (e.g: CMS, Back Office, etc.)
* to trigger a new production deployment that will replace the currently deployed instance, once deployed.
*
* Endpoint meant to be integrated into 3rd party tools, so it might be used by non-technical people.
* (e.g: customer "editor" role, customer success, customer support, etc.)
*
* XXX Technical staff can use a similar feature by running the GitHub Actions directly through the GitHub UI, and don't necessarily need to use this endpoint.
* (e.g: https://github.com/UnlyEd/next-right-now/actions)
*
* @example http://localhost:8888/api/startVercelDeployment?forceNoRedirect=true&customerAuthToken=customer1 For easier debug, using valid token for customer1
* @example http://localhost:8888/api/startVercelDeployment?forceNoRedirect=true&customerAuthToken=customer2 For easier debug, using valid token for customer2
*
* @param req
* @param res
* @method GET
*/
const startVercelDeployment = async (req: EndpointRequest, res: NextApiResponse): Promise<void> => {
try {
configureReq(req, { fileLabel });
await logEvent(AMPLITUDE_EVENTS.API_INVOKED, null, {
apiEndpoint: AMPLITUDE_API_ENDPOINTS.START_VERCEL_DEPLOYMENT,
});
Sentry.withScope((scope): void => {
scope.setTag('alertType', ALERT_TYPES.VERCEL_DEPLOYMENT_INVOKED);
Sentry.captureEvent({
message: 'API endpoint "startVercelDeployment" invoked.',
level: Sentry.Severity.Log,
});
});
const {
customerAuthToken,
platformReleaseRef = process.env.NEXT_PUBLIC_NRN_PRESET, // XXX Because the NEXT_PUBLIC_NRN_PRESET contains the branch's name, it's suitable as a good default, for NRN. (But, you won't want this default in a private fork)
redirectTo = '/',
}: EndpointRequestQuery = req?.query;
const forceNoRedirect = !!size(req?.query?.forceNoRedirect); // Any non-empty value is considered as true
const statusCode = forceNoRedirect ? 200 : 302; // Using a statusCode of 200 will break the redirection, making it ineffective
// XXX For the sake of simplicity, our "customerAuthToken" is the same as the customer ref.
// This is better than using no token at all, but it's still a rather weak security check.
// Feel free to implement your own authentication protocol.
if (customerAuthToken !== process.env.NEXT_PUBLIC_CUSTOMER_REF) {
const errorMessage = `Query parameter "customerAuthToken" is not valid (using "${customerAuthToken}"). Access refused.`;
Sentry.captureException(new Error(errorMessage));
logger.error(errorMessage);
await flushSafe();
return redirect(res, redirectTo, statusCode);
}
if (!process.env.GITHUB_DISPATCH_TOKEN) {
let errorMessage;
switch (process.env.NEXT_PUBLIC_APP_STAGE) {
case 'development':
errorMessage = `Env variable "GITHUB_DISPATCH_TOKEN" is not defined. Please define it in your ".env.local" file.`;
break;
case 'staging':
case 'production':
errorMessage = `Env variable "GITHUB_DISPATCH_TOKEN" is not defined. Please create a Vercel secret using "vercel secrets add nrn-github-dispatch-token YOUR_TOKEN".`;
break;
}
Sentry.captureException(new Error(errorMessage));
logger.error(errorMessage);
await flushSafe();
return redirect(res, redirectTo, statusCode);
}
if (!platformReleaseRef) {
const errorMessage = `Query parameter "platformReleaseRef" is not defined.`;
Sentry.captureException(new Error(errorMessage));
logger.error(errorMessage);
await flushSafe();
return redirect(res, redirectTo, statusCode);
}
// Dispatch the GitHub Actions workflow, which will then trigger the Vercel deployment
await dispatchWorkflowByPath(platformReleaseRef, process.env.NEXT_PUBLIC_APP_STAGE === 'production' ? GITHUB_ACTION_WORKFLOW_FILE_PATH_PRODUCTION : GITHUB_ACTION_WORKFLOW_FILE_PATH_STAGING);
// Redirect the end-user
redirect(res, redirectTo, statusCode);
} catch (e) {
Sentry.captureException(e);
logger.error(e.message);
await flushSafe();
res.json({
error: true,
message: process.env.NEXT_PUBLIC_APP_STAGE === 'production' ? undefined : e.message,
});
}
};
export default startVercelDeployment;