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
2 changes: 2 additions & 0 deletions .changeset/curly-olives-jog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
4 changes: 4 additions & 0 deletions .changeset/gate-release-notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
---

CI: gate PRs targeting main on a properly-filled release-notes-next.md file.
6 changes: 6 additions & 0 deletions .changeset/in-process-mirror-scheduler.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"ornn-api": minor
"ornn-web": minor
---

In-process GitHub mirror reconcile scheduler with admin-configurable cadence (#437). The k8s `CronJob` is gone — the periodic mirror reconcile now runs inside the `ornn-api` pod via Agenda, multipod-safe via per-fire row locking on a shared MongoDB collection. The schedule is editable on the admin GitHub mirror settings page (preset dropdown + custom cron), interpreted in Singapore time (UTC+8, no DST), defaulting to `0 2 * * *` (daily 2am SGT). Empty schedule disables the scheduled reconcile without affecting publish-time webhooks. Mirror configuration is now unified under `SettingsService` — a one-shot boot migration copies any legacy `platform_settings.githubMirror` values into the new section on first boot.
6 changes: 6 additions & 0 deletions .changeset/mirror-scheduled-run-status.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"ornn-api": minor
"ornn-web": minor
---

Surface last-run status of the scheduled mirror reconcile (#475). Both the GitHub mirror settings page and the legacy mirror dashboard now show whether the most recent *scheduled* fire succeeded, failed (with the error message), is currently running, or hasn't happened yet — sourced from Agenda's persisted recurring-job doc so the view is consistent across pods and survives restarts. The previous in-process `lastReconcile` block on `GET /admin/mirror/status` is replaced with a new `scheduledRun` block. Manual `Reconcile now` clicks from the dashboard still work but do not appear in this widget; the 409 "already running" guard on the manual reconcile endpoint is unchanged.
4 changes: 4 additions & 0 deletions .changeset/release-body-too-long-fallback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
---

CI: harden the `changeset-release` workflow against GitHub's 125 000-char release-body limit. Big releases like v0.6.0 (87 consumed changesets) used to fail the `Create tag + GitHub Release` step; the workflow now falls back to a short body linking to the in-repo CHANGELOGs when the inline body would exceed the cap.
4 changes: 4 additions & 0 deletions .changeset/release-notes-from-repo-file.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
---

CI: release workflow now reads `.github/release-notes-next.md` as the GitHub Release body, with a fall-back to a short link-to-CHANGELOG body when the file isn't curated. Replaces the raw-CHANGELOG dump that blew past GitHub's 125 000-char limit on v0.6.0.
4 changes: 4 additions & 0 deletions .changeset/release-notes-template-and-dated.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
---

CI: restructure release notes into a fixed `release-notes-template.md` (immutable, holds format and instructions) + per-release `release-notes-<yyyymmdd>.md` files (copied from template, filled in, retained in repo as historical record). CI gate on develop → main PRs now validates the most recent dated file; release workflow reads the same file. Docs in CONTRIBUTING.md + CLAUDE.md updated.
15 changes: 15 additions & 0 deletions .github/release-notes-20260513.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
## Fixed

- Few technical bugs fixed.

## New Feature

- Mirror reconcile now runs in-process; no external Kubernetes CronJob.
- Mirror schedule editable from admin settings — presets or custom cron, Singapore time.
- "Last run" status on mirror settings page and dashboard: succeeded, failed, running, or never.
- Technical enhancement.

## Changed

- Mirror dashboard's "Last reconcile" tile is now consistent across pods and survives restarts.
- Technical enhancement.
73 changes: 73 additions & 0 deletions .github/release-notes-template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<!--
=============================================================================
DO NOT EDIT THIS FILE.

It's the immutable template that every release's notes file is copied
from. The CI gate on develop → main PRs (check-release-notes.yml)
expects this template to keep its placeholders intact, so the next
release author has a clean source to copy from.

For each release: copy this file to `.github/release-notes-<yyyymmdd>.md`
where <yyyymmdd> is the date the release is being prepared (e.g.
`release-notes-20260512.md`). Edit the copy, not this template.

After release, the dated file stays in the repo as a historical record
of that release's user-facing notes.
=============================================================================

@claude / future-me, when preparing a new release:

1. cp .github/release-notes-template.md \
.github/release-notes-$(date +%Y%m%d).md
2. Read every file under `.changeset/*.md` (these get consumed at
release time). Also skim the bits of `ornn-api/CHANGELOG.md` and
`ornn-web/CHANGELOG.md` that landed since the previous tag if you
want extra context.
3. In the new dated file (NOT this template), replace each
`(write here)` block below with a brief, user-facing summary.
Drop an entire section if there's nothing for it.

Style rules:

- One bullet = one product-level fact a user / agent developer
actually cares about. 6–12 words. Plain prose.
- Cluster purely-technical items (refactors, dep bumps, infra / CI,
type fixes, internal renames) into a single trailing bullet per
section:
Fixed → "Few technical bugs fixed"
New Feature → "Technical enhancement"
Changed → "Technical enhancement"
Don't expand the technical bucket into individual bullets.
- No PR / issue refs, no version numbers, no author thanks, no
"see #N" links. The CHANGELOG link at the bottom of the
auto-generated release body already covers all of that.
- English only. Tight.
- If a section ends up with only the technical-bucket bullet, that's
fine — keep it. If a section ends up with zero bullets (genuinely
nothing happened in that bucket), delete the whole section
including its heading.

How the workflow uses the dated file:

- `.github/workflows/changeset-release.yml` State B finds the most
recent `.github/release-notes-<yyyymmdd>.md` (this template is
excluded), strips its leading HTML comment block, and uses the
remaining prose as the GitHub Release body, with a CHANGELOG-link
footer appended.
- `.github/workflows/check-release-notes.yml` gates PRs to `main` on
a dated file being present with all three sections and no
`(write here)` placeholder. The PR can't merge until the gate is
green.
-->

## Fixed

- (write here)

## New Feature

- (write here)

## Changed

- (write here)
92 changes: 75 additions & 17 deletions .github/workflows/changeset-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -161,26 +161,84 @@ jobs:
git tag -a "${TAG}" -m "Release ${TAG}"
git push origin "${TAG}"

extract_section() {
awk -v v="$1" '
/^## / {
if (found) { exit }
if ($0 ~ "^## " v "$") { found=1; next }
}
found { print }
' "$2"
}
API_NOTES=$(extract_section "${VERSION}" ornn-api/CHANGELOG.md)
WEB_NOTES=$(extract_section "${VERSION}" ornn-web/CHANGELOG.md)

BODY=$(cat <<EOF
## \`ornn-api\` ${VERSION}
${API_NOTES}
## \`ornn-web\` ${VERSION}
${WEB_NOTES}
# Release-body resolution, in priority order (#431 / #435):
#
# 1. Most recent `.github/release-notes-<yyyymmdd>.md` file
# (filename sort, descending). If it exists and the
# author has filled it in (placeholder string
# `(write here)` removed), strip its HTML comment block
# and use the remaining prose as the release body.
# Footer with CHANGELOG links is always appended.
#
# 2. Short body that just links to the per-package
# CHANGELOG sections on the tag. Used when (1) wasn't
# filled in, or when the full inline notes would
# exceed GitHub's 125 000-char body cap (#429 / #430).
#
# `.github/release-notes-template.md` is the immutable
# template the dated file is copied from — never read here.
# The filename-pattern filter (8-digit date) excludes it
# from the dated-file search.
#
# Inline-CHANGELOG body was retired — even when it fits, the
# raw consumed changesets are engineer-speak that doesn't
# belong on a public release page. Authors curate the
# user-facing summary into the dated file before opening
# their develop → main PR, and the gate in
# `.github/workflows/check-release-notes.yml` enforces it
# on PRs to main.
REPO_URL="https://github.com/${GITHUB_REPOSITORY}"
CHANGELOG_FOOTER=$(cat <<EOF

---

Full per-PR detail: [\`ornn-api\` CHANGELOG](${REPO_URL}/blob/${TAG}/ornn-api/CHANGELOG.md#${VERSION//./}) · [\`ornn-web\` CHANGELOG](${REPO_URL}/blob/${TAG}/ornn-web/CHANGELOG.md#${VERSION//./})
EOF
)

RELEASE_NOTES_FILE=$(ls -1 .github/release-notes-*.md 2>/dev/null \
| grep -E '/release-notes-[0-9]{8}\.md$' \
| sort -r \
| head -1)

USE_CURATED=false
if [ -n "$RELEASE_NOTES_FILE" ] && [ -f "$RELEASE_NOTES_FILE" ] && ! grep -qF '(write here)' "$RELEASE_NOTES_FILE"; then
USE_CURATED=true
fi

if $USE_CURATED; then
echo "Using curated release notes from $RELEASE_NOTES_FILE"
# Strip the leading HTML comment block (instructions to the
# author). Everything between `<!--` and the matching `-->`
# on its own line is dropped.
CURATED=$(awk '
BEGIN { in_comment = 0 }
/^<!--/ { in_comment = 1; next }
/-->$/ { if (in_comment) { in_comment = 0; next } }
!in_comment { print }
' "$RELEASE_NOTES_FILE")
BODY="${CURATED}${CHANGELOG_FOOTER}"
else
echo "No curated dated release-notes file found (or placeholder still present) — using short-body fallback."
BODY=$(cat <<EOF
Release notes weren't curated for v${VERSION}. The full per-PR detail is in the in-repo CHANGELOGs on the \`${TAG}\` tag:
${CHANGELOG_FOOTER}
EOF
)
fi

# Final length safety check — even a curated body shouldn't
# exceed the cap, but guard against an author pasting in a
# 100k-char block of prose by accident.
if [ ${#BODY} -gt 120000 ]; then
echo "::warning::Resolved body is ${#BODY} chars (cap 120 000). Falling back to short link body."
BODY=$(cat <<EOF
Release notes body exceeded the size cap. Full per-PR detail is in the in-repo CHANGELOGs:
${CHANGELOG_FOOTER}
EOF
)
fi

gh release create "${TAG}" --title "${TAG}" --notes "${BODY}"

- name: Open sync PR main → develop (B)
Expand Down
109 changes: 109 additions & 0 deletions .github/workflows/check-release-notes.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
name: Check release notes

# Gates every PR targeting `main` on a properly-filled
# `.github/release-notes-<yyyymmdd>.md` file. The release workflow
# falls back to a short link-to-CHANGELOG body when no dated file
# exists or it isn't curated — that fallback is fine for emergencies
# but a poor default. This gate forces the maintainer to create and
# fill in a dated release-notes file BEFORE the release-bump train
# starts, so v<X.Y.Z> always ships with curated release notes on the
# GitHub Releases page. Tracked in #433 / #435.
#
# Files involved:
# - `.github/release-notes-template.md` — immutable template (never
# edited; this gate ignores it and even fails if someone deletes
# it).
# - `.github/release-notes-<yyyymmdd>.md` — one per release, copied
# from the template and filled in. The most recent (by filename
# sort) is what the gate validates.
#
# Required-status check binding (one-time, in the repo Settings UI):
# add `Check release notes` to main's branch-protection list so the
# gate is actually load-bearing.

on:
pull_request:
branches: [main]
types: [opened, synchronize, reopened, edited]

jobs:
check-release-notes:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.sha }}

- name: Validate release notes file
run: |
set -u
TEMPLATE=".github/release-notes-template.md"

# ── 0. Template must exist ─────────────────────────────────
# The template is the canonical source for the dated-file
# format. If someone deletes it, the next maintainer has no
# source to copy from. Fail loud.
if [ ! -f "$TEMPLATE" ]; then
echo "::error file=$TEMPLATE::Template file does not exist. Restore $TEMPLATE — it is the canonical source for per-release notes files."
exit 1
fi

# ── 1. Find the most recent dated release-notes file ──────
# Pattern: release-notes-YYYYMMDD.md (8 digits). The
# template's filename (release-notes-template.md) does NOT
# match this pattern, so it's naturally excluded. Sort
# descending so the freshest date wins.
LATEST=$(ls -1 .github/release-notes-*.md 2>/dev/null \
| grep -E '/release-notes-[0-9]{8}\.md$' \
| sort -r \
| head -1)

if [ -z "$LATEST" ]; then
EXPECTED=".github/release-notes-$(date -u +%Y%m%d).md"
echo "::error file=$TEMPLATE::No dated release-notes file found. Copy the template to a dated file before merging to main:"
echo "::error file=$TEMPLATE:: cp $TEMPLATE $EXPECTED"
echo "::error file=$TEMPLATE:: \$EDITOR $EXPECTED # fill in the three sections"
exit 1
fi

echo "Validating: $LATEST"
FAILED=0

# ── 2. Filename format sanity ──────────────────────────────
# ls already filtered by the regex, but assert again so a
# future ls flag change can't silently let through a
# `release-notes-foo.md` lookalike.
BASENAME=$(basename "$LATEST")
if ! echo "$BASENAME" | grep -qE '^release-notes-[0-9]{8}\.md$'; then
echo "::error file=$LATEST::Filename does not match the expected pattern \`release-notes-YYYYMMDD.md\` (8 digits). Got: $BASENAME"
FAILED=1
fi

# ── 3. All three section headings present ──────────────────
for section in "## Fixed" "## New Feature" "## Changed"; do
if ! grep -qF "$section" "$LATEST"; then
echo "::error file=$LATEST::Missing required heading: \`$section\`. Release notes must contain Fixed / New Feature / Changed sections — leave a section with only the technical-bucket bullet (\"Few technical bugs fixed\" / \"Technical enhancement\") if nothing notable happened in that bucket."
FAILED=1
fi
done

# ── 4. Placeholder `(write here)` removed ──────────────────
# The template uses `(write here)` for each bullet slot. Any
# remaining occurrence in the dated file means at least one
# section hasn't been filled in.
if grep -qF '(write here)' "$LATEST"; then
echo "::error file=$LATEST::Release notes still contain the \`(write here)\` placeholder. Replace each placeholder with brief user-facing bullets (6-12 words each). Cluster technical-only items into a single \`Few technical bugs fixed\` / \`Technical enhancement\` trailing bullet."
FAILED=1
fi

if [ "$FAILED" -ne 0 ]; then
echo ""
echo "Release-notes flow reminder:"
echo " - $TEMPLATE has the formatting rules and the per-file copy command."
echo " - Run \`bun changeset status\` to see what's pending for this release."
echo " - One bullet = one product-level fact, 6-12 words."
echo " - Drop a section entirely if there's genuinely nothing for it."
exit 1
fi

echo "✓ Release notes valid — $LATEST has all three sections, no placeholders."
16 changes: 14 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,19 @@ Select affected package(s), semver bump level (`patch` / `minor` / `major`), wri

Fully automated on the `main` side. No local script to run — developer action is "open a PR, review a PR". `.github/workflows/changeset-release.yml` is a state machine driven by `push: main`.

**Step 1 — Promote `develop` → `main`.** Open a PR `develop → main` and merge it. Regular PR; it carries whatever features + unconsumed `.changeset/*.md` files have piled up on `develop` since the last release.
**Step 0 — Curate release notes.** Before opening the develop → main PR, the maintainer creates a dated release-notes file:

```bash
cp .github/release-notes-template.md .github/release-notes-$(date -u +%Y%m%d).md
# edit the new file — replace each `(write here)` with user-facing bullets
git add .github/release-notes-*.md && git commit && git push
```

The template (`.github/release-notes-template.md`) is immutable — it stays in the repo and supplies the format / instructions for future releases. The dated file (`.github/release-notes-<yyyymmdd>.md`) is per-release; after release it stays in the repo as a historical record. The release workflow reads the most recent dated file at release time and uses it as the GitHub Release body (HTML comment block stripped, CHANGELOG-link footer appended automatically). The CI gate `.github/workflows/check-release-notes.yml` fails the develop → main PR if no dated file exists, if any of `## Fixed` / `## New Feature` / `## Changed` is missing, or if the `(write here)` placeholder is still in the file.

Each dated file has three sections — see the template's comment block for the formatting rules. The short version: one bullet = one product-level fact, 6–12 words; cluster purely-technical items into a single trailing `Few technical bugs fixed` / `Technical enhancement` bullet per section; no PR / issue refs, no version numbers.

**Step 1 — Promote `develop` → `main`.** Open a PR `develop → main` and merge it. Regular PR; it carries whatever features + unconsumed `.changeset/*.md` files + the new `release-notes-<yyyymmdd>.md` file have piled up on `develop` since the last release.

**Step 2 — Review the bot's release-bump PR.** On the `main` push from Step 1, the workflow sees pending `.changeset/*.md` files, so it:

Expand All @@ -125,7 +137,7 @@ Review that PR. Merge with **Squash and merge** (keeps history linear; `main` en
**Step 3 — Tag + GitHub Release + sync back to `develop`.** On the `main` push from Step 2, the workflow sees no pending changesets + `ornn-api/package.json`'s version has no matching `v<version>` tag, so it:

1. Creates an annotated `v<version>` tag and pushes it.
2. Extracts the `## <version>` section from each package's `CHANGELOG.md`, builds a combined body, and calls `gh release create`.
2. Finds the most recent `.github/release-notes-<yyyymmdd>.md`, strips its HTML comment block, appends a CHANGELOG-links footer, and calls `gh release create` with that body. If no dated file exists (or it still has the `(write here)` placeholder), falls back to a short body that just links to the in-repo `CHANGELOG.md` files. A length safety check kicks the body back to the short fallback if it exceeds 120 000 chars (GitHub API caps release bodies at 125 000).
3. Creates branch `sync/post-release-v<version>` from `main`.
4. Opens PR `sync/post-release-v<version> → develop` — **auto-approved + auto-merged** by the same workflow via a direct `PUT /repos/.../pulls/:n/merge` API call with `merge_method: merge`. No human action for the sync step; the PR is a deterministic replay of a commit that already passed CI on `main`.

Expand Down
Loading