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
10 changes: 5 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions docs/releases/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# DevWiki Release Notes

- [v0.10.0](v0.10.0.md)
- [v0.9.0](v0.9.0.md)
11 changes: 11 additions & 0 deletions docs/releases/v0.9.0.md
Original file line number Diff line number Diff line change
@@ -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))
102 changes: 56 additions & 46 deletions scripts/sync-release-docs.mjs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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;

Expand All @@ -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
Expand All @@ -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 "";
Expand All @@ -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")) {
Expand All @@ -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);
});
}
68 changes: 68 additions & 0 deletions scripts/sync-release-docs.test.mjs
Original file line number Diff line number Diff line change
@@ -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)");
});
});
2 changes: 1 addition & 1 deletion vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ export default defineConfig({
},
test: {
environment: "node",
include: ["src/**/*.test.ts"],
include: ["src/**/*.test.ts", "scripts/**/*.test.mjs"],
},
});