Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: CI

on:
push:
branches: [main]
branches: [main, "release/**"]
pull_request:
workflow_dispatch:

Expand Down
64 changes: 33 additions & 31 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,41 @@ permissions:
contents: read

jobs:
# Decided once, before either registry publishes: two jobs each reading
# main could disagree if main moved to a new line between them.
# Every release from this branch is a patch of the 0.1 line, published
# under `release-0.1` only: `next` and `latest` belong to main's line,
# and `alpha` is no longer moved (scripts/dist-tag.mjs).
dist-tag:
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.tag.outputs.tag }}
version: ${{ steps.tag.outputs.version }}
steps:
- uses: actions/checkout@v7

- uses: actions/setup-node@v7
with:
node-version: 26

- name: Choose the dist-tag from the version
id: tag
run: |
VERSION=$(node -p "require('./package.json').version")
git fetch --no-tags --depth=1 origin main
MAIN_VERSION=$(git show FETCH_HEAD:package.json | node -p "JSON.parse(require('fs').readFileSync(0, 'utf8')).version")
TAG=$(node scripts/dist-tag.mjs "$VERSION" "$MAIN_VERSION")
echo "Publishing $VERSION under dist-tag $TAG"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"

# Consumed by internal projects that already authenticate to GitHub.
github-packages:
if: >-
github.event_name == 'release' ||
inputs.target == 'both' || inputs.target == 'github-packages'
runs-on: ubuntu-latest
needs: dist-tag
environment: release
permissions:
contents: read
Expand Down Expand Up @@ -66,23 +95,9 @@ jobs:
exit 1
fi

- name: Choose the dist-tag from the version
id: tag
run: |
VERSION=$(node -p "require('./package.json').version")
case "$VERSION" in
*-alpha*) TAG=alpha ;;
*-beta*) TAG=beta ;;
*-rc*) TAG=rc ;;
*-*) TAG=next ;;
*) TAG=latest ;;
esac
echo "Publishing $VERSION under dist-tag $TAG"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"

- name: Publish
if: github.event_name == 'release' || inputs.dry_run == false
run: pnpm publish --no-git-checks --access public --tag ${{ steps.tag.outputs.tag }} --registry https://npm.pkg.github.com
run: pnpm publish --no-git-checks --access public --tag ${{ needs.dist-tag.outputs.tag }} --registry https://npm.pkg.github.com
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Expand All @@ -99,6 +114,7 @@ jobs:
github.event_name == 'release' ||
inputs.target == 'both' || inputs.target == 'npmjs'
runs-on: ubuntu-latest
needs: dist-tag
environment: release
permissions:
contents: read
Expand Down Expand Up @@ -140,20 +156,6 @@ jobs:
exit 1
fi

- name: Choose the dist-tag from the version
id: tag
run: |
VERSION=$(node -p "require('./package.json').version")
case "$VERSION" in
*-alpha*) TAG=alpha ;;
*-beta*) TAG=beta ;;
*-rc*) TAG=rc ;;
*-*) TAG=next ;;
*) TAG=latest ;;
esac
echo "Publishing $VERSION under dist-tag $TAG"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"

