Publish packages #7
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Publish packages | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: 'Release version or npm tag (e.g., 1.2.3, 1.2.3-rc.1, or canary)' | |
| required: false | |
| type: string | |
| permissions: | |
| id-token: write | |
| contents: write | |
| jobs: | |
| publish: | |
| name: Publish packages | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: pnpm/action-setup@v4 | |
| with: | |
| version: '10.30.3' | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 24 | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Determine release mode | |
| id: release | |
| env: | |
| VERSION_INPUT: ${{ github.event.inputs.version || '' }} | |
| run: | | |
| input="$VERSION_INPUT" | |
| timestamp=$(date -u +'%Y%m%d%H%M%S') | |
| strict_version_regex='^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$' | |
| prerelease_regex='^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)-([0-9A-Za-z-]+)\.([0-9]+)$' | |
| if [[ "$input" =~ $strict_version_regex ]]; then | |
| publish_tag='latest' | |
| package_version="$input" | |
| release_tag="v$input" | |
| release_name="v$input" | |
| prerelease='false' | |
| mode='release' | |
| elif [[ "$input" =~ $prerelease_regex ]]; then | |
| publish_tag="${BASH_REMATCH[4]}" | |
| package_version="$input" | |
| release_tag="v$input" | |
| release_name="v$input" | |
| prerelease='true' | |
| mode='prerelease' | |
| else | |
| publish_tag="${input:-dev}" | |
| if [[ ! "$publish_tag" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; then | |
| echo "Invalid npm dist-tag: $publish_tag" | |
| exit 1 | |
| fi | |
| package_version='' | |
| dev_suffix="-dev.$timestamp" | |
| release_tag="dev-$timestamp" | |
| release_name="Development build $timestamp" | |
| prerelease='true' | |
| mode='development' | |
| fi | |
| { | |
| echo "publish_tag=$publish_tag" | |
| echo "package_version=$package_version" | |
| echo "release_tag=$release_tag" | |
| echo "release_name=$release_name" | |
| echo "prerelease=$prerelease" | |
| echo "mode=$mode" | |
| echo "dev_suffix=${dev_suffix:-}" | |
| } >> "$GITHUB_OUTPUT" | |
| echo "Mode: $mode" | |
| echo "NPM tag: $publish_tag" | |
| if [ -n "$package_version" ]; then | |
| echo "Package version: $package_version" | |
| else | |
| echo "Package version suffix: $dev_suffix" | |
| fi | |
| - name: Update package versions | |
| env: | |
| MODE: ${{ steps.release.outputs.mode }} | |
| PACKAGE_VERSION: ${{ steps.release.outputs.package_version }} | |
| DEV_SUFFIX: ${{ steps.release.outputs.dev_suffix }} | |
| run: | | |
| for dir in packages/*; do | |
| [ -f "$dir/package.json" ] || continue | |
| if [ "$MODE" = 'development' ]; then | |
| node -e ' | |
| const fs = require("fs"); | |
| const path = process.argv[1]; | |
| const suffix = process.argv[2]; | |
| const pkg = JSON.parse(fs.readFileSync(path, "utf8")); | |
| pkg.version += suffix; | |
| fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + "\n"); | |
| console.log(`Updated ${pkg.name} to ${pkg.version}`); | |
| ' "$dir/package.json" "$DEV_SUFFIX" | |
| else | |
| node -e ' | |
| const fs = require("fs"); | |
| const path = process.argv[1]; | |
| const version = process.argv[2]; | |
| const pkg = JSON.parse(fs.readFileSync(path, "utf8")); | |
| pkg.version = version; | |
| fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + "\n"); | |
| console.log(`Updated ${pkg.name} to ${pkg.version}`); | |
| ' "$dir/package.json" "$PACKAGE_VERSION" | |
| fi | |
| done | |
| - name: Build packages | |
| run: pnpm dlx turbo build --filter='./packages/*' | |
| - name: Publish packages with npm trusted publishing | |
| env: | |
| NPM_TAG: ${{ steps.release.outputs.publish_tag }} | |
| run: | | |
| PACKAGES=( | |
| "commandkit:packages/commandkit" | |
| "create-commandkit:packages/create-commandkit" | |
| "@commandkit/redis:packages/redis" | |
| "@commandkit/i18n:packages/i18n" | |
| "@commandkit/cache:packages/cache" | |
| "@commandkit/analytics:packages/analytics" | |
| "@commandkit/ratelimit:packages/ratelimit" | |
| "@commandkit/ai:packages/ai" | |
| "@commandkit/queue:packages/queue" | |
| "@commandkit/tasks:packages/tasks" | |
| "@commandkit/workflow:packages/workflow" | |
| ) | |
| for entry in "${PACKAGES[@]}"; do | |
| IFS=":" read -r name path <<< "$entry" | |
| version=$(node -p "require('./$path/package.json').version") | |
| echo "Publishing $name@$version under '$NPM_TAG'..." | |
| if npm view "$name@$version" version >/dev/null 2>&1; then | |
| echo "📦 $name@$version already exists on npm, skipping..." | |
| elif ( | |
| cd "$path" | |
| npm publish --provenance --access public --tag "$NPM_TAG" | |
| ); then | |
| echo "✅ Published $name@$version under '$NPM_TAG'" | |
| elif npm view "$name@$version" version >/dev/null 2>&1; then | |
| echo "📦 $name@$version was published by another process, skipping..." | |
| else | |
| echo "❌ Failed to publish $name@$version" | |
| exit 1 | |
| fi | |
| done | |
| - name: Generate changelog | |
| env: | |
| RELEASE_TAG: ${{ steps.release.outputs.release_tag }} | |
| NPM_TAG: ${{ steps.release.outputs.publish_tag }} | |
| RELEASE_MODE: ${{ steps.release.outputs.mode }} | |
| run: | | |
| previous_tag=$(git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1) | |
| if [ -z "$previous_tag" ]; then | |
| commit_range='HEAD' | |
| echo 'No previous stable tag found, using all commits' | |
| else | |
| commit_range="$previous_tag..HEAD" | |
| echo "Generating changelog from $previous_tag to HEAD" | |
| fi | |
| PACKAGES=( | |
| "commandkit:packages/commandkit" | |
| "create-commandkit:packages/create-commandkit" | |
| "@commandkit/redis:packages/redis" | |
| "@commandkit/i18n:packages/i18n" | |
| "@commandkit/cache:packages/cache" | |
| "@commandkit/analytics:packages/analytics" | |
| "@commandkit/ratelimit:packages/ratelimit" | |
| "@commandkit/ai:packages/ai" | |
| "@commandkit/queue:packages/queue" | |
| "@commandkit/tasks:packages/tasks" | |
| "@commandkit/workflow:packages/workflow" | |
| ) | |
| changelog="## $RELEASE_TAG\n\n" | |
| changelog+="Published under the npm \`$NPM_TAG\` tag in \`$RELEASE_MODE\` mode.\n" | |
| for entry in "${PACKAGES[@]}"; do | |
| IFS=":" read -r name path <<< "$entry" | |
| commits=$(git log "$commit_range" --pretty=format:"%s|%h" -- "$path" 2>/dev/null || true) | |
| [ -z "$commits" ] && continue | |
| features='' | |
| fixes='' | |
| chores='' | |
| docs='' | |
| refactors='' | |
| performance='' | |
| tests='' | |
| others='' | |
| while IFS='|' read -r message hash; do | |
| line="- ${message} (\`${hash}\`)" | |
| case "$message" in | |
| feat:*|feat\(*) features+=$'\n'"$line" ;; | |
| fix:*|fix\(*) fixes+=$'\n'"$line" ;; | |
| chore:*|chore\(*) chores+=$'\n'"$line" ;; | |
| docs:*|docs\(*) docs+=$'\n'"$line" ;; | |
| refactor:*|refactor\(*) refactors+=$'\n'"$line" ;; | |
| perf:*|perf\(*) performance+=$'\n'"$line" ;; | |
| test:*|test\(*|tests:*|tests\(*) tests+=$'\n'"$line" ;; | |
| *) others+=$'\n'"$line" ;; | |
| esac | |
| done <<< "$commits" | |
| package_section="### ${name}\n" | |
| has_content=false | |
| for category in \ | |
| "Features|$features" \ | |
| "Bug Fixes|$fixes" \ | |
| "Chores|$chores" \ | |
| "Documentation|$docs" \ | |
| "Refactors|$refactors" \ | |
| "Performance|$performance" \ | |
| "Tests|$tests" \ | |
| "Other Changes|$others"; do | |
| IFS='|' read -r label content <<< "$category" | |
| if [ -n "$content" ]; then | |
| package_section+="\n#### ${label}\n${content}\n" | |
| has_content=true | |
| fi | |
| done | |
| if [ "$has_content" = true ]; then | |
| changelog+="\n${package_section}\n" | |
| fi | |
| done | |
| if [ "$changelog" = "## $RELEASE_TAG\n\nPublished under the npm \`$NPM_TAG\` tag in \`$RELEASE_MODE\` mode.\n" ]; then | |
| changelog+="\nNo package changes detected since the last stable release.\n" | |
| fi | |
| printf '%b' "$changelog" > /tmp/changelog.md | |
| - name: Create GitHub release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ steps.release.outputs.release_tag }} | |
| name: ${{ steps.release.outputs.release_name }} | |
| body_path: /tmp/changelog.md | |
| prerelease: ${{ steps.release.outputs.prerelease }} | |
| draft: false | |
| generate_release_notes: false |