Skip to content
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
88 changes: 88 additions & 0 deletions packages/api-client/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,26 @@ export interface paths {
patch?: never;
trace?: never;
};
"/baseline": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Find an eligible baseline from a list of commits
* @description Find the build eligible to be used as a baseline among a list of commits. Useful when no Git provider is connected: the CLI can send the candidate ancestor commits and let Argos pick the closest one that has an eligible (complete, valid, approved and not rejected) baseline build.
*/
post: operations["findBaseline"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/auth/cli/token": {
parameters: {
query?: never;
Expand Down Expand Up @@ -1570,6 +1590,74 @@ export interface operations {
};
};
};
findBaseline: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: {
content: {
"application/json": {
/** @description The commits to look for an eligible baseline, ordered from the closest to the furthest ancestor. The first commit with an eligible baseline wins. */
commits: components["schemas"]["Sha1Hash"][];
/**
* @description The name of the build to find a baseline for.
* @default default
*/
name?: string;
/**
* @description The mode of the build to find a baseline for.
* @default ci
* @enum {string}
*/
mode?: "ci" | "monitoring";
};
};
};
responses: {
/** @description The eligible baseline build, or null when none is found */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
/** @description The eligible baseline build found among the commits, or null when none is found. */
baseline: components["schemas"]["Build"] | null;
};
};
};
/** @description Invalid parameters */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Error"];
};
};
/** @description Unauthorized */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Error"];
};
};
/** @description Server error */
500: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Error"];
};
};
};
};
exchangeCliToken: {
parameters: {
query?: never;
Expand Down
67 changes: 66 additions & 1 deletion packages/core/src/ci-environment/git.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { branch, getMergeBaseCommitSha, getRepositoryURL, head } from "./git";
import {
branch,
getMergeBaseCommitSha,
getRepositoryURL,
head,
listAncestorCommits,
} from "./git";

describe("#head", () => {
it("returns the current commit", () => {
Expand All @@ -14,13 +20,13 @@
// These tests are just examples to run locally
// Hard to make it work reliably in CI

describe.skip("#branch", () => {

Check warning on line 23 in packages/core/src/ci-environment/git.test.ts

View workflow job for this annotation

GitHub Actions / lint

Disabled test suite - if you want to skip a test suite temporarily, use .todo() instead
it("returns the current branch", () => {
expect(branch()).toBe("main");
});
});

describe.skip("#getRepositoryURL", () => {

Check warning on line 29 in packages/core/src/ci-environment/git.test.ts

View workflow job for this annotation

GitHub Actions / lint

Disabled test suite - if you want to skip a test suite temporarily, use .todo() instead
it("returns the repository URL", () => {
expect(getRepositoryURL()).toBe(
"git@github.com:argos-ci/argos-javascript.git",
Expand Down Expand Up @@ -74,6 +80,65 @@
});
});

describe("#listAncestorCommits", () => {
let cwd: string;
let root: string;
let repoDir: string;
// Commit SHAs ordered from the oldest (first) to the newest (head).
let shas: string[];
let firstSha: string;
let headSha: string;

beforeEach(() => {
cwd = process.cwd();
root = mkdtempSync(join(tmpdir(), "argos-git-ancestors-test-"));
repoDir = join(root, "repo");

const bareDir = join(root, "origin.git");
execFileSync("git", ["init", "--bare", bareDir]);
execFileSync("git", ["init", repoDir]);
const git = (...args: string[]) =>
execFileSync("git", ["-C", repoDir, ...args]);
git("remote", "add", "origin", bareDir);
git("config", "user.email", "test@argos-ci.com");
git("config", "user.name", "Argos Test");

// Create a linear history of 5 commits.
shas = [];
for (let i = 0; i < 5; i++) {
git("commit", "--allow-empty", "-m", `commit ${i}`);
shas.push(git("rev-parse", "HEAD").toString().trim());
}
firstSha = shas[0] as string;
headSha = shas[shas.length - 1] as string;
git("branch", "-M", "main");
git("push", "origin", "main");

process.chdir(repoDir);
});

afterEach(() => {
process.chdir(cwd);
rmSync(root, { recursive: true, force: true });
});

it("lists ancestors closest first, excluding the commit itself", async () => {
const ancestors = await listAncestorCommits({ sha: headSha, limit: 10 });
// All commits but the head, ordered from the closest to the furthest.
expect(ancestors).toEqual([...shas].reverse().slice(1));
});

it("limits the number of ancestors returned", async () => {
const ancestors = await listAncestorCommits({ sha: headSha, limit: 2 });
expect(ancestors).toEqual([...shas].reverse().slice(1, 3));
});

it("returns an empty array when the commit has no ancestor", async () => {
const ancestors = await listAncestorCommits({ sha: firstSha, limit: 10 });
expect(ancestors).toEqual([]);
});
});

describe("#getMergeBaseCommitSha (lock contention)", () => {
let cwd: string;
let root: string;
Expand Down
33 changes: 25 additions & 8 deletions packages/core/src/ci-environment/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,16 +216,33 @@ function listShas(path: string, maxCount?: number): string[] {
return shas;
}

export async function listParentCommits(input: {
/**
* List the ancestor commits of a commit, ordered from the closest to the
* furthest ancestor, up to `limit` commits. The commit itself is excluded.
*
* The history is deepened with a shallow fetch so this works on the shallow
* clones typically used in CI. When the commit is not on the remote (e.g. not
* pushed yet) or the fetch fails, we fall back to whatever local history is
* available; when the commit is unknown locally too, we return an empty list.
*/
export async function listAncestorCommits(input: {
sha: string;
}): Promise<string[] | null> {
const limit = 200;
limit: number;
}): Promise<string[]> {
// Fetch one extra commit since the commit itself is excluded from the result.
const depth = input.limit + 1;
try {
await runGitFetch([`--depth=${limit}`, "origin", input.sha]);
await runGitFetch([`--depth=${depth}`, "origin", input.sha]);
} catch (error) {
if (getGitErrorOutput(error).includes("not our ref")) {
return [];
}
debug(
`Failed to deepen history for ${input.sha}, using local history`,
getGitErrorOutput(error),
);
}
try {
return listShas(input.sha, depth).slice(1);
} catch (error) {
debug(`Failed to list ancestors of ${input.sha}`, getGitErrorOutput(error));
return [];
}
return listShas(input.sha, limit);
}
11 changes: 6 additions & 5 deletions packages/core/src/ci-environment/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,17 +54,18 @@ export function getMergeBaseCommitSha(input: {
}

/**
* Get the merge base commit.
* List the ancestor commits of a commit, closest first, up to `limit` commits.
*/
export function listParentCommits(input: {
export function listAncestorCommits(input: {
sha: string;
}): Promise<string[] | null> {
limit: number;
}): Promise<string[]> {
const context = createContext();
const service = getCiService(context);
if (!service) {
return Promise.resolve(null);
return Promise.resolve([]);
}
return service.listParentCommits(input, context);
return service.listAncestorCommits(input, context);
}
/**
* Get the CI environment.
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/ci-environment/services/bitrise.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getMergeBaseCommitSha, listParentCommits } from "../git";
import { getMergeBaseCommitSha, listAncestorCommits } from "../git";
import type { Service, Context } from "../types";

function getPrNumber(context: Context) {
Expand Down Expand Up @@ -36,7 +36,7 @@ const service: Service = {
};
},
getMergeBaseCommitSha,
listParentCommits,
listAncestorCommits,
};

export default service;
9 changes: 7 additions & 2 deletions packages/core/src/ci-environment/services/buildkite.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import type { Context, Service } from "../types";
import { head, branch, getMergeBaseCommitSha, listParentCommits } from "../git";
import {
head,
branch,
getMergeBaseCommitSha,
listAncestorCommits,
} from "../git";
import { getRepositoryNameFromURL } from "../../util/url";

function getRepository(context: Context): string | null {
Expand Down Expand Up @@ -35,7 +40,7 @@ const service: Service = {
};
},
getMergeBaseCommitSha,
listParentCommits,
listAncestorCommits,
};

export default service;
4 changes: 2 additions & 2 deletions packages/core/src/ci-environment/services/circleci.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getMergeBaseCommitSha, listParentCommits } from "../git";
import { getMergeBaseCommitSha, listAncestorCommits } from "../git";
import type { Service, Context } from "../types";

function getPrNumber(context: Context) {
Expand Down Expand Up @@ -48,7 +48,7 @@ const service: Service = {
};
},
getMergeBaseCommitSha,
listParentCommits,
listAncestorCommits,
};

export default service;
4 changes: 2 additions & 2 deletions packages/core/src/ci-environment/services/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
branch,
checkIsGitRepository,
getMergeBaseCommitSha,
listParentCommits,
listAncestorCommits,
getRepositoryURL,
} from "../git";
import { getRepositoryNameFromURL } from "../../util/url";
Expand Down Expand Up @@ -38,7 +38,7 @@ const service: Service = {
};
},
getMergeBaseCommitSha,
listParentCommits,
listAncestorCommits,
};

export default service;
4 changes: 2 additions & 2 deletions packages/core/src/ci-environment/services/github-actions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { existsSync, readFileSync } from "node:fs";
import type { Service, Context } from "../types";
import { getMergeBaseCommitSha, listParentCommits } from "../git";
import { getMergeBaseCommitSha, listAncestorCommits } from "../git";
import type * as webhooks from "@octokit/webhooks";
import type { RepositoryDispatchContext } from "@vercel/repository-dispatch/context";
import {
Expand Down Expand Up @@ -338,7 +338,7 @@ const service: Service = {
};
},
getMergeBaseCommitSha,
listParentCommits,
listAncestorCommits,
};

export default service;
4 changes: 2 additions & 2 deletions packages/core/src/ci-environment/services/gitlab.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getMergeBaseCommitSha, listParentCommits } from "../git";
import { getMergeBaseCommitSha, listAncestorCommits } from "../git";
import type { Context, Service } from "../types";

function getRepository(context: Context): string | null {
Expand Down Expand Up @@ -38,7 +38,7 @@ const service: Service = {
};
},
getMergeBaseCommitSha,
listParentCommits,
listAncestorCommits,
};

export default service;
4 changes: 2 additions & 2 deletions packages/core/src/ci-environment/services/heroku.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Service } from "../types";
import { getMergeBaseCommitSha, listParentCommits } from "../git";
import { getMergeBaseCommitSha, listAncestorCommits } from "../git";

const service: Service = {
name: "Heroku",
Expand All @@ -20,7 +20,7 @@ const service: Service = {
nonce: env.HEROKU_TEST_RUN_ID || null,
}),
getMergeBaseCommitSha,
listParentCommits,
listAncestorCommits,
};

export default service;
4 changes: 2 additions & 2 deletions packages/core/src/ci-environment/services/travis.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Context, Service } from "../types";
import { getMergeBaseCommitSha, listParentCommits } from "../git";
import { getMergeBaseCommitSha, listAncestorCommits } from "../git";

function getRepository(context: Context): string | null {
const { env } = context;
Expand Down Expand Up @@ -47,7 +47,7 @@ const service: Service = {
};
},
getMergeBaseCommitSha,
listParentCommits,
listAncestorCommits,
};

export default service;
5 changes: 3 additions & 2 deletions packages/core/src/ci-environment/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,11 @@ export interface Service {
},
ctx: Context,
): Promise<string | null>;
listParentCommits(
listAncestorCommits(
input: {
sha: string;
limit: number;
},
ctx: Context,
): Promise<string[] | null>;
): Promise<string[]>;
}
Loading
Loading