Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
256 changes: 256 additions & 0 deletions .github/workflows/release-pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
name: Create Release PR

on:
workflow_dispatch:
inputs:
version_type:
description: 'Version bump type'
required: true
type: choice
options:
- patch
- minor
- major
- auto
default: 'auto'
custom_version:
description: 'Custom version (overrides version_type if set, e.g., 1.2.3)'
required: false
type: string

permissions:
contents: write
pull-requests: write

jobs:
create-release-pr:
runs-on: ubuntu-latest

steps:
- name: Checkout main branch
uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}

- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: latest

- name: Get current version
id: current
run: |
CURRENT_VERSION=$(jq -r '.version' package.json)
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
echo "Current version: $CURRENT_VERSION"

- name: Determine version bump from commits
id: analyze
if: inputs.version_type == 'auto' && inputs.custom_version == ''
run: |
# Get commits since last tag
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")

if [ -z "$LAST_TAG" ]; then
COMMITS=$(git log --oneline)
else
COMMITS=$(git log --oneline ${LAST_TAG}..HEAD)
fi

echo "Commits since last tag:"
echo "$COMMITS"

# Analyze commits for version bump (conventional commits)
BUMP="patch"

if echo "$COMMITS" | grep -qiE "^[a-f0-9]+ (feat|feature)(\(.+\))?!:|BREAKING CHANGE"; then
BUMP="major"
elif echo "$COMMITS" | grep -qiE "^[a-f0-9]+ (feat|feature)(\(.+\))?:"; then
BUMP="minor"
fi

echo "bump=$BUMP" >> $GITHUB_OUTPUT
echo "Determined bump type: $BUMP"

- name: Calculate new version
id: version
run: |
CURRENT="${{ steps.current.outputs.version }}"

# Use custom version if provided
if [ -n "${{ inputs.custom_version }}" ]; then
NEW_VERSION="${{ inputs.custom_version }}"
else
# Determine bump type
if [ "${{ inputs.version_type }}" == "auto" ]; then
BUMP="${{ steps.analyze.outputs.bump }}"
else
BUMP="${{ inputs.version_type }}"
fi

# Split version into parts
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"

# Validate no prerelease suffix (e.g., 1.0.0-alpha.1)
if [[ ! "$MAJOR" =~ ^[0-9]+$ ]] || [[ ! "$MINOR" =~ ^[0-9]+$ ]] || [[ ! "$PATCH" =~ ^[0-9]+$ ]]; then
echo "::error::Prerelease versions not supported (got: $CURRENT). Use explicit version input instead."
exit 1
fi

case "$BUMP" in
major)
NEW_VERSION="$((MAJOR + 1)).0.0"
;;
minor)
NEW_VERSION="${MAJOR}.$((MINOR + 1)).0"
;;
patch)
NEW_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))"
;;
esac
fi

echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "New version: $NEW_VERSION"

- name: Create release branch
run: |
BRANCH_NAME="release-v${{ steps.version.outputs.new_version }}"
git checkout -b "$BRANCH_NAME"
echo "Created branch: $BRANCH_NAME"

- name: Update package.json version
run: |
NEW_VERSION="${{ steps.version.outputs.new_version }}"
jq --arg v "$NEW_VERSION" '.version = $v' package.json > package.json.tmp
mv package.json.tmp package.json
echo "Updated package.json to version $NEW_VERSION"

- name: Generate changelog entry
id: changelog
run: |
NEW_VERSION="${{ steps.version.outputs.new_version }}"
TODAY=$(date +%Y-%m-%d)
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")

# Get commits grouped by type
if [ -z "$LAST_TAG" ]; then
COMMITS=$(git log --pretty=format:"%s" HEAD)
else
COMMITS=$(git log --pretty=format:"%s" ${LAST_TAG}..HEAD)
fi

# Build changelog entry
ENTRY="## [$NEW_VERSION] - $TODAY"$'\n\n'

