From 225f8a2f940cfaad9f888e2b94560553455b81de Mon Sep 17 00:00:00 2001 From: JeremyDev87 Date: Sat, 9 May 2026 10:19:37 +0900 Subject: [PATCH] =?UTF-8?q?docs(release):=20v1=20=EC=B6=9C=EC=8B=9C=20?= =?UTF-8?q?=EA=B2=8C=EC=9D=B4=ED=8A=B8=EB=A5=BC=20=EB=B3=B4=EA=B0=95?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yml | 9 ++- README.en.md | 4 +- README.md | 4 +- bin/maximus.js | 3 +- docs/github-action-marketplace.md | 25 +++++-- docs/npm-wrapper-runtime.md | 18 +++-- docs/release-operator-runbook.md | 80 +++++++++++++++------ scripts/assert-installed-native-runtime.mjs | 3 +- scripts/update-release-docs.mjs | 11 +-- scripts/validate-rust-release-wiring.mjs | 44 ++++++++++-- test/github-action-wiring.test.js | 2 +- test/release-docs.test.js | 10 +-- 12 files changed, 153 insertions(+), 60 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a1f76ef..0b5d560 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,11 +1,14 @@ name: release -# Source of truth is the verified release tag itself. Publishing a GitHub Release -# does not trigger npm publication again; Release Drafter only prepares notes. +# Source of truth is the verified package release tag itself. Moving major tags +# such as v1 are consumer pointers only and must not trigger npm publication. +# Publishing a GitHub Release does not trigger npm publication again; Release +# Drafter only prepares notes. on: push: tags: - - "v*" + - "v[0-9]+.[0-9]+.[0-9]+" + - "v[0-9]+.[0-9]+.[0-9]+-*" workflow_dispatch: inputs: release_tag: diff --git a/README.en.md b/README.en.md index aed1143..dd281d8 100644 --- a/README.en.md +++ b/README.en.md @@ -130,9 +130,9 @@ Default inputs: - `command`: `audit`, `doctor`, `fix` - `path`: project path to inspect, default `.` - `registry-url`: optional npm registry override for pre-release smoke or private registry validation -- `release-tag`: replace this with a published release tag, for example `v0.1.0` +- `release-tag`: replace this with a published immutable release tag, for example `v1.0.0`. After the stable major tag passes smoke, `v1` is also valid. -Maintainers should use the [release operator runbook](https://github.com/JeremyDev87/maximus/blob/master/docs/release-operator-runbook.md) for alpha or stable releases and same-tag reruns. Release Drafter only refreshes draft notes on `master`; actual publication stays in the tag-driven release workflow. +Maintainers should use the [release operator runbook](https://github.com/JeremyDev87/maximus/blob/master/docs/release-operator-runbook.md) for alpha or stable releases and same-tag reruns. Release Drafter only refreshes draft notes on `master`; actual publication and major tag promotion stay gated by the tag-driven release workflow and action smoke results. ## Local Development diff --git a/README.md b/README.md index 2d2dcef..e512e50 100644 --- a/README.md +++ b/README.md @@ -130,9 +130,9 @@ Maximus audit - `command`: `audit`, `doctor`, `fix` - `path`: 검사할 프로젝트 경로, 기본값 `.` - `registry-url`: pre-release smoke나 사설 registry 검증이 필요할 때만 쓰는 optional npm registry override -- `release-tag`: publish된 릴리즈 태그를 넣으세요. 예: `v0.1.0` +- `release-tag`: publish된 immutable 릴리즈 태그를 넣으세요. 예: `v1.0.0`. 안정 major tag가 smoke를 통과한 뒤에는 `v1`도 사용할 수 있습니다. -유지보수자가 실제 alpha/stable 릴리즈를 준비하거나 같은 태그를 안전하게 재실행할 때는 [release operator runbook](https://github.com/JeremyDev87/maximus/blob/master/docs/release-operator-runbook.md)을 기준으로 진행합니다. Release Drafter는 `master`에서 draft notes만 갱신하며, 실제 publish는 tag-driven release workflow만 담당합니다. +유지보수자가 실제 alpha/stable 릴리즈를 준비하거나 같은 태그를 안전하게 재실행할 때는 [release operator runbook](https://github.com/JeremyDev87/maximus/blob/master/docs/release-operator-runbook.md)을 기준으로 진행합니다. Release Drafter는 `master`에서 draft notes만 갱신하며, 실제 publish와 major tag promotion은 tag-driven release workflow와 action smoke 결과를 기준으로 진행합니다. ## 로컬 개발 diff --git a/bin/maximus.js b/bin/maximus.js index 5249b8a..6d8d277 100755 --- a/bin/maximus.js +++ b/bin/maximus.js @@ -14,6 +14,7 @@ const cliArgs = process.argv.slice(2); const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const frozenJsReferenceNote = "Rust가 Maximus의 canonical runtime입니다. 포함된 JS reference는 frozen 상태이며 legacy 호환 명령을 위한 임시 compatibility bridge로만 유지됩니다."; +const EXECUTABLE_PROBE_TIMEOUT_MS = 5000; try { const runtime = await resolveRuntime(cliArgs); @@ -404,7 +405,7 @@ async function probeExecutable(binaryPath) { const timeout = setTimeout(() => { child.kill("SIGKILL"); settle(false); - }, 1000); + }, EXECUTABLE_PROBE_TIMEOUT_MS); child.on("error", () => { clearTimeout(timeout); diff --git a/docs/github-action-marketplace.md b/docs/github-action-marketplace.md index 7952539..17bcba9 100644 --- a/docs/github-action-marketplace.md +++ b/docs/github-action-marketplace.md @@ -10,7 +10,18 @@ ## 사용 경로 -안정 태그를 발행한 뒤에는 다음처럼 wrapper action 경로로 사용할 수 있습니다. +안정 태그를 발행한 뒤에는 root action 또는 Marketplace wrapper action 경로로 사용할 수 있습니다. + +Root action: + +```yaml +- uses: JeremyDev87/maximus@v1 + with: + command: audit + path: . +``` + +Marketplace wrapper action: ```yaml - uses: JeremyDev87/maximus/.github/actions/marketplace-wrapper@v1 @@ -27,9 +38,12 @@ ## 버전 태그 전략 -- stable consumer 예시는 major tag `v1`를 우선으로 안내합니다. -- 재현 가능한 pinning이 필요하면 `v1.2.3`처럼 immutable release tag를 사용합니다. -- `v1` 같은 moving major tag는 stable release가 준비된 뒤에만 최신 stable release로 이동합니다. +- stable consumer 예시는 moving major tag `v1`를 우선으로 안내합니다. +- 재현 가능한 pinning이 필요하면 `v1.0.0`처럼 immutable release tag를 사용합니다. +- `v1`은 `v1.0.0` 같은 immutable stable tag publish가 끝난 뒤에만 같은 commit으로 이동합니다. +- `v1`은 prerelease tag로 이동하지 않습니다. +- `v1` 이동은 npm publication trigger가 아니며, `release.yml`은 `v1.0.0` 같은 package release tag만 받습니다. +- `v1` 이동 후에는 `action-smoke.yml`을 `--ref v1`로 실행해서 root action과 marketplace wrapper action을 둘 다 검증합니다. ## 구현 원칙 @@ -40,5 +54,6 @@ ## 유지보수 체크리스트 - root `action.yml` 입력이 바뀌면 wrapper action 입력도 같은 turn에 동기화합니다. -- release smoke는 여전히 root action contract와 published tag 기준으로 검증합니다. +- release smoke는 root action contract, marketplace wrapper contract, published tag 기준으로 검증합니다. +- `v1` tag 이동은 immutable stable tag publish와 smoke가 끝난 뒤 별도 확인을 받고 수행합니다. - README 예시 추가가 필요하면 `README.md` / `README.en.md`를 소유한 별도 lane에서 처리합니다. diff --git a/docs/npm-wrapper-runtime.md b/docs/npm-wrapper-runtime.md index 19fce1e..3c31a4e 100644 --- a/docs/npm-wrapper-runtime.md +++ b/docs/npm-wrapper-runtime.md @@ -3,8 +3,9 @@ ## 목적 - 루트 `maximus` npm package를 thin launcher로 유지하면서 실제 실행은 Rust binary로 위임한다. -- 플랫폼별 binary package를 optional dependency로 두고, hard cutover 전까지는 packed install과 repo 개발 환경 모두에서 reference runtime fallback을 허용한다. +- 플랫폼별 binary package를 optional dependency로 두고, hard cutover 전까지는 packed install과 repo 개발 환경 모두에서 제한적 reference runtime fallback을 허용한다. - placeholder platform package가 잘못 publish되더라도 hard cutover 전에는 wrapper가 이를 무시하고 JS reference runtime으로 fallback한다. +- v1.0.0의 정식 native runtime 표면은 macOS와 Linux glibc 4개 package로 고정한다. ## 런타임 선택 순서 @@ -17,21 +18,24 @@ 3. `target/debug/maximus`가 없고 `target/release/maximus`가 있으면 그 binary를 사용한다. 4. repository local binary가 없고 설치된 platform package가 있으면 그 안의 `bin/maximus` Rust binary를 실행한다. - 단, placeholder marker(`MAXIMUS_RUST_BINARY_PLACEHOLDER`)가 있으면 실행하지 않고 다음 후보로 넘어간다. -5. hard cutover 전까지 설치된 root package 안의 `src/cli.js` reference runtime으로 fallback한다. +5. hard cutover 전까지 설치된 root package 안의 `src/cli.js` reference runtime으로 제한적으로 fallback한다. 6. 위 경로가 모두 없을 때만 wrapper가 실패한다. ## unsupported 정책 -- 지원 플랫폼: +- v1.0.0 native runtime 지원 플랫폼: - `darwin-arm64` - `darwin-x64` - `linux-arm64-gnu` - `linux-x64-gnu` -- hard cutover 전 미지원 플랫폼: +- v1.0.0 prebuilt native runtime 미지원 플랫폼: - Windows - Linux musl - 기타 미지원 CPU 조합 -- 미지원 플랫폼에서도 `src/cli.js` reference runtime이 남아 있는 동안은 JS fallback으로 계속 동작한다. +- 미지원 플랫폼에서도 `src/cli.js` reference runtime이 남아 있는 동안은 limited compatibility fallback으로만 동작한다. +- JS fallback 허용 범위는 config file이 없고 Rust-only flag가 없는 legacy-compatible `audit`, `doctor`, `fix --dry-run` 흐름이다. +- `maximus.config.json`, `.maximusrc.json`, `--only`, `--skip`, `--fail-on`, `--diff`, `--fix-id`, `--fix-prefix`, `--format`, `--output`, `fix` without `--dry-run`에는 native Rust runtime이 필요하다. +- Windows와 Linux musl은 v1.0.0에서 정식 native 지원 플랫폼으로 표시하지 않는다. JS fallback 제거는 별도 hard cutover 작업으로 다룬다. - reference runtime이 제거된 뒤에는 wrapper가 명확한 unsupported 오류 메시지를 출력해야 한다. ## package layout @@ -51,9 +55,9 @@ ## local smoke 1. 루트 package tarball 생성: - - `npm pack --json > /tmp/maximus-npm-pack.json` + - `env npm_config_cache=/tmp/maximus-release-pack/.npm-cache npm pack --json --pack-destination /tmp/maximus-release-pack > /tmp/maximus-release-pack/pack.json` 2. packed wrapper smoke: - - `node ./scripts/run-packed-wrapper-smoke.mjs /tmp/maximus-npm-pack.json ./test/fixtures/clean-project` + - `node ./scripts/run-packed-wrapper-smoke.mjs /tmp/maximus-release-pack/pack.json ./test/fixtures/clean-project` helper script는 다음을 보장한다. diff --git a/docs/release-operator-runbook.md b/docs/release-operator-runbook.md index 05ff7c1..bf4f72e 100644 --- a/docs/release-operator-runbook.md +++ b/docs/release-operator-runbook.md @@ -2,15 +2,23 @@ This runbook is for maintainers preparing and promoting Maximus releases. -It documents the preflight checks, the alpha-to-stable promotion path, and the rerun rules that match the checked-in GitHub workflows. It does not publish anything by itself. The tag-driven workflow in `.github/workflows/release.yml` remains the only release path. +It documents the preflight checks, the alpha-to-stable promotion path, the v1 major tag policy, and the rerun rules that match the checked-in GitHub workflows. It does not publish anything by itself. The tag-driven workflow in `.github/workflows/release.yml` remains the only release path. ## Release Model -- The release source of truth is the verified Git tag. -- `package.json` version and the tag must match exactly. For example, `0.2.0-alpha.1` must be released from `v0.2.0-alpha.1`. +- The release source of truth is the verified package release tag. +- `package.json` version and the tag must match exactly. For example, `1.0.0-alpha.1` must be released from `v1.0.0-alpha.1`. +- `.github/workflows/release.yml` listens only to package release tags such as `v1.0.0` and `v1.0.0-alpha.1`; moving major tags such as `v1` must not trigger npm publication. - Prerelease versions publish with the npm dist-tag `next`. - Stable versions publish with the npm dist-tag `latest`. -- `.github/workflows/release-drafter.yml` only maintains draft notes on `master`. It does not publish npm packages or run release smoke jobs. +- v1.0.0 ships the root package `@jeremyfellaz/maximus` plus four native runtime packages: + - `@jeremyfellaz/maximus-darwin-arm64` + - `@jeremyfellaz/maximus-darwin-x64` + - `@jeremyfellaz/maximus-linux-arm64-gnu` + - `@jeremyfellaz/maximus-linux-x64-gnu` +- Windows and Linux musl do not have prebuilt native runtime packages in v1.0.0. They remain limited compatibility targets only while the frozen JS fallback is still present. +- The moving major action tag `v1` is updated only after the immutable stable tag, such as `v1.0.0`, has completed npm publication plus root action and marketplace wrapper smoke. Never move `v1` for prerelease tags. +- `.github/workflows/release-drafter.yml` only maintains draft notes on `master`. It does not publish npm packages, move `v1`, or run release smoke jobs. - `workflow_dispatch` reruns are only valid for an existing tag ref. Do not run the release workflow from `master` or any other branch. ## Preflight Before Creating A New Tag @@ -20,7 +28,7 @@ Run this checklist on a clean `master` checkout before creating a new release ta 1. Pull the target commit from `master`. 2. Confirm the release notes draft looks correct on GitHub. Treat Release Drafter output as notes only. 3. Confirm the package namespace state with npm. -4. Run the local final gate. +4. Run the local final gate from the exact release candidate SHA. Suggested commands: @@ -28,7 +36,9 @@ Suggested commands: git switch master git pull --ff-only -export RELEASE_VERSION=0.2.0-alpha.1 +export RELEASE_VERSION=1.0.0 +export NPM_CONFIG_CACHE=/tmp/maximus-npm-cache +export PACK_ROOT=/tmp/maximus-release-pack cargo test --workspace npm test @@ -37,20 +47,20 @@ node --test test/github-action-wiring.test.js node --test test/release-workflow-context.test.js node --test test/wrapper-runtime.test.js test/packed-wrapper-fallback.test.js -npm_config_cache=/tmp/maximus-npm-cache npm view "@jeremyfellaz/maximus@$RELEASE_VERSION" version +env npm_config_cache="$NPM_CONFIG_CACHE" npm view "@jeremyfellaz/maximus@$RELEASE_VERSION" version for package in \ @jeremyfellaz/maximus-darwin-arm64 \ @jeremyfellaz/maximus-darwin-x64 \ @jeremyfellaz/maximus-linux-arm64-gnu \ @jeremyfellaz/maximus-linux-x64-gnu do - npm_config_cache=/tmp/maximus-npm-cache npm view "${package}@${RELEASE_VERSION}" version + env npm_config_cache="$NPM_CONFIG_CACHE" npm view "${package}@${RELEASE_VERSION}" version done -rm -rf /tmp/maximus-release-pack -mkdir -p /tmp/maximus-release-pack -/bin/zsh -lc 'npm_config_cache=/tmp/maximus-release-pack/.npm-cache npm pack --json --pack-destination /tmp/maximus-release-pack > /tmp/maximus-release-pack/pack.json' -node ./scripts/run-packed-wrapper-smoke.mjs /tmp/maximus-release-pack/pack.json ./test/fixtures/clean-project +rm -rf "$PACK_ROOT" +mkdir -p "$PACK_ROOT" +env npm_config_cache="$PACK_ROOT/.npm-cache" npm pack --json --pack-destination "$PACK_ROOT" > "$PACK_ROOT/pack.json" +node ./scripts/run-packed-wrapper-smoke.mjs "$PACK_ROOT/pack.json" ./test/fixtures/clean-project ``` How to read the npm checks: @@ -58,6 +68,7 @@ How to read the npm checks: - Before the first public release, `npm view "@$RELEASE_VERSION" version` returning `E404` is acceptable. - After a release already exists, that exact version should resolve. - If npm returns an auth or permission failure instead of `E404`, stop and confirm the publishing account has access to the `@jeremyfellaz` scope before tagging. +- Keep `npm_config_cache` pointed at a disposable path for all local npm preflight and `npm pack` commands. Some maintainer machines may have a default npm cache that is not writable by the current user. ## Preflight Before A Same-Tag Rerun @@ -68,8 +79,10 @@ The local verification target must match the tag commit, not the current tip of Suggested commands: ```bash -export RELEASE_TAG=v0.2.0 -export RELEASE_VERSION=0.2.0 +export RELEASE_TAG=v1.0.0 +export RELEASE_VERSION=1.0.0 +export NPM_CONFIG_CACHE=/tmp/maximus-npm-cache +export PACK_ROOT=/tmp/maximus-release-pack git fetch --tags origin git switch --detach "$RELEASE_TAG" @@ -81,15 +94,20 @@ node --test test/github-action-wiring.test.js node --test test/release-workflow-context.test.js node --test test/wrapper-runtime.test.js test/packed-wrapper-fallback.test.js -npm_config_cache=/tmp/maximus-npm-cache npm view "@jeremyfellaz/maximus@$RELEASE_VERSION" version +env npm_config_cache="$NPM_CONFIG_CACHE" npm view "@jeremyfellaz/maximus@$RELEASE_VERSION" version for package in \ @jeremyfellaz/maximus-darwin-arm64 \ @jeremyfellaz/maximus-darwin-x64 \ @jeremyfellaz/maximus-linux-arm64-gnu \ @jeremyfellaz/maximus-linux-x64-gnu do - npm_config_cache=/tmp/maximus-npm-cache npm view "${package}@${RELEASE_VERSION}" version + env npm_config_cache="$NPM_CONFIG_CACHE" npm view "${package}@${RELEASE_VERSION}" version done + +rm -rf "$PACK_ROOT" +mkdir -p "$PACK_ROOT" +env npm_config_cache="$PACK_ROOT/.npm-cache" npm pack --json --pack-destination "$PACK_ROOT" > "$PACK_ROOT/pack.json" +node ./scripts/run-packed-wrapper-smoke.mjs "$PACK_ROOT/pack.json" ./test/fixtures/clean-project ``` Rules: @@ -113,8 +131,8 @@ Example: ```bash git switch master git pull --ff-only -git tag -a v0.2.0-alpha.1 -m "release: v0.2.0-alpha.1" -git push origin v0.2.0-alpha.1 +git tag -a v1.0.0-alpha.1 -m "release: v1.0.0-alpha.1" +git push origin v1.0.0-alpha.1 ``` Expected behavior: @@ -122,6 +140,7 @@ Expected behavior: - The release workflow publishes with dist-tag `next`. - Platform packages publish before the root wrapper. - Published-wrapper smoke and GitHub Action smoke both run against the same tagged snapshot. +- The moving major tag `v1` is not moved for prerelease candidates. ## Stable Promotion Flow @@ -130,23 +149,37 @@ Use this path after a prerelease has been validated and you are ready to promote 1. Open a version-only PR that removes the prerelease suffix across the package manifests. 2. Merge that PR to `master`. 3. Re-run the new-tag preflight checklist on the new stable commit. -4. Create and push the stable tag that matches the stable package version. +4. Create and push the immutable stable tag that matches the stable package version. 5. Watch the release workflow and verify the stable version resolves on npm. +6. After the immutable tag publish, published-wrapper smoke, and action smoke are all green, move the `v1` major tag to that same commit. +7. Re-run `action-smoke.yml` through `v1` to prove both the root action and marketplace wrapper work from the moving major tag. Example: ```bash git switch master git pull --ff-only -git tag -a v0.2.0 -m "release: v0.2.0" -git push origin v0.2.0 +git tag -a v1.0.0 -m "release: v1.0.0" +git push origin v1.0.0 +``` + +Major tag promotion after the release workflow is green: + +```bash +export RELEASE_TAG=v1.0.0 +export RELEASE_SHA="$(git rev-list -n 1 "$RELEASE_TAG")" +git tag -f v1 "$RELEASE_TAG" +git push origin refs/tags/v1 --force-with-lease +gh workflow run action-smoke.yml --ref v1 -f release_tag=v1 -f release_sha="$RELEASE_SHA" ``` Expected behavior: - The release workflow publishes with dist-tag `latest`. - The release tag and `package.json` version match exactly. -- Release Drafter continues to prepare the next notes draft on `master`, but it does not publish or promote anything by itself. +- Moving `v1` does not trigger the tag-driven release workflow. +- Root action smoke and marketplace wrapper smoke both pass before and after `v1` moves. +- Release Drafter continues to prepare the next notes draft on `master`, but it does not publish, promote, or move tags by itself. ## Safe Reruns @@ -157,7 +190,7 @@ Use `workflow_dispatch` only with the same tag as both the selected ref and the Example: ```bash -gh workflow run release.yml --ref v0.2.0 -f release_tag=v0.2.0 +gh workflow run release.yml --ref v1.0.0 -f release_tag=v1.0.0 ``` Rules: @@ -200,3 +233,4 @@ Rules: - Keep release-related docs aligned: `README.md`, `README.en.md`, `CONTRIBUTING.md`, this runbook, and the release workflows should describe the same release model. - If a change touches release wiring, package naming, or packed-install behavior, re-run the full preflight checklist before asking a maintainer to tag a release. +- Do not run tag creation, tag movement, release workflow dispatch, or npm publication without an explicit maintainer confirmation for that operation. diff --git a/scripts/assert-installed-native-runtime.mjs b/scripts/assert-installed-native-runtime.mjs index cd4537f..be837c8 100644 --- a/scripts/assert-installed-native-runtime.mjs +++ b/scripts/assert-installed-native-runtime.mjs @@ -7,6 +7,7 @@ import { access, open } from "node:fs/promises"; import { fileURLToPath } from "node:url"; const PLACEHOLDER_MARKER = "MAXIMUS_RUST_BINARY_PLACEHOLDER"; +const EXECUTABLE_PROBE_TIMEOUT_MS = 5000; export async function assertInstalledNativeRuntime(installRoot) { const runtime = await inspectInstalledNativeRuntime(installRoot); @@ -187,7 +188,7 @@ async function probeExecutable(binaryPath) { const timeout = setTimeout(() => { child.kill("SIGKILL"); settle(false); - }, 1000); + }, EXECUTABLE_PROBE_TIMEOUT_MS); child.on("error", () => { clearTimeout(timeout); diff --git a/scripts/update-release-docs.mjs b/scripts/update-release-docs.mjs index ea00a2b..1da79b8 100644 --- a/scripts/update-release-docs.mjs +++ b/scripts/update-release-docs.mjs @@ -9,7 +9,8 @@ const readmePaths = [ ]; const markerStart = ""; const markerEnd = ""; -const exampleReleaseTag = "v0.1.0"; +const exampleReleaseTag = "v1.0.0"; +const majorActionTag = "v1"; const isCheckMode = process.argv.includes("--check"); @@ -72,9 +73,9 @@ export function updateReleaseDocs(readmePath, text) { "- `command`: `audit`, `doctor`, `fix`", "- `path`: 검사할 프로젝트 경로, 기본값 `.`", "- `registry-url`: pre-release smoke나 사설 registry 검증이 필요할 때만 쓰는 optional npm registry override", - `- \`release-tag\`: publish된 릴리즈 태그를 넣으세요. 예: \`${exampleReleaseTag}\``, + `- \`release-tag\`: publish된 immutable 릴리즈 태그를 넣으세요. 예: \`${exampleReleaseTag}\`. 안정 major tag가 smoke를 통과한 뒤에는 \`${majorActionTag}\`도 사용할 수 있습니다.`, "", - "유지보수자가 실제 alpha/stable 릴리즈를 준비하거나 같은 태그를 안전하게 재실행할 때는 [release operator runbook](https://github.com/JeremyDev87/maximus/blob/master/docs/release-operator-runbook.md)을 기준으로 진행합니다. Release Drafter는 `master`에서 draft notes만 갱신하며, 실제 publish는 tag-driven release workflow만 담당합니다.", + "유지보수자가 실제 alpha/stable 릴리즈를 준비하거나 같은 태그를 안전하게 재실행할 때는 [release operator runbook](https://github.com/JeremyDev87/maximus/blob/master/docs/release-operator-runbook.md)을 기준으로 진행합니다. Release Drafter는 `master`에서 draft notes만 갱신하며, 실제 publish와 major tag promotion은 tag-driven release workflow와 action smoke 결과를 기준으로 진행합니다.", `${markerEnd}`, ].join("\n"), "README.en.md": [ @@ -93,9 +94,9 @@ export function updateReleaseDocs(readmePath, text) { "- `command`: `audit`, `doctor`, `fix`", "- `path`: project path to inspect, default `.`", "- `registry-url`: optional npm registry override for pre-release smoke or private registry validation", - `- \`release-tag\`: replace this with a published release tag, for example \`${exampleReleaseTag}\``, + `- \`release-tag\`: replace this with a published immutable release tag, for example \`${exampleReleaseTag}\`. After the stable major tag passes smoke, \`${majorActionTag}\` is also valid.`, "", - "Maintainers should use the [release operator runbook](https://github.com/JeremyDev87/maximus/blob/master/docs/release-operator-runbook.md) for alpha or stable releases and same-tag reruns. Release Drafter only refreshes draft notes on `master`; actual publication stays in the tag-driven release workflow.", + "Maintainers should use the [release operator runbook](https://github.com/JeremyDev87/maximus/blob/master/docs/release-operator-runbook.md) for alpha or stable releases and same-tag reruns. Release Drafter only refreshes draft notes on `master`; actual publication and major tag promotion stay gated by the tag-driven release workflow and action smoke results.", `${markerEnd}`, ].join("\n"), }; diff --git a/scripts/validate-rust-release-wiring.mjs b/scripts/validate-rust-release-wiring.mjs index a20761d..fb28b86 100644 --- a/scripts/validate-rust-release-wiring.mjs +++ b/scripts/validate-rust-release-wiring.mjs @@ -26,6 +26,7 @@ const requiredFiles = { readmeKo: "README.md", readmeEn: "README.en.md", marketplaceGuide: "docs/github-action-marketplace.md", + npmWrapperRuntime: "docs/npm-wrapper-runtime.md", contributing: "CONTRIBUTING.md", releaseRunbook: "docs/release-operator-runbook.md", releaseContextAssertion: "scripts/assert-release-workflow-context.mjs", @@ -53,6 +54,7 @@ export async function validateRustReleaseWiring(repoRoot = process.cwd()) { validateReleaseDrafterConfig(fileContents.releaseDrafterConfig); validateReadmes(fileContents.readmeKo, fileContents.readmeEn); validateMarketplaceGuide(fileContents.marketplaceGuide); + validateNpmWrapperRuntime(fileContents.npmWrapperRuntime); validateContributing(fileContents.contributing); validateReleaseRunbook(fileContents.releaseRunbook); validateReleaseContextAssertion(fileContents.releaseContextAssertion); @@ -252,7 +254,8 @@ function validateManualReleaseBumpWorkflow(manualReleaseBumpText) { } function validateReleaseWorkflow(releaseText) { - assertContains(releaseText, 'push:\n tags:\n - "v*"', "release tag push trigger"); + assertContains(releaseText, 'Moving major tags', "release major tag non-publish comment"); + assertContains(releaseText, 'push:\n tags:\n - "v[0-9]+.[0-9]+.[0-9]+"\n - "v[0-9]+.[0-9]+.[0-9]+-*"', "release package tag push trigger"); assertContains(releaseText, "workflow_dispatch:", "release manual trigger"); assertContains(releaseText, "release_tag:", "release workflow dispatch tag input"); assertContains(releaseText, "validate-release-context:", "release context validation job"); @@ -287,6 +290,10 @@ function validateReleaseWorkflow(releaseText) { assertContains(releaseText, "publish-platform-packages", "wrapper publish ordering"); assertContains(releaseText, "- publish-wrapper", "published wrapper smoke ordering"); assertContains(releaseText, "strategy:\n fail-fast: false\n matrix:", "published wrapper smoke matrix"); + assert.ok( + !releaseText.includes('- "v*"'), + "release workflow should not run for moving major tags like v1", + ); assert.ok( !releaseText.includes("types: [published]"), "release workflow should not use release.published as the source of truth", @@ -305,23 +312,42 @@ function validateReadmes(readmeKoText, readmeEnText) { assertContains(readmeKoText, "npx @jeremyfellaz/maximus audit", "Korean README scoped npx example"); assertContains(readmeKoText, "## GitHub Action", "Korean README action section"); assertContains(readmeKoText, "uses: JeremyDev87/maximus@", "Korean README action example"); - assertContains(readmeKoText, "예: `v0.1.0`", "Korean README release tag guidance"); + assertContains(readmeKoText, "예: `v1.0.0`", "Korean README release tag guidance"); + assertContains(readmeKoText, "`v1`도 사용할 수 있습니다", "Korean README major tag guidance"); assertContains(readmeKoText, "release operator runbook", "Korean README runbook link"); assertContains(readmeKoText, "draft notes", "Korean README draft notes wording"); assertContains(readmeEnText, "npx @jeremyfellaz/maximus audit", "English README scoped npx example"); assertContains(readmeEnText, "## GitHub Action", "English README action section"); assertContains(readmeEnText, "uses: JeremyDev87/maximus@", "English README action example"); - assertContains(readmeEnText, "for example `v0.1.0`", "English README release tag guidance"); + assertContains(readmeEnText, "for example `v1.0.0`", "English README release tag guidance"); + assertContains(readmeEnText, "`v1` is also valid", "English README major tag guidance"); assertContains(readmeEnText, "release operator runbook", "English README runbook link"); assertContains(readmeEnText, "draft notes", "English README draft notes wording"); } function validateMarketplaceGuide(guideText) { + assertContains(guideText, "JeremyDev87/maximus@v1", "marketplace guide root major tag usage"); assertContains(guideText, "JeremyDev87/maximus/.github/actions/marketplace-wrapper@v1", "marketplace guide subpath usage"); + assertContains(guideText, "`v1.0.0`", "marketplace guide immutable tag guidance"); + assertContains(guideText, "`release.yml`은 `v1.0.0` 같은 package release tag만 받습니다", "marketplace guide v1 non-publish contract"); + assertContains(guideText, "`action-smoke.yml`을 `--ref v1`", "marketplace guide v1 smoke guidance"); assertContains(guideText, "`registry-url`", "marketplace guide registry input"); assertContains(guideText, "root `action.yml`", "marketplace guide root action source-of-truth note"); } +function validateNpmWrapperRuntime(runtimeText) { + assertContains(runtimeText, "v1.0.0 native runtime 지원 플랫폼", "npm runtime v1 support heading"); + assertContains(runtimeText, "Windows", "npm runtime Windows unsupported policy"); + assertContains(runtimeText, "Linux musl", "npm runtime musl unsupported policy"); + assertContains(runtimeText, "limited compatibility fallback", "npm runtime fallback wording"); + assertContains(runtimeText, "fix --dry-run", "npm runtime fix dry-run compatibility"); + assertContains(runtimeText, "별도 hard cutover 작업", "npm runtime hard cutover split"); + + for (const packageName of ["darwin-arm64", "darwin-x64", "linux-arm64-gnu", "linux-x64-gnu"]) { + assertContains(runtimeText, packageName, `npm runtime support policy for ${packageName}`); + } +} + function validateContributing(contributingText) { assertContains(contributingText, "docs/release-operator-runbook.md", "CONTRIBUTING runbook link"); assertContains(contributingText, "Release Drafter as draft-notes automation", "CONTRIBUTING release-drafter contract"); @@ -348,9 +374,14 @@ function validateReleaseRunbook(releaseRunbookText) { assertContains(releaseRunbookText, "## Preflight Before Creating A New Tag", "runbook new-tag preflight section"); assertContains(releaseRunbookText, "## Preflight Before A Same-Tag Rerun", "runbook rerun preflight section"); assertContains(releaseRunbookText, 'git switch --detach "$RELEASE_TAG"', "runbook detached tag rerun command"); - assertContains(releaseRunbookText, 'gh workflow run release.yml --ref v0.2.0 -f release_tag=v0.2.0', "runbook rerun workflow command"); - assertContains(releaseRunbookText, 'npm view "@jeremyfellaz/maximus@$RELEASE_VERSION" version', "runbook exact root wrapper version check"); - assertContains(releaseRunbookText, 'npm view "${package}@${RELEASE_VERSION}" version', "runbook exact platform package version check"); + assertContains(releaseRunbookText, 'gh workflow run release.yml --ref v1.0.0 -f release_tag=v1.0.0', "runbook rerun workflow command"); + assertContains(releaseRunbookText, 'env npm_config_cache="$NPM_CONFIG_CACHE" npm view "@jeremyfellaz/maximus@$RELEASE_VERSION" version', "runbook exact root wrapper version check"); + assertContains(releaseRunbookText, 'env npm_config_cache="$NPM_CONFIG_CACHE" npm view "${package}@${RELEASE_VERSION}" version', "runbook exact platform package version check"); + assertContains(releaseRunbookText, 'env npm_config_cache="$PACK_ROOT/.npm-cache" npm pack --json --pack-destination "$PACK_ROOT"', "runbook temp-cache npm pack command"); + assertContains(releaseRunbookText, "git tag -f v1 \"$RELEASE_TAG\"", "runbook v1 moving tag command"); + assertContains(releaseRunbookText, "gh workflow run action-smoke.yml --ref v1 -f release_tag=v1", "runbook v1 action smoke command"); + assertContains(releaseRunbookText, "Moving `v1` does not trigger the tag-driven release workflow.", "runbook v1 non-publish contract"); + assertContains(releaseRunbookText, "Windows and Linux musl do not have prebuilt native runtime packages in v1.0.0", "runbook unsupported platform contract"); assertContains(releaseRunbookText, "Do not validate a same-tag rerun from a newer `master` checkout.", "runbook rerun master warning"); for (const packageName of platformPackages) { @@ -360,6 +391,7 @@ function validateReleaseRunbook(releaseRunbookText) { function validateNativeRuntimeAssertion(nativeRuntimeAssertionText) { assertContains(nativeRuntimeAssertionText, "MAXIMUS_RUST_BINARY_PLACEHOLDER", "native runtime placeholder marker check"); + assertContains(nativeRuntimeAssertionText, "EXECUTABLE_PROBE_TIMEOUT_MS = 5000", "native runtime probe timeout"); assertContains(nativeRuntimeAssertionText, "node_modules", "native runtime node_modules lookup"); assertContains(nativeRuntimeAssertionText, "Verified native runtime", "native runtime success output"); } diff --git a/test/github-action-wiring.test.js b/test/github-action-wiring.test.js index 23d5df1..e4e2d53 100644 --- a/test/github-action-wiring.test.js +++ b/test/github-action-wiring.test.js @@ -5,7 +5,7 @@ import { validateRustReleaseWiring } from "../scripts/validate-rust-release-wiri test("Rust release wiring validation passes for the checked-in GitHub automation files", async () => { const summary = await validateRustReleaseWiring(process.cwd()); - assert.equal(summary.checkedFiles.length, 24); + assert.equal(summary.checkedFiles.length, 25); assert.deepEqual(summary.platformPackages, [ "@jeremyfellaz/maximus-darwin-arm64", "@jeremyfellaz/maximus-darwin-x64", diff --git a/test/release-docs.test.js b/test/release-docs.test.js index 2f46d4e..070b73a 100644 --- a/test/release-docs.test.js +++ b/test/release-docs.test.js @@ -10,8 +10,9 @@ test("release docs generator preserves the static release-tag example", async () const nextText = updateReleaseDocs(readmePath, readmeText); assert.equal(nextText, readmeText); - assert.match(nextText, /예: `v0\.1\.0`/); - assert.doesNotMatch(nextText, /예: `v0\.1\.3`/); + assert.match(nextText, /예: `v1\.0\.0`/); + assert.match(nextText, /`v1`도 사용할 수 있습니다/); + assert.doesNotMatch(nextText, /예: `v0\.1\.0`/); }); test("English release docs generator preserves the static release-tag example", async () => { @@ -20,6 +21,7 @@ test("English release docs generator preserves the static release-tag example", const nextText = updateReleaseDocs(readmePath, readmeText); assert.equal(nextText, readmeText); - assert.match(nextText, /for example `v0\.1\.0`/); - assert.doesNotMatch(nextText, /for example `v0\.1\.3`/); + assert.match(nextText, /for example `v1\.0\.0`/); + assert.match(nextText, /`v1` is also valid/); + assert.doesNotMatch(nextText, /for example `v0\.1\.0`/); });