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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ jobs:
python3 -m http.server 8765 --directory public >"${RUNNER_TEMP}/identifier-server.log" 2>&1 &
server_pid="$!"
trap 'kill "${server_pid}"' EXIT
IDENTIFIER_BASE_URL=http://127.0.0.1:8765 npm run smoke:catalog

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Wait for the local server before starting the catalog smoke

When Python has not finished binding port 8765 before this newly added command starts, smoke:catalog immediately launches eight workers and permanently records their connection failures instead of retrying. This is reproducible with the workflow's command sequence—the smoke reported 68 of 320 failed requests while the server became ready during the run—so otherwise valid CI runs can fail; poll a known route or add retry handling before starting the smoke.

Useful? React with 👍 / 👎.

IDENTIFIER_BASE_URL=http://127.0.0.1:8765 npm run smoke:live

- name: Summarize catalog change
Expand Down
82 changes: 71 additions & 11 deletions .github/workflows/deploy-cloudflare-workers.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
name: Deploy Cloudflare Workers
run-name: Publish Registry Stack identifiers ${{ inputs.released_tag }} (${{ inputs.request_id }})

on:
push:
branches: [main]
workflow_dispatch:
inputs:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restrict production deployments to the main branch

When this workflow is dispatched with a non-main ref, the checkout and subsequent build use the publisher files from that ref, but no condition verifies github.ref. Because workflow_dispatch permits selecting a branch, a write-capable caller can deploy an unreviewed feature-branch bundle with the production Cloudflare credentials despite the new reviewed-main publication model; reject any ref other than refs/heads/main before the deploy job runs.

Useful? React with 👍 / 👎.

released_tag:
description: Exact released Registry Stack tag
required: true
type: string
source_sha:
description: Exact released Registry Stack source commit
required: true
type: string
catalog_sha256:
description: SHA-256 of the exact released identifier catalog
required: true
type: string
request_id:
description: Registry Stack release publication correlation ID
required: true
type: string

permissions:
contents: read
Expand All @@ -30,15 +46,40 @@ jobs:

- name: Read pinned Registry Stack source
id: source
env:
INPUT_CATALOG_SHA256: ${{ inputs.catalog_sha256 }}
INPUT_RELEASED_TAG: ${{ inputs.released_tag }}
INPUT_REQUEST_ID: ${{ inputs.request_id }}
INPUT_SOURCE_SHA: ${{ inputs.source_sha }}
shell: bash
run: |
node <<'NODE'
const { appendFileSync, readFileSync } = require('node:fs');
const source = JSON.parse(readFileSync('src/upstream/source.json', 'utf8'));
if (!/^[0-9a-f]{40}$/.test(source.commit)) {
throw new Error('src/upstream/source.json does not pin a full commit');
const commit = process.env.INPUT_SOURCE_SHA;
const catalog = process.env.INPUT_CATALOG_SHA256;
const tag = process.env.INPUT_RELEASED_TAG;
const request = process.env.INPUT_REQUEST_ID;
if (!/^[0-9a-f]{40}$/.test(commit)) {
throw new Error('identifier publication does not name a full source commit');
}
if (!/^[0-9a-f]{64}$/.test(catalog)) {
throw new Error('identifier publication does not name a catalog SHA-256');
}
if (!/^v0\.[0-9]+\.[0-9]+$/.test(tag)) {
throw new Error('released_tag must be a canonical v0.x.y tag');
}
if (!/^[A-Za-z0-9._-]{1,128}$/.test(request)) {
throw new Error('request_id contains unsupported characters');
}
if (source.commit !== commit || source.catalog_sha256 !== catalog) {
throw new Error(
'release publication inputs do not match the reviewed publisher bundle',
);
}
appendFileSync(process.env.GITHUB_OUTPUT, `commit=${source.commit}\n`);
appendFileSync(process.env.GITHUB_OUTPUT, `commit=${commit}\n`);
appendFileSync(process.env.GITHUB_OUTPUT, `catalog_sha256=${catalog}\n`);
appendFileSync(process.env.GITHUB_OUTPUT, `released_tag=${tag}\n`);
NODE

- name: Checkout pinned Registry Stack source
Expand All @@ -52,7 +93,9 @@ jobs:

- name: Require source provenance from Registry Stack main
env:
EXPECTED_CATALOG_SHA256: ${{ steps.source.outputs.catalog_sha256 }}
PINNED_SOURCE: ${{ steps.source.outputs.commit }}
RELEASED_TAG: ${{ steps.source.outputs.released_tag }}
shell: bash
run: |
set -euo pipefail
Expand All @@ -64,11 +107,26 @@ jobs:
exit 1
fi

- name: Check pinned upstream catalog
run: npm run check:upstream -- _upstream/registry-stack
git -C _upstream/registry-stack fetch origin \
"refs/tags/${RELEASED_TAG}:refs/tags/${RELEASED_TAG}"
if [[ "$(git -C _upstream/registry-stack cat-file -t "refs/tags/${RELEASED_TAG}")" != tag ]]; then
echo "Released Registry Stack tag must be annotated" >&2
exit 1
fi
test "$(git -C _upstream/registry-stack rev-parse "refs/tags/${RELEASED_TAG}^{}")" = \
"${PINNED_SOURCE}"

- name: Check generated output
run: npm test
actual_catalog_sha256="$(
git -C _upstream/registry-stack show \
"${PINNED_SOURCE}:products/identifiers/generated/catalog.v1.json" \
| sha256sum | awk '{print $1}'
)"
test "${actual_catalog_sha256}" = "${EXPECTED_CATALOG_SHA256}"

