Files
simstudioai--sim/scripts/create-single-release.ts
WeHub Mirror 6bf8bebf51
CI / Test and Build (push) Failing after 1s
CI / Migrate Dev DB (push) Has been skipped
CI / Migrate DB (push) Has been skipped
CodeQL / Analyze actions (push) Has been cancelled
CodeQL / Analyze javascript-typescript (push) Has been cancelled
CI / Detect Version (push) Has been cancelled
CI / Detect Desktop Changes (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/cron.Dockerfile, ubuntu-latest, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build AMD64 (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/cron.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/db.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/pii.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/realtime.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-8vcpu-ubuntu-2404-arm, ./docker/app.Dockerfile, linux-arm64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
Helm Chart / Lint, test, and validate chart (push) Has been cancelled
Helm Chart / Chart version bumped (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Build Dev ECR (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core) (push) Has been cancelled
CI / Promote Images (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Check Desktop Signing Secrets (push) Has been cancelled
CI / Desktop Release (push) Has been cancelled
CI / Create Desktop Prerelease (push) Has been cancelled
CI / Desktop Prerelease Build (push) Has been cancelled
CI / Publish Desktop Prerelease (push) Has been cancelled
CI / Prune Desktop Prereleases (push) Has been cancelled
Helm Chart / Install on kind and run helm test (push) Has been cancelled
WeHub snapshot of cb28d14c6f2c081de7a0d8729a8c816c9adef67a
2026-08-10 11:17:50 +08:00

418 lines
12 KiB
TypeScript
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bun
import { execSync } from 'node:child_process'
import { Octokit } from '@octokit/rest'
import { sleep } from '@sim/utils/helpers'
const GITHUB_TOKEN = process.env.GH_PAT
const REPO_OWNER = 'simstudioai'
const REPO_NAME = 'sim'
if (!GITHUB_TOKEN) {
console.error('❌ GH_PAT environment variable is required')
process.exit(1)
}
const targetVersion = process.argv[2]
if (!targetVersion) {
console.error('❌ Version argument is required')
console.error('Usage: bun run scripts/create-single-release.ts v0.3.XX')
process.exit(1)
}
const octokit = new Octokit({
auth: GITHUB_TOKEN,
})
interface VersionCommit {
hash: string
version: string
title: string
date: string
author: string
}
interface CommitDetail {
hash: string
message: string
author: string
githubUsername: string
prNumber?: string
}
function execCommand(command: string): string {
try {
return execSync(command, { encoding: 'utf8' }).trim()
} catch (error) {
console.error(`❌ Command failed: ${command}`)
throw error
}
}
function findVersionCommit(version: string): VersionCommit | null {
console.log(`🔍 Finding commit for version ${version}...`)
const gitLog = execCommand('git log --oneline --format="%H|%s|%ai|%an" main')
const lines = gitLog.split('\n').filter((line) => line.trim())
for (const line of lines) {
const [hash, message, date, author] = line.split('|')
const versionMatch = message.match(/^\s*(v\d+\.\d+\.?\d*):\s*(.+)$/)
if (versionMatch && versionMatch[1] === version) {
return {
hash,
version,
title: versionMatch[2],
date: new Date(date).toISOString(),
author,
}
}
}
return null
}
function findPreviousVersionCommit(currentVersion: string): VersionCommit | null {
console.log(`🔍 Finding previous version before ${currentVersion}...`)
const gitLog = execCommand('git log --oneline --format="%H|%s|%ai|%an" main')
const lines = gitLog.split('\n').filter((line) => line.trim())
let foundCurrent = false
for (const line of lines) {
const [hash, message, date, author] = line.split('|')
const versionMatch = message.match(/^\s*(v\d+\.\d+\.?\d*):\s*(.+)$/)
if (versionMatch) {
if (versionMatch[1] === currentVersion) {
foundCurrent = true
continue
}
if (foundCurrent) {
return {
hash,
version: versionMatch[1],
title: versionMatch[2],
date: new Date(date).toISOString(),
author,
}
}
}
}
return null
}
async function fetchGitHubCommitDetails(
commitHashes: string[]
): Promise<Map<string, CommitDetail>> {
console.log(`🔍 Fetching GitHub commit details for ${commitHashes.length} commits...`)
const commitMap = new Map<string, CommitDetail>()
for (let i = 0; i < commitHashes.length; i++) {
const hash = commitHashes[i]
try {
const { data: commit } = await octokit.rest.repos.getCommit({
owner: REPO_OWNER,
repo: REPO_NAME,
ref: hash,
})
const prMatch = commit.commit.message.match(/\(#(\d+)\)/)
const prNumber = prMatch ? prMatch[1] : undefined
const githubUsername = commit.author?.login || commit.committer?.login || 'unknown'
let cleanMessage = commit.commit.message.split('\n')[0]
if (prNumber) {
cleanMessage = cleanMessage.replace(/\s*\(#\d+\)\s*$/, '')
}
commitMap.set(hash, {
hash,
message: cleanMessage,
author: commit.commit.author?.name || 'Unknown',
githubUsername,
prNumber,
})
await sleep(100)
} catch (error: any) {
console.warn(`⚠️ Could not fetch commit ${hash.substring(0, 7)}: ${error?.message || error}`)
try {
const gitData = execCommand(`git log --format="%s|%an" -1 ${hash}`).split('|')
let message = gitData[0] || 'Unknown commit'
const prMatch = message.match(/\(#(\d+)\)/)
const prNumber = prMatch ? prMatch[1] : undefined
if (prNumber) {
message = message.replace(/\s*\(#\d+\)\s*$/, '')
}
commitMap.set(hash, {
hash,
message,
author: gitData[1] || 'Unknown',
githubUsername: 'unknown',
prNumber,
})
} catch (fallbackError) {
console.error(`❌ Failed to get fallback data for ${hash.substring(0, 7)}`)
}
}
}
return commitMap
}
async function getCommitsBetweenVersions(
currentCommit: VersionCommit,
previousCommit?: VersionCommit
): Promise<CommitDetail[]> {
try {
let range: string
if (previousCommit) {
range = `${previousCommit.hash}..${currentCommit.hash}`
console.log(
`🔍 Getting commits between ${previousCommit.version} and ${currentCommit.version}`
)
} else {
range = `${currentCommit.hash}~10..${currentCommit.hash}`
console.log(`🔍 Getting commits before first version ${currentCommit.version}`)
}
const gitLog = execCommand(`git log --oneline --format="%H|%s" ${range}`)
if (!gitLog.trim()) {
console.log(`⚠️ No commits found in range ${range}`)
return []
}
const commitEntries = gitLog.split('\n').filter((line) => line.trim())
const nonVersionCommits = commitEntries.filter((line) => {
const [, message] = line.split('|')
const isVersionCommit = message.match(/^v\d+\.\d+/)
if (isVersionCommit) {
console.log(`⏭️ Skipping version commit: ${message.substring(0, 50)}...`)
return false
}
return true
})
console.log(`📋 After filtering version commits: ${nonVersionCommits.length} commits`)
if (nonVersionCommits.length === 0) {
return []
}
const commitHashes = nonVersionCommits.map((line) => line.split('|')[0])
const commitMap = await fetchGitHubCommitDetails(commitHashes)
return commitHashes.map((hash) => commitMap.get(hash)!).filter(Boolean)
} catch (error) {
console.error(`❌ Error getting commits between versions:`, error)
return []
}
}
function categorizeCommit(message: string): 'features' | 'fixes' | 'improvements' | 'other' {
const msgLower = message.toLowerCase()
if (/^feat(\(|:|!)/.test(msgLower)) {
return 'features'
}
if (/^fix(\(|:|!)/.test(msgLower)) {
return 'fixes'
}
if (/^(improvement|improve|perf|refactor)(\(|:|!)/.test(msgLower)) {
return 'improvements'
}
if (/^(chore|docs|style|test|ci|build)(\(|:|!)/.test(msgLower)) {
return 'other'
}
if (msgLower.includes('feat') || msgLower.includes('implement') || msgLower.includes('new ')) {
return 'features'
}
if (msgLower.includes('fix') || msgLower.includes('bug') || msgLower.includes('error')) {
return 'fixes'
}
if (
msgLower.includes('improve') ||
msgLower.includes('enhance') ||
msgLower.includes('upgrade') ||
msgLower.includes('optimization') ||
msgLower.includes('add') ||
msgLower.includes('update')
) {
return 'improvements'
}
return 'other'
}
async function generateReleaseBody(
versionCommit: VersionCommit,
previousCommit?: VersionCommit
): Promise<string> {
console.log(`📝 Generating release body for ${versionCommit.version}...`)
const commits = await getCommitsBetweenVersions(versionCommit, previousCommit)
if (commits.length === 0) {
console.log(`⚠️ No commits found, using simple format`)
return `${versionCommit.title}
[View changes on GitHub](https://github.com/${REPO_OWNER}/${REPO_NAME}/compare/${previousCommit?.version || 'v1.0.0'}...${versionCommit.version})`
}
console.log(`📋 Processing ${commits.length} commits for categorization`)
const features = commits.filter((c) => categorizeCommit(c.message) === 'features')
const fixes = commits.filter((c) => categorizeCommit(c.message) === 'fixes')
const improvements = commits.filter((c) => categorizeCommit(c.message) === 'improvements')
const others = commits.filter((c) => categorizeCommit(c.message) === 'other')
console.log(
`📊 Categories: ${features.length} features, ${improvements.length} improvements, ${fixes.length} fixes, ${others.length} other`
)
let body = ''
if (features.length > 0) {
body += '## Features\n\n'
for (const commit of features) {
const prLink = commit.prNumber ? ` (#${commit.prNumber})` : ''
body += `- ${commit.message}${prLink}\n`
}
body += '\n'
}
if (improvements.length > 0) {
body += '## Improvements\n\n'
for (const commit of improvements) {
const prLink = commit.prNumber ? ` (#${commit.prNumber})` : ''
body += `- ${commit.message}${prLink}\n`
}
body += '\n'
}
if (fixes.length > 0) {
body += '## Bug Fixes\n\n'
for (const commit of fixes) {
const prLink = commit.prNumber ? ` (#${commit.prNumber})` : ''
body += `- ${commit.message}${prLink}\n`
}
body += '\n'
}
if (others.length > 0) {
body += '## Other Changes\n\n'
for (const commit of others) {
const prLink = commit.prNumber ? ` (#${commit.prNumber})` : ''
body += `- ${commit.message}${prLink}\n`
}
body += '\n'
}
const uniqueContributors = new Set<string>()
commits.forEach((commit) => {
if (commit.githubUsername && commit.githubUsername !== 'unknown') {
uniqueContributors.add(commit.githubUsername)
}
})
if (uniqueContributors.size > 0) {
body += '## Contributors\n\n'
for (const contributor of Array.from(uniqueContributors).sort()) {
body += `- @${contributor}\n`
}
body += '\n'
}
body += `[View changes on GitHub](https://github.com/${REPO_OWNER}/${REPO_NAME}/compare/${previousCommit?.version || 'v1.0.0'}...${versionCommit.version})`
return body.trim()
}
async function main() {
try {
console.log(`🚀 Creating single release for ${targetVersion}...`)
const versionCommit = findVersionCommit(targetVersion)
if (!versionCommit) {
console.error(`❌ No commit found for version ${targetVersion}`)
process.exit(1)
}
console.log(
`✅ Found version commit: ${versionCommit.hash.substring(0, 7)} - ${versionCommit.title}`
)
const previousCommit = findPreviousVersionCommit(targetVersion)
if (previousCommit) {
console.log(`✅ Found previous version: ${previousCommit.version}`)
} else {
console.log(`️ No previous version found (this might be the first release)`)
}
try {
const existingRelease = await octokit.rest.repos.getReleaseByTag({
owner: REPO_OWNER,
repo: REPO_NAME,
tag: targetVersion,
})
if (existingRelease.data) {
console.log(`️ Release ${targetVersion} already exists, skipping creation`)
console.log(
`🔗 View release: https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/tag/${targetVersion}`
)
return
}
} catch (error: any) {
if (error.status !== 404) {
throw error
}
}
const releaseBody = await generateReleaseBody(versionCommit, previousCommit || undefined)
console.log(`🚀 Creating GitHub release for ${targetVersion}...`)
await octokit.rest.repos.createRelease({
owner: REPO_OWNER,
repo: REPO_NAME,
tag_name: targetVersion,
name: targetVersion,
body: releaseBody,
draft: false,
prerelease: false,
target_commitish: versionCommit.hash,
})
console.log(`✅ Successfully created release: ${targetVersion}`)
console.log(
`🔗 View release: https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/tag/${targetVersion}`
)
} catch (error) {
console.error('❌ Script failed:', error)
process.exit(1)
}
}
main()