fix(atlassian): tighten health checks and review coverage
This commit is contained in:
@@ -50,7 +50,7 @@ Optional:
|
|||||||
|
|
||||||
## Command Notes
|
## Command Notes
|
||||||
|
|
||||||
- `health` validates local configuration and shows the resolved Jira and Confluence base URLs.
|
- `health` validates local configuration, probes Jira and Confluence separately, and reports one product as unavailable without masking the other.
|
||||||
- `jira-create` requires `--type`, `--summary`, and either `--project` or `ATLASSIAN_DEFAULT_PROJECT`.
|
- `jira-create` requires `--type`, `--summary`, and either `--project` or `ATLASSIAN_DEFAULT_PROJECT`.
|
||||||
- `jira-update` requires `--issue` and at least one of `--summary` or `--description-file`.
|
- `jira-update` requires `--issue` and at least one of `--summary` or `--description-file`.
|
||||||
- `conf-create` requires `--title`, `--body-file`, and either `--space` or `ATLASSIAN_DEFAULT_SPACE`.
|
- `conf-create` requires `--title`, `--body-file`, and either `--space` or `ATLASSIAN_DEFAULT_SPACE`.
|
||||||
|
|||||||
@@ -58,6 +58,12 @@ If any check fails, stop and return:
|
|||||||
- `pnpm atlassian conf-children --page 12345`
|
- `pnpm atlassian conf-children --page 12345`
|
||||||
- `pnpm atlassian raw --product jira|confluence --method GET|POST|PUT --path ...`
|
- `pnpm atlassian raw --product jira|confluence --method GET|POST|PUT --path ...`
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
- `pnpm atlassian jira-search --jql "project = ENG ORDER BY updated DESC" --max-results 10`
|
||||||
|
- `pnpm atlassian conf-comment --page 12345 --body-file comment.storage.html --dry-run`
|
||||||
|
- `pnpm atlassian raw --product jira --method GET --path "/rest/api/3/issue/ENG-123"`
|
||||||
|
|
||||||
## Safety Rules
|
## Safety Rules
|
||||||
|
|
||||||
- Default output is JSON; only switch to text output when the user needs a human-readable summary.
|
- Default output is JSON; only switch to text output when the user needs a human-readable summary.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Command } from "commander";
|
|||||||
import { createConfluenceClient } from "./confluence.js";
|
import { createConfluenceClient } from "./confluence.js";
|
||||||
import { loadConfig } from "./config.js";
|
import { loadConfig } from "./config.js";
|
||||||
import { readWorkspaceFile } from "./files.js";
|
import { readWorkspaceFile } from "./files.js";
|
||||||
|
import { runHealthCheck } from "./health.js";
|
||||||
import { createJiraClient } from "./jira.js";
|
import { createJiraClient } from "./jira.js";
|
||||||
import { writeOutput } from "./output.js";
|
import { writeOutput } from "./output.js";
|
||||||
import { runRawCommand } from "./raw.js";
|
import { runRawCommand } from "./raw.js";
|
||||||
@@ -84,19 +85,11 @@ export function buildProgram(context: CliContext = {}) {
|
|||||||
.command("health")
|
.command("health")
|
||||||
.description("Validate configuration and Atlassian connectivity")
|
.description("Validate configuration and Atlassian connectivity")
|
||||||
.option("--format <format>", "Output format", "json")
|
.option("--format <format>", "Output format", "json")
|
||||||
.action((options) => {
|
.action(async (options) => {
|
||||||
|
const payload = await runHealthCheck(runtime.getConfig(), runtime.fetchImpl);
|
||||||
writeOutput(
|
writeOutput(
|
||||||
runtime.stdout,
|
runtime.stdout,
|
||||||
{
|
payload,
|
||||||
ok: true,
|
|
||||||
data: {
|
|
||||||
baseUrl: runtime.getConfig().baseUrl,
|
|
||||||
jiraBaseUrl: runtime.getConfig().jiraBaseUrl,
|
|
||||||
confluenceBaseUrl: runtime.getConfig().confluenceBaseUrl,
|
|
||||||
defaultProject: runtime.getConfig().defaultProject,
|
|
||||||
defaultSpace: runtime.getConfig().defaultSpace,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
resolveFormat(options.format),
|
resolveFormat(options.format),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
69
skills/atlassian/claude-code/scripts/src/health.ts
Normal file
69
skills/atlassian/claude-code/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") ?? "";
|
const contentType = response.headers.get("content-type") ?? "";
|
||||||
|
|
||||||
if (contentType.includes("application/json")) {
|
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();
|
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: {
|
export async function sendJsonRequest(options: {
|
||||||
config: AtlassianConfig;
|
config: AtlassianConfig;
|
||||||
fetchImpl?: FetchLike;
|
fetchImpl?: FetchLike;
|
||||||
@@ -58,7 +79,7 @@ export async function sendJsonRequest(options: {
|
|||||||
throw customError;
|
throw customError;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`${options.errorPrefix}: ${response.status} ${response.statusText}`);
|
throw createStatusError(options.errorPrefix, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
return parseResponse(response);
|
return parseResponse(response);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export type AtlassianConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type CommandOutput<T> = {
|
export type CommandOutput<T> = {
|
||||||
ok: true;
|
ok: boolean;
|
||||||
data: T;
|
data: T;
|
||||||
dryRun?: boolean;
|
dryRun?: boolean;
|
||||||
raw?: unknown;
|
raw?: unknown;
|
||||||
|
|||||||
@@ -60,6 +60,12 @@ If any check fails, stop and return:
|
|||||||
- `pnpm atlassian conf-children --page 12345`
|
- `pnpm atlassian conf-children --page 12345`
|
||||||
- `pnpm atlassian raw --product jira|confluence --method GET|POST|PUT --path ...`
|
- `pnpm atlassian raw --product jira|confluence --method GET|POST|PUT --path ...`
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
- `pnpm atlassian jira-search --jql "project = ENG ORDER BY updated DESC" --max-results 10`
|
||||||
|
- `pnpm atlassian conf-update --page 12345 --title "Runbook" --body-file page.storage.html --dry-run`
|
||||||
|
- `pnpm atlassian raw --product confluence --method POST --path "/wiki/api/v2/pages" --body-file page.json --dry-run`
|
||||||
|
|
||||||
## Safety Rules
|
## Safety Rules
|
||||||
|
|
||||||
- Default output is JSON; prefer that for agent workflows.
|
- Default output is JSON; prefer that for agent workflows.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Command } from "commander";
|
|||||||
import { createConfluenceClient } from "./confluence.js";
|
import { createConfluenceClient } from "./confluence.js";
|
||||||
import { loadConfig } from "./config.js";
|
import { loadConfig } from "./config.js";
|
||||||
import { readWorkspaceFile } from "./files.js";
|
import { readWorkspaceFile } from "./files.js";
|
||||||
|
import { runHealthCheck } from "./health.js";
|
||||||
import { createJiraClient } from "./jira.js";
|
import { createJiraClient } from "./jira.js";
|
||||||
import { writeOutput } from "./output.js";
|
import { writeOutput } from "./output.js";
|
||||||
import { runRawCommand } from "./raw.js";
|
import { runRawCommand } from "./raw.js";
|
||||||
@@ -84,19 +85,11 @@ export function buildProgram(context: CliContext = {}) {
|
|||||||
.command("health")
|
.command("health")
|
||||||
.description("Validate configuration and Atlassian connectivity")
|
.description("Validate configuration and Atlassian connectivity")
|
||||||
.option("--format <format>", "Output format", "json")
|
.option("--format <format>", "Output format", "json")
|
||||||
.action((options) => {
|
.action(async (options) => {
|
||||||
|
const payload = await runHealthCheck(runtime.getConfig(), runtime.fetchImpl);
|
||||||
writeOutput(
|
writeOutput(
|
||||||
runtime.stdout,
|
runtime.stdout,
|
||||||
{
|
payload,
|
||||||
ok: true,
|
|
||||||
data: {
|
|
||||||
baseUrl: runtime.getConfig().baseUrl,
|
|
||||||
jiraBaseUrl: runtime.getConfig().jiraBaseUrl,
|
|
||||||
confluenceBaseUrl: runtime.getConfig().confluenceBaseUrl,
|
|
||||||
defaultProject: runtime.getConfig().defaultProject,
|
|
||||||
defaultSpace: runtime.getConfig().defaultSpace,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
resolveFormat(options.format),
|
resolveFormat(options.format),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
69
skills/atlassian/codex/scripts/src/health.ts
Normal file
69
skills/atlassian/codex/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") ?? "";
|
const contentType = response.headers.get("content-type") ?? "";
|
||||||
|
|
||||||
if (contentType.includes("application/json")) {
|
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();
|
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: {
|
export async function sendJsonRequest(options: {
|
||||||
config: AtlassianConfig;
|
config: AtlassianConfig;
|
||||||
fetchImpl?: FetchLike;
|
fetchImpl?: FetchLike;
|
||||||
@@ -58,7 +79,7 @@ export async function sendJsonRequest(options: {
|
|||||||
throw customError;
|
throw customError;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`${options.errorPrefix}: ${response.status} ${response.statusText}`);
|
throw createStatusError(options.errorPrefix, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
return parseResponse(response);
|
return parseResponse(response);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export type AtlassianConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type CommandOutput<T> = {
|
export type CommandOutput<T> = {
|
||||||
ok: true;
|
ok: boolean;
|
||||||
data: T;
|
data: T;
|
||||||
dryRun?: boolean;
|
dryRun?: boolean;
|
||||||
raw?: unknown;
|
raw?: unknown;
|
||||||
|
|||||||
@@ -73,6 +73,12 @@ If any check fails, stop and return:
|
|||||||
- `pnpm atlassian conf-children --page 12345`
|
- `pnpm atlassian conf-children --page 12345`
|
||||||
- `pnpm atlassian raw --product jira|confluence --method GET|POST|PUT --path ...`
|
- `pnpm atlassian raw --product jira|confluence --method GET|POST|PUT --path ...`
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
- `pnpm atlassian jira-get --issue ENG-123`
|
||||||
|
- `pnpm atlassian conf-search --query "title ~ \\\"Runbook\\\"" --max-results 10 --start-at 0`
|
||||||
|
- `pnpm atlassian raw --product confluence --method POST --path "/wiki/api/v2/pages" --body-file page.json --dry-run`
|
||||||
|
|
||||||
## Safety Rules
|
## Safety Rules
|
||||||
|
|
||||||
- Prefer JSON output for agent use.
|
- Prefer JSON output for agent use.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Command } from "commander";
|
|||||||
import { createConfluenceClient } from "./confluence.js";
|
import { createConfluenceClient } from "./confluence.js";
|
||||||
import { loadConfig } from "./config.js";
|
import { loadConfig } from "./config.js";
|
||||||
import { readWorkspaceFile } from "./files.js";
|
import { readWorkspaceFile } from "./files.js";
|
||||||
|
import { runHealthCheck } from "./health.js";
|
||||||
import { createJiraClient } from "./jira.js";
|
import { createJiraClient } from "./jira.js";
|
||||||
import { writeOutput } from "./output.js";
|
import { writeOutput } from "./output.js";
|
||||||
import { runRawCommand } from "./raw.js";
|
import { runRawCommand } from "./raw.js";
|
||||||
@@ -84,19 +85,11 @@ export function buildProgram(context: CliContext = {}) {
|
|||||||
.command("health")
|
.command("health")
|
||||||
.description("Validate configuration and Atlassian connectivity")
|
.description("Validate configuration and Atlassian connectivity")
|
||||||
.option("--format <format>", "Output format", "json")
|
.option("--format <format>", "Output format", "json")
|
||||||
.action((options) => {
|
.action(async (options) => {
|
||||||
|
const payload = await runHealthCheck(runtime.getConfig(), runtime.fetchImpl);
|
||||||
writeOutput(
|
writeOutput(
|
||||||
runtime.stdout,
|
runtime.stdout,
|
||||||
{
|
payload,
|
||||||
ok: true,
|
|
||||||
data: {
|
|
||||||
baseUrl: runtime.getConfig().baseUrl,
|
|
||||||
jiraBaseUrl: runtime.getConfig().jiraBaseUrl,
|
|
||||||
confluenceBaseUrl: runtime.getConfig().confluenceBaseUrl,
|
|
||||||
defaultProject: runtime.getConfig().defaultProject,
|
|
||||||
defaultSpace: runtime.getConfig().defaultSpace,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
resolveFormat(options.format),
|
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") ?? "";
|
const contentType = response.headers.get("content-type") ?? "";
|
||||||
|
|
||||||
if (contentType.includes("application/json")) {
|
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();
|
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: {
|
export async function sendJsonRequest(options: {
|
||||||
config: AtlassianConfig;
|
config: AtlassianConfig;
|
||||||
fetchImpl?: FetchLike;
|
fetchImpl?: FetchLike;
|
||||||
@@ -58,7 +79,7 @@ export async function sendJsonRequest(options: {
|
|||||||
throw customError;
|
throw customError;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`${options.errorPrefix}: ${response.status} ${response.statusText}`);
|
throw createStatusError(options.errorPrefix, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
return parseResponse(response);
|
return parseResponse(response);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export type AtlassianConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type CommandOutput<T> = {
|
export type CommandOutput<T> = {
|
||||||
ok: true;
|
ok: boolean;
|
||||||
data: T;
|
data: T;
|
||||||
dryRun?: boolean;
|
dryRun?: boolean;
|
||||||
raw?: unknown;
|
raw?: unknown;
|
||||||
|
|||||||
@@ -58,6 +58,12 @@ If any check fails, stop and return:
|
|||||||
- `pnpm atlassian conf-children --page 12345`
|
- `pnpm atlassian conf-children --page 12345`
|
||||||
- `pnpm atlassian raw --product jira|confluence --method GET|POST|PUT --path ...`
|
- `pnpm atlassian raw --product jira|confluence --method GET|POST|PUT --path ...`
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
- `pnpm atlassian jira-transition --issue ENG-123 --transition 31 --dry-run`
|
||||||
|
- `pnpm atlassian conf-create --space OPS --title "Runbook" --body-file page.storage.html --dry-run`
|
||||||
|
- `pnpm atlassian raw --product jira --method GET --path "/rest/api/3/issue/ENG-123"`
|
||||||
|
|
||||||
## Safety Rules
|
## Safety Rules
|
||||||
|
|
||||||
- Prefer JSON output for machine consumption.
|
- Prefer JSON output for machine consumption.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Command } from "commander";
|
|||||||
import { createConfluenceClient } from "./confluence.js";
|
import { createConfluenceClient } from "./confluence.js";
|
||||||
import { loadConfig } from "./config.js";
|
import { loadConfig } from "./config.js";
|
||||||
import { readWorkspaceFile } from "./files.js";
|
import { readWorkspaceFile } from "./files.js";
|
||||||
|
import { runHealthCheck } from "./health.js";
|
||||||
import { createJiraClient } from "./jira.js";
|
import { createJiraClient } from "./jira.js";
|
||||||
import { writeOutput } from "./output.js";
|
import { writeOutput } from "./output.js";
|
||||||
import { runRawCommand } from "./raw.js";
|
import { runRawCommand } from "./raw.js";
|
||||||
@@ -84,19 +85,11 @@ export function buildProgram(context: CliContext = {}) {
|
|||||||
.command("health")
|
.command("health")
|
||||||
.description("Validate configuration and Atlassian connectivity")
|
.description("Validate configuration and Atlassian connectivity")
|
||||||
.option("--format <format>", "Output format", "json")
|
.option("--format <format>", "Output format", "json")
|
||||||
.action((options) => {
|
.action(async (options) => {
|
||||||
|
const payload = await runHealthCheck(runtime.getConfig(), runtime.fetchImpl);
|
||||||
writeOutput(
|
writeOutput(
|
||||||
runtime.stdout,
|
runtime.stdout,
|
||||||
{
|
payload,
|
||||||
ok: true,
|
|
||||||
data: {
|
|
||||||
baseUrl: runtime.getConfig().baseUrl,
|
|
||||||
jiraBaseUrl: runtime.getConfig().jiraBaseUrl,
|
|
||||||
confluenceBaseUrl: runtime.getConfig().confluenceBaseUrl,
|
|
||||||
defaultProject: runtime.getConfig().defaultProject,
|
|
||||||
defaultSpace: runtime.getConfig().defaultSpace,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
resolveFormat(options.format),
|
resolveFormat(options.format),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
69
skills/atlassian/opencode/scripts/src/health.ts
Normal file
69
skills/atlassian/opencode/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") ?? "";
|
const contentType = response.headers.get("content-type") ?? "";
|
||||||
|
|
||||||
if (contentType.includes("application/json")) {
|
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();
|
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: {
|
export async function sendJsonRequest(options: {
|
||||||
config: AtlassianConfig;
|
config: AtlassianConfig;
|
||||||
fetchImpl?: FetchLike;
|
fetchImpl?: FetchLike;
|
||||||
@@ -58,7 +79,7 @@ export async function sendJsonRequest(options: {
|
|||||||
throw customError;
|
throw customError;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`${options.errorPrefix}: ${response.status} ${response.statusText}`);
|
throw createStatusError(options.errorPrefix, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
return parseResponse(response);
|
return parseResponse(response);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export type AtlassianConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type CommandOutput<T> = {
|
export type CommandOutput<T> = {
|
||||||
ok: true;
|
ok: boolean;
|
||||||
data: T;
|
data: T;
|
||||||
dryRun?: boolean;
|
dryRun?: boolean;
|
||||||
raw?: unknown;
|
raw?: unknown;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Command } from "commander";
|
|||||||
import { createConfluenceClient } from "./confluence.js";
|
import { createConfluenceClient } from "./confluence.js";
|
||||||
import { loadConfig } from "./config.js";
|
import { loadConfig } from "./config.js";
|
||||||
import { readWorkspaceFile } from "./files.js";
|
import { readWorkspaceFile } from "./files.js";
|
||||||
|
import { runHealthCheck } from "./health.js";
|
||||||
import { createJiraClient } from "./jira.js";
|
import { createJiraClient } from "./jira.js";
|
||||||
import { writeOutput } from "./output.js";
|
import { writeOutput } from "./output.js";
|
||||||
import { runRawCommand } from "./raw.js";
|
import { runRawCommand } from "./raw.js";
|
||||||
@@ -84,19 +85,11 @@ export function buildProgram(context: CliContext = {}) {
|
|||||||
.command("health")
|
.command("health")
|
||||||
.description("Validate configuration and Atlassian connectivity")
|
.description("Validate configuration and Atlassian connectivity")
|
||||||
.option("--format <format>", "Output format", "json")
|
.option("--format <format>", "Output format", "json")
|
||||||
.action((options) => {
|
.action(async (options) => {
|
||||||
|
const payload = await runHealthCheck(runtime.getConfig(), runtime.fetchImpl);
|
||||||
writeOutput(
|
writeOutput(
|
||||||
runtime.stdout,
|
runtime.stdout,
|
||||||
{
|
payload,
|
||||||
ok: true,
|
|
||||||
data: {
|
|
||||||
baseUrl: runtime.getConfig().baseUrl,
|
|
||||||
jiraBaseUrl: runtime.getConfig().jiraBaseUrl,
|
|
||||||
confluenceBaseUrl: runtime.getConfig().confluenceBaseUrl,
|
|
||||||
defaultProject: runtime.getConfig().defaultProject,
|
|
||||||
defaultSpace: runtime.getConfig().defaultSpace,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
resolveFormat(options.format),
|
resolveFormat(options.format),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
69
skills/atlassian/shared/scripts/src/health.ts
Normal file
69
skills/atlassian/shared/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") ?? "";
|
const contentType = response.headers.get("content-type") ?? "";
|
||||||
|
|
||||||
if (contentType.includes("application/json")) {
|
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();
|
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: {
|
export async function sendJsonRequest(options: {
|
||||||
config: AtlassianConfig;
|
config: AtlassianConfig;
|
||||||
fetchImpl?: FetchLike;
|
fetchImpl?: FetchLike;
|
||||||
@@ -58,7 +79,7 @@ export async function sendJsonRequest(options: {
|
|||||||
throw customError;
|
throw customError;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`${options.errorPrefix}: ${response.status} ${response.statusText}`);
|
throw createStatusError(options.errorPrefix, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
return parseResponse(response);
|
return parseResponse(response);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export type AtlassianConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type CommandOutput<T> = {
|
export type CommandOutput<T> = {
|
||||||
ok: true;
|
ok: boolean;
|
||||||
data: T;
|
data: T;
|
||||||
dryRun?: boolean;
|
dryRun?: boolean;
|
||||||
raw?: unknown;
|
raw?: unknown;
|
||||||
|
|||||||
131
skills/atlassian/shared/scripts/tests/http.test.ts
Normal file
131
skills/atlassian/shared/scripts/tests/http.test.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import test from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
|
import { sendJsonRequest } from "../src/http.js";
|
||||||
|
import { runCli } from "./helpers.js";
|
||||||
|
|
||||||
|
const baseConfig = {
|
||||||
|
baseUrl: "https://example.atlassian.net",
|
||||||
|
jiraBaseUrl: "https://example.atlassian.net",
|
||||||
|
confluenceBaseUrl: "https://example.atlassian.net",
|
||||||
|
email: "dev@example.com",
|
||||||
|
apiToken: "secret-token",
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseEnv = {
|
||||||
|
ATLASSIAN_BASE_URL: "https://example.atlassian.net",
|
||||||
|
ATLASSIAN_EMAIL: "dev@example.com",
|
||||||
|
ATLASSIAN_API_TOKEN: "secret-token",
|
||||||
|
};
|
||||||
|
|
||||||
|
test("health probes Jira and Confluence independently", async () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
|
||||||
|
const result = await runCli({
|
||||||
|
args: ["health"],
|
||||||
|
env: baseEnv,
|
||||||
|
fetchImpl: async (input) => {
|
||||||
|
const url = typeof input === "string" ? input : input.toString();
|
||||||
|
calls.push(url);
|
||||||
|
|
||||||
|
if (url.endsWith("/rest/api/3/myself")) {
|
||||||
|
return new Response(JSON.stringify({ accountId: "1" }), {
|
||||||
|
status: 200,
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(JSON.stringify({ message: "Forbidden" }), {
|
||||||
|
status: 403,
|
||||||
|
statusText: "Forbidden",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
"https://example.atlassian.net/rest/api/3/myself",
|
||||||
|
"https://example.atlassian.net/wiki/api/v2/spaces?limit=1",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.deepEqual(JSON.parse(result.stdout), {
|
||||||
|
ok: false,
|
||||||
|
data: {
|
||||||
|
baseUrl: "https://example.atlassian.net",
|
||||||
|
jiraBaseUrl: "https://example.atlassian.net",
|
||||||
|
confluenceBaseUrl: "https://example.atlassian.net",
|
||||||
|
products: {
|
||||||
|
jira: {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
},
|
||||||
|
confluence: {
|
||||||
|
ok: false,
|
||||||
|
status: 403,
|
||||||
|
message:
|
||||||
|
"Confluence health check failed: 403 Forbidden - verify product permissions for this account",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("sendJsonRequest maps 401, 403, 404, and 429 to actionable messages", async () => {
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
status: 401,
|
||||||
|
statusText: "Unauthorized",
|
||||||
|
expected: /401 Unauthorized - check ATLASSIAN_EMAIL and ATLASSIAN_API_TOKEN/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 403,
|
||||||
|
statusText: "Forbidden",
|
||||||
|
expected: /403 Forbidden - verify product permissions for this account/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 404,
|
||||||
|
statusText: "Not Found",
|
||||||
|
expected: /404 Not Found - verify the resource identifier or API path/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 429,
|
||||||
|
statusText: "Too Many Requests",
|
||||||
|
expected: /429 Too Many Requests - retry later or reduce request rate/,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
for (const entry of cases) {
|
||||||
|
await assert.rejects(
|
||||||
|
sendJsonRequest({
|
||||||
|
config: baseConfig,
|
||||||
|
url: "https://example.atlassian.net/rest/api/3/issue/ENG-1",
|
||||||
|
method: "GET",
|
||||||
|
errorPrefix: "Jira request failed",
|
||||||
|
fetchImpl: async () =>
|
||||||
|
new Response(JSON.stringify({ message: entry.statusText }), {
|
||||||
|
status: entry.status,
|
||||||
|
statusText: entry.statusText,
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
entry.expected,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("sendJsonRequest reports malformed JSON responses clearly", async () => {
|
||||||
|
await assert.rejects(
|
||||||
|
sendJsonRequest({
|
||||||
|
config: baseConfig,
|
||||||
|
url: "https://example.atlassian.net/rest/api/3/issue/ENG-1",
|
||||||
|
method: "GET",
|
||||||
|
errorPrefix: "Jira request failed",
|
||||||
|
fetchImpl: async () =>
|
||||||
|
new Response("{", {
|
||||||
|
status: 200,
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
/Malformed JSON response from Atlassian API/,
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user