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
57 changes: 52 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,48 @@
# Release — versioning and publishing to public npm through the standard
# Changesets flow, the same shape linq-kit uses.
#
# Two modes, chosen automatically by changesets/action@v1 on every push to
# main:
#
# 1. Unconsumed changeset files exist in `.changeset/*.md` → the action
# opens (or refreshes) a "chore: release dds" PR that bumps the fixed
# group's versions and deletes the consumed changesets. Merging that PR
# is the release decision.
#
# 2. No unconsumed changesets (the release PR was just merged, or a
# version was bumped by hand) → the action runs `pnpm release`, which
# is `changeset publish`: every package whose version is not yet on
# npm is published, and a git tag per package is pushed. Versions
# already on npm are skipped, so re-runs are safe.
#
# The publish only happens after the full verification gate below — the
# same gates CI runs — so a red main never reaches npm. Publishing uses the
# repository `NPM_TOKEN` secret with npm provenance (`id-token: write`).
#
# Before 2026-09-03 this workflow ran only on a hand-pushed `dds-v*` tag;
# two release commits (0.4.0, 0.4.1) sat on main for days without a tag and
# npm stayed at 0.3.0. The tag is no longer the trigger.

name: Release public packages

on:
push:
tags:
- "dds-v*"
branches: [main]

concurrency: ${{ github.workflow }}-${{ github.ref }}

permissions:
contents: read
contents: write
pull-requests: write
id-token: write

jobs:
publish:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v7
with:
Expand All @@ -36,7 +65,25 @@ jobs:
- run: pnpm run verify:site-kit:ui
- run: pnpm run verify:site-kit:browser
- run: pnpm run verify:site-kit:release
- run: pnpm release
- name: Create the release PR or publish to npm
id: changesets
uses: changesets/action@v1
with:
publish: pnpm release
version: pnpm version-packages
commit: "chore: release dds"
title: "chore: release dds"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_CONFIG_PROVENANCE: true
- name: Summarise what was published
if: steps.changesets.outputs.published == 'true'
env:
PUBLISHED: ${{ steps.changesets.outputs.publishedPackages }}
run: |
{
echo "### Published to npm"
echo ""
echo "$PUBLISHED" | jq -r '.[] | "- `\(.name)@\(.version)`"'
} >> "$GITHUB_STEP_SUMMARY"
10 changes: 7 additions & 3 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,13 @@ pnpm 워크스페이스. `pnpm install` 후:
`verify:site-kit:release` — 정확한 로케일·카탈로그 계약, 공통 UI 동작,
다국어 검색 문서, 패키징·신규 소비자·publish dry-run 게이트

다섯 공개 패키지는 Changesets의 lockstep 그룹으로 릴리스한다. 방향성
아이콘에는 `dds-icon--directional`을 사용하며 mirror/유지 목록은
`@devslab/dds-icons/direction-policy.json`으로 함께 배포한다.
다섯 공개 패키지는 Changesets의 lockstep 그룹으로 릴리스하며, 릴리스는
자동이다: `main`에 푸시될 때마다 `.github/workflows/release.yml`이 전체 검증
게이트를 통과한 뒤 `changesets/action`이 — 대기 중인 changeset 파일이 있으면
"chore: release dds" 버전 PR을 열고, 그 PR이 방금 머지됐으면 `main`에는 있지만
npm에는 아직 없는 버전을 배포한다. 버전 PR 머지가 곧 릴리스 결정이고, 손으로
태그를 푸시하지 않는다. 방향성 아이콘에는 `dds-icon--directional`을 사용하며
mirror/유지 목록은 `@devslab/dds-icons/direction-policy.json`으로 함께 배포한다.

배포 가능한 DDS 패키지는 모두 공개 npm의 `@devslab/*`를 사용하며 DevsLab
Source-Available License 1.0을 따른다. compatibility canary는 비공개·미배포로
Expand Down
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,14 @@ pnpm workspace. `pnpm install`, then:
`verify:site-kit:release` — exact locale/catalog contracts, shared UI behavior,
localized discovery documents, pack, fresh-consumer and publish dry-run gates

The five public packages release in lockstep through Changesets. Directional
icons use `dds-icon--directional`; the authoritative mirror/keep lists ship as
`@devslab/dds-icons/direction-policy.json`.
The five public packages release in lockstep through Changesets, and the release
is automatic: every push to `main` runs `.github/workflows/release.yml`, which
passes the full verification gate and then lets `changesets/action` either open
the "chore: release dds" version PR (when changeset files are waiting) or publish
whatever version is on `main` but not yet on npm (when that PR has just merged).
Merging the version PR is the release decision; no tag is pushed by hand.
Directional icons use `dds-icon--directional`; the authoritative mirror/keep
lists ship as `@devslab/dds-icons/direction-policy.json`.

