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
76 changes: 38 additions & 38 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,45 @@ 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.
# main's line publishes a prerelease under `next` and a plain version
# under `latest`; an older line, released from its release/<line>
# branch, under `release-<line>` only (scripts/dist-tag.mjs). 0.1's
# alphas before the release/0.1 branch went out under `alpha`; that tag
# is no longer moved. The registry still sets `latest` on a package's
# first publish, so @lablup/ui-common-cli's `latest` is its first alpha
# until 0.2.0; the docs say `@next` until then. Do not move `latest` here.
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 (main is at $MAIN_VERSION)"
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 @@ -80,23 +113,6 @@ jobs:
fi
done

# Any prerelease goes to `next`, a plain version to `latest`. 0.1's
# prereleases went out under `alpha`; that tag is no longer moved.
# The registry still sets `latest` on a package's first publish, so
# @lablup/ui-common-cli's `latest` is its first alpha until 0.2.0; the
# docs say `@next` until then. Do not move `latest` here.
- name: Choose the dist-tag from the version
id: tag
run: |
VERSION=$(node -p "require('./package.json').version")
case "$VERSION" in
*-*) TAG=next ;;
*) TAG=latest ;;
esac
echo "Publishing $VERSION under dist-tag $TAG"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"

# pnpm packs both, rewriting the CLI's `workspace:*` peer to the exact
# version; the library goes first so the CLI's peer exists when it lands.
- name: Pack both packages
Expand All @@ -110,7 +126,7 @@ jobs:
if: github.event_name == 'release' || inputs.dry_run == false
run: |
for PKG in lablup-ui-common lablup-ui-common-cli; do
npm publish "$RUNNER_TEMP/packed/$PKG-${{ steps.tag.outputs.version }}.tgz" --access public --tag ${{ steps.tag.outputs.tag }} --registry https://npm.pkg.github.com
npm publish "$RUNNER_TEMP/packed/$PKG-${{ needs.dist-tag.outputs.version }}.tgz" --access public --tag ${{ needs.dist-tag.outputs.tag }} --registry https://npm.pkg.github.com
done
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand All @@ -119,7 +135,7 @@ jobs:
if: github.event_name == 'workflow_dispatch' && inputs.dry_run
run: |
for PKG in lablup-ui-common lablup-ui-common-cli; do
npm publish "$RUNNER_TEMP/packed/$PKG-${{ steps.tag.outputs.version }}.tgz" --dry-run --access public --tag ${{ steps.tag.outputs.tag }} --registry https://npm.pkg.github.com
npm publish "$RUNNER_TEMP/packed/$PKG-${{ needs.dist-tag.outputs.version }}.tgz" --dry-run --access public --tag ${{ needs.dist-tag.outputs.tag }} --registry https://npm.pkg.github.com
done

# GitHub Packages requires authentication even for public packages, so an
Expand All @@ -131,6 +147,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 @@ -186,23 +203,6 @@ jobs:
fi
done

# Any prerelease goes to `next`, a plain version to `latest`. 0.1's
# prereleases went out under `alpha`; that tag is no longer moved.
# The registry still sets `latest` on a package's first publish, so
# @lablup/ui-common-cli's `latest` is its first alpha until 0.2.0; the
# docs say `@next` until then. Do not move `latest` here.
- name: Choose the dist-tag from the version
id: tag
run: |
VERSION=$(node -p "require('./package.json').version")
case "$VERSION" in
*-*) TAG=next ;;
*) TAG=latest ;;
esac
echo "Publishing $VERSION under dist-tag $TAG"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"

# pnpm packs both, rewriting the CLI's `workspace:*` peer to the exact
# version; the library goes first so the CLI's peer exists when it lands.
- name: Pack both packages
Expand All @@ -224,7 +224,7 @@ jobs:
if: github.event_name == 'release' || inputs.dry_run == false
run: |
for PKG in lablup-ui-common lablup-ui-common-cli; do
npm publish "$RUNNER_TEMP/packed/$PKG-${{ steps.tag.outputs.version }}.tgz" --provenance --access public --tag ${{ steps.tag.outputs.tag }} --registry https://registry.npmjs.org
npm publish "$RUNNER_TEMP/packed/$PKG-${{ needs.dist-tag.outputs.version }}.tgz" --provenance --access public --tag ${{ needs.dist-tag.outputs.tag }} --registry https://registry.npmjs.org
done
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Expand All @@ -233,7 +233,7 @@ jobs:
if: github.event_name == 'workflow_dispatch' && inputs.dry_run
run: |
for PKG in lablup-ui-common lablup-ui-common-cli; do
npm publish "$RUNNER_TEMP/packed/$PKG-${{ steps.tag.outputs.version }}.tgz" --dry-run --access public --tag ${{ steps.tag.outputs.tag }} --registry https://registry.npmjs.org
npm publish "$RUNNER_TEMP/packed/$PKG-${{ needs.dist-tag.outputs.version }}.tgz" --dry-run --access public --tag ${{ needs.dist-tag.outputs.tag }} --registry https://registry.npmjs.org
done
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ Versioning follows the policy in [CONTRIBUTING.md](CONTRIBUTING.md#versioning).

## [Unreleased]

### Changed

- 0.1 is maintained on the `release/0.1` branch. Its patch releases publish
under the `release-0.1` dist-tag and never move `next` or `latest`; the
publish workflow picks the tag by comparing the version with `main`'s
(`scripts/dist-tag.mjs`). README's Install now names `@next`, since
`latest` is still 0.1 until 0.2.0.

## [0.2.0-alpha.15]

Hardening for the apps moving off 0.1: ui-common's styles now sit in their
Expand Down
71 changes: 68 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,8 @@ contract), and the CSS entry point paths.