- name: Confirm the credential is valid for npmjs
run: npm whoami --registry https://registry.npmjs.org
env:
Expand All @@ -164,12 +166,12 @@ jobs:
# since both pack from the same files allowlist.
- name: Publish
if: github.event_name == 'release' || inputs.dry_run == false
run: npm publish --provenance --access public --tag ${{ steps.tag.outputs.tag }} --registry https://registry.npmjs.org
run: npm publish --provenance --access public --tag ${{ needs.dist-tag.outputs.tag }} --registry https://registry.npmjs.org
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Dry run
if: github.event_name == 'workflow_dispatch' && inputs.dry_run
run: npm publish --dry-run --access public --tag ${{ steps.tag.outputs.tag }} --registry https://registry.npmjs.org
run: npm publish --dry-run --access public --tag ${{ needs.dist-tag.outputs.tag }} --registry https://registry.npmjs.org
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ Versioning follows the policy in [CONTRIBUTING.md](CONTRIBUTING.md#versioning).

## [Unreleased]

### Changed

- This branch, `release/0.1`, maintains the 0.1 line. Its patch releases
publish under the `release-0.1` dist-tag; `alpha`, `next` and `latest` no
longer move from here.

## [0.1.0-alpha.23]

### Added
Expand Down
32 changes: 26 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,32 @@ consumer and who is doing that migration.

## Releasing

1. Update the version in `package.json` and add a `CHANGELOG.md` entry.
2. Merge to `main` and confirm CI is green, including the external install job.
3. Create a GitHub release tagged `v<version>`. The publish workflow verifies
that the tag matches `package.json` and refuses to publish on a mismatch.
4. The workflow runs in the `release` environment with `packages: write` and
the built-in `GITHUB_TOKEN`. No long-lived credential is stored here.
This branch, `release/0.1`, carries the 0.1 line after `main` moved to 0.2.
It was cut from `v0.1.0-alpha.23`. It takes fixes only: the "Patch" row of
[Versioning](#versioning), with no new components, props or exports. The full
procedure is in `main`'s CONTRIBUTING.md, "Patching an older line".

1. Branch from `release/0.1` and open the pull request against it. If the bug
is also on `main`, fix it there first and `git cherry-pick -x` the commit;
otherwise say in the pull request that `main` is unaffected.
2. Bump `package.json` to the next `0.1.0-alpha.N` and add a `CHANGELOG.md`
entry. Merge and confirm CI is green, including the external install job.
3. Create the GitHub release on this branch:

```
gh release create v0.1.0-alpha.24 --target release/0.1 --prerelease --latest=false \
--title v0.1.0-alpha.24 --notes-file notes.md
```

`notes.md` holds the CHANGELOG entry.

The publish workflow verifies that the tag matches `package.json`, and
publishes under `release-0.1` only (`scripts/dist-tag.mjs` compares the
version with `main`'s), so `next`, `latest` and `alpha` do not move.
It runs in the `release` environment.

4. Check `npm view @lablup/ui-common dist-tags`, then copy the CHANGELOG
entry into `main`'s `CHANGELOG.md`.

## Pre-public review

Expand Down
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# @lablup/ui-common

> **This is the 0.1 maintenance branch.** 0.1 gets fixes only, released as
> patches of the 0.1 line under the `release-0.1` dist-tag. New work is 0.2,
> on [`main`](https://github.com/lablup/ui-common/tree/main), which rebuilds
> the package on Astryx; its README explains the upgrade. To fix a 0.1 bug,
> open the pull request against `release/0.1`
> ([CONTRIBUTING.md](CONTRIBUTING.md#releasing)).

Product-neutral UI components and design tokens shared across Lablup products.

Consumers are Lablup product frontends, including
Expand All @@ -11,9 +18,14 @@ a component impossible to share.
## Install

```
pnpm add @lablup/ui-common
pnpm add @lablup/ui-common@release-0.1
```

`release-0.1` is the newest 0.1 patch. Without it, `latest` resolves to 0.1
only until 0.2.0 is published. 0.1 has no plain release, so its patches
continue the prerelease sequence (`0.1.0-alpha.24`, …): a `^0.1.0-alpha.N`
range picks them up, and an exact pin moves by hand.

That is npmjs, which needs no authentication and is the right route for
essentially everyone, including open-source consumers and forked CI.

Expand Down
5 changes: 5 additions & 0 deletions scripts/dist-tag.d.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/** Types for the release dist-tag rule, so tests can import it. */

export declare function releaseLine(version: string): string;

export declare function distTagFor(version: string, mainVersion: string): string;
65 changes: 65 additions & 0 deletions scripts/dist-tag.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* The npm dist-tag a release publishes under.
*
* `main` develops the current release line. Its versions publish under `next`
* (a prerelease) or `latest` (a plain version). An older line is maintained on
* a `release/<line>` branch, and its hotfixes publish under `release-<line>`
* only, so a fix to an old line never moves the tags that point at the current
* one. A line is `<major>.<minor>` before 1.0 and `<major>` from 1.0 on: the
* part a breaking change bumps (CONTRIBUTING.md, "Versioning").
*
* Usage: node scripts/dist-tag.mjs <version> <version on main>
*/

import { pathToFileURL } from "node:url";

const SEMVER =
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z.-]+)?$/;

function parse(version) {
const match = SEMVER.exec(version);
if (!match) throw new Error(`Not a semver version: ${JSON.stringify(version)}`);
return {
major: Number(match[1]),
minor: Number(match[2]),
prerelease: match[4] !== undefined,
};
}

/** The release line a version belongs to: `0.1`, `0.2`, … then `1`, `2`, … */
export function releaseLine(version) {
const { major, minor } = parse(version);
return major === 0 ? `0.${minor}` : `${major}`;
}

function lineKey(version) {
const { major, minor } = parse(version);
return major === 0 ? [0, minor] : [major, 0];
}

/**
* @param {string} version the version being published
* @param {string} mainVersion the version in `package.json` on `main`
*/
export function distTagFor(version, mainVersion) {
const [major, minor] = lineKey(version);
const [mainMajor, mainMinor] = lineKey(mainVersion);
if (major === mainMajor && minor === mainMinor) {
return parse(version).prerelease ? "next" : "latest";
}
if (major > mainMajor || (major === mainMajor && minor > mainMinor)) {
throw new Error(
`${version} is ahead of main (${mainVersion}). Release a new line from main, after bumping it there.`,
);
}
return `release-${releaseLine(version)}`;
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const [version, mainVersion] = process.argv.slice(2);
if (!version || !mainVersion) {
console.error("Usage: node scripts/dist-tag.mjs <version> <version on main>");
process.exit(2);
}
console.log(distTagFor(version, mainVersion));
}
33 changes: 33 additions & 0 deletions src/distTag.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { distTagFor, releaseLine } from "../scripts/dist-tag.mjs";

describe("releaseLine", () => {
it("is major.minor before 1.0 and major from 1.0 on", () => {
expect(releaseLine("0.1.0-alpha.23")).toBe("0.1");
expect(releaseLine("0.2.3")).toBe("0.2");
expect(releaseLine("1.4.0")).toBe("1");
});

it("refuses something that is not a version", () => {
expect(() => releaseLine("v0.1.0")).toThrow(/Not a semver/);
});
});

describe("distTagFor", () => {
it("publishes main's line under next or latest", () => {
expect(distTagFor("0.2.0-alpha.16", "0.2.0-alpha.15")).toBe("next");
expect(distTagFor("0.2.0", "0.2.0-alpha.15")).toBe("latest");
expect(distTagFor("1.3.0", "1.2.0")).toBe("latest");
});

it("publishes an older line under its own tag, prerelease or not", () => {
expect(distTagFor("0.1.0-alpha.24", "0.2.0-alpha.15")).toBe("release-0.1");
expect(distTagFor("0.1.1", "0.2.0")).toBe("release-0.1");
expect(distTagFor("1.4.2", "2.0.0-rc.1")).toBe("release-1");
expect(distTagFor("0.9.1", "1.0.0")).toBe("release-0.9");
});

it("refuses a version ahead of main", () => {
expect(() => distTagFor("0.3.0-alpha.0", "0.2.0")).toThrow(/ahead of main/);
expect(() => distTagFor("1.0.0", "0.9.0")).toThrow(/ahead of main/);
});
});
Loading