- name: Check exact source and generated output
run: |
npm run check:upstream -- _upstream/registry-stack
npm test

- name: Build static site
run: npm run build
Expand All @@ -82,5 +140,7 @@ jobs:
command: deploy
gitHubToken: ${{ secrets.GITHUB_TOKEN }}

- name: Smoke every catalog problem URI on the live host
run: npm run smoke:problem-routes
- name: Smoke the exact catalog and compatibility routes on the live host
run: |
npm run smoke:catalog
npm run smoke:live
6 changes: 4 additions & 2 deletions .github/workflows/smoke-live.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,7 @@ jobs:
with:
node-version: "22.12.0"

- name: Check representative live identifiers
run: npm run smoke:live
- name: Check the published catalog and compatibility routes
run: |
npm run smoke:catalog
npm run smoke:live
55 changes: 39 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ Deployment is configured for Cloudflare Workers Static Assets with:
Published identifiers are stable contracts.

- Do not repurpose an identifier after publication.
- Publish only identifiers with `status: active` in the pinned source catalog.
- Remove identifiers that leave the source catalog. Removed paths return `404`.
- Publish identifiers in the pinned source catalog as `active`.
- Preserve a previously published identifier that leaves the active source
catalog and mark its last reviewed record `deprecated`. Never reuse its URI
for another kind or meaning.
- Do not change an identifier's kind.
- Use lowercase product scopes in paths.
- Use `code` for programmatic branching in client code. Problem `type` URLs are
Expand Down Expand Up @@ -129,18 +131,26 @@ The Cloudflare account is configured with:
- DNS record: proxied `AAAA id -> 100::`
- Worker route: `id.registrystack.org/*` in the `registrystack.org` zone

Deploy locally with:
Build a local deployment candidate with:

```sh
npm run build
npx wrangler deploy
```

Do not use that command to publish a Registry Stack release. Production
publication is owned by the release workflow described below.

### GitHub Actions deployment