All distributable DDS packages use `@devslab/*` on the public npm registry under the
DevsLab Source-Available License 1.0. The compatibility canary remains private and
Expand Down
22 changes: 20 additions & 2 deletions scripts/verify-site-kit-release.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { basename, dirname, join, resolve } from "node:path";
import { pathToFileURL } from "node:url";

const workspace = resolve(new URL("..", import.meta.url).pathname.replace(/^\/(?:([A-Za-z]:))/, "$1"));
Expand All @@ -29,6 +29,24 @@ const runPnpm = (args, cwd) => {
if (result.status !== 0) throw new Error(`pnpm ${args.join(" ")} failed\n${result.stdout}\n${result.stderr}`);
return result.stdout;
};
// `npm publish --dry-run` still asks the registry whether the version exists and
// refuses one that is already published. On main right after a release that is
// the normal state, not a defect — the pack, the manifest and the fresh-consumer
// install are what this gate checks — so "already published" counts as passed.
const publishDryRun = (tarball, cwd) => {
const result = spawnSync(npmCli, [...npmPrefix, "publish", tarball, "--dry-run", "--json", "--ignore-scripts"], {
cwd,
encoding: "utf8",
env: { ...process.env, NPM_CONFIG_CACHE: join(temp, ".npm-cache") },
});
if (result.status === 0) return;
const output = `${result.stdout}\n${result.stderr}`;
if (/cannot publish over the previously published versions/i.test(output)) {
console.log(`${basename(tarball)} is already on npm; publish dry-run skipped`);
return;
}
throw new Error(`publish ${basename(tarball)} --dry-run failed\n${output}`);
};

try {
const packageNames = ["dds-tokens", "dds-css", "dds-icons", "dds-solid", "site-kit"];
Expand All @@ -45,7 +63,7 @@ try {
const packageRoot = join(workspace, "packages", "site-kit");
const bundle = await readFile(join(packageRoot, "dist", "solid.js"), "utf8");
assert.match(bundle, /from\s+["']@devslab\/dds-solid["']/, "site-kit must externalize dds-solid");
run(["publish", siteKitTarball, "--dry-run", "--json", "--ignore-scripts"], packageRoot);
publishDryRun(siteKitTarball, packageRoot);
await writeFile(join(temp, "package.json"), JSON.stringify({ private: true, type: "module" }), "utf8");
run(["install", "--ignore-scripts", "--no-audit", "--no-fund", ...tarballs], temp);
const installedRoot = join(temp, "node_modules", "@devslab", "site-kit");
Expand Down
22 changes: 20 additions & 2 deletions scripts/verify-solid-release.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import assert from "node:assert/strict";
import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { basename, dirname, join, resolve } from "node:path";
import { spawnSync } from "node:child_process";

const workspace = resolve(new URL("..", import.meta.url).pathname.replace(/^\/(?:([A-Za-z]:))/, "$1"));
Expand All @@ -28,6 +28,24 @@ const runPnpm = (args, cwd) => {
if (result.status !== 0) throw new Error(`pnpm ${args.join(" ")} failed\n${result.stdout}\n${result.stderr}`);
return result.stdout;
};
// `npm publish --dry-run` still asks the registry whether the version exists and
// refuses one that is already published. On main right after a release that is
// the normal state, not a defect — the pack, the manifest and the fresh-consumer
// install are what this gate checks — so "already published" counts as passed.
const publishDryRun = (tarball, cwd) => {
const result = spawnSync(npmCli, [...npmPrefix, "publish", tarball, "--dry-run", "--json", "--ignore-scripts"], {
cwd,
encoding: "utf8",
env: { ...process.env, NPM_CONFIG_CACHE: join(temp, ".npm-cache") },
});
if (result.status === 0) return;
const output = `${result.stdout}\n${result.stderr}`;
if (/cannot publish over the previously published versions/i.test(output)) {
console.log(`${basename(tarball)} is already on npm; publish dry-run skipped`);
return;
}
throw new Error(`publish ${basename(tarball)} --dry-run failed\n${output}`);
};
const runNode = (args, cwd) => {
const result = spawnSync(process.execPath, args, { cwd, encoding: "utf8" });
if (result.status !== 0) throw new Error(`node ${args.join(" ")} failed\n${result.stdout}\n${result.stderr}`);
Expand All @@ -51,7 +69,7 @@ try {
const source = await readFile(join(packageRoot, bundle), "utf8");
assert.match(source, /from\s+["']@devslab\/dds-icons["']/, `${bundle} must externalize dds-icons`);
}
run(["publish", solidTarball, "--dry-run", "--json", "--ignore-scripts"], packageRoot);
publishDryRun(solidTarball, packageRoot);
await writeFile(join(temp, "package.json"), JSON.stringify({ private: true, type: "module" }), "utf8");
run(["install", "--ignore-scripts", "--no-audit", "--no-fund", ...tarballs, "solid-js@1.9.15", "jsdom@30.0.1"], temp);
const installedRoot = join(temp, "node_modules", "@devslab", "dds-solid");
Expand Down