name: Create Release
on:
workflow_dispatch:
inputs:
version_bump:
description: 'Version bump type'
required: true
type: choice
options:
- patch
- minor
- major
default: 'patch'
is_prerelease:
description: 'Create as prerelease (beta)'
required: false
type: boolean
default: false
release_name:
description: 'Release name (optional - leave blank for auto-generated)'
required: false
type: string
permissions:
contents: write
pull-requests: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
with:
fetch-depth: 0
token: ${{ secrets.RELEASE_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
- name: Configure Git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Get current version
id: current_version
run: |
CURRENT_VERSION=$(node -p "require('./package.json').version")
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
echo "Current version: $CURRENT_VERSION"
- name: Calculate new version
id: new_version
run: |
CURRENT_VERSION="${{ steps.current_version.outputs.version }}"
IS_PRERELEASE="${{ inputs.is_prerelease }}"
# Normalize old format to new format if needed (self-correction)
# Old format: X.Y.Zb[N] or X.Y.Z-b[N]
# New format: X.Y.Z-beta.[N]
if [[ "$CURRENT_VERSION" =~ ^([0-9]+\.[0-9]+\.[0-9]+)-?b([0-9]+)$ ]]; then
echo "⚠️ Old format detected: $CURRENT_VERSION"
BASE_VERSION="${BASH_REMATCH[1]}"
BETA_NUM="${BASH_REMATCH[2]}"
CURRENT_VERSION="${BASE_VERSION}-beta.${BETA_NUM}"
echo "✓ Normalized to: $CURRENT_VERSION"
fi
# Check if current version is beta (standard semver format: X.Y.Z-beta.[N])
CURRENT_IS_BETA=false
if [[ "$CURRENT_VERSION" =~ ^([0-9]+\.[0-9]+\.[0-9]+)-beta\.([0-9]+)$ ]]; then
CURRENT_IS_BETA=true
BASE_VERSION="${BASH_REMATCH[1]}"
BETA_NUM="${BASH_REMATCH[2]}"
else
BASE_VERSION="$CURRENT_VERSION"
BETA_NUM=0
fi
# Parse base version and validate format
IFS='.' read -ra VERSION_PARTS <<< "$BASE_VERSION"
if [[ "${#VERSION_PARTS[@]}" -ne 3 ]]; then
echo "Error: BASE_VERSION '$BASE_VERSION' is not in the expected 'X.Y.Z' format." >&2
exit 1
fi
MAJOR="${VERSION_PARTS[0]}"
MINOR="${VERSION_PARTS[1]}"
PATCH="${VERSION_PARTS[2]}"
# Function to apply version bump
apply_version_bump() {
case "${{ inputs.version_bump }}" in
major)
MAJOR=$((MAJOR + 1))
MINOR=0
PATCH=0
;;
minor)
MINOR=$((MINOR + 1))
PATCH=0
;;
patch)
PATCH=$((PATCH + 1))
;;
esac
}
# Smart beta logic handles 4 scenarios (semver-compatible):
# 1. Beta -> Beta: Increment beta number only (e.g., 1.0.0-beta.1 -> 1.0.0-beta.2)
# 2. Stable -> Beta: Apply version bump + add -beta.1 (e.g., 1.0.0 -> 1.0.1-beta.1 for patch)
# 3. Beta -> Stable: Remove beta suffix to finalize (e.g., 1.0.0-beta.2 -> 1.0.0)
# 4. Stable -> Stable: Apply normal version bump (e.g., 1.0.0 -> 1.0.1 for patch)
if [[ "$IS_PRERELEASE" == "true" ]]; then
if [[ "$CURRENT_IS_BETA" == "true" ]]; then
# Scenario 1: Beta -> Beta: Increment beta number
BETA_NUM=$((BETA_NUM + 1))
NEW_VERSION="${BASE_VERSION}-beta.${BETA_NUM}"
echo "Incrementing beta: $CURRENT_VERSION -> $NEW_VERSION"
else
# Scenario 2: Stable -> Beta: Apply version bump + -beta.1
apply_version_bump
NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}-beta.1"
echo "Creating first beta: $CURRENT_VERSION -> $NEW_VERSION"
fi
else
if [[ "$CURRENT_IS_BETA" == "true" ]]; then
# Scenario 3: Beta -> Stable: Remove suffix (finalize)
NEW_VERSION="$BASE_VERSION"
echo "Finalizing beta: $CURRENT_VERSION -> $NEW_VERSION"
else
# Scenario 4: Stable -> Stable: Normal version bump
apply_version_bump
NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}"
echo "Normal version bump: $CURRENT_VERSION -> $NEW_VERSION"
fi
fi
echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "New version: $NEW_VERSION"
- name: Update package.json version
run: |
npm version ${{ steps.new_version.outputs.version }} --no-git-tag-version
- name: Update package-lock.json
run: |
npm install --package-lock-only
- name: Commit version bump
run: |
set -e
git add package.json package-lock.json
git commit -m "chore: bump version to ${{ steps.new_version.outputs.version }}"
git push
- name: Create Git tag
run: |
set -e
VERSION="${{ steps.new_version.outputs.version }}"
# Check if tag exists on remote (skip if exists - idempotent)
if git ls-remote --tags origin "refs/tags/v${VERSION}" | grep -q "v${VERSION}"; then
echo "Tag v${VERSION} already exists on remote. Skipping creation."
else
git tag -a "v${VERSION}" -m "Release v${VERSION}"
git push origin "v${VERSION}"
fi
- name: Determine release name
id: release_name
run: |
if [ -z "${{ inputs.release_name }}" ]; then
echo "name=v${{ steps.new_version.outputs.version }}" >> $GITHUB_OUTPUT
else
echo "name=${{ inputs.release_name }}" >> $GITHUB_OUTPUT
fi
- name: Check if prerelease
id: prerelease_check
run: |
VERSION="${{ steps.new_version.outputs.version }}"
IS_PRERELEASE="false"
# Check for semver-compatible beta format: X.Y.Z-beta.[N]
if [[ "$VERSION" =~ -beta\.[0-9]+$ ]]; then
IS_PRERELEASE="true"
fi
echo "is_prerelease=$IS_PRERELEASE" >> $GITHUB_OUTPUT
echo "Is Prerelease: $IS_PRERELEASE"
- name: Create GitHub Release
uses: actions/github-script@v8
with:
script: |
const tagName = 'v${{ steps.new_version.outputs.version }}';
// Check if release already exists
try {
await github.rest.repos.getReleaseByTag({
owner: context.repo.owner,
repo: context.repo.repo,
tag: tagName
});
core.info(`Release ${tagName} already exists. Skipping creation.`);
return;
} catch (error) {
if (error.status !== 404) {
throw error;
}
}
// Create the release
await github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: tagName,
name: '${{ steps.release_name.outputs.name }}',
draft: false,
prerelease: ${{ steps.prerelease_check.outputs.is_prerelease == 'true' }},
generate_release_notes: true
});
- name: Install dependencies and build
run: |
npm ci
npm run build
- name: Install production dependencies for archive
run: |
# Remove all dependencies and reinstall only production dependencies
# This reduces archive size and avoids including dev tools
rm -rf node_modules
npm ci --omit=dev
- name: Create release archive
run: |
# Create directory structure for extraction
mkdir -p lacylights-mcp
cp -r src/ dist/ node_modules/ package.json package-lock.json lacylights-mcp/
tar -czf lacylights-mcp-${{ steps.new_version.outputs.version }}.tar.gz \
--exclude='.git' \
--exclude='.github' \
--exclude='node_modules/.cache' \
--exclude='*.test.ts' \
--exclude='__tests__' \
lacylights-mcp/
rm -rf lacylights-mcp
- name: Upload release asset
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
gh release upload v${{ steps.new_version.outputs.version }} \
lacylights-mcp-${{ steps.new_version.outputs.version }}.tar.gz \
--clobber
- name: Calculate SHA256 checksum
id: checksum
run: |
ARTIFACT="lacylights-mcp-${{ steps.new_version.outputs.version }}.tar.gz"
SHA256=$(sha256sum "$ARTIFACT" | awk '{print $1}')
FILE_SIZE=$(wc -c < "$ARTIFACT" | tr -d ' ')
# Check if this is a prerelease (semver beta format: X.Y.Z-beta.[N])
VERSION="${{ steps.new_version.outputs.version }}"
IS_PRERELEASE="false"
if [[ "$VERSION" =~ -beta\.[0-9]+$ ]]; then
IS_PRERELEASE="true"
fi
echo "sha256=$SHA256" >> $GITHUB_OUTPUT
echo "artifact=$ARTIFACT" >> $GITHUB_OUTPUT
echo "file_size=$FILE_SIZE" >> $GITHUB_OUTPUT
echo "is_prerelease=$IS_PRERELEASE" >> $GITHUB_OUTPUT
echo "SHA256: $SHA256"
echo "File Size: $FILE_SIZE bytes"
echo "Is Prerelease: $IS_PRERELEASE"
- name: Upload to S3
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_DIST_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_DIST_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ secrets.AWS_DIST_REGION }}
run: |
COMPONENT="mcp"
VERSION="${{ steps.new_version.outputs.version }}"
ARTIFACT="${{ steps.checksum.outputs.artifact }}"
echo "Uploading $ARTIFACT to S3..."
aws s3 cp "$ARTIFACT" \
"s3://${{ secrets.AWS_DIST_BUCKET }}/releases/${COMPONENT}/$ARTIFACT" \
--content-type "application/gzip"
echo "✓ Upload successful"
- name: Create version-specific metadata JSON
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_DIST_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_DIST_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ secrets.AWS_DIST_REGION }}
run: |
set -e
COMPONENT="mcp"
VERSION="${{ steps.new_version.outputs.version }}"
ARTIFACT="${{ steps.checksum.outputs.artifact }}"
SHA256="${{ steps.checksum.outputs.sha256 }}"
FILE_SIZE="${{ steps.checksum.outputs.file_size }}"
IS_PRERELEASE="${{ steps.checksum.outputs.is_prerelease }}"
# Create version-specific metadata JSON
cat > "${VERSION}.json" <<EOF
{
"version": "$VERSION",
"url": "https://dist.lacylights.com/releases/${COMPONENT}/$ARTIFACT",
"sha256": "$SHA256",
"releaseDate": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"isPrerelease": $IS_PRERELEASE,
"fileSize": $FILE_SIZE
}
EOF
echo "Uploading ${VERSION}.json..."
aws s3 cp "${VERSION}.json" \
"s3://${{ secrets.AWS_DIST_BUCKET }}/releases/${COMPONENT}/${VERSION}.json" \
--content-type "application/json" \
--cache-control "max-age=31536000, immutable" \
--metadata-directive REPLACE
echo "✓ ${VERSION}.json uploaded"
- name: Update latest.json
if: steps.prerelease_check.outputs.is_prerelease == 'false'
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_DIST_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_DIST_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ secrets.AWS_DIST_REGION }}
run: |
COMPONENT="mcp"
VERSION="${{ steps.new_version.outputs.version }}"
ARTIFACT="${{ steps.checksum.outputs.artifact }}"
SHA256="${{ steps.checksum.outputs.sha256 }}"
FILE_SIZE="${{ steps.checksum.outputs.file_size }}"
IS_PRERELEASE="${{ steps.checksum.outputs.is_prerelease }}"
# Only update latest.json for stable releases, not prereleases
if [[ "$IS_PRERELEASE" == "false" ]]; then
# Create latest.json
cat > latest.json <<EOF
{
"version": "$VERSION",
"url": "https://dist.lacylights.com/releases/${COMPONENT}/$ARTIFACT",
"sha256": "$SHA256",
"releaseDate": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"isPrerelease": $IS_PRERELEASE,
"fileSize": $FILE_SIZE
}
EOF
echo "Uploading latest.json..."
aws s3 cp latest.json \
"s3://${{ secrets.AWS_DIST_BUCKET }}/releases/${COMPONENT}/latest.json" \
--content-type "application/json" \
--metadata-directive REPLACE
echo "✓ latest.json updated"
else
echo "Skipping latest.json upload for prerelease ($VERSION)"
fi
- name: Update DynamoDB
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_DIST_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_DIST_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ secrets.AWS_DIST_REGION }}
run: |
COMPONENT="mcp"
VERSION="${{ steps.new_version.outputs.version }}"
ARTIFACT="${{ steps.checksum.outputs.artifact }}"
SHA256="${{ steps.checksum.outputs.sha256 }}"
FILE_SIZE="${{ steps.checksum.outputs.file_size }}"
IS_PRERELEASE="${{ steps.checksum.outputs.is_prerelease }}"
echo "Updating DynamoDB..."
aws dynamodb put-item \
--table-name lacylights-releases \
--item "{
\"component\": {\"S\": \"$COMPONENT\"},
\"version\": {\"S\": \"$VERSION\"},
\"url\": {\"S\": \"https://dist.lacylights.com/releases/${COMPONENT}/$ARTIFACT\"},
\"sha256\": {\"S\": \"$SHA256\"},
\"releaseDate\": {\"S\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"},
\"isPrerelease\": {\"BOOL\": $IS_PRERELEASE},
\"fileSize\": {\"N\": \"$FILE_SIZE\"}
}"
echo "✓ DynamoDB updated: component=$COMPONENT, version=$VERSION"
- name: Summary
run: |
echo "## Release Created Successfully! 🎉" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Version:** v${{ steps.new_version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "**Bump Type:** ${{ inputs.version_bump }}" >> $GITHUB_STEP_SUMMARY
echo "**Previous Version:** v${{ steps.current_version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Release URL:** https://github.com/${{ github.repository }}/releases/tag/v${{ steps.new_version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "**Release Asset:** lacylights-mcp-${{ steps.new_version.outputs.version }}.tar.gz" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Distribution" >> $GITHUB_STEP_SUMMARY
echo "**Download URL:** https://dist.lacylights.com/releases/mcp/${{ steps.checksum.outputs.artifact }}" >> $GITHUB_STEP_SUMMARY
echo "**SHA256:** \`${{ steps.checksum.outputs.sha256 }}\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**API Endpoints:**" >> $GITHUB_STEP_SUMMARY
echo "- Latest: https://dist.lacylights.com/releases/mcp/latest.json" >> $GITHUB_STEP_SUMMARY