diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index be9221e..b64b2bc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,10 +17,10 @@ jobs: if: github.event_name == 'pull_request' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: 22 cache: npm @@ -51,13 +51,13 @@ jobs: new_release_version: ${{ steps.semantic.outputs.new_release_version }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 persist-credentials: false - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: 22 @@ -116,7 +116,7 @@ jobs: packages: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up QEMU uses: docker/setup-qemu-action@v3 diff --git a/README.md b/README.md index 6f7479c..1af2b67 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ Owner/editor members can upload `png`, `jpeg`, `webp`, and `gif` images from the ## Release notes - [v0.10.0](docs/releases/v0.10.0.md): latest release notes +- [Release archive](docs/releases/README.md): all release notes ## Member management diff --git a/docs/releases/README.md b/docs/releases/README.md new file mode 100644 index 0000000..eb9a067 --- /dev/null +++ b/docs/releases/README.md @@ -0,0 +1,4 @@ +# DevWiki Release Notes + +- [v0.10.0](v0.10.0.md) +- [v0.9.0](v0.9.0.md) diff --git a/docs/releases/v0.9.0.md b/docs/releases/v0.9.0.md new file mode 100644 index 0000000..860f3ae --- /dev/null +++ b/docs/releases/v0.9.0.md @@ -0,0 +1,11 @@ +# DevWiki v0.9.0 Release Notes + +Release date: 2026-06-02 + +## GitHub Release Notes + +## [0.9.0](https://github.com/geekgoing/devwiki/compare/v0.8.0...v0.9.0) (2026-06-02) + +### Features + +* 머지 후 화면과 검색 흐름 정리 ([088b083](https://github.com/geekgoing/devwiki/commit/088b08353e1b55997e4c61f966bbcdabcdb12979)) diff --git a/scripts/sync-release-docs.mjs b/scripts/sync-release-docs.mjs index 21093d4..d0df26b 100644 --- a/scripts/sync-release-docs.mjs +++ b/scripts/sync-release-docs.mjs @@ -1,9 +1,11 @@ -import { mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises"; +import { mkdir, readFile, readdir, writeFile } from "node:fs/promises"; import path from "node:path"; +import { pathToFileURL } from "node:url"; const RELEASE_DIR = path.join("docs", "releases"); +const RELEASE_ARCHIVE_FILE = "README.md"; -function parseArgs(argv) { +export function parseArgs(argv) { const args = { date: new Date().toISOString().slice(0, 10), notesFile: null, @@ -42,7 +44,7 @@ function parseArgs(argv) { return args; } -function normalizeVersion(value) { +export function normalizeVersion(value) { const trimmed = value.trim(); const version = trimmed.startsWith("v") ? trimmed.slice(1) : trimmed; @@ -56,7 +58,7 @@ function normalizeVersion(value) { }; } -function buildReleaseDoc({ date, notes, tag }) { +export function buildReleaseDoc({ date, notes, tag }) { const releaseNotes = notes.trim() || "No release notes were generated."; return `# DevWiki ${tag} Release Notes @@ -69,6 +71,15 @@ ${releaseNotes} `; } +export function buildReleaseArchive(tags) { + const links = tags.map((tag) => `- [${tag}](${tag}.md)`).join("\n"); + + return `# DevWiki Release Notes + +${links} +`; +} + async function readReleaseNotes(notesFile) { if (!notesFile) { return ""; @@ -77,10 +88,11 @@ async function readReleaseNotes(notesFile) { return readFile(notesFile, "utf8"); } -function updateReadmeReleaseLink(readme, tag) { +export function updateReadmeReleaseLink(readme, tag) { const section = `## Release notes - [${tag}](docs/releases/${tag}.md): latest release notes +- [Release archive](docs/releases/README.md): all release notes `; if (readme.includes("## Release notes")) { @@ -99,60 +111,58 @@ function updateReadmeReleaseLink(readme, tag) { return readme.replace(marker, `${section}\n${marker}`); } -async function renameLatestReleaseDocIfNeeded(targetPath, targetTag) { - let entries = []; - - try { - entries = await readdir(RELEASE_DIR); - } catch { - return; - } +function compareReleaseTagsDesc(left, right) { + return right.localeCompare(left, undefined, { + numeric: true, + sensitivity: "base", + }); +} - if (entries.includes(`${targetTag}.md`)) { - return; - } +async function getReleaseDocTags(releaseDir) { + const entries = await readdir(releaseDir); - const releaseDocs = entries + return entries .filter((entry) => /^v\d+\.\d+\.\d+\.md$/.test(entry)) - .sort((left, right) => - right.localeCompare(left, undefined, { - numeric: true, - sensitivity: "base", - }), - ); - - if (releaseDocs.length !== 1) { - return; - } - - await rename( - path.join(RELEASE_DIR, releaseDocs[0]), - targetPath, - ); + .map((entry) => entry.replace(/\.md$/, "")) + .sort(compareReleaseTagsDesc); } -async function main() { - const args = parseArgs(process.argv.slice(2)); - const { tag } = normalizeVersion(args.version); - const notes = await readReleaseNotes(args.notesFile); - const releasePath = path.join(RELEASE_DIR, `${tag}.md`); +export async function syncReleaseDocs({ cwd = process.cwd(), date, notesFile, version }) { + const { tag } = normalizeVersion(version); + const notes = await readReleaseNotes(notesFile); + const releaseDir = path.join(cwd, RELEASE_DIR); + const releasePath = path.join(releaseDir, `${tag}.md`); + const archivePath = path.join(releaseDir, RELEASE_ARCHIVE_FILE); - await mkdir(RELEASE_DIR, { recursive: true }); - await renameLatestReleaseDocIfNeeded(releasePath, tag); + await mkdir(releaseDir, { recursive: true }); await writeFile( releasePath, buildReleaseDoc({ - date: args.date, + date, notes, tag, }), ); + await writeFile(archivePath, buildReleaseArchive(await getReleaseDocTags(releaseDir))); - const readme = await readFile("README.md", "utf8"); - await writeFile("README.md", updateReadmeReleaseLink(readme, tag)); + const readmePath = path.join(cwd, "README.md"); + const readme = await readFile(readmePath, "utf8"); + await writeFile(readmePath, updateReadmeReleaseLink(readme, tag)); } -main().catch((error) => { - console.error(error instanceof Error ? error.message : error); - process.exit(1); -}); +async function main() { + const args = parseArgs(process.argv.slice(2)); + + await syncReleaseDocs({ + date: args.date, + notesFile: args.notesFile, + version: args.version, + }); +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + main().catch((error) => { + console.error(error instanceof Error ? error.message : error); + process.exit(1); + }); +} diff --git a/scripts/sync-release-docs.test.mjs b/scripts/sync-release-docs.test.mjs new file mode 100644 index 0000000..a1c8e90 --- /dev/null +++ b/scripts/sync-release-docs.test.mjs @@ -0,0 +1,68 @@ +import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; + +import { syncReleaseDocs } from "./sync-release-docs.mjs"; + +const tempDirs = []; + +async function makeFixture() { + const cwd = await mkdtemp(path.join(os.tmpdir(), "devwiki-release-docs-")); + tempDirs.push(cwd); + + await mkdir(path.join(cwd, "docs", "releases"), { recursive: true }); + await writeFile( + path.join(cwd, "README.md"), + `# DevWiki + +## Release notes + +- [v0.9.0](docs/releases/v0.9.0.md): latest release notes + +## Member management +`, + ); + await writeFile( + path.join(cwd, "docs", "releases", "v0.9.0.md"), + "# DevWiki v0.9.0 Release Notes\n", + ); + await writeFile( + path.join(cwd, "release-notes.md"), + "## [0.10.0](https://github.com/geekgoing/devwiki/compare/v0.9.0...v0.10.0)\n", + ); + + return cwd; +} + +afterEach(async () => { + await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); +}); + +describe("syncReleaseDocs", () => { + it("adds the new release doc without deleting older release docs", async () => { + const cwd = await makeFixture(); + + await syncReleaseDocs({ + cwd, + date: "2026-06-02", + notesFile: path.join(cwd, "release-notes.md"), + version: "v0.10.0", + }); + + await expect(readFile(path.join(cwd, "docs", "releases", "v0.9.0.md"), "utf8")) + .resolves.toContain("v0.9.0"); + await expect(readFile(path.join(cwd, "docs", "releases", "v0.10.0.md"), "utf8")) + .resolves.toContain("0.10.0"); + await expect(readFile(path.join(cwd, "docs", "releases", "README.md"), "utf8")) + .resolves.toBe(`# DevWiki Release Notes + +- [v0.10.0](v0.10.0.md) +- [v0.9.0](v0.9.0.md) +`); + await expect(readFile(path.join(cwd, "README.md"), "utf8")) + .resolves.toContain("[v0.10.0](docs/releases/v0.10.0.md)"); + await expect(readFile(path.join(cwd, "README.md"), "utf8")) + .resolves.toContain("[Release archive](docs/releases/README.md)"); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts index 6c26f2c..9ce0aa2 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -9,6 +9,6 @@ export default defineConfig({ }, test: { environment: "node", - include: ["src/**/*.test.ts"], + include: ["src/**/*.test.ts", "scripts/**/*.test.mjs"], }, });