Skip to content

Commit 6069ad9

Browse files
committed
docs: update release-note documentation to include version bump and GitHub Release process
- Enhanced the release-note documentation to clarify the steps for version bumping, committing changes, tagging, and creating a GitHub Release. - Added detailed instructions for user confirmation and the final release note output. - Updated the changelog section to reflect the final release content and requirements for using the `gh` CLI.
1 parent 9bc7984 commit 6069ad9

4 files changed

Lines changed: 239 additions & 10 deletions

File tree

.agents/skills/release-note/SKILL.md

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
---
22
name: release-note
3-
description: Generate a semver-compliant release note from git history
3+
description: Generate a semver-compliant release note from git history; on user confirmation, bump version, update CHANGELOG, create git tag, and publish a GitHub Release with gh
44
user-invocable: true
55
---
66

7-
Generate a release note for this project following https://semver.org/.
7+
Generate a release note for this project following https://semver.org/. When the user **confirms** the version, also **commit** (if needed), **tag**, **push the tag**, and **`gh release create`** using the final notes as the release description.
88

99
## Steps
1010

@@ -75,15 +75,17 @@ Omit empty sections.
7575
After the draft, ask the user:
7676

7777
> **Does `v<next>` look right?**
78-
> - Confirm → finalize and print the clean release note
78+
> - **Confirm** → finalize files, **create git tag**, **create GitHub Release** with the release notes as the description (see step 7)
7979
> - Provide a different version (e.g. `v2.0.0`) → reformat with that version
80-
> - Cancel → discard
80+
> - **Cancel** → discard (no file writes, no tag, no release)
8181
82-
Once confirmed:
82+
### 7. After confirmation — files, tag, and GitHub Release
8383

84-
1. **Update `package.json`** — read the file, set `"version"` to the confirmed version (without the `v` prefix, e.g. `"1.3.0"`), and write it back.
84+
Use the **confirmed** version as `v<next>` (e.g. `v0.4.0`). The **final release body** is the same content as in `CHANGELOG.md` for that version: title `## Release v<next> — <YYYY-MM-DD>` plus sections (omit empty ones). Do **not** include the draft-only line `> Suggested bump: ...`.
8585

86-
2. **Update `CHANGELOG.md`** — prepend the new release section (without the `> Suggested bump:` line) to the top of the entries in `CHANGELOG.md`. If the file doesn't exist, create it with a standard header:
86+
1. **Update `package.json`** — set `"version"` to the semver without `v` (e.g. `"0.4.0"`).
87+
88+
2. **Update `CHANGELOG.md`** — prepend the final release section (same as the GitHub Release description). If the file does not exist, create it with:
8789
```markdown
8890
# Changelog
8991

@@ -93,6 +95,39 @@ Once confirmed:
9395

9496
---
9597
```
96-
Then append the release section below the `---` separator.
9798

98-
3. **Output the final release note** ready to paste into a GitHub Release or PR description.
99+
3. **Commit** the version bump and changelog (if the repo should record the release commit before tagging):
100+
```bash
101+
git add package.json CHANGELOG.md
102+
git commit -m "chore(release): v<next>"
103+
git push origin HEAD
104+
```
105+
Skip commit/push only if the user already committed these changes or explicitly asks not to.
106+
107+
4. **Create an annotated tag** at the release commit:
108+
```bash
109+
git tag -a "v<next>" -m "Release v<next>"
110+
git push origin "v<next>"
111+
```
112+
If the tag already exists locally or on the remote, stop and resolve (delete mistaken tag or bump version) before continuing.
113+
114+
5. **Create the GitHub Release** with the same notes as `CHANGELOG` for this version:
115+
```bash
116+
# Write the final body to a temp file, then:
117+
gh release create "v<next>" --title "Release v<next>" --notes-file /tmp/release-v<next>.md
118+
```
119+
Alternatively pass `--notes` with the full markdown string. Do not use `--generate-notes` if you want the curated changelog to match `CHANGELOG.md`.
120+
121+
Requirements: `gh` CLI authenticated (`gh auth status`), and permission to create releases on the repo.
122+
123+
6. **Output** the final release note in chat for the user’s records (same text as in the Release description).
124+
125+
#### If `gh release create` should create the tag (alternative)
126+
127+
Some teams skip the local annotated tag and let GitHub create the tag from the default branch when publishing the release:
128+
129+
```bash
130+
gh release create "v<next>" --title "Release v<next>" --notes-file /tmp/release-v<next>.md --target <branch>
131+
```
132+
133+
Prefer the **annotated tag + push + `gh release create`** flow (steps 4–5) so the tag exists locally and matches the release commit; use `--target` only when aligning with a specific branch policy.

src/app/rss/articles/route.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import type { Article, User } from "@/backend/models/domain-models";
2+
import { persistenceRepository } from "@/backend/persistence/persistence-repositories";
3+
import { DatabaseTableName } from "@/backend/persistence/persistence-contracts";
4+
import { markdocToHtmlForRss } from "@/lib/markdown/markdoc-html-string";
5+
import getFileUrl from "@/utils/getFileUrl";
6+
import * as sk from "sqlkit";
7+
import { and, desc, neq } from "sqlkit";
8+
9+
const RSS_ITEM_LIMIT = 100;
10+
11+
function escapeXml(text: string): string {
12+
return text
13+
.replace(/&/g, "&amp;")
14+
.replace(/</g, "&lt;")
15+
.replace(/>/g, "&gt;")
16+
.replace(/"/g, "&quot;")
17+
.replace(/'/g, "&apos;");
18+
}
19+
20+
function rfc822Date(d: Date): string {
21+
return d.toUTCString();
22+
}
23+
24+
export async function GET(request: Request) {
25+
const feedUrl = new URL(request.url);
26+
const siteUrl = feedUrl.origin;
27+
28+
const { nodes } = await persistenceRepository.article.paginate({
29+
where: and(neq("published_at", null), neq("approved_at", null)),
30+
page: 1,
31+
limit: RSS_ITEM_LIMIT,
32+
orderBy: [desc("published_at")],
33+
columns: [
34+
"id",
35+
"title",
36+
"handle",
37+
"body",
38+
"cover_image",
39+
"published_at",
40+
"created_at",
41+
"updated_at",
42+
],
43+
joins: [
44+
{
45+
as: "user",
46+
table: DatabaseTableName.users,
47+
type: "left",
48+
on: {
49+
foreignField: "id",
50+
localField: "author_id",
51+
},
52+
columns: ["id", "name", "username"],
53+
} as sk.Join<Article, User>,
54+
],
55+
});
56+
57+
const articles = nodes.filter((a) => a.handle && a.user?.username);
58+
const now = new Date();
59+
60+
const channelItems = articles
61+
.map((article) => {
62+
const username = article.user!.username!;
63+
const href = `${siteUrl}/@${username}/${article.handle}`;
64+
const pub = article.published_at ?? article.created_at ?? now;
65+
66+
const author =
67+
article.user?.name?.trim() || (username ? `@${username}` : "TechDiary");
68+
69+
const coverSrc = article.cover_image
70+
? getFileUrl(article.cover_image).trim()
71+
: "";
72+
const mediaThumb =
73+
coverSrc !== ""
74+
? `\n <media:thumbnail url="${escapeXml(coverSrc)}" />`
75+
: "";
76+
77+
return ` <item>
78+
<title>${escapeXml(article.title)}</title>
79+
<link>${escapeXml(href)}</link>
80+
<guid isPermaLink="true">${escapeXml(href)}</guid>
81+
<pubDate>${rfc822Date(pub)}</pubDate>
82+
<dc:creator>${escapeXml(author)}</dc:creator>${mediaThumb}
83+
<description><![CDATA[${markdocToHtmlForRss(article.body, {
84+
featureImageUrl: coverSrc || undefined,
85+
featureImageAlt: article.title,
86+
})}]]></description>
87+
</item>`;
88+
})
89+
.join("\n");
90+
91+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
92+
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
93+
<channel>
94+
<title>TechDiary — Articles</title>
95+
<link>${escapeXml(siteUrl)}</link>
96+
<description>Latest published articles on TechDiary</description>
97+
<language>bn-bd</language>
98+
<lastBuildDate>${rfc822Date(now)}</lastBuildDate>
99+
<atom:link href="${escapeXml(feedUrl.href)}" rel="self" type="application/rss+xml"/>
100+
${channelItems}
101+
</channel>
102+
</rss>
103+
`;
104+
105+
return new Response(xml, {
106+
headers: {
107+
"Content-Type": "application/rss+xml; charset=utf-8",
108+
"Cache-Control": "public, s-maxage=300, stale-while-revalidate=600",
109+
},
110+
});
111+
}

src/components/widgets/SocialLinksWidget.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ const SocialLinksWidget = () => {
5757
</a>
5858

5959
<a
60-
href="/sitemaps/articles/sitemap.xml"
60+
href="/rss/articles"
6161
target="_blank"
6262
rel="nofollow"
6363
className="text-foreground flex items-center space-x-2"
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import Markdoc, { type Node, Tag } from "@markdoc/markdoc";
2+
import { removeMarkdownSyntax } from "@/lib/utils";
3+
4+
function safeForCdata(html: string): string {
5+
return html.replace(/]]>/g, "]]]]><![CDATA[>");
6+
}
7+
8+
function escapeHtmlAttr(s: string): string {
9+
return s
10+
.replace(/&/g, "&amp;")
11+
.replace(/"/g, "&quot;")
12+
.replace(/'/g, "&#39;")
13+
.replace(/</g, "&lt;");
14+
}
15+
16+
function escapeHtmlText(s: string): string {
17+
return s
18+
.replace(/&/g, "&amp;")
19+
.replace(/</g, "&lt;")
20+
.replace(/>/g, "&gt;");
21+
}
22+
23+
function featureImageMarkup(url: string, alt: string): string {
24+
return `<figure class="rss-feature-image"><img src="${escapeHtmlAttr(url)}" alt="${escapeHtmlAttr(alt)}" /></figure>`;
25+
}
26+
27+
export type MarkdocRssOptions = {
28+
featureImageUrl?: string | null;
29+
featureImageAlt?: string;
30+
};
31+
32+
/**
33+
* Renders article Markdoc to an HTML string for RSS (no React / react-dom/server).
34+
* Uses Markdoc’s HTML renderer; custom tags match site features where useful.
35+
*/
36+
export function markdocToHtmlForRss(
37+
body: string | null | undefined,
38+
opts?: MarkdocRssOptions,
39+
): string {
40+
const md = body ?? "";
41+
const alt = opts?.featureImageAlt ?? "";
42+
const featureUrl = opts?.featureImageUrl?.trim();
43+
const prefix = featureUrl ? featureImageMarkup(featureUrl, alt) : "";
44+
45+
try {
46+
const ast = Markdoc.parse(md);
47+
const content = Markdoc.transform(ast, {
48+
tags: {
49+
youtube: {
50+
attributes: {
51+
id: { type: String, required: true },
52+
},
53+
transform(node: Node) {
54+
return new Tag("iframe", {
55+
src: `https://www.youtube.com/embed/${node.attributes.id}`,
56+
width: "560",
57+
height: "315",
58+
title: "YouTube video",
59+
allow:
60+
"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture",
61+
allowfullscreen: "true",
62+
});
63+
},
64+
},
65+
livecode: {
66+
attributes: {
67+
template: { type: String, default: "react" },
68+
content: { type: String, required: true },
69+
},
70+
transform(node: Node) {
71+
return new Tag("pre", { class: "livecode-rss" }, [
72+
node.attributes.content as string,
73+
]);
74+
},
75+
},
76+
},
77+
});
78+
return safeForCdata(prefix + Markdoc.renderers.html(content));
79+
} catch {
80+
const text = removeMarkdownSyntax(md, 200) ?? "";
81+
return safeForCdata(`${prefix}<p>${escapeHtmlText(text)}</p>`);
82+
}
83+
}

0 commit comments

Comments
 (0)