diff --git a/.buildkite/assets/404-report.html b/.buildkite/assets/404-report.html
new file mode 100644
index 0000000000..c0de80fc0b
--- /dev/null
+++ b/.buildkite/assets/404-report.html
@@ -0,0 +1,212 @@
+
+
+
+
+
+ E2E Report - pending
+
+
+
+ 404 Error | Page not found
+ E2E Report still pending...
+
+
+
+
diff --git a/.buildkite/pipelines/pull_request/pipeline.ts b/.buildkite/pipelines/pull_request/pipeline.ts
index c9d21b873f..5cb77e9d59 100644
--- a/.buildkite/pipelines/pull_request/pipeline.ts
+++ b/.buildkite/pipelines/pull_request/pipeline.ts
@@ -22,18 +22,12 @@ import {
prettierStep,
storybookStep,
typeCheckStep,
+ firebasePreDeployStep,
} from '../../steps';
-import {
- bkEnv,
- ChangeContext,
- createDeployment,
- uploadPipeline,
- createDeploymentStatus,
- Step,
- CustomCommandStep,
-} from '../../utils';
+import { bkEnv, ChangeContext, uploadPipeline, Step, CustomCommandStep } from '../../utils';
import { getBuildConfig } from '../../utils/build';
import { MetaDataKeys } from '../../utils/constants';
+import { createDeployment, createDeploymentStatus, createOrUpdateDeploymentComment } from '../../utils/deployment';
void (async () => {
try {
@@ -61,6 +55,7 @@ void (async () => {
typeCheckStep(),
storybookStep(),
e2eServerStep(),
+ firebasePreDeployStep(),
ghpDeployStep(),
playwrightStep(),
firebaseDeployStep(),
@@ -93,8 +88,12 @@ void (async () => {
const skipDeployStep = (steps.find(({ key }) => key === 'deploy_fb') as CustomCommandStep)?.skip ?? false;
await setMetadata(MetaDataKeys.skipDeployment, skipDeployStep ? 'true' : 'false');
if (!skipDeployStep) {
- await createDeployment();
- await createDeploymentStatus({ state: 'queued' });
+ if (bkEnv.isPullRequest) {
+ await createOrUpdateDeploymentComment({ state: 'pending' });
+ } else {
+ await createDeployment();
+ await createDeploymentStatus({ state: 'queued' });
+ }
}
pipeline.steps = steps;
diff --git a/.buildkite/scripts/steps/e2e_server.ts b/.buildkite/scripts/steps/e2e_server.ts
index 01201a5d61..4c958d68fe 100644
--- a/.buildkite/scripts/steps/e2e_server.ts
+++ b/.buildkite/scripts/steps/e2e_server.ts
@@ -6,12 +6,19 @@
* Side Public License, v 1.
*/
-import { createDeploymentStatus, exec, yarnInstall, compress, startGroup } from '../../utils';
+import { exec, yarnInstall, compress, startGroup, bkEnv } from '../../utils';
+import { createDeploymentStatus, createOrUpdateDeploymentComment } from '../../utils/deployment';
void (async () => {
await yarnInstall();
- await createDeploymentStatus({ state: 'pending' });
+ if (bkEnv.isPullRequest) {
+ await createOrUpdateDeploymentComment({
+ state: 'pending',
+ });
+ } else {
+ await createDeploymentStatus({ state: 'pending' });
+ }
startGroup('Generating e2e server files');
await exec('yarn test:e2e:generate');
diff --git a/.buildkite/scripts/steps/firebase_deploy.ts b/.buildkite/scripts/steps/firebase_deploy.ts
index 565cc8ae30..a815951dc5 100644
--- a/.buildkite/scripts/steps/firebase_deploy.ts
+++ b/.buildkite/scripts/steps/firebase_deploy.ts
@@ -9,10 +9,13 @@
import fs from 'fs';
import path from 'path';
-import { createDeploymentStatus, firebaseDeploy, downloadArtifacts, startGroup, decompress } from '../../utils';
+import { firebaseDeploy, downloadArtifacts, startGroup, decompress, bkEnv } from '../../utils';
+import { createDeploymentStatus } from '../../utils/deployment';
void (async () => {
- await createDeploymentStatus({ state: 'in_progress' });
+ if (!bkEnv.isPullRequest) {
+ await createDeploymentStatus({ state: 'in_progress' });
+ }
const outDir = 'e2e_server/public';
@@ -38,6 +41,7 @@ void (async () => {
});
startGroup('Check deployment files');
+
const hasStorybookIndex = fs.existsSync('./e2e_server/public/index.html');
const hasE2EIndex = fs.existsSync('./e2e_server/public/e2e/index.html');
const hasE2EReportIndex = fs.existsSync('./e2e_server/public/e2e-report/index.html');
diff --git a/.buildkite/scripts/steps/firebase_pre_deploy.ts b/.buildkite/scripts/steps/firebase_pre_deploy.ts
new file mode 100644
index 0000000000..269fe6298b
--- /dev/null
+++ b/.buildkite/scripts/steps/firebase_pre_deploy.ts
@@ -0,0 +1,63 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import fs from 'fs';
+import path from 'path';
+
+import { firebaseDeploy, downloadArtifacts, startGroup, decompress, bkEnv } from '../../utils';
+import { createDeploymentStatus, createOrUpdateDeploymentComment } from '../../utils/deployment';
+
+void (async () => {
+ if (bkEnv.isPullRequest) {
+ await createOrUpdateDeploymentComment({
+ state: 'pending',
+ preDeploy: true,
+ });
+ } else {
+ await createDeploymentStatus({ state: 'in_progress' });
+ }
+
+ const outDir = 'e2e_server/public';
+
+ const storybookSrc = '.buildkite/artifacts/storybook.gz';
+ await downloadArtifacts(storybookSrc, 'build_storybook');
+ await decompress({
+ src: storybookSrc,
+ dest: outDir,
+ });
+
+ const e2eSrc = '.buildkite/artifacts/e2e_server.gz';
+ await downloadArtifacts(e2eSrc, 'build_e2e');
+ await decompress({
+ src: e2eSrc,
+ dest: path.join(outDir, 'e2e'),
+ });
+
+ startGroup('Check deployment files');
+
+ const hasStorybookIndex = fs.existsSync('./e2e_server/public/index.html');
+ const hasE2EIndex = fs.existsSync('./e2e_server/public/e2e/index.html');
+ const missingFiles = [
+ ['storybook', hasStorybookIndex],
+ ['e2e server', hasE2EIndex],
+ ]
+ .filter(([, exists]) => !exists)
+ .map(([f]) => f as string);
+
+ if (missingFiles.length > 0) {
+ throw new Error(`Error: Missing deployment files: [${missingFiles.join(', ')}]`);
+ }
+
+ // Move 404 file to /e2e-report
+ fs.mkdirSync('./e2e_server/public/e2e-report');
+ fs.renameSync('./.buildkite/assets/404-report.html', './e2e_server/public/e2e-report/index.html');
+
+ await firebaseDeploy({
+ preDeploy: true,
+ });
+})();
diff --git a/.buildkite/scripts/steps/storybook.ts b/.buildkite/scripts/steps/storybook.ts
index 6c8fffc1b2..86350b9fb2 100644
--- a/.buildkite/scripts/steps/storybook.ts
+++ b/.buildkite/scripts/steps/storybook.ts
@@ -6,12 +6,19 @@
* Side Public License, v 1.
*/
-import { bkEnv, compress, createDeploymentStatus, exec, startGroup, yarnInstall } from '../../utils';
+import { bkEnv, compress, exec, startGroup, yarnInstall } from '../../utils';
+import { createDeploymentStatus, createOrUpdateDeploymentComment } from '../../utils/deployment';
void (async () => {
await yarnInstall();
- await createDeploymentStatus({ state: 'pending' });
+ if (bkEnv.isPullRequest) {
+ await createOrUpdateDeploymentComment({
+ state: 'pending',
+ });
+ } else {
+ await createDeploymentStatus({ state: 'pending' });
+ }
startGroup('Building storybook');
await exec('yarn build', {
diff --git a/.buildkite/steps/firebase_pre_deploy.ts b/.buildkite/steps/firebase_pre_deploy.ts
new file mode 100644
index 0000000000..2bb4552ca4
--- /dev/null
+++ b/.buildkite/steps/firebase_pre_deploy.ts
@@ -0,0 +1,23 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { createStep, CustomCommandStep, commandStepDefaults } from '../utils';
+
+export const firebasePreDeployStep = createStep(() => {
+ return {
+ ...commandStepDefaults,
+ label: ':firebase: Pre Deploy - firebase',
+ key: 'pre_deploy_fb',
+ allow_dependency_failure: false,
+ depends_on: ['build_storybook', 'build_e2e'],
+ commands: ['npx ts-node .buildkite/scripts/steps/firebase_pre_deploy.ts'],
+ env: {
+ ECH_CHECK_ID: 'pre_deploy_fb',
+ },
+ };
+});
diff --git a/.buildkite/steps/index.ts b/.buildkite/steps/index.ts
index 3589b2d4cc..9b1d10fb18 100644
--- a/.buildkite/steps/index.ts
+++ b/.buildkite/steps/index.ts
@@ -14,5 +14,6 @@ export * from './prettier';
export * from './playwright';
export * from './storybook';
export * from './e2e_server';
+export * from './firebase_pre_deploy';
export * from './firebase_deploy';
export * from './ghp_deploy';
diff --git a/.buildkite/utils/build.ts b/.buildkite/utils/build.ts
index ccc1a31257..53b24afceb 100644
--- a/.buildkite/utils/build.ts
+++ b/.buildkite/utils/build.ts
@@ -31,6 +31,7 @@ export const getBuildConfig = (): BuildConfig => {
{ name: 'API', id: 'api' },
{ name: 'Build - e2e server', id: 'build_e2e' },
{ name: 'Build - Storybook', id: 'build_storybook' },
+ { name: 'Pre Deploy - firebase', id: 'pre_deploy_fb' },
{ name: 'Eslint', id: 'eslint' },
{ name: 'Prettier', id: 'prettier' },
{ name: 'Deploy - firebase', id: 'deploy_fb' },
diff --git a/.buildkite/utils/buildkite.ts b/.buildkite/utils/buildkite.ts
index 0bfa28332a..189e790cd6 100644
--- a/.buildkite/utils/buildkite.ts
+++ b/.buildkite/utils/buildkite.ts
@@ -158,13 +158,68 @@ interface JobStep {
key: string;
}
+interface PreviousDeployCommitShaResponse {
+ data: {
+ pipeline: {
+ builds: {
+ edges: [
+ {
+ node: {
+ commit: string;
+ jobs: {
+ edges: [
+ {
+ node: {
+ passed: boolean;
+ };
+ },
+ ];
+ };
+ };
+ },
+ ];
+ };
+ };
+ };
+}
+
+/**
+ * Returns previously successful firebase deploy commit sha for the current branch
+ */
+export async function getPreviousDeployCommitSha(): Promise {
+ const { data } = await buildkiteGQLQuery(`query getPreviousDeployCommitSha {
+ pipeline(slug: "${bkEnv.organizationSlug}/${bkEnv.pipelineSlug}") {
+ builds(first: 1, branch: "${bkEnv.branch}", state: PASSED) {
+ edges {
+ node {
+ commit
+ jobs(first: 1, step: { key: "deploy_fb" }, state: FINISHED) {
+ edges {
+ node {
+ ... on JobTypeCommand {
+ passed
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}`);
+
+ const { commit, jobs } = data?.pipeline?.builds?.edges?.[0]?.node ?? {};
+
+ return jobs?.edges?.[0]?.node?.passed ? commit : null;
+}
+
export async function getBuildJobs(stepKey?: string): Promise {
const buildId = bkEnv.buildId;
if (!buildId) throw new Error(`Error: no job id found [${buildId}]`);
const jobQuery = stepKey ? `first: 100, step: { key: "${stepKey}" }` : 'first: 100';
- const { data } = await buildkiteGQLQuery(`query getBuildJobs {
+ const { data } = await buildkiteGQLQuery(`query getBuildJobs {
build(uuid: "${buildId}") {
jobs(${jobQuery}) {
edges {
@@ -194,7 +249,7 @@ export async function getBuildJobs(stepKey?: string): Promise {
);
}
-interface BuildJobsReponse {
+interface BuildJobsResponse {
data: {
build: {
jobs: {
diff --git a/.buildkite/utils/constants.ts b/.buildkite/utils/constants.ts
index 436576f578..032a356988 100644
--- a/.buildkite/utils/constants.ts
+++ b/.buildkite/utils/constants.ts
@@ -12,6 +12,9 @@
export const MetaDataKeys = {
skipDeployment: 'skipDeployment',
deploymentId: 'deploymentId',
+ deploymentCommentId: 'deploymentCommentId',
+ deploymentPreviousSha: 'deploymentPreviousSha',
+ deploymentStatus: 'deploymentStatus',
deploymentUrl: 'deploymentUrl',
};
diff --git a/.buildkite/utils/deployment.ts b/.buildkite/utils/deployment.ts
new file mode 100644
index 0000000000..46e242c74f
--- /dev/null
+++ b/.buildkite/utils/deployment.ts
@@ -0,0 +1,195 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { RestEndpointMethodTypes } from '@octokit/rest';
+import { getMetadata, setMetadata, metadataExists } from 'buildkite-agent-node';
+import { Optional } from 'utility-types';
+
+import { bkEnv, getPreviousDeployCommitSha } from './buildkite';
+import { getNumber } from './common';
+import { MetaDataKeys } from './constants';
+import { getDeploymentUrl } from './firebase';
+import { octokit, defaultGHOptions, getComment, commentByKey } from './github';
+import { OctokitParameters } from './types';
+
+export interface UpdateDeploymentCommentOptions {
+ sha?: string;
+ state: 'pending' | 'success' | 'failure';
+ deploymentUrl?: string;
+ previousSha?: string;
+ errorCmd?: string;
+ errorMsg?: string;
+ jobLink?: string;
+ preDeploy?: boolean;
+}
+
+export async function createOrUpdateDeploymentComment(options: UpdateDeploymentCommentOptions) {
+ const { state, preDeploy, sha = bkEnv.commit! } = options;
+ const skipDeployment = await getMetadata(MetaDataKeys.skipDeployment);
+ console.log('MetaDataKeys.skipDeployment', skipDeployment);
+ const deploymentStatus = await getMetadata(MetaDataKeys.deploymentStatus);
+ const status = preDeploy ? `${state}-preDeploy` : state;
+
+ if (process.env.BLOCK_REQUESTS || !bkEnv.isPullRequest || skipDeployment === 'true' || deploymentStatus === status)
+ return;
+
+ await setMetadata(MetaDataKeys.deploymentStatus, status);
+
+ const previousCommentId = await getMetadata(MetaDataKeys.deploymentCommentId);
+
+ if (state === 'pending' && !previousCommentId) {
+ // initial cleanup - delete previous comments
+ const { data: botComments } = await octokit.issues.listComments({
+ ...defaultGHOptions,
+ issue_number: bkEnv.pullRequestNumber!,
+ });
+ const deployComments = botComments.filter(commentByKey('deployment'));
+ await Promise.all(
+ deployComments.map(async ({ id }) => {
+ await octokit.issues.deleteComment({
+ ...defaultGHOptions,
+ comment_id: id,
+ });
+ }),
+ );
+ const previousSha = await getPreviousDeployCommitSha();
+ if (previousSha) {
+ await setMetadata(MetaDataKeys.deploymentPreviousSha, previousSha);
+ }
+ }
+
+ if (previousCommentId) {
+ await octokit.issues.deleteComment({
+ ...defaultGHOptions,
+ comment_id: Number(previousCommentId),
+ });
+ }
+
+ const deploymentUrl = options.deploymentUrl ?? (await getDeploymentUrl());
+ const previousSha = options.previousSha ?? (await getMetadata(MetaDataKeys.deploymentPreviousSha));
+ const commentBody = getComment('deployment', { ...options, state, preDeploy, sha, deploymentUrl, previousSha });
+
+ const {
+ data: { id },
+ } = await octokit.issues.createComment({
+ ...defaultGHOptions,
+ issue_number: bkEnv.pullRequestNumber!,
+ body: commentBody,
+ });
+
+ await setMetadata(MetaDataKeys.deploymentCommentId, id.toString());
+}
+
+/**
+ * Must clear previous deployments when deployment `transient_environment` is t`rue`
+ */
+export async function updatePreviousDeployments(
+ state: RestEndpointMethodTypes['repos']['createDeploymentStatus']['parameters']['state'] = 'inactive',
+) {
+ const currentDeploymentId = getNumber(await getMetadata(MetaDataKeys.deploymentId));
+ const { data: deployments } = await octokit.repos.listDeployments({
+ ...defaultGHOptions,
+ task: getDeploymentTask(),
+ per_page: 100, // should never get this high
+ });
+
+ await Promise.all(
+ deployments.map(async ({ id }) => {
+ if (id === currentDeploymentId) return;
+ const {
+ data: [{ state: currentState, log_url, environment_url }],
+ } = await octokit.repos.listDeploymentStatuses({
+ ...defaultGHOptions,
+ deployment_id: id,
+ per_page: 1,
+ });
+
+ if (!['in_progress', 'queued', 'pending', 'success'].includes(currentState)) {
+ return;
+ }
+
+ console.log(`Updating deployment ${id} state: ${currentState} -> ${state}`);
+
+ await octokit.repos.createDeploymentStatus({
+ ...defaultGHOptions,
+ deployment_id: id,
+ description: 'This deployment precedes a newer deployment',
+ log_url,
+ environment_url,
+ state,
+ });
+ }),
+ );
+}
+
+export const createDeploymentStatus = async (
+ options: Optional> = {},
+) => {
+ if (process.env.BLOCK_REQUESTS) return;
+
+ console.trace('createDeploymentStatus', options.state);
+
+ console.log('MetaDataKeys.skipDeployment', await getMetadata(MetaDataKeys.skipDeployment));
+
+ if ((await getMetadata(MetaDataKeys.skipDeployment)) === 'true') return;
+
+ const deployment_id = options.deployment_id ?? getNumber(await getMetadata(MetaDataKeys.deploymentId));
+
+ if (deployment_id === null) throw new Error(`Failed to set status, no deployment found`);
+
+ try {
+ await octokit.rest.repos.createDeploymentStatus({
+ ...defaultGHOptions,
+ state: 'success',
+ log_url: bkEnv.jobUrl,
+ ...options,
+ deployment_id,
+ });
+ } catch {
+ // Should not throw as this would be an issue with CI
+ console.error(`Failed to create deployment status for deployment [${deployment_id}]`);
+ }
+};
+
+function getDeploymentTask() {
+ return bkEnv.isPullRequest ? `deploy:pr:${bkEnv.pullRequestNumber}` : `deploy:${bkEnv.branch}`;
+}
+
+export const createDeployment = async (
+ options: Optional> = {},
+): Promise => {
+ if (process.env.BLOCK_REQUESTS) return null;
+ if (await metadataExists(MetaDataKeys.deploymentId)) {
+ console.warn(`Deployment already exists for build`);
+ }
+
+ const ref = bkEnv.commit;
+
+ if (!ref) throw new Error(`Failed to set status, no ref available`);
+
+ try {
+ const response = await octokit.repos.createDeployment({
+ ...defaultGHOptions,
+ environment: bkEnv.isPullRequest ? 'pull-requests' : bkEnv.branch,
+ transient_environment: bkEnv.isPullRequest, // sets previous statuses to inactive
+ production_environment: false,
+ task: getDeploymentTask(),
+ ...options,
+ auto_merge: false, // use branch as is without merging with base
+ required_contexts: [],
+ ref,
+ });
+ if (response.status === 202) return null; // Merged branch response
+
+ await setMetadata(MetaDataKeys.deploymentId, String(response.data.id));
+ return response.data.id;
+ } catch (error) {
+ console.error(`Failed to create deployment for ref [${ref}]`);
+ throw error;
+ }
+};
diff --git a/.buildkite/utils/exec.ts b/.buildkite/utils/exec.ts
index 120c7fd154..4a4b9072a3 100644
--- a/.buildkite/utils/exec.ts
+++ b/.buildkite/utils/exec.ts
@@ -17,7 +17,7 @@ interface ExecOptions extends ExecSyncOptionsWithBufferEncoding {
args?: string;
input?: string;
failureMsg?: string;
- onFailure?: () => Promise | void;
+ onFailure?: (err: { command: string; message: string }) => Promise | void;
onSuccess?: () => Promise | void;
cwd?: string;
/**
@@ -32,6 +32,7 @@ interface ExecOptions extends ExecSyncOptionsWithBufferEncoding {
* Logic to run before next retry
*/
onRetry?: () => void | Promise;
+ allowFailure?: boolean;
}
export const wait = (n: number) => new Promise((resolve) => setTimeout(resolve, n));
@@ -54,6 +55,7 @@ export const exec = async (
retry = 0,
retryWait = 0,
onRetry,
+ allowFailure = false,
}: ExecOptions = {},
): Promise => {
let retryCount = 0;
@@ -81,9 +83,16 @@ export const exec = async (
if (retryWait) await wait(retryWait * 1000);
return await execInner();
}
+
+ if (allowFailure) {
+ throw error; // still need to catch
+ }
+
+ const errorMsg = getErrorMsg(error);
console.error(`❌ Failed to run command: [${command}]`);
+
await setJobMetadata('failed', 'true');
- await onFailure?.();
+ await onFailure?.({ command, message: errorMsg });
await updateCheckStatus(
{
status: 'completed',
@@ -104,3 +113,15 @@ export const yarnInstall = async (cwd?: string, ignoreScripts = true) => {
const scriptFlag = ignoreScripts ? ' --ignore-scripts' : '';
await exec(`yarn install --frozen-lockfile${scriptFlag}`, { cwd, retry: 5, retryWait: 15 });
};
+
+function getErrorMsg(error: unknown): string {
+ const output: Array = (error as any)?.output ?? [];
+ if (output?.length > 0) {
+ return output
+ .filter(Boolean)
+ .map((s) => s?.trim())
+ .join('\n\n');
+ }
+
+ return error instanceof Error ? error.message : typeof error === 'string' ? error : '';
+}
diff --git a/.buildkite/utils/firebase.ts b/.buildkite/utils/firebase.ts
index 025b3a2097..7c32d59cab 100644
--- a/.buildkite/utils/firebase.ts
+++ b/.buildkite/utils/firebase.ts
@@ -13,39 +13,33 @@ import { fileSync } from 'tmp';
import { bkEnv, startGroup } from './buildkite';
import { DEFAULT_FIREBASE_URL, MetaDataKeys } from './constants';
+import { createDeploymentStatus, createOrUpdateDeploymentComment } from './deployment';
import { exec } from './exec';
-import {
- octokit,
- commentByKey,
- createDeploymentStatus,
- updatePreviousDeployments,
- defaultGHOptions,
- getComment,
-} from './github';
// Set up Google Application Credentials for use by the Firebase CLI
// https://cloud.google.com/docs/authentication/production#finding_credentials_automatically
function createGACFile() {
- const tmpFile = fileSync({ postfix: '.json' });
const gac = process.env.FIREBASE_AUTH;
if (!gac) throw new Error('Error: unable to find FIREBASE_AUTH');
+ const tmpFile = fileSync({ postfix: '.json' });
writeSync(tmpFile.fd, gac);
-
return tmpFile.name;
}
interface DeployOptions {
expires?: string;
- redeploy?: boolean;
+ preDeploy?: boolean;
}
+const getChannelId = () =>
+ bkEnv.isPullRequest ? `pr-${bkEnv.pullRequestNumber!}` : bkEnv.isMainBranch ? null : bkEnv.branch;
+
export const firebaseDeploy = async (opt: DeployOptions = {}) => {
- const expires = opt.expires ?? '7d';
+ const expires = opt.expires ?? '30d';
startGroup('Deploying to firebase');
- const channelId = bkEnv.isPullRequest ? `pr-${bkEnv.pullRequestNumber!}` : bkEnv.isMainBranch ? null : bkEnv.branch;
-
+ const channelId = getChannelId();
const gacFile = createGACFile();
const command = channelId
? `npx firebase-tools hosting:channel:deploy ${channelId} --expires ${expires} --no-authorized-domains --json`
@@ -57,10 +51,19 @@ export const firebaseDeploy = async (opt: DeployOptions = {}) => {
...process.env,
GOOGLE_APPLICATION_CREDENTIALS: gacFile,
},
- async onFailure() {
- await createDeploymentStatus({
- state: 'failure',
- });
+ async onFailure(err) {
+ if (bkEnv.isPullRequest) {
+ await createOrUpdateDeploymentComment({
+ state: 'failure',
+ jobLink: bkEnv.jobUrl,
+ errorCmd: err.command,
+ errorMsg: err.message,
+ });
+ } else {
+ await createDeploymentStatus({
+ state: 'failure',
+ });
+ }
},
});
@@ -74,38 +77,53 @@ export const firebaseDeploy = async (opt: DeployOptions = {}) => {
await setMetadata(MetaDataKeys.deploymentUrl, deploymentUrl);
if (bkEnv.isPullRequest) {
- // deactivate old deployments
- await updatePreviousDeployments();
-
- const { data: botComments } = await octokit.issues.listComments({
- ...defaultGHOptions,
- issue_number: bkEnv.pullRequestNumber!,
+ await createOrUpdateDeploymentComment({
+ state: 'success',
+ preDeploy: opt.preDeploy,
+ deploymentUrl,
});
- const deployComments = botComments.filter((c) => commentByKey('deployments')(c.body));
- await Promise.all(
- deployComments.map(async ({ id }) => {
- await octokit.issues.deleteComment({
- ...defaultGHOptions,
- comment_id: id,
- });
- }),
- );
- await octokit.issues.createComment({
- ...defaultGHOptions,
- issue_number: bkEnv.pullRequestNumber!,
- body: getComment('deployments', deploymentUrl, bkEnv.commit!),
+ } else {
+ await createDeploymentStatus({
+ state: 'success',
+ environment_url: deploymentUrl,
});
}
- await createDeploymentStatus({
- state: 'success',
- environment_url: deploymentUrl,
- });
return deploymentUrl;
} else {
throw new Error(`Error: Firebase deployment resulted in ${status}`);
}
};
+interface ChannelDeploymentInfo {
+ status: string;
+ result?: {
+ url?: string;
+ };
+}
+
+/**
+ * Returns deployment id for given PR channel
+ */
+export async function getDeploymentUrl() {
+ try {
+ const channelId = getChannelId();
+ const gacFile = createGACFile();
+ const deploymentJson = await exec(`npx firebase-tools hosting:channel:open ${channelId} --json`, {
+ cwd: './e2e_server',
+ stdio: 'pipe',
+ allowFailure: true,
+ env: {
+ ...process.env,
+ GOOGLE_APPLICATION_CREDENTIALS: gacFile,
+ },
+ });
+ const info = JSON.parse(deploymentJson) as ChannelDeploymentInfo;
+ return info?.result?.url || undefined;
+ } catch {
+ return undefined;
+ }
+}
+
interface DeployResult {
status: string;
result:
diff --git a/.buildkite/utils/github.ts b/.buildkite/utils/github.ts
index 8c21073d89..2b6ef6fad9 100644
--- a/.buildkite/utils/github.ts
+++ b/.buildkite/utils/github.ts
@@ -9,18 +9,16 @@
import { createAppAuth, StrategyOptions } from '@octokit/auth-app';
import { components } from '@octokit/openapi-types';
import { retry } from '@octokit/plugin-retry';
-import { Octokit, RestEndpointMethodTypes } from '@octokit/rest';
-import { getMetadata, metadataExists, setMetadata } from 'buildkite-agent-node';
+import { Octokit } from '@octokit/rest';
+import { getMetadata } from 'buildkite-agent-node';
import ghpages from 'gh-pages';
import minimatch, { IOptions as MinimatchOptions } from 'minimatch';
import { Optional } from 'utility-types';
import { getJobCheckName } from './build';
import { bkEnv } from './buildkite';
-import { getNumber } from './common';
-import { MetaDataKeys } from './constants';
+import { UpdateDeploymentCommentOptions } from './deployment';
import { CheckStatusOptions, CreateCheckOptions } from './octokit';
-import { OctokitParameters } from './types';
if (!process.env.GITHUB_AUTH) throw new Error('GITHUB_AUTH env variable must be defined');
@@ -274,72 +272,6 @@ export const updateCheckStatus = async (
}
};
-export const getDeploymentTask = () =>
- bkEnv.isPullRequest ? `deploy:pr:${bkEnv.pullRequestNumber}` : `deploy:${bkEnv.branch}`;
-
-export const createDeployment = async (
- options: Optional> = {},
-): Promise => {
- if (process.env.BLOCK_REQUESTS) return null;
- if (await metadataExists(MetaDataKeys.deploymentId)) {
- console.warn(`Deployment already exists for build`);
- }
-
- const ref = bkEnv.commit;
-
- if (!ref) throw new Error(`Failed to set status, no ref available`);
-
- try {
- const response = await octokit.repos.createDeployment({
- ...defaultGHOptions,
- environment: bkEnv.isPullRequest ? 'pull-requests' : bkEnv.branch,
- transient_environment: bkEnv.isPullRequest, // sets previous statuses to inactive
- production_environment: false,
- task: getDeploymentTask(),
- ...options,
- auto_merge: false, // use branch as is without merging with base
- required_contexts: [],
- ref,
- });
- if (response.status === 202) return null; // Merged branch response
-
- await setMetadata(MetaDataKeys.deploymentId, String(response.data.id));
- return response.data.id;
- } catch (error) {
- console.error(`Failed to create deployment for ref [${ref}]`);
- throw error;
- }
-};
-
-export const createDeploymentStatus = async (
- options: Optional> = {},
-) => {
- if (process.env.BLOCK_REQUESTS) return;
-
- console.trace('createDeploymentStatus', options.state);
-
- console.log('MetaDataKeys.skipDeployment', await getMetadata(MetaDataKeys.skipDeployment));
-
- if ((await getMetadata(MetaDataKeys.skipDeployment)) === 'true') return;
-
- const deployment_id = options.deployment_id ?? getNumber(await getMetadata(MetaDataKeys.deploymentId));
-
- if (deployment_id === null) throw new Error(`Failed to set status, no deployment found`);
-
- try {
- await octokit.rest.repos.createDeploymentStatus({
- ...defaultGHOptions,
- state: 'success',
- log_url: bkEnv.jobUrl,
- ...options,
- deployment_id,
- });
- } catch {
- // Should not throw as this would be an issue with CI
- console.error(`Failed to create deployment status for deployment [${deployment_id}]`);
- }
-};
-
interface GLQPullRequestFiles {
repository: {
pullRequest: {
@@ -359,45 +291,6 @@ interface GLQPullRequestFiles {
};
}
-export async function updatePreviousDeployments(
- state: RestEndpointMethodTypes['repos']['createDeploymentStatus']['parameters']['state'] = 'inactive',
-) {
- const currentDeploymentId = getNumber(await getMetadata(MetaDataKeys.deploymentId));
- const { data: deployments } = await octokit.repos.listDeployments({
- ...defaultGHOptions,
- task: getDeploymentTask(),
- per_page: 100, // should never get this high
- });
-
- await Promise.all(
- deployments.map(async ({ id }) => {
- if (id === currentDeploymentId) return;
- const {
- data: [{ state: currentState, log_url, environment_url }],
- } = await octokit.repos.listDeploymentStatuses({
- ...defaultGHOptions,
- deployment_id: id,
- per_page: 1,
- });
-
- if (!['in_progress', 'queued', 'pending', 'success'].includes(currentState)) {
- return;
- }
-
- console.log(`Updating deployment ${id} state: ${currentState} -> ${state}`);
-
- await octokit.repos.createDeploymentStatus({
- ...defaultGHOptions,
- deployment_id: id,
- description: 'This deployment precedes a newer deployment',
- log_url,
- environment_url,
- state,
- });
- }),
- );
-}
-
export async function getFileDiffs(): Promise {
if (!bkEnv.isPullRequest) return [];
@@ -476,7 +369,8 @@ function generateMsg(key: string, body: string): string {
const reMsgKey = /^/;
export function commentByKey(key: T) {
- return (comment?: string): boolean => {
+ return (commentRaw?: string | { body?: string }): boolean => {
+ const comment = typeof commentRaw === 'string' ? commentRaw : commentRaw?.body;
if (!comment) return false;
const [, commentKey] = reMsgKey.exec(comment) ?? [];
return commentKey === key;
@@ -487,12 +381,60 @@ export const comments = {
communityPR() {
return `Community pull request, @elastic/datavis please add the \`ci:approved ✅\` label to allow this and future builds.`;
},
- deployments(deploymentUrl: string, sha: string) {
- return `## Deployments - ${sha}
+ deployment({
+ deploymentUrl,
+ sha,
+ previousSha,
+ state,
+ errorCmd,
+ errorMsg,
+ jobLink,
+ preDeploy = false,
+ }: UpdateDeploymentCommentOptions) {
+ console.log(`DEPLOYMENT STATUS - ${state} - preDeploy: ${preDeploy}`);
+
+ if (state === 'failure') {
+ const errorCmdMsg = errorCmd
+ ? `\n\n**Command failed:**
+\`\`\`
+${errorCmd}
+\`\`\`\n\n`
+ : '\n';
+ const err = errorMsg
+ ? `${errorCmdMsg}
+**Error:**
+
+\`\`\`
+${errorMsg}
+\`\`\``
+ : errorCmdMsg;
+ const finalMessage = `## ❌ Failed Deployment - ${sha}
+Failure${jobLink ? ` - [failed job](${jobLink})` : ''}${err}
+`;
+ return `${finalMessage.trim()}\n\ncc: @nickofthyme`;
+ }
+
+ if (state === 'pending') {
+ const updateComment = previousSha ? `\n> 🚧 Updating deployment from ${previousSha}` : '';
+ const deploymentMsg =
+ previousSha && deploymentUrl
+ ? `### Old deployment - ${previousSha}
+- [Storybook](${deploymentUrl})
+- [e2e server](${deploymentUrl}/e2e)
+- ([Playwright report](${deploymentUrl}/e2e-report)`
+ : `- ⏳ Storybook
+- ⏳ e2e server
+- ⏳ Playwright report`;
+ return `## ⏳ Pending Deployment - ${sha}${updateComment}
+
+${deploymentMsg}`;
+ }
+
+ return `## ✅ Successful ${preDeploy ? 'Preliminary ' : ''}Deployment - ${sha}
-- Storybook ([link](${deploymentUrl}))
-- e2e server ([link](${deploymentUrl}/e2e))
-- Playwright report ([link](${deploymentUrl}/e2e-report))`;
+- [Storybook](${deploymentUrl})
+- [e2e server](${deploymentUrl}/e2e)
+${preDeploy ? '- ⏳ Playwright report - Running e2e tests' : `- [Playwright report](${deploymentUrl}/e2e-report)`}`;
},
};
diff --git a/.eslintignore b/.eslintignore
index 6f4dc702cf..854bc5195a 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -8,6 +8,7 @@ wiki
**/node_modules
coverage
license_header.js
+*.html
# Editor configs
.vscode
diff --git a/github_bot/.firebaserc b/github_bot/.firebaserc
new file mode 100644
index 0000000000..33aaa80e7c
--- /dev/null
+++ b/github_bot/.firebaserc
@@ -0,0 +1,5 @@
+{
+ "projects": {
+ "default": "ech-e2e-ci"
+ }
+}
diff --git a/github_bot/.nvmrc b/github_bot/.nvmrc
index b6a7d89c68..6d80269a4f 100644
--- a/github_bot/.nvmrc
+++ b/github_bot/.nvmrc
@@ -1 +1 @@
-16
+18.16.0
diff --git a/github_bot/Dockerfile b/github_bot/Dockerfile
index b09a3837b7..cfba4b6735 100644
--- a/github_bot/Dockerfile
+++ b/github_bot/Dockerfile
@@ -1,5 +1,6 @@
FROM node:16-alpine as builder
WORKDIR /app
+COPY firebase.json .firebaserc ./
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
COPY tsconfig.main.json ./tsconfig.json
@@ -9,6 +10,7 @@ RUN yarn build
FROM node:16-alpine
ENV NODE_ENV=production
WORKDIR /app
+COPY firebase.json .firebaserc ./
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
COPY --from=builder /app/build ./build
diff --git a/github_bot/firebase.json b/github_bot/firebase.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/github_bot/firebase.json
@@ -0,0 +1 @@
+{}
diff --git a/github_bot/src/build.ts b/github_bot/src/build.ts
index 322705194c..e9b3a1c57b 100644
--- a/github_bot/src/build.ts
+++ b/github_bot/src/build.ts
@@ -28,6 +28,7 @@ export const getBuildConfig = (isMainBranch: boolean): BuildConfig => ({
{ name: 'API', id: 'api' },
{ name: 'Build - e2e server', id: 'build_e2e' },
{ name: 'Build - Storybook', id: 'build_storybook' },
+ { name: 'Pre Deploy - firebase', id: 'pre_deploy_fb' },
{ name: 'Eslint', id: 'eslint' },
{ name: 'Prettier', id: 'prettier' },
{ name: 'Deploy - firebase', id: 'deploy_fb' },
diff --git a/github_bot/src/buildkite/finished_build.ts b/github_bot/src/buildkite/finished_build.ts
index 0d73bfd069..602a729df6 100644
--- a/github_bot/src/buildkite/finished_build.ts
+++ b/github_bot/src/buildkite/finished_build.ts
@@ -26,8 +26,6 @@ interface MetaData {
* This function runs to cleanup the commit statuses for all build statuses.
*/
export async function handleFinishedBuild(body: BuildkiteWebhookPayload, res: Response) {
- console.log(body);
-
const build = body?.build;
if (!build || !build?.commit) {
res.sendStatus(400); // need sha to set statuses
@@ -52,8 +50,6 @@ export async function handleFinishedBuild(body: BuildkiteWebhookPayload({
- branch: `${head.repo?.owner.login}:${head.ref}`,
+ context: ` - ${ctx.name} | ${ctx.payload.action}`,
+ branch: `${head.repo!.owner.login}:${head.ref}`,
commit: head.sha,
message: commit.commit.message,
ignore_pipeline_branch_filters: true,
diff --git a/github_bot/src/github/events/issue_comment/utils.ts b/github_bot/src/github/events/issue_comment/utils.ts
index d72b15df86..da799de91a 100644
--- a/github_bot/src/github/events/issue_comment/utils.ts
+++ b/github_bot/src/github/events/issue_comment/utils.ts
@@ -55,11 +55,5 @@ export function hasCommentAction(
const actionText = body.replace(ciNamePattern, '');
const foundActions = actionKeys.filter((k) => actions[k].test(actionText));
- console.log({
- body,
- actionText,
- actions: foundActions,
- });
-
return foundActions.length > 0;
}
diff --git a/github_bot/src/github/events/pull_request/cleanup.ts b/github_bot/src/github/events/pull_request/cleanup.ts
index 22d3f32d28..f3519336b9 100644
--- a/github_bot/src/github/events/pull_request/cleanup.ts
+++ b/github_bot/src/github/events/pull_request/cleanup.ts
@@ -9,48 +9,46 @@
import { Probot } from 'probot';
import { buildkiteClient } from '../../../utils/buildkite';
+import { deleteDeployment } from '../../../utils/firebase';
import { commentByKey } from '../../../utils/github_utils/comments';
-import { updateLastDeployment } from '../../../utils/github_utils/deployments';
import { updateAllChecks } from '../../utils';
/**
* Cleanups prs after closure. Includes:
*
* - cancels running buildkite builds
- * - sets GitHub deployment to inactive
- * - deletes deployment issue comment
- *
- * TODOs
* - deletes firebase deployment (auto expires after 7 days)
+ * - deletes deployment issue comment
*/
export function cleanup(app: Probot) {
+ // @ts-ignore - probot issue https://github.com/probot/probot/issues/1680
app.on('pull_request.closed', async (ctx) => {
console.log(`------- Triggered probot ${ctx.name} | ${ctx.payload.action}`);
- const { head } = ctx.payload.pull_request;
-
- await buildkiteClient.cancelRunningBuilds(head.sha, async () => {
- if (!ctx.payload.pull_request.merged) {
- await updateAllChecks(ctx, {
- status: 'completed',
- conclusion: 'cancelled',
- });
- }
- });
-
- await updateLastDeployment(ctx);
-
const { data: botComments } = await ctx.octokit.issues.listComments({
...ctx.repo(),
issue_number: ctx.payload.pull_request.number,
});
- const deployComments = botComments.filter((c) => commentByKey('deployments')(c.body));
+ const deployComments = botComments.filter((c) => commentByKey('deployment')(c.body));
for (const { id } of deployComments) {
await ctx.octokit.issues.deleteComment({
...ctx.repo(),
comment_id: id,
});
}
+
+ deleteDeployment(ctx.payload.number);
+
+ const { head } = ctx.payload.pull_request;
+
+ await buildkiteClient.cancelRunningBuilds(head.sha, async () => {
+ if (!ctx.payload.pull_request.merged) {
+ await updateAllChecks(ctx, {
+ status: 'completed',
+ conclusion: 'cancelled',
+ });
+ }
+ });
});
}
diff --git a/github_bot/src/github/events/pull_request/trigger_build.ts b/github_bot/src/github/events/pull_request/trigger_build.ts
index 6550aa3060..c71b2525ab 100644
--- a/github_bot/src/github/events/pull_request/trigger_build.ts
+++ b/github_bot/src/github/events/pull_request/trigger_build.ts
@@ -36,6 +36,7 @@ const prActionTriggers = new Set['action']>([
* build trigger for base and 3rd - party forked branches
*/
export function setupBuildTrigger(app: Probot) {
+ // @ts-ignore - TS complains here about ctx being too large of a union
app.on('pull_request', async (ctx) => {
if (
!prActionTriggers.has(ctx.payload.action) ||
@@ -108,7 +109,8 @@ export function setupBuildTrigger(app: Probot) {
}
const build = await buildkiteClient.triggerBuild({
- branch: head.label, // user:branch
+ context: ` - ${ctx.name} | ${ctx.payload.action}`,
+ branch: head.label, // :
commit: head.sha,
message: commit.commit.message,
ignore_pipeline_branch_filters: true,
diff --git a/github_bot/src/github/events/push/trigger_build.ts b/github_bot/src/github/events/push/trigger_build.ts
index 2687c4cca7..7cc93c16b9 100644
--- a/github_bot/src/github/events/push/trigger_build.ts
+++ b/github_bot/src/github/events/push/trigger_build.ts
@@ -10,15 +10,16 @@ import { Probot } from 'probot';
import { getConfig } from '../../../config';
import { buildkiteClient } from '../../../utils/buildkite';
-import { checkCommitFn, isBaseRepo, testPatternString, updateAllChecks } from '../../utils';
+import { checkCommitFn, isBaseRepo, testPatternString, updateAllChecks, getBranchFromRef } from '../../utils';
/**
- * build trigger for pushes to select base branches not pull requests
+ * Build trigger for pushes to select base branches, only on base repo not forks
*/
export function setupBuildTrigger(app: Probot) {
// @ts-ignore - probot issue https://github.com/probot/probot/issues/1680
app.on('push', async (ctx) => {
- const [branch] = ctx.payload.ref.split('/').reverse();
+ console.log(`------- Triggered probot ${ctx.name}`);
+ const branch = getBranchFromRef(ctx.payload.ref);
if (
!branch ||
@@ -44,7 +45,8 @@ export function setupBuildTrigger(app: Probot) {
}
const build = await buildkiteClient.triggerBuild({
- branch,
+ context: ` - ${ctx.name}`,
+ branch: `${ctx.payload.repository.owner.login}:${branch}`,
commit: ctx.payload.after,
message: ctx.payload.head_commit?.message,
ignore_pipeline_branch_filters: true,
diff --git a/github_bot/src/github/utils.ts b/github_bot/src/github/utils.ts
index 748dfabb6b..29a6fc4ece 100644
--- a/github_bot/src/github/utils.ts
+++ b/github_bot/src/github/utils.ts
@@ -18,6 +18,11 @@ import { githubClient } from '../utils/github';
type GetPullResponseData = RestEndpointMethodTypes['pulls']['get']['response']['data'];
+/**
+ * Strips away `refs/xxx` from `refs/xxx/`
+ */
+export const getBranchFromRef = (ref: string) => ref.replace(/^refs\/(.+?\/){0,1}/, '');
+
export function isBaseRepo({
id,
fork,
@@ -93,8 +98,6 @@ export async function isValidUser(ctx: ProbotEventContext<'issue_comment' | 'pul
...ctx.repo(),
username,
});
- console.log({ permission });
-
if (status === 200 && requiredPermission.has(permission)) {
return true;
} else {
@@ -223,8 +226,6 @@ export async function updateAllChecks(
throw new Error('Missing check runs, pagination required');
}
- console.log(checkRuns);
-
for (const { id, external_id, details_url, status } of checkRuns) {
if (status !== 'completed' || external_id === main.id) {
await ctx.octokit.checks.update({
@@ -255,8 +256,6 @@ export async function updateAllChecks(
}
export async function syncChecks(ctx: ProbotEventContext<'pull_request'>) {
- console.log('syncChecks');
-
const [previousCommitSha] = await getLatestCommits(ctx);
if (!previousCommitSha) throw new Error('Unable to load previous commit');
@@ -269,6 +268,7 @@ export async function syncChecks(ctx: ProbotEventContext<'pull_request'>) {
ref: previousCommitSha,
});
+ console.log('syncChecks');
console.log(checks.map((c) => `${c.name}: status ${c.status}`));
await Promise.all(
@@ -336,41 +336,6 @@ export async function getLatestCommits(ctx: ProbotEventContext<'pull_request'>,
return response.repository.pullRequest.commits.nodes.map((n) => n.commit.oid);
}
-// TODO remove or use this function
-export async function updatePreviousDeployments(
- ctx: ProbotEventContext<'pull_request'>,
- state: RestEndpointMethodTypes['repos']['createDeploymentStatus']['parameters']['state'] = 'inactive',
-) {
- const { data: deployments } = await ctx.octokit.repos.listDeployments({
- ...ctx.repo(),
- ref: ctx.payload.pull_request.head.ref,
- per_page: 100,
- });
-
- await Promise.all(
- deployments.map(async ({ id }) => {
- const {
- data: [data],
- } = await ctx.octokit.repos.listDeploymentStatuses({
- ...ctx.repo(),
- deployment_id: id,
- per_page: 1,
- });
-
- if (data && ['in_progress', 'queued', 'pending'].includes(data.state)) {
- const { environment, ...status } = data;
- await ctx.octokit.repos.createDeploymentStatus({
- ...ctx.repo(),
- ...status,
- // @ts-ignore - bad type for environment
- environment,
- state,
- });
- }
- }),
- );
-}
-
export function pickDefined>(source: R): R {
return Object.keys(source).reduce((acc, key) => {
const val = source[key];
diff --git a/github_bot/src/utils/buildkite.ts b/github_bot/src/utils/buildkite.ts
index 5f11c36ff6..76e1abd3c2 100644
--- a/github_bot/src/utils/buildkite.ts
+++ b/github_bot/src/utils/buildkite.ts
@@ -99,7 +99,8 @@ class Buildkite {
// TODO build out separate pipelines to handle tasks on a run-as-needed basis
const url = `organizations/elastic/pipelines/${this.pipelineSlug}/builds`;
- console.log(`Triggering pipeline '${this.pipelineSlug}' against ${options.branch}...`);
+ console.log(`Trigger pipeline '${this.pipelineSlug}' against '${options.branch}'${options.context ?? ''}`);
+ console.log(JSON.stringify(options, null, 2));
try {
const response = await this.http.post(url, options);
diff --git a/github_bot/src/utils/firebase.ts b/github_bot/src/utils/firebase.ts
new file mode 100644
index 0000000000..257c771c53
--- /dev/null
+++ b/github_bot/src/utils/firebase.ts
@@ -0,0 +1,47 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { execSync } from 'child_process';
+import fs from 'fs';
+import os from 'os';
+import path from 'path';
+
+/**
+ * Deletes deployment of a given PR channel
+ */
+export function deleteDeployment(pr: number) {
+ try {
+ const channelId = `pr-${pr}`;
+ const gacFilePath = createGACFile();
+ execSync(`npx firebase-tools hosting:channel:delete ${channelId} --force`, {
+ encoding: 'utf8',
+ stdio: 'pipe',
+ env: {
+ ...process.env,
+ GOOGLE_APPLICATION_CREDENTIALS: gacFilePath,
+ } as any,
+ });
+ } catch (error) {
+ // Nothing to do, likely auto-deleted past expiry
+ console.error(error);
+ }
+}
+
+// Set up Google Application Credentials for use by the Firebase CLI
+// https://cloud.google.com/docs/authentication/production#finding_credentials_automatically
+function createGACFile(): string {
+ const gac = process.env.FIREBASE_AUTH;
+ if (!gac) throw new Error('Error: unable to find FIREBASE_AUTH');
+
+ const tmpdir = os.tmpdir();
+ const filePath = path.join(tmpdir, 'gac.json');
+
+ fs.writeFileSync(filePath, gac);
+
+ return filePath;
+}
diff --git a/github_bot/src/utils/github_utils/comments.ts b/github_bot/src/utils/github_utils/comments.ts
index 56b7a2e873..85fae2a4a3 100644
--- a/github_bot/src/utils/github_utils/comments.ts
+++ b/github_bot/src/utils/github_utils/comments.ts
@@ -23,12 +23,8 @@ export const comments = {
communityPR() {
return `Community pull request, @elastic/datavis please add the \`ci:approved ✅\` label to allow this and future builds.`;
},
- deployments(deploymentUrl: string, sha: string) {
- return `## Deployments - ${sha}
-
-- Storybook ([link](${deploymentUrl}))
-- e2e server ([link](${deploymentUrl}/e2e))
-- Playwright report ([link](${deploymentUrl}/e2e-report))`;
+ deployment() {
+ return ''; // needed for type lookup
},
};
diff --git a/github_bot/src/utils/github_utils/deployments.ts b/github_bot/src/utils/github_utils/deployments.ts
deleted file mode 100644
index 35676db0a6..0000000000
--- a/github_bot/src/utils/github_utils/deployments.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-import { RestEndpointMethodTypes } from '@octokit/rest';
-
-import { ProbotEventContext } from '../../github/types';
-
-export async function updateLastDeployment(
- ctx: ProbotEventContext<'pull_request'>,
- state: RestEndpointMethodTypes['repos']['createDeploymentStatus']['parameters']['state'] = 'inactive',
-) {
- const { data: deployments = [] } = await ctx.octokit.repos.listDeployments({
- ...ctx.repo(),
- task: `deploy:pr:${ctx.payload.pull_request.number}`,
- per_page: 2, // in case there is on successful and one pending
- });
-
- for (const deployment of deployments) {
- const {
- data: [status],
- } = await ctx.octokit.repos.listDeploymentStatuses({
- ...ctx.repo(),
- deployment_id: deployment.id,
- per_page: 1,
- });
-
- if (!status) continue;
- const { state: currentState, log_url, environment_url } = status;
-
- if (!['in_progress', 'queued', 'pending', 'success'].includes(currentState)) {
- continue;
- }
-
- console.log(`Updating deployment ${deployment.id} state: ${currentState} -> ${state}`);
-
- await ctx.octokit.repos.createDeploymentStatus({
- ...ctx.repo(),
- deployment_id: deployment.id,
- description: 'Deployment destroyed after PR closed',
- log_url,
- environment_url,
- state,
- });
- }
-}
diff --git a/github_bot/src/utils/types.ts b/github_bot/src/utils/types.ts
index 2d2759d5c3..783fa3b192 100644
--- a/github_bot/src/utils/types.ts
+++ b/github_bot/src/utils/types.ts
@@ -27,6 +27,10 @@ export interface BuildkiteTriggerBuildOptions<
E extends Record = Record,
MD extends Record = Record,
> {
+ /**
+ * trigger context, used for logging
+ */
+ context?: string;
/**
* Ref, SHA or tag to be built.
*