Skip to content

fix(storage): prune redundant intermediate message_update frames (#1279) #47

fix(storage): prune redundant intermediate message_update frames (#1279)

fix(storage): prune redundant intermediate message_update frames (#1279) #47

name: Publish @openmaic packages
# Publishes the @openmaic/* package family (dsl, generation, storage, renderer, editor, importer) to npm.
#
# Scope is pinned to those five packages by name on purpose: the workspace also
# contains vendored forks (mathml2omml, pptxgenjs) whose names we do NOT own, so
# a publish must never touch them.
#
# THE ONLY RELEASE INPUT IS A VERSION BUMP THAT LANDED ON MAIN.
#
# A package's version in its manifest is the release intent, and merging that
# bump is what releases it. Which packages go out is decided by comparing those
# manifests against the registry, never by how the run was triggered. That keeps
# one source of truth, puts every release through pull request review and CI,
# and is the only shape a branch protection rule can actually guard.
#
# `@openmaic/<name>@<version>` tags are an OUTPUT of a release, written after a
# package lands on the registry. They are markers, not triggers: a tag cannot
# start a publish, because tags can be created by anyone with write access on
# any commit, including commits that never reached main.
#
# Triggers:
# - push to `main` touching one of the @openmaic/* package manifests
# (real publish; the normal path)
# - manual run via the Actions tab. Validation runs from any ref; unchecking
# `dry_run` performs a real publish and is therefore restricted to `main` by
# the `release` environment.
#
# REQUIRED REPOSITORY SETUP:
# - npm org `openmaic` created and owning the `@openmaic` scope
# - a GitHub Environment named `release` whose deployment branch rule allows
# ONLY `main`, holding NPM_TOKEN (an automation/granular token with publish
# rights to @openmaic/*, 2FA set to "auth only")
# - NPM_TOKEN must NOT also exist as a repository secret. A repository secret
# is readable by a workflow on any branch, which would let a branch-local
# edit of this file collect the token without the environment.
on:
push:
branches: [main]
paths:
- "packages/@openmaic/dsl/package.json"
- "packages/@openmaic/generation/package.json"
- "packages/@openmaic/storage/package.json"
- "packages/@openmaic/renderer/package.json"
- "packages/@openmaic/editor/package.json"
- "packages/@openmaic/importer/package.json"
workflow_dispatch:
inputs:
dry_run:
description: "Pack and validate only, do not publish"
type: boolean
default: true
# One publication at a time for the whole repository. Keying this on the ref
# would let two runs race for the same version, each deciding independently that
# the version is unpublished.
concurrency:
group: publish-openmaic
cancel-in-progress: false
jobs:
# A full commit SHA is the only immutable GitHub Action reference. Version
# comments beside each pin keep the selected release human-readable.
# SECURITY BOUNDARY: package installation, builds and packing happen only in
# `validate`, before any job can read NPM_TOKEN. `publish` receives immutable
# tarballs and verifies their digests before giving those exact files to npm.
# This structurally removes build-code tampering with the git index, validated
# bytes differing from published bytes, and build code rewriting enforcement
# scripts before the token-bearing step uses them: the token-bearing job runs
# neither install nor build code.
# Everything that does not need the token, so it can run from any ref and a
# dry run stays useful to contributors.
validate:
name: Validate release candidate
runs-on: ubuntu-latest
# This job runs `pnpm install` and the package builds, which execute
# third-party code. It gets no write scope and no git credential.
permissions:
contents: read
# storage's PostgreSQL contract suites skip themselves without a database.
# Publishing storage without ever running them would ship the one backend
# whose behaviour only a real PostgreSQL can confirm.
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: openmaic
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres -d openmaic"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
cache: pnpm
# ci.yml is a separate workflow, so its copy of the version check cannot
# stop a release that reaches here with CI red, skipped, or never run.
# This decides, against the registry, which packages may be published, and
# refuses to reuse or downgrade a published version.
- name: Validate package versions
run: node scripts/check-package-version-bumps.mjs --release
- run: pnpm install --frozen-lockfile
# Build in dependency order (dsl first); generation/storage/renderer/editor/importer
# resolve @openmaic/dsl through the workspace link.
- name: Build @openmaic packages
run: >-
pnpm -r
--filter "@openmaic/dsl"
--filter "@openmaic/generation"
--filter "@openmaic/storage"
--filter "@openmaic/renderer"
--filter "@openmaic/editor"
--filter "@openmaic/importer"
run build
# Some packages generate tracked, publishable files as part of `build`
# (renderer's fonts.css and its KaTeX font snapshot). If the committed
# copies are stale, the build rewrites them here and the registry would
# receive content that is not in the commit being released.
#
# Compare against GITHUB_SHA, not the index or HEAD. A bare `git diff`
# misses staged changes, while build code can create a commit and move
# HEAD. This check exists precisely because build code ran before it, so
# its reference point must be something that code cannot move. Disabling
# replacement objects also prevents a local replace ref from redirecting
# the commit comparison.
- name: Verify the build did not rewrite tracked files
run: |
set -euo pipefail
actual_head="$(git --no-replace-objects rev-parse HEAD)"
if [ "$actual_head" != "$GITHUB_SHA" ]; then
echo "::error::HEAD moved from $GITHUB_SHA to $actual_head during the build."
exit 1
fi
if ! git --no-replace-objects diff --quiet "$GITHUB_SHA"; then
echo "::error::The build modified tracked repository files."
echo "Regenerate them and commit the result before releasing."
git --no-replace-objects --no-pager diff --stat "$GITHUB_SHA"
exit 1
fi
echo "Tracked repository files match the commit being released."
# Seal the checked build before any repository test code runs. Uploading
# immediately after packing makes the publish input immutable before the
# tests or smoke test can write to their local copies. `needs: validate`
# still prevents publication unless every later validation step passes.
#
# Pack each package exactly once. pnpm resolves workspace:^ in the packed
# manifest, while the config form suppresses prepack and prepare (pnpm
# pack does not accept --ignore-scripts).
- name: Pack release artifacts and record digests
env:
RELEASE_ARTIFACTS: ${{ runner.temp }}/openmaic-package-tarballs
run: |
set -euo pipefail
mkdir "$RELEASE_ARTIFACTS"
for pkg in dsl generation storage renderer editor importer; do
echo "::group::@openmaic/$pkg"
( cd "packages/@openmaic/$pkg" \
&& pnpm pack --config.ignore-scripts=true --pack-destination "$RELEASE_ARTIFACTS" )
echo "::endgroup::"
done
node scripts/verify-package-artifacts.mjs --write "$RELEASE_ARTIFACTS"
- name: Verify freshly packed release artifacts
env:
RELEASE_ARTIFACTS: ${{ runner.temp }}/openmaic-package-tarballs
run: node scripts/verify-package-artifacts.mjs "$RELEASE_ARTIFACTS"
# actions/upload-artifact v4 artifacts cannot be modified after upload.
# Tests below keep using the same local tarballs, while `publish` receives
# this already-uploaded snapshot only after the whole validate job passes.
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: openmaic-package-tarballs-${{ github.sha }}
path: ${{ runner.temp }}/openmaic-package-tarballs
if-no-files-found: error
overwrite: true
retention-days: 7
# storage runs separately, under the json reporter, so that the run leaves
# a record of WHICH suites executed. STORAGE_PG_CONTRACT_REQUIRED only
# protects against a missing database: it is a `throw` inside the test
# modules, so it never fires if vitest stops collecting those modules at
# all, and what decides collection is storage's vitest.config.ts — an
# ignored publishable input, so excluding `*.pg.test.ts` there needs no
# version bump and leaves the run green. The assertion below is outside
# the tests for exactly that reason.
- name: Test & typecheck
env:
PG_CONTRACT_URL: postgresql://postgres:postgres@localhost:5432/openmaic
STORAGE_PG_CONTRACT_REQUIRED: "1"
STORAGE_VITEST_RESULTS: ${{ runner.temp }}/storage-vitest.json
STORAGE_PG_BASELINE: ${{ runner.temp }}/storage-pg-baseline.json
run: |
set -euo pipefail
pnpm --filter "@openmaic/dsl" \
--filter "@openmaic/generation" --filter "@openmaic/renderer" \
--filter "@openmaic/editor" \
--filter "@openmaic/importer" run test
node scripts/assert-pg-contract-suites.mjs --capture-baseline "$STORAGE_PG_BASELINE"
pnpm --filter "@openmaic/storage" exec \
vitest run --reporter=default --reporter=json \
--outputFile.json="$STORAGE_VITEST_RESULTS"
node scripts/assert-pg-contract-suites.mjs "$STORAGE_VITEST_RESULTS" \
--baseline "$STORAGE_PG_BASELINE"
pnpm --filter "@openmaic/dsl" --filter "@openmaic/generation" \
--filter "@openmaic/storage" --filter "@openmaic/renderer" \
--filter "@openmaic/editor" run typecheck
# KNOWN LIMITATION: this smoke test reads the writable local directory,
# not the uploaded snapshot. Because upload already happened, a test
# process can replace a poisoned local tarball with a valid one and make
# this pass while `publish` downloads the poisoned snapshot, whose own
# SHA256SUMS can still be internally consistent. This cannot be closed
# while packing, uploading, and testing share one job and filesystem;
# closing it requires a separate packing-only job that runs no tests.
- name: Smoke-test publishable tarballs
env:
RELEASE_ARTIFACTS: ${{ runner.temp }}/openmaic-package-tarballs
run: pnpm test:package-tarballs -- "$RELEASE_ARTIFACTS"
# The only job that can reach NPM_TOKEN. It is separate so the environment,
# and therefore the token, is never attached to a run that is merely
# validating.
publish:
name: Publish to npm
needs: validate
if: github.event_name == 'push' || inputs.dry_run == false
runs-on: ubuntu-latest
environment: release
outputs:
published_versions: ${{ steps.publish.outputs.published_versions }}
permissions:
actions: read # required to read this commit's CI conclusion
id-token: write # required for npm provenance
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
# The first-parent check needs real history.
fetch-depth: 0
# Publishing holds no repository credential. Writing release markers
# remains isolated in the `mark` job.
persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
registry-url: https://registry.npmjs.org
- name: Download validated package tarballs
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: openmaic-package-tarballs-${{ github.sha }}
path: ${{ runner.temp }}/openmaic-package-tarballs
- name: Verify validated package tarballs
run: node scripts/verify-package-artifacts.mjs "$RELEASE_ARTIFACTS"
env:
RELEASE_ARTIFACTS: ${{ runner.temp }}/openmaic-package-tarballs
# The environment's branch rule is the real boundary; this is defence in
# depth, and it catches the case the rule cannot see. First-parent only:
# plain reachability would also accept every intermediate commit of every
# branch merged with a merge commit, including states no reviewer ever
# saw, such as a bad tree reverted by the next commit in the same pull
# request.
- name: Require a commit on main's first-parent history
run: |
set -euo pipefail
git fetch --no-tags origin main
# Not `| grep -qx`: grep exits at the first match, git rev-list then
# dies of SIGPIPE, and pipefail turns a found commit into a failure
# as soon as main's history outgrows the pipe buffer.
if [ "$(git rev-list --first-parent origin/main | grep -cx "$GITHUB_SHA" || true)" -eq 0 ]; then
echo "::error::Refusing to publish $GITHUB_SHA: it is not on main's first-parent history."
echo "Release a commit that landed on main, not one merged underneath it."
exit 1
fi
echo "$GITHUB_SHA is on main's first-parent history."
# Being on main is not the same as having passed the gate. ci.yml runs
# concurrently with this workflow on a push to main and blocks nothing, so
# wait for CI on this exact commit and require it to be green.
- name: Require a green CI run for this commit
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
# Identify the run by workflow file and triggering event, not by a
# check-run display name: names are not unique, and any other workflow
# or app publishing a check with the same name could otherwise stand
# in for a red version gate.
query="repos/$GITHUB_REPOSITORY/actions/workflows/ci.yml/runs?head_sha=$GITHUB_SHA&event=push&per_page=100"
deadline=$((SECONDS + 1800))
while :; do
gh api "$query" > "$RUNNER_TEMP/ci-runs.json"
status="$(jq -r '([.workflow_runs[]] | sort_by(.run_started_at) | last // {}) | .status // "absent"' "$RUNNER_TEMP/ci-runs.json")"
conclusion="$(jq -r '([.workflow_runs[]] | sort_by(.run_started_at) | last // {}) | .conclusion // "none"' "$RUNNER_TEMP/ci-runs.json")"
if [ "$status" = "completed" ]; then
if [ "$conclusion" = "success" ]; then
echo "CI succeeded for $GITHUB_SHA."
break
fi
echo "::error::Refusing to publish $GITHUB_SHA: CI concluded \"$conclusion\"."
exit 1
fi
if [ "$SECONDS" -ge "$deadline" ]; then
echo "::error::No completed CI run for $GITHUB_SHA within the wait window (status: $status)."
echo "Release from a commit that CI has actually validated on a push to main."
exit 1
fi
echo "Waiting for CI on $GITHUB_SHA (status: $status)..."
sleep 30
done
# No install or build runs in this job. This whole-tree check is defence
# in depth after repository code has run. It exists precisely because
# code ran before it, so its reference point must be GITHUB_SHA, which
# that code cannot move, rather than HEAD, which it can. Disabling
# replacement objects prevents local indirection during the comparison.
- name: Verify the release checkout is unchanged
run: |
set -euo pipefail
actual_head="$(git --no-replace-objects rev-parse HEAD)"
if [ "$actual_head" != "$GITHUB_SHA" ]; then
echo "::error::HEAD moved from $GITHUB_SHA to $actual_head before publication."
exit 1
fi
if ! git --no-replace-objects diff --quiet "$GITHUB_SHA"; then
echo "::error::Tracked repository files differ from the release commit."
git --no-replace-objects --no-pager diff --stat "$GITHUB_SHA"
exit 1
fi
# Publish one package at a time. A single recursive publish that dies
# partway leaves earlier packages on the registry with nothing recording
# it; per-package publishing makes an ordinary re-run pick up exactly
# where it stopped, because a version already on the registry is no
# longer in the plan.
# The registry preflight is deliberately in this token-bearing step,
# immediately before the sequential loop, to minimize the race window.
- name: Registry preflight and publish validated tarballs
id: publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_CONFIG_PROVENANCE: "true"
RELEASE_PLAN_PATH: ${{ runner.temp }}/release-plan.json
RELEASE_ARTIFACTS: ${{ runner.temp }}/openmaic-package-tarballs
run: |
set -euo pipefail
published_versions=""
# Anchor the validated digests in this shell before any repository
# script runs with the npm token. A child process can alter files,
# but it cannot rewrite its parent shell's scalar.
trusted_digests="$(< "$RELEASE_ARTIFACTS/SHA256SUMS")"
node scripts/verify-package-artifacts.mjs "$RELEASE_ARTIFACTS"
node scripts/check-package-version-bumps.mjs --release
for pkg in dsl generation storage renderer editor importer; do
name="@openmaic/$pkg"
version="$(node -p "require('./packages/@openmaic/$pkg/package.json').version")"
planned="$(node -e 'const plan = require(process.env.RELEASE_PLAN_PATH);
const [name, version] = process.argv.slice(1);
process.stdout.write(plan.some((e) => e.package === name && e.version === version) ? "yes" : "no");' \
"$name" "$version")"
if [ "$planned" != "yes" ]; then
echo "Skipping $name@$version: already on the registry."
continue
fi
filename="openmaic-$pkg-$version.tgz"
tarball="$RELEASE_ARTIFACTS/$filename"
expected_digest="$(
printf '%s\n' "$trusted_digests" | awk -v filename="$filename" \
'$2 == filename { print $1 }'
)"
if [ -z "$expected_digest" ]; then
echo "::error::$filename has no validated SHA-256 digest."
exit 1
fi
if ! actual_digest="$(sha256sum "$tarball" | awk '{print $1}')"; then
echo "::error::Could not re-verify $filename immediately before publication."
exit 1
fi
if [ "$actual_digest" != "$expected_digest" ]; then
echo "::error::$filename failed its pre-publish SHA-256 re-check."
exit 1
fi
echo "Re-verified $filename immediately before publication."
# npm accepts a tarball package-spec and attaches provenance to the
# supplied bytes. --ignore-scripts also prevents publish lifecycle
# scripts from executing in the token-bearing job.
npm publish "$tarball" --access public --provenance --ignore-scripts
echo "Published $name@$version."
published_versions="${published_versions:+$published_versions,}$name@$version"
done
printf 'published_versions=%s\n' "$published_versions" >> "$GITHUB_OUTPUT"
# Writing the release markers is the only thing that needs a git credential,
# so it is the only job that holds one, and it installs nothing and builds
# nothing. It reconciles rather than only marking this run's publishes: a
# marker that failed to write previously is retried here, which the release
# plan alone could never do, because a version already on the registry is
# excluded from it.
#
# Tags are created through the GitHub API with GITHUB_TOKEN, and GitHub does
# not start new workflow runs from GITHUB_TOKEN writes.
mark:
name: Mark released versions
needs: publish
runs-on: ubuntu-latest
permissions:
contents: write # required to push the release marker tags
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
- name: Write missing release markers
env:
GIT_COMMIT: ${{ github.sha }}
GH_TOKEN: ${{ github.token }}
PUBLISHED_VERSIONS: ${{ needs.publish.outputs.published_versions }}
run: |
set -euo pipefail
for pkg in dsl generation storage renderer editor importer; do
name="@openmaic/$pkg"
version="$(node -p "require('./packages/@openmaic/$pkg/package.json').version")"
tag="$name@$version"
if [[ ",$PUBLISHED_VERSIONS," == *",$tag,"* ]]; then
echo "Published $tag in this run; registry propagation is not required."
elif ! npm view "$name@$version" version --registry https://registry.npmjs.org >/dev/null 2>&1; then
echo "$tag is not on the registry; nothing to mark."
continue
fi
if git rev-parse -q --verify "refs/tags/$tag" >/dev/null; then
existing="$(git rev-parse "refs/tags/$tag^{commit}")"
if [ "$existing" = "$GIT_COMMIT" ]; then
echo "Release marker $tag already points at $GIT_COMMIT."
else
echo "Release marker $tag already exists at $existing; leaving it."
fi
continue
fi
if gh api --method POST "repos/$GITHUB_REPOSITORY/git/refs" \
-f ref="refs/tags/$tag" -f sha="$GIT_COMMIT" >/dev/null; then
echo "Marked $tag at $GIT_COMMIT."
else
echo "::warning::Could not write the release marker $tag; $name@$version is published."
fi
done