From 7489fd44ab274e5dd3ffb55658cb406c300ecc6b Mon Sep 17 00:00:00 2001 From: touyou <465697+touyou@users.noreply.github.com> Date: Tue, 14 Jul 2026 15:36:22 +0900 Subject: [PATCH] =?UTF-8?q?fix(ci):=20publish.yml=20=E3=81=A7=20npm=20publ?= =?UTF-8?q?ish=20=E5=BE=8C=E3=81=AB=20git=20tag=20/=20GitHub=20Release=20?= =?UTF-8?q?=E3=82=92=E8=87=AA=E5=8B=95=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v1.0.6 で GitHub Release の手動作成が漏れていたため、 sparkle-design-cli / sparkle-design-internal の publish.yml と同じ方式 (npm publish 成功後に CHANGELOG.md の該当セクションを release notes として自動でタグ付け・Release 作成)を移植した。 Co-Authored-By: Claude Sonnet 5 --- .github/workflows/publish.yml | 62 ++++++++++++++++++++- CHANGELOG.md | 4 ++ scripts/extract-changelog-section.mjs | 80 +++++++++++++++++++++++++++ 3 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 scripts/extract-changelog-section.mjs diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 4c7b2a3..0c7719b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -7,7 +7,13 @@ jobs: publish: runs-on: ubuntu-latest permissions: - contents: read + # git tag の push と `gh release create` に contents: write が必要。 + # 従来は手動タグ付け運用だったが、v1.0.6 で GitHub Release 作成が漏れたため + # 自動化する(sparkle-design-cli / sparkle-design-internal の publish.yml と同じ対応)。 + # en: `contents: write` is required to push the release tag and create the + # GitHub Release. Manual tagging was missed for v1.0.6, so this mirrors + # sparkle-design-cli / sparkle-design-internal's automation. + contents: write steps: - uses: actions/checkout@v4 @@ -27,3 +33,57 @@ jobs: - run: npm publish env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Tag commit and create GitHub Release + # npm publish が成功した直後に同じ commit に `vX.Y.Z` tag を打ち、 + # CHANGELOG.md の `## [X.Y.Z]` セクションを release notes として + # GitHub Release を自動作成する。手動 tag 漏れを防ぐための後段処理。 + # en: Tag the commit and create a matching GitHub Release after a + # successful npm publish, using the CHANGELOG section as notes. + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + VERSION=$(node -p "require('./package.json').version") + TAG="v${VERSION}" + + if git rev-parse --verify "refs/tags/${TAG}" >/dev/null 2>&1; then + echo "tag ${TAG} already exists locally — skipping git tag push" + else + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git tag "${TAG}" + git push origin "${TAG}" + fi + + if gh release view "${TAG}" >/dev/null 2>&1; then + echo "release ${TAG} already exists — leaving notes untouched" + exit 0 + fi + + NOTES_FILE="$(mktemp)" + node scripts/extract-changelog-section.mjs "${VERSION}" > "${NOTES_FILE}" + if [ ! -s "${NOTES_FILE}" ]; then + # CHANGELOG に該当セクションが無いとき。release は作るが + # 内容は fallback note にして、抜け漏れを後で人間が拾えるよう + # warn を出す。 + # en: No CHANGELOG entry — emit a fallback note and warn so a + # follow-up PR can fix CHANGELOG without blocking publish. + echo "::warning::CHANGELOG.md に [${VERSION}] セクションが見つかりません。fallback note で release を作成します。" + printf 'Release %s\n\nCHANGELOG.md にこのバージョン用のセクションがありません。後追いで CHANGELOG を更新してから本 release notes も編集してください。\n' "${VERSION}" > "${NOTES_FILE}" + fi + + gh release create "${TAG}" \ + --title "${TAG}" \ + --notes-file "${NOTES_FILE}" + + - name: Summary + run: | + VERSION=$(node -p "require('./package.json').version") + { + echo "### Published" + echo "" + echo "- package: \`sparkle-design\`" + echo "- version: \`$VERSION\`" + echo "- GitHub Release: [v${VERSION}](https://github.com/${{ github.repository }}/releases/tag/v${VERSION})" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/CHANGELOG.md b/CHANGELOG.md index 51363d0..cc33ec8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ ## [Unreleased] +### Changed + +- `Publish to npm` workflow に npm publish 後の git tag 付け・GitHub Release 自動作成ステップを追加(v1.0.6 で GitHub Release 作成が漏れていたため。`sparkle-design-cli` / `sparkle-design-internal` の publish.yml と同じ方式) + ## [1.0.6] - 2026-07-14 ### Fixed diff --git a/scripts/extract-changelog-section.mjs b/scripts/extract-changelog-section.mjs new file mode 100644 index 0000000..3ae5e15 --- /dev/null +++ b/scripts/extract-changelog-section.mjs @@ -0,0 +1,80 @@ +#!/usr/bin/env node +// CHANGELOG.md から `## [X.Y.Z]` 形式のセクション本文を 1 つだけ stdout に出す。 +// publish.yml の GitHub Release 自動作成 step が release notes として食わせる +// 想定で、見つからなければ exit 0 + 空出力(caller 側で fallback note に切り替える)。 +// +// usage: node scripts/extract-changelog-section.mjs +// 例: node scripts/extract-changelog-section.mjs 2.0.7-rc.4 +// +// CHANGELOG.md の path はリポジトリ root 固定。CI(actions/checkout した +// working-directory)からも開発者ローカル(リポジトリ root で実行)からも +// `process.cwd()/CHANGELOG.md` で解決される前提。 +// +// en: Print one `## [X.Y.Z]` section body from CHANGELOG.md to stdout for +// publish.yml's GitHub Release step. Empty stdout + exit 0 means the section +// is missing — the caller should fall back to a generic note. + +import fs from 'node:fs'; +import path from 'node:path'; + +const version = process.argv[2]; +if (!version) { + console.error('usage: extract-changelog-section.mjs '); + process.exit(1); +} + +const changelogPath = path.resolve(process.cwd(), 'CHANGELOG.md'); +let content; +try { + content = fs.readFileSync(changelogPath, 'utf8'); +} catch (error) { + // CHANGELOG.md が無い / 読めないのは workflow 側の構成ミス。stderr に + // 原因を出して exit 1(fallback ではなく fail にする)。 + // en: Missing CHANGELOG.md is a workflow misconfig, not a soft miss. + console.error(`failed to read CHANGELOG.md: ${error.message}`); + process.exit(1); +} + +// 探すマーカーは `## []` から始まる行。version 自体に regex メタ文字 +// (`.` など)が含まれるので、indexOf で literal 検索する。同じ version 文字列 +// が body 中に偶然出る可能性を避けるため、行頭起点 (`\n## [` / 先頭) のみ採用。 +// en: Search by literal `## []` at line start to avoid accidental +// substring matches from body text mentioning the same version. +const marker = `## [${version}]`; +const candidates = []; +if (content.startsWith(marker)) { + candidates.push(0); +} +let searchFrom = 0; +const needle = `\n## [${version}]`; +while (true) { + const idx = content.indexOf(needle, searchFrom); + if (idx === -1) break; + candidates.push(idx + 1); + searchFrom = idx + needle.length; +} +if (candidates.length === 0) { + // セクション無し。caller の fallback に任せるため空出力で exit 0。 + // en: Section missing — exit 0 with empty output so the caller can fall + // back to a generic release note. + process.exit(0); +} + +const startIdx = candidates[0]; +const rest = content.slice(startIdx); +// 見出し行 `## [X.Y.Z] - YYYY-MM-DD` 自体は出力から外し、本文だけを返す。 +// 入れたままだと `gh release create --title vX.Y.Z` で付くタイトルと release +// 本文冒頭の見出しが二重表示になり Release UI が冗長になる。 +// en: Strip the `## [X.Y.Z]` heading line itself — `gh release create +// --title vX.Y.Z` already shows the version, so leaving the heading in the +// notes body would duplicate it in the Release UI. +const firstNewlineIdx = rest.indexOf('\n'); +const bodyStartIdx = firstNewlineIdx === -1 ? rest.length : firstNewlineIdx + 1; +// 次の `## [` で打ち切り。同じ version 行を 2 度目に踏まないよう本文開始 +// 位置から探す。 +// en: Stop at the next `## [` heading, starting from the body offset to +// skip the current heading. +const nextHeaderIdx = rest.indexOf('\n## [', bodyStartIdx); +const sectionBody = + nextHeaderIdx === -1 ? rest.slice(bodyStartIdx) : rest.slice(bodyStartIdx, nextHeaderIdx); +process.stdout.write(`${sectionBody.trim()}\n`);