diff --git a/.claude/skills/update-changelog.md b/.claude/skills/update-changelog.md new file mode 100644 index 0000000..0878f17 --- /dev/null +++ b/.claude/skills/update-changelog.md @@ -0,0 +1,59 @@ +I need you to analyze the code changes in this SDK repository and update the CHANGELOG.md file. + +Changes were made by syncing the OpenAPI spec from managed-service. + +## Environment Variables + +The following environment variables are set. Read each one individually +using `printenv ` (one variable per command): + +- MANAGED_SERVICE_PR_URL — The managed-service PR URL that triggered this sync +- MANAGED_SERVICE_SHA — (optional) The managed-service commit SHA if the PR was merged + +## Instructions + +Please follow these steps: + +1. Use git diff to see all changes in the working tree +2. Read the current CHANGELOG.md file to understand its existing structure +3. Update CHANGELOG.md following Keep a Changelog conventions (https://keepachangelog.com/): + - Add all new entries under the [Unreleased] section + - For each change, choose the appropriate subsection: + - Added: for new features + - Changed: for changes in existing functionality + - Deprecated: for soon-to-be removed features + - Removed: for now removed features + - Fixed: for any bug fixes + - Security: in case of vulnerabilities + - If a subsection header (e.g., ### Added) does not exist under [Unreleased], create it + - If the subsection header already exists, add new entries at the top of that subsection + - Maintain the subsection order: Added, Changed, Deprecated, Removed, Fixed, Security +4. For each change, determine if it's a breaking change for SDK consumers +5. If a change is breaking, prefix that specific entry with "Breaking Change: " +6. Write entries as bullet points using past tense (e.g., "Added X field", not "Add X field") + +IMPORTANT: Only modify CHANGELOG.md. Do not modify any other files including generated code, spec files, or configuration files. + +After updating CHANGELOG.md, you MUST output structured metadata in the following format. +This is critical - end your response with these exact markers: + +---COMMIT_TITLE--- + +---COMMIT_BODY--- + + +Managed-service-pr-url: $MANAGED_SERVICE_PR_URL +---PR_TITLE--- + +---PR_DESCRIPTION--- + + +IMPORTANT GUIDANCE FOR COMMIT AND PR TITLES: +- Describe the actual SDK/API changes from a user perspective +- NEVER write titles like "Update CHANGELOG.md" - the changelog update is just a side effect, not the main change +- If changes are focused on one area, describe that (e.g., "Add MFA audit log actions", "Update invoice field descriptions") +- If there are multiple unrelated changes that can't fit in 72 characters, use "Sync OpenAPI spec from managed-service" +- Focus on WHAT changed in the API/SDK, not the process of updating files + +The commit body MUST include the trailer "Managed-service-pr-url: $MANAGED_SERVICE_PR_URL" exactly as shown. +Do not add any text after the PR description section. diff --git a/.github/workflows/openapi-sync.yml b/.github/workflows/openapi-sync.yml new file mode 100644 index 0000000..1cf69c2 --- /dev/null +++ b/.github/workflows/openapi-sync.yml @@ -0,0 +1,60 @@ +name: Sync OpenAPI Spec from Managed Service + +on: + workflow_dispatch: + inputs: + event_type: + description: 'Event type (openapi-spec-changed or openapi-spec-merged)' + required: true + type: choice + options: + - openapi-spec-changed + - openapi-spec-merged + pr_url: + description: 'Managed-service PR URL' + required: true + type: string + sha: + description: 'Managed-service commit SHA (required for merged events)' + required: false + type: string + +# Required permissions for the workflow +permissions: + contents: write + pull-requests: write + id-token: write + +jobs: + openapi-sync: + runs-on: ubuntu-latest + env: + EVENT_TYPE: ${{ github.event.inputs.event_type }} + MANAGED_SERVICE_PR_URL: ${{ github.event.inputs.pr_url }} + MANAGED_SERVICE_SHA: ${{ github.event.inputs.sha }} + MANAGED_SERVICE_TOKEN: ${{ secrets.MANAGED_SERVICE_TOKEN }} + FORK_OWNER: crl-gh-actions-pr-bot + FORK_PUSH_TOKEN: ${{ secrets.FORK_PUSH_TOKEN }} + CREATE_PR_TOKEN: ${{ github.token }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + token: ${{ github.token }} + persist-credentials: false + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: go.mod + + - name: Authenticate to Google Cloud (Vertex AI) + uses: google-github-actions/auth@v3 + with: + project_id: vertex-model-runners + service_account: vertex-ai-user@vertex-model-runners.iam.gserviceaccount.com + workload_identity_provider: projects/108106229451/locations/global/workloadIdentityPools/vertex-ai-user/providers/vertex-ai-user + + - name: Run OpenAPI sync workflow + run: scripts/openapi-sync.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 83d84e8..e3683df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Added automated workflow for OpenAPI spec synchronization from managed-service. + Supports both `openapi-spec-changed` (creates/updates PRs) and `openapi-spec-merged` + (updates PRs with exact merged commit) event types. + ## [7.1.0] - 2026-04-14 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index bf74797..841dd84 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,3 +3,22 @@ ## GitHub Actions - When writing or modifying GitHub Actions workflows, always look up the latest major version of each action before using it. Do not assume you know the current version. + +## Versioning + +CHANGELOG.md is the single source of truth for the version. When committing changes, add a user-facing entry under the `## [Unreleased]` section describing what changed. Follow the [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) format with these subsections: `### Added`, `### Changed`, `### Deprecated`, `### Removed`, `### Fixed`, `### Security`. Prefix breaking changes with "Breaking Change: ". + +When releasing a new version, move `[Unreleased]` entries in CHANGELOG.md to a new `[x.y.z]` section. + +### Writing good changelog entries + +- Be brief. One sentence per entry. +- Write from the user's perspective. Describe what they can now do, not what code changed. +- Start each entry with a verb (e.g. Add, Update, Fix, Remove). +- Group related changes into one entry when they form a single feature (e.g. "Add support for folder management operations"). +- Don't list individual fields, models, or implementation details unless they're the primary change. +- For OpenAPI spec syncs, mention the new operations and capabilities, not the sync process itself. The managed-service PR reference belongs in the commit message, +not the changelog. + +Good: `Add support for JWT issuer management operations (CreateJwtIssuer, GetJwtIssuer, ListJwtIssuers, UpdateJwtIssuer, DeleteJwtIssuer).` +Bad: `Sync OpenAPI spec from managed-service PR #1234 and add JwtIssuer CRUD operations with Audience, IssuerUrl, JwksUri, and ClaimMap fields.` diff --git a/Makefile b/Makefile index ff08288..f31f021 100644 --- a/Makefile +++ b/Makefile @@ -6,9 +6,9 @@ TOOLPATH := $(abspath bin) .PHONY: generate-openapi-client generate-openapi-client: bin/goimports rm -rf ./pkg/client/*.go docs - docker-compose -f ./docker-compose.yml run --rm \ + docker compose -f ./docker-compose.yml run --rm \ jq -M -f filter.jq internal/spec/openapi.json > internal/openapi-generator/api/openapi-modified.json - docker-compose -f ./docker-compose.yml run --rm \ + docker compose -f ./docker-compose.yml run --rm \ openapi-generator generate \ -g go \ -i internal/openapi-generator/api/openapi-modified.json \ diff --git a/scripts/lib/find-sdk-pr.sh b/scripts/lib/find-sdk-pr.sh new file mode 100755 index 0000000..2b81cb0 --- /dev/null +++ b/scripts/lib/find-sdk-pr.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# Find an SDK PR that corresponds to a managed-service PR URL. +# +# This searches all open PRs in the current repository and inspects +# all commits in each PR to find one containing a trailer: +# Managed-service-pr-url: +# +# Usage: +# find_sdk_pr +# +# Exports on success: +# SDK_PR_NUMBER: The SDK PR number +# SDK_PR_BRANCH: The SDK PR branch name +# +# Returns: +# 0 if found, 1 if not found + +# Get the script directory to find helper functions +LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$LIB_DIR/logging.sh" + +find_sdk_pr() { + local managed_service_pr_url="$1" + + if [[ -z "${managed_service_pr_url:-}" ]]; then + log_error "managed_service_pr_url is required" + return 1 + fi + + if [[ -z "${CREATE_PR_TOKEN:-}" ]]; then + log_error "CREATE_PR_TOKEN environment variable is not set" + return 1 + fi + + if [[ -z "${GITHUB_REPOSITORY:-}" ]]; then + log_error "GITHUB_REPOSITORY environment variable is not set" + return 1 + fi + + log_info "Searching for SDK PR corresponding to: $managed_service_pr_url" + + export GH_TOKEN="$CREATE_PR_TOKEN" + + # Get all open SDK PRs + local sdk_open_prs + sdk_open_prs=$(gh pr list --repo "$GITHUB_REPOSITORY" --state open --json number --jq '.[].number') + + for sdk_pr_num in $sdk_open_prs; do + log_info "Checking SDK PR #$sdk_pr_num" + + # Get all commits in this SDK PR + local sdk_commits + sdk_commits=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$sdk_pr_num/commits" --jq '.[].sha') + + # Check each commit's message for the trailer + for sdk_commit_sha in $sdk_commits; do + local sdk_commit_msg + sdk_commit_msg=$(gh api "repos/$GITHUB_REPOSITORY/git/commits/$sdk_commit_sha" --jq '.message') + + if echo "$sdk_commit_msg" | grep --quiet --fixed-strings "Managed-service-pr-url: $managed_service_pr_url"; then + log_info "Found matching SDK PR #$sdk_pr_num (commit $sdk_commit_sha)" + + # Get the SDK PR branch + local sdk_pr_branch + sdk_pr_branch=$(gh pr view "$sdk_pr_num" --repo "$GITHUB_REPOSITORY" --json headRefName --jq '.headRefName') + + # Export results + export SDK_PR_NUMBER="$sdk_pr_num" + export SDK_PR_BRANCH="$sdk_pr_branch" + return 0 + fi + done + done + + log_info "No matching SDK PR found" + return 1 +} diff --git a/scripts/lib/git-askpass.sh b/scripts/lib/git-askpass.sh new file mode 100755 index 0000000..6bd3f3f --- /dev/null +++ b/scripts/lib/git-askpass.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# GIT_ASKPASS script for fork authentication. Reads credentials from +# environment variables so the token is never written to disk. +set -euo pipefail + +# Get the script directory to find helper functions +LIB_DIR="$(cd "$(dirname "$0")" && pwd)" +source "$LIB_DIR/logging.sh" + +if [ -z "$GIT_FORK_USER" ] || [ -z "$GIT_FORK_PASSWORD" ]; then + log_error "GIT_FORK_USER and GIT_FORK_PASSWORD must be set" + exit 1 +fi + +case "$1" in + Username*) echo "$GIT_FORK_USER" ;; + Password*) echo "$GIT_FORK_PASSWORD" ;; +esac diff --git a/scripts/lib/invoke-claude-changelog.sh b/scripts/lib/invoke-claude-changelog.sh new file mode 100755 index 0000000..9c2662a --- /dev/null +++ b/scripts/lib/invoke-claude-changelog.sh @@ -0,0 +1,139 @@ +#!/bin/bash +set -euo pipefail + +# Invoke Claude to update changelog and generate commit/PR metadata. +# +# This script calls the Claude CLI to analyze code changes, update CHANGELOG.md, +# and return structured metadata for commit messages and PR descriptions. +# +# Usage: +# invoke-claude-changelog.sh [managed_service_sha] +# +# Outputs: +# commit_title, commit_body, pr_title, pr_description to stdout +# Exit code 0 on success, 1 on failure + +# Get the script directory to find helper functions +LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$LIB_DIR/logging.sh" + +MANAGED_SERVICE_PR_URL="$1" +MANAGED_SERVICE_SHA="${2:-}" + +if [[ -z "${MANAGED_SERVICE_PR_URL:-}" ]]; then + log_error "managed_service_pr_url is required" + exit 1 +fi + +# Check that claude CLI is available +if ! command -v claude >/dev/null 2>&1; then + log_error "claude CLI is not installed or not in PATH" + exit 1 +fi + +log_info "Invoking Claude to analyze changes and update changelog..." + +PROMPT_TEMPLATE="$LIB_DIR/../../.claude/skills/update-changelog.md" + +if [[ ! -f "$PROMPT_TEMPLATE" ]]; then + log_error "Prompt template not found at $PROMPT_TEMPLATE" + exit 1 +fi + +# Export environment variables for Claude to access +export MANAGED_SERVICE_PR_URL +export MANAGED_SERVICE_SHA + +# Copy prompt template to temp file +PROMPT_FILE=$(mktemp) +cp "$PROMPT_TEMPLATE" "$PROMPT_FILE" + +# Add SHA trailer instruction if provided +if [[ -n "${MANAGED_SERVICE_SHA:-}" ]]; then + cat >> "$PROMPT_FILE" <<'EOF' + +IMPORTANT: The commit body must also include this trailer immediately after the Managed-service-pr-url trailer: +Managed-service-commit-SHA: $MANAGED_SERVICE_SHA +EOF +fi + +# Output file for Claude's JSON response +OUTPUT_FILE=$(mktemp) + +# Call Claude CLI +log_info "Calling Claude CLI..." +claude --print \ + --model claude-opus-4-6 \ + --output-format json \ + --max-turns 10 \ + --allowedTools "Read,Grep,Glob,Edit,Bash(git:*),Bash(printenv:*)" \ + < "$PROMPT_FILE" \ + > "$OUTPUT_FILE" 2>&1 || { + log_error "Claude CLI failed" + cat "$OUTPUT_FILE" >&2 + rm --force "$PROMPT_FILE" "$OUTPUT_FILE" + exit 1 + } + +# Extract result text from JSON +RESULT_TEXT=$(jq --raw-output '.result // empty' "$OUTPUT_FILE") + +if [[ -z "${RESULT_TEXT:-}" ]]; then + log_error "Claude produced empty result" + cat "$OUTPUT_FILE" >&2 + rm --force "$PROMPT_FILE" "$OUTPUT_FILE" + exit 1 +fi + +log_info "Claude response received, extracting metadata..." + +# Extract commit title (between ---COMMIT_TITLE--- and ---COMMIT_BODY---) +COMMIT_TITLE=$(echo "$RESULT_TEXT" | awk '/---COMMIT_TITLE---/{flag=1;next}/---COMMIT_BODY---/{flag=0}flag' | grep --invert-match '^[[:space:]]*$' | head --lines=1) + +# Extract commit body (between ---COMMIT_BODY--- and ---PR_TITLE---) +COMMIT_BODY=$(echo "$RESULT_TEXT" | awk '/---COMMIT_BODY---/{flag=1;next}/---PR_TITLE---/{flag=0}flag') + +# Extract PR title (between ---PR_TITLE--- and ---PR_DESCRIPTION---) +PR_TITLE=$(echo "$RESULT_TEXT" | awk '/---PR_TITLE---/{flag=1;next}/---PR_DESCRIPTION---/{flag=0}flag' | grep --invert-match '^[[:space:]]*$' | head --lines=1) + +# Extract PR description (after ---PR_DESCRIPTION---) +PR_DESCRIPTION=$(echo "$RESULT_TEXT" | awk '/---PR_DESCRIPTION---/{flag=1;next}flag') + +# Validate extractions +if [[ -z "${COMMIT_TITLE:-}" ]]; then + log_error "Could not extract commit title from Claude output" + echo "Claude output:" >&2 + echo "$RESULT_TEXT" >&2 + rm --force "$PROMPT_FILE" "$OUTPUT_FILE" + exit 1 +fi + +if [[ -z "${PR_TITLE:-}" ]]; then + log_error "Could not extract PR title from Claude output" + echo "Claude output:" >&2 + echo "$RESULT_TEXT" >&2 + rm --force "$PROMPT_FILE" "$OUTPUT_FILE" + exit 1 +fi + +# Output results using GitHub Actions output format with delimiters +echo "commit_title<&2 +} + +# Output a warning message using GitHub Actions workflow command format +log_warning() { + local message="$1" + echo "::warning::$message" >&2 +} + +# Output a notice message using GitHub Actions workflow command format +log_notice() { + local message="$1" + echo "::notice::$message" +} diff --git a/scripts/lib/validation.sh b/scripts/lib/validation.sh new file mode 100755 index 0000000..8e45c77 --- /dev/null +++ b/scripts/lib/validation.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Validation helper functions + +# Check if required commands are available in PATH +# Usage: check_required_commands "cmd1" "cmd2" "cmd3" +check_required_commands() { + local missing_commands=() + + for cmd in "$@"; do + if ! command -v "$cmd" >/dev/null 2>&1; then + missing_commands+=("$cmd") + fi + done + + if [[ ${#missing_commands[@]} -gt 0 ]]; then + log_error "Required command(s) not found in PATH: ${missing_commands[*]}" + return 1 + fi + + return 0 +} diff --git a/scripts/openapi-sync.sh b/scripts/openapi-sync.sh new file mode 100755 index 0000000..010d3c8 --- /dev/null +++ b/scripts/openapi-sync.sh @@ -0,0 +1,388 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Sync OpenAPI spec from managed-service PR and regenerate client. +# +# This script orchestrates the entire OpenAPI sync workflow: +# 1. Validates event payload +# 2. Resolves managed-service PR metadata +# 3. Finds or creates SDK branch +# 4. Syncs OpenAPI spec and regenerates client +# 5. Updates changelog with Claude +# 6. Commits and pushes changes +# 7. Creates or updates SDK PR +# +# Usage: +# sync-openapi.sh +# +# Required environment variables: +# EVENT_TYPE: "openapi-spec-changed" or "openapi-spec-merged" +# MANAGED_SERVICE_PR_URL: URL of the managed-service PR +# MANAGED_SERVICE_SHA: Commit SHA (required for openapi-spec-merged) +# MANAGED_SERVICE_TOKEN: GitHub token with managed-service read access +# FORK_PUSH_TOKEN: GitHub token with fork push access +# FORK_OWNER: GitHub username that owns the fork +# CREATE_PR_TOKEN: GitHub token for creating PRs +# GITHUB_REPOSITORY: Repository in owner/repo format + +# Get the script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source helper functions +source "$SCRIPT_DIR/lib/logging.sh" +source "$SCRIPT_DIR/lib/validation.sh" +source "$SCRIPT_DIR/lib/find-sdk-pr.sh" + +# Validate required environment variables +validate_env() { + local required_vars=( + "EVENT_TYPE" + "MANAGED_SERVICE_PR_URL" + "MANAGED_SERVICE_TOKEN" + "FORK_PUSH_TOKEN" + "FORK_OWNER" + "CREATE_PR_TOKEN" + "GITHUB_REPOSITORY" + ) + + for var in "${required_vars[@]}"; do + if [[ -z "${!var:-}" ]]; then + log_error "Required environment variable $var is not set" + exit 1 + fi + done + + if [[ "$EVENT_TYPE" == "openapi-spec-merged" && -z "${MANAGED_SERVICE_SHA:-}" ]]; then + log_error "MANAGED_SERVICE_SHA is required for openapi-spec-merged event" + exit 1 + fi +} + +# Resolve managed-service PR metadata from URL +resolve_managed_service_pr() { + if [[ ! "$MANAGED_SERVICE_PR_URL" =~ github\.com/([^/]+)/([^/]+)/pull/([0-9]+) ]]; then + log_error "Invalid PR URL format: $MANAGED_SERVICE_PR_URL" + exit 1 + fi + + MS_OWNER="${BASH_REMATCH[1]}" + MS_REPO="${BASH_REMATCH[2]}" + MS_PR_NUMBER="${BASH_REMATCH[3]}" + + log_info "Managed-service PR: $MS_OWNER/$MS_REPO#$MS_PR_NUMBER" + + # Export for use in other functions + export MS_OWNER MS_REPO MS_PR_NUMBER +} + +# Configure Git +configure_git() { + git config user.name "crl-console-bot" + git config user.email "crl-console-bot@users.noreply.github.com" + + FORK_URL="https://github.com/$FORK_OWNER/$(basename "$GITHUB_REPOSITORY").git" + log_info "Adding fork remote: $FORK_URL" + git remote add fork "$FORK_URL" +} + +# Determine or create SDK branch +# Two scenarios: +# 1. openapi-spec-changed: Create new SDK PR or update existing one +# 2. openapi-spec-merged: Update existing SDK PR (must already exist) +determine_sdk_branch() { + export GIT_ASKPASS="$SCRIPT_DIR/lib/git-askpass.sh" + export GIT_FORK_USER="$FORK_OWNER" + export GIT_FORK_PASSWORD="$FORK_PUSH_TOKEN" + export GIT_TERMINAL_PROMPT=0 + + chmod +x "$GIT_ASKPASS" + + # Search all open PRs for one with a commit containing the managed-service PR URL + if find_sdk_pr "$MANAGED_SERVICE_PR_URL"; then + SDK_BRANCH="$SDK_PR_BRANCH" + log_info "Using existing SDK PR branch: $SDK_BRANCH" + + git fetch fork "$SDK_BRANCH" || { + log_error "Could not fetch branch $SDK_BRANCH from fork" + exit 1 + } + git checkout -B "$SDK_BRANCH" "fork/$SDK_BRANCH" + EXISTING_SDK_PR="true" + else + # No existing SDK PR found + if [[ "$EVENT_TYPE" == "openapi-spec-merged" ]]; then + # For merged events, we expect an SDK PR to already exist from the earlier + # openapi-spec-changed event. If not found, something went wrong. + log_error "No SDK PR found for openapi-spec-merged event" + exit 1 + fi + + # For changed events, create a new branch for a new SDK PR + # Base it on the latest pending deploy branch to include any unreleased changes + TIMESTAMP=$(date --utc +%Y%m%d-%H%M%S) + SDK_BRANCH="automation/openapi-spec-change-$TIMESTAMP" + log_info "Creating new SDK branch: $SDK_BRANCH from $PENDING_DEPLOY_BRANCH" + + # Configure askpass for origin authentication + export GIT_FORK_USER="x-access-token" + export GIT_FORK_PASSWORD="$CREATE_PR_TOKEN" + + git fetch origin "$PENDING_DEPLOY_BRANCH" + git checkout -b "$SDK_BRANCH" "origin/$PENDING_DEPLOY_BRANCH" + + # Restore fork credentials for later use + export GIT_FORK_USER="$FORK_OWNER" + export GIT_FORK_PASSWORD="$FORK_PUSH_TOKEN" + + EXISTING_SDK_PR="false" + fi + + export SDK_BRANCH EXISTING_SDK_PR +} + +# Sync OpenAPI spec and regenerate client +# Fetches the spec from managed-service and regenerates SDK client code. +# For openapi-spec-changed events: fetch from PR branch +# For openapi-spec-merged events: fetch from the exact merged commit SHA +sync_and_regenerate() { + SPEC_PATH_IN_MS="console/ui/cc-console/assets/docs/api/latest/openapi.json" + LOCAL_SPEC_PATH="internal/spec/openapi.json" + + # Use commit SHA if provided (merged event), otherwise use PR head (changed event) + if [[ -n "${MANAGED_SERVICE_SHA:-}" ]]; then + REF="$MANAGED_SERVICE_SHA" + log_info "Fetching openapi.json from $MS_OWNER/$MS_REPO at commit $MANAGED_SERVICE_SHA" + else + REF="refs/pull/$MS_PR_NUMBER/head" + log_info "Fetching openapi.json from $MS_OWNER/$MS_REPO PR #$MS_PR_NUMBER" + fi + + # Fetch the OpenAPI spec file from managed-service + export GH_TOKEN="$MANAGED_SERVICE_TOKEN" + gh api "repos/$MS_OWNER/$MS_REPO/contents/$SPEC_PATH_IN_MS?ref=$REF" \ + --jq '.content' | base64 --decode > "$LOCAL_SPEC_PATH.new" + + # Compare with current spec to see if anything changed + if cmp --silent "$LOCAL_SPEC_PATH" "$LOCAL_SPEC_PATH.new"; then + log_info "No changes to $LOCAL_SPEC_PATH" + rm "$LOCAL_SPEC_PATH.new" + HAS_CHANGES="false" + else + # Spec changed - replace it and regenerate the client code + log_info "Spec file changed, replacing $LOCAL_SPEC_PATH" + mv "$LOCAL_SPEC_PATH.new" "$LOCAL_SPEC_PATH" + + # Regenerate SDK client from the new spec using Docker + # Note: sudo is required because docker containers run as root by default, + # creating files owned by root. We then chown them back to the current user. + log_info "Running make generate-openapi-client" + sudo make generate-openapi-client + sudo chown --recursive "$(id --user):$(id --group)" internal/openapi-generator/ pkg/client/ docs/ README.md + + # Check if regeneration produced any changes + if [[ -z "$(git status --porcelain)" ]]; then + HAS_CHANGES="false" + else + HAS_CHANGES="true" + fi + fi + + export HAS_CHANGES +} + +# Update changelog with Claude +# Uses Claude AI to analyze the git diff, update CHANGELOG.md, and generate +# commit/PR metadata (title, body, description). +update_changelog() { + if [[ "$HAS_CHANGES" != "true" ]]; then + return + fi + + log_info "Setting up Claude CLI..." + curl --fail --silent --show-error --location https://claude.ai/install.sh | bash -s -- "latest" + log_info "Claude CLI installed: $(claude --version)" + + # Configure Claude to use Vertex AI for authentication + log_info "Invoking Claude to update changelog..." + export CLAUDE_CODE_USE_VERTEX="1" + export ANTHROPIC_VERTEX_PROJECT_ID="vertex-model-runners" + export CLOUD_ML_REGION="us-east5" + + # Call helper script that invokes Claude and returns structured output + CLAUDE_OUTPUT=$("$SCRIPT_DIR/lib/invoke-claude-changelog.sh" "$MANAGED_SERVICE_PR_URL" "${MANAGED_SERVICE_SHA:-}") + + # Parse the structured output (delimited by EOF markers) + COMMIT_TITLE=$(echo "$CLAUDE_OUTPUT" | awk '/commit_title<}" + + check_required_commands gh git jq make curl + validate_env + resolve_managed_service_pr + configure_git + find_pending_deploy_branch + determine_sdk_branch + sync_and_regenerate + update_changelog + commit_changes + push_to_fork + create_or_update_pr + + if [[ "$HAS_CHANGES" == "false" && "$EVENT_TYPE" != "openapi-spec-merged" ]]; then + log_info "No changes detected after syncing OpenAPI spec and regenerating client" + fi + + log_info "=== OpenAPI sync workflow completed ===" +} + +main