From 864d01ce4b84c1d9b5aa408834b13158ae4d87da Mon Sep 17 00:00:00 2001 From: Saurabh Sharma Date: Fri, 6 Mar 2026 12:57:57 +0530 Subject: [PATCH 1/2] docs: update CONTRIBUTING.md and RELEASING.md to clarify release process and remove changeset requirement - Revised CONTRIBUTING.md to state that contributors do not need to add changesets; maintainers handle versioning and changelogs. - Updated RELEASING.md to reflect the new manual workflows for Prepare Release and Publish Release, detailing the steps for maintainers. - Added prerequisites for running workflows and clarified the versioning strategy for packages. --- .github/workflows/prepare-release.yml | 182 +++++++++++++ .github/workflows/publish-release.yml | 125 +++++++++ .github/workflows/release.yml | 93 ------- .github/workflows/rollback.yml | 166 ++++++++++++ CONTRIBUTING.md | 31 +-- RELEASING.md | 359 +++++++++----------------- 6 files changed, 603 insertions(+), 353 deletions(-) create mode 100644 .github/workflows/prepare-release.yml create mode 100644 .github/workflows/publish-release.yml delete mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/rollback.yml diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml new file mode 100644 index 0000000..5950f6e --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -0,0 +1,182 @@ +name: Prepare Release + +on: + workflow_dispatch: + inputs: + release_type: + description: 'Bump type' + required: true + type: choice + options: + - patch + - minor + - major + default: patch + +concurrency: + group: prepare-release-${{ github.ref }} + cancel-in-progress: false + +permissions: + contents: write + +jobs: + prepare: + runs-on: ubuntu-latest + steps: + - name: Check actor is admin + uses: actions/github-script@v7 + with: + script: | + const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: context.actor, + }); + if (data.permission !== 'admin') { + throw new Error(`Only repo admins can run Prepare Release. Current permission: ${data.permission}`); + } + + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: '1.3.10' + + - uses: actions/setup-node@v4 + with: + node-version: 20 + registry-url: 'https://registry.npmjs.org' + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Get current version and compute next version + id: version + run: | + CURRENT=$(node -p "require('./packages/core/package.json').version") + echo "current=$CURRENT" >> $GITHUB_OUTPUT + IFS='.' read -r MAJ MIN PATCH <<< "$CURRENT" + case "${{ inputs.release_type }}" in + major) echo "next=$((MAJ + 1)).0.0" >> $GITHUB_OUTPUT ;; + minor) echo "next=${MAJ}.$((MIN + 1)).0" >> $GITHUB_OUTPUT ;; + patch) echo "next=${MAJ}.${MIN}.$((PATCH + 1))" >> $GITHUB_OUTPUT ;; + *) echo "next=${MAJ}.${MIN}.$((PATCH + 1))" >> $GITHUB_OUTPUT ;; + esac + + - name: Get previous release tag + id: previous_tag + run: | + PREV=$(git tag -l '@markitjs/core@*' --sort=-v:refname | head -n1) + echo "tag=${PREV:-}" >> $GITHUB_OUTPUT + + - name: Generate release notes + id: release_notes + uses: actions/github-script@v7 + with: + script: | + const nextVersion = '${{ steps.version.outputs.next }}'; + const tagName = `@markitjs/core@${nextVersion}`; + const previousTag = '${{ steps.previous_tag.outputs.tag }}'; + const body = previousTag + ? { tag_name: tagName, previous_tag_name: previousTag, target_commitish: 'main' } + : { tag_name: tagName, target_commitish: 'main' }; + const { data } = await github.rest.repos.generateReleaseNotes({ + owner: context.repo.owner, + repo: context.repo.repo, + requestBody: body, + }); + const fs = require('fs'); + fs.writeFileSync('release-notes-body.md', data.body || ''); + core.setOutput('name', data.name || `Release ${nextVersion}`); + + - name: Create changeset file + run: | + find .changeset -name '*.md' ! -name 'README.md' -delete 2>/dev/null || true + RT="${{ inputs.release_type }}" + ID=$(openssl rand -hex 4) + { + echo "---" + echo '"@markitjs/core": '"$RT" + echo '"@markitjs/react": '"$RT" + echo '"@markitjs/angular": '"$RT" + echo "---" + echo "" + cat release-notes-body.md + } > ".changeset/release-${ID}.md" + + - name: Version packages + run: bunx changeset version + + - name: Check for changes + id: changes + run: | + if git diff --quiet packages/core/package.json packages/react/package.json packages/angular/package.json 2>/dev/null; then + echo "No version or changelog changes. Nothing to release." + echo "has_changes=false" >> $GITHUB_OUTPUT + exit 0 + fi + echo "has_changes=true" >> $GITHUB_OUTPUT + + - name: Persist release list + if: steps.changes.outputs.has_changes == 'true' + id: release_list + run: | + for pkg in core react angular; do + name=$(node -p "require('./packages/$pkg/package.json').name") + version=$(node -p "require('./packages/$pkg/package.json').version") + echo "${name}@${version}" + done | jq -R -s -c 'split("\n") | map(select(length > 0))' > release-list.json + cat release-list.json + + - name: Commit and push + if: steps.changes.outputs.has_changes == 'true' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add packages/core/package.json packages/react/package.json packages/angular/package.json \ + packages/core/CHANGELOG.md packages/react/CHANGELOG.md packages/angular/CHANGELOG.md \ + .changeset/ + git status + git commit -m "chore: version packages" || exit 0 + git push origin main + + - name: Create and push tags + if: steps.changes.outputs.has_changes == 'true' + run: | + while IFS= read -r line; do + tag="${line}" + [ -z "$tag" ] && continue + git tag "$tag" + git push origin "$tag" + done < <(jq -r '.[]' release-list.json) + + - name: Create draft GitHub Releases + if: steps.changes.outputs.has_changes == 'true' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const list = JSON.parse(fs.readFileSync('release-list.json', 'utf8')); + let body = ''; + try { + body = fs.readFileSync('release-notes-body.md', 'utf8'); + } catch (_) {} + for (const pkgVersion of list) { + const lastAt = pkgVersion.lastIndexOf('@'); + const name = pkgVersion.slice(0, lastAt); + const version = pkgVersion.slice(lastAt + 1); + const tag = pkgVersion; + const pkgDir = name.replace('@markitjs/', ''); + await github.rest.repos.createRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + tag_name: tag, + name: `${name} v${version}`, + body: body || `See [CHANGELOG](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/main/packages/${pkgDir}/CHANGELOG.md) for details.`, + draft: true, + prerelease: version.includes('-'), + }); + } diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml new file mode 100644 index 0000000..fa9d95d --- /dev/null +++ b/.github/workflows/publish-release.yml @@ -0,0 +1,125 @@ +name: Publish Release + +on: + workflow_dispatch: + inputs: + version: + description: 'Version to publish (e.g. 1.0.0)' + required: true + type: string + +concurrency: + group: publish-release-${{ github.ref }} + cancel-in-progress: false + +permissions: + contents: write + id-token: write + +jobs: + publish: + runs-on: ubuntu-latest + environment: npm-publish + steps: + - name: Check actor is admin + uses: actions/github-script@v7 + with: + script: | + const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: context.actor, + }); + if (data.permission !== 'admin') { + throw new Error(`Only repo admins can run Publish Release. Current permission: ${data.permission}`); + } + + - name: Validate version and tag exists + id: validate + uses: actions/github-script@v7 + with: + script: | + const version = '${{ inputs.version }}'; + if (!/^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$/.test(version)) { + core.setFailed('Invalid version format. Use semver (e.g. 1.0.0).'); + return; + } + const tag = `@markitjs/core@${version}`; + try { + await github.rest.repos.getReleaseByTag({ + owner: context.repo.owner, + repo: context.repo.repo, + tag: tag, + }); + } catch (err) { + if (err.status === 404) { + core.setFailed(`Version ${version} has not been prepared. Run Prepare Release first, then run Publish with that version.`); + } else { + throw err; + } + } + + - uses: actions/checkout@v4 + with: + ref: refs/tags/@markitjs/core@${{ inputs.version }} + fetch-depth: 0 + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: '1.3.10' + + - uses: actions/setup-node@v4 + with: + node-version: 20 + registry-url: 'https://registry.npmjs.org' + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Typecheck + run: bun run typecheck + + - name: Build + run: bun run build + + - name: Test + run: bun run test + + - name: Verify @markitjs/core CJS + run: node -e "require('./packages/core/dist/index.cjs')" + + - name: Verify @markitjs/core ESM + run: node --input-type=module -e "import './packages/core/dist/index.js'" + + - name: Verify @markitjs/react CJS + run: node -e "require('./packages/react/dist/index.cjs')" + + - name: Verify @markitjs/react ESM + run: node --input-type=module -e "import './packages/react/dist/index.js'" + + - name: Publish to npm + run: bunx changeset publish --no-git-tag + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Publish draft GitHub Releases + uses: actions/github-script@v7 + with: + script: | + const version = '${{ inputs.version }}'; + const packages = ['@markitjs/core', '@markitjs/react', '@markitjs/angular']; + for (const name of packages) { + const tag = `${name}@${version}`; + const { data: release } = await github.rest.repos.getReleaseByTag({ + owner: context.repo.owner, + repo: context.repo.repo, + tag: tag, + }); + await github.rest.repos.updateRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + release_id: release.id, + draft: false, + }); + } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 55575a8..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,93 +0,0 @@ -name: Release - -on: - push: - branches: [main] - workflow_dispatch: - -concurrency: - group: release-${{ github.ref }} - cancel-in-progress: false - -permissions: - contents: write - pull-requests: write - id-token: write - -jobs: - release: - runs-on: ubuntu-latest - environment: npm-publish - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - uses: oven-sh/setup-bun@v2 - with: - bun-version: '1.3.10' - - - uses: actions/setup-node@v4 - with: - node-version: 20 - registry-url: 'https://registry.npmjs.org' - - - name: Install dependencies - run: bun install --frozen-lockfile - - - name: Typecheck - run: bun run typecheck - - - name: Build - run: bun run build - - - name: Test - run: bun run test - - - name: Verify @markitjs/core CJS - run: node -e "require('./packages/core/dist/index.cjs')" - - - name: Verify @markitjs/core ESM - run: node --input-type=module -e "import './packages/core/dist/index.js'" - - - name: Verify @markitjs/react CJS - run: node -e "require('./packages/react/dist/index.cjs')" - - - name: Verify @markitjs/react ESM - run: node --input-type=module -e "import './packages/react/dist/index.js'" - - - name: Create Release Pull Request or Publish - id: changesets - uses: changesets/action@v1 - with: - publish: bunx changeset publish - version: bunx changeset version - title: 'chore: version packages' - commit: 'chore: version packages' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - - name: Create GitHub Releases - if: steps.changesets.outputs.published == 'true' - uses: actions/github-script@v7 - with: - script: | - const publishedPackages = ${{ steps.changesets.outputs.publishedPackages }}; - - for (const pkg of publishedPackages) { - const tag = `${pkg.name}@${pkg.version}`; - const pkgDir = pkg.name.replace('@markitjs/', ''); - - await github.rest.repos.createRelease({ - owner: context.repo.owner, - repo: context.repo.repo, - tag_name: tag, - name: `${pkg.name} v${pkg.version}`, - body: `Published to npm: [\`${pkg.name}@${pkg.version}\`](https://www.npmjs.com/package/${pkg.name}/v/${pkg.version})\n\nSee [CHANGELOG](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/main/packages/${pkgDir}/CHANGELOG.md) for details.`, - draft: false, - prerelease: pkg.version.includes('-'), - }); - } diff --git a/.github/workflows/rollback.yml b/.github/workflows/rollback.yml new file mode 100644 index 0000000..acdb53d --- /dev/null +++ b/.github/workflows/rollback.yml @@ -0,0 +1,166 @@ +name: Rollback + +on: + workflow_dispatch: + inputs: + version: + description: 'Version to roll back (e.g. 1.0.0)' + required: true + type: string + confirm_rollback: + description: 'Set to "true" or the version to confirm rollback' + required: false + type: string + +concurrency: + group: rollback-${{ github.ref }} + cancel-in-progress: false + +permissions: + contents: write + +jobs: + rollback: + runs-on: ubuntu-latest + environment: npm-publish + steps: + - name: Check actor is admin + uses: actions/github-script@v7 + with: + script: | + const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: context.actor, + }); + if (data.permission !== 'admin') { + throw new Error(`Only repo admins can run Rollback. Current permission: ${data.permission}`); + } + + - name: Require confirmation + run: | + CONFIRM="${{ inputs.confirm_rollback }}" + VERSION="${{ inputs.version }}" + if [ -z "$CONFIRM" ] || [ "$CONFIRM" != "true" ] && [ "$CONFIRM" != "$VERSION" ]; then + echo "::error::Set confirm_rollback to 'true' or to the version ($VERSION) to confirm rollback." + exit 1 + fi + + - name: Validate version + id: validate + uses: actions/github-script@v7 + with: + script: | + const version = '${{ inputs.version }}'; + if (!/^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$/.test(version)) { + core.setFailed('Invalid version format. Use semver (e.g. 1.0.0).'); + return; + } + const tag = `@markitjs/core@${version}`; + try { + await github.rest.repos.getReleaseByTag({ + owner: context.repo.owner, + repo: context.repo.repo, + tag: tag, + }); + } catch (err) { + if (err.status === 404) { + core.setFailed(`No release found for version ${version}.`); + } else { + throw err; + } + } + + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Delete GitHub Releases + uses: actions/github-script@v7 + with: + script: | + const version = '${{ inputs.version }}'; + const packages = ['@markitjs/core', '@markitjs/react', '@markitjs/angular']; + for (const name of packages) { + const tag = `${name}@${version}`; + try { + const { data: release } = await github.rest.repos.getReleaseByTag({ + owner: context.repo.owner, + repo: context.repo.repo, + tag: tag, + }); + await github.rest.repos.deleteRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + release_id: release.id, + }); + } catch (err) { + if (err.status !== 404) throw err; + } + } + + - name: Delete tags + uses: actions/github-script@v7 + with: + script: | + const version = '${{ inputs.version }}'; + const packages = ['@markitjs/core', '@markitjs/react', '@markitjs/angular']; + for (const name of packages) { + const tag = `${name}@${version}`; + try { + await github.rest.git.deleteRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: `refs/tags/${tag}`, + }); + } catch (err) { + if (err.status !== 404) throw err; + } + } + + - name: Unpublish or deprecate on npm + uses: actions/setup-node@v4 + with: + node-version: 20 + registry-url: 'https://registry.npmjs.org' + + - name: Unpublish or deprecate each package + run: | + V="${{ inputs.version }}" + for pkg in @markitjs/core @markitjs/react @markitjs/angular; do + if npm unpublish "${pkg}@${V}" --force 2>/dev/null; then + echo "Unpublished ${pkg}@${V}" + else + npm deprecate "${pkg}@${V}" "Rolled back. Use a different version." || true + echo "Deprecated ${pkg}@${V}" + fi + done + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Find and revert version commit + id: revert + run: | + V="${{ inputs.version }}" + COMMIT=$(git log main --oneline -100 -- packages/core/package.json | while read hash msg; do + if git show "$hash:packages/core/package.json" 2>/dev/null | grep -q "\"version\": \"$V\""; then + echo "$hash" + break + fi + done) + if [ -z "$COMMIT" ]; then + echo "::warning::Could not find commit that set version to $V. Skipping revert." + echo "commit=" >> $GITHUB_OUTPUT + else + echo "commit=$COMMIT" >> $GITHUB_OUTPUT + fi + + - name: Revert commit and push + if: steps.revert.outputs.commit != '' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout main + git pull origin main + git revert ${{ steps.revert.outputs.commit }} --no-edit + git push origin main diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6f2cc49..f8c2dae 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,8 +46,9 @@ bun run bench # Run Playwright performance benchmarks 2. Make your changes. Keep commits focused and descriptive. 3. Run `bun run format` to auto-format. 4. Run `bun run build && bun run test && bun run typecheck` to verify everything passes. -5. Add a changeset (see below). -6. Open a pull request against `main`. +5. Open a pull request against `main`. + +You do **not** need to add a changeset. Releases are cut by maintainers using the Prepare Release and Publish Release workflows (see [RELEASING.md](RELEASING.md)). ## Commit messages @@ -64,21 +65,7 @@ test: add edge case for nested highlights ## Changesets -We use [Changesets](https://github.com/changesets/changesets) for versioning and changelogs. - -**Before opening a PR with user-facing changes**, run: - -```bash -bunx changeset -``` - -This prompts you to: - -1. Select which package(s) changed. -2. Choose the semver bump type (patch / minor / major). -3. Write a summary of the change. - -A markdown file is created in `.changeset/` — commit it with your PR. CI will warn if a changeset is missing. +Releases use [Changesets](https://github.com/changesets/changesets) under the hood, but **contributors do not run `bunx changeset`**. Maintainers (repo admins) run the **Prepare Release** workflow, which creates the changeset and bumps versions using GitHub-generated release notes. If you’re curious how versioning works, see [RELEASING.md](RELEASING.md). ## Pull Request Guidelines @@ -110,13 +97,13 @@ markit/ ## Releasing -The release process is documented in [RELEASING.md](RELEASING.md). In short: +Releases are documented in [RELEASING.md](RELEASING.md). In short: -1. Add a changeset to your PR with `bunx changeset` -2. After merge, a bot opens a "Version Packages" PR with bumped versions and changelogs -3. When a maintainer merges that PR, packages are published to npm automatically +1. Maintainers merge PRs to `main` (no changesets in PRs). +2. When ready to release, an **admin** runs the **Prepare Release** workflow (chooses patch / minor / major). It generates release notes, bumps versions, creates tags, and draft GitHub Releases. +3. The same admin runs the **Publish Release** workflow with the version number. It runs tests, publishes to npm, and publishes the GitHub Releases. -Contributors only need to worry about step 1 — the rest is handled by automation. +Contributors only need to merge their PRs — the rest is done by maintainers via the two workflows. ## Questions? diff --git a/RELEASING.md b/RELEASING.md index ecb7164..5172729 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,29 +1,35 @@ # Releasing -This guide covers the complete release process for MarkIt. It is intended for maintainers with npm publish access. +This guide covers the release process for MarkIt. It is intended for maintainers with npm publish access. -For contributing changes (including how to add changesets), see [CONTRIBUTING.md](CONTRIBUTING.md). +For contributing (no changesets required in PRs), see [CONTRIBUTING.md](CONTRIBUTING.md). --- ## How It Works -MarkIt uses [Changesets](https://github.com/changesets/changesets) for versioning and changelog generation, with GitHub Actions automating the publish pipeline. +MarkIt uses [Changesets](https://github.com/changesets/changesets) for versioning and changelog generation. Releases are fully automated via two **manual** GitHub Actions runs — only repo **admins** can trigger them. Nothing runs automatically on push. -The release process has two steps: +**Two steps:** -1. **Version PR** — When PRs with changesets merge to `main`, a bot opens (or updates) a "Version Packages" pull request that bumps versions and updates changelogs. -2. **Publish** — When a maintainer merges the Version PR, the release workflow publishes all bumped packages to npm and creates GitHub Releases. +1. **Prepare release** — You run the **Prepare Release** workflow, choose the release type (patch / minor / major). It generates release notes from GitHub (commits/PRs since last release), creates a changeset, bumps versions, updates changelogs, pushes to `main`, creates tags, and creates **draft** GitHub Releases. +2. **Publish release** — When you’re ready to ship, you run the **Publish Release** workflow and enter the version (e.g. `1.0.0`). It runs tests, build, and pre-checks, publishes to npm, and publishes the draft GitHub Releases. -This two-step model ensures a human always reviews version bumps and changelog entries before anything is published. +There is no “Version PR” — you never run `bunx changeset` in PRs. Prepare creates the changeset when you run it. --- -## Versioning Strategy +## Prerequisites (one-time setup) + +- **GitHub:** Only users with **admin** role on the repo can run Prepare, Publish, and Rollback workflows. +- **npm:** An npm account, the `@markitjs` scope, and a Granular Access Token (write, bypass 2FA). Store it in a GitHub Environment secret (see [npm Setup](#npm-setup) below). +- **GitHub Environment:** Create an environment named `npm-publish` with the `NPM_TOKEN` secret. The Publish and Rollback workflows use it. -All packages (`@markitjs/core`, `@markitjs/react`, `@markitjs/angular`) use **fixed versioning** — they always share the same version number. When any package gets a changeset, all three are bumped together. +--- + +## Versioning Strategy -This is configured in `.changeset/config.json` via the `fixed` array. +All packages (`@markitjs/core`, `@markitjs/react`, `@markitjs/angular`) use **fixed versioning** — they always share the same version number. This is configured in `.changeset/config.json` via the `fixed` array. ### Semantic Versioning @@ -33,94 +39,102 @@ This is configured in `.changeset/config.json` via the `fixed` array. | Minor | New feature, backwards-compatible | `1.0.0 → 1.1.0` | | Major | Breaking change | `1.0.0 → 2.0.0` | -### Pre-1.0 - -While packages are at `0.x.y`, minor bumps may contain breaking changes. This is standard semver for pre-stable software. +You choose the bump when you run **Prepare Release** (patch / minor / major). --- -## Stable Release (Step by Step) +## Stable Release (step by step) -### 1. Add a changeset to your PR +### 1. Merge PRs to main -Before opening a PR with user-facing changes: +Develop as usual. Open PRs, get them merged to `main`. **You do not add changesets** — no `bunx changeset` in PRs. CI still runs (build, test, typecheck, etc.). -```bash -bunx changeset -``` +### 2. Prepare the release + +When you’re ready to cut a release: -This prompts you to: +1. Go to **GitHub → Actions**. +2. Select the **Prepare Release** workflow. +3. Click **Run workflow**. +4. Choose **release_type**: `patch`, `minor`, or `major` (dropdown). +5. Run the workflow. -- Select which packages changed (all are bumped together due to fixed versioning) -- Choose the semver bump type (patch / minor / major) -- Write a human-readable summary of the change +The workflow will: -A `.changeset/.md` file is created. Commit it with your PR. +- Generate release notes using GitHub’s API (from commits/PRs since the last release). +- Create a changeset with that summary and your chosen bump type. +- Run `changeset version` (bump packages, update CHANGELOGs). +- Commit and push to `main`. +- Create git tags (`@markitjs/core@x.y.z`, etc.). +- Create **draft** GitHub Releases (you can edit the draft body on GitHub if needed). -### 2. PR merges to main +If there’s nothing to release (e.g. no new commits since last tag), the run exits with a message and does nothing. -CI validates: format, typecheck, build, test, bundle size, and changeset presence. +### 3. Publish the release -After merge, the release workflow (`release.yml`) runs and the `changesets/action` detects pending changeset files. It opens (or updates) a pull request titled **"chore: version packages"**. +When you’re happy with the draft and want to ship: -### 3. Review the Version PR +1. Go to **GitHub → Actions**. +2. Select the **Publish Release** workflow. +3. Click **Run workflow**. +4. Enter **version** (e.g. `1.0.0` — the version that Prepare just created). +5. Run the workflow. -The Version PR contains: +The workflow will: -- Version bumps in all `package.json` files -- Updated `CHANGELOG.md` in each package directory -- All `.changeset/*.md` files consumed +- Check out the release at that version (by tag). +- Run typecheck, build, test, and CJS/ESM checks. +- Publish all three packages to npm. +- Publish the draft GitHub Releases (they become visible on the Releases page). -If more PRs with changesets merge while the Version PR is open, it auto-updates to include them. The highest bump type wins (e.g., one patch + one minor = minor). +### 4. Verify -### 4. Merge the Version PR +- Check [npmjs.com/package/@markitjs/core](https://www.npmjs.com/package/@markitjs/core) for the new version. +- Check the [Releases page](https://github.com/saurabhiam/markit/releases) for the new GitHub Releases. -This triggers the release workflow again. This time there are no pending changesets, so `changesets/action` runs `changeset publish`: +--- + +## When to use Rollback + +Use the **Rollback** workflow when you need to undo a release (e.g. bad publish, wrong version). -- Publishes all bumped packages to npm (under the `latest` tag) -- Creates git tags (`@markitjs/core@x.y.z`, `@markitjs/react@x.y.z`, `@markitjs/angular@x.y.z`) -- Creates GitHub Releases with links to changelogs and npm +1. Go to **GitHub → Actions** → **Rollback**. +2. Click **Run workflow**. +3. Enter **version** (e.g. `1.0.0`) to roll back. +4. Optionally set **confirm_rollback** (e.g. `true` or the version again) so the job doesn’t fail with “Set confirm_rollback to confirm.” +5. Run the workflow. -### 5. Verify +The workflow will: -- Check [npmjs.com/package/@markitjs/core](https://www.npmjs.com/package/@markitjs/core) for the new version -- Check the [Releases page](https://github.com/saurabhiam/markit/releases) for GitHub Releases -- Documentation auto-deploys to GitHub Pages on the same `main` push +- Validate that the version exists (semver + at least one release/tag). +- Delete the GitHub Releases for that version. +- Delete the git tags. +- Unpublish the packages from npm (or deprecate them if unpublish is no longer allowed, e.g. after 72 hours). +- Revert the “chore: version packages” commit on `main`. + +Only repo admins can run Rollback. --- ## Prerelease (Alpha / Beta / RC) -Prereleases allow publishing unstable versions for testing without affecting the `latest` npm tag. +Prereleases allow publishing unstable versions without affecting the `latest` npm tag. The **Prerelease** workflow (if enabled) runs on push to `next`. ### Enter prerelease mode ```bash git checkout -b next main - -# Choose the prerelease type: alpha, beta, or rc -bunx changeset pre enter beta - +bunx changeset pre enter beta # or alpha, rc git add .changeset/pre.json git commit -m "chore: enter beta prerelease mode" git push -u origin next ``` -### Develop on the next branch - -Work normally — create PRs targeting `next`, add changesets. When PRs merge to `next`, the prerelease workflow (`prerelease.yml`) handles versioning and publishing automatically. - -Published versions look like: `1.0.0-beta.0`, `1.0.0-beta.1`, etc. - -### npm dist-tags for prereleases +### Develop on next -| Prerelease type | npm tag | Install command | -| --------------- | ------- | ---------------------------------- | -| `alpha` | `alpha` | `npm install @markitjs/core@alpha` | -| `beta` | `beta` | `npm install @markitjs/core@beta` | -| `rc` | `next` | `npm install @markitjs/core@next` | +Create PRs targeting `next`. When they merge, the prerelease workflow handles versioning and publishing. Versions look like `1.0.0-beta.0`, `1.0.0-beta.1`, etc. -### Exit prerelease mode and go stable +### Exit prerelease mode ```bash bunx changeset pre exit @@ -128,7 +142,7 @@ git add .changeset/pre.json git commit -m "chore: exit prerelease mode" ``` -Merge the `next` branch back to `main` via a PR. The next release from `main` will be a stable version. +Merge `next` back to `main` via a PR when you’re ready for the next stable release. --- @@ -142,211 +156,80 @@ Merge the `next` branch back to `main` via a PR. The next release from `main` wi ### Token creation -As of November 2025, npm only supports **Granular Access Tokens**. Write-enabled tokens have a **maximum expiration of 90 days**. +npm supports **Granular Access Tokens** with a maximum expiration of 90 days for write tokens. -1. Go to [npmjs.com](https://www.npmjs.com) → click your avatar → **Access Tokens** -2. Click **Generate New Token** (Granular Access Token is the only option) -3. Configure: +1. Go to [npmjs.com](https://www.npmjs.com) → avatar → **Access Tokens** +2. **Generate New Token** (Granular Access Token) +3. Configure: name, **Packages and scopes** → Read and write for `@markitjs`, **Bypass two-factor authentication** checked (required for CI), expiration 90 days. +4. Copy the token — it is shown only once. -| Field | Value | -| ------------------------------------ | ------------------------------------------------- | -| **Token name** | `github-actions-markitjs` | -| **Description** | `CI/CD publishing from GitHub Actions` | -| **Bypass two-factor authentication** | Checked (required for CI — no human to enter 2FA) | -| **Allowed IP Ranges** | Leave blank (GitHub Actions IPs rotate) | -| **Expiration** | 90 days (maximum allowed for write tokens) | -| **Packages and scopes** | Read and write, scoped to `@markitjs` | -| **Organizations** | `markitjs` → Read and write | +### GitHub Environment -4. Click **Generate Token** -5. **Copy the token immediately** — it is shown only once +1. Repo → **Settings** → **Environments** → **New environment** → name: `npm-publish` +2. Add protection rules if desired (e.g. required reviewers, deployment branches: `main`, `next`). +3. **Environment secrets** → **Add secret** → Name: `NPM_TOKEN`, Value: the npm token. -### GitHub Environment setup +The **Publish Release** and **Rollback** workflows use the `npm-publish` environment so they have access to `NPM_TOKEN`. -The release workflow uses a GitHub Environment called `npm-publish` for protection: +### Token rotation (every ~80 days) -1. Go to the repository → **Settings** → **Environments** → **New environment** -2. Name: `npm-publish` -3. Configure protection rules: +Set a calendar reminder. Before the token expires: -| Setting | Value | Why | -| ----------------------- | --------------------------------- | -------------------------------------------- | -| **Required reviewers** | Add maintainer(s) | Every publish requires human approval | -| **Prevent self-review** | Unchecked (for solo maintainers) | You need to approve your own releases | -| **Wait timer** | 0 | No delay needed | -| **Deployment branches** | Selected branches: `main`, `next` | Only release/prerelease branches can publish | +1. Generate a new token on npm (same settings). +2. Update `NPM_TOKEN` in the GitHub `npm-publish` environment. +3. Delete the old token on npm. -4. Under **Environment secrets** → **Add secret**: - - Name: `NPM_TOKEN` - - Value: the token from the step above - -Environment secrets are more secure than repository-level secrets — they are only exposed to workflows that reference the `npm-publish` environment. - -The release workflow uses `actions/setup-node` with `registry-url` to configure npm authentication at runtime. No `.npmrc` file is needed in the repository. - -### Token rotation (every 80 days) - -npm write tokens expire after 90 days. Set a **calendar reminder for 80 days** after each token creation. - -**Rotation procedure:** - -1. Go to [npmjs.com](https://www.npmjs.com) → Access Tokens → **Generate New Token** (same settings as above) -2. Go to GitHub → Settings → Environments → `npm-publish` → update the `NPM_TOKEN` secret with the new token -3. Go back to npmjs.com → **delete the old token** -4. Set a new 80-day calendar reminder - -**If the token expires before rotation:** - -- The release workflow will fail at the publish step with an authentication error -- No packages will be published (safe failure — nothing breaks) -- Generate a new token, update the secret, and re-run the workflow manually via `workflow_dispatch` +If the token expires, the Publish workflow will fail at the npm step; fix the secret and re-run. --- ## Troubleshooting -### Version PR not appearing - -The `changesets/action` only creates a Version PR when `.changeset/*.md` files exist (excluding `README.md`). Verify: - -```bash -ls .changeset/*.md -``` - -If no files exist, no changeset was added. Run `bunx changeset` to create one. - -### Publish failed - -1. Check the [Actions tab](https://github.com/saurabhiam/markit/actions/workflows/release.yml) for the failed run -2. Common causes: - - **npm token expired** (most common): Rotate the token — see [Token rotation](#token-rotation-every-80-days) above - - **Build failure**: Fix the build, the next push to `main` will re-trigger - - **Package name conflict**: Ensure the `@markitjs` scope is available on npm - - **2FA prompt**: Ensure the token was created with "Bypass two-factor authentication" checked -3. After fixing, re-run the release workflow manually via `workflow_dispatch` - -### npm token expired - -Symptoms: publish step fails with `401 Unauthorized` or `ENEEDAUTH`. - -Fix: Generate a new token on npmjs.com, update `NPM_TOKEN` in the `npm-publish` GitHub Environment, re-run the workflow. See [Token rotation](#token-rotation-every-80-days). +### Prepare: “No changes to release” -### Version PR has wrong bump level +The workflow found no new commits (or no diff) since the last release. Merge more PRs to `main` and run Prepare again, or confirm the last tag is what you expect. -Edit the changeset `.md` files before merging the Version PR. Or close the Version PR, update the changesets on `main`, and let the bot create a new one. +### Publish: version not found -### Stuck Version PR +You entered a version that wasn’t prepared. Run **Prepare Release** first, then run **Publish Release** with the version that Prepare produced (check the Prepare run output or the draft releases). -If the Version PR gets stale or conflicts: +### Publish failed (tests, build, npm) -1. Close the existing Version PR -2. Manually trigger the release workflow via Actions → Release → Run workflow -3. The action will create a fresh Version PR +1. Check the [Actions](https://github.com/saurabhiam/markit/actions) run for the failing step. +2. Common causes: npm token expired (rotate and update `NPM_TOKEN`), build/test failure (fix on `main` and re-run Publish). +3. Re-run the **Publish Release** workflow after fixing. ### Accidentally published a bad version -npm packages cannot be unpublished after 72 hours. Instead, deprecate: - -```bash -npm deprecate @markitjs/core@1.2.3 "This version has a critical bug. Please use 1.2.4." -npm deprecate @markitjs/react@1.2.3 "This version has a critical bug. Please use 1.2.4." -npm deprecate @markitjs/angular@1.2.3 "This version has a critical bug. Please use 1.2.4." -``` - -Then publish a patch fix as quickly as possible. - -Within 72 hours, you can unpublish: - -```bash -npm unpublish @markitjs/core@1.2.3 -``` +Use **Rollback** with that version. It will unpublish (if within 72 hours) or deprecate on npm, delete releases and tags, and revert the version commit. After 72 hours, npm only allows deprecation, not unpublish. --- -## First Release (One-Time Setup) - -Before the very first publish, verify these prerequisites: +## First release (one-time) -``` -[ ] npm organization @markitjs exists on npmjs.com -[ ] npm Granular Access Token created (write, @markitjs scope, bypass 2FA) -[ ] GitHub Environment npm-publish created with NPM_TOKEN secret -[ ] GitHub Environment has required reviewers and branch restrictions -[ ] All three packages have correct names in package.json (@markitjs/core, @markitjs/react, @markitjs/angular) -[ ] .changeset/config.json has the fixed group and ignore list configured -``` - -To trigger the first release: +Before the first publish: -1. Create a changeset: `bunx changeset` — select a package, choose the bump type (likely `minor` for first feature release), write a summary -2. Commit and push to `main` -3. The release workflow opens a "chore: version packages" PR -4. Review the PR — verify versions and changelogs -5. Merge it — packages publish to npm, tags and GitHub Releases are created -6. Verify: `npm info @markitjs/core` should show the published version - ---- +- [ ] npm org `@markitjs` exists, token created, `NPM_TOKEN` in GitHub Environment `npm-publish` +- [ ] `.changeset/config.json` has the fixed group and ignore list -## Hotfix Release +To do the first release: -For critical fixes that need to ship immediately: - -1. Create a branch from `main`: `git checkout -b fix/critical-issue main` -2. Fix the issue -3. Add a changeset: `bunx changeset` (select `patch`) -4. Open and merge PR to `main` -5. The Version PR will appear — merge it immediately -6. Packages publish automatically +1. Merge at least one PR to `main` (or have commits since the repo start). +2. Run **Prepare Release** with **release_type** = `minor` (or `major` for 1.0.0). +3. Run **Publish Release** with the version shown (e.g. `0.1.0` or `1.0.0`). +4. Verify on npm and the Releases page. --- -## Maintainer Checklist - -### Before merging a Version PR - -``` -[ ] CHANGELOG entries are accurate and describe user-facing impact -[ ] Version bump level is correct (patch / minor / major) -[ ] No unintended packages being bumped -[ ] Breaking changes (if any) are clearly documented -[ ] CI checks pass on the Version PR -``` - -### After publish (automated, but verify) - -``` -[ ] Packages visible on npmjs.com with correct version -[ ] npm install @markitjs/core@ works -[ ] GitHub Releases created with changelog links -[ ] Documentation site updated (auto-deploys) -``` - -### Every 80 days (token rotation) - -``` -[ ] Generate a new npm token (90-day expiry, write access, @markitjs scope) -[ ] Update NPM_TOKEN in GitHub Environment npm-publish -[ ] Delete the old token on npmjs.com -[ ] Set next 80-day calendar reminder -``` - -### Quarterly maintenance - -``` -[ ] GitHub Environment protection rules are still correct -[ ] Review and clean up any stale prerelease tags on npm -[ ] Verify npm org membership and permissions are current -``` - ---- - -## Workflow Files Reference - -| Workflow | File | Trigger | Purpose | -| ----------------- | ----------------------------------------- | ------------------------------ | ------------------------------------------- | -| CI | `.github/workflows/ci.yml` | Push/PR to `main` | Build, test, typecheck, format, node compat | -| Release | `.github/workflows/release.yml` | Push to `main`, manual | Version PR management + npm publish | -| Prerelease | `.github/workflows/prerelease.yml` | Push to `next` | Prerelease version management + publish | -| Docs | `.github/workflows/docs.yml` | Push to `main` (path-filtered) | Build and deploy documentation | -| Bundle Size | `.github/workflows/bundle-size.yml` | PR to `main` | Measure and report bundle sizes | -| Dependency Review | `.github/workflows/dependency-review.yml` | PR to `main` | Block vulnerable/problematic dependencies | +## Workflow reference + +| Workflow | File | Trigger | Purpose | +| ----------------- | ----------------------------------------- | -------------------- | -------------------------------------------- | +| Prepare Release | `.github/workflows/prepare-release.yml` | Manual (admin only) | release_type → changeset, version, tags, drafts | +| Publish Release | `.github/workflows/publish-release.yml` | Manual (admin only) | version → test, build, npm publish, publish releases | +| Rollback | `.github/workflows/rollback.yml` | Manual (admin only) | version → delete releases/tags, unpublish/deprecate, revert | +| CI | `.github/workflows/ci.yml` | Push/PR to `main` | Build, test, typecheck, format | +| Prerelease | `.github/workflows/prerelease.yml` | Push to `next` | Prerelease version + publish | +| Docs | `.github/workflows/docs.yml` | Push to `main` | Build and deploy documentation | +| Bundle Size | `.github/workflows/bundle-size.yml` | PR to `main` | Measure bundle sizes | +| Dependency Review | `.github/workflows/dependency-review.yml` | PR to `main` | Block vulnerable dependencies | From 848161e4c67a099b3695cf5cd295aecb48392d33 Mon Sep 17 00:00:00 2001 From: Saurabh Sharma Date: Fri, 6 Mar 2026 12:58:55 +0530 Subject: [PATCH 2/2] docs: enhance RELEASING.md table formatting for improved clarity - Updated the workflow reference table in RELEASING.md to enhance readability and alignment. - Ensured consistent formatting across all workflow descriptions for better understanding of release processes. --- RELEASING.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index 5172729..0af6586 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -223,13 +223,13 @@ To do the first release: ## Workflow reference -| Workflow | File | Trigger | Purpose | -| ----------------- | ----------------------------------------- | -------------------- | -------------------------------------------- | -| Prepare Release | `.github/workflows/prepare-release.yml` | Manual (admin only) | release_type → changeset, version, tags, drafts | -| Publish Release | `.github/workflows/publish-release.yml` | Manual (admin only) | version → test, build, npm publish, publish releases | -| Rollback | `.github/workflows/rollback.yml` | Manual (admin only) | version → delete releases/tags, unpublish/deprecate, revert | -| CI | `.github/workflows/ci.yml` | Push/PR to `main` | Build, test, typecheck, format | -| Prerelease | `.github/workflows/prerelease.yml` | Push to `next` | Prerelease version + publish | -| Docs | `.github/workflows/docs.yml` | Push to `main` | Build and deploy documentation | -| Bundle Size | `.github/workflows/bundle-size.yml` | PR to `main` | Measure bundle sizes | -| Dependency Review | `.github/workflows/dependency-review.yml` | PR to `main` | Block vulnerable dependencies | +| Workflow | File | Trigger | Purpose | +| ----------------- | ----------------------------------------- | ------------------- | ----------------------------------------------------------- | +| Prepare Release | `.github/workflows/prepare-release.yml` | Manual (admin only) | release_type → changeset, version, tags, drafts | +| Publish Release | `.github/workflows/publish-release.yml` | Manual (admin only) | version → test, build, npm publish, publish releases | +| Rollback | `.github/workflows/rollback.yml` | Manual (admin only) | version → delete releases/tags, unpublish/deprecate, revert | +| CI | `.github/workflows/ci.yml` | Push/PR to `main` | Build, test, typecheck, format | +| Prerelease | `.github/workflows/prerelease.yml` | Push to `next` | Prerelease version + publish | +| Docs | `.github/workflows/docs.yml` | Push to `main` | Build and deploy documentation | +| Bundle Size | `.github/workflows/bundle-size.yml` | PR to `main` | Measure bundle sizes | +| Dependency Review | `.github/workflows/dependency-review.yml` | PR to `main` | Block vulnerable dependencies |