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
99 changes: 99 additions & 0 deletions .github/workflows/deploy-site.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Build apps/website and publish it to the repo that serves t27.ai.
#
# anomaly-register A51: four PRs merged, every check green, and the live site
# changed nothing for a week. This app deploys from gHashTag/ghashtag.github.io,
# which holds build output only, and a human copied dist/ into it. Nothing in
# the pipeline noticed, because every other check reads the source.
#
# The deploy needs write access to a second repository, which a workflow token
# does not have. It requires a secret named GHIO_TOKEN -- a fine-grained PAT
# with Contents: write on gHashTag/ghashtag.github.io and nothing else. Until
# that secret exists this workflow does not run, and check:deployed keeps
# reporting the gap on every PR.
#
# Manual only, on purpose. Publishing to the live domain on every push to main
# would mean any merge broadcasts, and this project's rule is that content is
# approved before it goes out.
name: Deploy site to t27.ai

on:
workflow_dispatch:
inputs:
reason:
description: 'What is being published (goes in the commit message)'
required: true

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: npm
cache-dependency-path: apps/website/package-lock.json

- name: Build
working-directory: apps/website
run: |
npm ci
npx vite build

# The same three checks the PR runs, against the tree being published.
# A deploy that skips them is how a broken build reaches the domain.
- name: Checks
working-directory: apps/website
run: |
npm run typecheck:ratchet
npm run check:api

- name: Checkout the deploy repo
uses: actions/checkout@v4
with:
repository: gHashTag/ghashtag.github.io
token: ${{ secrets.GHIO_TOKEN }}
path: ghio

# Copy the build, then delete bundles the new index.html cannot reach.
# Skipping that step is how 74 orphans accumulated once, keeping
# withdrawn claims greppable for weeks after the components were fixed.
- name: Copy the build and drop unreachable bundles
run: |
cp -R apps/website/dist/assets/. ghio/assets/
cp apps/website/dist/index.html ghio/index.html
cp apps/website/dist/manifest.json ghio/manifest.json
node - <<'EOF'
const {readFileSync, readdirSync, unlinkSync} = require('node:fs');
const {join} = require('node:path');
const A = 'ghio/assets';
const entry = [...readFileSync('ghio/index.html','utf8')
.matchAll(/assets\/([\w.-]+\.(?:js|css))/g)].map(m => m[1]);
const seen = new Set(); const stack = [...entry];
while (stack.length) {
const f = stack.pop();
if (seen.has(f)) continue;
seen.add(f);
try {
for (const m of readFileSync(join(A,f),'utf8').matchAll(/["']\.\/([\w.-]+\.(?:js|css))["']/g))
if (!seen.has(m[1])) stack.push(m[1]);
} catch {}
}
let n = 0;
for (const f of readdirSync(A))
if (/\.(js|css)$/.test(f) && !seen.has(f)) { unlinkSync(join(A,f)); n++; }
console.log(` ${seen.size} reachable, ${n} orphan(s) removed`);
EOF

- name: Commit and push
working-directory: ghio
run: |
git config user.name gHashTag
git config user.email admin@t27.ai
git add -A
git diff --cached --quiet && { echo " nothing changed — the live site is already this build"; exit 0; }
git commit -m "Deploy: ${{ inputs.reason }}

Built from gHashTag/trinity@${{ github.sha }} by the deploy workflow.
See anomaly-register A51 for why this exists."
git push
13 changes: 13 additions & 0 deletions .github/workflows/website-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,16 @@ jobs:
env:
CHROME_PATH: ${{ steps.chrome.outputs.chrome-path }}
run: npm run check:render -- --no-build

# A51: four PRs merged, every check green, and the live site changed
# nothing -- this app deploys from a second repository that holds build
# output only, copied there by hand. Every check above runs against the
# source, so all of them pass on a tree whose output nobody published.
#
# This one fetches t27.ai and compares the entry bundle. It never fails
# the job: a merge is not a deploy, and blocking one on the other
# inverts the order. It is here so the gap is visible in the log rather
# than discovered a week later in the site's <head>.
- name: Is the live site this build?
continue-on-error: true
run: npm run check:deployed
1 change: 1 addition & 0 deletions apps/website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"typecheck:ratchet": "node scripts/typecheck-ratchet.mjs",
"check:api": "node scripts/api-contract-check.mjs",
"check:render": "node scripts/render-check.mjs",
"check:deployed": "node scripts/deployed-check.mjs",
"check:aria": "node scripts/aria-refs-check.mjs",
"typecheck:update": "node scripts/typecheck-ratchet.mjs --update"
},
Expand Down
62 changes: 62 additions & 0 deletions apps/website/scripts/deployed-check.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Is what t27.ai serves the same thing this tree builds?
//
// anomaly-register A51: four PRs merged, every check green, and the live site
// changed nothing. The SPA deploys from a second repository that holds build
// output only, and nothing copies a build into it. The site kept serving
// "First chip with native SU(3) Unitary Core" for a week after the tree that
// produces it stopped saying that.
//
// A51 concluded no test in the source repo could see this. That was wrong by
// one assumption: a test that FETCHES THE LIVE SITE can. This is that test.
//
// npm run check:deployed
//
// It compares what the deployed index.html loads against what dist/index.html
// loads. Vite hashes bundle names by content, so equal names mean equal
// bundles and a different name means the deploy is behind -- or ahead, which
// is worth knowing too.
//
// What it cannot tell you: WHICH direction, or whether the difference matters.
// A whitespace change moves the hash. So this warns; it does not fail a build.
// A gate that blocks a merge because someone has not deployed yet inverts the
// order of operations.
import { readFileSync, existsSync } from 'node:fs';

const SITE = process.env.DEPLOY_URL ?? 'https://t27.ai/';
const entryOf = (html) => (html.match(/assets\/(index-[\w-]+\.js)/) ?? [])[1];

if (!existsSync('dist/index.html')) {
console.log(' no dist/ — run `npx vite build` first (nothing to compare)');
process.exit(0);
}
const local = entryOf(readFileSync('dist/index.html', 'utf8'));
if (!local) {
console.error(' no entry bundle in dist/index.html. That is a build problem, not a deploy one.');
process.exit(1);
}

let live;
try {
const res = await fetch(SITE, { headers: { 'User-Agent': 'deployed-check' } });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
live = entryOf(await res.text());
} catch (e) {
// Offline, or the site is down. Neither is a defect in this change, and a
// check that fails when the network does gets muted.
console.log(` could not reach ${SITE} (${e.message}) — skipping`);
process.exit(0);
}

if (!live) {
console.error(` ${SITE} serves no index-*.js bundle. Either it is not this app, or it is broken.`);
process.exit(1);
}
if (live === local) {
console.log(` deployed: ${live} — the live site is this build`);
process.exit(0);
}
console.log(`\n the live site is NOT this build:\n`);
console.log(` ${SITE.padEnd(28)} ${live}`);
console.log(` dist/index.html ${local}\n`);
console.log(' Merging does not deploy this site. The build output lives in a second');
console.log(' repository and is copied there by hand — see anomaly-register A51.');
Loading