# Features (normalize case before sed to handle Feat/FEAT variants)
FEATURES=$(echo "$COMMITS" | grep -iE "^feat(\(.+\))?:" | tr '[:upper:]' '[:lower:]' | sed 's/^feat(\([^)]*\)): /- **\1**: /' | sed 's/^feat: /- /' || true)
if [ -n "$FEATURES" ]; then
ENTRY+="### Added"$'\n'"$FEATURES"$'\n\n'
fi

# Fixes (normalize case before sed to handle Fix/FIX variants)
FIXES=$(echo "$COMMITS" | grep -iE "^fix(\(.+\))?:" | tr '[:upper:]' '[:lower:]' | sed 's/^fix(\([^)]*\)): /- **\1**: /' | sed 's/^fix: /- /' || true)
if [ -n "$FIXES" ]; then
ENTRY+="### Fixed"$'\n'"$FIXES"$'\n\n'
fi

# Other changes (refactor, perf, style, or non-conventional commits)
OTHERS=$(echo "$COMMITS" | grep -ivE "^(feat|fix|chore|ci|docs|test)(\(.+\))?:" | head -10 | sed 's/^/- /' || true)
if [ -n "$OTHERS" ]; then
ENTRY+="### Changed"$'\n'"$OTHERS"$'\n\n'
fi

# Save entry to file for use in PR body
echo "$ENTRY" > /tmp/changelog_entry.md

# Prepend to CHANGELOG.md if it exists
# Preserves full header (everything before first "## [" section marker)
if [ -f "CHANGELOG.md" ]; then
# Find line number of first version section (## [x.y.z] or ## [Unreleased])
HEADER_END=$(grep -n '^## \[' CHANGELOG.md | head -1 | cut -d: -f1)
if [ -n "$HEADER_END" ]; then
HEADER_END=$((HEADER_END - 1))
head -n "$HEADER_END" CHANGELOG.md > /tmp/changelog_new.md
echo "" >> /tmp/changelog_new.md
cat /tmp/changelog_entry.md >> /tmp/changelog_new.md
tail -n +"$((HEADER_END + 1))" CHANGELOG.md >> /tmp/changelog_new.md
else
# No version sections found, append to end
cat CHANGELOG.md > /tmp/changelog_new.md
echo "" >> /tmp/changelog_new.md
cat /tmp/changelog_entry.md >> /tmp/changelog_new.md
fi
mv /tmp/changelog_new.md CHANGELOG.md
echo "Updated CHANGELOG.md"
fi

- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

- name: Commit changes
run: |
NEW_VERSION="${{ steps.version.outputs.new_version }}"
git add package.json
[ -f CHANGELOG.md ] && git add CHANGELOG.md
git commit -m "chore(release): prepare v$NEW_VERSION"

- name: Push branch
run: |
BRANCH_NAME="release-v${{ steps.version.outputs.new_version }}"
git push -u origin "$BRANCH_NAME"

- name: Create Pull Request
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
NEW_VERSION="${{ steps.version.outputs.new_version }}"
CURRENT_VERSION="${{ steps.current.outputs.version }}"

# Create PR body
cat << 'EOF' > /tmp/pr_body.md
## Release v$NEW_VERSION

This PR prepares the release of version **$NEW_VERSION** (from $CURRENT_VERSION).

### Changes included

EOF

# Append changelog entry if exists
if [ -f /tmp/changelog_entry.md ]; then
cat /tmp/changelog_entry.md >> /tmp/pr_body.md
fi

cat << 'EOF' >> /tmp/pr_body.md

---

### Checklist

- [ ] Version bump is correct
- [ ] CHANGELOG.md is accurate
- [ ] All tests pass

### After merge

When this PR is merged:
1. A git tag `v$NEW_VERSION` will be created automatically
2. A GitHub release will be published
3. The package will be published to npm
EOF

# Replace version placeholders
sed -i "s/\$NEW_VERSION/$NEW_VERSION/g" /tmp/pr_body.md
sed -i "s/\$CURRENT_VERSION/$CURRENT_VERSION/g" /tmp/pr_body.md

