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
27 changes: 27 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,33 @@ jobs:
echo "new_release_published=false" >> "$GITHUB_OUTPUT"
fi

- name: Sync release notes docs
if: steps.semantic.outputs.new_release_published == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_VERSION: ${{ steps.semantic.outputs.new_release_version }}
run: |
RELEASE_TAG="v${RELEASE_VERSION}"
NOTES_FILE="${RUNNER_TEMP}/release-notes.md"

gh release view "${RELEASE_TAG}" --json body --jq .body > "${NOTES_FILE}"
node scripts/sync-release-docs.mjs \
--version "${RELEASE_VERSION}" \
--date "$(date -u +%F)" \
--notes-file "${NOTES_FILE}"

if git diff --quiet -- README.md docs/releases; then
echo "release notes docs are already synced for ${RELEASE_TAG}"
exit 0
fi

git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add README.md docs/releases
git commit -m "docs: 릴리즈 노트 ${RELEASE_TAG} 동기화"
git push "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" HEAD:main

build-and-push:
needs: release
runs-on: ubuntu-latest
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
- Per-member favorites
- Database-backed full-text search with body snippets
- Expanded, threaded document discussions
- Recent discussion activity on the home dashboard
- Share metadata, generated app icons, and document link copying

## Local setup
Expand Down Expand Up @@ -43,7 +44,7 @@ Owner/editor members can upload `png`, `jpeg`, `webp`, and `gif` images from the

## Release notes

- [v0.5.0](docs/releases/v0.5.0.md): threaded discussions, learning-route cleanup, search and navigation polish
- [v0.9.0](docs/releases/v0.9.0.md): latest release notes

## Member management

Expand Down
40 changes: 0 additions & 40 deletions docs/releases/v0.5.0.md

This file was deleted.

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))
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"verify:mvp-data": "node scripts/verify-devwiki-mvp-data.mjs",
"verify:mvp-ui": "node scripts/verify-devwiki-mvp-ui.mjs",
"verify:supabase": "node scripts/verify-supabase-readiness.mjs",
"release:sync-docs": "node scripts/sync-release-docs.mjs",
"seed:concepts": "node scripts/seed-backend-interview-concepts.mjs"
},
"dependencies": {
Expand Down
158 changes: 158 additions & 0 deletions scripts/sync-release-docs.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
import path from "node:path";

const RELEASE_DIR = path.join("docs", "releases");

function parseArgs(argv) {
const args = {
date: new Date().toISOString().slice(0, 10),
notesFile: null,
version: null,
};

for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
const next = argv[index + 1];

if (arg === "--version") {
args.version = next;
index += 1;
continue;
}

if (arg === "--notes-file") {
args.notesFile = next;
index += 1;
continue;
}

if (arg === "--date") {
args.date = next;
index += 1;
continue;
}

throw new Error(`Unknown argument: ${arg}`);
}

if (!args.version) {
throw new Error("Missing --version");
}

return args;
}

function normalizeVersion(value) {
const trimmed = value.trim();
const version = trimmed.startsWith("v") ? trimmed.slice(1) : trimmed;

if (!/^\d+\.\d+\.\d+$/.test(version)) {
throw new Error(`Invalid semver version: ${value}`);
}

return {
tag: `v${version}`,
version,
};
}

function buildReleaseDoc({ date, notes, tag }) {
const releaseNotes = notes.trim() || "No release notes were generated.";

return `# DevWiki ${tag} Release Notes

Release date: ${date}

## GitHub Release Notes

${releaseNotes}
`;
}

async function readReleaseNotes(notesFile) {
if (!notesFile) {
return "";
}

return readFile(notesFile, "utf8");
}

function updateReadmeReleaseLink(readme, tag) {
const section = `## Release notes

- [${tag}](docs/releases/${tag}.md): latest release notes
`;

if (readme.includes("## Release notes")) {
return readme.replace(
/## Release notes\n\n(?:- .+\n?)+/,
section,
);
}

const marker = "## Member management";

if (!readme.includes(marker)) {
return `${readme.trimEnd()}\n\n${section}`;
}

return readme.replace(marker, `${section}\n${marker}`);
}

