Skip to content

Publish packages

Publish packages #7

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