From 5025071ca86ec08acd6131fb618517d231b2e744 Mon Sep 17 00:00:00 2001 From: Matt Perpick Date: Wed, 22 Oct 2025 21:30:59 -0400 Subject: [PATCH] build and release --- .github/workflows/publish-gem-prerelease.yaml | 46 ++++++++++ .github/workflows/publish-gem.yaml | 61 +++++++++++++ .gitignore | 3 + Rakefile | 83 +++++++++++++++++ scripts/generate-release-notes.sh | 31 +++++++ scripts/push-release-tag.sh | 91 +++++++++++++++++++ scripts/validate-release-tag.sh | 62 +++++++++++++ 7 files changed, 377 insertions(+) create mode 100644 .github/workflows/publish-gem-prerelease.yaml create mode 100644 .github/workflows/publish-gem.yaml create mode 100755 scripts/generate-release-notes.sh create mode 100755 scripts/push-release-tag.sh create mode 100755 scripts/validate-release-tag.sh diff --git a/.github/workflows/publish-gem-prerelease.yaml b/.github/workflows/publish-gem-prerelease.yaml new file mode 100644 index 00000000..240918b8 --- /dev/null +++ b/.github/workflows/publish-gem-prerelease.yaml @@ -0,0 +1,46 @@ +# +# This workflow is used to publish the Ruby SDK to RubyGems as a prerelease. +# The version number is automatically modified to append an "alpha" suffix with +# the GitHub run number, so you can push multiple prereleases from any branch. +# + +name: Publish Ruby SDK Prerelease + +on: + workflow_dispatch: + inputs: + ref: + description: "Publish the given Git ref as a prerelease (branch, tag, or commit SHA)" + required: true + type: string + default: "main" + +jobs: + build-and-publish-prerelease: + runs-on: ubuntu-latest + + permissions: + contents: write + id-token: write + + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.ref }} + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.4' + bundler-cache: true + + - name: Run linter + run: bundle exec rake lint + + - name: Configure RubyGems credentials + uses: rubygems/configure-rubygems-credentials@main + + - name: Build and publish prerelease + run: bundle exec rake release:prerelease + env: + GITHUB_RUN_NUMBER: ${{ github.run_number }} diff --git a/.github/workflows/publish-gem.yaml b/.github/workflows/publish-gem.yaml new file mode 100644 index 00000000..945150a3 --- /dev/null +++ b/.github/workflows/publish-gem.yaml @@ -0,0 +1,61 @@ +# +# This workflow publishes the Ruby SDK to RubyGems when a release tag is pushed. +# It validates the tag, ensures the commit is on main, runs tests, and publishes. +# + +name: Publish Ruby SDK + +on: + push: + tags: + - 'v*.*.*' + +jobs: + validate: + runs-on: ubuntu-latest + outputs: + release_tag: ${{ steps.get-tag.outputs.tag }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get release tag + id: get-tag + run: echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.4' + bundler-cache: true + + - name: Validate release tag + run: bash scripts/validate-release-tag.sh + + publish: + needs: validate + runs-on: ubuntu-latest + + permissions: + contents: write + id-token: write + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.4' + bundler-cache: true + + - name: Configure RubyGems credentials + uses: rubygems/configure-rubygems-credentials@main + + - name: Release + run: bundle exec rake release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index c3a7f1e9..8cdb3e38 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,6 @@ # Ignore mise local settings .mise.local.toml .claude + +# Release artifacts +changelog.md diff --git a/Rakefile b/Rakefile index 5aaae18d..c7604a99 100644 --- a/Rakefile +++ b/Rakefile @@ -43,3 +43,86 @@ desc "Verify CI (lint + test)" task ci: [:lint, :test] task default: :ci + +# Release tasks +namespace :release do + desc "Validate release tag and version" + task :validate do + sh "bash scripts/validate-release-tag.sh" + end + + desc "Build the gem" + task build: [:clean] do + sh "gem build braintrust.gemspec" + end + + desc "Publish gem to RubyGems (requires authentication)" + task :publish do + gem_file = FileList["braintrust-*.gem"].first + unless gem_file + puts "Error: No gem file found. Run 'rake release:build' first." + exit 1 + end + sh "gem push #{gem_file}" + end + + desc "Generate changelog for release" + task :changelog do + sh "bash scripts/generate-release-notes.sh > changelog.md" + puts "✓ Changelog generated: changelog.md" + end + + desc "Create GitHub release" + task :github do + unless File.exist?("changelog.md") + puts "Error: changelog.md not found. Run 'rake release:changelog' first." + exit 1 + end + + require_relative "lib/braintrust/version" + tag = "v#{Braintrust::VERSION}" + + sh "gh release create #{tag} --title 'Release #{tag}' --notes-file changelog.md" + puts "✓ GitHub release created: #{tag}" + end + + desc "Build and publish prerelease (modifies version with alpha suffix)" + task :prerelease do + # Get current version + require_relative "lib/braintrust/version" + original_version = Braintrust::VERSION + + # Generate prerelease version with GitHub run number or timestamp + run_number = ENV["GITHUB_RUN_NUMBER"] || Time.now.to_i.to_s + prerelease_version = "#{original_version}.alpha.#{run_number}" + + puts "Original version: #{original_version}" + puts "Prerelease version: #{prerelease_version}" + + # Temporarily modify version.rb + version_file = "lib/braintrust/version.rb" + content = File.read(version_file) + modified_content = content.gsub( + /VERSION = "#{Regexp.escape(original_version)}"/, + "VERSION = \"#{prerelease_version}\"" + ) + + File.write(version_file, modified_content) + + begin + # Build and publish + Rake::Task["release:build"].invoke + Rake::Task["release:publish"].invoke + puts "✓ Prerelease #{prerelease_version} published successfully!" + ensure + # Restore original version + File.write(version_file, content) + puts "Restored original version.rb" + end + end +end + +desc "Full release: validate, lint, generate changelog, build, publish, and create GitHub release" +task release: ["release:validate", :lint, "release:changelog", "release:build", "release:publish", "release:github"] do + puts "✓ Release completed successfully!" +end diff --git a/scripts/generate-release-notes.sh b/scripts/generate-release-notes.sh new file mode 100755 index 00000000..05fcaa1e --- /dev/null +++ b/scripts/generate-release-notes.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Script to generate release notes for GitHub Release +# Compares current tag with previous tag + +set -euo pipefail + +# Get the repository root +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$REPO_ROOT" + +# Get current tag +CURRENT_TAG="${GITHUB_REF_NAME:-$(git describe --tags --exact-match 2>/dev/null || echo "")}" + +if [ -z "$CURRENT_TAG" ]; then + echo "Error: No tag found. This script should be run on a tagged commit." + exit 1 +fi + +# Get previous tag +PREVIOUS_TAG=$(git describe --tags --abbrev=0 "${CURRENT_TAG}^" 2>/dev/null || echo "") + +# Generate release notes +if [ -n "$PREVIOUS_TAG" ]; then + echo "## Changes since $PREVIOUS_TAG" + echo "" + git log "${PREVIOUS_TAG}..${CURRENT_TAG}" --pretty=format:"- %s (%h)" --no-merges +else + echo "## Initial Release" + echo "" + echo "First release of the Braintrust Ruby SDK" +fi diff --git a/scripts/push-release-tag.sh b/scripts/push-release-tag.sh new file mode 100755 index 00000000..0c8bab49 --- /dev/null +++ b/scripts/push-release-tag.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# Script to push a release tag for the Ruby SDK +# Inspired by py/scripts/push-release-tag.sh + +set -euo pipefail + +# Parse arguments +DRY_RUN=false +while [[ $# -gt 0 ]]; do + case $1 in + --dry-run) + DRY_RUN=true + shift + ;; + *) + echo "Unknown option: $1" + echo "Usage: $0 [--dry-run]" + exit 1 + ;; + esac +done + +# Get the repository root +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$REPO_ROOT" + +# Fetch latest tags +echo "Fetching latest tags..." +git fetch --tags + +# Get version from version.rb +VERSION=$(ruby -r "./lib/braintrust/version.rb" -e "puts Braintrust::VERSION") +TAG="v${VERSION}" + +# Check if tag already exists +if git rev-parse "$TAG" >/dev/null 2>&1; then + echo "Error: Tag $TAG already exists" + exit 1 +fi + +# Get current commit info +COMMIT_SHA=$(git rev-parse HEAD) +COMMIT_SHORT_SHA=$(git rev-parse --short HEAD) +REPO_URL=$(git config --get remote.origin.url | sed 's/\.git$//' | sed 's/git@github.com:/https:\/\/github.com\//') + +# Get the previous tag for comparison +PREVIOUS_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + +echo "" +echo "========================================" +echo "Release Information" +echo "========================================" +echo "New version tag: $TAG" +echo "Current commit: $COMMIT_SHA" +echo "Commit URL: ${REPO_URL}/commit/${COMMIT_SHA}" +if [ -n "$PREVIOUS_TAG" ]; then + echo "Previous tag: $PREVIOUS_TAG" + echo "Changelog: ${REPO_URL}/compare/${PREVIOUS_TAG}...${COMMIT_SHORT_SHA}" +fi +echo "========================================" +echo "" + +if [ "$DRY_RUN" = true ]; then + echo "DRY RUN: Would create and push tag $TAG" + echo "Exiting without making changes." + exit 0 +fi + +# Require confirmation +echo "This will create and push tag $TAG to trigger the production release." +echo "Type 'YOLO' to confirm:" +read -r CONFIRMATION + +if [ "$CONFIRMATION" != "YOLO" ]; then + echo "Confirmation failed. Aborting." + exit 1 +fi + +# Create and push the tag +echo "" +echo "Creating tag $TAG..." +git tag "$TAG" + +echo "Pushing tag $TAG..." +git push origin "$TAG" + +echo "" +echo "✓ Tag $TAG has been pushed successfully!" +echo "" +echo "Monitor the release workflow at:" +echo "${REPO_URL}/actions" diff --git a/scripts/validate-release-tag.sh b/scripts/validate-release-tag.sh new file mode 100755 index 00000000..c7b86908 --- /dev/null +++ b/scripts/validate-release-tag.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# Script to validate a release tag for the Ruby SDK +# Ensures the tag matches the version in version.rb and is on the main branch + +set -euo pipefail + +# Get the repository root +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$REPO_ROOT" + +# Get the current tag (should be set in CI environment) +RELEASE_TAG="${GITHUB_REF_NAME:-}" + +if [ -z "$RELEASE_TAG" ]; then + echo "Error: GITHUB_REF_NAME is not set" + echo "This script should be run in a GitHub Actions environment" + exit 1 +fi + +echo "Validating release tag: $RELEASE_TAG" + +# Extract version from tag (remove 'v' prefix) +TAG_VERSION="${RELEASE_TAG#v}" + +# Get version from version.rb +VERSION=$(ruby -r "./lib/braintrust/version.rb" -e "puts Braintrust::VERSION") + +echo "Tag version: $TAG_VERSION" +echo "version.rb: $VERSION" + +# Validate version matches +if [ "$TAG_VERSION" != "$VERSION" ]; then + echo "" + echo "Error: Tag version does not match version.rb" + echo " Tag: $TAG_VERSION" + echo " version.rb: $VERSION" + exit 1 +fi + +echo "✓ Version matches" + +# Validate the tag is on the main branch +MAIN_BRANCH="main" +TAG_COMMIT=$(git rev-parse "$RELEASE_TAG") +MAIN_COMMIT=$(git rev-parse "origin/$MAIN_BRANCH") + +# Check if the tag commit is an ancestor of main or is main +if ! git merge-base --is-ancestor "$TAG_COMMIT" "$MAIN_COMMIT" && [ "$TAG_COMMIT" != "$MAIN_COMMIT" ]; then + echo "" + echo "Error: Tag $RELEASE_TAG is not on the $MAIN_BRANCH branch" + echo " Tag commit: $TAG_COMMIT" + echo " Main commit: $MAIN_COMMIT" + exit 1 +fi + +echo "✓ Tag is on the $MAIN_BRANCH branch" + +echo "" +echo "✓ Release tag validation successful" +echo " Tag: $RELEASE_TAG" +echo " Version: $VERSION" +echo " Commit: $TAG_COMMIT"