mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 11:06:16 -08:00
Publish "How to Add an Xtream Codes Account to IPTVnator" as the first evergreen guide: what the server URL, username and password are, the Add playlist flow with the connection test and its four verdicts, the Auto-detect method for pasted provider messages, what the import syncs, Account info, refresh, troubleshooting and a seven-question FAQ. The guide is cross-linked from the three download pages and llms.txt. Blog posts gain an optional `faq` frontmatter list: BlogPost.astro renders it as an accordion after the body and emits FAQPage JSON-LD next to the BlogPosting entry. LinkCards and PostButton keep internal links in the same tab. Guide screenshots come from the release capture script: manifest shots may carry a `group`, `--group guides` captures only those into apps/website/public/blog/guides/screenshots/, and a release run skips them. New setup actions open the Add playlist dialog with the mock's fictional Xtream credentials (connection test shown), the Auto-detect method with a labeled hand-out, and the Xtream Live TV view. Dialog helpers and fixture identities move into shared modules so the driver and the navigation actions cannot import each other cyclically. tools/testing/website-guides.test.mjs checks the FAQPage schema, the download-hub link and the shipped screenshots of every guide; screenshot-guards.test.mjs covers group validation and output routing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
257 lines
14 KiB
Markdown
257 lines
14 KiB
Markdown
# Release Pipeline
|
||
|
||
How a release is assembled, from the note an author writes during an ordinary
|
||
PR to the draft GitHub release a human publishes.
|
||
|
||
The agent-facing entry points are the `release-notes` skill (writing notes) and
|
||
the `release-cut` skill (running a release). This document is the contract they
|
||
reference: the asset set, the ordering constraints, and the reasons behind
|
||
them. The skills stay short on purpose; the detail lives here.
|
||
|
||
## Two phases
|
||
|
||
**During ordinary PRs** every user-visible change adds one
|
||
`.changes/<area>-<slug>.md` note, written while the context is fresh. CI's
|
||
"Release note gate" enforces it. Format and field table: `.changes/README.md`.
|
||
|
||
**At release time** `tools/release/build-release-notes.mjs` fans those notes
|
||
out into every surface, then deletes them. Nothing derives the version — it is
|
||
chosen deliberately by bumping `package.json`.
|
||
|
||
## Surfaces built from one set of notes
|
||
|
||
| Surface | Command | Writes |
|
||
| --- | --- | --- |
|
||
| `CHANGELOG.md` section | `release:notes:changelog` | the file |
|
||
| Website blog scaffold | `release:notes:blog` | `apps/website/src/content/blog/<vX-Y>-release-notes.mdx` |
|
||
| GitHub release body | (tag build) | via `extract-changelog-section.mjs --public` |
|
||
| Telegram announcement | `release:notes:telegram` | stdout |
|
||
| Reddit announcement | `release:notes:reddit` | stdout |
|
||
| 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
|
||
note files. The `CHANGELOG.md` keeps every entry's text, but **`highlight:`
|
||
lives only in the note files** and is not recoverable afterwards. Every surface
|
||
that reads it — both announcements and the cards — must therefore run before
|
||
`--consume`. The cards additionally need `release:screenshots` to have already
|
||
published its frames, since they composite them.
|
||
|
||
## `highlight:` — what leads a release
|
||
|
||
An optional note field naming one of the release's two or three headline
|
||
changes. It is rejected on `type: internal`, and capped at **60 characters** to
|
||
keep it headline-sized — roughly what the hero card fits on one line.
|
||
|
||
The cap is an authoring guideline, not a rendering guarantee: character count
|
||
is not width. Card text wraps by *estimated rendered width*
|
||
(`estimateTextWidth`), because 34 `W` at font-size 52 measures ~1948px where
|
||
1072px are available — a character-capped line still ran off the canvas.
|
||
|
||
The SVG names `DM Sans`, but nothing guarantees it is installed: every host
|
||
resolves the fallback chain differently, and the same line measures 0.389 em
|
||
per `r` here against about 0.49 em elsewhere. No estimate can be both tight
|
||
and correct across environments, so the factors sit well above the widest
|
||
observation — locally the model over-estimates every sample by at least 1.25×.
|
||
|
||
That estimate is deliberately **inverted**: narrow characters are enumerated
|
||
and everything else is assumed wide. Enumerating the wide ones instead cannot
|
||
converge — successive review passes each found another under-estimated glyph
|
||
(`W`, then CJK and emoji, then the `ae` ligature) — and a glyph the list misses
|
||
crops the card while every unit test still passes. With the wide default the
|
||
estimate can only run high, and running high costs an early line break nobody
|
||
sees. `tools/release/highlight-cards.test.mjs` renders each sample through sharp and asserts
|
||
the estimate never falls below the measured ink width, which is the guard
|
||
against that whole class of bug.
|
||
|
||
Text that cannot fit even after wrapping is ellipsized, and each emitted line
|
||
carries an SVG `textLength` clamp when the estimate still says it would
|
||
overflow.
|
||
|
||
Highlights drive three behaviors:
|
||
|
||
- **Telegram** leads with them and folds everything else into a "…plus N more"
|
||
counter. A `type: breaking` note is never folded, highlighted or not:
|
||
announcing a breaking change as "fixes and improvements" is worse than a
|
||
longer post. If the breaking changes alone cannot fit the 4096-character
|
||
limit, the render fails with an actionable error rather than dropping one.
|
||
- **Reddit** gives each one an `## Highlights` subsection, with the remaining
|
||
changes grouped below. Its suggested title names as many highlights as
|
||
Reddit's separate 300-character title cap allows and counts the rest — five
|
||
highlights at the validated 60-character maximum already overshoot it while
|
||
the body stays nowhere near its own limit. The body is bounded the same way
|
||
at Reddit's 40,000-character post limit — this repository's accumulated notes already render ~37,000 —
|
||
dropping from the tail of the grouped list, which is ordered breaking →
|
||
feature → fix → perf so the least consequential go first. A breaking change
|
||
is never dropped there either.
|
||
- **The blog scaffold** gives each highlight a row in the opening "What
|
||
changed" table and its own `##` section ahead of everything else, instead of
|
||
emitting `TODO headline (<area>)`. Shape below.
|
||
|
||
Prose fields keep `#`. `parseFrontmatterLine` strips trailing `# comment` text
|
||
only from closed-vocabulary fields (`type`, `area`, `issues`, `screenshot`),
|
||
whose values can never contain one — `highlight: Sources #N chip` is a headline.
|
||
|
||
### Internal-only releases
|
||
|
||
A release whose notes are all `type: internal` is a legal shape: the authored
|
||
GitHub body is empty and GitHub's generated commit list carries the detail.
|
||
Both announcement formats then print an explanation on stderr, leave stdout
|
||
empty, and exit 0 — the same shape `extract-changelog-section.mjs --public`
|
||
already uses for its empty public body.
|
||
|
||
## Blog scaffold shape
|
||
|
||
`renderBlogScaffold` (`tools/release/release-notes-blog.mjs`) emits the shape
|
||
the published posts end up in, so the editor starts from the form rather than
|
||
from the inventory — the v0.23 post shipped as the raw type-grouped list with
|
||
area prefixes and had to be restructured after publication. In order:
|
||
narrative intro (TODO) → `ReleaseMeta` → `## What changed` (a `ChangeTable`
|
||
with one row per highlight; the theme is the default area label, the impact a
|
||
TODO) → the "About the screenshots" alert when any note names a screenshot →
|
||
one `##` section per highlight or screenshot note, with a `StatusPill`
|
||
matching the note type → `## Breaking changes` → the remaining features folded
|
||
into themed `##` sections → `## Performance` → `## Everything else`, holding
|
||
every remaining fix under a `Spoiler` grouped by theme → the before-updating
|
||
alert → `## Thanks` → `## Download` link cards (release tag, full notes, the
|
||
compare link when a previous version is known, all releases).
|
||
|
||
Themes come from `BLOG_THEMES`: a conventional-commit `area` says nothing to a
|
||
reader ("matching", "window-controls", "electron-backend"), so notes fold into
|
||
reader-facing headings ("Stalker portals", "Live TV, EPG and M3U"). An unmapped
|
||
area lands in "Other changes" rather than failing; add it to the map when it
|
||
recurs. Two defaults are deliberately dumb: every non-highlighted fix goes into
|
||
the spoiler, and every bullet keeps its full note body. Promoting the fixes
|
||
users will notice, compressing bullets to one line and writing the bold
|
||
lead-ins is the editorial pass, and the scaffold marks where with `TODO`. Only
|
||
the components the post actually uses are imported, so an MDX build never
|
||
fails on an unused import.
|
||
|
||
## Highlight cards
|
||
|
||
`tools/release/highlight-cards.mjs` plans and lays out;
|
||
`tools/release/generate-highlight-cards.mjs` renders through sharp. Output is 1200×630 (Open Graph), matching the website
|
||
palette in `apps/website/tailwind.config.mjs`.
|
||
|
||
- One card per highlight, plus a release hero card written as both `hero.png`
|
||
and the `hero.jpg` the blog scaffold's frontmatter references.
|
||
- A highlight naming a `screenshot:` gets a framed screenshot strip along the
|
||
bottom; one without gets a typographic layout instead. The frame is opaque
|
||
and painted after the text, so the body's line budget is **derived from the
|
||
space left above it**, never assumed — a fixed count sliced the last line in
|
||
half whenever the headline wrapped to two lines.
|
||
- Card filenames come from the note filename, never the screenshot slug: two
|
||
highlights may legitimately share one manifest shot, and naming cards after
|
||
it made the second overwrite the first.
|
||
|
||
Screenshots come only from the capture script running against the mock servers.
|
||
Never publish one taken from a real playlist or account — streams, logos and
|
||
metadata are copyrighted, and credentials must never reach a published image.
|
||
|
||
The same script also produces the evergreen screenshots of the website guides.
|
||
Manifest shots that carry `"group": "guides"` are skipped by a release run and
|
||
captured only with `pnpm release:screenshots --group guides`, which publishes
|
||
into `apps/website/public/blog/guides/screenshots/` instead of a release folder
|
||
(`outputDirectoryFor` in `screenshot-guards.mjs`). Guide shots go through every
|
||
guard a release shot does; the add-playlist dialog shots fill the form with the
|
||
mock's fictional `marketing` credentials and use a labeled hand-out for the
|
||
Auto-detect method rather than a `get.php?username=…` link, because G4 rejects
|
||
any URL carrying query credentials.
|
||
|
||
Output lands in `dist/release-highlight-cards/v<version>/`, outside version
|
||
control — keyed by the exact version, because 0.24.0 and 0.24.1 share a blog
|
||
post but not a card set. A run first removes the cards a previous run left in
|
||
that directory (only files matching what this tool writes), so a renamed or
|
||
dropped highlight cannot leave a stale image waiting to be published. Copying a
|
||
card into the website tree is a deliberate manual act.
|
||
|
||
A release with no `highlight:` notes is not an error: the hero card is still
|
||
rendered and the run exits 0. An internal-only release has nothing public to
|
||
put on a card and exits 0, first clearing any cards an earlier run of the same
|
||
version left behind. An **empty** `.changes/` directory is a different thing
|
||
and does fail: it almost always means this step ran after `--consume`, and
|
||
reporting that as "internal-only" would hide the one ordering mistake the
|
||
pipeline is built to prevent.
|
||
|
||
## Draft verification
|
||
|
||
The `v*` tag build creates a **draft** GitHub release.
|
||
`pnpm run release:verify:draft` (`tools/release/verify-draft-release.mjs`) is
|
||
the gate that runs before a human publishes it. It is strictly read-only: it
|
||
never publishes, edits or deletes.
|
||
|
||
1. **Find the run.** `gh run list` reports what is indexed *right now* — its
|
||
`--limit` caps how many runs come back, it does not wait for one to appear,
|
||
and a tag pushed seconds ago routinely is not indexed yet. The verifier
|
||
polls (10 attempts, 6 s apart) before concluding the tag was never pushed.
|
||
2. **Wait for it.** An in-progress run is streamed through
|
||
`gh run watch --exit-status`. A completed run with a non-success conclusion
|
||
fails immediately. A missing `gh` binary and an interrupted watch are
|
||
reported as themselves, not as a build failure — `spawnSync` surfaces both
|
||
as `status: null`.
|
||
3. **Check the release.** Draft status, the authored body, and the complete
|
||
asset set below.
|
||
|
||
The authored-body check compares the release body against the **local
|
||
`CHANGELOG.md` section**, not against emptiness. The tag workflow appends
|
||
GitHub's generated notes to the authored text (`FULL_BODY` in
|
||
`.github/workflows/build-and-make.yaml`), so the body is never empty and an emptiness test could
|
||
never fail. An internal-only release, whose public section is legitimately
|
||
empty, is reported as such rather than warned about.
|
||
|
||
An already-published release still gets its asset report — auditing one after
|
||
the fact is useful — but **never a success exit**. Reporting a pass for a
|
||
pre-publication gate after publication would claim a boundary already crossed.
|
||
|
||
### Required asset set
|
||
|
||
27 assets, verified against a real complete matrix build. When the build matrix
|
||
in `.github/workflows/build-and-make.yaml` gains or loses a target, update
|
||
`requiredAssetRules()` in the same PR.
|
||
|
||
| Platform | Assets |
|
||
| --- | --- |
|
||
| macOS | `-mac-{x64,arm64}.{dmg,zip}` + a `.blockmap` for each (8) |
|
||
| Windows | `-windows-x64-setup.exe` + `.blockmap` (2) |
|
||
| DEB | `-linux-{amd64,arm64,armv7l}.deb` (3) |
|
||
| AppImage | `-linux-{x86_64,arm64,armv7l}.AppImage` (3) |
|
||
| Snap | `-linux-{amd64,armhf}.snap` (2) |
|
||
| RPM | `-linux-x86_64.rpm` (1) |
|
||
| Flatpak | `-linux-x86_64.flatpak` (1) |
|
||
| Pacman | `-linux-x64.pacman` **or** `-linux-x86_64.pkg.tar.*` (1) |
|
||
| Updater metadata | `latest.yml`, `latest-mac.yml`, `latest-linux.yml`, `latest-linux-arm.yml`, `latest-linux-arm64.yml` (5) |
|
||
| Source compliance | `linux-frame-copy-runtime-sources.tar.xz` (1) |
|
||
|
||
Electron Builder has shipped both pacman artifact shapes, so either satisfies
|
||
that rule. Rules compare plain strings rather than a regex built from the
|
||
version — `requiredAssetRules()` is exported, and escaping an interpolated
|
||
value correctly would be a standing trap.
|
||
|
||
An asset no rule claims is reported as a `NOTE:` and does **not** fail the run:
|
||
a new build target should surface for a human to notice, not block a release
|
||
until the rules catch up.
|
||
|
||
## After verification
|
||
|
||
Publishing the GitHub release is manual. That publication automatically
|
||
verifies its Snap assets and uploads them to `edge`; installed-Snap smoke and
|
||
candidate/stable promotion remain manual (see
|
||
`tools/packaging/validate-snap-release-boundary.mjs`). Keep the blog post a
|
||
draft during artifact verification, then publish it in a follow-up commit and
|
||
verify the website deployment.
|
||
|
||
## Validation
|
||
|
||
```bash
|
||
pnpm run release:notes:validate # every note parses and satisfies the schema
|
||
pnpm nx run release-tools:test # the tooling's own unit tests
|
||
pnpm nx run release-tools:lint
|
||
```
|