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
158 changes: 152 additions & 6 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 @@ -184,6 +204,26 @@ export interface paths {
patch?: never;
trace?: never;
};
"/me": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* Get the current user
* @description Retrieve the user associated with the personal access token used to authenticate the request.
*/
get: operations["getMe"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/project": {
parameters: {
query?: never;
Expand Down Expand Up @@ -718,6 +758,12 @@ export interface components {
message: string;
}[];
};
/** @description A user. */
User: {
id: string;
slug: string;
name: string | null;
};
/** @description Project */
Project: {
id: string;
Expand Down Expand Up @@ -1002,12 +1048,6 @@ export interface components {
/** @description Date the review was created. */
date: string;
};
/** @description A user. */
User: {
id: string;
slug: string;
name: string | null;
};
/** @description A comment posted on a build. */
Comment: {
id: string;
Expand Down Expand Up @@ -1570,6 +1610,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 Expand Up @@ -1791,6 +1899,44 @@ export interface operations {
};
};
};
getMe: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description The authenticated user */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["User"];
};
};
/** @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"];
};
};
};
};
getAuthProject: {
parameters: {
query?: never;
Expand Down
45 changes: 45 additions & 0 deletions packages/cli/e2e/whoami.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test } from "vitest";

import { getRequiredEnv, run, type CommandError } from "./utils";

const userAccessToken = getRequiredEnv("USER_ACCESS_TOKEN");

const baseEnv: NodeJS.ProcessEnv = {
...process.env,
HOME: mkdtempSync(join(tmpdir(), "argos-cli-e2e-")),
ARGOS_API_BASE_URL: process.env.ARGOS_API_BASE_URL,
ARGOS_TOKEN: "",
};

describe("argos whoami", () => {
test("fails when no token is provided", () => {
let error: CommandError | undefined;
try {
run(["whoami"], baseEnv);
} catch (err) {
error = err as CommandError;
}
expect(error).toBeDefined();
expect(error?.status).not.toBe(0);
expect(error?.stderr).toContain("No Argos token found");
});

test("prints the authenticated user in JSON mode", () => {
const output = run(
["whoami", "--token", userAccessToken, "--json"],
baseEnv,
);
const user = JSON.parse(output.stdout);
expect(user.id).toBeDefined();
expect(user.slug).toBeDefined();
});

test("prints human-readable user data", () => {
const output = run(["whoami", "--token", userAccessToken], baseEnv);
expect(output.stdout).toContain("Logged in to Argos as");
expect(output.stdout).toContain("Slug:");
});
});
28 changes: 28 additions & 0 deletions packages/cli/src/commands/whoami.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { Command } from "commander";
import { createApiClient, unwrap } from "../lib/api";
import { formatMe } from "../lib/format";
import { handleCliError, output } from "../lib/run";
import { resolveToken } from "../lib/target";
import { jsonOption, tokenOption } from "../options";

type WhoamiOptions = {
token?: string | undefined;
json?: boolean | undefined;
};

export function whoamiCommand(program: Command) {
program
.command("whoami")
.description("Display the user authenticated with the current Argos token")
.addOption(tokenOption)
.addOption(jsonOption)
.action(async (options: WhoamiOptions) => {
try {
const client = createApiClient(await resolveToken(options));
const user = unwrap(await client.GET("/me"));
output(user, options, formatMe);
} catch (error) {
handleCliError(error, "user");
}
});
}
2 changes: 2 additions & 0 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { commentCommand } from "./commands/comment";
import { loginCommand } from "./commands/login";
import { logoutCommand } from "./commands/logout";
import { deployCommand } from "./commands/deploy";
import { whoamiCommand } from "./commands/whoami";

const __dirname = fileURLToPath(new URL(".", import.meta.url));

Expand All @@ -33,6 +34,7 @@ commentCommand(program);
loginCommand(program);
logoutCommand(program);
deployCommand(program);
whoamiCommand(program);

if (!process.argv.slice(2).length) {
program.outputHelp();
Expand Down
8 changes: 8 additions & 0 deletions packages/cli/src/lib/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ function formatUser(user: User | null | undefined): string {
return user.name ? `${user.name} (@${user.slug})` : `@${user.slug}`;
}

export function formatMe(user: User): string {
return [
`Logged in to Argos as ${formatUser(user)}.`,
`Slug: ${user.slug}`,
`Name: ${formatValue(user.name)}`,
].join("\n");
}

export function formatStats(stats: Build["stats"]): string {
if (!stats) {
return "-";
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/lib/target.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export type BuildTarget = ProjectPath & {
* Resolve the API token, preferring an explicit token (`--token` /
* `ARGOS_TOKEN`) over the one stored by `argos login`.
*/
async function resolveToken(options: TargetOptions): Promise<string> {
export async function resolveToken(options: TargetOptions): Promise<string> {
const token =
options.token || process.env["ARGOS_TOKEN"] || (await getStoredToken());
if (!token) {
Expand Down
Loading