Set up auto-publishing and alpha branch (#4390)

* Set up alpha branch and auto-publishing

* Restore 4.5.0 version number

* Set up post-merge hook to stablize version and fix tests

* Restore .gitattributes

* Fix to publish.yml

* Revert PNPM version to version 8 for Node 16 compatibility

* Another attempt to resolve PNPM version

* Another attempt to resolve PNPM version
This commit is contained in:
Matthew Dean
2025-12-14 11:37:47 -08:00
committed by GitHub
parent 432286970a
commit 5cd329c9d0
15 changed files with 4479 additions and 8452 deletions

192
.github/TESTING_PUBLISHING.md vendored Normal file
View File

@@ -0,0 +1,192 @@
# Testing the Publishing Flow
This guide explains how to test the publishing workflow without actually publishing to npm.
## Dry Run Mode
The publishing script supports a dry-run mode that shows what would happen without making any changes:
```bash
# Test from master branch
git checkout master
DRY_RUN=true pnpm run publish
# Or use the flag
pnpm run publish --dry-run
```
Dry-run mode will:
- ✅ Show what version would be created
- ✅ Show what packages would be published
- ✅ Show what git operations would happen
-**NOT** commit any changes
-**NOT** create git tags
-**NOT** push to remote
-**NOT** publish to npm
## Testing Locally
### 1. Test Version Calculation
```bash
# Check current version
node -p "require('./packages/less/package.json').version"
# Run dry-run to see what version would be created
DRY_RUN=true pnpm run publish
```
### 2. Test Branch Validation
```bash
# Try from a feature branch (should fail)
git checkout -b test-branch
pnpm run publish
# Should error: "Publishing is only allowed from 'master' or 'alpha' branches"
```
### 3. Test Alpha Branch Validations
```bash
# Switch to alpha branch
git checkout alpha
# Test with dry-run
DRY_RUN=true GITHUB_REF_NAME=alpha pnpm run publish
# This will show:
# - Version validation (must contain -alpha.)
# - Master sync check
# - Version comparison with master
```
### 4. Test Version Override
```bash
# Test explicit version override
EXPLICIT_VERSION=4.5.0 DRY_RUN=true pnpm run publish
# Should show: "✨ Using explicit version: 4.5.0"
```
## Testing the GitHub Actions Workflow
### 1. Test Workflow Syntax
```bash
# Validate workflow YAML
gh workflow view publish.yml
# Or use act (local GitHub Actions runner)
act push -W .github/workflows/publish.yml
```
### 2. Test on a Test Branch
Create a test branch that mimics master/alpha:
```bash
# Create test branch from master
git checkout -b test-publish-master master
# Make a small change
echo "# test" >> TEST.md
git add TEST.md
git commit -m "test: publishing workflow"
# Push to trigger workflow (if you want to test the full flow)
# Note: This will actually try to publish if version changed!
```
### 3. Test Workflow Manually
You can manually trigger the workflow from GitHub Actions UI:
1. Go to Actions tab
2. Select "Publish to NPM" workflow
3. Click "Run workflow"
4. Select branch and run
**Warning**: This will actually publish if conditions are met!
## Testing Specific Scenarios
### Test Master Branch Publishing
```bash
git checkout master
DRY_RUN=true pnpm run publish
# Should show:
# - Patch version increment (e.g., 4.4.2 → 4.4.3)
# - Publishing with 'latest' tag
# - Regular release creation
```
### Test Alpha Branch Publishing
```bash
git checkout alpha
DRY_RUN=true GITHUB_REF_NAME=alpha pnpm run publish
# Should show:
# - Alpha version increment (e.g., 5.0.0-alpha.1 → 5.0.0-alpha.2)
# - Publishing with 'alpha' tag
# - Pre-release creation
# - All alpha validations passing
```
### Test Version Validation
```bash
# Test that alpha versions can't go to latest
# (This is enforced in the script, so it will fail before publishing)
# Test that non-alpha versions can't go to alpha tag
# (Also enforced in the script)
```
## Safe Testing Checklist
Before actually publishing:
- [ ] Run dry-run mode to verify version calculation
- [ ] Verify branch restrictions work (try from wrong branch)
- [ ] Test alpha validations (if testing alpha branch)
- [ ] Check that version override works (if needed)
- [ ] Verify package.json files would be updated correctly
- [ ] Review what git operations would happen
- [ ] Confirm npm tag assignment is correct
## Troubleshooting
### Script fails with "branch not allowed"
Make sure you're on `master` or `alpha` branch, or set `GITHUB_REF_NAME` environment variable:
```bash
GITHUB_REF_NAME=master DRY_RUN=true pnpm run publish
```
### Version calculation seems wrong
Check the current version in `packages/less/package.json`:
```bash
node -p "require('./packages/less/package.json').version"
```
### Alpha validations failing
Make sure:
- Alpha branch is up-to-date with master
- Current version contains `-alpha.`
- Alpha base version is >= master version
## Real Publishing Test (Use with Caution)
If you want to test the actual publishing flow:
1. **Use a test npm package** (create a scoped package like `@your-username/less-test`)
2. **Temporarily modify the script** to use your test package name
3. **Test on a separate branch** that won't trigger the workflow
4. **Clean up** after testing
**Never test on the actual `less` package unless you're ready to publish!**

View File

@@ -32,8 +32,6 @@ jobs:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 8
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}

194
.github/workflows/publish.yml vendored Normal file
View File