async function renameLatestReleaseDocIfNeeded(targetPath, targetTag) {
let entries = [];

try {
entries = await readdir(RELEASE_DIR);
} catch {
return;
}

if (entries.includes(`${targetTag}.md`)) {
return;
}

const releaseDocs = 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,
);
}

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`);

await mkdir(RELEASE_DIR, { recursive: true });
await renameLatestReleaseDocIfNeeded(releasePath, tag);
await writeFile(
releasePath,
buildReleaseDoc({
date: args.date,
notes,
tag,
}),
);

const readme = await readFile("README.md", "utf8");
await writeFile("README.md", updateReadmeReleaseLink(readme, tag));
}

main().catch((error) => {
console.error(error instanceof Error ? error.message : error);
process.exit(1);
});
91 changes: 84 additions & 7 deletions src/app/(protected)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {
ArrowRight,
BookOpen,
Clock3,
MessageSquare,
MessageSquareText,
Route,
Search,
Expand All @@ -27,9 +29,14 @@ import {
documentDetailPath,
} from "@/lib/content-routes";
import { getCurrentMember, getCurrentUser } from "@/lib/auth";
import { getDocuments } from "@/lib/documents";
import { getDocuments, getRecentDiscussions } from "@/lib/documents";
import { formatDate } from "@/lib/format";
import { isSupabaseConfigured } from "@/lib/supabase/env";
import type { DocumentContentType, DocumentSummary } from "@/types/devwiki";
import type {
DocumentContentType,
DocumentSummary,
RecentDiscussion,
} from "@/types/devwiki";

const sectionCards = [
{
Expand Down Expand Up @@ -127,17 +134,60 @@ function SmallDocumentLink({ document }: { document: DocumentSummary }) {
);
}

function DiscussionDocumentLink({
discussion,
}: {
discussion: RecentDiscussion;
}) {
const href = `${documentDetailPath(discussion.document)}#discussion`;

return (
<Link
href={href}
className="group block rounded-lg border bg-muted/35 px-3 py-2 transition hover:border-primary/25 hover:bg-accent/60"
>
<span className="line-clamp-1 text-sm font-medium transition group-hover:text-primary">
{discussion.document.title}
</span>
<span className="mt-1 line-clamp-2 text-xs leading-5 text-muted-foreground">
{discussion.latestCommentBody}
</span>
<span className="mt-2 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
<span>{contentTypeLabels[discussion.document.contentType]}</span>
<span className="inline-flex items-center gap-1">
<MessageSquare size={11} aria-hidden />
{discussion.totalCommentCount}개
</span>
{discussion.replyCount ? (
<span>답글 {discussion.replyCount}개</span>
) : null}
</span>
<span className="mt-1 flex flex-wrap items-center gap-1.5 text-xs text-muted-foreground">
<Clock3 size={11} aria-hidden />
{discussion.latestCommentAuthorLabel} ·{" "}
{formatDate(discussion.latestCommentAt)}
</span>
</Link>
);
}

export default async function Home() {
const configured = isSupabaseConfigured();
const user = await getCurrentUser();
const member = await getCurrentMember();

const canReadPrivate = !configured || Boolean(member);
const allDocuments = await getDocuments({
status: "active",
canReadPrivate,
viewerId: user?.id,
});
const [allDocuments, recentDiscussions] = await Promise.all([
getDocuments({
status: "active",
canReadPrivate,
viewerId: user?.id,
}),
getRecentDiscussions({
canReadPrivate,
viewerId: user?.id,
}),
]);
const allFavoriteDocuments = allDocuments.filter(
(document) => document.isFavorite,
);
Expand Down Expand Up @@ -302,6 +352,33 @@ export default async function Home() {
)}
</CardContent>
</Card>

<Card size="sm">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<MessageSquare
size={16}
className="text-primary"
aria-hidden
/>
최근 토론
</CardTitle>
</CardHeader>
<CardContent className="grid gap-2">
{recentDiscussions.length ? (
recentDiscussions.map((discussion) => (
<DiscussionDocumentLink
key={discussion.document.id}
discussion={discussion}
/>
))
) : (
<p className="rounded-lg bg-muted px-3 py-2 text-sm leading-6 text-muted-foreground">
아직 최근 토론이 없습니다.
</p>
)}
</CardContent>
</Card>
</aside>
</section>
</div>
Expand Down
Loading
Loading