gh pr create \
--title "Release v$NEW_VERSION" \
--body-file /tmp/pr_body.md \
--base main \
--label "release" \
--label "automated"
137 changes: 137 additions & 0 deletions .github/workflows/release-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
name: Publish Release

on:
pull_request:
types: [closed]
branches: [main]

permissions:
contents: write
id-token: write

jobs:
publish:
# Only run if PR was merged and has release label
if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release')
runs-on: ubuntu-latest
environment: publish # Required for npm OIDC trusted publisher

steps:
- name: Checkout code
uses: actions/checkout@v4
with:
# Pin to merge commit SHA to avoid race condition with new commits
ref: ${{ github.event.pull_request.merge_commit_sha }}
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}

- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: latest

- name: Setup Node.js (for npm publish)
uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'

- name: Install dependencies
run: bun install

- name: Build
run: bun run build

- name: Run tests
run: bun test

- name: Get version from package.json
id: version
run: |
VERSION=$(jq -r '.version' package.json)
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Release version: $VERSION"

- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

- name: Create and push tag
run: |
VERSION="${{ steps.version.outputs.version }}"
TAG="v$VERSION"

# Check if tag already exists
if git rev-parse "$TAG" >/dev/null 2>&1; then
echo "Tag $TAG already exists, skipping tag creation"
else
git tag -a "$TAG" -m "Release $TAG"
git push origin "$TAG"
echo "Created and pushed tag: $TAG"
fi

- name: Generate release notes
id: release_notes
run: |
VERSION="${{ steps.version.outputs.version }}"

# Get previous tag (exclude current version to find actual previous release)
PREV_TAG=$(git describe --tags --abbrev=0 --exclude="v${VERSION}" HEAD 2>/dev/null || echo "")

# Build release notes
cat << EOF > /tmp/release_notes.md
## What's Changed

EOF

# Get commits for this release
if [ -n "$PREV_TAG" ]; then
git log --pretty=format:"- %s (%h)" ${PREV_TAG}..HEAD >> /tmp/release_notes.md
echo "" >> /tmp/release_notes.md
echo "" >> /tmp/release_notes.md
echo "**Full Changelog**: https://github.com/${{ github.repository }}/compare/${PREV_TAG}...v${VERSION}" >> /tmp/release_notes.md
else
git log --pretty=format:"- %s (%h)" >> /tmp/release_notes.md
fi

- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${{ steps.version.outputs.version }}"
TAG="v$VERSION"

# Check if release already exists
if gh release view "$TAG" >/dev/null 2>&1; then
echo "Release $TAG already exists, skipping release creation"
else
gh release create "$TAG" \
--title "$TAG" \
--notes-file /tmp/release_notes.md
echo "Created GitHub release: $TAG"
fi

- name: Publish to npm
# Uses OIDC trusted publisher - no NPM_TOKEN needed
run: |
VERSION="${{ steps.version.outputs.version }}"
PACKAGE_NAME="opencode-toolbox"

# Check if this version is already published on npm (idempotency)
if npm view "$PACKAGE_NAME@$VERSION" >/dev/null 2>&1; then
echo "Version $PACKAGE_NAME@$VERSION already exists on npm, skipping publish"
else
npm publish --provenance --access public
echo "Published to npm: $PACKAGE_NAME@$VERSION"
fi

- name: Post release summary
run: |
VERSION="${{ steps.version.outputs.version }}"
echo "## Release Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "✅ **Version**: $VERSION" >> $GITHUB_STEP_SUMMARY
echo "✅ **Git Tag**: v$VERSION" >> $GITHUB_STEP_SUMMARY
echo "✅ **GitHub Release**: https://github.com/${{ github.repository }}/releases/tag/v$VERSION" >> $GITHUB_STEP_SUMMARY
echo "✅ **npm Package**: https://www.npmjs.com/package/opencode-toolbox/v/$VERSION" >> $GITHUB_STEP_SUMMARY
Loading