@@ -0,0 +1,194 @@
name: Publish to NPM
on:
push:
branches:
- master
- alpha
paths-ignore:
- '**.md'
- '.github/**'
- 'docs/**'
permissions:
id-token: write # Required for OIDC trusted publishing
contents: read
jobs:
publish:
name: Publish to NPM
runs-on: ubuntu-latest
# Only run on push events, not pull requests
if: github.event_name == 'push'
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Install pnpm
uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 'lts/*'
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run tests
run: pnpm run test
- name: Build
run: |
cd packages/less
pnpm run build
- name: Configure Git
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
- name: Determine branch and tag type
id: branch-info
run: |
BRANCH="${{ github.ref_name }}"
echo "branch=$BRANCH" >> $GITHUB_OUTPUT
if [ "$BRANCH" = "alpha" ]; then
echo "is_alpha=true" >> $GITHUB_OUTPUT
echo "npm_tag=alpha" >> $GITHUB_OUTPUT
echo "release_type=prerelease" >> $GITHUB_OUTPUT
else
echo "is_alpha=false" >> $GITHUB_OUTPUT
echo "npm_tag=latest" >> $GITHUB_OUTPUT
echo "release_type=release" >> $GITHUB_OUTPUT
fi
- name: Validate alpha branch requirements
if: steps.branch-info.outputs.is_alpha == 'true'
run: |
# Fetch master branch
git fetch origin master:master || true
# Check 1: Alpha branch must not be behind master
echo "🔍 Checking if alpha branch is up to date with master..."
MASTER_COMMITS=$(git rev-list --count alpha..master 2>/dev/null || echo "0")
if [ "$MASTER_COMMITS" -gt 0 ]; then
echo "❌ ERROR: Alpha branch is behind master by $MASTER_COMMITS commit(s)"
echo " Alpha branch must include all commits from master before publishing"
exit 1
fi
echo "✅ Alpha branch is up to date with master"
# Check 2: Get current version and validate it contains 'alpha'
CURRENT_VERSION=$(node -p "require('./packages/less/package.json').version")
echo "📦 Current version: $CURRENT_VERSION"
if [[ ! "$CURRENT_VERSION" =~ -alpha\. ]]; then
echo "❌ ERROR: Alpha branch version must contain '-alpha.'"
echo " Current version: $CURRENT_VERSION"
echo " Expected format: X.Y.Z-alpha.N"
exit 1
fi
echo "✅ Version contains 'alpha' suffix"
# Check 3: Alpha base version must be >= master version
echo "🔍 Comparing alpha base version with master version..."
MASTER_VERSION=$(git show master:packages/less/package.json 2>/dev/null | node -p "try { JSON.parse(require('fs').readFileSync(0, 'utf-8')).version } catch(e) { '0.0.0' }" || echo "0.0.0")
if [ "$MASTER_VERSION" = "0.0.0" ]; then
echo "⚠️ Could not determine master version, skipping comparison"
else
echo "📦 Master version: $MASTER_VERSION"
# Extract base version (remove -alpha.X suffix)
ALPHA_BASE=$(echo "$CURRENT_VERSION" | sed 's/-alpha\.[0-9]*$//')
echo "📦 Alpha base version: $ALPHA_BASE"
# Compare versions using semver from root workspace
COMPARE_RESULT=$(node -e "
const semver = require('semver');
const alphaBase = process.argv[1];
const master = process.argv[2];
if (semver.lt(alphaBase, master)) {
console.log('ERROR');
} else {
console.log('OK');
}
" "$ALPHA_BASE" "$MASTER_VERSION" 2>/dev/null || echo "ERROR")
if [ "$COMPARE_RESULT" = "ERROR" ]; then
echo "❌ ERROR: Alpha base version ($ALPHA_BASE) is lower than master version ($MASTER_VERSION)"
echo " According to semver, alpha base version must be >= master version"
exit 1
fi
echo "✅ Alpha base version is >= master version"
fi
- name: Ensure npm 11.5.1 or later for trusted publishing
run: npm install -g npm@latest
- name: Bump version and publish
id: publish
env:
GITHUB_REF_NAME: ${{ github.ref_name }}
run: |
pnpm run publish
# Extract version from package.json
VERSION=$(node -p "require('./packages/less/package.json').version")
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "tag=v$VERSION" >> $GITHUB_OUTPUT
- name: Create GitHub Release (Master)
if: steps.branch-info.outputs.is_alpha != 'true'
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.publish.outputs.tag }}
release_name: Release ${{ steps.publish.outputs.tag }}
body: |
## Changes
See [CHANGELOG.md](https://github.com/less/less.js/blob/master/CHANGELOG.md) for details.
## Installation
```bash
npm install less@${{ steps.publish.outputs.version }}
```
draft: false
prerelease: false
- name: Create GitHub Pre-Release (Alpha)
if: steps.branch-info.outputs.is_alpha == 'true'
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.publish.outputs.tag }}
release_name: Alpha Release ${{ steps.publish.outputs.tag }}
body: |
## Alpha Release
This is an alpha release from the alpha branch.
## Installation
```bash
npm install less@${{ steps.publish.outputs.version }} --tag alpha
```
Or:
```bash
npm install less@alpha
```
draft: false
prerelease: true

5
.husky/post-merge Executable file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# Post-merge hook to preserve alpha versions when merging master into alpha
node scripts/post-merge-version-fix.js

1
.husky/pre-commit Normal file
View File

@@ -0,0 +1 @@
pnpm test

2
.nvmrc
View File

@@ -1 +1 @@
v16
v18

View File

