This commit adds an API endpoint for notifying the preview server about PR updates (`/pr-updated`). According to the update, the preview server can take several actions. Currently, it will only check and (if necessary) update the PR's preview visibility (but more actions could be supported in the future). The API can be used with an automatic trigger (e.g. a GitHub webhook) to instantly update a PR's preview visibility when it changes. Fixes #16526
154 lines
5.5 KiB
TypeScript
154 lines
5.5 KiB
TypeScript
// Imports
|
|
import * as bodyParser from 'body-parser';
|
|
import * as express from 'express';
|
|
import * as http from 'http';
|
|
import {GithubPullRequests} from '../common/github-pull-requests';
|
|
import {assertNotMissingOrEmpty} from '../common/utils';
|
|
import {BuildCreator} from './build-creator';
|
|
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from './build-events';
|
|
import {BUILD_VERIFICATION_STATUS, BuildVerifier} from './build-verifier';
|
|
import {UploadError} from './upload-error';
|
|
|
|
// Constants
|
|
const AUTHORIZATION_HEADER = 'AUTHORIZATION';
|
|
const X_FILE_HEADER = 'X-FILE';
|
|
|
|
// Interfaces - Types
|
|
interface UploadServerConfig {
|
|
buildsDir: string;
|
|
domainName: string;
|
|
githubOrganization: string;
|
|
githubTeamSlugs: string[];
|
|
githubToken: string;
|
|
repoSlug: string;
|
|
secret: string;
|
|
trustedPrLabel: string;
|
|
}
|
|
|
|
// Classes
|
|
class UploadServerFactory {
|
|
// Methods - Public
|
|
public create({
|
|
buildsDir,
|
|
domainName,
|
|
githubOrganization,
|
|
githubTeamSlugs,
|
|
githubToken,
|
|
repoSlug,
|
|
secret,
|
|
trustedPrLabel,
|
|
}: UploadServerConfig): http.Server {
|
|
assertNotMissingOrEmpty('domainName', domainName);
|
|
|
|
const buildVerifier = new BuildVerifier(secret, githubToken, repoSlug, githubOrganization, githubTeamSlugs,
|
|
trustedPrLabel);
|
|
const buildCreator = this.createBuildCreator(buildsDir, githubToken, repoSlug, domainName);
|
|
|
|
const middleware = this.createMiddleware(buildVerifier, buildCreator);
|
|
const httpServer = http.createServer(middleware as any);
|
|
|
|
httpServer.on('listening', () => {
|
|
const info = httpServer.address();
|
|
console.info(`Up and running (and listening on ${info.address}:${info.port})...`);
|
|
});
|
|
|
|
return httpServer;
|
|
}
|
|
|
|
// Methods - Protected
|
|
protected createBuildCreator(buildsDir: string, githubToken: string, repoSlug: string,
|
|
domainName: string): BuildCreator {
|
|
const buildCreator = new BuildCreator(buildsDir);
|
|
const githubPullRequests = new GithubPullRequests(githubToken, repoSlug);
|
|
const postPreviewsComment = (pr: number, shas: string[]) => {
|
|
const body = shas.
|
|
map(sha => `You can preview ${sha} at https://pr${pr}-${sha}.${domainName}/.`).
|
|
join('\n');
|
|
|
|
return githubPullRequests.addComment(pr, body);
|
|
};
|
|
|
|
buildCreator.on(CreatedBuildEvent.type, ({pr, sha, isPublic}: CreatedBuildEvent) => {
|
|
if (isPublic) {
|
|
postPreviewsComment(pr, [sha]);
|
|
}
|
|
});
|
|
|
|
buildCreator.on(ChangedPrVisibilityEvent.type, ({pr, shas, isPublic}: ChangedPrVisibilityEvent) => {
|
|
if (isPublic && shas.length) {
|
|
postPreviewsComment(pr, shas);
|
|
}
|
|
});
|
|
|
|
return buildCreator;
|
|
}
|
|
|
|
protected createMiddleware(buildVerifier: BuildVerifier, buildCreator: BuildCreator): express.Express {
|
|
const middleware = express();
|
|
const jsonParser = bodyParser.json();
|
|
|
|
middleware.get(/^\/create-build\/([1-9][0-9]*)\/([0-9a-f]{40})\/?$/, (req, res) => {
|
|
const pr = req.params[0];
|
|
const sha = req.params[1];
|
|
const archive = req.header(X_FILE_HEADER);
|
|
const authHeader = req.header(AUTHORIZATION_HEADER);
|
|
|
|
if (!authHeader) {
|
|
this.throwRequestError(401, `Missing or empty '${AUTHORIZATION_HEADER}' header`, req);
|
|
} else if (!archive) {
|
|
this.throwRequestError(400, `Missing or empty '${X_FILE_HEADER}' header`, req);
|
|
} else {
|
|
Promise.resolve().
|
|
then(() => buildVerifier.verify(+pr, authHeader)).
|
|
then(verStatus => verStatus === BUILD_VERIFICATION_STATUS.verifiedAndTrusted).
|
|
then(isPublic => buildCreator.create(pr, sha, archive, isPublic).
|
|
then(() => res.sendStatus(isPublic ? 201 : 202))).
|
|
catch(err => this.respondWithError(res, err));
|
|
}
|
|
});
|
|
middleware.get(/^\/health-check\/?$/, (_req, res) => res.sendStatus(200));
|
|
middleware.post(/^\/pr-updated\/?$/, jsonParser, (req, res) => {
|
|
const {action, number: prNo}: {action?: string, number?: number} = req.body;
|
|
const visMayHaveChanged = !action || (action === 'labeled') || (action === 'unlabeled');
|
|
|
|
if (!visMayHaveChanged) {
|
|
res.sendStatus(200);
|
|
} else if (!prNo) {
|
|
this.throwRequestError(400, `Missing or empty 'number' field`, req);
|
|
} else {
|
|
Promise.resolve().
|
|
then(() => buildVerifier.getPrIsTrusted(prNo)).
|
|
then(isPublic => buildCreator.updatePrVisibility(String(prNo), isPublic)).
|
|
then(() => res.sendStatus(200)).
|
|
catch(err => this.respondWithError(res, err));
|
|
}
|
|
});
|
|
middleware.all('*', req => this.throwRequestError(404, 'Unknown resource', req));
|
|
middleware.use((err: any, _req: any, res: express.Response, _next: any) => this.respondWithError(res, err));
|
|
|
|
return middleware;
|
|
}
|
|
|
|
protected respondWithError(res: express.Response, err: any) {
|
|
if (!(err instanceof UploadError)) {
|
|
err = new UploadError(500, String((err && err.message) || err));
|
|
}
|
|
|
|
const statusText = http.STATUS_CODES[err.status] || '???';
|
|
console.error(`Upload error: ${err.status} - ${statusText}`);
|
|
console.error(err.message);
|
|
|
|
res.status(err.status).end(err.message);
|
|
}
|
|
|
|
protected throwRequestError(status: number, error: string, req: express.Request) {
|
|
const message = `${error} in request: ${req.method} ${req.originalUrl}` +
|
|
(!req.body ? '' : ` ${JSON.stringify(req.body)}`);
|
|
|
|
throw new UploadError(status, message);
|
|
}
|
|
}
|
|
|
|
// Exports
|
|
export const uploadServerFactory = new UploadServerFactory();
|