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
62 changes: 61 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Comment on lines +50 to +57

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"
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
80 changes: 80 additions & 0 deletions scripts/extract-changelog-section.mjs
Original file line number Diff line number Diff line change
@@ -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 <version>
// 例: 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 <version>');
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>]` から始まる行。version 自体に regex メタ文字
// (`.` など)が含まれるので、indexOf で literal 検索する。同じ version 文字列
// が body 中に偶然出る可能性を避けるため、行頭起点 (`\n## [` / 先頭) のみ採用。
// en: Search by literal `## [<version>]` 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`);
Loading