diff --git a/.github/workflows/publish-manual.yml b/.github/workflows/publish-manual.yml index f8d4658ec..81b8c65b4 100644 --- a/.github/workflows/publish-manual.yml +++ b/.github/workflows/publish-manual.yml @@ -2,6 +2,10 @@ name: (Re)Publish public packages manually # This only attempts to publish the public packages to npm. # It does not bump the version, it does not update the changelogs, it does not create a github release. +# Prevent more than one non-canary npm publishing job from running at the same time +concurrency: + group: npm-publish + # Package publishing is manually triggered on github actions dashboard on: workflow_dispatch @@ -12,6 +16,9 @@ jobs: timeout-minutes: 15 runs-on: ubuntu-latest-16-cores-open + outputs: + is_latest_version: ${{ steps.publish_step.outputs.is_latest_version }} + steps: - name: Check out code uses: actions/checkout@v3 @@ -20,6 +27,7 @@ jobs: uses: ./.github/actions/setup - name: Publish + id: publish_step run: yarn tsx ./scripts/publish-manual.ts env: GH_TOKEN: ${{ github.token }} @@ -28,9 +36,8 @@ jobs: publish_templates: name: Publishes code templates to separate repositories uses: tldraw/tldraw/.github/workflows/publish-templates.yml@main + if: ${{ needs.deploy.outputs.is_latest_version == 'true' }} secrets: VITE_TEMPLATE_REPO_SSH_DEPLOY_KEY: ${{ secrets.VITE_TEMPLATE_REPO_SSH_DEPLOY_KEY }} NEXTJS_TEMPLATE_REPO_SSH_DEPLOY_KEY: ${{ secrets.NEXTJS_TEMPLATE_REPO_SSH_DEPLOY_KEY }} - needs: [ - deploy - ] \ No newline at end of file + needs: deploy diff --git a/.github/workflows/publish-new.yml b/.github/workflows/publish-new.yml index debf56ee4..7b6c5d2c3 100644 --- a/.github/workflows/publish-new.yml +++ b/.github/workflows/publish-new.yml @@ -1,6 +1,10 @@ name: Publish new packages from main # This bumps the version, updates the changelogs, publishes a GitHub release, and publishes the packages to npm. +# Prevent more than one non-canary npm publishing job from running at the same time +concurrency: + group: npm-publish + # Package publishing is manually triggered on github actions dashboard on: workflow_dispatch: diff --git a/.github/workflows/publish-patch.yml b/.github/workflows/publish-patch.yml new file mode 100644 index 000000000..4e2900ba6 --- /dev/null +++ b/.github/workflows/publish-patch.yml @@ -0,0 +1,61 @@ +name: Publish patch release +# This bumps the patch version, updates the changelogs in the release branch only, publishes a GitHub release, and publishes the packages to npm. + +# Prevent more than one non-canary npm publishing job from running at the same time +concurrency: + group: npm-publish + +# Package publishing is manually triggered on github actions dashboard +on: + push: + branches: + - 'v[0-9]*.[0.9]*.x' + +jobs: + deploy: + name: Publish patch release + environment: npm deploy + timeout-minutes: 15 + runs-on: ubuntu-latest-16-cores-open + outputs: + is_latest_version: ${{ steps.publish_step.outputs.is_latest_version }} + + steps: + - name: Generate GH token + id: generate_token + uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 + with: + app_id: ${{ secrets.HUPPY_APP_ID }} + private_key: ${{ secrets.HUPPY_APP_PRIVATE_KEY }} + + - name: Check out code + uses: actions/checkout@v3 + with: + token: ${{ steps.generate_token.outputs.token }} + + - name: Prepare repository + # Fetch full git history and tags for auto + run: git fetch --unshallow --tags + + - name: Run our setup + uses: ./.github/actions/setup + + - name: Publish + id: publish_step + run: | + git config --global user.name 'huppy-bot[bot]' + git config --global user.email '128400622+huppy-bot[bot]@users.noreply.github.com' + yarn tsx ./scripts/publish-patch.ts + env: + GH_TOKEN: ${{ steps.generate_token.outputs.token }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + HUPPY_TOKEN: ${{ secrets.HUPPY_TOKEN }} + + publish_templates: + name: Publishes code templates to separate repositories + uses: tldraw/tldraw/.github/workflows/publish-templates.yml@main + if: ${{ needs.deploy.outputs.is_latest_version == 'true' }} + secrets: + VITE_TEMPLATE_REPO_SSH_DEPLOY_KEY: ${{ secrets.VITE_TEMPLATE_REPO_SSH_DEPLOY_KEY }} + NEXTJS_TEMPLATE_REPO_SSH_DEPLOY_KEY: ${{ secrets.NEXTJS_TEMPLATE_REPO_SSH_DEPLOY_KEY }} + needs: deploy diff --git a/scripts/publish-manual.ts b/scripts/publish-manual.ts index eac5dad1c..a4571fe0b 100644 --- a/scripts/publish-manual.ts +++ b/scripts/publish-manual.ts @@ -1,8 +1,22 @@ -import { publish } from './lib/publishing' +import { appendFileSync } from 'fs' +import { exec } from './lib/exec' +import { getLatestVersion, publish } from './lib/publishing' // This expects the package.json files to be in the correct state. // You might want to run this locally after a failed publish attempt on CI. // Or if you need to hotfix a package it might be desirable to run this. // Generate a npm automation token and run this with the NPM_TOKEN env var set. -publish() +async function main() { + const latestVersionInBranch = await getLatestVersion() + const latestVersionOnNpm = (await exec('npm', ['show', 'tldraw', 'version'])).trim() + + const isLatestVersion = latestVersionInBranch.format() === latestVersionOnNpm + if (process.env.GITHUB_OUTPUT) { + appendFileSync(process.env.GITHUB_OUTPUT, `is_latest_version=${isLatestVersion}\n`) + } + + publish() +} + +main() diff --git a/scripts/publish-new.ts b/scripts/publish-new.ts index f2f3af92c..0ea0e0dca 100644 --- a/scripts/publish-new.ts +++ b/scripts/publish-new.ts @@ -70,6 +70,8 @@ async function main() { const releaseType = getReleaseType() const nextVersion = await getNextVersion(releaseType) + const isPrerelease = parse(nextVersion)!.prerelease.length > 0 + console.log('Releasing version', nextVersion) await setAllVersions(nextVersion) @@ -111,10 +113,18 @@ async function main() { title: `v${nextVersion}`, }) + const gitTag = `v${nextVersion}` + // create and push a new tag - await exec('git', ['tag', '-f', `v${nextVersion}`]) + await exec('git', ['tag', '-f', gitTag]) await exec('git', ['push', '--follow-tags']) + // create new 'release' branch called e.g. v2.0.x or v4.3.x, for making patch releases + if (!isPrerelease) { + const { major, minor } = parse(nextVersion)! + await exec('git', ['push', 'origin', `${gitTag}:refs/heads/v${major}.${minor}.x`]) + } + // create a release on github await auto.runRelease({ useVersion: nextVersion }) diff --git a/scripts/publish-patch.ts b/scripts/publish-patch.ts new file mode 100644 index 000000000..69f120169 --- /dev/null +++ b/scripts/publish-patch.ts @@ -0,0 +1,111 @@ +import { Auto } from '@auto-it/core' +import fetch from 'cross-fetch' +import glob from 'glob' +import { assert } from 'node:console' +import { appendFileSync } from 'node:fs' +import { exec } from './lib/exec' +import { nicelog } from './lib/nicelog' +import { getLatestVersion, publish, setAllVersions } from './lib/publishing' +import { getAllWorkspacePackages } from './lib/workspace' + +async function main() { + const huppyToken = process.env.HUPPY_TOKEN + assert(huppyToken && typeof huppyToken === 'string', 'HUPPY_ACCESS_KEY env var must be set') + + const latestVersionInBranch = await getLatestVersion() + const latestVersionOnNpm = (await exec('npm', ['show', 'tldraw', 'version'])).trim() + + const isLatestVersion = latestVersionInBranch.format() === latestVersionOnNpm + if (process.env.GITHUB_OUTPUT) { + appendFileSync(process.env.GITHUB_OUTPUT, `is_latest_version=${isLatestVersion}\n`) + } + + const nextVersion = latestVersionInBranch.inc('patch').format() + // check we're on the main branch on HEAD + const currentBranch = (await exec('git', ['rev-parse', '--abbrev-ref', 'HEAD'])).toString().trim() + if (currentBranch !== `v${latestVersionInBranch.major}.${latestVersionInBranch.minor}.x`) { + throw new Error('Branch name does not match expected format: v{major}.{minor}.x') + } + + // we could probably do this a lot earlier in the yml file but 🤷‍♂️ + const numberOfCommitsSinceBranch = Number( + (await exec('git', ['rev-list', '--count', `HEAD`, '^main'])).toString().trim() + ) + + if (numberOfCommitsSinceBranch === 0) { + // Skip release if there are no commits since this branch was created during the initial release + // for this . version. + nicelog('Initial push, skipping release') + return + } + + nicelog('Releasing version', nextVersion) + + await setAllVersions(nextVersion) + + // stage the changes + const packageJsonFilesToAdd = [] + for (const workspace of await getAllWorkspacePackages()) { + if (workspace.relativePath.startsWith('packages/')) { + packageJsonFilesToAdd.push(`${workspace.relativePath}/package.json`) + } + } + const versionFilesToAdd = glob.sync('**/*/version.ts', { + ignore: ['node_modules/**'], + follow: false, + }) + console.log('versionFilesToAdd', versionFilesToAdd) + await exec('git', [ + 'add', + '--update', + 'lerna.json', + ...packageJsonFilesToAdd, + ...versionFilesToAdd, + ]) + + const auto = new Auto({ + plugins: ['npm'], + baseBranch: currentBranch, + owner: 'tldraw', + repo: 'tldraw', + verbose: true, + disableTsNode: true, + }) + + await auto.loadConfig() + + // this creates a new commit + await auto.changelog({ + useVersion: nextVersion, + title: `v${nextVersion}`, + }) + + // create and push a new tag + await exec('git', ['tag', '-f', `v${nextVersion}`]) + await exec('git', ['push', '--follow-tags']) + + // create a release on github + await auto.runRelease({ useVersion: 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 (isLatestVersion) { + nicelog('Notifying huppy of release...') + const huppyResponse = await fetch('https://tldraw-repo-sync.fly.dev/api/on-release', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ apiKey: huppyToken, tagToRelease: `v${nextVersion}`, canary: false }), + }) + nicelog( + `huppy: [${huppyResponse.status} ${huppyResponse.statusText}] ${await huppyResponse.text()}` + ) + } +} + +main()