perf: fix top 5 high-performance-go.md violations from a 3-agent audit #5435
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: CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| merge_group: | |
| branches: [main] | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| jobs: | |
| zizmor: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| security-events: write | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: Install zizmor | |
| run: pip install zizmor==1.6.0 | |
| - name: Run zizmor | |
| run: zizmor --format sarif . > results.sarif | |
| - name: Upload SARIF | |
| if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository | |
| uses: github/codeql-action/upload-sarif@5c8a8a642e79153f5d047b10ec1cba1d1cc65699 # v3 | |
| with: | |
| sarif_file: results.sarif | |
| # `zizmor --format sarif` always exits 0 so the SARIF reaches | |
| # the upload step. Re-run in text mode after the upload to | |
| # exit 13 on any finding, which fails the job and blocks the | |
| # PR. Use if: always() so a fork PR (where the upload step is | |
| # skipped) still gates on findings. | |
| - name: Fail on findings | |
| if: always() | |
| run: zizmor . | |
| lint: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 | |
| with: | |
| # tools/go.mod, not go.mod: dev tools live in the alternate | |
| # modfile so their go floor (vhs needs >= 1.25.8) stays out | |
| # of the consumer-facing go.mod. Toolchain selection ignores | |
| # -modfile and setup-go pins GOTOOLCHAIN=local, so the | |
| # installed toolchain must already satisfy the tools floor. | |
| # (setup-go only parses files NAMED go.mod or go.work — | |
| # hence tools/go.mod rather than a root go.tools.mod.) | |
| go-version-file: tools/go.mod | |
| # setup-go keys its module cache on the root go.sum only; | |
| # golangci-lint builds from tools/go.sum and type-checks the | |
| # repo against go.sum, so hash both or the tool's dependency | |
| # tree re-downloads on every run. | |
| cache-dependency-path: | | |
| go.sum | |
| tools/go.sum | |
| - run: go tool -modfile=tools/go.mod golangci-lint run | |
| # Tracked manifests must stay pinned at 0.0.0-dev between releases; | |
| # the release workflow rewrites them with `mdsmith-release stamp` | |
| # before publishing each channel. A hand edit on main would ship a | |
| # wrong version on the next tag, so fail fast here. | |
| version-guard: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 | |
| with: | |
| go-version-file: go.mod | |
| - run: go run ./cmd/mdsmith-release check | |
| # Enforce the release pipeline's secret-gating invariant across | |
| # every workflow under .github/workflows/: in release.yml each job | |
| # that declares `environment: release` must list the `gate` job in | |
| # `needs:` (literal env names, matched case-insensitively the way | |
| # GitHub matches them) and must not carry an approval-bypassing | |
| # `if:` (always() / failure() / cancelled()); `gate` must be the | |
| # lone job on `release-approval`; and no other workflow may target | |
| # either environment. The `release` environment carries no required | |
| # reviewer (so a single approval on the `release-approval` | |
| # environment covers the whole run); the `needs: gate` edge is then | |
| # the only thing keeping a credential job from reading a publisher | |
| # secret — VSCE_PAT, OVSX_PAT, WINGET_PR_TOKEN, the Homebrew/Scoop | |
| # dispatch tokens — or minting a publishing OIDC token before that | |
| # approval lands. Without this guard the property would be | |
| # convention; a future ungated `environment: release` job could | |
| # disclose a secret with no approval. Runtime logic lives in | |
| # mdsmith-release per docs/development/release-tooling.md. | |
| release-gate-guard: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 | |
| with: | |
| go-version-file: go.mod | |
| - run: go run ./cmd/mdsmith-release check-release-gates | |
| # Post-publication coverage is an invariant too: every directly | |
| # consumable channel keeps a smoke-test matrix entry in | |
| # release.yml, so an install path broken at the source (the | |
| # v0.40.0 go.mod replace directive broke `go install m@version` | |
| # and nothing in the pipeline noticed) fails the release run | |
| # instead of a user. | |
| - run: go run ./cmd/mdsmith-release check-release-smoke | |
| # Product messaging surfaces (READMEs, package manifests, hugo | |
| # config, hero frontmatter, plugin manifests) are derived from | |
| # docs/brand/messaging.md via `mdsmith-release sync-messaging`. | |
| # A hand edit to any tracked field would be reverted on the next | |
| # sync, so fail fast on drift. See plan/210. | |
| messaging-drift: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 | |
| with: | |
| go-version-file: go.mod | |
| - run: go run ./cmd/mdsmith-release sync-messaging --check | |
| # The website install picker reads website/data/channels.yaml, | |
| # generated from docs/development/release-channels/*.md via | |
| # `mdsmith-release sync-channels`. A hand edit to the data file | |
| # would be reverted on the next sync, so fail fast on drift. | |
| # See plan/222. | |
| channels-drift: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 | |
| with: | |
| go-version-file: go.mod | |
| - run: go run ./cmd/mdsmith-release sync-channels --check | |
| # Block any future PR from adding install-time or publish-time | |
| # lifecycle scripts to the published npm packages. The shai-hulud | |
| # / TanStack worm class (npm supply-chain compromise, 2025-2026) | |
| # propagates by inserting `preinstall` / `postinstall` hooks into | |
| # otherwise innocuous packages so the hook runs on every | |
| # `npm install`. The same family of attacks can target the | |
| # publishing side via `prepublishOnly` / `prepack` etc., which | |
| # would run in the release workflow with the OIDC publish token | |
| # in scope. mdsmith's npm shim (npm/mdsmith/bin/mdsmith.js) | |
| # deliberately runs zero lifecycle code; this guard enforces the | |
| # property in CI rather than relying on convention. The VS Code | |
| # extension is published via `vsce package --no-dependencies` so | |
| # it doesn't ship lifecycle hooks to end users, but the manifest | |
| # is checked here too as defense in depth. | |
| npm-lifecycle-guard: | |
| runs-on: ubuntu-latest | |
| # Inherits workflow-level `permissions: contents: read`, but | |
| # an explicit per-job block documents the minimum scope so a | |
| # future workflow-level change cannot silently widen this | |
| # job's privileges. The job only reads checked-out manifests | |
| # and never calls the GitHub API. | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: Reject lifecycle scripts in published manifests | |
| run: | | |
| # `set -euo pipefail` so a parse failure in jq (e.g. a | |
| # malformed package.json a malicious PR slips in) fails | |
| # the step rather than being silently swallowed. Without | |
| # `set -e`, a non-zero jq exit on one manifest would not | |
| # propagate out of the loop and `status` would stay 0. | |
| set -euo pipefail | |
| # Hooks npm executes on `npm install`, `npm pack`, or | |
| # `npm publish` — any of these would let a malicious | |
| # package version (or a compromised upstream dep merged | |
| # into a manifest) run code at install or publish time. | |
| # Install-time: preinstall, install, postinstall, | |
| # preuninstall, uninstall, postuninstall, | |
| # preprepare, prepare, postprepare | |
| # Publish/pack-time: prepublish (legacy), prepublishOnly, | |
| # prepack, postpack, publish, postpublish | |
| # | |
| # Note on `prepublish`: it is the deprecated pre-npm@4 | |
| # hybrid hook that historically ran on BOTH `npm install` | |
| # (local installs) AND `npm publish`. Modern npm (>=7) | |
| # only fires it on publish-from-source-checkout flows, but | |
| # it still executes in older clients still in use across | |
| # CI images. Since the threat model is install-time code | |
| # execution, `prepublish` stays banned despite the | |
| # potential for false positives in repos that use it for | |
| # benign build steps. mdsmith's published packages do not | |
| # declare any `prepublish` hook; new ones land in the doc | |
| # at docs/security/2026-05-12-supply-chain-hardening.md | |
| # before this list is relaxed. | |
| # The banned set is passed as a jq array. The select | |
| # expression binds the entry's key to $k first so the | |
| # `$banned | index($k)` pipeline's right-hand side | |
| # evaluates against the array (not the entry record). | |
| # `index` returns the array offset of $k or null, which | |
| # `select` treats as a truthy/falsy filter. This does | |
| # exact-string membership, so a future hook with a regex | |
| # metacharacter in its name (or a typo in the banned | |
| # list) cannot silently shift semantics the way a grep | |
| # alternation could. `index` is portable across jq 1.5+ | |
| # (older runners) where `IN` is unavailable. | |
| banned_json='["preinstall","install","postinstall","preuninstall","uninstall","postuninstall","preprepare","prepare","postprepare","prepublish","prepublishOnly","prepack","postpack","publish","postpublish"]' | |
| status=0 | |
| for manifest in npm/mdsmith/package.json editors/vscode/package.json; do | |
| if [ ! -f "$manifest" ]; then | |
| echo "missing $manifest" >&2 | |
| status=1 | |
| continue | |
| fi | |
| # jq emits one `key=value` line per .scripts entry whose | |
| # KEY is in the banned set. `.[]?` is the optional | |
| # iterator: yields nothing on a missing/empty .scripts | |
| # so manifests that legitimately declare no scripts | |
| # pass cleanly. No grep / no regex — the | |
| # `.key as $k | $banned | index($k)` form is jq's | |
| # literal set-membership test (returns the banned-array | |
| # index of $k, or null on miss). | |
| hits=$(jq -r --argjson banned "$banned_json" \ | |
| '.scripts // {} | to_entries | .[]? | select(.key as $k | $banned | index($k)) | "\(.key)=\(.value)"' \ | |
| "$manifest") | |
| if [ -n "$hits" ]; then | |
| echo "$manifest declares forbidden lifecycle script(s):" >&2 | |
| echo "$hits" >&2 | |
| echo "If a hook is genuinely required, update docs/security/" >&2 | |
| echo "2026-05-12-supply-chain-hardening.md before allowlisting." >&2 | |
| status=1 | |
| fi | |
| done | |
| exit $status | |
| mdsmith: | |
| uses: ./.github/workflows/mdsmith-check.yml | |
| skill-eval: | |
| uses: ./.github/workflows/skill-eval.yml | |
| mdsmith-fixed-version: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - uses: ./.github/actions/setup-mdsmith-pinned-version | |
| - run: mdsmith check . | |
| demo: | |
| uses: ./.github/workflows/record-demo.yml | |
| with: | |
| # MP4 skips the slow GIF palette-quantization encoding pass. | |
| # The published GIF is produced separately by demo.yml. | |
| output_format: mp4 | |
| lsp-bench: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 | |
| with: | |
| go-version-file: go.mod | |
| # Plan 121: enforce p95 squiggle-update budget | |
| # (150 ms / 1k lines, 500 ms / 5k lines). | |
| - run: go test -run=^$ -bench=. -benchtime=20x ./internal/lsp/... | |
| check-bench: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 | |
| with: | |
| go-version-file: go.mod | |
| # Plan 175: tiered `mdsmith check` p95 budgets — Small | |
| # (60 files / 2 s, per-file overhead) and Large (600 files | |
| # / 12 s, superlinear scaling), full production rule set. | |
| - run: go test -run=^$ -bench=. -benchtime=20x ./internal/engine/... | |
| markdown-bench: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 | |
| with: | |
| go-version-file: go.mod | |
| # Plan 175: public pkg/markdown parse p95 budgets — Small | |
| # (~150-line doc / 10 ms, per-parse overhead) and Large | |
| # (~3000-line doc / 100 ms, parse scaling), canonical parser | |
| # including the <?...?> processing-instruction block. | |
| - run: go test -run=^$ -bench=. -benchtime=20x ./pkg/markdown/... | |
| cuelite-bench: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 | |
| with: | |
| go-version-file: go.mod | |
| # Plan 240: the differential oracle left with the cuelang.org | |
| # dependency, so the cuelite benchmarks are engine-only and the | |
| # old CUE-relative factor gate is replaced by an absolute | |
| # allocs/op guard (TestValidateAllocBudget in bench_test.go, | |
| # asserted here verbosely so the numbers land in the job log). | |
| - run: go test -run=^$ -bench=. -benchtime=20x ./cue/cuelite/... | |
| - run: go test -v -run=TestValidateAllocBudget ./cue/cuelite/ | |
| # Build-tag A/B job for the vendored goldmark fork (plan 197+198). | |
| # pkg/goldmark/ is part of the main module — it was a nested module | |
| # wired via a go.mod replace directive until that broke `go install | |
| # m@version`, which rejects modules with replace directives — so the | |
| # root `test` job's `go test ./...` already covers the default | |
| # (arena) path. This job re-runs the fork's tests with the | |
| # `goldmark_upstream` tag so newArenaForParse returns nil and every | |
| # allocator falls back to the upstream constructor. Both paths must | |
| # stay green — the equivalence harness in equivalence_test.go also | |
| # diffs them in-process via parser.WithNoArena(), but this CI axis | |
| # catches a divergence that only surfaces under one build tag. | |
| # Coverage is intentionally NOT uploaded to Codecov: the in-tree | |
| # fork is `ignore:`-d in codecov.yml because its drift gate is the | |
| # equivalence harness, not the project-wide coverage gate. | |
| goldmark-fork-test: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 | |
| with: | |
| go-version-file: go.mod | |
| - name: Run fork tests (upstream A/B path) | |
| run: go test -tags goldmark_upstream ./pkg/goldmark/... | |
| bench-fragments: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 | |
| with: | |
| go-version-file: go.mod | |
| # Plan 175: doc-number drift gate. Regenerate the benchmark | |
| # fragments from the committed JSON and re-run `mdsmith fix`; | |
| # any diff means a fragment was hand-edited or a number went | |
| # stale. The cross-tool table can only change via run.sh | |
| # promoting fresh JSON into docs/research/benchmarks/data/. | |
| - run: | | |
| python3 docs/research/benchmarks/gen_fragments.py \ | |
| docs/research/benchmarks/data docs/research/benchmarks | |
| go run ./cmd/mdsmith fix \ | |
| docs/research/benchmarks/results.fragment.md \ | |
| docs/research/benchmarks/headline.fragment.md | |
| go run ./cmd/mdsmith fix . | |
| git diff --exit-code -- \ | |
| docs/research/benchmarks/results.fragment.md \ | |
| docs/research/benchmarks/headline.fragment.md \ | |
| docs/features/performance.md \ | |
| docs/background/markdown-linters.md \ | |
| docs/research/benchmarks/README.md | |
| vscode-extension: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| id-token: write | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - uses: ./.github/actions/setup-bun | |
| - name: Install extension dependencies | |
| working-directory: editors/vscode | |
| # `--ignore-scripts` blocks postinstall / preinstall hooks from | |
| # every dep in the resolved tree. See release.yml for rationale; | |
| # the goal is to neutralize the install-time code execution | |
| # path that the shai-hulud / TanStack-class worm relies on. | |
| run: bun install --frozen-lockfile --ignore-scripts | |
| - name: TypeScript typecheck | |
| working-directory: editors/vscode | |
| # Bun's bundler/transpiler tolerates type errors, so `bun | |
| # test` and `bun run build.ts` both happily accept code | |
| # that `tsc` would reject. Run an explicit `tsc --noEmit` | |
| # so a TS regression fails CI before it ships in the .vsix. | |
| run: bunx tsc --noEmit | |
| - name: Run extension unit tests with coverage | |
| working-directory: editors/vscode | |
| # `text` keeps the human-readable summary in the job log; | |
| # `lcov` writes coverage/lcov.info for the codecov upload. | |
| run: bun test --coverage --coverage-reporter=text --coverage-reporter=lcov | |
| - name: Upload TypeScript coverage to Codecov | |
| if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository | |
| uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 | |
| with: | |
| use_oidc: true | |
| files: editors/vscode/coverage/lcov.info | |
| flags: typescript | |
| fail_ci_if_error: false | |
| - name: Compile extension | |
| working-directory: editors/vscode | |
| run: bun run build.ts --production | |
| - name: Package .vsix | |
| working-directory: editors/vscode | |
| run: bunx --bun @vscode/vsce package --no-dependencies --out mdsmith.vsix | |
| - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | |
| with: | |
| name: mdsmith-vscode-extension | |
| path: editors/vscode/mdsmith.vsix | |
| vscode-e2e: | |
| # Proves the @mdsmith/cli package and the VS Code extension work | |
| # together: build.ts stages the canonical shim + a slot for every | |
| # platform, `vsce package` ships them all, and the bundled shim | |
| # resolves and runs this host's real binary — including the | |
| # regression where an empty mdsmith.path crashed the LSP client | |
| # with command:"". Heavy (real go build + vsce), so it is its own | |
| # job and gated behind MDSMITH_VSIX_E2E. | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - uses: ./.github/actions/setup-bun | |
| - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 | |
| with: | |
| go-version-file: go.mod | |
| - name: Install extension dependencies | |
| working-directory: editors/vscode | |
| run: bun install --frozen-lockfile --ignore-scripts | |
| - name: Build the .vsix and run the cross-platform e2e | |
| working-directory: editors/vscode | |
| env: | |
| MDSMITH_VSIX_E2E: "1" | |
| run: bun test ./src/e2e/vsix.e2e.ts | |
| # Plan 217: the Obsidian plugin. Unlike the VS Code extension (which | |
| # bundles a native binary per platform), the plugin ships ONE WASM | |
| # artifact that runs on desktop and mobile alike. This job builds the | |
| # engine to WebAssembly, typechecks + tests the plugin, builds it, and | |
| # zips dist/ into mdsmith-obsidian-<version>.zip — the same artifact | |
| # the release pipeline attaches to the GitHub Release. | |
| obsidian-plugin: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - uses: ./.github/actions/setup-bun | |
| - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 | |
| with: | |
| go-version-file: go.mod | |
| - name: Install plugin dependencies | |
| working-directory: editors/obsidian | |
| # `--ignore-scripts` neutralizes install-time code execution | |
| # from every dep in the resolved tree (see release.yml). | |
| run: bun install --frozen-lockfile --ignore-scripts | |
| - name: TypeScript typecheck | |
| working-directory: editors/obsidian | |
| # Bun's bundler tolerates type errors; run `tsc --noEmit` so a | |
| # TS regression fails CI before it ships in the zip. | |
| run: bunx tsc --noEmit | |
| - name: Run plugin unit tests | |
| working-directory: editors/obsidian | |
| # The runtime, workspace, diagnostics, settings, and build tests | |
| # build the WASM artifact on demand via the Go toolchain above. | |
| run: bun test --preload ./src/test-setup.ts | |
| - name: Build the engine to WebAssembly | |
| # build.ts can compile the WASM itself, but building it here | |
| # (and pointing build.ts at the output) keeps the size-budget | |
| # flags in one place — cmd/mdsmith-wasm/build.sh. | |
| run: bash cmd/mdsmith-wasm/build.sh | |
| - name: Build the plugin | |
| working-directory: editors/obsidian | |
| env: | |
| MDSMITH_OBSIDIAN_WASM_DIR: ${{ github.workspace }}/cmd/mdsmith-wasm/dist | |
| run: bun run build.ts --production | |
| - name: Package the release zip | |
| # Runtime logic lives in mdsmith-release per | |
| # docs/development/release-tooling.md. package-obsidian reads the | |
| # version from dist/manifest.json and writes | |
| # mdsmith-obsidian-<version>.zip with the five files Obsidian | |
| # loads, flat at the top level, via archive/zip (no `zip` | |
| # binary). outDir is editors/obsidian so the upload glob matches. | |
| run: go run ./cmd/mdsmith-release package-obsidian editors/obsidian/dist editors/obsidian | |
| - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | |
| with: | |
| name: mdsmith-obsidian-plugin | |
| path: editors/obsidian/mdsmith-obsidian-*.zip | |
| # Plan 215: the mdsmith engine compiled to WebAssembly. The smoke | |
| # test (cmd/mdsmith-wasm/smoke_test.go) builds the artifact and runs | |
| # it through a Node harness, asserting `session.check` matches the | |
| # native engine on an in-memory fixture. It SKIPS when node is absent, | |
| # so it must run in a job that installs node or it never gates. The | |
| # same `go test` run also enforces the artifact size budget | |
| # (size_test.go, a regression guard now at ~11.2 MB raw / ~2.8 MB | |
| # gzipped after cuelang.org/go left the graph) and the Go↔JS method-set | |
| # parity (methods_test.go). tinygo is built in the separate tinygo-wasm | |
| # job below, which is now enforcing (plan 247). | |
| wasm: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 | |
| with: | |
| go-version-file: go.mod | |
| - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: "22" | |
| # golangci-lint (the lint job) runs under the native GOOS, so it | |
| # skips the //go:build js && wasm bridge entirely. go vet does | |
| # cross-compile, so run it here to give the bridge at least | |
| # vet-level static analysis. | |
| - name: Vet the WASM bridge | |
| run: GOOS=js GOARCH=wasm go vet ./cmd/mdsmith-wasm/... | |
| - name: WASM smoke test, size budget, and method-set parity | |
| run: go test ./cmd/mdsmith-wasm/... | |
| # Plan 247: tinygo wasm build — enforcing. The os.Chmod, os.SameFile, | |
| # and os.Symlink/filepath.EvalSymlinks calls that previously blocked | |
| # this build are now behind build-tagged seams, so the job gates PRs. | |
| # TestTinyGoWASMArtifactSizeBudget asserts the artifact is at or under | |
| # the plan-215/247 budget of 8 MiB. | |
| tinygo-wasm: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 | |
| with: | |
| go-version-file: go.mod | |
| - name: Install tinygo (checksum-pinned .deb) | |
| env: | |
| TINYGO_VERSION: "0.39.0" | |
| TINYGO_SHA256: "775f15974e35059c8f3a141266bd9d293b5d556a3e44d5e6356c5602e9f386ab" | |
| run: | | |
| url="https://github.com/tinygo-org/tinygo/releases/download/v${TINYGO_VERSION}/tinygo_${TINYGO_VERSION}_amd64.deb" | |
| curl -fsSL -o /tmp/tinygo.deb "$url" | |
| echo "${TINYGO_SHA256} /tmp/tinygo.deb" | sha256sum -c - | |
| sudo dpkg -i /tmp/tinygo.deb | |
| tinygo version | |
| - name: Build the engine with tinygo | |
| run: tinygo build -target wasm -o /tmp/mdsmith-tiny.wasm ./cmd/mdsmith-wasm | |
| - name: tinygo size budget | |
| run: go test -run TestTinyGoWASMArtifactSizeBudget ./cmd/mdsmith-wasm/... | |
| test: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| id-token: write | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 | |
| with: | |
| go-version-file: go.mod | |
| - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | |
| with: | |
| python-version: "3.12" | |
| - name: Install Python build tooling | |
| # internal/release/buildwheels_test.go exercises BuildWheels | |
| # which orchestrates `python -m build`, `python -m wheel`, | |
| # and the hatchling build backend. Without these the test | |
| # silently skips, defeating the point of having an | |
| # integration test in CI. | |
| run: python -m pip install --upgrade build wheel hatchling | |
| - name: Install mdsmith | |
| run: go install ./cmd/mdsmith | |
| - name: Run tests with coverage | |
| # -json streams structured events into `mdsmith-release | |
| # test-summary`, which tallies the unit/integration/e2e test | |
| # counts into the job summary and echoes a terse (non -v) log | |
| # to the console. set -o pipefail keeps a `go test` failure | |
| # fatal despite the pipe (it is also the GitHub default shell's | |
| # behaviour). -coverprofile still writes unit.cov for the | |
| # Codecov upload below. | |
| run: | | |
| set -o pipefail | |
| mkdir -p "$GITHUB_WORKSPACE/e2e-cover" | |
| E2E_COVERDIR="$GITHUB_WORKSPACE/e2e-cover" \ | |
| go test -json -covermode=atomic -coverprofile=unit.cov ./... \ | |
| | go run ./cmd/mdsmith-release test-summary | |
| - name: Merge coverage profiles | |
| run: | | |
| e2e_profile="$GITHUB_WORKSPACE/e2e-cover/e2e_coverage.txt" | |
| if [ ! -f "$e2e_profile" ]; then | |
| echo "e2e_coverage.txt not found — cmd/mdsmith coverage will be missing" >&2 | |
| exit 1 | |
| fi | |
| e2e_lines=$(grep -c 'cmd/mdsmith/' "$e2e_profile" || true) | |
| if [ "$e2e_lines" -eq 0 ]; then | |
| echo "e2e profile contains no cmd/mdsmith/ coverage lines" >&2 | |
| exit 1 | |
| fi | |
| # Sum per-block hit counts across the per-package unit | |
| # profile and the e2e subprocess-binary profile. A plain | |
| # concatenation left duplicate cmd/mdsmith blocks and | |
| # Codecov took the last (often zero) one, so in-process | |
| # coverage of cmd/mdsmith was clobbered by the e2e | |
| # profile's zeros. merge-coverage also validates the mode | |
| # lines agree. | |
| go run ./cmd/mdsmith-release merge-coverage \ | |
| -o merged.cov unit.cov "$e2e_profile" | |
| - name: Coverage summary | |
| run: | | |
| total=$(go tool cover -func=merged.cov | tail -1) | |
| { | |
| echo '## Test Coverage' | |
| echo '```' | |
| echo "$total" | |
| echo '```' | |
| echo '' | |
| echo '<details><summary>Per-package breakdown</summary>' | |
| echo '' | |
| echo '```' | |
| go tool cover -func=merged.cov | |
| echo '```' | |
| echo '</details>' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Upload Go coverage to Codecov | |
| if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository | |
| uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 | |
| with: | |
| use_oidc: true | |
| files: merged.cov | |
| flags: go | |
| fail_ci_if_error: false |