diff --git a/packages/core/src/ci-environment/index.ts b/packages/core/src/ci-environment/index.ts index 749b9d6..6314192 100644 --- a/packages/core/src/ci-environment/index.ts +++ b/packages/core/src/ci-environment/index.ts @@ -24,9 +24,9 @@ const services = [ git, ]; -export const getCiEnvironment = ({ +export async function getCiEnvironment({ env = process.env, -}: Options = {}): CiEnvironment | null => { +}: Options = {}): Promise { const ctx = { env }; debug("Detecting CI environment", { env }); const service = services.find((service) => service.detect(ctx)); @@ -34,11 +34,11 @@ export const getCiEnvironment = ({ // Service matched if (service) { debug("Internal service matched", service.name); - const variables = service.config(ctx); + const variables = await service.config(ctx); const ciEnvironment = { name: service.name, ...variables }; debug("CI environment", ciEnvironment); return ciEnvironment; } return null; -}; +} diff --git a/packages/core/src/ci-environment/services/github-actions.ts b/packages/core/src/ci-environment/services/github-actions.ts index 422a837..93d2989 100644 --- a/packages/core/src/ci-environment/services/github-actions.ts +++ b/packages/core/src/ci-environment/services/github-actions.ts @@ -1,5 +1,66 @@ import { existsSync, readFileSync } from "node:fs"; import type { Service, Context } from "../types"; +import axios from "axios"; +import { debug } from "../../debug"; + +type EventPayload = { + pull_request?: { + head: { + sha: string; + ref: string; + }; + number: number; + }; + deployment?: { + sha: string; + environment: string; + }; +}; + +type GitHubPullRequest = { + number: number; + head: { + ref: string; + sha: string; + }; +}; + +/** + * When triggered by a deployment we try to get the pull request number from the + * deployment sha. + */ +async function getPullRequestFromHeadSha({ env }: Context, sha: string) { + debug("Fetching pull request number from head sha", sha); + if (!env.GITHUB_REPOSITORY || !env.GITHUB_TOKEN) { + debug("Aborting because GITHUB_REPOSITORY or GITHUB_TOKEN is missing"); + return null; + } + try { + const result = await axios.get( + `https://github.com/gitapi/repos/${env.GITHUB_REPOSITORY}/pulls`, + { + params: { + head: sha, + }, + headers: { + Accept: "application/vnd.github+json", + Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, + "X-GitHub-Api-Version": "2022-11-28", + }, + }, + ); + if (result.data.length === 0) { + debug("Aborting because no pull request found"); + return null; + } + const firstPr = result.data[0]; + debug("PR found", firstPr); + return firstPr; + } catch (error) { + debug("Error while fetching pull request number from head sha", error); + return null; + } +} const getBranch = ({ env }: Context) => { if (env.GITHUB_HEAD_REF) { @@ -20,16 +81,6 @@ const getRepository = ({ env }: Context) => { return env.GITHUB_REPOSITORY.split("/")[1]; }; -interface EventPayload { - pull_request?: { - head: { - sha: string; - ref: string; - }; - number: number; - }; -} - const readEventPayload = ({ env }: Context): EventPayload | null => { if (!env.GITHUB_EVENT_PATH) return null; if (!existsSync(env.GITHUB_EVENT_PATH)) return null; @@ -39,18 +90,37 @@ const readEventPayload = ({ env }: Context): EventPayload | null => { const service: Service = { name: "GitHub Actions", detect: ({ env }) => Boolean(env.GITHUB_ACTIONS), - config: ({ env }) => { + config: async ({ env }) => { const payload = readEventPayload({ env }); - return { - commit: process.env.GITHUB_SHA || null, - branch: payload?.pull_request?.head.ref || getBranch({ env }) || null, + + const commonConfig = { owner: env.GITHUB_REPOSITORY_OWNER || null, repository: getRepository({ env }), jobId: env.GITHUB_JOB || null, runId: env.GITHUB_RUN_ID || null, + nonce: `${env.GITHUB_RUN_ID}-${env.GITHUB_RUN_ATTEMPT}` || null, + }; + + // If the job is triggered by from a "deployment" or a "deployment_status" + if (payload?.deployment) { + debug("Deployment event detected"); + const { sha } = payload.deployment; + const pullRequest = await getPullRequestFromHeadSha({ env }, sha); + return { + ...commonConfig, + commit: payload.deployment.sha, + branch: pullRequest?.head.ref || payload.deployment.environment || null, + prNumber: pullRequest?.number || null, + prHeadCommit: pullRequest?.head.sha || null, + }; + } + + return { + ...commonConfig, + commit: process.env.GITHUB_SHA || null, + branch: payload?.pull_request?.head.ref || getBranch({ env }) || null, prNumber: payload?.pull_request?.number || null, prHeadCommit: payload?.pull_request?.head.sha ?? null, - nonce: `${env.GITHUB_RUN_ID}-${env.GITHUB_RUN_ATTEMPT}` || null, }; }, }; diff --git a/packages/core/src/ci-environment/types.ts b/packages/core/src/ci-environment/types.ts index 395a649..3ea6a06 100644 --- a/packages/core/src/ci-environment/types.ts +++ b/packages/core/src/ci-environment/types.ts @@ -22,5 +22,7 @@ export interface CiEnvironment { export interface Service { name: string; detect(ctx: Context): boolean; - config(ctx: Context): Omit; + config( + ctx: Context, + ): Omit | Promise>; } diff --git a/packages/core/src/config.test.ts b/packages/core/src/config.test.ts index a6e850f..964d93a 100644 --- a/packages/core/src/config.test.ts +++ b/packages/core/src/config.test.ts @@ -2,21 +2,21 @@ import { describe, it, expect } from "vitest"; import { readConfig } from "./config"; describe("#createConfig", () => { - it("gets config", () => { - const config = readConfig({ + it("gets config", async () => { + const config = await readConfig({ commit: "f16f980bd17cccfa93a1ae7766727e67950773d0", }); expect(config.commit).toBe("f16f980bd17cccfa93a1ae7766727e67950773d0"); }); - it("throws with invalid commit", () => { - expect(() => readConfig({ commit: "xx" })).toThrow( + it("throws with invalid commit", async () => { + await expect(() => readConfig({ commit: "xx" })).toThrow( "commit: Invalid commit", ); }); - it("throws with invalid token", () => { - expect(() => + it("throws with invalid token", async () => { + await expect(() => readConfig({ commit: "f16f980bd17cccfa93a1ae7766727e67950773d0", token: "invalid", diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 6792980..95cfd93 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -148,10 +148,10 @@ const createConfig = () => { }); }; -export const readConfig = (options: Partial = {}) => { +export async function readConfig(options: Partial = {}) { const config = createConfig(); - const ciEnv = getCiEnvironment(); + const ciEnv = await getCiEnvironment(); config.load({ apiBaseUrl: options.apiBaseUrl ?? config.get("apiBaseUrl"), @@ -183,4 +183,4 @@ export const readConfig = (options: Partial = {}) => { config.validate(); return config.get(); -}; +} diff --git a/packages/core/src/upload.ts b/packages/core/src/upload.ts index d48d852..59a0027 100644 --- a/packages/core/src/upload.ts +++ b/packages/core/src/upload.ts @@ -47,15 +47,17 @@ export interface UploadParameters { referenceCommit?: string; } -const getConfigFromOptions = ({ parallel, ...options }: UploadParameters) => { - const config = readConfig({ +async function getConfigFromOptions({ + parallel, + ...options +}: UploadParameters) { + return readConfig({ ...options, parallel: Boolean(parallel), parallelNonce: parallel ? parallel.nonce : null, parallelTotal: parallel ? parallel.total : null, }); - return config; -}; +} async function uploadFilesToS3( files: { url: string; path: string; contentType: string }[], @@ -86,11 +88,11 @@ async function uploadFilesToS3( /** * Upload screenshots to argos-ci.com. */ -export const upload = async (params: UploadParameters) => { +export async function upload(params: UploadParameters) { debug("Starting upload with params", params); // Read config - const config = getConfigFromOptions(params); + const config = await getConfigFromOptions(params); const files = params.files ?? ["**/*.{png,jpg,jpeg}"]; debug("Using config and files", config, files); @@ -209,4 +211,4 @@ export const upload = async (params: UploadParameters) => { }); return { build: result.build, screenshots }; -}; +} diff --git a/packages/playwright/src/reporter.ts b/packages/playwright/src/reporter.ts index 301481d..e9660e8 100644 --- a/packages/playwright/src/reporter.ts +++ b/packages/playwright/src/reporter.ts @@ -36,12 +36,12 @@ export type ArgosReporterOptions = Omit & { uploadToArgos?: boolean; }; -const getParallelFromConfig = ( +async function getParallelFromConfig( config: FullConfig, -): null | UploadParameters["parallel"] => { +): Promise { if (!config.shard) return null; if (config.shard.total === 1) return null; - const argosConfig = readConfig(); + const argosConfig = await readConfig(); if (!argosConfig.parallelNonce) { throw new Error( "Playwright shard mode detected. Please specify ARGOS_PARALLEL_NONCE env variable. Read /parallel-testing", @@ -51,7 +51,7 @@ const getParallelFromConfig = ( total: config.shard.total, nonce: argosConfig.parallelNonce, }; -}; +} class ArgosReporter implements Reporter { uploadDir!: string; @@ -130,7 +130,7 @@ class ArgosReporter implements Reporter { async onEnd(_result: FullResult) { if (!this.uploadToArgos) return; - const parallel = getParallelFromConfig(this.playwrightConfig); + const parallel = await getParallelFromConfig(this.playwrightConfig); try { const res = await upload({