Skip to content

perf: fix top 5 high-performance-go.md violations from a 3-agent audit #5435

perf: fix top 5 high-performance-go.md violations from a 3-agent audit

perf: fix top 5 high-performance-go.md violations from a 3-agent audit #5435

Workflow file for this run

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