Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
139 lines
5.4 KiB
TypeScript
139 lines
5.4 KiB
TypeScript
import { appendFileSync, readFileSync } from 'node:fs'
|
|
import { Octokit } from '@octokit/rest'
|
|
import { parse } from 'semver'
|
|
import { extractChangelog } from './extract-draft-changelog'
|
|
import { getAnyPackageDiff } from './lib/didAnyPackageChange'
|
|
import { exec } from './lib/exec'
|
|
import { nicelog } from './lib/nicelog'
|
|
import {
|
|
getLatestTldrawVersionFromNpm,
|
|
publish,
|
|
publishProductionDocsAndExamplesAndBemo,
|
|
setAllVersions,
|
|
triggerBumpVersionsWorkflow,
|
|
} from './lib/publishing'
|
|
import { uploadStaticAssets } from './lib/upload-static-assets'
|
|
|
|
async function main() {
|
|
const currentBranch = (await exec('git', ['rev-parse', '--abbrev-ref', 'HEAD'])).toString().trim()
|
|
const match = currentBranch.match(/^v(\d+)\.(\d+)\.x$/)
|
|
if (!match) {
|
|
throw new Error('Branch name does not match expected format: v{major}.{minor}.x')
|
|
}
|
|
const [major, minor] = match.slice(1).map(Number)
|
|
const latestVersionInBranch = await getLatestTldrawVersionFromNpm({
|
|
versionPrefix: `${major}.${minor}`,
|
|
})
|
|
|
|
// Check if this commit is already tagged with the initial release for this major.minor version
|
|
const expectedInitialTag = `v${latestVersionInBranch.major}.${latestVersionInBranch.minor}.0`
|
|
const tagsAtThisCommit = (await exec('git', ['tag', '--points-at', 'HEAD']))
|
|
.toString()
|
|
.trim()
|
|
.split('\n')
|
|
|
|
if (tagsAtThisCommit.includes(expectedInitialTag)) {
|
|
// Skip release if this is the initial release commit for this major.minor version
|
|
nicelog('Initial release commit, skipping patch release')
|
|
return
|
|
} else {
|
|
nicelog('Not an initial release commit, continuing with patch release')
|
|
}
|
|
|
|
const latestVersion = await getLatestTldrawVersionFromNpm()
|
|
const isLatestVersion = latestVersion.compare(latestVersionInBranch) === 0
|
|
|
|
if (isLatestVersion) {
|
|
await publishProductionDocsAndExamplesAndBemo()
|
|
}
|
|
|
|
// Ensure asset directories exist before comparing package contents.
|
|
// CI may skip postinstall (and thus refresh-assets) when install-state.gz is cached.
|
|
await exec('yarn', ['refresh-assets', '--force'])
|
|
|
|
// Skip releasing a new version if the package contents are identical.
|
|
// This may happen when cherry-picking docs-only changes.
|
|
if (!(await getAnyPackageDiff(latestVersionInBranch.format()))) {
|
|
nicelog('No packages have changed, skipping release')
|
|
return
|
|
}
|
|
|
|
if (process.env.GITHUB_OUTPUT) {
|
|
appendFileSync(process.env.GITHUB_OUTPUT, `is_latest_version=${isLatestVersion}\n`)
|
|
}
|
|
|
|
// Capture the previous tag BEFORE calling .inc(): semver's SemVer.prototype.inc()
|
|
// mutates the instance in place, so reading latestVersionInBranch.format() afterwards
|
|
// would return the new version and leave prevTag === tag, producing an empty changelog.
|
|
const prevTag = `v${latestVersionInBranch.format()}`
|
|
|
|
// Determine the version to bump from. Normally this is the latest version
|
|
// published on npm for this branch. But if a previous patch release was
|
|
// interrupted after the "v X.Y.Z [skip ci]" version-bump commit was pushed
|
|
// but before npm publish completed, the local package.json files (and the
|
|
// matching git tag) will be ahead of npm. In that case we bump from the
|
|
// local version so we sidestep the orphaned version+tag entirely instead
|
|
// of trying to re-create them - both `git commit` (no staged changes) and
|
|
// `git push --follow-tags` (tag already exists at a different commit on
|
|
// origin) would otherwise fail and permanently wedge the branch.
|
|
const localTldrawVersion = parse(
|
|
JSON.parse(readFileSync('packages/tldraw/package.json', 'utf8')).version
|
|
)
|
|
const baseVersion =
|
|
localTldrawVersion && localTldrawVersion.compare(latestVersionInBranch) > 0
|
|
? localTldrawVersion
|
|
: latestVersionInBranch
|
|
if (baseVersion !== latestVersionInBranch) {
|
|
nicelog(
|
|
`Local package version ${baseVersion.format()} is ahead of latest npm version ` +
|
|
`${latestVersionInBranch.format()}; bumping from local to skip past orphaned version.`
|
|
)
|
|
}
|
|
|
|
// .inc() mutates baseVersion; reparse so callers downstream of us see the
|
|
// original instance unchanged.
|
|
const nextVersion = parse(baseVersion.format())!.inc('patch').format()
|
|
nicelog('Releasing version', nextVersion)
|
|
|
|
await setAllVersions(nextVersion, { stageChanges: true })
|
|
|
|
const tag = `v${nextVersion}`
|
|
|
|
// create and push a new tag
|
|
await exec('git', ['commit', '-m', `${tag} [skip ci]`])
|
|
await exec('git', ['tag', '-a', tag, '-m', tag, '-f'])
|
|
await exec('git', ['push', '--follow-tags'])
|
|
|
|
// Generate changelog and create GitHub release
|
|
nicelog('Generating changelog...')
|
|
const changelog = await extractChangelog(prevTag, 'HEAD')
|
|
|
|
nicelog('Creating GitHub release...')
|
|
const octokit = new Octokit({ auth: process.env.GH_TOKEN })
|
|
await octokit.repos.createRelease({
|
|
owner: 'tldraw',
|
|
repo: 'tldraw',
|
|
tag_name: tag,
|
|
name: tag,
|
|
body: changelog,
|
|
draft: false,
|
|
prerelease: false,
|
|
})
|
|
|
|
await uploadStaticAssets(nextVersion)
|
|
|
|
// if we're on the latest version, publish to npm under 'latest' tag.
|
|
// otherwise we don't want to overwrite the latest tag, so we publish under 'revision'.
|
|
// semver rules will still be respected because there's no prerelease tag in the version,
|
|
// so clients will get the updated version if they have a range like ^1.0.0
|
|
await publish(isLatestVersion ? 'latest' : 'revision')
|
|
|
|
// If this is the latest version, trigger version bump on main branch to sync all package versions
|
|
if (isLatestVersion) {
|
|
nicelog('This is the latest version, triggering version bump on main branch')
|
|
await triggerBumpVersionsWorkflow(process.env.GH_TOKEN!)
|
|
}
|
|
}
|
|
|
|
main()
|