fix(storage): prune redundant intermediate message_update frames (#1279) #47
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |