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
70 changes: 70 additions & 0 deletions .github/scripts/test_methodology.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
const assert = require('node:assert/strict');
const { readFileSync } = require('node:fs');
const { test } = require('node:test');
const vm = require('node:vm');

const html = readFileSync('methodology/index.html', 'utf8');
const latest = 'https://github.com/transitrix/methodology/releases/latest';
const api = 'https://api.github.com/repos/transitrix/methodology/releases/latest';
const script = [...html.matchAll(/<script>([\s\S]*?)<\/script>/gi)].at(-1)[1];

function initialElements() {
const elements = {};
for (const id of ['spec-link', 'version-placeholder']) {
const match = html.match(new RegExp(`<([a-z]+) id="${id}"([^>]*)>([^<]*)</\\1>`, 'i'));
assert.ok(match, `Missing ${id}`);
elements[id] = { textContent: match[3], href: match[2].match(/href="([^"]+)"/)?.[1] };
}
return elements;
}

async function load(fetch) {
const elements = initialElements();
const context = vm.createContext({
document: { getElementById: id => elements[id] },
fetch: url => { assert.equal(url, api); return fetch(); },
console: { error() {} },
});
await vm.runInContext(script, context, { timeout: 1000 });
return elements;
}

test('without JavaScript, the link opens the latest release and text remains useful', () => {
const elements = initialElements();
assert.equal(elements['spec-link'].href, latest);
assert.match(elements['version-placeholder'].textContent, /latest published specification/);
});

test('the fetched release determines the version and link, including future releases', async () => {
for (const tag of ['v7.0.0', 'v42.3.1']) {
const elements = await load(async () => ({ ok: true, json: async () => ({ tag_name: tag }) }));
assert.equal(elements['spec-link'].href, `https://github.com/transitrix/methodology/releases/tag/${tag}`);
assert.equal(elements['spec-link'].textContent, `Read the ${tag} methodology specification on GitHub`);
assert.match(elements['version-placeholder'].textContent, new RegExp(tag.slice(1).replaceAll('.', '\\.')));
}
});

test('HTTP, network, JSON and invalid release failures preserve the latest-release link', async () => {
const failures = [
async () => { throw new Error('Network unavailable'); },
async () => ({ ok: false }),
async () => ({ ok: true, json: async () => { throw new Error('Invalid JSON'); } }),
...[null, {}, { tag_name: '' }, { tag_name: 7 }, { tag_name: 'v8', draft: true },
{ tag_name: 'v8', prerelease: true }].map(release =>
async () => ({ ok: true, json: async () => release })),
];
for (const fetch of failures) {
const elements = await load(fetch);
assert.equal(elements['spec-link'].href, latest);
assert.equal(elements['version-placeholder'].textContent, 'View the latest methodology specification on GitHub.');
}
});

test('release names stay text and release tags stay within the tag URL', async () => {
const elements = await load(async () => ({ ok: true, json: async () => ({
tag_name: 'v8/preview?#', name: '<img src=x> (breaking)',
}) }));
assert.equal(elements['spec-link'].href, 'https://github.com/transitrix/methodology/releases/tag/v8%2Fpreview%3F%23');
assert.match(elements['version-placeholder'].textContent, /Breaking changes/);
assert.doesNotMatch(elements['version-placeholder'].textContent, /<img/);
});
47 changes: 15 additions & 32 deletions .github/workflows/methodology-version-check.yml
Original file line number Diff line number Diff line change
@@ -1,49 +1,32 @@
name: Methodology version check

# The /methodology/ page serves the published specification address.
# It fetches the latest transitrix/methodology release at runtime via JavaScript.
# This check verifies that if the page is committed with a static version (for any reason),
# it matches the current latest stable release.

on:
push:
branches: [main]
paths: [methodology/index.html]
paths:
- methodology/index.html
- .github/scripts/test_methodology.cjs
- .github/workflows/methodology-version-check.yml
pull_request:
branches: [main]
paths: [methodology/index.html]
paths:
- methodology/index.html
- .github/scripts/test_methodology.cjs
- .github/workflows/methodology-version-check.yml

