docs: plan for What's New release numbers and commit IDs
Three tasks, each ending with the suite green: give every release a number and a commit, switch the popup over and drop the date-shaped version string, then document the convention and verify. The order matters — the new fields land first so the component never sits on a broken type mid-change.
This commit is contained in:
parent
ebfee9eeaa
commit
a0e02759d2
1 changed files with 605 additions and 0 deletions
605
docs/superpowers/plans/2026-11-02-whats-new-release-ids.md
Normal file
605
docs/superpowers/plans/2026-11-02-whats-new-release-ids.md
Normal file
|
|
@ -0,0 +1,605 @@
|
|||
# What's New — release numbers and commit IDs instead of dates — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** The What's New popup identifies every release by a release number and the commit SHA it shipped with, and shows no dates at all.
|
||||
|
||||
**Architecture:** `src/lib/release-notes.ts` stays a hand-written static array. Its `version: string` (a date-shaped string) is replaced by `release: number` plus a new `commit: string`. `src/components/whats-new-dialog.tsx` renders "Release N" and the SHA in the slot the date used to occupy, and the date formatter is **deleted** so no code path can turn a date into text. The release number doubles as the localStorage "seen" marker.
|
||||
|
||||
**Tech Stack:** Next.js (App Router), React, TypeScript (`strict: true`), Tailwind, vitest + @testing-library/react (jsdom), base-ui `Dialog`.
|
||||
|
||||
**Design:** `docs/superpowers/specs/2026-11-02-whats-new-release-ids-design.md`
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- **No dates in the popup.** Not the `date` field, not the version string, not a formatted date. The date formatter is deleted, not merely hidden.
|
||||
- The `date` field **stays** in `src/lib/release-notes.ts` as an authoring reference and is **never rendered**.
|
||||
- Release numbers are contiguous `1..N`, unique, and the array is ordered newest first. The next release after this change is **Release 6**.
|
||||
- Every release carries the short SHA of the last commit that landed in it.
|
||||
- The commit SHA is **plain text, never a link** — the Forgejo instance is on an internal host.
|
||||
- The release notes stay hand-written. No generator, no fetch, no changelog table.
|
||||
- The popup stays client-side and offline-capable: no server round-trip, no account state.
|
||||
- No date is removed anywhere else in the app. `BuildStamp`, order dates, invoice dates and every other date are untouched.
|
||||
- Stage by explicit path. **Never `git add -A`** — this repo carries unrelated untracked artifacts (`reports/`, `scripts/data/backups/`).
|
||||
- Commit and push to `main` when a task's tests pass.
|
||||
|
||||
## Task order, and why it is this way
|
||||
|
||||
Task 1 adds the new fields without removing the old one, so the component still compiles and the popup still behaves exactly as it does today. Task 2 switches the popup over and removes `version` in the same commit. Task 3 documents the convention and verifies the whole thing.
|
||||
|
||||
Every task ends with the full suite green. Do not reorder them.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Give every release a number and a commit
|
||||
|
||||
**Files:**
|
||||
- Create: `src/lib/release-notes.test.ts`
|
||||
- Modify: `src/lib/release-notes.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: nothing.
|
||||
- Produces: `ReleaseNote` gains `release: number` and `commit: string`. `RELEASE_NOTES: ReleaseNote[]` (newest first) and `LATEST_RELEASE: ReleaseNote` (the same object as `RELEASE_NOTES[0]`) are unchanged in shape. `version: string` is still present — Task 2 removes it.
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Create `src/lib/release-notes.test.ts`:
|
||||
|
||||
```ts
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { LATEST_RELEASE, RELEASE_NOTES } from "./release-notes";
|
||||
|
||||
describe("the release-note registry", () => {
|
||||
it("numbers the releases 1..N with no gaps and no duplicates", () => {
|
||||
const numbers = RELEASE_NOTES.map((r) => r.release);
|
||||
|
||||
expect(new Set(numbers).size).toBe(numbers.length);
|
||||
expect([...numbers].sort((a, b) => a - b)).toEqual(
|
||||
Array.from({ length: RELEASE_NOTES.length }, (_, i) => i + 1)
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps the newest release at the top of the list", () => {
|
||||
const numbers = RELEASE_NOTES.map((r) => r.release);
|
||||
|
||||
expect(numbers).toEqual([...numbers].sort((a, b) => b - a));
|
||||
expect(LATEST_RELEASE).toBe(RELEASE_NOTES[0]);
|
||||
});
|
||||
|
||||
it("gives every release a commit SHA", () => {
|
||||
for (const release of RELEASE_NOTES) {
|
||||
expect(release.commit).toMatch(/^[0-9a-f]{7,40}$/);
|
||||
}
|
||||
});
|
||||
|
||||
it("gives every release a headline and at least one item", () => {
|
||||
for (const release of RELEASE_NOTES) {
|
||||
expect(release.title.length).toBeGreaterThan(0);
|
||||
expect(release.items.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the test to verify it fails**
|
||||
|
||||
Run: `npx vitest run src/lib/release-notes.test.ts`
|
||||
|
||||
Expected: FAIL. `r.release` and `r.commit` do not exist yet, so `numbers` is `[undefined, undefined, undefined, undefined, undefined]` and the numbering and SHA assertions fail.
|
||||
|
||||
- [ ] **Step 3: Add the two fields to the interface**
|
||||
|
||||
In `src/lib/release-notes.ts`, change the interface to:
|
||||
|
||||
```ts
|
||||
export interface ReleaseNote {
|
||||
/** Version marker. Bumped to re-notify operators. */
|
||||
version: string;
|
||||
/**
|
||||
* Release number. 1 = the first release. This replaces the date-shaped
|
||||
* `version` string and becomes the "seen" marker.
|
||||
*/
|
||||
release: number;
|
||||
/** Short SHA of the commit this release went out with. */
|
||||
commit: string;
|
||||
/**
|
||||
* Authoring reference only — the day the release was written up.
|
||||
* Deliberately never rendered: the popup carries no dates.
|
||||
*/
|
||||
date: string;
|
||||
/** One-line headline for the release. */
|
||||
title: string;
|
||||
/** The changes, one sentence each. */
|
||||
items: string[];
|
||||
}
|
||||
```
|
||||
|
||||
`version` stays for now. Task 2 removes it.
|
||||
|
||||
- [ ] **Step 4: Fill in the release number and commit on every entry**
|
||||
|
||||
In `src/lib/release-notes.ts`, give each of the five entries its `release` number and `commit` SHA. The array is newest first, so the numbers run 5 down to 1:
|
||||
|
||||
```ts
|
||||
export const RELEASE_NOTES: ReleaseNote[] = [
|
||||
{
|
||||
version: "2026.11.01",
|
||||
release: 5,
|
||||
commit: "139ea25",
|
||||
date: "2026-11-01",
|
||||
title: "Delivery Pool, letterhead lock and a dark mode section",
|
||||
items: [
|
||||
"Delivery Pool: invoiced lines waiting to go out, one card per customer invoice. Assign a driver, upload the signed copy, and the delivery closes.",
|
||||
"Quote letterheads are locked to the customer's company — one letterhead per company, so quotes always go out on the right paper.",
|
||||
"The counter never sells at R0. Items without a price stay off the till, and customer lines go to the price list instead.",
|
||||
"The sidebar now has a Dark Mode section with a light/dark switch. Theme colours live in Settings → Appearance.",
|
||||
"This What's New popup — press it in the sidebar to see what changed.",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "2026.10.30",
|
||||
release: 4,
|
||||
commit: "83aec14",
|
||||
date: "2026-10-30",
|
||||
title: "Collection Confirm gets a second pair of eyes",
|
||||
items: [
|
||||
"Collection Confirm: a Notes button on each PO card, so the driver photos can be checked against the PO before you confirm.",
|
||||
"The AI never changes a price. A different cost on the invoice is flagged as a red Price tick for a person to settle.",
|
||||
"Collection Pool: stock lines can go back to pending and be marked ready again.",
|
||||
"Quotes now record who they were sent to.",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "2026.10.28",
|
||||
release: 3,
|
||||
commit: "3eea87e",
|
||||
date: "2026-10-28",
|
||||
title: "Auto-push and a shortcut to AI settings",
|
||||
items: [
|
||||
"Collection Confirm can auto-push purple cards straight on to invoicing, with a two-day log of everything it moved.",
|
||||
"The Settings page links straight to AI settings.",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "2026.10.26",
|
||||
release: 2,
|
||||
commit: "b212d0f",
|
||||
date: "2026-10-26",
|
||||
title: "A phone board for the managers",
|
||||
items: [
|
||||
"/manager is a mobile board for the managers: every order and where it is, on a phone.",
|
||||
"The preview popup is the same one the desk uses on /search.",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "2026.10.24",
|
||||
release: 1,
|
||||
commit: "55cc7fa",
|
||||
date: "2026-10-24",
|
||||
title: "One POS matcher, AI-only quote matching",
|
||||
items: [
|
||||
"One size-aware matcher now drives the quote assistant, the order fill and the blue price check — so they can't disagree about an item.",
|
||||
"The quote assistant prices only an exact match and shows the closest one instead when it isn't sure.",
|
||||
"Quote matching is AI-only. The old rule-based scorer is retired; the AI links an order when it's sure and suggests otherwise.",
|
||||
"The blue price check badge appears on both Process pages.",
|
||||
],
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
Each `commit` is the **last commit that landed in that release**:
|
||||
|
||||
| Release | Anchor commit | What landed there |
|
||||
|---|---|---|
|
||||
| 5 | `139ea25` | What's New button + Dark Mode section in the sidebar |
|
||||
| 4 | `83aec14` | quotes record `email_to` |
|
||||
| 3 | `3eea87e` | Collection Confirm auto-push with a 2-day log |
|
||||
| 2 | `b212d0f` | `/manager` mobile orders board |
|
||||
| 1 | `55cc7fa` | merge of `feat/ai-only-quote-match` |
|
||||
|
||||
Titles, items and `date` values are carried over untouched. Only the identity is added.
|
||||
|
||||
- [ ] **Step 5: Run the test to verify it passes**
|
||||
|
||||
Run: `npx vitest run src/lib/release-notes.test.ts`
|
||||
|
||||
Expected: PASS, 4 tests.
|
||||
|
||||
- [ ] **Step 6: Run the whole suite to confirm nothing else moved**
|
||||
|
||||
Run: `npx vitest run`
|
||||
|
||||
Expected: PASS. The popup still renders exactly as before — Task 1 changes the data, not the screen.
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add src/lib/release-notes.ts src/lib/release-notes.test.ts
|
||||
git commit -m "refactor(release-notes): give every release a number and a commit
|
||||
|
||||
The version strings are dates (\"2026.11.01\"), so a release has no
|
||||
identity that is not a date. Every entry now also carries a release
|
||||
number and the short SHA of the commit it went out with.
|
||||
|
||||
The date-shaped version string is left in place for now — the popup
|
||||
switches over and drops it in the next commit, so this one stays
|
||||
green on its own.
|
||||
|
||||
A test holds the numbering unique, contiguous and newest-first, and
|
||||
holds every commit to a real SHA."
|
||||
```
|
||||
|
||||
- [ ] **Step 8: Push**
|
||||
|
||||
```bash
|
||||
git push origin main
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: The popup shows the number and the commit, and no date
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/lib/release-notes.ts`
|
||||
- Modify: `src/components/whats-new-dialog.tsx`
|
||||
- Modify: `src/components/whats-new-dialog.test.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `ReleaseNote.release` (number) and `ReleaseNote.commit` (string) from Task 1.
|
||||
- Produces: `WhatsNewButton` — unchanged signature `({ className }: { className?: string })`. The "seen" marker written to `localStorage["wfdash-whats-new-seen"]` is now `String(LATEST_RELEASE.release)`, i.e. `"5"`.
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Replace the whole of `src/components/whats-new-dialog.test.tsx` with:
|
||||
|
||||
```tsx
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
import { WhatsNewButton } from "./whats-new-dialog";
|
||||
import { LATEST_RELEASE } from "@/lib/release-notes";
|
||||
|
||||
const SEEN_KEY = "wfdash-whats-new-seen";
|
||||
|
||||
/**
|
||||
* Any date that could reach the popup: the ISO shape ("2026-11-01"), the
|
||||
* "1 Nov 2026" shape, and the "November 1, 2026" shape.
|
||||
*/
|
||||
const DATE_PATTERN =
|
||||
/\d{4}-\d{2}-\d{2}|\d{1,2}\s+(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{4}|(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\s+\d{1,2},?\s+\d{4}/i;
|
||||
|
||||
describe("WhatsNewButton", () => {
|
||||
beforeEach(() => {
|
||||
window.localStorage.clear();
|
||||
});
|
||||
|
||||
it("flags the latest release as new until it has been read", () => {
|
||||
render(<WhatsNewButton />);
|
||||
|
||||
expect(screen.getByTestId("whats-new-badge")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("marks the release as seen when the popup is opened", () => {
|
||||
render(<WhatsNewButton />);
|
||||
|
||||
// The dialog is opened by us, not by base-ui, so the "seen" stamp
|
||||
// has to be written here. A version that only fired from
|
||||
// onOpenChange would leave the badge lit forever.
|
||||
fireEvent.click(screen.getByRole("button", { name: "What's New" }));
|
||||
|
||||
expect(window.localStorage.getItem(SEEN_KEY)).toBe(
|
||||
String(LATEST_RELEASE.release)
|
||||
);
|
||||
});
|
||||
|
||||
it("does not flag a release the operator has already read", () => {
|
||||
window.localStorage.setItem(SEEN_KEY, String(LATEST_RELEASE.release));
|
||||
render(<WhatsNewButton />);
|
||||
|
||||
expect(screen.queryByTestId("whats-new-badge")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows the release notes in the popup", async () => {
|
||||
render(<WhatsNewButton />);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "What's New" }));
|
||||
|
||||
expect(await screen.findByText(LATEST_RELEASE.title)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("identifies the release by number and commit", async () => {
|
||||
render(<WhatsNewButton />);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "What's New" }));
|
||||
|
||||
expect(
|
||||
await screen.findByText(`Release ${LATEST_RELEASE.release}`)
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText(LATEST_RELEASE.commit)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("never shows a date in the popup", async () => {
|
||||
render(<WhatsNewButton />);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "What's New" }));
|
||||
|
||||
await screen.findByText(LATEST_RELEASE.title);
|
||||
|
||||
// The whole component is on the page here — the button and the
|
||||
// portalled dialog — so this covers every string the popup can put
|
||||
// on screen.
|
||||
expect(document.body.textContent ?? "").not.toMatch(DATE_PATTERN);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the test to verify it fails**
|
||||
|
||||
Run: `npx vitest run src/components/whats-new-dialog.test.tsx`
|
||||
|
||||
Expected: FAIL on three counts — the "seen" marker is still the old version string, "Release 5" is not on screen, and the popup still renders "1 Nov 2026", which the date guard catches.
|
||||
|
||||
- [ ] **Step 3: Delete the date formatter**
|
||||
|
||||
In `src/components/whats-new-dialog.tsx`, delete the whole function:
|
||||
|
||||
```ts
|
||||
function formatDate(iso: string): string {
|
||||
const d = new Date(`${iso}T00:00:00`);
|
||||
if (Number.isNaN(d.getTime())) return iso;
|
||||
return d.toLocaleDateString("en-ZA", {
|
||||
day: "numeric",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Switch the "seen" marker to the release number**
|
||||
|
||||
In `src/components/whats-new-dialog.tsx`, add a module-level marker next to `SEEN_KEY`:
|
||||
|
||||
```ts
|
||||
const SEEN_KEY = "wfdash-whats-new-seen";
|
||||
|
||||
/**
|
||||
* The "seen" marker for the newest release. It is the release number, so
|
||||
* adding a new entry in src/lib/release-notes.ts is what re-notifies every
|
||||
* operator.
|
||||
*/
|
||||
const LATEST_MARKER = String(LATEST_RELEASE.release);
|
||||
```
|
||||
|
||||
Then replace `markSeen` and `isNew`:
|
||||
|
||||
```tsx
|
||||
const markSeen = useCallback(() => {
|
||||
try {
|
||||
window.localStorage.setItem(SEEN_KEY, LATEST_MARKER);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
setState((s) => ({ ...s, seen: LATEST_MARKER }));
|
||||
}, []);
|
||||
|
||||
const isNew = state.mounted && state.seen !== LATEST_MARKER;
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Render the release number and the commit**
|
||||
|
||||
In `src/components/whats-new-dialog.tsx`, replace the badge row:
|
||||
|
||||
```tsx
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide",
|
||||
i === 0
|
||||
? "bg-primary/15 text-primary"
|
||||
: "bg-muted text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
Release {release.release}
|
||||
</span>
|
||||
<span className="font-mono text-xs text-muted-foreground">
|
||||
{release.commit}
|
||||
</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
The commit sits in the slot the date used to occupy. It is monospace, matching the build stamp in the corner of the app.
|
||||
|
||||
- [ ] **Step 6: Update the file header comment**
|
||||
|
||||
In `src/components/whats-new-dialog.tsx`, replace the header comment block with:
|
||||
|
||||
```tsx
|
||||
/**
|
||||
* What's New — a button in the sidebar that opens a popup of the
|
||||
* releases that have landed since the operator last looked.
|
||||
*
|
||||
* Each release is identified by its number and by the commit it went out
|
||||
* with. Nothing in here carries a date: the date formatter that used to
|
||||
* sit next to the release label is gone, so a date cannot reach the
|
||||
* screen even by accident.
|
||||
*
|
||||
* "Seen" is tracked in localStorage against the latest release number.
|
||||
* Add a new entry in src/lib/release-notes.ts and the button lights up
|
||||
* again for everyone — no server round-trip, no account state.
|
||||
*/
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Drop the date-shaped version string from the registry**
|
||||
|
||||
The component no longer reads `version`, so it can go. In `src/lib/release-notes.ts`, remove `version` from the interface:
|
||||
|
||||
```ts
|
||||
export interface ReleaseNote {
|
||||
/** Release number. 1 = the first release. Also the "seen" marker. */
|
||||
release: number;
|
||||
/** Short SHA of the commit this release went out with. */
|
||||
commit: string;
|
||||
/**
|
||||
* Authoring reference only — the day the release was written up.
|
||||
* Deliberately never rendered: the popup carries no dates.
|
||||
*/
|
||||
date: string;
|
||||
/** One-line headline for the release. */
|
||||
title: string;
|
||||
/** The changes, one sentence each. */
|
||||
items: string[];
|
||||
}
|
||||
```
|
||||
|
||||
Then remove the `version:` line from all five entries in `RELEASE_NOTES`.
|
||||
|
||||
- [ ] **Step 8: Update the registry header comment**
|
||||
|
||||
In `src/lib/release-notes.ts`, replace the header comment block with:
|
||||
|
||||
```ts
|
||||
/**
|
||||
* What's New — the release-note registry behind the sidebar popup.
|
||||
*
|
||||
* A plain data file, newest first. There's no changelog table and no
|
||||
* fetch: the notes are written by hand when a release goes out, and the
|
||||
* popup renders whatever is in here. That keeps the whole feature
|
||||
* client-side (it opens instantly, works offline) and means the copy
|
||||
* stays deliberate rather than being generated from commit messages.
|
||||
*
|
||||
* A release is identified by its number and by the commit it went out
|
||||
* with. There are no dates on screen — `date` is kept here only so the
|
||||
* author can see which window of commits a release covers while writing
|
||||
* it, and it is deliberately never rendered.
|
||||
*
|
||||
* To publish a release:
|
||||
* 1. merge the work, then take the short SHA — `git log --oneline -1`
|
||||
* 2. add an entry at the top of the array with the next release number
|
||||
* 3. the release number is the "seen" marker, so bumping it is what
|
||||
* makes the What's New button light up for every operator again
|
||||
*/
|
||||
```
|
||||
|
||||
- [ ] **Step 9: Run the test to verify it passes**
|
||||
|
||||
Run: `npx vitest run src/components/whats-new-dialog.test.tsx`
|
||||
|
||||
Expected: PASS, 6 tests.
|
||||
|
||||
- [ ] **Step 10: Confirm the date formatter is really gone**
|
||||
|
||||
Run: `grep -rn "formatDate\|toLocaleDateString\|release.date" src/components/whats-new-dialog.tsx`
|
||||
|
||||
Expected: no output.
|
||||
|
||||
- [ ] **Step 11: Run the whole suite**
|
||||
|
||||
Run: `npx vitest run`
|
||||
|
||||
Expected: PASS. `src/components/sidebar.test.tsx` only asserts the button exists, so it is unaffected.
|
||||
|
||||
- [ ] **Step 12: Commit**
|
||||
|
||||
```bash
|
||||
git add src/lib/release-notes.ts src/components/whats-new-dialog.tsx src/components/whats-new-dialog.test.tsx
|
||||
git commit -m "feat(whats-new): show the release number and commit instead of the date
|
||||
|
||||
The popup rendered the day each release went out, which gave the office
|
||||
a way to ask what was done on a given day. Each release now shows its
|
||||
number and the short SHA it shipped with, and the date-shaped version
|
||||
string is gone from the registry.
|
||||
|
||||
The date formatter is deleted rather than hidden, so no code path can
|
||||
put a date on screen, and a test fails the build if one comes back.
|
||||
|
||||
The 'seen' marker moves from the version string to the release number,
|
||||
so the badge lights up once for everyone on this deploy and then
|
||||
settles."
|
||||
```
|
||||
|
||||
- [ ] **Step 13: Push**
|
||||
|
||||
```bash
|
||||
git push origin main
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Document the convention, and verify the whole thing
|
||||
|
||||
**Files:**
|
||||
- Modify: `AGENTS.md` (append a new section at the end of the file)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: the final shape of `src/lib/release-notes.ts` from Task 2.
|
||||
- Produces: nothing consumed by code. The section tells the next agent how to publish a release and that dates are banned from the popup.
|
||||
|
||||
- [ ] **Step 1: Add the AGENTS.md section**
|
||||
|
||||
Append to the end of `AGENTS.md`:
|
||||
|
||||
```markdown
|
||||
# What's New (sidebar popup) — release numbers and commits, never dates
|
||||
|
||||
The sidebar's "What's New" popup lists releases from `src/lib/release-notes.ts` — a
|
||||
hand-written array, newest first. There is no changelog table and no fetch: the notes are
|
||||
typed in when a release goes out. The "New" badge lights up when the top entry's release
|
||||
number differs from `wfdash-whats-new-seen` in the operator's localStorage.
|
||||
|
||||
- A release is identified by **its number** and **the short SHA of the commit it went out
|
||||
with** (`commit`). The SHA is plain text — never a link; the Forgejo instance is on an
|
||||
internal host.
|
||||
- **No dates on screen.** The `date` field in the data file is an authoring reference only
|
||||
(which window of commits a release covers) and is never rendered. The date formatter was
|
||||
deleted, so there is no code path that can turn a date into text, and
|
||||
`src/components/whats-new-dialog.test.tsx` fails the build if a date comes back.
|
||||
- To publish a release: merge, take `git log --oneline -1`, add an entry at the top with the
|
||||
next number (the next one after this change is 6) and that SHA. The number is the "seen"
|
||||
marker — bumping it re-notifies everyone.
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the full test suite**
|
||||
|
||||
Run: `npx vitest run`
|
||||
|
||||
Expected: PASS, no failures. The suite is the only cheap regression net this repo has.
|
||||
|
||||
- [ ] **Step 3: Lint the four touched files**
|
||||
|
||||
Run: `npx eslint src/lib/release-notes.ts src/lib/release-notes.test.ts src/components/whats-new-dialog.tsx src/components/whats-new-dialog.test.tsx`
|
||||
|
||||
Expected: no output, exit 0.
|
||||
|
||||
- [ ] **Step 4: Type-check**
|
||||
|
||||
Run: `npx tsc --noEmit`
|
||||
|
||||
Expected: no errors.
|
||||
|
||||
- [ ] **Step 5: Confirm no date survives in the popup**
|
||||
|
||||
Run: `grep -rn "date" src/components/whats-new-dialog.tsx`
|
||||
|
||||
Expected: no output.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add AGENTS.md
|
||||
git commit -m "docs: the What's New popup carries release numbers, never dates"
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Push**
|
||||
|
||||
```bash
|
||||
git push origin main
|
||||
```
|
||||
|
||||
- [ ] **Step 8: Report**
|
||||
|
||||
Tell Dave:
|
||||
- the popup now shows "Release N" and the commit SHA, and no date;
|
||||
- the five existing releases are numbered 1-5, newest is Release 5;
|
||||
- the next release he ships is Release 6 — merge, `git log --oneline -1`, add an entry at the top of `src/lib/release-notes.ts`;
|
||||
- the "New" badge lights up once for every operator on the next deploy, then settles.
|
||||
Loading…
Reference in a new issue