The `.github/workflows/deploy-cloudflare-workers.yml` workflow deploys `public/`
to Cloudflare Workers on pushes to `main` and on manual dispatch. Configure
these repository secrets before enabling it:
The `.github/workflows/deploy-cloudflare-workers.yml` workflow deploys the
reviewed bundle committed to `main` only when Registry Stack release publication
dispatches it with an exact annotated release tag, source commit, catalog
SHA-256, and correlation ID. The committed `src/upstream/source.json` must match
those inputs. The workflow verifies that bundle against the tagged source before
it builds or deploys. It does not deploy on a publisher repository push, and it
does not import unreviewed source during deployment. Configure these repository
secrets before enabling it:

- `CLOUDFLARE_ACCOUNT_ID`
- `CLOUDFLARE_API_TOKEN` scoped to deploy the `registrystack-id` Worker and its
Expand All @@ -149,10 +159,22 @@ these repository secrets before enabling it:
The initial deployment, DNS record, and Worker route were created manually from
the Cloudflare dashboard.

After each deployment, the workflow runs `npm run smoke:problem-routes`, which
fetches every active problem identifier from the same vendored catalog against
the live host and fails the deployment if any of them does not return HTTP
200.
After each deployment, the workflow fetches every active and deprecated
identifier in the reviewed publisher catalogs plus every retained immutable
artifact. It compares the live response with the generated bytes and expected
media type, then checks representative compatibility routes.

Registry Stack needs one fine-grained `IDENTIFIER_PUBLISHER_TOKEN` repository
secret with Actions read/write access only to `registrystack/registrystack-id`.
Its release workflow uses the token to dispatch this workflow and wait for the
correlated run. The Cloudflare credentials remain only in this repository.

To recover an interrupted publication, rerun the failed Registry Stack release
workflow. An already-public exact release takes its read-only closeout path and
dispatches the same tag, source commit, and catalog digest again. Alternatively,
dispatch this workflow from `main` with all four recorded inputs. Never change
the source commit or digest for a released tag; fix forward with a new release
if immutable public bytes conflict.

The `ci.yml` workflow checks every pull request against the exact pinned
Registry Stack commit, rebuilds the site, runs a local availability smoke, and
Expand All @@ -171,9 +193,10 @@ The job does not approve or merge its pull request and grants no Cloudflare or
DNS authority. The synchronization branch is automation-owned; human changes
must go to Registry Stack source or a separate publisher branch.

The scheduled `smoke-live.yml` workflow checks representative active problem,
schema, and vocabulary identifiers, plus the absence of one removed namespace.
Availability is monitored after publication and is not a Registry Stack
source-build gate. The post-deploy `smoke:problem-routes` step in
`deploy-cloudflare-workers.yml` is narrower and exhaustive rather than
representative: it checks every active problem identifier, every time.
Before tagging a Registry Stack release, merge a reviewed synchronization pull
request whose pinned commit and catalog digest are the exact release source.
The later release dispatch will fail closed if `main` contains another bundle.

The scheduled `smoke-live.yml` workflow repeats the exact-catalog smoke plus
representative compatibility checks. Availability is monitored after
publication and is not a Registry Stack source-build gate.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"check:upstream": "node scripts/check-upstream-artifacts.mjs",
"check:problem-routes": "node scripts/check-problem-routes.mjs",
"report:catalog": "node scripts/report-catalog-change.mjs",
"smoke:catalog": "node scripts/smoke-catalog.mjs",
"smoke:live": "node scripts/smoke-live.mjs",
"smoke:problem-routes": "node scripts/smoke-problem-routes.mjs",
"test": "node --test scripts/*.test.mjs && npm run check && npm run check:problem-routes"
Expand Down
51 changes: 8 additions & 43 deletions public/_headers
Original file line number Diff line number Diff line change
Expand Up @@ -60,77 +60,42 @@
Access-Control-Allow-Origin: *
Cache-Control: public, max-age=86400

/.well-known/registrystack-identifiers
Content-Type: application/json; charset=utf-8
Access-Control-Allow-Origin: *
Cache-Control: public, max-age=300

