mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-22 14:24:13 +08:00
* feat: add Azure AI Foundry provider support Add support for Azure AI Foundry as a fourth cloud provider option alongside Anthropic API, AWS Bedrock, and Google Vertex AI. Changes: - Add use_foundry input to enable Azure AI Foundry authentication - Add Azure environment variables (ANTHROPIC_FOUNDRY_RESOURCE, ANTHROPIC_FOUNDRY_API_KEY, ANTHROPIC_FOUNDRY_BASE_URL) - Support automatic base URL construction from resource name - Add validation logic with mutual exclusivity checks for all providers - Add comprehensive test coverage (7 Azure-specific tests, 3 mutual exclusivity tests) - Add complete Azure AI Foundry documentation with OIDC and API key authentication - Update README to reference Azure AI Foundry support Features: - Primary authentication via Microsoft Entra ID (OIDC) using azure/login action - Optional API key authentication fallback - Custom model deployment name support via ANTHROPIC_DEFAULT_*_MODEL variables - Clear validation error messages for missing configuration All tests pass (25 validation tests total). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor: rename Azure AI Foundry to Microsoft Foundry and remove API key support - Rename all references from "Azure AI Foundry" to "Microsoft Foundry" - Remove ANTHROPIC_FOUNDRY_API_KEY support (OIDC only) - Update documentation to reflect OIDC-only authentication - Update tests to remove API key test case 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * docs: simplify Microsoft Foundry setup and remove URL auto-construction - Link to official docs instead of duplicating setup instructions - Remove automatic base URL construction from resource name - Pass ANTHROPIC_FOUNDRY_BASE_URL as-is 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
337 lines
14 KiB
TypeScript
337 lines
14 KiB
TypeScript
#!/usr/bin/env bun
|
|
|
|
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
import { validateEnvironmentVariables } from "../src/validate-env";
|
|
|
|
describe("validateEnvironmentVariables", () => {
|
|
let originalEnv: NodeJS.ProcessEnv;
|
|
|
|
beforeEach(() => {
|
|
// Save the original environment
|
|
originalEnv = { ...process.env };
|
|
// Clear relevant environment variables
|
|
delete process.env.ANTHROPIC_API_KEY;
|
|
delete process.env.CLAUDE_CODE_USE_BEDROCK;
|
|
delete process.env.CLAUDE_CODE_USE_VERTEX;
|
|
delete process.env.CLAUDE_CODE_USE_FOUNDRY;
|
|
delete process.env.AWS_REGION;
|
|
delete process.env.AWS_ACCESS_KEY_ID;
|
|
delete process.env.AWS_SECRET_ACCESS_KEY;
|
|
delete process.env.AWS_SESSION_TOKEN;
|
|
delete process.env.AWS_BEARER_TOKEN_BEDROCK;
|
|
delete process.env.ANTHROPIC_BEDROCK_BASE_URL;
|
|
delete process.env.ANTHROPIC_VERTEX_PROJECT_ID;
|
|
delete process.env.CLOUD_ML_REGION;
|
|
delete process.env.GOOGLE_APPLICATION_CREDENTIALS;
|
|
delete process.env.ANTHROPIC_VERTEX_BASE_URL;
|
|
delete process.env.ANTHROPIC_FOUNDRY_RESOURCE;
|
|
delete process.env.ANTHROPIC_FOUNDRY_BASE_URL;
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Restore the original environment
|
|
process.env = originalEnv;
|
|
});
|
|
|
|
describe("Direct Anthropic API", () => {
|
|
test("should pass when ANTHROPIC_API_KEY is provided", () => {
|
|
process.env.ANTHROPIC_API_KEY = "test-api-key";
|
|
|
|
expect(() => validateEnvironmentVariables()).not.toThrow();
|
|
});
|
|
|
|
test("should fail when ANTHROPIC_API_KEY is missing", () => {
|
|
expect(() => validateEnvironmentVariables()).toThrow(
|
|
"Either ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN is required when using direct Anthropic API.",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("AWS Bedrock", () => {
|
|
test("should pass when all required Bedrock variables are provided", () => {
|
|
process.env.CLAUDE_CODE_USE_BEDROCK = "1";
|
|
process.env.AWS_REGION = "us-east-1";
|
|
process.env.AWS_ACCESS_KEY_ID = "test-access-key";
|
|
process.env.AWS_SECRET_ACCESS_KEY = "test-secret-key";
|
|
|
|
expect(() => validateEnvironmentVariables()).not.toThrow();
|
|
});
|
|
|
|
test("should pass with optional Bedrock variables", () => {
|
|
process.env.CLAUDE_CODE_USE_BEDROCK = "1";
|
|
process.env.AWS_REGION = "us-east-1";
|
|
process.env.AWS_ACCESS_KEY_ID = "test-access-key";
|
|
process.env.AWS_SECRET_ACCESS_KEY = "test-secret-key";
|
|
process.env.AWS_SESSION_TOKEN = "test-session-token";
|
|
process.env.ANTHROPIC_BEDROCK_BASE_URL = "https://test.url";
|
|
|
|
expect(() => validateEnvironmentVariables()).not.toThrow();
|
|
});
|
|
|
|
test("should construct Bedrock base URL from AWS_REGION when ANTHROPIC_BEDROCK_BASE_URL is not provided", () => {
|
|
// This test verifies our action.yml change, which constructs:
|
|
// ANTHROPIC_BEDROCK_BASE_URL: ${{ env.ANTHROPIC_BEDROCK_BASE_URL || (env.AWS_REGION && format('https://bedrock-runtime.{0}.amazonaws.com', env.AWS_REGION)) }}
|
|
|
|
process.env.CLAUDE_CODE_USE_BEDROCK = "1";
|
|
process.env.AWS_REGION = "us-west-2";
|
|
process.env.AWS_ACCESS_KEY_ID = "test-access-key";
|
|
process.env.AWS_SECRET_ACCESS_KEY = "test-secret-key";
|
|
// ANTHROPIC_BEDROCK_BASE_URL is intentionally not set
|
|
|
|
// The actual URL construction happens in the composite action in action.yml
|
|
// This test is a placeholder to document the behavior
|
|
expect(() => validateEnvironmentVariables()).not.toThrow();
|
|
|
|
// In the actual action, ANTHROPIC_BEDROCK_BASE_URL would be:
|
|
// https://bedrock-runtime.us-west-2.amazonaws.com
|
|
});
|
|
|
|
test("should fail when AWS_REGION is missing", () => {
|
|
process.env.CLAUDE_CODE_USE_BEDROCK = "1";
|
|
process.env.AWS_ACCESS_KEY_ID = "test-access-key";
|
|
process.env.AWS_SECRET_ACCESS_KEY = "test-secret-key";
|
|
|
|
expect(() => validateEnvironmentVariables()).toThrow(
|
|
"AWS_REGION is required when using AWS Bedrock.",
|
|
);
|
|
});
|
|
|
|
test("should fail when only AWS_SECRET_ACCESS_KEY is provided without bearer token", () => {
|
|
process.env.CLAUDE_CODE_USE_BEDROCK = "1";
|
|
process.env.AWS_REGION = "us-east-1";
|
|
process.env.AWS_SECRET_ACCESS_KEY = "test-secret-key";
|
|
|
|
expect(() => validateEnvironmentVariables()).toThrow(
|
|
"Either AWS_BEARER_TOKEN_BEDROCK or both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are required when using AWS Bedrock.",
|
|
);
|
|
});
|
|
|
|
test("should fail when only AWS_ACCESS_KEY_ID is provided without bearer token", () => {
|
|
process.env.CLAUDE_CODE_USE_BEDROCK = "1";
|
|
process.env.AWS_REGION = "us-east-1";
|
|
process.env.AWS_ACCESS_KEY_ID = "test-access-key";
|
|
|
|
expect(() => validateEnvironmentVariables()).toThrow(
|
|
"Either AWS_BEARER_TOKEN_BEDROCK or both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are required when using AWS Bedrock.",
|
|
);
|
|
});
|
|
|
|
test("should pass when AWS_BEARER_TOKEN_BEDROCK is provided instead of access keys", () => {
|
|
process.env.CLAUDE_CODE_USE_BEDROCK = "1";
|
|
process.env.AWS_REGION = "us-east-1";
|
|
process.env.AWS_BEARER_TOKEN_BEDROCK = "test-bearer-token";
|
|
|
|
expect(() => validateEnvironmentVariables()).not.toThrow();
|
|
});
|
|
|
|
test("should pass when both bearer token and access keys are provided", () => {
|
|
process.env.CLAUDE_CODE_USE_BEDROCK = "1";
|
|
process.env.AWS_REGION = "us-east-1";
|
|
process.env.AWS_BEARER_TOKEN_BEDROCK = "test-bearer-token";
|
|
process.env.AWS_ACCESS_KEY_ID = "test-access-key";
|
|
process.env.AWS_SECRET_ACCESS_KEY = "test-secret-key";
|
|
|
|
expect(() => validateEnvironmentVariables()).not.toThrow();
|
|
});
|
|
|
|
test("should fail when no authentication method is provided", () => {
|
|
process.env.CLAUDE_CODE_USE_BEDROCK = "1";
|
|
process.env.AWS_REGION = "us-east-1";
|
|
|
|
expect(() => validateEnvironmentVariables()).toThrow(
|
|
"Either AWS_BEARER_TOKEN_BEDROCK or both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are required when using AWS Bedrock.",
|
|
);
|
|
});
|
|
|
|
test("should report missing region and authentication", () => {
|
|
process.env.CLAUDE_CODE_USE_BEDROCK = "1";
|
|
|
|
expect(() => validateEnvironmentVariables()).toThrow(
|
|
/AWS_REGION is required when using AWS Bedrock.*Either AWS_BEARER_TOKEN_BEDROCK or both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are required when using AWS Bedrock/s,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("Google Vertex AI", () => {
|
|
test("should pass when all required Vertex variables are provided", () => {
|
|
process.env.CLAUDE_CODE_USE_VERTEX = "1";
|
|
process.env.ANTHROPIC_VERTEX_PROJECT_ID = "test-project";
|
|
process.env.CLOUD_ML_REGION = "us-central1";
|
|
|
|
expect(() => validateEnvironmentVariables()).not.toThrow();
|
|
});
|
|
|
|
test("should pass with optional Vertex variables", () => {
|
|
process.env.CLAUDE_CODE_USE_VERTEX = "1";
|
|
process.env.ANTHROPIC_VERTEX_PROJECT_ID = "test-project";
|
|
process.env.CLOUD_ML_REGION = "us-central1";
|
|
process.env.GOOGLE_APPLICATION_CREDENTIALS = "/path/to/creds.json";
|
|
process.env.ANTHROPIC_VERTEX_BASE_URL = "https://test.url";
|
|
|
|
expect(() => validateEnvironmentVariables()).not.toThrow();
|
|
});
|
|
|
|
test("should fail when ANTHROPIC_VERTEX_PROJECT_ID is missing", () => {
|
|
process.env.CLAUDE_CODE_USE_VERTEX = "1";
|
|
process.env.CLOUD_ML_REGION = "us-central1";
|
|
|
|
expect(() => validateEnvironmentVariables()).toThrow(
|
|
"ANTHROPIC_VERTEX_PROJECT_ID is required when using Google Vertex AI.",
|
|
);
|
|
});
|
|
|
|
test("should fail when CLOUD_ML_REGION is missing", () => {
|
|
process.env.CLAUDE_CODE_USE_VERTEX = "1";
|
|
process.env.ANTHROPIC_VERTEX_PROJECT_ID = "test-project";
|
|
|
|
expect(() => validateEnvironmentVariables()).toThrow(
|
|
"CLOUD_ML_REGION is required when using Google Vertex AI.",
|
|
);
|
|
});
|
|
|
|
test("should report all missing Vertex variables", () => {
|
|
process.env.CLAUDE_CODE_USE_VERTEX = "1";
|
|
|
|
expect(() => validateEnvironmentVariables()).toThrow(
|
|
/ANTHROPIC_VERTEX_PROJECT_ID is required when using Google Vertex AI.*CLOUD_ML_REGION is required when using Google Vertex AI/s,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("Microsoft Foundry", () => {
|
|
test("should pass when ANTHROPIC_FOUNDRY_RESOURCE is provided", () => {
|
|
process.env.CLAUDE_CODE_USE_FOUNDRY = "1";
|
|
process.env.ANTHROPIC_FOUNDRY_RESOURCE = "test-resource";
|
|
|
|
expect(() => validateEnvironmentVariables()).not.toThrow();
|
|
});
|
|
|
|
test("should pass when ANTHROPIC_FOUNDRY_BASE_URL is provided", () => {
|
|
process.env.CLAUDE_CODE_USE_FOUNDRY = "1";
|
|
process.env.ANTHROPIC_FOUNDRY_BASE_URL =
|
|
"https://test-resource.services.ai.azure.com";
|
|
|
|
expect(() => validateEnvironmentVariables()).not.toThrow();
|
|
});
|
|
|
|
test("should pass when both resource and base URL are provided", () => {
|
|
process.env.CLAUDE_CODE_USE_FOUNDRY = "1";
|
|
process.env.ANTHROPIC_FOUNDRY_RESOURCE = "test-resource";
|
|
process.env.ANTHROPIC_FOUNDRY_BASE_URL =
|
|
"https://custom.services.ai.azure.com";
|
|
|
|
expect(() => validateEnvironmentVariables()).not.toThrow();
|
|
});
|
|
|
|
test("should construct Foundry base URL from resource name when ANTHROPIC_FOUNDRY_BASE_URL is not provided", () => {
|
|
// This test verifies our action.yml change, which constructs:
|
|
// ANTHROPIC_FOUNDRY_BASE_URL: ${{ env.ANTHROPIC_FOUNDRY_BASE_URL || (env.ANTHROPIC_FOUNDRY_RESOURCE && format('https://{0}.services.ai.azure.com', env.ANTHROPIC_FOUNDRY_RESOURCE)) }}
|
|
|
|
process.env.CLAUDE_CODE_USE_FOUNDRY = "1";
|
|
process.env.ANTHROPIC_FOUNDRY_RESOURCE = "my-foundry-resource";
|
|
// ANTHROPIC_FOUNDRY_BASE_URL is intentionally not set
|
|
|
|
// The actual URL construction happens in the composite action in action.yml
|
|
// This test is a placeholder to document the behavior
|
|
expect(() => validateEnvironmentVariables()).not.toThrow();
|
|
|
|
// In the actual action, ANTHROPIC_FOUNDRY_BASE_URL would be:
|
|
// https://my-foundry-resource.services.ai.azure.com
|
|
});
|
|
|
|
test("should fail when neither ANTHROPIC_FOUNDRY_RESOURCE nor ANTHROPIC_FOUNDRY_BASE_URL is provided", () => {
|
|
process.env.CLAUDE_CODE_USE_FOUNDRY = "1";
|
|
|
|
expect(() => validateEnvironmentVariables()).toThrow(
|
|
"Either ANTHROPIC_FOUNDRY_RESOURCE or ANTHROPIC_FOUNDRY_BASE_URL is required when using Microsoft Foundry.",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("Multiple providers", () => {
|
|
test("should fail when both Bedrock and Vertex are enabled", () => {
|
|
process.env.CLAUDE_CODE_USE_BEDROCK = "1";
|
|
process.env.CLAUDE_CODE_USE_VERTEX = "1";
|
|
// Provide all required vars to isolate the mutual exclusion error
|
|
process.env.AWS_REGION = "us-east-1";
|
|
process.env.AWS_ACCESS_KEY_ID = "test-access-key";
|
|
process.env.AWS_SECRET_ACCESS_KEY = "test-secret-key";
|
|
process.env.ANTHROPIC_VERTEX_PROJECT_ID = "test-project";
|
|
process.env.CLOUD_ML_REGION = "us-central1";
|
|
|
|
expect(() => validateEnvironmentVariables()).toThrow(
|
|
"Cannot use multiple providers simultaneously. Please set only one of: CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_USE_VERTEX, or CLAUDE_CODE_USE_FOUNDRY.",
|
|
);
|
|
});
|
|
|
|
test("should fail when both Bedrock and Foundry are enabled", () => {
|
|
process.env.CLAUDE_CODE_USE_BEDROCK = "1";
|
|
process.env.CLAUDE_CODE_USE_FOUNDRY = "1";
|
|
// Provide all required vars to isolate the mutual exclusion error
|
|
process.env.AWS_REGION = "us-east-1";
|
|
process.env.AWS_ACCESS_KEY_ID = "test-access-key";
|
|
process.env.AWS_SECRET_ACCESS_KEY = "test-secret-key";
|
|
process.env.ANTHROPIC_FOUNDRY_RESOURCE = "test-resource";
|
|
|
|
expect(() => validateEnvironmentVariables()).toThrow(
|
|
"Cannot use multiple providers simultaneously. Please set only one of: CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_USE_VERTEX, or CLAUDE_CODE_USE_FOUNDRY.",
|
|
);
|
|
});
|
|
|
|
test("should fail when both Vertex and Foundry are enabled", () => {
|
|
process.env.CLAUDE_CODE_USE_VERTEX = "1";
|
|
process.env.CLAUDE_CODE_USE_FOUNDRY = "1";
|
|
// Provide all required vars to isolate the mutual exclusion error
|
|
process.env.ANTHROPIC_VERTEX_PROJECT_ID = "test-project";
|
|
process.env.CLOUD_ML_REGION = "us-central1";
|
|
process.env.ANTHROPIC_FOUNDRY_RESOURCE = "test-resource";
|
|
|
|
expect(() => validateEnvironmentVariables()).toThrow(
|
|
"Cannot use multiple providers simultaneously. Please set only one of: CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_USE_VERTEX, or CLAUDE_CODE_USE_FOUNDRY.",
|
|
);
|
|
});
|
|
|
|
test("should fail when all three providers are enabled", () => {
|
|
process.env.CLAUDE_CODE_USE_BEDROCK = "1";
|
|
process.env.CLAUDE_CODE_USE_VERTEX = "1";
|
|
process.env.CLAUDE_CODE_USE_FOUNDRY = "1";
|
|
// Provide all required vars to isolate the mutual exclusion error
|
|
process.env.AWS_REGION = "us-east-1";
|
|
process.env.AWS_ACCESS_KEY_ID = "test-access-key";
|
|
process.env.AWS_SECRET_ACCESS_KEY = "test-secret-key";
|
|
process.env.ANTHROPIC_VERTEX_PROJECT_ID = "test-project";
|
|
process.env.CLOUD_ML_REGION = "us-central1";
|
|
process.env.ANTHROPIC_FOUNDRY_RESOURCE = "test-resource";
|
|
|
|
expect(() => validateEnvironmentVariables()).toThrow(
|
|
"Cannot use multiple providers simultaneously. Please set only one of: CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_USE_VERTEX, or CLAUDE_CODE_USE_FOUNDRY.",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("Error message formatting", () => {
|
|
test("should format error message properly with multiple errors", () => {
|
|
process.env.CLAUDE_CODE_USE_BEDROCK = "1";
|
|
// Missing all required Bedrock vars
|
|
|
|
let error: Error | undefined;
|
|
try {
|
|
validateEnvironmentVariables();
|
|
} catch (e) {
|
|
error = e as Error;
|
|
}
|
|
|
|
expect(error).toBeDefined();
|
|
expect(error!.message).toMatch(
|
|
/^Environment variable validation failed:/,
|
|
);
|
|
expect(error!.message).toContain(
|
|
" - AWS_REGION is required when using AWS Bedrock.",
|
|
);
|
|
expect(error!.message).toContain(
|
|
" - Either AWS_BEARER_TOKEN_BEDROCK or both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are required when using AWS Bedrock.",
|
|
);
|
|
});
|
|
});
|
|
});
|