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
13 changes: 12 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ jobs:
node-version: 22
cache: pnpm

- name: Install cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2

- name: Install website dependencies
run: pnpm install --frozen-lockfile

Expand All @@ -92,8 +95,16 @@ jobs:
MDBASE_RS_DIR: .sources/mdbase-rs
MDBASE_TS_DIR: .sources/mdbase

- name: Reject stale Connect release updates
if: github.event_name == 'pull_request'
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
git fetch --no-tags --depth=1 origin "$BASE_SHA"
pnpm check:release-order "$BASE_SHA"

- name: Type and content checks
run: pnpm test:deploy:dev && pnpm check:release && pnpm check
run: pnpm test:deploy:dev && pnpm test:release-update && pnpm check:release && pnpm check
env:
GH_TOKEN: ${{ github.token }}

Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ jobs:
node-version: 22
cache: pnpm

- name: Install cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2

- name: Install website dependencies
run: pnpm install --frozen-lockfile

Expand Down
150 changes: 150 additions & 0 deletions .github/workflows/update-connect-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
name: Update Connect release

on:
repository_dispatch:
types: [connect-client-release-published]
workflow_dispatch:
inputs:
tag:
description: Published mdbase Connect release tag
required: true
type: string

permissions:
contents: read

concurrency:
group: connect-release-update
cancel-in-progress: false

jobs:
update:
name: Verify release and open pull request
runs-on: ubuntu-24.04
steps:
- name: Select release tag
id: release
env:
DISPATCH_TAG: ${{ github.event.client_payload.tag }}
MANUAL_TAG: ${{ inputs.tag }}
run: |
tag="${DISPATCH_TAG:-$MANUAL_TAG}"
if [[ ! "$tag" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]]; then
echo "Invalid Connect release tag." >&2
exit 1
fi
echo "tag=$tag" >> "$GITHUB_OUTPUT"
echo "branch=automation/connect-release-$tag" >> "$GITHUB_OUTPUT"

- name: Check out website
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: main
persist-credentials: false

- name: Read specification source identity
id: specification
run: |
echo "repository=$(jq -r '.specification.repository' site-sources.json)" >> "$GITHUB_OUTPUT"
echo "ref=$(jq -r '.specification.ref' site-sources.json)" >> "$GITHUB_OUTPUT"

- name: Check out specification
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: ${{ steps.specification.outputs.repository }}
ref: ${{ steps.specification.outputs.ref }}
path: .sources/mdbase-spec
persist-credentials: false

- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 10

- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
cache: pnpm

- name: Install cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2

- name: Install website dependencies
run: pnpm install --frozen-lockfile

- name: Build specification artifact
working-directory: .sources/mdbase-spec/site
run: npm ci && npm run build

- name: Verify public evidence and generate release record
env:
GH_TOKEN: ${{ github.token }}
MDBASE_RELEASE_NPM_ATTEMPTS: "15"
MDBASE_RELEASE_NPM_DELAY_MS: "60000"
run: node scripts/update-connect-release.mjs '${{ steps.release.outputs.tag }}'

- name: Validate generated website
env:
GH_TOKEN: ${{ github.token }}
MDBASE_SPEC_DIR: .sources/mdbase-spec
run: pnpm test

- name: Require a release-only change
id: changes
run: |
mapfile -t changed < <(git diff --name-only)
if [ "${#changed[@]}" -eq 0 ]; then
echo "present=false" >> "$GITHUB_OUTPUT"
exit 0
fi
printf '%s\n' "${changed[@]}" | sort > "$RUNNER_TEMP/changed"
printf '%s\n' site-sources.json src/data/connect-release.json | sort > "$RUNNER_TEMP/expected"
diff -u "$RUNNER_TEMP/expected" "$RUNNER_TEMP/changed"
echo "present=true" >> "$GITHUB_OUTPUT"

- name: Create website automation token
if: steps.changes.outputs.present == 'true'
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ vars.RELEASE_AUTOMATION_APP_ID }}
private-key: ${{ secrets.RELEASE_AUTOMATION_APP_PRIVATE_KEY }}
owner: mdbase-dev
repositories: mdbase.dev
permission-contents: write
permission-pull-requests: write

