ci(release): mechanize the releases-wall entry — never hand-written again
Every release used to get its releases/open-brainy.json entry typed by hand
after the fact. scripts/wall-entry.mjs derives it from the CHANGELOG entry
release.sh just composed (headline = first bullet, items = every bullet,
hash stripped) and prepends it, refusing by name on a duplicate version and
validating the whole file's shape + newest-first ordering before and after
it writes.
release.sh now runs it as its own step, between the CHANGELOG update and
the release commit, and stages releases/open-brainy.json into that commit.
The product engine's rail runs this identical script against its own
releases/brainy.json, unchanged — each repo's wall file lives beside the
CHANGELOG it derives from; there is no cross-repo step.
A --check mode validates a wall file's exact key set, field types, and
newest-first ordering with no duplicates, read-only. tests/unit/release/wall-entry.test.ts
covers derivation, prepend, duplicate refusal, and --check's shape/ordering
checks over temp copies — never the real files. --check also runs green
against both releases/open-brainy.json and releases/brainy.json as they
stand today.
2026-09-02 14:15:39 -07:00
#!/usr/bin/env node
/ * *
* @ module scripts / wall - entry
* @ description The releases - wall entry , made mechanical . The fleet ' s HQ page
2026-09-02 14:51:33 -07:00
* reads one public JSON per product from the ONE releases repo on The Source
* ( soulcraftlabs / releases , files < product > . json at its root — shape
* { product , entries : [ { version , date , headline , items , url , thumb ? } ] } ) , at
* https : //source.soulcraft.com/soulcraftlabs/releases/raw/branch/main/<product>.json.
* Those entries were hand - written after every release , then briefly written
* into this repo ' s own releases / < product > . json ; this script is the one door
* that composes an entry and lands it in the shared repo , so it is never
* hand - written and never forked across repos again .
ci(release): mechanize the releases-wall entry — never hand-written again
Every release used to get its releases/open-brainy.json entry typed by hand
after the fact. scripts/wall-entry.mjs derives it from the CHANGELOG entry
release.sh just composed (headline = first bullet, items = every bullet,
hash stripped) and prepends it, refusing by name on a duplicate version and
validating the whole file's shape + newest-first ordering before and after
it writes.
release.sh now runs it as its own step, between the CHANGELOG update and
the release commit, and stages releases/open-brainy.json into that commit.
The product engine's rail runs this identical script against its own
releases/brainy.json, unchanged — each repo's wall file lives beside the
CHANGELOG it derives from; there is no cross-repo step.
A --check mode validates a wall file's exact key set, field types, and
newest-first ordering with no duplicates, read-only. tests/unit/release/wall-entry.test.ts
covers derivation, prepend, duplicate refusal, and --check's shape/ordering
checks over temp copies — never the real files. --check also runs green
against both releases/open-brainy.json and releases/brainy.json as they
stand today.
2026-09-02 14:15:39 -07:00
*
* Two modes :
*
2026-09-02 14:51:33 -07:00
* 1. Generate + publish ( default ) :
ci(release): mechanize the releases-wall entry — never hand-written again
Every release used to get its releases/open-brainy.json entry typed by hand
after the fact. scripts/wall-entry.mjs derives it from the CHANGELOG entry
release.sh just composed (headline = first bullet, items = every bullet,
hash stripped) and prepends it, refusing by name on a duplicate version and
validating the whole file's shape + newest-first ordering before and after
it writes.
release.sh now runs it as its own step, between the CHANGELOG update and
the release commit, and stages releases/open-brainy.json into that commit.
The product engine's rail runs this identical script against its own
releases/brainy.json, unchanged — each repo's wall file lives beside the
CHANGELOG it derives from; there is no cross-repo step.
A --check mode validates a wall file's exact key set, field types, and
newest-first ordering with no duplicates, read-only. tests/unit/release/wall-entry.test.ts
covers derivation, prepend, duplicate refusal, and --check's shape/ordering
checks over temp copies — never the real files. --check also runs green
against both releases/open-brainy.json and releases/brainy.json as they
stand today.
2026-09-02 14:15:39 -07:00
* node wall - entry . mjs -- product < p > -- version < v > -- date < YYYY - MM - DD > \
2026-09-02 14:51:33 -07:00
* -- from - changelog < CHANGELOG . md >
ci(release): mechanize the releases-wall entry — never hand-written again
Every release used to get its releases/open-brainy.json entry typed by hand
after the fact. scripts/wall-entry.mjs derives it from the CHANGELOG entry
release.sh just composed (headline = first bullet, items = every bullet,
hash stripped) and prepends it, refusing by name on a duplicate version and
validating the whole file's shape + newest-first ordering before and after
it writes.
release.sh now runs it as its own step, between the CHANGELOG update and
the release commit, and stages releases/open-brainy.json into that commit.
The product engine's rail runs this identical script against its own
releases/brainy.json, unchanged — each repo's wall file lives beside the
CHANGELOG it derives from; there is no cross-repo step.
A --check mode validates a wall file's exact key set, field types, and
newest-first ordering with no duplicates, read-only. tests/unit/release/wall-entry.test.ts
covers derivation, prepend, duplicate refusal, and --check's shape/ordering
checks over temp copies — never the real files. --check also runs green
against both releases/open-brainy.json and releases/brainy.json as they
stand today.
2026-09-02 14:15:39 -07:00
* Derives an entry from the CHANGELOG . md entry for < v > ( headline = the
* entry ' s first bullet , items = every bullet , trimmed of its trailing
2026-09-02 14:51:33 -07:00
* commit hash ) , then :
* - clones ( or , if a cached clone already exists , fetches and resets )
* the releases repo into a local cache directory ,
* - prepends the entry to < cache > / < p > . j s o n , n e w e s t f i r s t — r e p l a c i n g
* any existing entry for the same version so a re - run is idempotent ,
* - validates the file ' s shape before and after ,
* - commits the change as "chore(wall): <p> <v>" and pushes main .
* A failure at any step ( clone , validation , commit , push , a
* non - fast - forward remote ) exits non - zero naming the cure . Nothing is
* ever skipped — the wall either lands correctly or the release fails .
ci(release): mechanize the releases-wall entry — never hand-written again
Every release used to get its releases/open-brainy.json entry typed by hand
after the fact. scripts/wall-entry.mjs derives it from the CHANGELOG entry
release.sh just composed (headline = first bullet, items = every bullet,
hash stripped) and prepends it, refusing by name on a duplicate version and
validating the whole file's shape + newest-first ordering before and after
it writes.
release.sh now runs it as its own step, between the CHANGELOG update and
the release commit, and stages releases/open-brainy.json into that commit.
The product engine's rail runs this identical script against its own
releases/brainy.json, unchanged — each repo's wall file lives beside the
CHANGELOG it derives from; there is no cross-repo step.
A --check mode validates a wall file's exact key set, field types, and
newest-first ordering with no duplicates, read-only. tests/unit/release/wall-entry.test.ts
covers derivation, prepend, duplicate refusal, and --check's shape/ordering
checks over temp copies — never the real files. --check also runs green
against both releases/open-brainy.json and releases/brainy.json as they
stand today.
2026-09-02 14:15:39 -07:00
*
2026-09-02 14:51:33 -07:00
* 2. Dry run :
* node wall - entry . mjs -- dry - run -- product < p > -- version < v > \
* -- date < YYYY - MM - DD > -- from - changelog < CHANGELOG . md >
* Derives the entry exactly as above and prints it , along with the file
* it would be written to , but touches no clone and no remote — usable
* from a fresh checkout with no cache and no network .
ci(release): mechanize the releases-wall entry — never hand-written again
Every release used to get its releases/open-brainy.json entry typed by hand
after the fact. scripts/wall-entry.mjs derives it from the CHANGELOG entry
release.sh just composed (headline = first bullet, items = every bullet,
hash stripped) and prepends it, refusing by name on a duplicate version and
validating the whole file's shape + newest-first ordering before and after
it writes.
release.sh now runs it as its own step, between the CHANGELOG update and
the release commit, and stages releases/open-brainy.json into that commit.
The product engine's rail runs this identical script against its own
releases/brainy.json, unchanged — each repo's wall file lives beside the
CHANGELOG it derives from; there is no cross-repo step.
A --check mode validates a wall file's exact key set, field types, and
newest-first ordering with no duplicates, read-only. tests/unit/release/wall-entry.test.ts
covers derivation, prepend, duplicate refusal, and --check's shape/ordering
checks over temp copies — never the real files. --check also runs green
against both releases/open-brainy.json and releases/brainy.json as they
stand today.
2026-09-02 14:15:39 -07:00
*
2026-09-02 14:51:33 -07:00
* 3. Validate only ( -- check ) :
* node wall - entry . mjs -- check -- file < path / to / product . json >
* Validates an arbitrary wall file ' s exact key set ( top - level and
* per - entry ) , field types , and strict - descending semver ordering with
* no duplicates . Read - only ; never writes . Exit 0 = clean , exit 1 =
* named violations printed to stderr .
*
* The remote and the local cache directory are each overridable
* ( -- remote / -- cache - dir , or WALL _ENTRY _RELEASES _REMOTE /
* WALL _ENTRY _RELEASES _CACHE _DIR ) so tests can point at a throwaway local
* bare repo and a throwaway cache directory — never the real remote or the
* real developer cache .
*
* No dependencies beyond the system ` git ` binary — CHANGELOG parsing ,
* semver comparison , and JSON shape checking are all hand - rolled below .
ci(release): mechanize the releases-wall entry — never hand-written again
Every release used to get its releases/open-brainy.json entry typed by hand
after the fact. scripts/wall-entry.mjs derives it from the CHANGELOG entry
release.sh just composed (headline = first bullet, items = every bullet,
hash stripped) and prepends it, refusing by name on a duplicate version and
validating the whole file's shape + newest-first ordering before and after
it writes.
release.sh now runs it as its own step, between the CHANGELOG update and
the release commit, and stages releases/open-brainy.json into that commit.
The product engine's rail runs this identical script against its own
releases/brainy.json, unchanged — each repo's wall file lives beside the
CHANGELOG it derives from; there is no cross-repo step.
A --check mode validates a wall file's exact key set, field types, and
newest-first ordering with no duplicates, read-only. tests/unit/release/wall-entry.test.ts
covers derivation, prepend, duplicate refusal, and --check's shape/ordering
checks over temp copies — never the real files. --check also runs green
against both releases/open-brainy.json and releases/brainy.json as they
stand today.
2026-09-02 14:15:39 -07:00
* /
2026-09-02 14:51:33 -07:00
import { readFileSync , writeFileSync , existsSync , mkdirSync } from 'node:fs'
import { execFileSync } from 'node:child_process'
import { homedir } from 'node:os'
import { dirname , join } from 'node:path'
ci(release): mechanize the releases-wall entry — never hand-written again
Every release used to get its releases/open-brainy.json entry typed by hand
after the fact. scripts/wall-entry.mjs derives it from the CHANGELOG entry
release.sh just composed (headline = first bullet, items = every bullet,
hash stripped) and prepends it, refusing by name on a duplicate version and
validating the whole file's shape + newest-first ordering before and after
it writes.
release.sh now runs it as its own step, between the CHANGELOG update and
the release commit, and stages releases/open-brainy.json into that commit.
The product engine's rail runs this identical script against its own
releases/brainy.json, unchanged — each repo's wall file lives beside the
CHANGELOG it derives from; there is no cross-repo step.
A --check mode validates a wall file's exact key set, field types, and
newest-first ordering with no duplicates, read-only. tests/unit/release/wall-entry.test.ts
covers derivation, prepend, duplicate refusal, and --check's shape/ordering
checks over temp copies — never the real files. --check also runs green
against both releases/open-brainy.json and releases/brainy.json as they
stand today.
2026-09-02 14:15:39 -07:00
2026-09-02 14:51:33 -07:00
const DEFAULT _REMOTE = 'git@source.soulcraft.com:soulcraftlabs/releases.git'
/** @returns {string} */
function defaultCacheDir ( ) {
const base = process . env . XDG _CACHE _HOME || join ( homedir ( ) , '.cache' )
return join ( base , 'soulcraft-releases' )
}
// Required on every entry; "thumb" is optional (may be absent, or present as
// string | null) — matching the HQ contract's {..., thumb?}.
const ENTRY _REQUIRED _KEYS = [ 'version' , 'date' , 'headline' , 'items' , 'url' ]
const ENTRY _OPTIONAL _KEYS = [ 'thumb' ]
const ENTRY _ALLOWED _KEYS = [ ... ENTRY _REQUIRED _KEYS , ... ENTRY _OPTIONAL _KEYS ]
const FILE _KEYS = [ 'product' , 'entries' ]
ci(release): mechanize the releases-wall entry — never hand-written again
Every release used to get its releases/open-brainy.json entry typed by hand
after the fact. scripts/wall-entry.mjs derives it from the CHANGELOG entry
release.sh just composed (headline = first bullet, items = every bullet,
hash stripped) and prepends it, refusing by name on a duplicate version and
validating the whole file's shape + newest-first ordering before and after
it writes.
release.sh now runs it as its own step, between the CHANGELOG update and
the release commit, and stages releases/open-brainy.json into that commit.
The product engine's rail runs this identical script against its own
releases/brainy.json, unchanged — each repo's wall file lives beside the
CHANGELOG it derives from; there is no cross-repo step.
A --check mode validates a wall file's exact key set, field types, and
newest-first ordering with no duplicates, read-only. tests/unit/release/wall-entry.test.ts
covers derivation, prepend, duplicate refusal, and --check's shape/ordering
checks over temp copies — never the real files. --check also runs green
against both releases/open-brainy.json and releases/brainy.json as they
stand today.
2026-09-02 14:15:39 -07:00
2026-09-02 14:59:49 -07:00
// The public permalink pattern, by product. Every entry MUST carry an https
// permalink: HQ's parser rejects a wall whose entries carry url: null (the
// whole feed became unreadable on 2026-09-02). A product whose forge repo is
// private links its PUBLIC package page on The Source instead of a release
// page that would 404 for HQ's readers.
ci(release): mechanize the releases-wall entry — never hand-written again
Every release used to get its releases/open-brainy.json entry typed by hand
after the fact. scripts/wall-entry.mjs derives it from the CHANGELOG entry
release.sh just composed (headline = first bullet, items = every bullet,
hash stripped) and prepends it, refusing by name on a duplicate version and
validating the whole file's shape + newest-first ordering before and after
it writes.
release.sh now runs it as its own step, between the CHANGELOG update and
the release commit, and stages releases/open-brainy.json into that commit.
The product engine's rail runs this identical script against its own
releases/brainy.json, unchanged — each repo's wall file lives beside the
CHANGELOG it derives from; there is no cross-repo step.
A --check mode validates a wall file's exact key set, field types, and
newest-first ordering with no duplicates, read-only. tests/unit/release/wall-entry.test.ts
covers derivation, prepend, duplicate refusal, and --check's shape/ordering
checks over temp copies — never the real files. --check also runs green
against both releases/open-brainy.json and releases/brainy.json as they
stand today.
2026-09-02 14:15:39 -07:00
const RELEASE _URL _PATTERNS = {
'open-brainy' : ( version ) => ` https://source.soulcraft.com/soulcraftlabs/open-brainy/releases/tag/v ${ version } ` ,
2026-09-02 14:59:49 -07:00
'brainy' : ( version ) => ` https://source.soulcraft.com/soulcraft/-/packages/npm/@soulcraft%2Fbrainy/ ${ version } ` ,
ci(release): mechanize the releases-wall entry — never hand-written again
Every release used to get its releases/open-brainy.json entry typed by hand
after the fact. scripts/wall-entry.mjs derives it from the CHANGELOG entry
release.sh just composed (headline = first bullet, items = every bullet,
hash stripped) and prepends it, refusing by name on a duplicate version and
validating the whole file's shape + newest-first ordering before and after
it writes.
release.sh now runs it as its own step, between the CHANGELOG update and
the release commit, and stages releases/open-brainy.json into that commit.
The product engine's rail runs this identical script against its own
releases/brainy.json, unchanged — each repo's wall file lives beside the
CHANGELOG it derives from; there is no cross-repo step.
A --check mode validates a wall file's exact key set, field types, and
newest-first ordering with no duplicates, read-only. tests/unit/release/wall-entry.test.ts
covers derivation, prepend, duplicate refusal, and --check's shape/ordering
checks over temp copies — never the real files. --check also runs green
against both releases/open-brainy.json and releases/brainy.json as they
stand today.
2026-09-02 14:15:39 -07:00
}
/ * *
* Parse argv into a flag map . ` --flag value ` sets a string ; ` --flag ` alone
* ( end of argv , or followed by another ` --flag ` ) sets boolean true .
* @ param { string [ ] } argv
* @ returns { Record < string , string | true > }
* /
function parseArgs ( argv ) {
/** @type {Record<string, string | true>} */
const args = { }
for ( let i = 0 ; i < argv . length ; i ++ ) {
const a = argv [ i ]
if ( ! a . startsWith ( '--' ) ) continue
const key = a . slice ( 2 )
const next = argv [ i + 1 ]
if ( next === undefined || next . startsWith ( '--' ) ) {
args [ key ] = true
} else {
args [ key ] = next
i ++
}
}
return args
}
/ * *
* Print a loud , named error and exit 1. Every refusal in this script goes
* through here so the failure mode is always the same shape : "wall-entry: <what>" .
* @ param { string } message
* @ returns { never }
* /
function fail ( message ) {
console . error ( ` wall-entry: ${ message } ` )
process . exit ( 1 )
}
/ * *
* @ param { string } version
* @ returns { { major : number , minor : number , patch : number , pre : string | null } | null }
* /
function parseSemver ( version ) {
const m = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/ . exec ( version )
if ( ! m ) return null
return { major : Number ( m [ 1 ] ) , minor : Number ( m [ 2 ] ) , patch : Number ( m [ 3 ] ) , pre : m [ 4 ] ? ? null }
}
/ * *
* @ param { string } a
* @ param { string } b
* @ returns { number } positive if a > b , negative if a < b , 0 if equal .
* /
function compareSemver ( a , b ) {
const pa = parseSemver ( a )
const pb = parseSemver ( b )
if ( ! pa || ! pb ) throw new Error ( ` cannot compare non-semver versions " ${ a } " vs " ${ b } " ` )
if ( pa . major !== pb . major ) return pa . major - pb . major
if ( pa . minor !== pb . minor ) return pa . minor - pb . minor
if ( pa . patch !== pb . patch ) return pa . patch - pb . patch
if ( pa . pre === pb . pre ) return 0
if ( pa . pre === null ) return 1 // a release outranks any prerelease of the same core version
if ( pb . pre === null ) return - 1
return pa . pre < pb . pre ? - 1 : pa . pre > pb . pre ? 1 : 0
}
/ * *
2026-09-02 14:51:33 -07:00
* Validate a wall file ' s full shape : top - level keys ( "product" , "entries" —
* no more , no less ) , per - entry keys and field types ( "thumb" optional ) , and
* strict - descending semver ordering with no duplicates . Collects every
* violation instead of failing on the first , so a caller reports the whole
* picture in one pass .
ci(release): mechanize the releases-wall entry — never hand-written again
Every release used to get its releases/open-brainy.json entry typed by hand
after the fact. scripts/wall-entry.mjs derives it from the CHANGELOG entry
release.sh just composed (headline = first bullet, items = every bullet,
hash stripped) and prepends it, refusing by name on a duplicate version and
validating the whole file's shape + newest-first ordering before and after
it writes.
release.sh now runs it as its own step, between the CHANGELOG update and
the release commit, and stages releases/open-brainy.json into that commit.
The product engine's rail runs this identical script against its own
releases/brainy.json, unchanged — each repo's wall file lives beside the
CHANGELOG it derives from; there is no cross-repo step.
A --check mode validates a wall file's exact key set, field types, and
newest-first ordering with no duplicates, read-only. tests/unit/release/wall-entry.test.ts
covers derivation, prepend, duplicate refusal, and --check's shape/ordering
checks over temp copies — never the real files. --check also runs green
against both releases/open-brainy.json and releases/brainy.json as they
stand today.
2026-09-02 14:15:39 -07:00
* @ param { unknown } data
* @ returns { string [ ] } Violation messages ; empty means the file is clean .
* /
function validateShape ( data ) {
/** @type {string[]} */
const errors = [ ]
if ( typeof data !== 'object' || data === null || Array . isArray ( data ) ) {
return [ 'top level: expected a JSON object' ]
}
const obj = /** @type {Record<string, unknown>} */ ( data )
const topKeys = Object . keys ( obj )
const missingTop = FILE _KEYS . filter ( ( k ) => ! ( k in obj ) )
const extraTop = topKeys . filter ( ( k ) => ! FILE _KEYS . includes ( k ) )
if ( missingTop . length ) errors . push ( ` top level: missing key(s) ${ missingTop . join ( ', ' ) } ` )
if ( extraTop . length ) errors . push ( ` top level: unexpected key(s) ${ extraTop . join ( ', ' ) } ` )
if ( typeof obj . product !== 'string' || obj . product . trim ( ) === '' ) {
errors . push ( 'top level: "product" must be a non-empty string' )
}
if ( ! Array . isArray ( obj . entries ) ) {
errors . push ( 'top level: "entries" must be an array' )
return errors // nothing further to check without an array
}
const entries = /** @type {unknown[]} */ ( obj . entries )
entries . forEach ( ( rawEntry , i ) => {
const label = ` entries[ ${ i } ] `
if ( typeof rawEntry !== 'object' || rawEntry === null || Array . isArray ( rawEntry ) ) {
errors . push ( ` ${ label } : expected an object ` )
return
}
const entry = /** @type {Record<string, unknown>} */ ( rawEntry )
const keys = Object . keys ( entry )
2026-09-02 14:51:33 -07:00
const missing = ENTRY _REQUIRED _KEYS . filter ( ( k ) => ! ( k in entry ) )
const extra = keys . filter ( ( k ) => ! ENTRY _ALLOWED _KEYS . includes ( k ) )
ci(release): mechanize the releases-wall entry — never hand-written again
Every release used to get its releases/open-brainy.json entry typed by hand
after the fact. scripts/wall-entry.mjs derives it from the CHANGELOG entry
release.sh just composed (headline = first bullet, items = every bullet,
hash stripped) and prepends it, refusing by name on a duplicate version and
validating the whole file's shape + newest-first ordering before and after
it writes.
release.sh now runs it as its own step, between the CHANGELOG update and
the release commit, and stages releases/open-brainy.json into that commit.
The product engine's rail runs this identical script against its own
releases/brainy.json, unchanged — each repo's wall file lives beside the
CHANGELOG it derives from; there is no cross-repo step.
A --check mode validates a wall file's exact key set, field types, and
newest-first ordering with no duplicates, read-only. tests/unit/release/wall-entry.test.ts
covers derivation, prepend, duplicate refusal, and --check's shape/ordering
checks over temp copies — never the real files. --check also runs green
against both releases/open-brainy.json and releases/brainy.json as they
stand today.
2026-09-02 14:15:39 -07:00
if ( missing . length ) errors . push ( ` ${ label } : missing key(s) ${ missing . join ( ', ' ) } ` )
if ( extra . length ) errors . push ( ` ${ label } : unexpected key(s) ${ extra . join ( ', ' ) } ` )
if ( typeof entry . version !== 'string' || ! parseSemver ( entry . version ) ) {
errors . push ( ` ${ label } : "version" must be a semver string (got ${ JSON . stringify ( entry . version ) } ) ` )
}
if ( typeof entry . date !== 'string' || ! /^\d{4}-\d{2}-\d{2}$/ . test ( entry . date ) || Number . isNaN ( Date . parse ( entry . date ) ) ) {
errors . push ( ` ${ label } : "date" must be a YYYY-MM-DD string (got ${ JSON . stringify ( entry . date ) } ) ` )
}
if ( typeof entry . headline !== 'string' || entry . headline . trim ( ) === '' ) {
errors . push ( ` ${ label } : "headline" must be a non-empty string ` )
}
if ( ! Array . isArray ( entry . items ) || entry . items . length === 0 || entry . items . some ( ( it ) => typeof it !== 'string' || it . trim ( ) === '' ) ) {
errors . push ( ` ${ label } : "items" must be a non-empty array of non-empty strings ` )
}
2026-09-02 14:59:49 -07:00
if ( typeof entry . url !== 'string' || ! /^https:\/\/\S+$/ . test ( entry . url ) ) {
errors . push ( ` ${ label } : "url" must be an https permalink — never null; HQ's parser rejects the whole feed ` )
ci(release): mechanize the releases-wall entry — never hand-written again
Every release used to get its releases/open-brainy.json entry typed by hand
after the fact. scripts/wall-entry.mjs derives it from the CHANGELOG entry
release.sh just composed (headline = first bullet, items = every bullet,
hash stripped) and prepends it, refusing by name on a duplicate version and
validating the whole file's shape + newest-first ordering before and after
it writes.
release.sh now runs it as its own step, between the CHANGELOG update and
the release commit, and stages releases/open-brainy.json into that commit.
The product engine's rail runs this identical script against its own
releases/brainy.json, unchanged — each repo's wall file lives beside the
CHANGELOG it derives from; there is no cross-repo step.
A --check mode validates a wall file's exact key set, field types, and
newest-first ordering with no duplicates, read-only. tests/unit/release/wall-entry.test.ts
covers derivation, prepend, duplicate refusal, and --check's shape/ordering
checks over temp copies — never the real files. --check also runs green
against both releases/open-brainy.json and releases/brainy.json as they
stand today.
2026-09-02 14:15:39 -07:00
}
2026-09-02 14:51:33 -07:00
if ( 'thumb' in entry && ! ( entry . thumb === null || typeof entry . thumb === 'string' ) ) {
errors . push ( ` ${ label } : "thumb" must be a string or null when present ` )
ci(release): mechanize the releases-wall entry — never hand-written again
Every release used to get its releases/open-brainy.json entry typed by hand
after the fact. scripts/wall-entry.mjs derives it from the CHANGELOG entry
release.sh just composed (headline = first bullet, items = every bullet,
hash stripped) and prepends it, refusing by name on a duplicate version and
validating the whole file's shape + newest-first ordering before and after
it writes.
release.sh now runs it as its own step, between the CHANGELOG update and
the release commit, and stages releases/open-brainy.json into that commit.
The product engine's rail runs this identical script against its own
releases/brainy.json, unchanged — each repo's wall file lives beside the
CHANGELOG it derives from; there is no cross-repo step.
A --check mode validates a wall file's exact key set, field types, and
newest-first ordering with no duplicates, read-only. tests/unit/release/wall-entry.test.ts
covers derivation, prepend, duplicate refusal, and --check's shape/ordering
checks over temp copies — never the real files. --check also runs green
against both releases/open-brainy.json and releases/brainy.json as they
stand today.
2026-09-02 14:15:39 -07:00
}
} )
// Ordering: newest first, strictly descending, no duplicate versions —
// checked only over entries whose version parsed (a bad version is
// already reported above; comparing it too would just be noise).
const versioned = entries
. map ( ( e , i ) => ( { i , version : /** @type {any} */ ( e ) ? . version } ) )
. filter ( ( e ) => typeof e . version === 'string' && parseSemver ( e . version ) )
for ( let i = 0 ; i < versioned . length - 1 ; i ++ ) {
const a = versioned [ i ]
const b = versioned [ i + 1 ]
const cmp = compareSemver ( a . version , b . version )
if ( cmp === 0 ) {
errors . push ( ` entries[ ${ a . i } ] and entries[ ${ b . i } ]: duplicate version ${ a . version } ` )
} else if ( cmp < 0 ) {
errors . push ( ` entries[ ${ a . i } ] ( ${ a . version } ) sits above entries[ ${ b . i } ] ( ${ b . version } ) — not newest-first ` )
}
}
return errors
}
/ * *
* Extract one version ' s entry body from a standard - version - style CHANGELOG . md
* ( headings ` ### [version](url) (date) ` , followed by ` - bullet (hash) ` lines
* until the next heading or EOF ) .
* @ param { string } changelog
* @ param { string } version
* @ returns { string [ ] } Bullet lines , trimmed of their leading "- " and
* trailing " (hash)" .
* /
function extractChangelogBullets ( changelog , version ) {
const lines = changelog . split ( '\n' )
const headingRe = /^### \[([^\]]+)\]\(.*\)\s*\(\d{4}-\d{2}-\d{2}\)\s*$/
let start = - 1
for ( let i = 0 ; i < lines . length ; i ++ ) {
const m = headingRe . exec ( lines [ i ] )
if ( m && m [ 1 ] === version ) {
start = i + 1
break
}
}
if ( start === - 1 ) {
fail (
` version ${ version } has no CHANGELOG entry yet — run this after the CHANGELOG step composes "### [ ${ version } ]", not before ` ,
)
}
/** @type {string[]} */
const bullets = [ ]
for ( let i = start ; i < lines . length ; i ++ ) {
if ( headingRe . test ( lines [ i ] ) ) break // next entry starts
const bulletMatch = /^- (.+?)(?:\s\(([0-9a-f]{6,40})\))?$/ . exec ( lines [ i ] . trim ( ) )
if ( lines [ i ] . trim ( ) . startsWith ( '- ' ) && bulletMatch ) {
const text = bulletMatch [ 1 ] . trim ( )
if ( text ) bullets . push ( text )
}
}
if ( bullets . length === 0 ) {
fail ( ` version ${ version } 's CHANGELOG entry has no bullets to derive a headline/items from ` )
}
return bullets
}
/ * *
* Derive a wall entry from a CHANGELOG . md .
2026-09-02 14:59:49 -07:00
* @ param { { product : string , version : string , date : string , changelogPath : string , url ? : string , thumb ? : string | null } } opts
* @ returns { { version : string , date : string , headline : string , items : string [ ] , url : string , thumb : string | null } }
ci(release): mechanize the releases-wall entry — never hand-written again
Every release used to get its releases/open-brainy.json entry typed by hand
after the fact. scripts/wall-entry.mjs derives it from the CHANGELOG entry
release.sh just composed (headline = first bullet, items = every bullet,
hash stripped) and prepends it, refusing by name on a duplicate version and
validating the whole file's shape + newest-first ordering before and after
it writes.
release.sh now runs it as its own step, between the CHANGELOG update and
the release commit, and stages releases/open-brainy.json into that commit.
The product engine's rail runs this identical script against its own
releases/brainy.json, unchanged — each repo's wall file lives beside the
CHANGELOG it derives from; there is no cross-repo step.
A --check mode validates a wall file's exact key set, field types, and
newest-first ordering with no duplicates, read-only. tests/unit/release/wall-entry.test.ts
covers derivation, prepend, duplicate refusal, and --check's shape/ordering
checks over temp copies — never the real files. --check also runs green
against both releases/open-brainy.json and releases/brainy.json as they
stand today.
2026-09-02 14:15:39 -07:00
* /
function deriveEntry ( { product , version , date , changelogPath , url , thumb } ) {
if ( ! parseSemver ( version ) ) fail ( ` --version " ${ version } " is not a semver string ` )
if ( ! /^\d{4}-\d{2}-\d{2}$/ . test ( date ) || Number . isNaN ( Date . parse ( date ) ) ) {
fail ( ` --date " ${ date } " is not a YYYY-MM-DD date ` )
}
if ( ! existsSync ( changelogPath ) ) fail ( ` --from-changelog " ${ changelogPath } " does not exist ` )
const changelog = readFileSync ( changelogPath , 'utf8' )
const items = extractChangelogBullets ( changelog , version )
const headline = items [ 0 ]
2026-09-02 14:59:49 -07:00
const pattern = RELEASE _URL _PATTERNS [ product ]
if ( url === undefined && pattern === undefined ) {
throw new Error ( ` wall-entry: no permalink pattern for product " ${ product } " — add one to RELEASE_URL_PATTERNS or pass --url; entries never carry url: null ` )
}
const resolvedUrl = url !== undefined ? url : pattern ( version )
ci(release): mechanize the releases-wall entry — never hand-written again
Every release used to get its releases/open-brainy.json entry typed by hand
after the fact. scripts/wall-entry.mjs derives it from the CHANGELOG entry
release.sh just composed (headline = first bullet, items = every bullet,
hash stripped) and prepends it, refusing by name on a duplicate version and
validating the whole file's shape + newest-first ordering before and after
it writes.
release.sh now runs it as its own step, between the CHANGELOG update and
the release commit, and stages releases/open-brainy.json into that commit.
The product engine's rail runs this identical script against its own
releases/brainy.json, unchanged — each repo's wall file lives beside the
CHANGELOG it derives from; there is no cross-repo step.
A --check mode validates a wall file's exact key set, field types, and
newest-first ordering with no duplicates, read-only. tests/unit/release/wall-entry.test.ts
covers derivation, prepend, duplicate refusal, and --check's shape/ordering
checks over temp copies — never the real files. --check also runs green
against both releases/open-brainy.json and releases/brainy.json as they
stand today.
2026-09-02 14:15:39 -07:00
const resolvedThumb = thumb !== undefined ? thumb : null
return { version , date , headline , items , url : resolvedUrl , thumb : resolvedThumb }
}
/ * *
* Load and shape - validate a wall file .
* @ param { string } filePath
* @ returns { Record < string , any > }
* /
function loadWallFile ( filePath ) {
2026-09-02 14:51:33 -07:00
if ( ! existsSync ( filePath ) ) fail ( ` " ${ filePath } " does not exist ` )
ci(release): mechanize the releases-wall entry — never hand-written again
Every release used to get its releases/open-brainy.json entry typed by hand
after the fact. scripts/wall-entry.mjs derives it from the CHANGELOG entry
release.sh just composed (headline = first bullet, items = every bullet,
hash stripped) and prepends it, refusing by name on a duplicate version and
validating the whole file's shape + newest-first ordering before and after
it writes.
release.sh now runs it as its own step, between the CHANGELOG update and
the release commit, and stages releases/open-brainy.json into that commit.
The product engine's rail runs this identical script against its own
releases/brainy.json, unchanged — each repo's wall file lives beside the
CHANGELOG it derives from; there is no cross-repo step.
A --check mode validates a wall file's exact key set, field types, and
newest-first ordering with no duplicates, read-only. tests/unit/release/wall-entry.test.ts
covers derivation, prepend, duplicate refusal, and --check's shape/ordering
checks over temp copies — never the real files. --check also runs green
against both releases/open-brainy.json and releases/brainy.json as they
stand today.
2026-09-02 14:15:39 -07:00
/** @type {unknown} */
let data
try {
data = JSON . parse ( readFileSync ( filePath , 'utf8' ) )
} catch ( err ) {
2026-09-02 14:51:33 -07:00
fail ( ` " ${ filePath } " is not valid JSON: ${ /** @type {Error} */ ( err ) . message } ` )
ci(release): mechanize the releases-wall entry — never hand-written again
Every release used to get its releases/open-brainy.json entry typed by hand
after the fact. scripts/wall-entry.mjs derives it from the CHANGELOG entry
release.sh just composed (headline = first bullet, items = every bullet,
hash stripped) and prepends it, refusing by name on a duplicate version and
validating the whole file's shape + newest-first ordering before and after
it writes.
release.sh now runs it as its own step, between the CHANGELOG update and
the release commit, and stages releases/open-brainy.json into that commit.
The product engine's rail runs this identical script against its own
releases/brainy.json, unchanged — each repo's wall file lives beside the
CHANGELOG it derives from; there is no cross-repo step.
A --check mode validates a wall file's exact key set, field types, and
newest-first ordering with no duplicates, read-only. tests/unit/release/wall-entry.test.ts
covers derivation, prepend, duplicate refusal, and --check's shape/ordering
checks over temp copies — never the real files. --check also runs green
against both releases/open-brainy.json and releases/brainy.json as they
stand today.
2026-09-02 14:15:39 -07:00
}
const errors = validateShape ( data )
if ( errors . length ) {
2026-09-02 14:51:33 -07:00
fail ( ` " ${ filePath } " fails shape validation — \n ${ errors . join ( '\n ' ) } ` )
ci(release): mechanize the releases-wall entry — never hand-written again
Every release used to get its releases/open-brainy.json entry typed by hand
after the fact. scripts/wall-entry.mjs derives it from the CHANGELOG entry
release.sh just composed (headline = first bullet, items = every bullet,
hash stripped) and prepends it, refusing by name on a duplicate version and
validating the whole file's shape + newest-first ordering before and after
it writes.
release.sh now runs it as its own step, between the CHANGELOG update and
the release commit, and stages releases/open-brainy.json into that commit.
The product engine's rail runs this identical script against its own
releases/brainy.json, unchanged — each repo's wall file lives beside the
CHANGELOG it derives from; there is no cross-repo step.
A --check mode validates a wall file's exact key set, field types, and
newest-first ordering with no duplicates, read-only. tests/unit/release/wall-entry.test.ts
covers derivation, prepend, duplicate refusal, and --check's shape/ordering
checks over temp copies — never the real files. --check also runs green
against both releases/open-brainy.json and releases/brainy.json as they
stand today.
2026-09-02 14:15:39 -07:00
}
return /** @type {Record<string, any>} */ ( data )
}
/ * *
2026-09-02 14:51:33 -07:00
* Run a git command , throwing an Error whose message is git ' s own stderr
* ( trimmed ) on failure — every caller wraps this to name the cure .
* @ param { string [ ] } args
* @ param { string } cwd
* @ returns { string } stdout , trimmed .
* /
function git ( args , cwd ) {
try {
return execFileSync ( 'git' , args , { cwd , encoding : 'utf8' , stdio : [ 'ignore' , 'pipe' , 'pipe' ] } ) . trim ( )
} catch ( err ) {
const stderr = /** @type {any} */ ( err ) . stderr
const message = ( typeof stderr === 'string' && stderr . trim ( ) ) || /** @type {Error} */ ( err ) . message
throw new Error ( message )
}
}
/ * *
* Ensure a clean , up - to - date local clone of the releases repo at
* ` cacheDir ` , checked out on ` main ` — cloning fresh if ` cacheDir ` has no
* ` .git ` , otherwise fetching and hard - resetting onto ` origin/main ` ( so a
* stray local commit or edit left by a previous failed run can never leak
* into the next one ) .
* @ param { string } remote
* @ param { string } cacheDir
* /
function ensureReleasesClone ( remote , cacheDir ) {
if ( existsSync ( join ( cacheDir , '.git' ) ) ) {
try {
git ( [ 'remote' , 'set-url' , 'origin' , remote ] , cacheDir )
git ( [ 'fetch' , '--prune' , 'origin' ] , cacheDir )
git ( [ 'checkout' , 'main' ] , cacheDir )
git ( [ 'reset' , '--hard' , 'origin/main' ] , cacheDir )
git ( [ 'clean' , '-fd' ] , cacheDir )
} catch ( err ) {
fail (
` cannot refresh the cached releases checkout at " ${ cacheDir } " from " ${ remote } " — ${ /** @type {Error} */ ( err ) . message } \n ` +
` cure: delete " ${ cacheDir } " and re-run so it re-clones from scratch, or confirm SSH access with "ssh -T git@source.soulcraft.com" ` ,
)
}
return
}
mkdirSync ( dirname ( cacheDir ) , { recursive : true } )
try {
git ( [ 'clone' , remote , cacheDir ] , dirname ( cacheDir ) )
} catch ( err ) {
fail (
` cannot clone " ${ remote } " — ${ /** @type {Error} */ ( err ) . message } \n ` +
` cure: confirm SSH access with "ssh -T git@source.soulcraft.com" and that the soulcraftlabs/releases repo exists yet ` ,
)
}
try {
git ( [ 'checkout' , 'main' ] , cacheDir )
} catch ( err ) {
fail (
` cloned " ${ remote } " into " ${ cacheDir } " but could not check out "main" — ${ /** @type {Error} */ ( err ) . message } \n ` +
` cure: confirm the releases repo's default branch is named "main" ` ,
)
}
}
/ * *
* Prepend ` entry ` to the wall at ` <cacheDir>/<product>.json ` , replacing any
* existing entry for the same version ( idempotent re - runs ) , validating
* before and after , committing , and pushing — or refusing loudly , naming
* the cure , at whichever step fails .
2026-09-02 14:59:49 -07:00
* @ param { { version : string , date : string , headline : string , items : string [ ] , url : string , thumb : string | null } } entry
2026-09-02 14:51:33 -07:00
* @ param { string } product
* @ param { string } remote
* @ param { string } cacheDir
ci(release): mechanize the releases-wall entry — never hand-written again
Every release used to get its releases/open-brainy.json entry typed by hand
after the fact. scripts/wall-entry.mjs derives it from the CHANGELOG entry
release.sh just composed (headline = first bullet, items = every bullet,
hash stripped) and prepends it, refusing by name on a duplicate version and
validating the whole file's shape + newest-first ordering before and after
it writes.
release.sh now runs it as its own step, between the CHANGELOG update and
the release commit, and stages releases/open-brainy.json into that commit.
The product engine's rail runs this identical script against its own
releases/brainy.json, unchanged — each repo's wall file lives beside the
CHANGELOG it derives from; there is no cross-repo step.
A --check mode validates a wall file's exact key set, field types, and
newest-first ordering with no duplicates, read-only. tests/unit/release/wall-entry.test.ts
covers derivation, prepend, duplicate refusal, and --check's shape/ordering
checks over temp copies — never the real files. --check also runs green
against both releases/open-brainy.json and releases/brainy.json as they
stand today.
2026-09-02 14:15:39 -07:00
* /
2026-09-02 14:51:33 -07:00
function publishEntry ( entry , product , remote , cacheDir ) {
ensureReleasesClone ( remote , cacheDir )
ci(release): mechanize the releases-wall entry — never hand-written again
Every release used to get its releases/open-brainy.json entry typed by hand
after the fact. scripts/wall-entry.mjs derives it from the CHANGELOG entry
release.sh just composed (headline = first bullet, items = every bullet,
hash stripped) and prepends it, refusing by name on a duplicate version and
validating the whole file's shape + newest-first ordering before and after
it writes.
release.sh now runs it as its own step, between the CHANGELOG update and
the release commit, and stages releases/open-brainy.json into that commit.
The product engine's rail runs this identical script against its own
releases/brainy.json, unchanged — each repo's wall file lives beside the
CHANGELOG it derives from; there is no cross-repo step.
A --check mode validates a wall file's exact key set, field types, and
newest-first ordering with no duplicates, read-only. tests/unit/release/wall-entry.test.ts
covers derivation, prepend, duplicate refusal, and --check's shape/ordering
checks over temp copies — never the real files. --check also runs green
against both releases/open-brainy.json and releases/brainy.json as they
stand today.
2026-09-02 14:15:39 -07:00
2026-09-02 14:51:33 -07:00
const filePath = join ( cacheDir , ` ${ product } .json ` )
if ( ! existsSync ( filePath ) ) {
ci(release): mechanize the releases-wall entry — never hand-written again
Every release used to get its releases/open-brainy.json entry typed by hand
after the fact. scripts/wall-entry.mjs derives it from the CHANGELOG entry
release.sh just composed (headline = first bullet, items = every bullet,
hash stripped) and prepends it, refusing by name on a duplicate version and
validating the whole file's shape + newest-first ordering before and after
it writes.
release.sh now runs it as its own step, between the CHANGELOG update and
the release commit, and stages releases/open-brainy.json into that commit.
The product engine's rail runs this identical script against its own
releases/brainy.json, unchanged — each repo's wall file lives beside the
CHANGELOG it derives from; there is no cross-repo step.
A --check mode validates a wall file's exact key set, field types, and
newest-first ordering with no duplicates, read-only. tests/unit/release/wall-entry.test.ts
covers derivation, prepend, duplicate refusal, and --check's shape/ordering
checks over temp copies — never the real files. --check also runs green
against both releases/open-brainy.json and releases/brainy.json as they
stand today.
2026-09-02 14:15:39 -07:00
fail (
2026-09-02 14:51:33 -07:00
` " ${ filePath } " does not exist in the releases repo — cure: seed " ${ product } .json" at the repo root first (it must exist before any release rail can prepend to it) ` ,
ci(release): mechanize the releases-wall entry — never hand-written again
Every release used to get its releases/open-brainy.json entry typed by hand
after the fact. scripts/wall-entry.mjs derives it from the CHANGELOG entry
release.sh just composed (headline = first bullet, items = every bullet,
hash stripped) and prepends it, refusing by name on a duplicate version and
validating the whole file's shape + newest-first ordering before and after
it writes.
release.sh now runs it as its own step, between the CHANGELOG update and
the release commit, and stages releases/open-brainy.json into that commit.
The product engine's rail runs this identical script against its own
releases/brainy.json, unchanged — each repo's wall file lives beside the
CHANGELOG it derives from; there is no cross-repo step.
A --check mode validates a wall file's exact key set, field types, and
newest-first ordering with no duplicates, read-only. tests/unit/release/wall-entry.test.ts
covers derivation, prepend, duplicate refusal, and --check's shape/ordering
checks over temp copies — never the real files. --check also runs green
against both releases/open-brainy.json and releases/brainy.json as they
stand today.
2026-09-02 14:15:39 -07:00
)
}
2026-09-02 14:51:33 -07:00
const wall = loadWallFile ( filePath )
ci(release): mechanize the releases-wall entry — never hand-written again
Every release used to get its releases/open-brainy.json entry typed by hand
after the fact. scripts/wall-entry.mjs derives it from the CHANGELOG entry
release.sh just composed (headline = first bullet, items = every bullet,
hash stripped) and prepends it, refusing by name on a duplicate version and
validating the whole file's shape + newest-first ordering before and after
it writes.
release.sh now runs it as its own step, between the CHANGELOG update and
the release commit, and stages releases/open-brainy.json into that commit.
The product engine's rail runs this identical script against its own
releases/brainy.json, unchanged — each repo's wall file lives beside the
CHANGELOG it derives from; there is no cross-repo step.
A --check mode validates a wall file's exact key set, field types, and
newest-first ordering with no duplicates, read-only. tests/unit/release/wall-entry.test.ts
covers derivation, prepend, duplicate refusal, and --check's shape/ordering
checks over temp copies — never the real files. --check also runs green
against both releases/open-brainy.json and releases/brainy.json as they
stand today.
2026-09-02 14:15:39 -07:00
2026-09-02 14:51:33 -07:00
if ( wall . product !== product ) {
fail ( ` " ${ filePath } " has product " ${ wall . product } ", but --product " ${ product } " was given — refusing a cross-product write ` )
ci(release): mechanize the releases-wall entry — never hand-written again
Every release used to get its releases/open-brainy.json entry typed by hand
after the fact. scripts/wall-entry.mjs derives it from the CHANGELOG entry
release.sh just composed (headline = first bullet, items = every bullet,
hash stripped) and prepends it, refusing by name on a duplicate version and
validating the whole file's shape + newest-first ordering before and after
it writes.
release.sh now runs it as its own step, between the CHANGELOG update and
the release commit, and stages releases/open-brainy.json into that commit.
The product engine's rail runs this identical script against its own
releases/brainy.json, unchanged — each repo's wall file lives beside the
CHANGELOG it derives from; there is no cross-repo step.
A --check mode validates a wall file's exact key set, field types, and
newest-first ordering with no duplicates, read-only. tests/unit/release/wall-entry.test.ts
covers derivation, prepend, duplicate refusal, and --check's shape/ordering
checks over temp copies — never the real files. --check also runs green
against both releases/open-brainy.json and releases/brainy.json as they
stand today.
2026-09-02 14:15:39 -07:00
}
2026-09-02 14:51:33 -07:00
const replacing = wall . entries . some ( ( e ) => e . version === entry . version )
wall . entries = [ entry , ... wall . entries . filter ( ( e ) => e . version !== entry . version ) ]
ci(release): mechanize the releases-wall entry — never hand-written again
Every release used to get its releases/open-brainy.json entry typed by hand
after the fact. scripts/wall-entry.mjs derives it from the CHANGELOG entry
release.sh just composed (headline = first bullet, items = every bullet,
hash stripped) and prepends it, refusing by name on a duplicate version and
validating the whole file's shape + newest-first ordering before and after
it writes.
release.sh now runs it as its own step, between the CHANGELOG update and
the release commit, and stages releases/open-brainy.json into that commit.
The product engine's rail runs this identical script against its own
releases/brainy.json, unchanged — each repo's wall file lives beside the
CHANGELOG it derives from; there is no cross-repo step.
A --check mode validates a wall file's exact key set, field types, and
newest-first ordering with no duplicates, read-only. tests/unit/release/wall-entry.test.ts
covers derivation, prepend, duplicate refusal, and --check's shape/ordering
checks over temp copies — never the real files. --check also runs green
against both releases/open-brainy.json and releases/brainy.json as they
stand today.
2026-09-02 14:15:39 -07:00
const postErrors = validateShape ( wall )
if ( postErrors . length ) {
fail ( ` the entry for ${ entry . version } would leave " ${ filePath } " invalid — \n ${ postErrors . join ( '\n ' ) } ` )
}
writeFileSync ( filePath , JSON . stringify ( wall , null , 2 ) + '\n' , 'utf8' )
2026-09-02 14:51:33 -07:00
const status = git ( [ 'status' , '--porcelain' , '--' , ` ${ product } .json ` ] , cacheDir )
if ( status === '' ) {
console . log ( ` wall-entry: " ${ product } .json" already carries an identical entry for ${ entry . version } — nothing to commit or push ` )
return
}
try {
git ( [ 'add' , ` ${ product } .json ` ] , cacheDir )
git ( [ 'commit' , '-m' , ` chore(wall): ${ product } ${ entry . version } ` ] , cacheDir )
} catch ( err ) {
fail ( ` cannot commit the wall entry in " ${ cacheDir } " — ${ /** @type {Error} */ ( err ) . message } \n cure: inspect " ${ cacheDir } " by hand and re-run once its git state is clean ` )
}
try {
git ( [ 'push' , 'origin' , 'main' ] , cacheDir )
} catch ( err ) {
fail (
` push to " ${ remote } " failed (likely a non-fast-forward — another release landed on main first) — ${ /** @type {Error} */ ( err ) . message } \n ` +
` cure: re-run this release step; it re-fetches and resets onto the latest origin/main before retrying ` ,
)
}
const sha = git ( [ 'rev-parse' , 'HEAD' ] , cacheDir )
console . log (
` wall-entry: ${ replacing ? 'replaced' : 'wrote' } v ${ entry . version } in " ${ product } .json" ( ${ wall . entries . length } entries, newest first) — pushed ${ sha } to ${ remote } main ` ,
)
ci(release): mechanize the releases-wall entry — never hand-written again
Every release used to get its releases/open-brainy.json entry typed by hand
after the fact. scripts/wall-entry.mjs derives it from the CHANGELOG entry
release.sh just composed (headline = first bullet, items = every bullet,
hash stripped) and prepends it, refusing by name on a duplicate version and
validating the whole file's shape + newest-first ordering before and after
it writes.
release.sh now runs it as its own step, between the CHANGELOG update and
the release commit, and stages releases/open-brainy.json into that commit.
The product engine's rail runs this identical script against its own
releases/brainy.json, unchanged — each repo's wall file lives beside the
CHANGELOG it derives from; there is no cross-repo step.
A --check mode validates a wall file's exact key set, field types, and
newest-first ordering with no duplicates, read-only. tests/unit/release/wall-entry.test.ts
covers derivation, prepend, duplicate refusal, and --check's shape/ordering
checks over temp copies — never the real files. --check also runs green
against both releases/open-brainy.json and releases/brainy.json as they
stand today.
2026-09-02 14:15:39 -07:00
}
function main ( ) {
const args = parseArgs ( process . argv . slice ( 2 ) )
if ( args . check ) {
2026-09-02 14:51:33 -07:00
const filePath = /** @type {string | undefined} */ ( args . file )
if ( ! filePath ) fail ( '--check needs --file <path>' )
ci(release): mechanize the releases-wall entry — never hand-written again
Every release used to get its releases/open-brainy.json entry typed by hand
after the fact. scripts/wall-entry.mjs derives it from the CHANGELOG entry
release.sh just composed (headline = first bullet, items = every bullet,
hash stripped) and prepends it, refusing by name on a duplicate version and
validating the whole file's shape + newest-first ordering before and after
it writes.
release.sh now runs it as its own step, between the CHANGELOG update and
the release commit, and stages releases/open-brainy.json into that commit.
The product engine's rail runs this identical script against its own
releases/brainy.json, unchanged — each repo's wall file lives beside the
CHANGELOG it derives from; there is no cross-repo step.
A --check mode validates a wall file's exact key set, field types, and
newest-first ordering with no duplicates, read-only. tests/unit/release/wall-entry.test.ts
covers derivation, prepend, duplicate refusal, and --check's shape/ordering
checks over temp copies — never the real files. --check also runs green
against both releases/open-brainy.json and releases/brainy.json as they
stand today.
2026-09-02 14:15:39 -07:00
const wall = loadWallFile ( /** @type {string} */ ( filePath ) )
console . log ( ` wall-entry --check: " ${ filePath } " OK — product " ${ wall . product } ", ${ wall . entries . length } entries, newest-first, no duplicates ` )
process . exit ( 0 )
}
2026-09-02 14:51:33 -07:00
// Generate mode (default, also covers --dry-run): --product, --version,
// --date, --from-changelog required.
ci(release): mechanize the releases-wall entry — never hand-written again
Every release used to get its releases/open-brainy.json entry typed by hand
after the fact. scripts/wall-entry.mjs derives it from the CHANGELOG entry
release.sh just composed (headline = first bullet, items = every bullet,
hash stripped) and prepends it, refusing by name on a duplicate version and
validating the whole file's shape + newest-first ordering before and after
it writes.
release.sh now runs it as its own step, between the CHANGELOG update and
the release commit, and stages releases/open-brainy.json into that commit.
The product engine's rail runs this identical script against its own
releases/brainy.json, unchanged — each repo's wall file lives beside the
CHANGELOG it derives from; there is no cross-repo step.
A --check mode validates a wall file's exact key set, field types, and
newest-first ordering with no duplicates, read-only. tests/unit/release/wall-entry.test.ts
covers derivation, prepend, duplicate refusal, and --check's shape/ordering
checks over temp copies — never the real files. --check also runs green
against both releases/open-brainy.json and releases/brainy.json as they
stand today.
2026-09-02 14:15:39 -07:00
const product = /** @type {string | undefined} */ ( args . product )
const version = /** @type {string | undefined} */ ( args . version )
const date = /** @type {string | undefined} */ ( args . date )
const fromChangelog = /** @type {string | undefined} */ ( args [ 'from-changelog' ] )
const missing = [ ]
if ( ! product ) missing . push ( '--product' )
if ( ! version ) missing . push ( '--version' )
if ( ! date ) missing . push ( '--date' )
if ( ! fromChangelog ) missing . push ( '--from-changelog' )
if ( missing . length ) {
fail (
` missing required flag(s): ${ missing . join ( ', ' ) } \n ` +
'Usage:\n' +
2026-09-02 14:51:33 -07:00
' wall-entry.mjs --product <p> --version <v> --date <YYYY-MM-DD> --from-changelog <CHANGELOG.md> [--dry-run]\n' +
' wall-entry.mjs --check --file <path/to/product.json>' ,
ci(release): mechanize the releases-wall entry — never hand-written again
Every release used to get its releases/open-brainy.json entry typed by hand
after the fact. scripts/wall-entry.mjs derives it from the CHANGELOG entry
release.sh just composed (headline = first bullet, items = every bullet,
hash stripped) and prepends it, refusing by name on a duplicate version and
validating the whole file's shape + newest-first ordering before and after
it writes.
release.sh now runs it as its own step, between the CHANGELOG update and
the release commit, and stages releases/open-brainy.json into that commit.
The product engine's rail runs this identical script against its own
releases/brainy.json, unchanged — each repo's wall file lives beside the
CHANGELOG it derives from; there is no cross-repo step.
A --check mode validates a wall file's exact key set, field types, and
newest-first ordering with no duplicates, read-only. tests/unit/release/wall-entry.test.ts
covers derivation, prepend, duplicate refusal, and --check's shape/ordering
checks over temp copies — never the real files. --check also runs green
against both releases/open-brainy.json and releases/brainy.json as they
stand today.
2026-09-02 14:15:39 -07:00
)
}
const urlArg = args . url === true ? undefined : /** @type {string | undefined} */ ( args . url )
const thumbArg = args . thumb === true ? undefined : /** @type {string | undefined} */ ( args . thumb )
const entry = deriveEntry ( {
product : /** @type {string} */ ( product ) ,
version : /** @type {string} */ ( version ) ,
date : /** @type {string} */ ( date ) ,
changelogPath : /** @type {string} */ ( fromChangelog ) ,
url : urlArg ,
thumb : thumbArg ,
} )
2026-09-02 14:51:33 -07:00
const remote = /** @type {string} */ ( args . remote ? ? process . env . WALL _ENTRY _RELEASES _REMOTE ? ? DEFAULT _REMOTE )
const cacheDir = /** @type {string} */ ( args [ 'cache-dir' ] ? ? process . env . WALL _ENTRY _RELEASES _CACHE _DIR ? ? defaultCacheDir ( ) )
if ( args [ 'dry-run' ] ) {
console . log ( ` wall-entry --dry-run: would write to " ${ join ( cacheDir , ` ${ product } .json ` ) } " in ${ remote } (main), pushed as "chore(wall): ${ product } ${ version } " ` )
console . log ( JSON . stringify ( entry , null , 2 ) )
process . exit ( 0 )
}
publishEntry ( entry , /** @type {string} */ ( product ) , remote , cacheDir )
ci(release): mechanize the releases-wall entry — never hand-written again
Every release used to get its releases/open-brainy.json entry typed by hand
after the fact. scripts/wall-entry.mjs derives it from the CHANGELOG entry
release.sh just composed (headline = first bullet, items = every bullet,
hash stripped) and prepends it, refusing by name on a duplicate version and
validating the whole file's shape + newest-first ordering before and after
it writes.
release.sh now runs it as its own step, between the CHANGELOG update and
the release commit, and stages releases/open-brainy.json into that commit.
The product engine's rail runs this identical script against its own
releases/brainy.json, unchanged — each repo's wall file lives beside the
CHANGELOG it derives from; there is no cross-repo step.
A --check mode validates a wall file's exact key set, field types, and
newest-first ordering with no duplicates, read-only. tests/unit/release/wall-entry.test.ts
covers derivation, prepend, duplicate refusal, and --check's shape/ordering
checks over temp copies — never the real files. --check also runs green
against both releases/open-brainy.json and releases/brainy.json as they
stand today.
2026-09-02 14:15:39 -07:00
}
main ( )