From 470b233b24af3db767155fa8ad15f4148e4f95ea Mon Sep 17 00:00:00 2001 From: Fiona Date: Sun, 9 Aug 2026 23:19:28 -0700 Subject: [PATCH] chore: drop the release script and document the flow that exists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `scripts/prepare-release.ts` could not perform a release in this repository. It checked out and branched from `main`, which tracks the upstream project — the release branch is `publish`, and it is the repository default. Running it moved the working tree to an unrelated branch and opened its PR against that branch. Delete it rather than repoint it: the flow it wrapped is three commands and one workflow dispatch, and the wrapper added a failure mode of its own by pushing the release tag before the PR merged, so any fixup commit needed a force-moved tag to stay in the release. `scripts/lib/semver.ts` had no other caller and goes with it. The release section of `docs/DEVELOPMENT.md` described that script step by step, so it is rewritten to describe the actual flow: version and changelog in a PR against `publish`, tag the merge commit, run the Publish workflow from the tag. It also referenced a chat channel inherited from upstream that does not exist here, and omitted the `npm_tag` input and the promotion command that goes with a staged release. Also removes the `release` entry from `package.json` scripts. --- docs/DEVELOPMENT.md | 65 +++++++-------- package.json | 3 +- scripts/lib/semver.spec.ts | 10 --- scripts/lib/semver.ts | 21 ----- scripts/prepare-release.ts | 160 ------------------------------------- 5 files changed, 34 insertions(+), 225 deletions(-) delete mode 100644 scripts/lib/semver.spec.ts delete mode 100644 scripts/lib/semver.ts delete mode 100644 scripts/prepare-release.ts diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index bb5e8b61..b6c121b2 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -185,54 +185,55 @@ No watching of HTML changes for now to avoid extra complexity. ### Prerequisites -- [`gh` CLI](https://cli.github.com/) installed and authenticated (`gh auth login`) -- `$EDITOR` environment variable set (e.g. `export EDITOR=vim` in your shell profile) -- npm Trusted Publisher configured on npmjs.com -- Maintain permission on the GitHub repository (required to push tags) +- Maintain permission on the GitHub repository (required to push tags and run the workflow) +- npm publishing credentials configured for the repository -### Release flow +`publish` is the release branch and the repository default. `main` tracks the upstream +project and is not part of this flow. -#### 1. Prepare the release (run locally) +### Release flow -```sh -yarn release -``` +#### 1. Open a release PR against `publish` -The script will: +On a branch off `publish`: -1. Validate prerequisites (`$EDITOR`, `gh` auth, clean working tree) -2. Sync with main and install latest deps -3. Prompt you to choose a version bump (patch / minor / major / custom) -4. Generate a changelog draft and open it in `$EDITOR` for review -5. Create a `release/vX.Y.Z` branch, commit, push, and create an annotated tag `vX.Y.Z` -6. Open a GitHub PR +- Set the new version in `package.json` — it is the source of truth, and the publish + workflow refuses to run if the tag does not match it. +- Add the matching `## [X.Y.Z]` section to `CHANGELOG.md`. The workflow extracts this + section verbatim as the GitHub release notes, so it stops at the next `## ` heading. +- Leave already-released sections alone. Compare against `git show vX.Y.Z:CHANGELOG.md` + to confirm a published section still says what that version actually shipped. -**Dry-run mode** (validates all checks and previews changelog without git changes): +Run the same gates the workflow runs, so a failure surfaces before the release: ```sh -yarn release --dry-run +yarn typecheck && yarn build && yarn test:unit && yarn format:check ``` -#### 2. Review and merge the PR +Merge the PR once CI is green. -- Review the generated changelog in the PR -- Edit `CHANGELOG.md` if needed (push commits directly to the release branch) +#### 2. Tag the merge commit -> **⚠️ Warning:** The release tag is created **before** the PR is merged. If you push fixup commits to the release branch, you **must** move the tag to the latest commit before merging — otherwise those commits will be excluded from the published release: -> -> ```sh -> git tag -a -f vX.Y.Z -m "vX.Y.Z" -> git push -f origin vX.Y.Z -> ``` +```sh +git checkout publish && git pull --ff-only +git tag -a vX.Y.Z -m "vX.Y.Z" +git push origin vX.Y.Z +``` -Merge the PR when ready. +The tag must be on the merge commit, and `vX.Y.Z` must match `package.json`. -#### 3. Trigger the publish workflow +#### 3. Run the publish workflow from the tag -A Slack message in `#rum-electron-sdk-ops` is sent when the tag is pushed in step 1. It includes a link to the GitHub Actions publish workflow and reminds you to review and merge the PR first. +Actions → **Publish** → **Run workflow**, selecting the tag `vX.Y.Z` in the ref dropdown. -Open the workflow link, click **Run workflow**, and select the tag `vX.Y.Z` in the ref dropdown. +- `dry_run` runs the whole pipeline — build, gates, package contents — without publishing + to npm or creating a GitHub release. Use it first. +- `npm_tag` chooses the dist-tag. Publishing under `next` leaves `npm install` resolving to + whatever `latest` already points at, so a release can be staged. Promote it afterwards + without republishing: -> **Dry-run option:** Enable the `dry_run` toggle to run the full pipeline (build, validate, extract changelog) without publishing to npm or creating a GitHub release. Useful to validate the pipeline before the real publish. + ```sh + npm dist-tag add @flashcatcloud/electron-sdk@X.Y.Z latest + ``` [1]: https://gitmoji.carloscuesta.me/ diff --git a/package.json b/package.json index cc024726..b3642348 100644 --- a/package.json +++ b/package.json @@ -108,8 +108,7 @@ "test:integration": "playwright test -c e2e --grep @integration", "lint": "eslint . --max-warnings 0", "format": "prettier --write .", - "format:check": "prettier --check .", - "release": "node scripts/prepare-release.ts" + "format:check": "prettier --check ." }, "devDependencies": { "@eslint/js": "10.0.0", diff --git a/scripts/lib/semver.spec.ts b/scripts/lib/semver.spec.ts deleted file mode 100644 index 58d07598..00000000 --- a/scripts/lib/semver.spec.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { bumpVersion } from './semver.ts'; - -describe('bumpVersion', () => { - it('bumps patch', () => expect(bumpVersion('1.2.3', 'patch')).toBe('1.2.4')); - it('bumps minor and resets patch', () => expect(bumpVersion('1.2.3', 'minor')).toBe('1.3.0')); - it('bumps major and resets minor+patch', () => expect(bumpVersion('1.2.3', 'major')).toBe('2.0.0')); - it('handles 0.x versions', () => expect(bumpVersion('0.1.0', 'minor')).toBe('0.2.0')); - it('throws on invalid semver', () => expect(() => bumpVersion('not-semver', 'patch')).toThrow()); -}); diff --git a/scripts/lib/semver.ts b/scripts/lib/semver.ts deleted file mode 100644 index 879fb281..00000000 --- a/scripts/lib/semver.ts +++ /dev/null @@ -1,21 +0,0 @@ -export type VersionBump = 'major' | 'minor' | 'patch'; - -export function bumpVersion(current: string, bump: VersionBump): string { - // Intentionally only supports stable X.Y.Z versions — pre-release strings (e.g. 1.0.0-beta.1) are not supported. - const match = current.match(/^(\d+)\.(\d+)\.(\d+)$/); - if (!match) throw new Error(`Invalid semver: ${current}`); - let major = Number(match[1]); - let minor = Number(match[2]); - let patch = Number(match[3]); - if (bump === 'major') { - major++; - minor = 0; - patch = 0; - } else if (bump === 'minor') { - minor++; - patch = 0; - } else { - patch++; - } - return `${major}.${minor}.${patch}`; -} diff --git a/scripts/prepare-release.ts b/scripts/prepare-release.ts deleted file mode 100644 index 293bb23c..00000000 --- a/scripts/prepare-release.ts +++ /dev/null @@ -1,160 +0,0 @@ -import fs from 'node:fs'; -import os from 'node:os'; -import path from 'node:path'; -import readline from 'node:readline/promises'; -import { runMain, printLog } from './lib/executionUtils.ts'; -import { command } from './lib/command.ts'; -import { bumpVersion } from './lib/semver.ts'; -import { generateChangelogSection } from './lib/changelog.ts'; -import { getCommitsSinceLastTag } from './lib/git.ts'; - -const DRY_RUN = process.argv.includes('--dry-run'); -const ROOT = path.join(import.meta.dirname, '..'); -const CHANGELOG_PATH = path.join(ROOT, 'CHANGELOG.md'); -const PACKAGE_JSON_PATH = path.join(ROOT, 'package.json'); -const YARN_LOCK_PATH = path.join(ROOT, 'yarn.lock'); - -runMain(async () => { - runPreflightChecks(); - - // ── Sync with main ─────────────────────────────────────────────────────── - printLog('Syncing with main...'); - command`git checkout main`.withLogs().run(); - command`git pull origin main`.withLogs().run(); - - // ── Compute new version ────────────────────────────────────────────────── - const pkg = JSON.parse(fs.readFileSync(PACKAGE_JSON_PATH, 'utf-8')); - const newVersion = await promptNewVersion(pkg.version); - printLog(`\nPreparing release ${newVersion}...`); - - // ── Generate and review changelog ──────────────────────────────────────── - const commits = getCommitsSinceLastTag(); - const today = new Date().toISOString().slice(0, 10); - const draft = generateChangelogSection(newVersion, today, commits); - const editedSection = openEditorForReview(draft); - - if (DRY_RUN) { - printLog('\n[DRY RUN] Would apply the following changes:'); - printLog(` - Bump package.json version: ${pkg.version} → ${newVersion}`); - printLog(` - Insert new section into CHANGELOG.md`); - printLog(` - Create branch: release/v${newVersion}`); - printLog(` - Commit: v${newVersion}`); - printLog(` - Push branch and create annotated tag: v${newVersion}`); - printLog(` - Open GitHub PR`); - return; - } - - // ── Apply changes and create PR ─────────────────────────────────────────── - applyChanges(newVersion, editedSection); - const prUrl = createReleaseBranchAndPR(newVersion); - printLog(`\n✅ Release PR opened: ${prUrl}`); - printLog('\n⚠️ WARNING: The release tag has been pushed BEFORE the PR is merged.'); - printLog(' If you push fixup commits to the release branch, you MUST move the tag:'); - printLog(` git tag -a -f v${newVersion} -m "v${newVersion}" && git push -f origin v${newVersion}`); - printLog('\nMerge the PR, then trigger the publish workflow from the tag.'); -}); - -function runPreflightChecks(): void { - const editor = process.env.EDITOR; - if (!editor) throw new Error('$EDITOR is not set. Set it to your preferred editor (e.g. export EDITOR=vim)'); - - try { - command`gh auth status`.run(); - } catch { - throw new Error('gh CLI is not authenticated. Run: gh auth login'); - } - - const status = command`git status --porcelain`.run().trim(); - if (status) throw new Error(`Working tree is not clean:\n${status}`); -} - -async function promptNewVersion(currentVersion: string): Promise { - const options: { label: string; version: string }[] = [ - { label: 'patch', version: bumpVersion(currentVersion, 'patch') }, - { label: 'minor', version: bumpVersion(currentVersion, 'minor') }, - { label: 'major', version: bumpVersion(currentVersion, 'major') }, - ]; - - console.log(`\nCurrent version: ${currentVersion}`); - options.forEach(({ label, version }, i) => console.log(` ${i + 1}) ${label} → ${version}`)); - console.log(` 4) custom`); - - const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); - let newVersion!: string; - - while (true) { - const answer = (await rl.question('\nChoose [1-4]: ')).trim(); - if (answer === '1') { - newVersion = options[0].version; - break; - } - if (answer === '2') { - newVersion = options[1].version; - break; - } - if (answer === '3') { - newVersion = options[2].version; - break; - } - if (answer === '4') { - const custom = (await rl.question('Enter version (X.Y.Z): ')).trim(); - if (/^\d+\.\d+\.\d+$/.test(custom)) { - newVersion = custom; - break; - } - console.log('Invalid format. Use X.Y.Z'); - continue; - } - console.log('Please enter 1, 2, 3, or 4'); - } - rl.close(); - return newVersion; -} - -function openEditorForReview(section: string): string { - const editor = process.env.EDITOR!; - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'release-')); - const tmpFile = path.join(tmpDir, 'CHANGELOG.md'); - fs.writeFileSync(tmpFile, section, 'utf-8'); - printLog(`Opening $EDITOR (${editor}) for changelog review...`); - // $EDITOR may contain arguments (e.g. "code --wait") — pass as array so command handles splitting. - command`${[...editor.split(' '), tmpFile]}`.withLogs().run(); - return fs.readFileSync(tmpFile, 'utf-8'); -} - -function applyChanges(newVersion: string, editedSection: string): void { - // Insert new section after the file header, before the first existing version entry. - const existing = fs.existsSync(CHANGELOG_PATH) ? fs.readFileSync(CHANGELOG_PATH, 'utf-8') : ''; - const firstSectionIndex = existing.search(/^## /m); - const updatedChangelog = - firstSectionIndex === -1 - ? existing + (existing.endsWith('\n') ? '' : '\n') + editedSection - : existing.slice(0, firstSectionIndex) + editedSection + '\n' + existing.slice(firstSectionIndex); - fs.writeFileSync(CHANGELOG_PATH, updatedChangelog, 'utf-8'); - - // Bump version in root package.json only. - // (We read/write the root directly rather than using findPackageJsonFiles because - // we only need to modify the published package, not the workspace packages.) - const pkg = JSON.parse(fs.readFileSync(PACKAGE_JSON_PATH, 'utf-8')); - pkg.version = newVersion; - fs.writeFileSync(PACKAGE_JSON_PATH, JSON.stringify(pkg, null, 2) + '\n', 'utf-8'); - - // Regenerate lock file in case the version bump affects workspace resolution. - printLog('Regenerating lock file...'); - command`yarn install`.withLogs().run(); -} - -function createReleaseBranchAndPR(newVersion: string): string { - const branch = `release/v${newVersion}`; - command`git checkout -b ${branch}`.withLogs().run(); - command`git add ${CHANGELOG_PATH} ${PACKAGE_JSON_PATH} ${YARN_LOCK_PATH}`.run(); - command`git commit -m v${newVersion}`.run(); - command`git push origin ${branch}`.withLogs().run(); - - printLog('Creating release tag...'); - command`git tag -a v${newVersion} -m v${newVersion}`.withLogs().run(); - command`git push origin v${newVersion}`.withLogs().run(); - - printLog('Creating GitHub PR...'); - return command`gh pr create --title v${newVersion} --body ${''} --base main`.run().trim(); -}