From f20c6a1121b0fda05e7bbc057bd7f22b77ef1393 Mon Sep 17 00:00:00 2001 From: "Joseph T. French" Date: Wed, 2 Sep 2026 00:25:43 -0500 Subject: [PATCH] chore(ci): call the shared tag-release workflow Deletes this repo's copy of tag-release.yml and calls the one in RoboFinSystems/robosystems, passing the product identity that used to be hardcoded in the prompt, the release title and the body links. This is how the release-notes fix reaches this repo. The local copy extracted the changelog with .content[0].text, which assumes the first content block is text; once CLAUDE_MODEL moved to a model that thinks by default, responses lead with a thinking block and that returned null, so every release here has silently fallen back to bare commit stats. Requires the parameterized tag-release to be on robosystems main first. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0113UQ7evQKHNf1L1tFiZzLD --- .github/workflows/create-release.yml | 4 +- .github/workflows/tag-release.yml | 363 --------------------------- 2 files changed, 3 insertions(+), 364 deletions(-) delete mode 100644 .github/workflows/tag-release.yml diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index a057365..73680e3 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -129,9 +129,11 @@ jobs: needs: create-release permissions: contents: write - uses: ./.github/workflows/tag-release.yml + uses: RoboFinSystems/robosystems/.github/workflows/tag-release.yml@main with: branch_ref: ${{ needs.create-release.outputs.branch_name }} + product_name: 'RoboSystems Python SDK' + project_kind: 'Python SDK' secrets: inherit create-summary: diff --git a/.github/workflows/tag-release.yml b/.github/workflows/tag-release.yml deleted file mode 100644 index 6205b2e..0000000 --- a/.github/workflows/tag-release.yml +++ /dev/null @@ -1,363 +0,0 @@ -name: Claude Tag Release - -on: - workflow_call: - inputs: - branch_ref: - description: 'The branch ref to tag' - required: true - type: string - outputs: - tag_name: - description: 'The created tag name' - value: ${{ jobs.tag.outputs.tag_name }} - version: - description: 'The version number' - value: ${{ jobs.tag.outputs.version }} - release_url: - description: 'The GitHub release URL' - value: ${{ jobs.tag.outputs.release_url }} - secrets: - ANTHROPIC_API_KEY: - required: true - -jobs: - tag: - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - contents: write - outputs: - tag_name: ${{ steps.get-version.outputs.tag_name }} - version: ${{ steps.get-version.outputs.version }} - release_url: ${{ steps.create-release.outputs.upload_url }} - steps: - - name: Checkout - uses: actions/checkout@v7 - with: - ref: ${{ inputs.branch_ref }} - # ACTIONS_TOKEN required to push tags to protected repo - token: ${{ secrets.ACTIONS_TOKEN || github.token }} - fetch-depth: 0 - - - name: Get Version from pyproject.toml - id: get-version - run: | - VERSION=$(grep "^version = " pyproject.toml | sed 's/version = "\(.*\)"/\1/') - echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "tag_name=v$VERSION" >> $GITHUB_OUTPUT - echo "Found version: $VERSION" - - - name: Check If Tag Exists - id: check-tag - env: - VERSION: ${{ steps.get-version.outputs.version }} - run: | - if [ -n "$(git tag -l "v${VERSION}")" ]; then - echo "tag_exists=true" >> $GITHUB_OUTPUT - echo "Tag v$VERSION already exists" - else - echo "tag_exists=false" >> $GITHUB_OUTPUT - echo "Tag v$VERSION does not exist yet" - fi - - - name: Check for curated release notes - if: steps.check-tag.outputs.tag_exists == 'false' - id: curated-notes - env: - VERSION: ${{ steps.get-version.outputs.version }} - run: | - # Milestone releases ship hand-written notes committed at - # .github/release-notes/v.md; when present they replace - # the generated delta changelog below. - NOTES_FILE=".github/release-notes/v${VERSION}.md" - if [ -f "$NOTES_FILE" ]; then - echo "found=true" >> $GITHUB_OUTPUT - echo "path=$NOTES_FILE" >> $GITHUB_OUTPUT - echo "📝 Curated release notes found: $NOTES_FILE" - else - echo "found=false" >> $GITHUB_OUTPUT - echo "No curated notes at $NOTES_FILE — generating changelog from changes since last tag" - fi - - - name: Analyze changes since last release - if: steps.check-tag.outputs.tag_exists == 'false' - id: analyze-changes - run: | - # Get the last release tag. The pattern is anchored at both ends so only - # a strict vMAJOR.MINOR.PATCH tag can be selected: LAST_TAG flows into - # later git commands and prompt text, so it must not carry trailing text. - LAST_TAG=$(git tag --sort=-version:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1) - - if [ -z "$LAST_TAG" ]; then - echo "No previous release found, analyzing all commits" - LAST_TAG="initial" - # For initial release, use different commands that don't require a range - FILES_CHANGED=$(git ls-files | wc -l) - COMMITS_COUNT=$(git rev-list --count HEAD) - # Get stats by diffing empty tree against HEAD - EMPTY_TREE=$(git hash-object -t tree /dev/null) - ADDITIONS=$(git diff --shortstat "$EMPTY_TREE" HEAD | grep -oE '[0-9]+ insertions?' | grep -oE '[0-9]+' || echo "0") - DELETIONS=$(git diff --shortstat "$EMPTY_TREE" HEAD | grep -oE '[0-9]+ deletions?' | grep -oE '[0-9]+' || echo "0") - COMMIT_MESSAGES=$(git log --oneline --no-color HEAD | head -30) - DETAILED_CHANGES=$(git diff --name-status --no-color "$EMPTY_TREE" HEAD | head -50) - PR_REFS=$(git log --oneline --no-color HEAD | grep -oE '#[0-9]+' | sort -u | head -10 || echo "") - else - echo "Analyzing changes since last release: $LAST_TAG" - # Use explicit two-commit diff syntax instead of range syntax - FILES_CHANGED=$(git diff --name-only "$LAST_TAG" HEAD | wc -l) - COMMITS_COUNT=$(git rev-list --count "$LAST_TAG"..HEAD) - ADDITIONS=$(git diff --shortstat "$LAST_TAG" HEAD | grep -oE '[0-9]+ insertions?' | grep -oE '[0-9]+' || echo "0") - DELETIONS=$(git diff --shortstat "$LAST_TAG" HEAD | grep -oE '[0-9]+ deletions?' | grep -oE '[0-9]+' || echo "0") - COMMIT_MESSAGES=$(git log --oneline --no-color "$LAST_TAG"..HEAD | head -30) - DETAILED_CHANGES=$(git diff --name-status --no-color "$LAST_TAG" HEAD | head -50) - PR_REFS=$(git log --oneline --no-color "$LAST_TAG"..HEAD | grep -oE '#[0-9]+' | sort -u | head -10 || echo "") - fi - - # Save outputs - echo "last_tag=$LAST_TAG" >> $GITHUB_OUTPUT - echo "files_changed=$FILES_CHANGED" >> $GITHUB_OUTPUT - echo "commits_count=$COMMITS_COUNT" >> $GITHUB_OUTPUT - echo "additions=$ADDITIONS" >> $GITHUB_OUTPUT - echo "deletions=$DELETIONS" >> $GITHUB_OUTPUT - - # Save multiline outputs - { - echo "commit_messages<> $GITHUB_OUTPUT - - { - echo "detailed_changes<> $GITHUB_OUTPUT - - { - echo "pr_refs<> $GITHUB_OUTPUT - - - name: Generate changelog with Claude - if: steps.check-tag.outputs.tag_exists == 'false' && steps.curated-notes.outputs.found != 'true' - id: generate-changelog - # SECURITY (H2): pass all git-derived, attacker-influenceable values - # (commit messages, changed filenames, PR refs) via env instead of - # splicing them into the script with ${{ ... }}. The heredoc expands - # $VAR from the environment but never re-evaluates command - # substitutions contained in an expansion result, so a commit message - # or filename like $(...) or `...` cannot execute. - env: - VERSION: ${{ steps.get-version.outputs.version }} - LAST_TAG: ${{ steps.analyze-changes.outputs.last_tag }} - FILES_CHANGED: ${{ steps.analyze-changes.outputs.files_changed }} - COMMITS_COUNT: ${{ steps.analyze-changes.outputs.commits_count }} - ADDITIONS: ${{ steps.analyze-changes.outputs.additions }} - DELETIONS: ${{ steps.analyze-changes.outputs.deletions }} - COMMIT_MESSAGES: ${{ steps.analyze-changes.outputs.commit_messages }} - DETAILED_CHANGES: ${{ steps.analyze-changes.outputs.detailed_changes }} - PR_REFS: ${{ steps.analyze-changes.outputs.pr_refs }} - run: | - # Create analysis prompt for Claude - cat > /tmp/changelog_prompt.txt << PROMPT_EOF - I need you to generate a concise but informative changelog for a software release. - - **Release Info:** - - Version: $VERSION - - Previous Version: $LAST_TAG - - Files Changed: $FILES_CHANGED - - Commits: $COMMITS_COUNT - - Lines Added: $ADDITIONS - - Lines Deleted: $DELETIONS - - **Recent Commits:** - $COMMIT_MESSAGES - - **File Changes:** - $DETAILED_CHANGES - - **PR References:** - $PR_REFS - - Please generate a changelog that includes: - 1. A brief 1-2 sentence summary of this release - 2. Key features/improvements (bullet points) - 3. Breaking changes (if any) - clearly marked with ⚠️ - 4. Notable technical changes worth mentioning - 5. Bug fixes (if any) - - Keep it concise but informative. Focus on what users and developers need to know. - Use markdown formatting. Don't include a version header as that will be added separately. - If there are security improvements, highlight them. - If this appears to be a maintenance/infrastructure release, emphasize that. - - Format your response as clean markdown without any prefix text. - PROMPT_EOF - - echo "✅ Changelog prompt created" - - - name: Call Claude API for changelog - if: steps.check-tag.outputs.tag_exists == 'false' && steps.curated-notes.outputs.found != 'true' - id: claude-changelog - env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - CLAUDE_MODEL: ${{ vars.CLAUDE_MODEL || 'claude-sonnet-4-6' }} - run: | - # Call Claude API with the analysis prompt - PROMPT=$(cat /tmp/changelog_prompt.txt) - - # Create a proper JSON payload using jq to handle escaping - PAYLOAD=$(jq -n \ - --arg model "$CLAUDE_MODEL" \ - --arg content "$PROMPT" \ - '{ - "model": $model, - "max_tokens": 3000, - "messages": [ - { - "role": "user", - "content": $content - } - ] - }') - - # The credential is fed to curl through a config file on stdin rather - # than an -H flag. Writing -H "x-api-key: $ANTHROPIC_API_KEY" would not - # help: the shell expands the variable before exec, so the literal key - # still lands in the curl process argv, which any other process on the - # runner can read. printf is a bash builtin, so it adds no process of - # its own, and curl parses the config at startup so --retry resends the - # header correctly. - RESPONSE=$(printf 'header = "x-api-key: %s"\n' "$ANTHROPIC_API_KEY" \ - | curl -s --config - --max-time 60 --retry 3 --retry-delay 10 -X POST "https://api.anthropic.com/v1/messages" \ - -H "Content-Type: application/json" \ - -H "anthropic-version: 2023-06-01" \ - -d "$PAYLOAD") - - # Check for API errors - if echo "$RESPONSE" | jq -e '.error' > /dev/null; then - echo "❌ Claude API error: $(echo "$RESPONSE" | jq -r '.error.message')" - echo "Using fallback changelog" - CHANGELOG="## What's Changed\n\n- ${{ steps.analyze-changes.outputs.commits_count }} commits with ${{ steps.analyze-changes.outputs.files_changed }} files changed\n- ${{ steps.analyze-changes.outputs.additions }} additions and ${{ steps.analyze-changes.outputs.deletions }} deletions\n\nSee commit history for detailed changes." - else - # Extract changelog from Claude's response - CHANGELOG=$(echo "$RESPONSE" | jq -r '.content[0].text') - - # Validate that Claude returned meaningful content - if [ -z "$CHANGELOG" ] || [ "$CHANGELOG" = "null" ]; then - echo "❌ Claude returned empty changelog, using fallback" - CHANGELOG="## What's Changed\n\n- ${{ steps.analyze-changes.outputs.commits_count }} commits with ${{ steps.analyze-changes.outputs.files_changed }} files changed\n- ${{ steps.analyze-changes.outputs.additions }} additions and ${{ steps.analyze-changes.outputs.deletions }} deletions\n\nSee commit history for detailed changes." - else - echo "✅ Claude changelog generated successfully" - fi - fi - - # Save changelog to output - { - echo "changelog<> $GITHUB_OUTPUT - - - name: Compose release body - if: steps.check-tag.outputs.tag_exists == 'false' - # SECURITY (H2): changelog content (generated or curated) is untrusted - # relative to the shell — emit it via printf/cat from env and files, - # never by splicing ${{ ... }} into the script. - env: - VERSION: ${{ steps.get-version.outputs.version }} - CURATED_FOUND: ${{ steps.curated-notes.outputs.found }} - CURATED_PATH: ${{ steps.curated-notes.outputs.path }} - CHANGELOG: ${{ steps.claude-changelog.outputs.changelog }} - LAST_TAG: ${{ steps.analyze-changes.outputs.last_tag }} - COMMITS_COUNT: ${{ steps.analyze-changes.outputs.commits_count }} - FILES_CHANGED: ${{ steps.analyze-changes.outputs.files_changed }} - ADDITIONS: ${{ steps.analyze-changes.outputs.additions }} - DELETIONS: ${{ steps.analyze-changes.outputs.deletions }} - run: | - if [ "$CURATED_FOUND" = "true" ]; then - cp "$CURATED_PATH" /tmp/release_changelog.md - else - printf '%s\n' "$CHANGELOG" > /tmp/release_changelog.md - fi - - { - echo "# RoboSystems Python SDK v${VERSION}" - echo "" - cat /tmp/release_changelog.md - echo "" - echo "---" - echo "" - if [ "$CURATED_FOUND" != "true" ]; then - echo "## 📊 Release Statistics" - echo "" - echo "- **Commits:** ${COMMITS_COUNT}" - echo "- **Files Changed:** ${FILES_CHANGED}" - echo "- **Lines Added:** ${ADDITIONS}" - echo "- **Lines Deleted:** ${DELETIONS}" - echo "- **Previous Release:** ${LAST_TAG}" - echo "" - fi - echo "## 🔗 Links" - echo "" - echo "- **Full Changelog:** [${LAST_TAG}...v${VERSION}](https://github.com/RoboFinSystems/robosystems-python-client/compare/${LAST_TAG}...v${VERSION})" - echo "- **All Releases:** [View all releases](https://github.com/RoboFinSystems/robosystems-python-client/releases)" - if [ "$CURATED_FOUND" != "true" ]; then - echo "" - echo "---" - echo "🤖 Generated with [Claude Code](https://claude.ai/code)" - fi - } > /tmp/release_body.md - - echo "✅ Release body composed ($([ "$CURATED_FOUND" = "true" ] && echo "curated notes" || echo "generated changelog"))" - - - name: Create Release Tag - if: steps.check-tag.outputs.tag_exists == 'false' - env: - VERSION: ${{ steps.get-version.outputs.version }} - run: | - git config --global user.name "github-actions[bot]" - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git tag -a "v$VERSION" -m "Release v$VERSION" - git push origin "v$VERSION" - - - name: Create GitHub Release - if: steps.check-tag.outputs.tag_exists == 'false' - id: create-release - uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3 - with: - tag_name: v${{ steps.get-version.outputs.version }} - name: Release v${{ steps.get-version.outputs.version }} - body_path: /tmp/release_body.md - draft: false - prerelease: false - env: - # ACTIONS_TOKEN preferred for release creation - GITHUB_TOKEN: ${{ secrets.ACTIONS_TOKEN || github.token }} - - - name: Create release summary - if: steps.check-tag.outputs.tag_exists == 'false' - # SECURITY (H2): the changelog (generated or curated) is untrusted; - # it is read from the file written by the compose step rather than - # interpolated with ${{ ... }} into the shell. The version and the - # previous tag are likewise passed via env — both originate outside - # this workflow (pyproject.toml and the git tag list). - env: - VERSION: ${{ steps.get-version.outputs.version }} - LAST_TAG: ${{ steps.analyze-changes.outputs.last_tag }} - run: | - echo "## 🚀 Release v$VERSION Created" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Release URL:** [v$VERSION](https://github.com/RoboFinSystems/robosystems-python-client/releases/tag/v$VERSION)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### 📊 Statistics" >> $GITHUB_STEP_SUMMARY - echo "- **Commits:** ${{ steps.analyze-changes.outputs.commits_count }}" >> $GITHUB_STEP_SUMMARY - echo "- **Files Changed:** ${{ steps.analyze-changes.outputs.files_changed }}" >> $GITHUB_STEP_SUMMARY - echo "- **Lines Added:** ${{ steps.analyze-changes.outputs.additions }}" >> $GITHUB_STEP_SUMMARY - echo "- **Lines Deleted:** ${{ steps.analyze-changes.outputs.deletions }}" >> $GITHUB_STEP_SUMMARY - echo "- **Previous Release:** ${LAST_TAG}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### 📝 Changelog" >> $GITHUB_STEP_SUMMARY - cat /tmp/release_changelog.md >> $GITHUB_STEP_SUMMARY