@@ -1,54 +1,145 @@
# Contributing to Less.js
> We welcome feature requests and bug reports. Please read these guidelines before submitting one.
Thank you for your interest in contributing to Less.js! Contributions come in many forms—fixing bugs, improving code quality, enhancing tooling, updating documentation, and occasionally adding new features. This guide will help you get started.
## Getting Started
<span class="warning">**Words that begin with the at sign (`@`) must be wrapped in backticks!** </span>. As a courtesy to avoid sending notifications to any user that might have the `@username` being referenced, please remember that GitHub usernames also start with the at sign. If you don't wrap them in backticks, users will get unintended notifications from you.
GitHub has other great markdown features as well, [go here to learn more about them](https://help.github.com/articles/github-flavored-markdown).
Before you begin, please note: **Words that begin with the at sign (`@`) must be wrapped in backticks!** This prevents unintended notifications to GitHub users. For example, use `` `@username` `` instead of `@username`.
GitHub has many great markdown features—[learn more about them here](https://help.github.com/articles/github-flavored-markdown).
## Reporting Issues
We only accept issues that are bug reports or feature requests. Bugs must be isolated and reproducible problems that we can fix within the Less.js core. Please read the following guidelines before opening any issue.
We welcome bug reports and feature requests! To help us help you, please follow these guidelines:
1. **Search for existing issues.** We get a lot of duplicate issues, and you'd help us out a lot by first checking if someone else has reported the same issue. Moreover, the issue may have already been resolved with a fix available.
2. **Create an isolated and reproducible test case.** Be sure the problem exists in Less.js's code with [reduced test cases](http://css-tricks.com/reduced-test-cases/) that should be included in each bug report.
3. **Test with the latest version**. We get a lot of issues that could be resolved by updating your version of Less.js.
4. **Include an example with source.** E.g. You can use [Less Preview](http://lesscss.org/less-preview/) to create a short test case.
5. **Share as much information as possible.** Include operating system and version. Describe how you use Less. If you use it in the browser, please include browser and version, and the version of Less.js you're using. Let us know if you're using the command line (`lessc`) or an external tool. And try to include steps to reproduce the bug.
6. If you have a solution or suggestion for how to fix the bug you're reporting, please include it, or make a pull request - don't assume the maintainers know how to fix it just because you do.
1. **Search for existing issues first.** Many issues have already been reported or resolved. Checking first saves everyone time.
2. **Create an isolated and reproducible test case.** Include [reduced test cases](http://css-tricks.com/reduced-test-cases/) that demonstrate the problem clearly.
3. **Test with the latest version.** Many issues are resolved in newer versions—please update first.
4. **Include examples with source code.** You can use [Less Preview](http://lesscss.org/less-preview/) to create a short test case.
5. **Share as much information as possible.** Include:
- Operating system and version
- How you're using Less (browser, command line, build tool, etc.)
- Browser and version (if applicable)
- Version of Less.js you're using
- Clear steps to reproduce the issue
6. **Suggest solutions if you have them.** If you know how to fix it, share your approach or submit a pull request!
Please report documentation issues in [the documentation project](https://github.com/less/less-docs).
## Feature Requests
* Please search for existing feature requests first to see if something similar already exists.
* Include a clear and specific use-case. We love new ideas, but we do not add language features without a reason.
* Consider whether or not your language feature would be better as a function or implemented in a 3rd-party build system
When suggesting features:
* **Search existing feature requests first** to see if something similar already exists. Many features are already planned or under consideration.
* **Include a clear and specific use-case.** Help us understand the practical need and how it would be used.
* **Consider alternatives.** Sometimes a function or a 3rd-party build system might be a better fit than a core language feature.
**Note:** Most helpful contributions to Less.js are organizational—addressing bugs, improving code quality, enhancing tooling, and updating documentation. The language features are generally stable, even if not all planned features have been implemented yet.
## Pull Requests
_Pull requests are encouraged!_
Pull requests are welcome! Here's how to make them go smoothly:
* Start by adding a feature request to get feedback and see how your idea is received.
* If your pull request solves an existing issue, but it's different in some way, _please create a new issue_ and make sure to discuss it with the core contributors. Otherwise you risk your hard work being rejected.
* Do not change the **./dist/** folder, we do this when releasing
* _Please add tests_ for your work. Tests are invoked using `npm test` command. It will run both node.js tests and browser (Headless Chrome) tests.
* **For new features, start with a feature request** to get feedback and see how your idea is received.
* **If your PR solves an existing issue**, but approaches it differently, please create a new issue first and discuss it with core contributors. This helps avoid wasted effort.
* **Don't modify the `./dist/` folder**—we handle that during releases.
* **Please add tests** for your work. Run tests using `npm test`, which runs both Node.js and browser (Headless Chrome) tests.
### Coding Standards
* Always use spaces, never tabs
* End lines in semi-colons.
* Loosely aim towards eslint standards
* End lines with semicolons
* Aim for ESLint standards
## Developing
If you want to take an issue just add a small comment saying you are having a go at something, so we don't get duplication.
If you want to work on an issue, add a comment saying you're taking it on—this helps prevent duplicate work.
Learn more about [developing Less.js](http://lesscss.org/usage/#developing-less).
## Releases
Releases are managed using PNPM. Instructions TBD
Releases are fully automated! Here's how it works:
### Automated Publishing
When code is pushed to specific branches, GitHub Actions automatically:
1. **Runs tests and builds** the project
2. **Bumps the version** automatically
3. **Publishes to npm** with the appropriate tag
4. **Creates a GitHub release**
### Release Branches
- **`master` branch**:
- Publishes regular releases (e.g., `4.4.2` → `4.4.3`)
- Published to npm with `latest` tag
- Creates regular GitHub releases
- Version auto-increments by patch unless explicitly set
- **`alpha` branch**:
- Publishes alpha releases (e.g., `5.0.0-alpha.1` → `5.0.0-alpha.2`)
- Published to npm with `alpha` tag
- Creates GitHub pre-releases
- Version auto-increments alpha suffix
### How to Publish
**For regular releases:**
1. Update version in `packages/less/package.json` (or let it auto-increment)
2. Commit and push to `master`
3. The workflow automatically publishes if the version changed
**For alpha releases:**
1. Make your changes on the `alpha` branch
2. Commit and push
3. The workflow automatically increments the alpha version and publishes
### Version Override
You can override auto-increment by including a version in your commit message:
```
feat: new feature
version: 4.5.0
```
### Security
We use npm's [trusted publishing](https://docs.npmjs.com/trusted-publishers) with OIDC authentication. This means:
- No long-lived tokens needed
- Automatic provenance generation
- Enhanced security through short-lived, workflow-specific credentials
The publishing workflow (`.github/workflows/publish.yml`) handles both release types automatically.
### Important Notes
- Publishing only works from `master` or `alpha` branches
- Alpha versions must contain `-alpha.` and are published to the `alpha` tag
- Regular versions are published to the `latest` tag
- Alpha branch must be up-to-date with master before publishing
- Alpha base version must be >= master version (semver)
### Merging Master into Alpha
When merging `master` into `alpha`, the version in `package.json` might be overwritten. We have two layers of protection:
1. **Post-merge git hook** (automatic): Automatically restores alpha versions after merges
- Installed automatically via husky when you run `pnpm install`
- Runs automatically after `git merge`
- Restores and increments the alpha version if it was overwritten
- Prompts you to commit the restored version
2. **Publishing script detection** (safety net): The publishing script also detects overwritten versions
- Searches git history for the last alpha version
- Restores and increments it (e.g., if it was `5.0.0-alpha.3`, it becomes `5.0.0-alpha.4`)
- Updates all package.json files accordingly
**Note**: The git hook is managed by husky and installs automatically. The publishing script protection works as a backup even if the hook isn't installed.
---
Thank you for contributing to Less.js!

View File

@@ -1,31 +1,35 @@
{
"name": "@less/root",
"private": true,
"version": "4.4.2",
"description": "Less monorepo",
"homepage": "http://lesscss.org",
"scripts": {
"publish": "lerna publish from-package --no-private",
"changelog": "github-changes -o less -r less.js -a --only-pulls --use-commit-body -m \"(YYYY-MM-DD)\"",
"test": "cd packages/less && npm test",
"postinstall": "npx only-allow pnpm"
},
"author": "Alexis Sellier <self@cloudhead.net>",
"contributors": [
"The Core Less Team"
],
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/less/less.js/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/less/less.js.git"
},
"devDependencies": {
"all-contributors-cli": "~6.26.1",
"github-changes": "^1.1.2",
"lerna": "^3.22.1",
"npm-run-all": "^4.1.5"
}
"name": "@less/root",
"private": true,
"version": "4.5.0",
"description": "Less monorepo",
"homepage": "http://lesscss.org",
"scripts": {
"publish": "node scripts/bump-and-publish.js",
"publish:dry-run": "DRY_RUN=true node scripts/bump-and-publish.js",
"prepare": "husky",
"changelog": "github-changes -o less -r less.js -a --only-pulls --use-commit-body -m \"(YYYY-MM-DD)\"",
"test": "cd packages/less && npm test",
"postinstall": "npx only-allow pnpm"
},
"author": "Alexis Sellier <self@cloudhead.net>",
"contributors": [
"The Core Less Team"
],
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/less/less.js/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/less/less.js.git"
},
"devDependencies": {
"all-contributors-cli": "~6.26.1",
"github-changes": "^1.1.2",
"husky": "~9.1.7",
"npm-run-all": "^4.1.5",
"semver": "^6.3.1"
},
"packageManager": "pnpm@8.15.0"
}

View File

@@ -1,6 +1,6 @@
{
"name": "less",
"version": "4.4.2",
"version": "4.5.0",
"description": "Leaner CSS",
"homepage": "http://lesscss.org",
"author": {

View File

@@ -1,14 +1,14 @@
{
"name": "@less/test-data",
"publishConfig": {
"access": "public"
},
"version": "4.4.2",
"description": "Less files and CSS results",
"author": "Alexis Sellier <self@cloudhead.net>",
"contributors": [
"The Core Less Team"
],
"license": "Apache-2.0",
"gitHead": "1df9072ee9ebdadc791bf35dfb1dbc3ef9f1948f"
"name": "@less/test-data",
"publishConfig": {
"access": "public"
},
"version": "4.5.0",
"description": "Less files and CSS results",
"author": "Alexis Sellier <self@cloudhead.net>",
"contributors": [
"The Core Less Team"
],
"license": "Apache-2.0",
"gitHead": "1df9072ee9ebdadc791bf35dfb1dbc3ef9f1948f"
}

View File

@@ -1,7 +1,7 @@
// https://github.com/less/less.js/issues/3541
@import (reference) url(https://cdn.jsdelivr.net/npm/@less/test-data/less/_main/selectors.less);
@import (reference) url("https://cdn.jsdelivr.net/npm/@less/test-data/less/_main/media.less");
@import (reference) url("https://cdn.jsdelivr.net/npm/@less/test-data/less/_main/empty.less?arg");
@import (reference) url(https://cdn.jsdelivr.net/npm/@less/test-data/tests-unit/selectors/selectors.less);
@import (reference) url("https://cdn.jsdelivr.net/npm/@less/test-data/tests-unit/media/media.less");
@import (reference) url("https://cdn.jsdelivr.net/npm/@less/test-data/tests-unit/empty/empty.less?arg");
.test {
color: @var;

View File

@@ -1,11 +1,11 @@
{
"name": "@less/test-import-module",
"private": true,
"version": "4.0.0",
"description": "Less files to be included in node_modules directory for testing import from node_modules",
"author": "Alexis Sellier <self@cloudhead.net>",
"contributors": [
"The Core Less Team"
],
"license": "Apache-2.0"
"name": "@less/test-import-module",
"private": true,
"version": "4.5.0",
"description": "Less files to be included in node_modules directory for testing import from node_modules",
"author": "Alexis Sellier <self@cloudhead.net>",
"contributors": [
"The Core Less Team"
],
"license": "Apache-2.0"
}

11664
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

467
scripts/bump-and-publish.js Executable file
View File

@@ -0,0 +1,467 @@
#!/usr/bin/env node
/**
* Version bumping and publishing script for Less.js monorepo
*
* This script:
* 1. Determines the next version (patch increment or explicit)
* 2. Updates all package.json files to the same version
* 3. Creates a git tag
* 4. Commits version changes
* 5. Publishes all packages to NPM
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const semver = require('semver');
const ROOT_DIR = path.resolve(__dirname, '..');
const PACKAGES_DIR = path.join(ROOT_DIR, 'packages');
// Get all package.json files
function getPackageFiles() {
const packages = [];
// Root package.json
const rootPkgPath = path.join(ROOT_DIR, 'package.json');
if (fs.existsSync(rootPkgPath)) {
packages.push(rootPkgPath);
}
// Package directories
const packageDirs = fs.readdirSync(PACKAGES_DIR, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => path.join(PACKAGES_DIR, dirent.name));
for (const pkgDir of packageDirs) {
const pkgPath = path.join(pkgDir, 'package.json');
if (fs.existsSync(pkgPath)) {
packages.push(pkgPath);
}
}
return packages;
}
// Read package.json
function readPackage(pkgPath) {
return JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
}
// Write package.json
function writePackage(pkgPath, pkg) {
const content = JSON.stringify(pkg, null, '\t') + '\n';
fs.writeFileSync(pkgPath, content, 'utf8');
}
// Parse version string
function parseVersion(version) {
const parts = version.split('.');
return {
major: parseInt(parts[0], 10),
minor: parseInt(parts[1], 10),
patch: parseInt(parts[2], 10),
prerelease: parts[3] || null
};
}
// Increment patch version
function incrementPatch(version) {
const parsed = parseVersion(version);
return `${parsed.major}.${parsed.minor}.${parsed.patch + 1}`;
}
// Get current version from main package
function getCurrentVersion() {
const lessPkgPath = path.join(PACKAGES_DIR, 'less', 'package.json');
const pkg = readPackage(lessPkgPath);
return pkg.version;
}
// Check if version was explicitly set (via environment variable or git commit message)
function getExplicitVersion() {
// Check for explicit version in environment
if (process.env.EXPLICIT_VERSION) {
return process.env.EXPLICIT_VERSION;
}
// Check git commit message for version bump instruction
try {
const commitMsg = execSync('git log -1 --pretty=%B', { encoding: 'utf8' });
const versionMatch = commitMsg.match(/version[:\s]+v?(\d+\.\d+\.\d+(?:-[a-z]+\.\d+)?)/i);
if (versionMatch) {
return versionMatch[1];
}
} catch (e) {
// Ignore errors
}
return null;
}
// Update all package.json files with new version
function updateAllVersions(newVersion) {
const packageFiles = getPackageFiles();
const updated = [];
for (const pkgPath of packageFiles) {
const pkg = readPackage(pkgPath);
if (pkg.version) {
pkg.version = newVersion;
writePackage(pkgPath, pkg);
updated.push(pkgPath);
}
}
return updated;
}
// Get packages that should be published (not private)
function getPublishablePackages() {
const packageFiles = getPackageFiles();
const publishable = [];
for (const pkgPath of packageFiles) {
const pkg = readPackage(pkgPath);
// Skip root package and private packages
if (!pkg.private && pkg.name && pkg.name !== '@less/root') {
publishable.push({
path: pkgPath,
name: pkg.name,
dir: path.dirname(pkgPath)
});
}
}
return publishable;
}
// Main function
function main() {
const dryRun = process.env.DRY_RUN === 'true' || process.argv.includes('--dry-run');
const branch = process.env.GITHUB_REF_NAME || execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim();
const isAlpha = branch === 'alpha';
const isMaster = branch === 'master';
if (dryRun) {
console.log(`🧪 DRY RUN MODE - No changes will be committed or published\n`);
}
// Enforce branch restrictions - only allow publishing from master or alpha branches
if (!isMaster && !isAlpha) {
console.error(`❌ ERROR: Publishing is only allowed from 'master' or 'alpha' branches`);
console.error(` Current branch: ${branch}`);
console.error(` Please switch to 'master' or 'alpha' branch before publishing`);
process.exit(1);
}
console.log(`🚀 Starting publish process for branch: ${branch}`);
// Get current version
let currentVersion = getCurrentVersion();
console.log(`📦 Current version: ${currentVersion}`);
// Protection: If on alpha branch and version was overwritten by a merge from master
if (isAlpha && !currentVersion.includes('-alpha.')) {
console.log(`\n⚠️ WARNING: Alpha branch version (${currentVersion}) doesn't contain '-alpha.'`);
console.log(` This likely happened due to merging master into alpha.`);
console.log(` Attempting to restore alpha version...`);
// Try to find the last alpha version from alpha branch history
let restoredVersion = null;
try {
// Get recent commits on alpha that modified package.json
const commits = execSync(
'git log alpha --oneline -20 -- packages/less/package.json',
{ cwd: ROOT_DIR, encoding: 'utf8' }
).trim().split('\n');
// Search through commits to find the last alpha version
for (const commitLine of commits) {
const commitHash = commitLine.split(' ')[0];
try {
const pkgContent = execSync(
`git show ${commitHash}:packages/less/package.json 2>/dev/null`,
{ cwd: ROOT_DIR, encoding: 'utf8' }
);
const pkg = JSON.parse(pkgContent);
if (pkg.version && pkg.version.includes('-alpha.')) {
restoredVersion = pkg.version;
console.log(` Found previous alpha version in commit ${commitHash}: ${restoredVersion}`);
break;
}
} catch (e) {
// Continue to next commit
}
}
if (restoredVersion) {
// Increment the alpha number from the restored version
const alphaMatch = restoredVersion.match(/^(\d+\.\d+\.\d+)-alpha\.(\d+)$/);
if (alphaMatch) {
const alphaNum = parseInt(alphaMatch[2], 10);
const newAlphaVersion = `${alphaMatch[1]}-alpha.${alphaNum + 1}`;
console.log(` Restoring and incrementing to: ${newAlphaVersion}`);
currentVersion = newAlphaVersion;
updateAllVersions(newAlphaVersion);
} else {
console.log(` Restoring to: ${restoredVersion}`);
currentVersion = restoredVersion;
updateAllVersions(restoredVersion);
}
} else {
// No previous alpha version found, create one from current version
const parsed = parseVersion(currentVersion);
const nextMajor = parsed.major + 1;
const newAlphaVersion = `${nextMajor}.0.0-alpha.1`;
console.log(` No previous alpha version found. Creating new: ${newAlphaVersion}`);
currentVersion = newAlphaVersion;
updateAllVersions(newAlphaVersion);
}
} catch (e) {
// If we can't find previous version, create a new alpha version
const parsed = parseVersion(currentVersion);
const nextMajor = parsed.major + 1;
const newAlphaVersion = `${nextMajor}.0.0-alpha.1`;
console.log(` Could not find previous alpha version. Creating: ${newAlphaVersion}`);
currentVersion = newAlphaVersion;
updateAllVersions(newAlphaVersion);
}
console.log(`✅ Restored/created alpha version: ${currentVersion}\n`);
}
// Determine next version
const explicitVersion = getExplicitVersion();
let nextVersion;
if (explicitVersion) {
nextVersion = explicitVersion;
console.log(`✨ Using explicit version: ${nextVersion}`);
} else if (isAlpha) {
// For alpha branch, use alpha versions
const parsed = parseVersion(currentVersion);
if (parsed.prerelease) {
// Already an alpha, increment alpha number
const alphaMatch = currentVersion.match(/^(\d+\.\d+\.\d+)-alpha\.(\d+)$/);
if (alphaMatch) {
const alphaNum = parseInt(alphaMatch[2], 10);
nextVersion = `${alphaMatch[1]}-alpha.${alphaNum + 1}`;
} else {
// Other prerelease format, determine base version and start alpha.1
const baseVersion = `${parsed.major}.${parsed.minor}.${parsed.patch}`;
nextVersion = `${baseVersion}-alpha.1`;
}
} else {
// Not an alpha version, determine next major and start alpha.1
const parsed = parseVersion(currentVersion);
const nextMajor = parsed.major + 1;
nextVersion = `${nextMajor}.0.0-alpha.1`;
}
console.log(`🔢 Auto-incrementing alpha version: ${nextVersion}`);
} else {
// For master, increment patch
nextVersion = incrementPatch(currentVersion);
console.log(`🔢 Auto-incrementing patch version: ${nextVersion}`);
}
// Update all package.json files
console.log(`📝 Updating all package.json files to version ${nextVersion}...`);
const updated = updateAllVersions(nextVersion);
console.log(`✅ Updated ${updated.length} package.json files`);
// Get publishable packages
const publishable = getPublishablePackages();
console.log(`📦 Found ${publishable.length} publishable packages:`);
publishable.forEach(pkg => console.log(` - ${pkg.name}`));
// Stage changes
console.log(`📌 Staging version changes...`);
if (!dryRun) {
execSync('git add package.json packages/*/package.json', { cwd: ROOT_DIR, stdio: 'inherit' });
} else {
console.log(` [DRY RUN] Would stage: package.json packages/*/package.json`);
}
// Commit
console.log(`💾 Committing version bump...`);
if (!dryRun) {
try {
execSync(`git commit -m "chore: bump version to ${nextVersion}"`, {
cwd: ROOT_DIR,
stdio: 'inherit'
});
} catch (e) {
// Commit might fail if nothing changed, that's okay
console.log(`⚠️ Commit skipped (no changes or already committed)`);
}
} else {
console.log(` [DRY RUN] Would commit: "chore: bump version to ${nextVersion}"`);
}
// Create tag
const tagName = `v${nextVersion}`;
console.log(`🏷️ Creating git tag: ${tagName}...`);
if (!dryRun) {
try {
execSync(`git tag -a "${tagName}" -m "Release ${tagName}"`, {
cwd: ROOT_DIR,
stdio: 'inherit'
});
} catch (e) {
console.log(`⚠️ Tag might already exist, continuing...`);
}
} else {
console.log(` [DRY RUN] Would create tag: ${tagName}`);
}
// Push commit and tag
console.log(`📤 Pushing to ${branch}...`);
if (!dryRun) {
try {
execSync(`git push origin ${branch}`, { cwd: ROOT_DIR, stdio: 'inherit' });
execSync(`git push origin "${tagName}"`, { cwd: ROOT_DIR, stdio: 'inherit' });
} catch (e) {
console.log(`⚠️ Push failed, but continuing with publish...`);
}
} else {
console.log(` [DRY RUN] Would push to: origin ${branch}`);
console.log(` [DRY RUN] Would push tag: origin ${tagName}`);
}
// Validate alpha branch requirements
if (isAlpha) {
console.log(`\n🔍 Validating alpha branch requirements...`);
// Validation 1: Version must contain 'alpha'
if (!nextVersion.includes('-alpha.')) {
console.error(`❌ ERROR: Alpha branch version must contain '-alpha.'`);
console.error(` Generated version: ${nextVersion}`);
console.error(` Expected format: X.Y.Z-alpha.N`);
process.exit(1);
}
console.log(`✅ Version contains 'alpha' suffix: ${nextVersion}`);
// Validation 2: Must publish with 'alpha' tag
// (This is enforced in the code below, but we log it for clarity)
console.log(`✅ Will publish with 'alpha' tag (enforced)`);
// Validation 3: Check if alpha is behind master
try {
execSync('git fetch origin master:master 2>/dev/null || true', { cwd: ROOT_DIR });
const masterCommits = execSync('git rev-list --count alpha..master 2>/dev/null || echo "0"', {
cwd: ROOT_DIR,
encoding: 'utf8'
}).trim();
if (parseInt(masterCommits, 10) > 0) {
console.error(`❌ ERROR: Alpha branch is behind master by ${masterCommits} commit(s)`);
console.error(` Alpha branch must include all commits from master before publishing`);
console.error(` Please merge master into alpha first`);
process.exit(1);
}
console.log(`✅ Alpha branch is up to date with master`);
} catch (e) {
console.log(`⚠️ Could not verify master sync status, continuing...`);
}
// Validation 4: Alpha base version must be >= master version
try {
const masterVersionStr = execSync('git show master:packages/less/package.json 2>/dev/null', {
cwd: ROOT_DIR,
encoding: 'utf8'
});
const masterPkg = JSON.parse(masterVersionStr);
const masterVersion = masterPkg.version;
// Extract base version from alpha version (remove -alpha.X)
const alphaBase = nextVersion.replace(/-alpha\.\d+$/, '');
// Semver comparison using semver library
const isGreaterOrEqual = semver.gte(alphaBase, masterVersion);
if (!isGreaterOrEqual) {
console.error(`❌ ERROR: Alpha base version (${alphaBase}) is lower than master version (${masterVersion})`);
console.error(` According to semver, alpha base version must be >= master version`);
process.exit(1);
}
console.log(`✅ Alpha base version (${alphaBase}) is >= master version (${masterVersion})`);
} catch (e) {
console.log(`⚠️ Could not compare with master version, continuing...`);
}
}
// Determine NPM tag based on branch and version
const npmTag = isAlpha ? 'alpha' : 'latest';
const isAlphaVersion = nextVersion.includes('-alpha.');
// Validation: Alpha versions must use 'alpha' tag, non-alpha versions must use 'latest' tag
if (isAlphaVersion && npmTag !== 'alpha') {
console.error(`❌ ERROR: Alpha version (${nextVersion}) must be published with 'alpha' tag, not '${npmTag}'`);
console.error(` Alpha versions cannot be published to 'latest' tag`);
process.exit(1);
}
if (!isAlphaVersion && npmTag === 'alpha') {
console.error(`❌ ERROR: Non-alpha version (${nextVersion}) cannot be published with 'alpha' tag`);
console.error(` Only versions containing '-alpha.' can be published to 'alpha' tag`);
process.exit(1);
}
// Enforce alpha tag for alpha branch
if (isAlpha && npmTag !== 'alpha') {
console.error(`❌ ERROR: Alpha branch must publish with 'alpha' tag, not '${npmTag}'`);
process.exit(1);
}
console.log(`\n📦 Publishing packages to NPM with tag: ${npmTag}...`);
for (const pkg of publishable) {
console.log(`\n📤 Publishing ${pkg.name}...`);
if (dryRun) {
console.log(` [DRY RUN] Would publish: ${pkg.name}@${nextVersion} with tag: ${npmTag}`);
console.log(` [DRY RUN] Command: npm publish --tag ${npmTag}`);
} else {
try {
execSync(`npm publish --tag ${npmTag}`, {
cwd: pkg.dir,
stdio: 'inherit',
env: { ...process.env, NODE_AUTH_TOKEN: process.env.NPM_TOKEN }
});
console.log(`✅ Successfully published ${pkg.name}@${nextVersion}`);
} catch (e) {
console.error(`❌ Failed to publish ${pkg.name}:`, e.message);
process.exit(1);
}
}
}
if (dryRun) {
console.log(`\n🧪 DRY RUN COMPLETE - No changes were made`);
console.log(` Would publish version: ${nextVersion}`);
console.log(` Would create tag: ${tagName}`);
console.log(` Would use NPM tag: ${npmTag}`);
} else {
console.log(`\n🎉 Successfully published all packages!`);
console.log(` Version: ${nextVersion}`);
console.log(` Tag: ${tagName}`);
console.log(` NPM Tag: ${npmTag}`);
}
// Output version for GitHub Actions
if (process.env.GITHUB_OUTPUT) {
const fs = require('fs');
fs.appendFileSync(process.env.GITHUB_OUTPUT, `version=${nextVersion}\n`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `tag=${tagName}\n`);
}
return { version: nextVersion, tag: tagName };
}
// Run if called directly
if (require.main === module) {
main();
}
module.exports = { main };

153
scripts/post-merge-version-fix.js Executable file
View File

@@ -0,0 +1,153 @@
#!/usr/bin/env node
/**
* Post-merge hook to preserve alpha versions when merging master into alpha branch
*
* This script runs after a merge and checks if:
* 1. We're on the alpha branch
* 2. The version in package.json doesn't contain '-alpha.' (was overwritten)
* 3. If so, restores the previous alpha version from git history
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const ROOT_DIR = path.resolve(__dirname, '..');
const LESS_PKG_PATH = path.join(ROOT_DIR, 'packages', 'less', 'package.json');
// Get current branch
function getCurrentBranch() {
try {
return execSync('git rev-parse --abbrev-ref HEAD', {
cwd: ROOT_DIR,
encoding: 'utf8'
}).trim();
} catch (e) {
return null;
}
}
// Read package.json version
function getVersion(pkgPath) {
try {
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
return pkg.version;
} catch (e) {
return null;
}
}
// Update version in package.json
function updateVersion(pkgPath, newVersion) {
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
pkg.version = newVersion;
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, '\t') + '\n', 'utf8');
}
// Find last alpha version from git history
function findLastAlphaVersion() {
try {
// Get recent commits on alpha that modified package.json
const commits = execSync(
'git log alpha --oneline -20 -- packages/less/package.json',
{ cwd: ROOT_DIR, encoding: 'utf8' }
).trim().split('\n');
// Search through commits to find the last alpha version
for (const commitLine of commits) {
const commitHash = commitLine.split(' ')[0];
try {
const pkgContent = execSync(
`git show ${commitHash}:packages/less/package.json 2>/dev/null`,
{ cwd: ROOT_DIR, encoding: 'utf8' }
);
const pkg = JSON.parse(pkgContent);
if (pkg.version && pkg.version.includes('-alpha.')) {
return pkg.version;
}
} catch (e) {
// Continue to next commit
}
}
} catch (e) {
// Ignore errors
}
return null;
}
// Update all package.json files with new version
function updateAllVersions(newVersion) {
const packageFiles = [
path.join(ROOT_DIR, 'package.json'),
path.join(ROOT_DIR, 'packages', 'less', 'package.json'),
path.join(ROOT_DIR, 'packages', 'test-data', 'package.json')
];
for (const pkgPath of packageFiles) {
if (fs.existsSync(pkgPath)) {
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
if (pkg.version) {
pkg.version = newVersion;
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, '\t') + '\n', 'utf8');
}
}
}
}
// Main function
function main() {
const branch = getCurrentBranch();
// Only run on alpha branch
if (branch !== 'alpha') {
return;
}
const currentVersion = getVersion(LESS_PKG_PATH);
if (!currentVersion) {
return;
}
// Check if version was overwritten (doesn't contain -alpha.)
if (!currentVersion.includes('-alpha.')) {
console.log(`\n⚠️ Post-merge: Alpha version was overwritten (${currentVersion})`);
console.log(` Attempting to restore alpha version...`);
const lastAlphaVersion = findLastAlphaVersion();
if (lastAlphaVersion) {
// Increment the alpha number
const alphaMatch = lastAlphaVersion.match(/^(\d+\.\d+\.\d+)-alpha\.(\d+)$/);
if (alphaMatch) {
const alphaNum = parseInt(alphaMatch[2], 10);
const newAlphaVersion = `${alphaMatch[1]}-alpha.${alphaNum + 1}`;
console.log(` Restoring and incrementing: ${lastAlphaVersion}${newAlphaVersion}`);
updateAllVersions(newAlphaVersion);
console.log(`✅ Restored alpha version: ${newAlphaVersion}`);
console.log(` Please commit this change: git add package.json packages/*/package.json && git commit -m "chore: restore alpha version after merge"`);
} else {
console.log(` Restoring to: ${lastAlphaVersion}`);
updateAllVersions(lastAlphaVersion);
console.log(`✅ Restored alpha version: ${lastAlphaVersion}`);
console.log(` Please commit this change: git add package.json packages/*/package.json && git commit -m "chore: restore alpha version after merge"`);
}
} else {
// No previous alpha version found, create one
const parts = currentVersion.split('.');
const nextMajor = parseInt(parts[0], 10) + 1;
const newAlphaVersion = `${nextMajor}.0.0-alpha.1`;
console.log(` No previous alpha version found. Creating: ${newAlphaVersion}`);
updateAllVersions(newAlphaVersion);
console.log(`✅ Created new alpha version: ${newAlphaVersion}`);
console.log(` Please commit this change: git add package.json packages/*/package.json && git commit -m "chore: restore alpha version after merge"`);
}
}
}
if (require.main === module) {
main();
}
module.exports = { main };