While the API is migrating, releases are prereleases (`0.2.0-alpha.N`).
Before 1.0, a breaking change bumps the minor version.
An older line keeps getting patch releases from its `release/<line>` branch
([Patching an older line](#patching-an-older-line)).

## Pull requests

Expand All @@ -479,12 +481,75 @@ today and who does the move.

## Releasing

1. Update the version in `package.json` and add a `CHANGELOG.md` entry.
`main` releases the current line. An older line is patched on its own
`release/<line>` branch ([Patching an older line](#patching-an-older-line)).
`scripts/dist-tag.mjs` picks the npm dist-tag by comparing the version with
the one on `main`:

| Version | Dist-tag |
| -------------------------------- | ---------------- |
| main's line, prerelease | `next` |
| main's line, plain | `latest` |
| an older line, prerelease or not | `release-<line>` |
| a line ahead of main | refused |

A line is `<major>.<minor>` before 1.0 and `<major>` after. The `alpha` tag,
which 0.1 published under before `release/0.1` existed, is no longer moved.

### From main

1. Update the version in `package.json` and `packages/cli/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.
that the tag matches both `package.json` files 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.
the built-in `GITHUB_TOKEN` for GitHub Packages, and `NPM_TOKEN` for
npmjs.

### Patching an older line

0.1 is maintained on `release/0.1`, cut from `v0.1.0-alpha.23`, its last
release before `main` moved to 0.2. When `main` moves to a new line, cut the
outgoing line's branch from its last release tag:

```
git push origin v0.2.<last>:refs/heads/release/0.2
```

A patch carries fixes only: the "Patch" row of [Versioning](#versioning). No
new components, props or exports, and no Astryx bump.

1. **Fix.** 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. 0.2 rebuilt every component on Astryx, so most 0.1 bugs exist
only in 0.1: say in the pull request whether `main` is affected.
2. **Version.** In the same or a follow-up pull request against
`release/0.1`, bump `package.json` and add the `CHANGELOG.md` entry there.
0.1 has no plain release, so its patches continue the prerelease sequence
(`0.1.0-alpha.24`, …); a line that has a plain release patches as
`<line>.<n+1>` (`0.2.1`). Publishing refuses only a version ahead of
`main`'s line, not one from another older line, so check the version
before tagging.
3. **Release.** Create the GitHub release on the branch, never on `main`:

```
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.

A release runs the publish workflow as it is at the tagged commit, so the
branch's own `publish.yml` publishes, and it moves only `release-0.1`.
`--latest=false` keeps GitHub's "Latest" badge on the current line.

4. **Check** `npm view @lablup/ui-common dist-tags`: `release-0.1` is the new
version and `next` and `latest` did not move.
5. **Record it on main.** Copy the entry into `main`'s `CHANGELOG.md`, in
version order below the 0.2 entries, so `main`'s changelog lists every
release.

## Pre-public review

Expand Down
32 changes: 30 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,41 @@ Consumers are Lablup product frontends, including
only. It has no API client, no application state, no router, and no
desktop-shell integration.

## Versions

| Line | Branch | State | Install |
| ---- | ------------- | ---------------------------- | ------------------------------- |
| 0.2 | `main` | Developed here (prereleases) | `@lablup/ui-common@next` |
| 0.1 | `release/0.1` | Maintained: fixes only | `@lablup/ui-common@release-0.1` |

Until 0.2.0 is published, the `latest` dist-tag still points at 0.1's last
alpha, so a bare `pnpm add @lablup/ui-common` installs 0.1: name `@next` for
0.2.

An app that stays on 0.1 keeps getting fixes as patch releases of the 0.1
line, published under the `release-0.1` dist-tag, never under `next` or
`latest`. 0.1 never had a plain release, so its fixes continue its prerelease
sequence (`0.1.0-alpha.24`, `0.1.0-alpha.25`, …), which a `^0.1.0-alpha.N`
range already accepts; an exact pin moves by hand:

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

0.1's own README and CHANGELOG are on the
[`release/0.1`](https://github.com/lablup/ui-common/tree/release/0.1) branch.
To fix a bug in 0.1, open the pull request against `release/0.1`
([CONTRIBUTING.md](CONTRIBUTING.md#patching-an-older-line)). Moving to 0.2 is
[Upgrading from 0.1](#upgrading-from-01).

## Install

```
pnpm add @lablup/ui-common @stylexjs/stylex
pnpm add @lablup/ui-common@next @stylexjs/stylex
```

That is npmjs, which needs no authentication.
That is npmjs, which needs no authentication. Drop `@next` once 0.2.0 is
published ([Versions](#versions)).

Peer dependencies:

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