/artifacts/sha256/6b660ca268062996a138839051ba35b306673f1720256b40419f48a7abb1c44f.json
/artifacts/sha256/*.json
Content-Type: application/schema+json; charset=utf-8
Access-Control-Allow-Origin: *
Cache-Control: public, max-age=31536000, immutable

/artifacts/sha256/f9724862a5922f80ae7b5ea9b87082697274cf79df29c950b2f2221c941f9a9e.json
Content-Type: application/schema+json; charset=utf-8
/artifacts/sha256/*.jsonld
Content-Type: application/ld+json; charset=utf-8
Access-Control-Allow-Origin: *
Cache-Control: public, max-age=31536000, immutable

/artifacts/sha256/76bab2f49eef78995b5df8cf21d45a179cdfc9842041a0d7f0b291fc6d632c1d.json
Content-Type: application/schema+json; charset=utf-8
/artifacts/sha256/*.md
Content-Type: text/markdown; charset=utf-8
Access-Control-Allow-Origin: *
Cache-Control: public, max-age=31536000, immutable

/artifacts/sha256/e45632976b2436dc468805b78e6ac35d4cb05340b260370d011eee68fcf28c5d.json
Content-Type: application/schema+json; charset=utf-8
/.well-known/registrystack-identifiers
Content-Type: application/json; charset=utf-8
Access-Control-Allow-Origin: *
Cache-Control: public, max-age=31536000, immutable
Cache-Control: public, max-age=300

/schemas/registry-record/v1
Content-Type: application/schema+json; charset=utf-8
Access-Control-Allow-Origin: *
Cache-Control: public, max-age=86400

/artifacts/sha256/31cae7de9e84904e300d0d77ffbfdfc7a1de6127719a0ccbb3f6a88d7306510e.json
Content-Type: application/schema+json; charset=utf-8
Access-Control-Allow-Origin: *
Cache-Control: public, max-age=31536000, immutable

/schemas/registry-relay/audit-event/v2alpha1
Content-Type: application/schema+json; charset=utf-8
Access-Control-Allow-Origin: *
Cache-Control: public, max-age=86400

/artifacts/sha256/e2b2be3baabb1c5f147a4095eade0d85702d814ec93049b62208fdd7ab1265b8.json
Content-Type: application/schema+json; charset=utf-8
Access-Control-Allow-Origin: *
Cache-Control: public, max-age=31536000, immutable

/artifacts/sha256/c030917d19b1f50c8d2b41fc7b2ef4fb07b803b5fa98ffd971f5c964f491f7e5.json
Content-Type: application/schema+json; charset=utf-8
Access-Control-Allow-Origin: *
Cache-Control: public, max-age=31536000, immutable

/artifacts/sha256/9beeda053e86d8ffa67755e91883efd0d2205026ee7d0aa7f38cecef7a6f781b.json
Content-Type: application/schema+json; charset=utf-8
Access-Control-Allow-Origin: *
Cache-Control: public, max-age=31536000, immutable

/contexts/registry-record/v1
Content-Type: application/ld+json; charset=utf-8
Access-Control-Allow-Origin: *
Cache-Control: public, max-age=86400

/artifacts/sha256/9b8bf452538214d8559b4e8034446bfade37b5f409cfeaaace650a567423be71.jsonld
Content-Type: application/ld+json; charset=utf-8
Access-Control-Allow-Origin: *
Cache-Control: public, max-age=31536000, immutable

/profiles/registry-record/v1
Content-Type: text/markdown; charset=utf-8
Access-Control-Allow-Origin: *
Cache-Control: public, max-age=86400

/artifacts/sha256/f165e62044df2129b5cc533d3199cf81ae23d263617f39437ee37be87d3b37fe.md
Content-Type: text/markdown; charset=utf-8
Access-Control-Allow-Origin: *
Cache-Control: public, max-age=31536000, immutable
Loading