fix(atlassian): tighten health checks and review coverage
This commit is contained in:
@@ -6,6 +6,7 @@ import { Command } from "commander";
|
||||
import { createConfluenceClient } from "./confluence.js";
|
||||
import { loadConfig } from "./config.js";
|
||||
import { readWorkspaceFile } from "./files.js";
|
||||
import { runHealthCheck } from "./health.js";
|
||||
import { createJiraClient } from "./jira.js";
|
||||
import { writeOutput } from "./output.js";
|
||||
import { runRawCommand } from "./raw.js";
|
||||
@@ -84,19 +85,11 @@ export function buildProgram(context: CliContext = {}) {
|
||||
.command("health")
|
||||
.description("Validate configuration and Atlassian connectivity")
|
||||
.option("--format <format>", "Output format", "json")
|
||||
.action((options) => {
|
||||
.action(async (options) => {
|
||||
const payload = await runHealthCheck(runtime.getConfig(), runtime.fetchImpl);
|
||||
writeOutput(
|
||||
runtime.stdout,
|
||||
{
|
||||
ok: true,
|
||||
data: {
|
||||
baseUrl: runtime.getConfig().baseUrl,
|
||||
jiraBaseUrl: runtime.getConfig().jiraBaseUrl,
|
||||
confluenceBaseUrl: runtime.getConfig().confluenceBaseUrl,
|
||||
defaultProject: runtime.getConfig().defaultProject,
|
||||
defaultSpace: runtime.getConfig().defaultSpace,
|
||||
},
|
||||
},
|
||||
payload,
|
||||
resolveFormat(options.format),
|
||||
);
|
||||
});
|
||||
|
||||
69
skills/atlassian/cursor/scripts/src/health.ts
Normal file
69
skills/atlassian/cursor/scripts/src/health.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { createJsonHeaders, createStatusError } from "./http.js";
|
||||
import type { AtlassianConfig, CommandOutput, FetchLike } from "./types.js";
|
||||
|
||||
type ProductHealth = {
|
||||
ok: boolean;
|
||||
status?: number;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
function buildUrl(baseUrl: string, path: string) {
|
||||
return new URL(path, `${baseUrl}/`).toString();
|
||||
}
|
||||
|
||||
export async function runHealthCheck(
|
||||
config: AtlassianConfig,
|
||||
fetchImpl: FetchLike | undefined,
|
||||
): Promise<CommandOutput<unknown>> {
|
||||
const client = fetchImpl ?? globalThis.fetch;
|
||||
|
||||
if (!client) {
|
||||
throw new Error("Fetch API is not available in this runtime");
|
||||
}
|
||||
|
||||
async function probe(product: "Jira" | "Confluence", url: string): Promise<ProductHealth> {
|
||||
try {
|
||||
const response = await client(url, {
|
||||
method: "GET",
|
||||
headers: createJsonHeaders(config, false),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = createStatusError(`${product} health check failed`, response);
|
||||
return {
|
||||
ok: false,
|
||||
status: response.status,
|
||||
message: error.message,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
status: response.status,
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
ok: false,
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const jira = await probe("Jira", buildUrl(config.jiraBaseUrl, "/rest/api/3/myself"));
|
||||
const confluence = await probe("Confluence", buildUrl(config.confluenceBaseUrl, "/wiki/api/v2/spaces?limit=1"));
|
||||
|
||||
return {
|
||||
ok: jira.ok && confluence.ok,
|
||||
data: {
|
||||
baseUrl: config.baseUrl,
|
||||
jiraBaseUrl: config.jiraBaseUrl,
|
||||
confluenceBaseUrl: config.confluenceBaseUrl,
|
||||
defaultProject: config.defaultProject,
|
||||
defaultSpace: config.defaultSpace,
|
||||
products: {
|
||||
jira,
|
||||
confluence,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -24,12 +24,33 @@ export async function parseResponse(response: Response) {
|
||||
const contentType = response.headers.get("content-type") ?? "";
|
||||
|
||||
if (contentType.includes("application/json")) {
|
||||
return response.json();
|
||||
try {
|
||||
return await response.json();
|
||||
} catch {
|
||||
throw new Error("Malformed JSON response from Atlassian API");
|
||||
}
|
||||
}
|
||||
|
||||
return response.text();
|
||||
}
|
||||
|
||||
export function createStatusError(errorPrefix: string, response: Response) {
|
||||
const base = `${errorPrefix}: ${response.status} ${response.statusText}`;
|
||||
|
||||
switch (response.status) {
|
||||
case 401:
|
||||
return new Error(`${base} - check ATLASSIAN_EMAIL and ATLASSIAN_API_TOKEN`);
|
||||
case 403:
|
||||
return new Error(`${base} - verify product permissions for this account`);
|
||||
case 404:
|
||||
return new Error(`${base} - verify the resource identifier or API path`);
|
||||
case 429:
|
||||
return new Error(`${base} - retry later or reduce request rate`);
|
||||
default:
|
||||
return new Error(base);
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendJsonRequest(options: {
|
||||
config: AtlassianConfig;
|
||||
fetchImpl?: FetchLike;
|
||||
@@ -58,7 +79,7 @@ export async function sendJsonRequest(options: {
|
||||
throw customError;
|
||||
}
|
||||
|
||||
throw new Error(`${options.errorPrefix}: ${response.status} ${response.statusText}`);
|
||||
throw createStatusError(options.errorPrefix, response);
|
||||
}
|
||||
|
||||
return parseResponse(response);
|
||||
|
||||
@@ -9,7 +9,7 @@ export type AtlassianConfig = {
|
||||
};
|
||||
|
||||
export type CommandOutput<T> = {
|
||||
ok: true;
|
||||
ok: boolean;
|
||||
data: T;
|
||||
dryRun?: boolean;
|
||||
raw?: unknown;
|
||||
|
||||
Reference in New Issue
Block a user