HarborHarbor
DocumentationGuidesPlugins
Recipes

Release Notes

Weekly release notes auto-generated from GitHub commits.

Every Monday at 09:00 UTC, scan the last 7 days of merged PRs in your repo, group them by label, summarise each group with orbit.ai, write the result as a workspace artifact, and post a link to Slack.

The Function

release-notes.ts

defineJob({
  name: "weekly-release-notes",
  description: "Generate weekly release notes from merged PRs.",
  handler: async (ctx) => {
    const since = new Date(Date.now() - 7 * 86_400_000).toISOString();

    const prs = await ctx.plugins.githubMcp.searchIssues({
      q: `repo:acme/api is:pr is:merged merged:>=${since.slice(0, 10)}`,
      sort: "updated",
    });

    const groups: Record<string, typeof prs> = {};
    for (const pr of prs) {
      for (const label of (pr.labels ?? []).map((l) => l.name)) {
        (groups[label] ??= []).push(pr);
      }
    }

    const sections = await Promise.all(
      Object.entries(groups).map(async ([label, items]) => {
        const summary = await ctx.orbit.ai.run({
          model: "anthropic/claude-3.7-sonnet",
          prompt: `Summarise these merged PRs as 3-5 user-facing bullet points:\n\n${items
            .map((p) => `- ${p.title} (#${p.number})`).join("\n")}`,
          maxTokens: 300,
        });
        return `## ${label}\n\n${summary}\n`;
      }),
    );

    const body = [`# Release notes (${since.slice(0, 10)} → today)`, ...sections].join("\n\n");

    const path = `release-notes-${new Date().toISOString().slice(0, 10)}.md`;
    await ctx.orbit.storage.put(path, body, { contentType: "text/markdown" });
    const url = await ctx.orbit.storage.url(path, { expiresIn: "30d" });

    await ctx.plugins.slackMcp.postMessage({
      channel: "#releases",
      text: `Weekly release notes: ${url}`,
    });

    return { artifact: path, url };
  },
});
hrbr inspect -f ./release-notes.job.ts
hrbr inspect 'return await hrbr.triggers.list({ limit: 20 })'

What you get

The artifact lives in workspace storage for 30 days. A signed URL means anyone with the link can read it — no Harbor login required (useful for contractors / public stakeholders).

Variations

  • Per-team feed: filter searchIssues by team label, write multiple artifacts.
  • HTML output: import /sdk/orbit to render a styled report instead of plain markdown.
  • Cross-repo: loop over a configured list of repos before grouping.