- name: Open release update pull request
if: steps.changes.outputs.present == 'true'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
BRANCH: ${{ steps.release.outputs.branch }}
TAG: ${{ steps.release.outputs.tag }}
run: |
existing="$(gh pr list --repo "$GITHUB_REPOSITORY" --state all --head "$BRANCH" --json url,state --jq '.[0] // empty')"
if [ -n "$existing" ]; then
state="$(jq -r .state <<<"$existing")"
url="$(jq -r .url <<<"$existing")"
if [ "$state" = "OPEN" ] || [ "$state" = "MERGED" ]; then
echo "Release update already represented by $url"
exit 0
fi
echo "The deterministic release PR was closed without merging: $url" >&2
exit 1
fi
if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then
echo "Remote branch $BRANCH exists without a pull request; refusing to overwrite it." >&2
exit 1
fi
git switch -c "$BRANCH"
git config user.name "mdbase release automation[bot]"
git config user.email "mdbase-release-automation[bot]@users.noreply.github.com"
git add site-sources.json src/data/connect-release.json
git commit -m "Update Connect downloads to $TAG"
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
git push --set-upstream origin "$BRANCH"
gh pr create \
--repo "$GITHUB_REPOSITORY" \
--base main \
--head "$BRANCH" \
--title "Update Connect downloads to $TAG" \
--body "Updates the Downloads release record from the published, Sigstore-verified mdbase Connect channel for [$TAG](https://github.com/mdbase-dev/mdbase-connect/releases/tag/$TAG).\n\nThe website release checks and full test suite passed before this PR was opened."
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ Override their paths with `MDBASE_SPEC_DIR`, `MDBASE_CONNECT_DIR`,

## Production build

Release checks require `gh` authentication and `cosign`; CI installs the pinned
Cosign version used by the Connect release workflow.

```sh
pnpm sync:sources
pnpm check
Expand All @@ -36,6 +39,22 @@ remain traceable to the release artifacts they document.
The first-party contract catalog is built from the commit pinned in
`site-sources.json`, then published as static files under `/contracts/`.

## Connect release updates

A published Connect desktop release dispatches its immutable tag to
`.github/workflows/update-connect-release.yml`. The workflow verifies the
GitHub release, Sigstore-signed channel manifest, tag commit, matching npm SDK,
and release assets; regenerates `src/data/connect-release.json` and the Connect
pin in `site-sources.json`; runs the complete website test suite; and opens a
normal pull request. It never deploys or pushes `main` directly. The same
workflow can be run manually with a tag to recover from a delayed npm
publication or failed dispatch.

The workflow uses a GitHub App configured as `RELEASE_AUTOMATION_APP_ID` and
`RELEASE_AUTOMATION_APP_PRIVATE_KEY`. Its installation on this repository needs
only `contents: write` and `pull requests: write`. The App token ensures the
resulting pull request triggers the ordinary protected `Site checks` workflow.

## Development deployment

```sh
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
"import:spec": "node scripts/import-spec.mjs",
"check:links": "node scripts/check-links.mjs",
"check:release": "node scripts/check-connect-release.mjs",
"check:release-order": "node scripts/check-connect-release-order.mjs",
"test:deploy:dev": "node --test scripts/deploy-pages-dev.test.mjs",
"test": "pnpm test:deploy:dev && pnpm check:release && pnpm check && pnpm build && pnpm import:spec && pnpm check:links"
"test:release-update": "node --test scripts/connect-release-record.test.mjs",
"test": "pnpm test:deploy:dev && pnpm test:release-update && pnpm check:release && pnpm check && pnpm build && pnpm import:spec && pnpm check:links"
},
"dependencies": {
"@astrojs/sitemap": "^3.7.3",
Expand Down
79 changes: 31 additions & 48 deletions public/mdbase-murmuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -800,7 +800,7 @@
particle.vx += accelerationX * step;
particle.vy += accelerationY * step;
const speed = Math.hypot(particle.vx, particle.vy);
const maximum = 2.5 + index % 5 * 0.08;
const maximum = 1.7 + index % 5 * 0.06;
if (speed > maximum) {
particle.vx = particle.vx / speed * maximum;
particle.vy = particle.vy / speed * maximum;
Expand Down Expand Up @@ -854,9 +854,9 @@

context.save();
context.strokeStyle = palette.muted;
context.globalAlpha = 0.88;
context.globalAlpha = 0.52;
context.lineWidth = 1;
context.setLineDash(edge.dashed ? [4, 5] : []);
context.setLineDash(edge.dashed ? [3, 5] : []);
context.beginPath();
context.moveTo(points[0].x, points[0].y);
points.slice(1).forEach((point) => context.lineTo(point.x, point.y));
Expand All @@ -867,16 +867,16 @@
const previousPoint = points[points.length - 2];
const angle = Math.atan2(finalPoint.y - previousPoint.y, finalPoint.x - previousPoint.x);
context.fillStyle = palette.muted;
context.globalAlpha = 0.95;
context.globalAlpha = 0.62;
context.beginPath();
context.moveTo(finalPoint.x, finalPoint.y);
context.lineTo(
finalPoint.x - Math.cos(angle - 0.48) * 6,
finalPoint.y - Math.sin(angle - 0.48) * 6
finalPoint.x - Math.cos(angle - 0.48) * 5,
finalPoint.y - Math.sin(angle - 0.48) * 5
);
context.lineTo(
finalPoint.x - Math.cos(angle + 0.48) * 6,
finalPoint.y - Math.sin(angle + 0.48) * 6
finalPoint.x - Math.cos(angle + 0.48) * 5,
finalPoint.y - Math.sin(angle + 0.48) * 5
);
context.closePath();
context.fill();
Expand Down Expand Up @@ -906,58 +906,39 @@
function drawNodeSurface(node, compact) {
const { x, y, w, h } = node.box;
context.save();

if (node.kind === "boundary") {
context.fillStyle = palette.surface;
context.strokeStyle = palette.line;
context.setLineDash([5, 5]);
} else if (node.kind === "authority") {
context.fillStyle = palette.surfaceSubtle;
context.strokeStyle = palette.accent;
context.setLineDash([]);
} else {
context.fillStyle =
node.kind === "service" ? palette.surfaceSubtle : palette.surface;
context.strokeStyle =
node.kind === "replica" ? palette.muted : palette.line;
context.setLineDash(node.kind === "replica" ? [4, 4] : []);
}

context.lineWidth = node.kind === "authority" ? 1.25 : 1;
context.fillStyle = palette.surface;
context.strokeStyle = node.kind === "authority" ? palette.muted : palette.lineSoft;
context.globalAlpha = node.kind === "authority" ? 0.78 : 0.64;
context.lineWidth = 1;
context.setLineDash(
node.kind === "boundary" || node.kind === "replica" ? [3, 5] : []
);
context.beginPath();
context.roundRect(x, y, w, h, compact ? 3 : 5);
context.rect(x, y, w, h);
context.fill();
context.stroke();
context.setLineDash([]);
context.restore();
}

function drawNodeLabel(node, compact) {
const { x, y, h } = node.box;
const { x, y } = node.box;
context.save();
const labelX = x + (compact ? 7 : 10);
const labelY = y + (compact ? 12 : 15);
const labelY = y + (compact ? 14 : 16);
context.textAlign = "left";
context.fillStyle =
node.kind === "authority" ? palette.accent : palette.ink;
context.font =
`600 ${compact ? 8.5 : 10}px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif`;
context.fillStyle = palette.ink;
context.globalAlpha = 0.78;
context.font = '17px "Atkinson Hyperlegible", sans-serif';
context.fillText(node.label, labelX, labelY);

if (node.detail && h > (compact ? 42 : 52)) {
context.fillStyle = palette.muted;
context.font =
`${compact ? 7.5 : 8.5}px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace`;
context.fillText(node.detail, labelX, labelY + (compact ? 10 : 12));
}
context.restore();
}

function drawNote(note, compact) {
context.save();
context.fillStyle = palette.muted;
context.font =
`600 ${compact ? 9 : 10}px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace`;
context.globalAlpha = 0.68;
context.font = '17px "Atkinson Hyperlegible", sans-serif';
context.textAlign = "center";
context.fillText(note.text, note.x, note.y);
context.restore();
Expand Down Expand Up @@ -986,22 +967,25 @@
}

function drawParticle(particle, compact) {
const baseSize = compact ? 2.5 : 3.25;
const baseSize = compact ? 1.8 : 2.25;
const size =
particle.role === "packet"
? baseSize * 1.5
? baseSize * 1.35
: baseSize * particle.scale;
const x = Math.round(particle.x - size / 2);
const y = Math.round(particle.y - size / 2);

context.save();
context.globalAlpha = particle.role === "packet" ? 0.82 : 0.62;
if (particle.role === "replica") {
context.strokeStyle = palette.replica;
context.lineWidth = 0.9;
context.strokeRect(x, y, Math.max(1.75, size), Math.max(1.75, size));
context.lineWidth = 0.8;
context.strokeRect(x, y, Math.max(1.5, size), Math.max(1.5, size));
} else {
context.fillStyle = particleFill(particle);
context.fillRect(x, y, Math.max(1.75, size), Math.max(1.75, size));
context.fillRect(x, y, Math.max(1.5, size), Math.max(1.5, size));
}
context.restore();
}

function draw(now) {
Expand All @@ -1021,7 +1005,6 @@
if (activeScene !== "intro") {
context.save();
context.globalAlpha = diagramAlpha;
diagram.edges.forEach((edge) => drawEdgeLabel(edge, compact));
diagram.nodes.forEach((node) => drawNodeLabel(node, compact));
diagram.notes.forEach((note) => drawNote(note, compact));
context.restore();
Expand Down
Loading