mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-09 01:16:15 -08:00
fix(release): act on the fourth Codex pass
- wrapText left a word longer than the line budget intact, so a valid 60-character unbroken highlight rendered past the edge of the 1200px card and was cropped. Overlong words are now broken at the budget, and a test asserts no returned line can exceed it. The previous test asserting the old behavior encoded the bug. - The announcement commands are documented as `pnpm --silent run`: reproduced that plain `pnpm run release:notes:telegram > post` writes pnpm's lifecycle banner into the file ahead of the post, so the advertised paste-ready output was not paste-ready. 186 tests passing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8c3aebecac
commit
62359c80fc
8 files changed
+60
-11
No files matched your search
+6
-3
@@ -79,8 +79,8 @@ pnpm run release:notes:validate
|
||||
pnpm run release:notes:github
|
||||
pnpm run release:notes:changelog
|
||||
pnpm run release:notes:blog
|
||||
pnpm run release:notes:telegram
|
||||
pnpm run release:notes:reddit
|
||||
pnpm --silent run release:notes:telegram
|
||||
pnpm --silent run release:notes:reddit
|
||||
node tools/release/build-release-notes.mjs --consume
|
||||
```
|
||||
|
||||
@@ -102,7 +102,10 @@ rather than duplicating it). Only `--consume` deletes anything.
|
||||
|
||||
The announcement formats print paste-ready posts to stdout: Telegram plain
|
||||
text guaranteed to fit the 4096-character limit, Reddit markdown with a
|
||||
suggested post title on the first line. Render and save them **before**
|
||||
suggested post title on the first line. Use `pnpm --silent run` for these two —
|
||||
plain `pnpm run` prints its lifecycle banner to the same stdout, so a
|
||||
redirected post starts with two lines of build noise. Render and save them
|
||||
**before**
|
||||
`--consume` — the changelog keeps the entries, but the `highlight:` metadata
|
||||
lives only in the note files. Publishing is manual; nothing posts anywhere.
|
||||
An internal-only release has nothing to announce: both formats then print an
|
||||
|
||||
@@ -33,7 +33,8 @@ pnpm run i18n:check
|
||||
`pnpm nx run electron-backend:build-e2e`, then
|
||||
`pnpm run release:screenshots`.
|
||||
5. Render the announcement drafts, saving output outside the repository:
|
||||
`pnpm run release:notes:telegram` and `pnpm run release:notes:reddit`.
|
||||
`pnpm --silent run release:notes:telegram` and the `:reddit` counterpart
|
||||
(`--silent`, or pnpm's lifecycle banner lands in the saved post).
|
||||
6. Render highlight cards after the screenshots:
|
||||
`pnpm run release:cards:generate`. Review them; copy `hero.jpg` into the
|
||||
blog post's asset directory if it should ship as the hero image.
|
||||
|
||||
@@ -33,7 +33,8 @@ pnpm run i18n:check
|
||||
`pnpm nx run electron-backend:build-e2e`, then
|
||||
`pnpm run release:screenshots`.
|
||||
5. Render the announcement drafts, saving output outside the repository:
|
||||
`pnpm run release:notes:telegram` and `pnpm run release:notes:reddit`.
|
||||
`pnpm --silent run release:notes:telegram` and the `:reddit` counterpart
|
||||
(`--silent`, or pnpm's lifecycle banner lands in the saved post).
|
||||
6. Render highlight cards after the screenshots:
|
||||
`pnpm run release:cards:generate`. Review them; copy `hero.jpg` into the
|
||||
blog post's asset directory if it should ship as the hero image.
|
||||
|
||||
@@ -71,7 +71,7 @@ This file provides guidance to coding agents working in this repository.
|
||||
- CI enforces this: the "Release note gate" job in `.github/workflows/ci.yml` fails PRs that change runtime code without an added `.changes/*.md` or the label (policy in `tools/release/check-release-note-gate.mjs`; tests/e2e/website/mock-server/docs paths are auto-exempt).
|
||||
- The `release-notes` skill covers writing notes; the `release-cut` skill covers the full release sequence. Canonical contract — surfaces, ordering constraints, the required draft asset set: `docs/architecture/release-pipeline.md`.
|
||||
- Validate before finishing: `pnpm run release:notes:validate`.
|
||||
- Announcement drafts and highlight cards are built from the same notes: `pnpm run release:notes:telegram` and `pnpm run release:notes:reddit` print paste-ready posts to stdout (Telegram is guaranteed to fit its 4096-character limit), and `pnpm run release:cards:generate` renders branded 1200×630 highlight cards plus a release hero into `dist/release-highlight-cards/v<version>/`. All three read `highlight:` metadata that exists only in the note files, so they must run before `build-release-notes.mjs --consume`; the cards additionally need `release:screenshots` to have run. Nothing is posted or copied into the website tree automatically.
|
||||
- Announcement drafts and highlight cards are built from the same notes: `pnpm --silent run release:notes:telegram` and `pnpm --silent run release:notes:reddit` print paste-ready posts to stdout (Telegram is guaranteed to fit its 4096-character limit; `--silent` keeps pnpm's lifecycle banner out of a redirected post), and `pnpm run release:cards:generate` renders branded 1200×630 highlight cards plus a release hero into `dist/release-highlight-cards/v<version>/`. All three read `highlight:` metadata that exists only in the note files, so they must run before `build-release-notes.mjs --consume`; the cards additionally need `release:screenshots` to have run. Nothing is posted or copied into the website tree automatically.
|
||||
- Pushes to `master` and `v*` can publish Docker images. A `v*` tag build creates a draft GitHub release.
|
||||
- `pnpm run release:verify:draft` waits for that tag build (polling until the run is indexed, then `gh run watch`) and verifies the draft's status, authored body, and complete required asset set. It is read-only and deliberately fails on an already-published release, because it is the gate that runs before publication.
|
||||
- Publishing the GitHub release verifies its Snap assets and automatically uploads them to `edge`; installed-Snap smoke and candidate/stable promotion remain manual.
|
||||
|
||||
@@ -37,7 +37,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
- CI enforces this: the "Release note gate" job in `.github/workflows/ci.yml` fails PRs that change runtime code without an added `.changes/*.md` or the label (policy in `tools/release/check-release-note-gate.mjs`; tests/e2e/website/mock-server/docs paths are auto-exempt).
|
||||
- The `release-notes` skill covers writing notes; the `release-cut` skill covers the full release sequence. Canonical contract — surfaces, ordering constraints, the required draft asset set: `docs/architecture/release-pipeline.md`.
|
||||
- Validate before finishing: `pnpm run release:notes:validate`.
|
||||
- Announcement drafts and highlight cards are built from the same notes: `pnpm run release:notes:telegram` and `pnpm run release:notes:reddit` print paste-ready posts to stdout (Telegram is guaranteed to fit its 4096-character limit), and `pnpm run release:cards:generate` renders branded 1200×630 highlight cards plus a release hero into `dist/release-highlight-cards/v<version>/`. All three read `highlight:` metadata that exists only in the note files, so they must run before `build-release-notes.mjs --consume`; the cards additionally need `release:screenshots` to have run. Nothing is posted or copied into the website tree automatically.
|
||||
- Announcement drafts and highlight cards are built from the same notes: `pnpm --silent run release:notes:telegram` and `pnpm --silent run release:notes:reddit` print paste-ready posts to stdout (Telegram is guaranteed to fit its 4096-character limit; `--silent` keeps pnpm's lifecycle banner out of a redirected post), and `pnpm run release:cards:generate` renders branded 1200×630 highlight cards plus a release hero into `dist/release-highlight-cards/v<version>/`. All three read `highlight:` metadata that exists only in the note files, so they must run before `build-release-notes.mjs --consume`; the cards additionally need `release:screenshots` to have run. Nothing is posted or copied into the website tree automatically.
|
||||
- Pushes to `master` and `v*` can publish Docker images. A `v*` tag build creates a draft GitHub release.
|
||||
- `pnpm run release:verify:draft` waits for that tag build (polling until the run is indexed, then `gh run watch`) and verifies the draft's status, authored body, and complete required asset set. It is read-only and deliberately fails on an already-published release, because it is the gate that runs before publication.
|
||||
- Publishing the GitHub release verifies its Snap assets and automatically uploads them to `edge`; installed-Snap smoke and candidate/stable promotion remain manual.
|
||||
|
||||
@@ -30,6 +30,11 @@ chosen deliberately by bumping `package.json`.
|
||||
| Highlight cards | `release:cards:generate` | `dist/release-highlight-cards/v<version>/` |
|
||||
| Screenshots | `release:screenshots` | `apps/website/public/blog/<vX-Y>/screenshots/` |
|
||||
|
||||
Run the two stdout commands as `pnpm --silent run …` whenever the output is
|
||||
redirected to a file or a clipboard. Without it pnpm prints its lifecycle
|
||||
banner (`> iptvnator@0.23.0 release:notes:telegram …`) to the same stdout, and
|
||||
the saved post starts with two lines of build noise.
|
||||
|
||||
### The ordering constraint that matters
|
||||
|
||||
`build-release-notes.mjs --consume` is the destructive boundary: it deletes the
|
||||
|
||||
@@ -61,8 +61,13 @@ export function escapeXml(text) {
|
||||
|
||||
/**
|
||||
* Greedy word wrap by character budget. SVG has no automatic text layout and
|
||||
* font metrics vary by host, so the budget is conservative; a single word
|
||||
* longer than the budget gets its own line rather than being cut.
|
||||
* font metrics vary by host, so the budget is conservative.
|
||||
*
|
||||
* Every returned line is at most `maxChars` long, including when a single
|
||||
* word exceeds the budget: such a word is broken at the budget rather than
|
||||
* left whole. Leaving it whole is what a naive wrap does, and a 60-character
|
||||
* unbroken headline — well inside the validated limit — then rendered past
|
||||
* the edge of the 1200px card and was cropped.
|
||||
*
|
||||
* @param {string} text
|
||||
* @param {number} maxChars
|
||||
@@ -75,6 +80,20 @@ export function wrapText(text, maxChars, maxLines) {
|
||||
let current = '';
|
||||
|
||||
for (const word of words) {
|
||||
if (word.length > maxChars) {
|
||||
if (current) {
|
||||
lines.push(current);
|
||||
}
|
||||
|
||||
for (let start = 0; start < word.length; start += maxChars) {
|
||||
lines.push(word.slice(start, start + maxChars));
|
||||
}
|
||||
|
||||
// Keep the final chunk open so a following short word can join it.
|
||||
current = lines.pop() ?? '';
|
||||
continue;
|
||||
}
|
||||
|
||||
const candidate = current ? `${current} ${word}` : word;
|
||||
|
||||
if (candidate.length <= maxChars || !current) {
|
||||
|
||||
@@ -78,13 +78,33 @@ describe('wrapText', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('gives an overlong single word its own line instead of cutting it', () => {
|
||||
it('breaks a word longer than the budget instead of overflowing the card', () => {
|
||||
assert.deepEqual(wrapText('supercalifragilistic ok', 10, 3), [
|
||||
'supercalifragilistic',
|
||||
'supercalif',
|
||||
'ragilistic',
|
||||
'ok',
|
||||
]);
|
||||
});
|
||||
|
||||
it('never returns a line wider than the budget', () => {
|
||||
// A 60-character unbroken highlight is valid input; on a 34-character
|
||||
// feature-card budget it must still fit inside the canvas.
|
||||
for (const [text, maxChars, maxLines] of [
|
||||
['W'.repeat(60), 34, 2],
|
||||
['W'.repeat(60), 30, 2],
|
||||
['W'.repeat(60), 60, 1],
|
||||
['a'.repeat(200), 34, 2],
|
||||
['short words only here', 34, 2],
|
||||
]) {
|
||||
for (const line of wrapText(text, maxChars, maxLines)) {
|
||||
assert.ok(
|
||||
line.length <= maxChars,
|
||||
`"${line}" is ${line.length} > ${maxChars}`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('ellipsizes the last kept line on overflow', () => {
|
||||
const lines = wrapText('aaa bbb ccc ddd eee', 3, 2);
|
||||
|
||||
|
||||
Reference in new issue
Block a user