-
Notifications
You must be signed in to change notification settings - Fork 1
658 lines (633 loc) · 29.8 KB
/
Copy pathci.yml
File metadata and controls
658 lines (633 loc) · 29.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
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