permissions:
contents: read

jobs:
check-methodology-version:
runs-on: ubuntu-latest
name: Methodology page version is current
name: Methodology release loading and fallback
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Fetch latest methodology release
id: methodology
run: |
RESPONSE=$(curl -s https://api.github.com/repos/transitrix/methodology/releases/latest)
TAG=$(echo "$RESPONSE" | jq -r '.tag_name')
VERSION="${TAG#v}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "tag=$TAG" >> $GITHUB_OUTPUT
echo "Latest methodology release: $TAG (v$VERSION)"

- name: Check page links to correct version
run: |
VERSION="${{ steps.methodology.outputs.version }}"
TAG="${{ steps.methodology.outputs.tag }}"
FILE="methodology/index.html"

if ! grep -q "$TAG" "$FILE"; then
echo "Error: methodology/index.html does not link to $TAG (latest release)"
echo "Page should link to: https://github.com/transitrix/methodology/releases/tag/$TAG"
exit 1
fi

echo "✓ Page correctly references $TAG"
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Verify dynamic release loading without network access
run: node --test .github/scripts/test_methodology.cjs
1 change: 1 addition & 0 deletions MANIFEST.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,4 @@ source and a verified deployment succeeds. This declaration does not deploy it.
- `.github/scripts/test_pages_package.py`
- `.github/scripts/check_live_site.py`
- `.github/scripts/test_live_site.py`
- `.github/scripts/test_methodology.cjs`
45 changes: 12 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@ npx serve .

## Contributing

Content changes go through pull requests. Routine updates are handled by automation.

**Work for this repository is filed in [`transitrix/transitrix-hq`](https://github.com/transitrix/transitrix-hq)** (a private repository), not in this repository's issue tracker. If you'd like to propose a change, report a problem, or request a feature, open a pull request here or email `hello@transitrix.com`.
To propose a change, open a pull request. To report a problem or request a feature, email `hello@transitrix.com`.

## Consumer website artifact

Expand All @@ -32,29 +30,15 @@ uploads `site-consumer-preview`. Build locally without deployment:
python3 .github/scripts/build_site.py --output /tmp/transitrix-site-preview
```

The output path must not exist. Reader URLs and included file bytes are preserved;
security contact, discovery files, downloads, domain markers and licensing remain.
Contributor scripts, templates, tests, workflows and source-only assets stay in the
public repository. Their exact paths remain covered by the repository inventory.
Uploading this preview does not change the Pages source or live publication.
A legacy repository-root source also serves some repository-only paths; select
GitHub Actions and perform a verified manual deployment to publish only the
consumer artifact.
The output path must not exist. `MANIFEST.md` declares the website files and
repository-only contributor tools separately. The preview includes only the
website files; generating or uploading it does not publish the site.

## Manual publication

`Deploy consumer website manually` runs only through `workflow_dispatch` on main.
It repeats the publication tests, builds the consumer directory and verifies a
regular-file `artifact.tar` before deploying through the existing `github-pages`
environment. It never deploys on pull requests or pushes. Only the deploy job has
Pages/OIDC write permissions; the standard Ubuntu runner uses no dependency cache
and retains its small Pages artifact for one day.

The tar packager preserves exact declared dot paths, including security.txt and
the static-host marker. It rejects missing/extra/changed content and links.
Activating this workflow requires selecting GitHub Actions as the Pages source;
this does not change DNS, custom-domain or HTTPS settings. Until that source change
is explicitly applied, legacy root publication remains active.
Run `Deploy consumer website manually` from main with GitHub Actions selected as
the Pages source. The workflow tests, builds and verifies the website archive
before deployment. Pushes and pull requests do not trigger this workflow.

After a completed manual deployment, verify the selected artifact and public
responses from a clean checkout of its exact SHA:
Expand All @@ -65,13 +49,8 @@ python3 .github/scripts/check_live_site.py --run-id RUN_ID --artifact-id ARTIFAC
--pages-deployment-id PAGES_DEPLOYMENT_ID --output /tmp/site-acceptance.json
```

Use the artifact and Pages deployment identifiers from the deployment log and
the corresponding `github-pages` environment record. The read-only verifier uses
existing `gh` access to bind the identifiers to a successful manual main run,
downloads that exact artifact, verifies all 58 archived file hashes, and requires
all 58 public files to return matching bytes. It also checks the exact 16 legacy
repository-only URLs for HTTP 404 and requires `text/plain` for security.txt.
Redirects, HTTP errors, fallback responses and changed bytes fail acceptance.
The JSON receipt records identifiers, hashes and selected cache/request headers;
keep it outside the checkout. A successful workflow alone is not public acceptance.
This command does not deploy or change hosting settings.
Use identifiers from the deployment log and the corresponding `github-pages`
environment record. The verifier requires existing `gh` access. It checks the
selected artifact, exact public file bytes, security.txt content type, and HTTP
404 responses for excluded repository-only paths. It writes a JSON receipt to
the output path. A successful workflow alone does not establish public acceptance.
15 changes: 9 additions & 6 deletions methodology/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
<main id="main" class="wrap">
<div class="hero">
<h1>The published methodology</h1>
<p class="lede"><span id="version-placeholder">Loading version...</span></p>
<p class="lede"><span id="version-placeholder">Read the latest published specification.</span></p>
</div>

<section>
Expand All @@ -69,7 +69,7 @@ <h2>Versions and stability</h2>

<h2>Read the full specification</h2>
<p>
<a id="spec-link" href="https://github.com/transitrix/methodology/releases/tag/v5.0.0">Read the full methodology specification on GitHub</a> — the specification is maintained as a published release alongside the toolkit source code.
<a id="spec-link" href="https://github.com/transitrix/methodology/releases/latest">Read the full methodology specification on GitHub</a>.
</p>
</section>
</main>
Expand All @@ -92,17 +92,20 @@ <h2>Read the full specification</h2>
const response = await fetch('https://api.github.com/repos/transitrix/methodology/releases/latest');
if (!response.ok) throw new Error('Failed to fetch release');
const release = await response.json();
const tag = release.tag_name; // e.g., "v5.0.0"
const version = tag.replace(/^v/, ''); // remove 'v' prefix
const releaseName = release.name || `Version ${version}`;
const tag = release.tag_name;
if (typeof tag !== 'string' || !tag.trim() || release.draft || release.prerelease) {
throw new Error('Invalid stable release');
}
const version = tag.replace(/^v/, '');
const releaseName = typeof release.name === 'string' ? release.name : '';

// Update version placeholder
const placeholder = document.getElementById('version-placeholder');
placeholder.textContent = `Version ${version}. ${releaseName.includes('(breaking)') ? 'Breaking changes: see the migration recipe.' : 'Stable release.'}`;

// Update spec link
const specLink = document.getElementById('spec-link');
specLink.href = `https://github.com/transitrix/methodology/releases/tag/${tag}`;
specLink.href = `https://github.com/transitrix/methodology/releases/tag/${encodeURIComponent(tag)}`;
specLink.textContent = `Read the v${version} methodology specification on GitHub`;
} catch (error) {
console.error('Could not fetch methodology version:', error);
Expand Down
2 changes: 1 addition & 1 deletion scripts/check_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
PDF that is out of date with a source that is not in this repository; do not
add a check that pretends to.

Explainer one-pagers (transitrix-hq#325) use the same published-PDF contract
Explainer one-pagers use the same published-PDF contract
(geometry, one page, checksum, no source.html) under explainers/, listed in
explainers/SHA256SUMS. Directories without a one-pager are ignored.

Expand Down
Loading