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

feat(ci): support running on "deployment_status" event #105

Merged
merged 1 commit into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 4 additions & 4 deletions packages/core/src/ci-environment/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,21 @@ const services = [
git,
];

export const getCiEnvironment = ({
export async function getCiEnvironment({
env = process.env,
}: Options = {}): CiEnvironment | null => {
}: Options = {}): Promise<CiEnvironment | null> {
const ctx = { env };
debug("Detecting CI environment", { env });
const service = services.find((service) => service.detect(ctx));

// 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;
};
}
117 changes: 102 additions & 15 deletions packages/core/src/ci-environment/services/github-actions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,80 @@
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) {
throw new Error("GITHUB_REPOSITORY is missing");
}
if (!env.GITHUB_TOKEN) {
if (!env.DISABLE_GITHUB_TOKEN_WARNING) {
console.log(
`
Running argos from a "deployment_status" event requires a GITHUB_TOKEN.
Please add \`GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}\` as environment variable.

Read more at https://argos-ci.com/docs/run-on-preview-deployment

To disable this warning, add \`DISABLE_GITHUB_TOKEN_WARNING: true\` as environment variable.
`.trim(),
);
}
return null;
}
try {
const result = await axios.get<GitHubPullRequest[]>(
`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 from head sha", error);
return null;
}
}

const getBranch = ({ env }: Context) => {
if (env.GITHUB_HEAD_REF) {
Expand All @@ -20,16 +95,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;
Expand All @@ -39,18 +104,40 @@ 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 sha = process.env.GITHUB_SHA || null;

if (!sha) {
throw new Error(`GITHUB_SHA is missing`);
}

const commonConfig = {
commit: sha,
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 pullRequest = await getPullRequestFromHeadSha({ env }, sha);
return {
...commonConfig,
branch: pullRequest?.head.ref || payload.deployment.environment || null,
prNumber: pullRequest?.number || null,
prHeadCommit: pullRequest?.head.sha || null,
};
}

return {
...commonConfig,
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,
};
},
};
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/ci-environment/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,7 @@ export interface CiEnvironment {
export interface Service {
name: string;
detect(ctx: Context): boolean;
config(ctx: Context): Omit<CiEnvironment, "name">;
config(
ctx: Context,
): Omit<CiEnvironment, "name"> | Promise<Omit<CiEnvironment, "name">>;
}
16 changes: 9 additions & 7 deletions packages/core/src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,27 @@ 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" })).rejects.toThrow(
"commit: Invalid commit",
);
});

it("throws with invalid token", () => {
expect(() =>
it("throws with invalid token", async () => {
await expect(() =>
readConfig({
commit: "f16f980bd17cccfa93a1ae7766727e67950773d0",
token: "invalid",
}),
).toThrow("token: Invalid Argos repository token (must be 40 characters)");
).rejects.toThrow(
"token: Invalid Argos repository token (must be 40 characters)",
);
});
});
6 changes: 3 additions & 3 deletions packages/core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,10 @@ const createConfig = () => {
});
};

export const readConfig = (options: Partial<Config> = {}) => {
export async function readConfig(options: Partial<Config> = {}) {
const config = createConfig();

const ciEnv = getCiEnvironment();
const ciEnv = await getCiEnvironment();

config.load({
apiBaseUrl: options.apiBaseUrl ?? config.get("apiBaseUrl"),
Expand Down Expand Up @@ -183,4 +183,4 @@ export const readConfig = (options: Partial<Config> = {}) => {
config.validate();

return config.get();
};
}
16 changes: 9 additions & 7 deletions packages/core/src/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }[],
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -209,4 +211,4 @@ export const upload = async (params: UploadParameters) => {
});

return { build: result.build, screenshots };
};
}
10 changes: 5 additions & 5 deletions packages/playwright/src/reporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ export type ArgosReporterOptions = Omit<UploadParameters, "files" | "root"> & {
uploadToArgos?: boolean;
};

const getParallelFromConfig = (
async function getParallelFromConfig(
config: FullConfig,
): null | UploadParameters["parallel"] => {
): Promise<null | UploadParameters["parallel"]> {
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",
Expand All @@ -51,7 +51,7 @@ const getParallelFromConfig = (
total: config.shard.total,
nonce: argosConfig.parallelNonce,
};
};
}

class ArgosReporter implements Reporter {
uploadDir!: string;
Expand Down Expand Up @@ -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({
Expand Down