Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs-sources.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ sources:
ref: main
path: happyview-modules/CHANGELOG.md
trackRelease: true
releaseTagPrefix: '@hypercerts-org/hypercerts-api@'
releaseTagPrefix: '@hypercerts-org/happyview-modules@'

- id: cgs-changelog
title: Certified Group Service changelog
Expand Down
41 changes: 32 additions & 9 deletions lib/external-docs-snapshot.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,15 +107,10 @@ async function fetchSourceUpdatedAt(source, token) {

/** Read a published stable GitHub release, never a package.json development version. */
async function fetchLatestRelease(source, token) {
const url = `https://api.github.com/repos/${source.repo}/releases/latest`;
const response = await fetch(url, {
headers: getGitHubHeaders(token, 'application/vnd.github+json', 'hypercerts-docs-build'),
cache: 'no-store',
});
// No published release is a supported state. The Markdown fetch separately verifies repository access.
if (response.status === 404) return null;
if (!response.ok) throw new Error(`Release metadata for ${source.id} returned ${response.status}. Refusing to label a failed lookup as unreleased. Set DOCS_SOURCE_TOKEN or run gh auth login locally.`);
const release = await response.json();
const release = source.releaseTagPrefix
? await fetchLatestPrefixedRelease(source, token)
: await fetchRepositoryLatestRelease(source, token);
if (!release) return null;
const tag = release.tag_name || '';
let versionTag = tag;
if (source.releaseTagPrefix) {
Expand All @@ -136,6 +131,34 @@ async function fetchLatestRelease(source, token) {
};
}

async function fetchReleaseMetadata(url, source, token) {
const response = await fetch(url, {
headers: getGitHubHeaders(token, 'application/vnd.github+json', 'hypercerts-docs-build'),
cache: 'no-store',
});
// No published release is a supported state. The Markdown fetch separately verifies repository access.
if (response.status === 404) return null;
if (!response.ok) throw new Error(`Release metadata for ${source.id} returned ${response.status}. Refusing to label a failed lookup as unreleased. Set DOCS_SOURCE_TOKEN or run gh auth login locally.`);
return response.json();
}

/** The repository's "latest" release, for repositories that release one package. */
function fetchRepositoryLatestRelease(source, token) {
return fetchReleaseMetadata(`https://api.github.com/repos/${source.repo}/releases/latest`, source, token);
}

/**
* The newest stable release tagged with the source's prefix. A repository that releases
* several packages has one "latest" release for all of them, so it cannot be used.
*/
async function fetchLatestPrefixedRelease(source, token) {
const releases = await fetchReleaseMetadata(`https://api.github.com/repos/${source.repo}/releases?per_page=100`, source, token);
if (!releases || releases.length === 0) return null;
const release = releases.find(item => !item.draft && !item.prerelease && (item.tag_name || '').startsWith(source.releaseTagPrefix));
if (!release) throw new Error(`No stable release of ${source.id} is tagged "${source.releaseTagPrefix}<major>.<minor>.<patch>" among the latest ${releases.length} releases of ${source.repo}. Check releaseTagPrefix.`);
return release;
}

/**
* Fetch one immutable Markdown snapshot used by rendering, search, raw exports, and fingerprinting.
*/
Expand Down
36 changes: 21 additions & 15 deletions test/external-docs-snapshot.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,33 +129,39 @@ test('published stable release metadata is captured in the build snapshot', asyn
assert.equal(snapshot.release.url, 'https://github.com/hypercerts-org/ePDS/releases/tag/v1.4.12');
});

test('tracks the API release behind its scoped package tag and rejects another package prefix', async context => {
test('tracks the API release behind its scoped package tag among the repository\'s other packages', async context => {
const originalFetch = global.fetch;
context.after(() => { global.fetch = originalFetch; });
const apiSource = {
id: 'api-changelog',
title: 'Hypercerts API changelog',
repo: 'hypercerts-org/api',
ref: 'main',
path: 'api/CHANGELOG.md',
path: 'happyview-modules/CHANGELOG.md',
trackRelease: true,
releaseTagPrefix: '@hypercerts-org/hypercerts-api@',
releaseTagPrefix: '@hypercerts-org/happyview-modules@',
};
const snapshotForTag = tag => {
global.fetch = async url => String(url).endsWith('/releases/latest')
? response({ json: { tag_name: tag, published_at: '2026-10-06T16:12:16Z', draft: false, prerelease: false } })
: String(url).includes('/commits?') ? response({ json: [] }) : response({ text: '# @hypercerts-org/hypercerts-api\n\n## 0.1.0' });
const release = (tag, extra = {}) => ({ tag_name: tag, published_at: '2026-11-03T09:22:45Z', draft: false, prerelease: false, ...extra });
const snapshotFor = releases => {
global.fetch = async url => String(url).includes('/releases?')
? response({ json: releases })
: String(url).includes('/commits?') ? response({ json: [] }) : response({ text: '# @hypercerts-org/happyview-modules\n\n## 0.3.0' });
return collectSourceSnapshot(apiSource);
};

const snapshot = await snapshotForTag('@hypercerts-org/hypercerts-api@0.1.0');
assert.equal(snapshot.release.version, '0.1.0');
assert.equal(snapshot.release.url, 'https://github.com/hypercerts-org/api/releases/tag/%40hypercerts-org%2Fhypercerts-api%400.1.0');
assert.equal(snapshot.path, 'api/CHANGELOG.md');
await assert.rejects(
() => snapshotForTag('@hypercerts-org/another-package@0.1.0'),
/stable semantic release tag/,
);
// The repository's newest release belongs to another package; the API's is found by its prefix.
const snapshot = await snapshotFor([
release('@hypercerts-org/api@0.1.0'),
release('@hypercerts-org/happyview-modules@0.4.0-beta.1', { prerelease: true }),
release('@hypercerts-org/happyview-modules@0.3.0'),
release('@hypercerts-org/hypercerts-api@0.2.0'),
]);
assert.equal(snapshot.release.version, '0.3.0');
assert.equal(snapshot.release.url, 'https://github.com/hypercerts-org/api/releases/tag/%40hypercerts-org%2Fhappyview-modules%400.3.0');
assert.equal(snapshot.path, 'happyview-modules/CHANGELOG.md');
assert.equal(await snapshotFor([]).then(item => item.release), null);
await assert.rejects(() => snapshotFor([release('@hypercerts-org/api@0.1.0')]), /No stable release of api-changelog/);
await assert.rejects(() => snapshotFor([release('@hypercerts-org/happyview-modules@0.3')]), /stable semantic release tag/);
});

test('an accessible repository without a GitHub release is explicitly unreleased', async context => {
Expand Down
2 changes: 1 addition & 1 deletion test/external-docs.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ test('configures API releases from the package changelog and scoped tag', () =>
ref: 'main',
path: 'happyview-modules/CHANGELOG.md',
trackRelease: true,
releaseTagPrefix: '@hypercerts-org/hypercerts-api@',
releaseTagPrefix: '@hypercerts-org/happyview-modules@',
});

const pagePath = join(__dirname, '..', 'pages', 'releases', 'api.md');
Expand Down
2 changes: 1 addition & 1 deletion test/releases.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function sources() {
ref: 'main',
path: component.id === 'api' ? 'happyview-modules/CHANGELOG.md' : 'CHANGELOG.md',
trackRelease: true,
...(component.id === 'api' ? { releaseTagPrefix: '@hypercerts-org/hypercerts-api@' } : {}),
...(component.id === 'api' ? { releaseTagPrefix: '@hypercerts-org/happyview-modules@' } : {}),
release: component.id === 'relay' ? null : {
version: component.id === 'lexicons' ? '2.0.0' : component.id === 'api' ? '0.1.0' : '0.6.0',
url: component.id === 'api'
Expand Down
Loading