fix(wall): every entry carries an https permalink — the product engine links its public package page; null refused, an unknown product refuses by name
Some checks failed
CI / Node 22 (push) Failing after 7m45s
CI / Node 24 (push) Failing after 7m46s
CI / Bun (latest) (push) Successful in 12m40s
CI / Integration + conformance (Node 22) (push) Failing after 17m16s

This commit is contained in:
David Snelling 2026-09-02 14:59:49 -07:00
parent adcb883e67
commit 97b5ea2d5d
2 changed files with 29 additions and 14 deletions

View file

@ -73,13 +73,14 @@ const ENTRY_OPTIONAL_KEYS = ['thumb']
const ENTRY_ALLOWED_KEYS = [...ENTRY_REQUIRED_KEYS, ...ENTRY_OPTIONAL_KEYS]
const FILE_KEYS = ['product', 'entries']
// The public release-page URL pattern, by product — only products with a
// PUBLIC forge repo get a derived link. A product without an entry here
// (e.g. "brainy", whose repo is private) gets url: null, matching every
// entry the fleet has shipped for it so far — a private link would 404 for
// anyone reading the public HQ page.
// 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.
const RELEASE_URL_PATTERNS = {
'open-brainy': (version) => `https://source.soulcraft.com/soulcraftlabs/open-brainy/releases/tag/v${version}`,
'brainy': (version) => `https://source.soulcraft.com/soulcraft/-/packages/npm/@soulcraft%2Fbrainy/${version}`,
}
/**
@ -203,8 +204,8 @@ function validateShape(data) {
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`)
}
if (!(entry.url === null || typeof entry.url === 'string')) {
errors.push(`${label}: "url" must be a string or null`)
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`)
}
if ('thumb' in entry && !(entry.thumb === null || typeof entry.thumb === 'string')) {
errors.push(`${label}: "thumb" must be a string or null when present`)
@ -274,8 +275,8 @@ function extractChangelogBullets(changelog, version) {
/**
* Derive a wall entry from a CHANGELOG.md.
* @param {{product: string, version: string, date: string, changelogPath: string, url?: string | null, thumb?: string | null}} opts
* @returns {{version: string, date: string, headline: string, items: string[], url: string | null, thumb: string | null}}
* @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}}
*/
function deriveEntry({ product, version, date, changelogPath, url, thumb }) {
if (!parseSemver(version)) fail(`--version "${version}" is not a semver string`)
@ -288,7 +289,11 @@ function deriveEntry({ product, version, date, changelogPath, url, thumb }) {
const items = extractChangelogBullets(changelog, version)
const headline = items[0]
const resolvedUrl = url !== undefined ? url : (RELEASE_URL_PATTERNS[product]?.(version) ?? null)
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)
const resolvedThumb = thumb !== undefined ? thumb : null
return { version, date, headline, items, url: resolvedUrl, thumb: resolvedThumb }
@ -382,7 +387,7 @@ function ensureReleasesClone(remote, cacheDir) {
* 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.
* @param {{version: string, date: string, headline: string, items: string[], url: string | null, thumb: string | null}} entry
* @param {{version: string, date: string, headline: string, items: string[], url: string, thumb: string | null}} entry
* @param {string} product
* @param {string} remote
* @param {string} cacheDir

View file

@ -192,8 +192,8 @@ describe('wall-entry.mjs — generate + publish', () => {
expect(readRemote(remoteDir, 'open-brainy')).toEqual(before)
})
it('derives no URL (null) for a product with no known public release-page pattern', () => {
seedRemote(remoteDir, 'brainy', [{ ...BASE_ENTRY, version: '11.0.5', url: null }])
it('derives the public package-page permalink for the product engine (private repo, never null)', () => {
seedRemote(remoteDir, 'brainy', [{ ...BASE_ENTRY, version: '11.0.5', url: 'https://source.soulcraft.com/soulcraft/-/packages/npm/@soulcraft%2Fbrainy/11.0.5' }])
writeFileSync(join(dir, 'CHANGELOG.md'), buildChangelog([{ version: '11.0.6', date: '2026-09-03', bullets: ['fix: a native-only fix'] }]))
const result = run(
@ -203,10 +203,20 @@ describe('wall-entry.mjs — generate + publish', () => {
expect(result.status).toBe(0)
const wall = readRemote(remoteDir, 'brainy')
expect(wall.entries[0].url).toBeNull()
expect(wall.entries[0].url).toBe('https://source.soulcraft.com/soulcraft/-/packages/npm/@soulcraft%2Fbrainy/11.0.6')
expect(wall.entries[0].thumb).toBeNull()
})
it('refuses a product with no permalink pattern, naming the cure', () => {
seedRemote(remoteDir, 'open-brainy', [BASE_ENTRY])
writeFileSync(join(dir, 'CHANGELOG.md'), buildChangelog([{ version: '1.0.0', date: '2026-09-03', bullets: ['feat: first'] }]))
const result = run(['--product', 'mystery', '--version', '1.0.0', '--date', '2026-09-03', '--from-changelog', 'CHANGELOG.md', '--remote', remoteDir, '--cache-dir', cacheDir], dir)
expect(result.status).not.toBe(0)
expect(result.stderr).toMatch(/no permalink pattern for product "mystery"/)
expect(result.stderr).toMatch(/never carry url: null/)
})
it('refuses when the CHANGELOG has no entry yet for the target version, and touches no remote', () => {
seedRemote(remoteDir, 'open-brainy', [])
writeFileSync(join(dir, 'CHANGELOG.md'), buildChangelog([{ version: '10.4.11', date: '2026-09-02', bullets: ['fix: whatever'] }]))