Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5e3c574
chore(ci):add automated dated release workflow
Andes-indica Aug 18, 2026
282c579
updated docs
Andes-indica Aug 18, 2026
941706e
chore(ci): fix release workflow f-string and update docs
Andes-indica Aug 18, 2026
10892c7
ci: add workflow_dispatch to auto-release for manual testing
Andes-indica Aug 18, 2026
2b95956
ci: fix release step GITHUB_TOKEN and use
Andes-indica Aug 18, 2026
97095c0
ci: pin actions/checkout to full commit SHA
Andes-indica Aug 18, 2026
1f4024b
ci: auto-release - migrate notes to Bun TS, fix lint/format; adjust w…
Andes-indica Aug 19, 2026
877f28c
ci(workflows): pin oven-sh/setup-bun v2 to commit SHA in auto-release…
Andes-indica Aug 19, 2026
a1a0719
ci:resolved since-declaration causing the test error
Andes-indica Aug 20, 2026
337fb92
ci:resolved tag format error
Andes-indica Aug 20, 2026
b8f6fc3
ci:fix release retry boundary and orphan tag handling
Andes-indica Aug 21, 2026
fa29954
fix release retry and orphan tag recovery
Andes-indica Aug 21, 2026
7ecc729
Merge branch 'Noveum:main' into ci/auto-release-workflow
Andes-indica Aug 23, 2026
7585b19
ci:fix automated release workflow contract
Andes-indica Aug 24, 2026
2517e9f
Merge branch 'Noveum:main' into ci/auto-release-workflow
Andes-indica Aug 24, 2026
9cb3df4
fix release workflow orphan recovery
Andes-indica Aug 24, 2026
a3021d3
fix automated release workflow wiring
Andes-indica Aug 25, 2026
6d9ca76
docs: align automated release guidance
imshashank Aug 25, 2026
1da4666
fix(ci): recover prior dated release tags
imshashank Aug 25, 2026
c4c6d60
fix(ci): recover historical dated releases
imshashank Aug 25, 2026
1e0fa1e
fix(ci): avoid overlapping recovery notes
imshashank Aug 25, 2026
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
74 changes: 74 additions & 0 deletions .github/workflows/auto-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
name: Automated Releases

on:
schedule:
- cron: '0 0 * * 0'
workflow_dispatch:
concurrency:
group: automated-release
cancel-in-progress: false

permissions:
contents: write
pull-requests: read

jobs:
tag-and-release:
name: Create dated tag and release notes
runs-on: ubuntu-latest
steps:
- name: Checkout
Comment thread
Andes-indica marked this conversation as resolved.
Outdated
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
fetch-depth: 0
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

- name: Setup Bun for scripts
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
with:
bun-version: '1.3.14'

- name: Install JS/TS deps
run: bun install --frozen-lockfile

- name: Build release notes (TypeScript)
id: build_notes
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
since_date: ${{ steps.since.outputs.since_date }}
run: |
set -euo pipefail
bun ./scripts/release-notes.ts
echo "notes_path=RELEASE_NOTES.md" >> "$GITHUB_OUTPUT"

- name: Create dated tag
Comment thread
Andes-indica marked this conversation as resolved.
Outdated
id: create_tag
run: |
set -euo pipefail
BASE_TAG=$(date -u +%Y.%m.%d)
TAG="$BASE_TAG"
COUNT=0
while git ls-remote --tags origin "refs/tags/$TAG" | grep -q .; do
COUNT=$((COUNT+1))
TAG="$BASE_TAG-$COUNT"
done
echo "Tag will be: $TAG"
git tag -a "$TAG" -m "Automated release $TAG" $GITHUB_SHA
git push origin "$TAG"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"

- name: Create GitHub release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
TAG=${{ steps.create_tag.outputs.tag }}
if [ ! -f RELEASE_NOTES.md ]; then
echo "No release notes found, creating placeholder."
echo "Automated release for ${GITHUB_SHA}" > RELEASE_NOTES.md
fi
gh release create "$TAG" --repo "$GITHUB_REPOSITORY" --title "$TAG" --notes-file RELEASE_NOTES.md
7 changes: 4 additions & 3 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,10 @@ enforced.
## Releases

Orbit ships continuously from `main`. There are no long lived release branches
and no backporting, so self-hosted deployments should track `main` or a recent
tag.
and no backporting. To make deployments traceable we publish automated dated
tags and GitHub releases (weekly and on merges to `main`), so self-hosted
deployments can track `main` or a recent dated tag.

Anything requiring action from someone self-hosting is labelled
[`breaking change`](https://github.com/Noveum/orbit/labels/breaking%20change)
and called out in the release notes.
and called out prominently in the generated release notes.
7 changes: 4 additions & 3 deletions docs/self-hosting.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,11 +261,12 @@ bun run build
Always complete the database release before the code that depends on it goes live.
The production Vercel build refuses to deploy when the configured database cannot
be verified or is missing a required schema object. Additional legacy tables and
indexes are reported and preserved. Orbit ships continuously from `main` and there
is no backporting, so track `main` or a recent tag.
indexes are reported and preserved. Orbit ships continuously from `main`. We
also publish automated dated tags and GitHub releases (weekly and on merges to
`main`) so you can track `main` or a recent dated tag for deployed versions.

Watch the [releases](https://github.com/Noveum/orbit/releases) for anything
labelled `breaking change`.
labelled `breaking change` and follow the upgrade notes in the associated release.
Comment thread
Andes-indica marked this conversation as resolved.

### Backups

Expand Down
130 changes: 130 additions & 0 deletions scripts/release-notes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
#!/usr/bin/env bun
import { writeFile } from 'node:fs/promises';

type Label = { name: string };
type PullRequest = {
number: number;
title: string;
html_url: string;
body?: string | null;
labels?: Label[];
merged_at?: string | null;
};

const repo = process.env['GITHUB_REPOSITORY'];
const token = process.env['GITHUB_TOKEN'];
const sinceEnv =
process.env['since_date'] || process.env['INPUT_SINCE'] || process.env['GITHUB_SINCE'];
const _githubSha = process.env['GITHUB_SHA'] || '';

if (!repo) {
console.error('GITHUB_REPOSITORY is not set');
process.exitCode = 2;
throw new Error('GITHUB_REPOSITORY is not set');
}

const since = sinceEnv || new Date(Date.now() - 7 * 24 * 3600 * 1000).toISOString();
const [owner, repoName] = repo.split('/');

const headers: Record<string, string> = {
Accept: 'application/vnd.github+json',
'User-Agent': 'orbit-release-bot',
};
if (token) headers['Authorization'] = `token ${token}`;

async function fetchJson(url: string): Promise<unknown> {
const res = await fetch(url, { headers });
const text = await res.text();
if (!res.ok) {
throw new Error(`Request failed ${res.status}: ${text}`);
}
return JSON.parse(text);
}

async function fetchPageOfPRs(page: number): Promise<PullRequest[]> {
Comment thread
Andes-indica marked this conversation as resolved.
Outdated
const url = `https://api.github.com/repos/${owner}/${repoName}/pulls?state=closed&per_page=100&sort=updated&direction=desc&page=${page}`;
const data = await fetchJson(url);
if (!Array.isArray(data)) return [];
return data as PullRequest[];
}

async function collectMergedPRs(): Promise<PullRequest[]> {
const merged: PullRequest[] = [];
const sinceDate = new Date(since);
for (let page = 1; ; page++) {
const pagePRs = await fetchPageOfPRs(page);
if (pagePRs.length === 0) break;
for (const p of pagePRs) {
if (!p.merged_at) continue;
const mergedAt = new Date(p.merged_at);
if (mergedAt < sinceDate) return merged;
merged.push(p);
}
if (pagePRs.length < 100) break;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
return merged;
}

function groupByArea(prs: PullRequest[]) {
const areas: Record<string, PullRequest[]> = {};
const breaking: PullRequest[] = [];
for (const p of prs) {
const labels = (p.labels || []).map((l) => l.name);
const body: string = p.body || '';
const hasBreakingLabel = labels.some((n) => n.toLowerCase() === 'breaking change');
const hasBreakingBody = /BREAKING CHANGE/.test(body);
if (hasBreakingLabel || hasBreakingBody) {
breaking.push(p);
continue;
}
const area = labels.find((n) => n.startsWith('area:')) || 'Other';
areas[area] = areas[area] || [];
areas[area].push(p);
}
return { areas, breaking };
}

function renderNotes(prs: PullRequest[]): string {
const { areas, breaking } = groupByArea(prs);
let body = `Automated release for ${_githubSha}\n\n`;

if (breaking.length) {
body += '## Breaking changes\n';
for (const pr of breaking) {
body += `- ${pr.title} (#${pr.number}) ${pr.html_url}\n`;
if (pr.body) {
const excerpt = pr.body.split(/\r?\n/).slice(0, 6).join('\n ');
body += `\n ${excerpt}\n`;
}
}
body += '\n';
}

const areaNames = Object.keys(areas).sort();
for (const area of areaNames) {
const title = area.replace('area:', 'Area: ');
body += `## ${title}\n`;
for (const pr of areas[area] ?? []) {
body += `- ${pr.title} (#${pr.number}) ${pr.html_url}\n`;
}
body += '\n';
}

if (!breaking.length && areaNames.length === 0) {
body += 'No merged pull requests found since the last tag.\n';
}

return body;
}

async function main(): Promise<void> {
const prs = await collectMergedPRs();
const notes = renderNotes(prs);
await writeFile('RELEASE_NOTES.md', notes, 'utf8');
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
console.log('WROTE RELEASE_NOTES.md');
}

main().catch((err: unknown) => {
console.error('Failed to generate release notes:', err);
process.exitCode = 2;
});
Loading