From 37deef499e84977f07d1f25b0b36efc921a55ff3 Mon Sep 17 00:00:00 2001 From: Pavlo Alkhimov <631749+pavlo-alkhimov@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:08:10 +0200 Subject: [PATCH 1/4] PlantUML-like shape metrics: tighter padding, line height, font size Upstream sizes a shape as text + 45px per axis with a 1.5 markdown line height; PlantUML uses text + 20px and ~1.36. The result is that most of a D2 box is padding, which reads as a small font in a large rectangle. Three constants close the gap, leaving diagram sources untouched: lib/shape/shape.go defaultPadding 40. -> 15. lib/textmeasure/markdown.go MarkdownLineHeight 1.5 -> 1.36 d2renderers/d2fonts/... FONT_SIZE_M 16 -> 14 Every shape derives its padding from defaultPadding as a multiple or a fraction, so one constant rescales them all consistently. Box heights now land within 1px of PlantUML at one, two and three lines. Line height drives both measurement and rendering, so baselines and boxes stay in agreement. Version is set to v0.9.0-plantuml-metrics.1 so it cannot be mistaken for an upstream release. Adds a workflow that cross-compiles Windows, Linux and macOS binaries and re-asserts the three constants after every build, and PLANTUML-METRICS.md documenting the rationale and how to rebase. Co-Authored-By: Claude Opus 5 --- .github/workflows/plantuml-metrics-build.yml | 86 ++++++++++++++++++++ PLANTUML-METRICS.md | 84 +++++++++++++++++++ d2renderers/d2fonts/d2fonts_common.go | 2 +- lib/shape/shape.go | 2 +- lib/textmeasure/markdown.go | 2 +- lib/version/version.go | 2 +- 6 files changed, 174 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/plantuml-metrics-build.yml create mode 100644 PLANTUML-METRICS.md diff --git a/.github/workflows/plantuml-metrics-build.yml b/.github/workflows/plantuml-metrics-build.yml new file mode 100644 index 0000000000..543d7a2f11 --- /dev/null +++ b/.github/workflows/plantuml-metrics-build.yml @@ -0,0 +1,86 @@ +name: plantuml-metrics build + +# Cross-compiles the patched d2 so no Go toolchain is needed on the target +# machine. Artifacts appear under the run's "Summary" page; a tag starting +# with "plantuml-metrics-v" additionally publishes a release. + +on: + push: + branches: [plantuml-metrics] + tags: ['plantuml-metrics-v*'] + workflow_dispatch: + +permissions: + contents: write + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - {goos: windows, goarch: amd64, ext: .exe} + - {goos: windows, goarch: arm64, ext: .exe} + - {goos: linux, goarch: amd64, ext: ''} + - {goos: darwin, goarch: arm64, ext: ''} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + # The version is also the default in lib/version/version.go, so a plain + # `go build` reports it too. Set here as well for tagged builds. + - name: Resolve version + id: v + run: | + if [ "${GITHUB_REF_TYPE}" = "tag" ]; then + echo "version=${GITHUB_REF_NAME#plantuml-metrics-}" >> "$GITHUB_OUTPUT" + else + echo "version=$(sed -n 's/.*Version = "\(.*\)".*/\1/p' lib/version/version.go)+$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" + fi + + - name: Build + env: + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} + CGO_ENABLED: '0' + run: | + out="d2-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.ext }}" + go build -trimpath \ + -ldflags "-s -w -X github.com/d2lang/d2/lib/version.Version=${{ steps.v.outputs.version }}" \ + -o "$out" . + ls -l "$out" + + # Guards the whole point of the fork: if these three constants ever come + # back from an upstream rebase, the build should fail loudly. + - name: Verify the patch is present + run: | + set -e + grep -q 'defaultPadding = 15\.' lib/shape/shape.go + grep -q 'MarkdownLineHeight = 1\.36' lib/textmeasure/markdown.go + grep -q 'FONT_SIZE_M = 14' d2renderers/d2fonts/d2fonts_common.go + echo "all three constants patched" + + - name: Check the layout engines are bundled + if: matrix.goos == 'linux' && matrix.goarch == 'amd64' + run: | + ./d2-linux-amd64 --version + ./d2-linux-amd64 layout | tee /tmp/layouts + grep -q tala /tmp/layouts + grep -q elk /tmp/layouts + + - uses: actions/upload-artifact@v4 + with: + name: d2-${{ matrix.goos }}-${{ matrix.goarch }} + path: d2-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.ext }} + if-no-files-found: error + + - name: Publish release on tag + if: github.ref_type == 'tag' + uses: softprops/action-gh-release@v2 + with: + files: d2-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.ext }} diff --git a/PLANTUML-METRICS.md b/PLANTUML-METRICS.md new file mode 100644 index 0000000000..e6f79298e0 --- /dev/null +++ b/PLANTUML-METRICS.md @@ -0,0 +1,84 @@ +# PlantUML metrics + +A four-line patch on top of D2 `v0.9.0` that makes shapes as tight around +their text as PlantUML's are. Nothing else is changed: no new syntax, no +options, and diagrams stay exactly as written. + +Upstream D2 sizes a shape as `text + 45px` on each axis and sets the +markdown line height to 1.5 times the font. PlantUML uses `text + 20px` and +about 1.36. The result is that a stock D2 box devotes roughly two thirds of +its height to padding, which reads as a small font in a large rectangle. + +## The patch + +| file | constant | upstream | here | +|---|---|---|---| +| `lib/shape/shape.go` | `defaultPadding` | `40.` | `15.` | +| `lib/textmeasure/markdown.go` | `MarkdownLineHeight` | `1.5` | `1.36` | +| `d2renderers/d2fonts/d2fonts_common.go` | `FONT_SIZE_M` | `16` | `14` | +| `lib/version/version.go` | `Version` | `v0.8.1-HEAD` | `v0.9.0-plantuml-metrics.1` | + +Every shape derives its padding from `defaultPadding` as a multiple or a +fraction, for example cylinder and cloud use `defaultPadding, defaultPadding/2` +and circle uses `defaultPadding/√2`. So the single constant rescales all +shapes and keeps their relative proportions. **Do not go below 15**: +`shape_stored_data.go` computes `defaultPadding - 10`, which turns negative. + +`MarkdownLineHeight` drives both measurement and rendering, so boxes and +baselines stay in agreement. Verified: baseline spacing moves from 24 to +21.8 at a 16px font and box heights follow exactly, so text cannot overflow. + +## Result + +Box height for the same label, against a real PlantUML render: + +| lines | PlantUML | stock D2 | this fork | +|---|---|---|---| +| 1 | 39.1 | 69 | 40 | +| 2 | 58.1 | 93 | 59 | +| 3 | 77.2 | 117 | 78 | + +Within one pixel at every line count. Widths come out slightly narrower than +PlantUML, 89 against 98 for "Hello World", because the two tools ship +different fonts. + +TALA and ELK are unaffected: shape sizes are computed before layout runs, and +both engines are still bundled in a source build. + +## Binaries + +The `plantuml-metrics build` workflow cross-compiles Windows, Linux and macOS +binaries on every push to this branch, so no Go toolchain is needed on the +target machine. Download them from the run's **Summary** page under +Artifacts. Pushing a tag like `plantuml-metrics-v1` also publishes a release. + +To build it yourself you need Go 1.27 or newer, and nothing else. There is no +cgo. The main package is at the repository root, not `cmd/d2`: + +``` +go build -o d2 . # host platform +GOOS=windows GOARCH=amd64 go build -o d2.exe . # cross-compile +``` + +## Installing on Windows with VS Code + +The *D2 in Markdown* extension spawns a bare `d2` and inherits `PATH`. It has +no setting for a binary path, and needs none. Put `d2.exe` where the stock one +lives, or earlier on `PATH`, then restart VS Code completely, since it reads +the environment at launch. Set `d2InMarkdown.layout` to `tala` or `elk`; it +defaults to `dagre`. + +Check what is actually being used with `d2 --version`. This fork reports +`v0.9.0-plantuml-metrics.1`, which no upstream release uses. + +## Tracking upstream + +`master` is untouched and tracks upstream, so a new release is: + +``` +git fetch upstream +git rebase v0.10.0 plantuml-metrics +``` + +Four one-line constant changes rarely conflict. The workflow re-checks all +three values after every build and fails loudly if a rebase drops one. diff --git a/d2renderers/d2fonts/d2fonts_common.go b/d2renderers/d2fonts/d2fonts_common.go index 8f8c4e3942..ef29dcd2e2 100644 --- a/d2renderers/d2fonts/d2fonts_common.go +++ b/d2renderers/d2fonts/d2fonts_common.go @@ -64,7 +64,7 @@ func (f Font) GetEncodedSubset(corpus string) string { const ( FONT_SIZE_XS = 13 FONT_SIZE_S = 14 - FONT_SIZE_M = 16 + FONT_SIZE_M = 14 FONT_SIZE_L = 20 FONT_SIZE_XL = 24 FONT_SIZE_XXL = 28 diff --git a/lib/shape/shape.go b/lib/shape/shape.go index 51558df548..32bbbf3c4e 100644 --- a/lib/shape/shape.go +++ b/lib/shape/shape.go @@ -34,7 +34,7 @@ const ( CODE_TYPE = "Code" IMAGE_TYPE = "Image" - defaultPadding = 40. + defaultPadding = 15. ) type Shape interface { diff --git a/lib/textmeasure/markdown.go b/lib/textmeasure/markdown.go index 9568d34d4d..86eccb9d04 100644 --- a/lib/textmeasure/markdown.go +++ b/lib/textmeasure/markdown.go @@ -21,7 +21,7 @@ var markdownRenderer goldmark.Markdown // these are css values from github-markdown.css so we can accurately compute the rendered dimensions const ( MarkdownFontSize = d2fonts.FONT_SIZE_M - MarkdownLineHeight = 1.5 + MarkdownLineHeight = 1.36 PaddingLeft_ul_ol_em = 2. MarginBottom_ul = 16. diff --git a/lib/version/version.go b/lib/version/version.go index 99b55bb2f5..c9a7a313cf 100644 --- a/lib/version/version.go +++ b/lib/version/version.go @@ -3,7 +3,7 @@ package version import "regexp" // Pre-built binaries will have version set correctly during build time. -var Version = "v0.8.1-HEAD" +var Version = "v0.9.0-plantuml-metrics.1" func OnlyNumbers() string { re, err := regexp.Compile("[0-9]+.[0-9]+.[0-9]+") From c7fc5a860640c40e949eb5694e4df64a6c57dc82 Mon Sep 17 00:00:00 2001 From: Pavlo Alkhimov <631749+pavlo-alkhimov@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:17:20 +0200 Subject: [PATCH 2/4] Version tags, README pointer, mirrored upstream tags Tag pattern becomes v*-plantuml-metrics* so a tagged build reports the same string the source defaults to, instead of a bare "v1". Adds a four-line banner at the top of README pointing at PLANTUML-METRICS.md, and documents releases plus the upstream remote needed before a rebase. Co-Authored-By: Claude Opus 5 --- .github/workflows/plantuml-metrics-build.yml | 6 ++--- PLANTUML-METRICS.md | 23 ++++++++++++++++++-- README.md | 5 +++++ 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/.github/workflows/plantuml-metrics-build.yml b/.github/workflows/plantuml-metrics-build.yml index 543d7a2f11..27a8373954 100644 --- a/.github/workflows/plantuml-metrics-build.yml +++ b/.github/workflows/plantuml-metrics-build.yml @@ -2,12 +2,12 @@ name: plantuml-metrics build # Cross-compiles the patched d2 so no Go toolchain is needed on the target # machine. Artifacts appear under the run's "Summary" page; a tag starting -# with "plantuml-metrics-v" additionally publishes a release. +# like "v0.9.0-plantuml-metrics.1" additionally publishes a release. on: push: branches: [plantuml-metrics] - tags: ['plantuml-metrics-v*'] + tags: ['v*-plantuml-metrics*'] workflow_dispatch: permissions: @@ -38,7 +38,7 @@ jobs: id: v run: | if [ "${GITHUB_REF_TYPE}" = "tag" ]; then - echo "version=${GITHUB_REF_NAME#plantuml-metrics-}" >> "$GITHUB_OUTPUT" + echo "version=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT" else echo "version=$(sed -n 's/.*Version = "\(.*\)".*/\1/p' lib/version/version.go)+$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" fi diff --git a/PLANTUML-METRICS.md b/PLANTUML-METRICS.md index e6f79298e0..aaca1d8a78 100644 --- a/PLANTUML-METRICS.md +++ b/PLANTUML-METRICS.md @@ -76,9 +76,28 @@ Check what is actually being used with `d2 --version`. This fork reports `master` is untouched and tracks upstream, so a new release is: ``` -git fetch upstream +git remote add upstream https://github.com/d2lang/d2.git # once +git fetch upstream --tags git rebase v0.10.0 plantuml-metrics ``` +Upstream's tags are mirrored into this fork, so `v0.10.0` resolves without +the extra remote once it has been fetched here. + Four one-line constant changes rarely conflict. The workflow re-checks all -three values after every build and fails loudly if a rebase drops one. +three values after every build and fails loudly if a rebase drops one. The +one file that can conflict is `README.md`, which carries a four-line banner +at the very top; keep it or drop it, it has no effect on the build. + +## Releases + +Tag with the version you want the binary to report, which also publishes a +GitHub release with all four platforms attached: + +``` +git tag v0.9.0-plantuml-metrics.1 +git push origin v0.9.0-plantuml-metrics.1 +``` + +Untagged pushes still produce artifacts, but those expire with the repo's +retention policy, so tag anything you actually install. diff --git a/README.md b/README.md index 4496236d36..edf3ac36f9 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ +> **This is a patched fork.** The `plantuml-metrics` branch makes shapes as +> tight around their text as PlantUML's are, via three constants and no +> syntax changes. See **[PLANTUML-METRICS.md](./PLANTUML-METRICS.md)**. +> `master` is untouched and tracks upstream. +
D2

From 298012b59cc05e1dfbc0fc5351c666a2443ed3ef Mon Sep 17 00:00:00 2001 From: Pavlo Alkhimov <631749+pavlo-alkhimov@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:58:08 +0200 Subject: [PATCH 3/4] d2compiler: fix nil-pointer panic when reporting a reserved-keyword error Compiling this panics with an invalid memory address: **.shape: rectangle b LINK -> b Field.LastPrimaryKey() returns a nil *d2ast.Key for a field that was created by an edge or a glob rather than written as `KEY: value`. compileReserved passes that result straight to errorf, and d2parser.Errorf calls GetRange() on the nil pointer, so the crash happens inside the code that was trying to report an ordinary compile error. Around sixty call sites in d2compiler pass LastPrimaryKey() the same way, so the crash is reachable by other routes. errorf now substitutes a zero-range node when handed a nil one, which covers every call site. It still delegates to d2parser.Errorf rather than formatting the message itself, so go vet does not begin treating errorf as a printf wrapper and flagging pre-existing non-constant format strings in this file. The site in the traceback additionally falls back to LastRef().AST(), keeping an accurate source position like the sibling error just below it. The panic becomes: reserved field LINK does not accept composite Co-Authored-By: Claude Opus 5 --- d2compiler/compile.go | 28 ++++++++++++++++++- d2compiler/compile_test.go | 12 ++++++++ .../reserved-composite-glob-edge.exp.json | 11 ++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 testdata/d2compiler/TestCompile/reserved-composite-glob-edge.exp.json diff --git a/d2compiler/compile.go b/d2compiler/compile.go index ff18ac5746..7892e65dfb 100644 --- a/d2compiler/compile.go +++ b/d2compiler/compile.go @@ -7,6 +7,7 @@ import ( "io" "io/fs" "net/url" + "reflect" "slices" "strconv" "strings" @@ -273,7 +274,26 @@ type compiler struct { activeClassMaps map[*d2ir.Map]struct{} } +// isNilNode reports whether n is nil, including a nil pointer stored in a +// non-nil interface. Field.LastPrimaryKey() returns a nil *d2ast.Key for a +// field that was created only by an edge or a glob, and many call sites pass +// its result straight to errorf. +func isNilNode(n d2ast.Node) bool { + if n == nil { + return true + } + switch v := reflect.ValueOf(n); v.Kind() { + case reflect.Ptr, reflect.Interface, reflect.Map, reflect.Slice: + return v.IsNil() + } + return false +} + func (c *compiler) errorf(n d2ast.Node, f string, v ...interface{}) { + if isNilNode(n) { + // Carry on with a zero range rather than dereferencing nil. + n = &d2ast.Key{} + } err := d2parser.Errorf(n, f, v...).(d2ast.Error) if c.err.ErrorsLookup == nil { c.err.ErrorsLookup = make(map[d2ast.Error]struct{}) @@ -594,7 +614,13 @@ func (c *compiler) compileReserved(attrs *d2graph.Attributes, f *d2ir.Field) { case "label", "icon", "tooltip": c.compilePosition(attrs, f) default: - c.errorf(f.LastPrimaryKey(), "reserved field %v does not accept composite", f.Name.ScalarString()) + // LastPrimaryKey() is nil when the field came from an edge + // or a glob; LastRef() always exists, as used just below. + var n d2ast.Node = f.LastRef().AST() + if k := f.LastPrimaryKey(); k != nil { + n = k + } + c.errorf(n, "reserved field %v does not accept composite", f.Name.ScalarString()) } } else { c.errorf(f.LastRef().AST(), `reserved field "%v" must have a value`, f.Name.ScalarString()) diff --git a/d2compiler/compile_test.go b/d2compiler/compile_test.go index 6cc7177c8a..84a2b208d5 100644 --- a/d2compiler/compile_test.go +++ b/d2compiler/compile_test.go @@ -3731,6 +3731,18 @@ object: { `, expErr: `d2/testdata/d2compiler/TestCompile/reserved-composite.d2:1:1: reserved field shape does not accept composite`, }, + { + // A reserved keyword used as a shape name, reachable only + // through an edge, in a file with a glob. The field then has no + // primary key, so LastPrimaryKey() is nil. Reporting the error + // used to dereference it and panic. + name: "reserved-composite-glob-edge", + text: `**.shape: rectangle +b +LINK -> b +`, + expErr: `d2/testdata/d2compiler/TestCompile/reserved-composite-glob-edge.d2:3:1: reserved field LINK does not accept composite`, + }, { name: "text_no_label", text: `a: "ok" { diff --git a/testdata/d2compiler/TestCompile/reserved-composite-glob-edge.exp.json b/testdata/d2compiler/TestCompile/reserved-composite-glob-edge.exp.json new file mode 100644 index 0000000000..78719045b7 --- /dev/null +++ b/testdata/d2compiler/TestCompile/reserved-composite-glob-edge.exp.json @@ -0,0 +1,11 @@ +{ + "graph": null, + "err": { + "errs": [ + { + "range": "d2/testdata/d2compiler/TestCompile/reserved-composite-glob-edge.d2,2:0:22-2:4:26", + "errmsg": "d2/testdata/d2compiler/TestCompile/reserved-composite-glob-edge.d2:3:1: reserved field LINK does not accept composite" + } + ] + } +} From 7ed14c4aa610daaaac65f429a691ddfc027836b6 Mon Sep 17 00:00:00 2001 From: Pavlo Alkhimov <631749+pavlo-alkhimov@users.noreply.github.com> Date: Tue, 22 Sep 2026 16:21:22 +0200 Subject: [PATCH 4/4] Drop upstream CI, build from master, update the docs This fork deliberately changes the geometry of every rendered diagram, so upstream's end-to-end tests cannot pass: verified, they fail on exactly the coordinates the metrics change moves. Keeping those workflows means a permanently red master and a thirty-minute job on every push to prove something already known. Removes ci.yml, release-archives.yml, weekly-race.yml, tala-fuzz.yml, the docker and npm staging jobs, and windows-msi.yml, leaving only plantuml-metrics-build.yml. Dropping release-archives.yml also stops a second, differently named set of binaries being built for every v* tag, which was a source of confusion about which download to install. The build workflow now triggers on master rather than the patch branch, since the work is being consolidated there. Docs updated accordingly: the rebase recipe no longer assumes a pristine mirror branch, and the compiler crash fix is documented again after the branch split dropped that section. What still guards correctness: the build asserts the three metric constants on every run, and go test ./d2compiler/ covers the panic fix. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 51 --- .github/workflows/docker-image-smoke.yml | 57 ---- .github/workflows/docker-publish.yml | 204 ------------ .github/workflows/npm-stage.yml | 192 ------------ .github/workflows/plantuml-metrics-build.yml | 2 +- .github/workflows/release-archives.yml | 310 ------------------- .github/workflows/tala-fuzz.yml | 55 ---- .github/workflows/weekly-race.yml | 30 -- .github/workflows/windows-msi.yml | 80 ----- PLANTUML-METRICS.md | 74 ++++- README.md | 7 +- 11 files changed, 68 insertions(+), 994 deletions(-) delete mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/docker-image-smoke.yml delete mode 100644 .github/workflows/docker-publish.yml delete mode 100644 .github/workflows/npm-stage.yml delete mode 100644 .github/workflows/release-archives.yml delete mode 100644 .github/workflows/tala-fuzz.yml delete mode 100644 .github/workflows/weekly-race.yml delete mode 100644 .github/workflows/windows-msi.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 5511451e98..0000000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: ci -on: - pull_request: - push: - branches: [master] - workflow_dispatch: - workflow_call: -permissions: - actions: read - contents: read -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - ci: - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 - with: - go-version-file: ./go.mod - cache: true - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: "24" - package-manager-cache: false - - run: COLOR=1 ./make.sh - env: - # Disable chat notifications from the shared CI helper. - CI_MAKE_ROOT: "0" - GITHUB_TOKEN: ${{ github.token }} - - name: Test TALA with the race detector - run: go test -race ./d2layouts/d2talalayout/... -count=1 -timeout=15m - - name: Verify representative release archive and size budget - run: ./ci/release/build.sh --rebuild --run=linux/amd64 --version=v0.0.0-ci - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - if: always() - with: - name: d2chaos - path: ./d2chaos/out - signed: - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - run: git submodule update --init - - run: COLOR=1 ./ci/sub/bin/ensure_signed.sh - env: - GITHUB_TOKEN: ${{ github.token }} diff --git a/.github/workflows/docker-image-smoke.yml b/.github/workflows/docker-image-smoke.yml deleted file mode 100644 index d6a9edc28a..0000000000 --- a/.github/workflows/docker-image-smoke.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: Docker image smoke - -on: - pull_request: - paths: - - .github/workflows/docker-image-smoke.yml - - .github/workflows/docker-publish.yml - - ci/release/docker/** - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} - cancel-in-progress: true - -jobs: - smoke: - strategy: - fail-fast: false - matrix: - include: - - runner: ubuntu-24.04 - arch: amd64 - - runner: ubuntu-24.04-arm - arch: arm64 - runs-on: ${{ matrix.runner }} - timeout-minutes: 45 - env: - ARCH: ${{ matrix.arch }} - IMAGE: d2:docker-smoke-${{ matrix.arch }} - VERSION: v0.0.0-docker-smoke - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - submodules: true - persist-credentials: false - - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 - with: - go-version-file: ./go.mod - cache: true - - name: Test release validation and publication guards without a registry - run: go test ./ci/release/docker - - name: Build representative release archive - run: ./ci/release/build.sh --rebuild --run="linux/$ARCH" --version="$VERSION" - - name: Prepare Docker context - run: | - mkdir -p "$RUNNER_TEMP/docker-context" - cp "ci/release/build/$VERSION/d2-$VERSION-linux-$ARCH.tar.gz" "$RUNNER_TEMP/docker-context/" - cp ci/release/docker/entrypoint.sh ci/release/docker/Dockerfile "$RUNNER_TEMP/docker-context/" - - name: Build and measure local image using the release build path - run: ./ci/release/docker/image.sh build local "$RUNNER_TEMP/docker-context" - - name: Smoke-test CLI, SVG, TALA, and PNG natively - run: ./ci/release/docker/image.sh smoke "$IMAGE" - - name: Clean up Docker image - if: always() - run: docker image rm --force "$IMAGE" >/dev/null 2>&1 || true diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml deleted file mode 100644 index 41bcb2d604..0000000000 --- a/.github/workflows/docker-publish.yml +++ /dev/null @@ -1,204 +0,0 @@ -name: Publish Docker release - -on: - workflow_dispatch: - inputs: - version: - description: Published D2 release tag to publish to Docker Hub - required: true - type: string - publish_latest: - description: Also update latest (stable releases only) - required: true - type: boolean - default: false - -permissions: - contents: read - -concurrency: - group: docker-production-release - cancel-in-progress: false - -env: - PRIMARY_DOCKER_IMAGE: d2lang/d2 - LEGACY_DOCKER_IMAGE: terrastruct/d2 - -jobs: - validate: - runs-on: ubuntu-24.04 - timeout-minutes: 5 - outputs: - release: ${{ steps.release.outputs.release }} - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - sparse-checkout: ci/release/docker - - name: Validate protected master dispatch and published release - id: release - env: - GH_TOKEN: ${{ github.token }} - PUBLISH_LATEST: ${{ inputs.publish_latest }} - REF_PROTECTED: ${{ github.ref_protected }} - VERSION: ${{ inputs.version }} - run: ./ci/release/docker/release.sh validate - - preflight: - needs: validate - runs-on: ubuntu-24.04 - timeout-minutes: 5 - environment: docker-release - env: - RELEASE: ${{ needs.validate.outputs.release }} - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - sparse-checkout: ci/release/docker - - name: Log in to Docker Hub - env: - DOCKERHUB_USERNAME: ${{ vars.DOCKERHUB_USERNAME }} - DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} - run: | - test -n "$DOCKERHUB_USERNAME" && test -n "$DOCKERHUB_TOKEN" - printf '%s' "$DOCKERHUB_TOKEN" | docker login --username "$DOCKERHUB_USERNAME" --password-stdin - - name: Verify immutability and refuse existing version tags in both repositories - run: ./ci/release/docker/release.sh preflight - - name: Clean up Docker credentials - if: always() - run: docker logout >/dev/null 2>&1 || true - - build: - needs: [validate, preflight] - strategy: - fail-fast: false - matrix: - include: - - runner: ubuntu-24.04 - arch: amd64 - - runner: ubuntu-24.04-arm - arch: arm64 - runs-on: ${{ matrix.runner }} - timeout-minutes: 60 - environment: docker-release - env: - ARCH: ${{ matrix.arch }} - RELEASE: ${{ needs.validate.outputs.release }} - VERSION: ${{ fromJSON(needs.validate.outputs.release).version }} - steps: - # Run the reviewed orchestration from the dispatch, including for older releases. - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - sparse-checkout: ci/release/docker - - name: Check out the exact release Dockerfile and entrypoint - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ fromJSON(needs.validate.outputs.release).commit }} - path: release-source - persist-credentials: false - sparse-checkout: | - ci/release/docker - d2plugin - - name: Download and verify exact release archive - env: - GH_TOKEN: ${{ github.token }} - run: ./ci/release/docker/release.sh download "$ARCH" "$RUNNER_TEMP/docker-context" release-source/ci/release/docker - - name: Log in to Docker Hub - env: - DOCKERHUB_USERNAME: ${{ vars.DOCKERHUB_USERNAME }} - DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} - run: | - test -n "$DOCKERHUB_USERNAME" && test -n "$DOCKERHUB_TOKEN" - printf '%s' "$DOCKERHUB_TOKEN" | docker login --username "$DOCKERHUB_USERNAME" --password-stdin - - name: Build once and stage identical digests in both repositories with provenance - id: image - run: ./ci/release/docker/image.sh build push "$RUNNER_TEMP/docker-context" - - name: Smoke-test staged CLI, SVG, TALA, and PNG natively - env: - IMAGE: ${{ steps.image.outputs.image }} - # Older release archives predate bundled TALA. Check the pinned source, - # so a missing TALA binary in a native release cannot silently skip smoke. - REQUIRE_TALA: ${{ hashFiles('release-source/d2plugin/plugin_tala.go') != '' }} - run: ./ci/release/docker/image.sh smoke "$IMAGE" - - name: Record verified architecture digest - env: - DIGEST: ${{ steps.image.outputs.digest }} - run: | - mkdir -p "$RUNNER_TEMP/digests" - printf '%s\n' "$DIGEST" >"$RUNNER_TEMP/digests/$ARCH.txt" - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: docker-digest-${{ matrix.arch }} - path: ${{ runner.temp }}/digests/${{ matrix.arch }}.txt - if-no-files-found: error - retention-days: 7 - - name: Clean up Docker credentials - if: always() - run: docker logout >/dev/null 2>&1 || true - - publish-version: - needs: [validate, build] - runs-on: ubuntu-24.04 - timeout-minutes: 10 - environment: docker-release - env: - RELEASE: ${{ needs.validate.outputs.release }} - outputs: - digest: ${{ steps.version.outputs.digest }} - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - sparse-checkout: ci/release/docker - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: docker-digest-* - merge-multiple: true - path: ${{ runner.temp }}/digests - - name: Log in to Docker Hub - env: - DOCKERHUB_USERNAME: ${{ vars.DOCKERHUB_USERNAME }} - DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} - run: | - test -n "$DOCKERHUB_USERNAME" && test -n "$DOCKERHUB_TOKEN" - printf '%s' "$DOCKERHUB_TOKEN" | docker login --username "$DOCKERHUB_USERNAME" --password-stdin - - name: Revalidate release and candidates, then publish and verify immutable version tags - id: version - env: - GH_TOKEN: ${{ github.token }} - run: ./ci/release/docker/release.sh publish-version "$RUNNER_TEMP/digests" - - name: Clean up Docker credentials - if: always() - run: docker logout >/dev/null 2>&1 || true - - publish-latest: - if: inputs.publish_latest - needs: [validate, publish-version] - runs-on: ubuntu-24.04 - timeout-minutes: 5 - environment: docker-release - env: - RELEASE: ${{ needs.validate.outputs.release }} - VERSION_DIGEST: ${{ needs.publish-version.outputs.digest }} - PUBLISH_LATEST: ${{ inputs.publish_latest }} - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - sparse-checkout: ci/release/docker - - name: Log in to Docker Hub - env: - DOCKERHUB_USERNAME: ${{ vars.DOCKERHUB_USERNAME }} - DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} - run: | - test -n "$DOCKERHUB_USERNAME" && test -n "$DOCKERHUB_TOKEN" - printf '%s' "$DOCKERHUB_TOKEN" | docker login --username "$DOCKERHUB_USERNAME" --password-stdin - - name: Revalidate stable release, then publish and verify latest in both repositories - env: - GH_TOKEN: ${{ github.token }} - run: ./ci/release/docker/release.sh publish-latest - - name: Clean up Docker credentials - if: always() - run: docker logout >/dev/null 2>&1 || true diff --git a/.github/workflows/npm-stage.yml b/.github/workflows/npm-stage.yml deleted file mode 100644 index 81006585f5..0000000000 --- a/.github/workflows/npm-stage.yml +++ /dev/null @@ -1,192 +0,0 @@ -name: npm stage - -on: - workflow_dispatch: - inputs: - version: - description: Version to stage, or "nightly" for a dated prerelease - required: true - type: string - default: nightly - d2_ref: - description: Exact D2 release tag to build, or empty for the dispatched master commit - required: false - type: string - default: "" -permissions: - contents: read - -concurrency: - group: npm-stage - cancel-in-progress: false - -jobs: - pack: - if: github.ref == 'refs/heads/master' && github.ref_protected - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - submodules: recursive - - - name: Select the exact D2 release source - if: inputs.d2_ref != '' - env: - D2_REF: ${{ inputs.d2_ref }} - run: | - set -euo pipefail - if ! [[ "$D2_REF" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?$ ]]; then - echo "d2_ref must be a v-prefixed semantic-version tag" >&2 - exit 1 - fi - source_commit=$(git rev-parse --verify "refs/tags/$D2_REF^{commit}") - git merge-base --is-ancestor "$source_commit" "$GITHUB_SHA" - git checkout --detach "$source_commit" - git submodule update --init --recursive - echo "Building D2 $D2_REF from $source_commit" >>"$GITHUB_STEP_SUMMARY" - - - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 - with: - go-version-file: ./go.mod - cache: true - - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: "24" - registry-url: https://registry.npmjs.org - package-manager-cache: false - - - name: Install supported npm and Bun - run: | - npm install --global npm@12.0.2 bun@1.3.14 - npm --version - bun --version - - - name: Install binaryen - run: | - BINARYEN_VERSION="version_131" - BINARYEN_SHA256="b5bf1f0eaf17c63ee588ff7a5954dc8f6ce2c26989051c66f24dfe9ece3e46db" - BINARYEN_ARCHIVE="$RUNNER_TEMP/binaryen-${BINARYEN_VERSION}-x86_64-linux.tar.gz" - curl -fsSL \ - "https://github.com/WebAssembly/binaryen/releases/download/${BINARYEN_VERSION}/binaryen-${BINARYEN_VERSION}-x86_64-linux.tar.gz" \ - -o "$BINARYEN_ARCHIVE" - echo "$BINARYEN_SHA256 $BINARYEN_ARCHIVE" | sha256sum --check --strict - tar -xzf "$BINARYEN_ARCHIVE" -C "$RUNNER_TEMP" - sudo cp "$RUNNER_TEMP/binaryen-${BINARYEN_VERSION}"/bin/* /usr/local/bin/ - wasm-opt --version - - - name: Build, test, and pack both package identities - env: - NPM_PACKAGE_TARGET: both - NPM_PACK_DIR: ${{ runner.temp }}/npm-packages - NPM_PUBLISH_MODE: pack - NPM_VERSION: ${{ inputs.version }} - run: | - CI_FORCE=1 CI_MAKE_ROOT=0 COLOR=1 ./make.sh js - cd d2js/js - bun test test/unit - - - name: Upload immutable package tarballs - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: npm-packages - path: ${{ runner.temp }}/npm-packages - if-no-files-found: error - compression-level: 0 - - stage: - name: stage-${{ matrix.package }} - needs: pack - runs-on: ubuntu-latest - timeout-minutes: 10 - environment: npm-release - permissions: - contents: read - id-token: write - strategy: - fail-fast: false - matrix: - package: - - d2lang - - terrastruct - steps: - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: "24" - registry-url: https://registry.npmjs.org - package-manager-cache: false - - - name: Install supported npm - run: | - npm install --global npm@12.0.2 - npm --version - - - name: Download immutable package tarballs - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: npm-packages - path: ${{ runner.temp }}/npm-packages - - - name: Verify and stage package for review - env: - PACKAGE_KEY: ${{ matrix.package }} - run: | - set -euo pipefail - - manifest="$RUNNER_TEMP/npm-packages/manifest.json" - case "$PACKAGE_KEY" in - d2lang) package_name=@d2lang/d2 ;; - terrastruct) package_name=@terrastruct/d2 ;; - *) - echo "unexpected package key: $PACKAGE_KEY" >&2 - exit 1 - ;; - esac - - metadata=$(node - "$manifest" "$package_name" <<'NODE' - const fs = require('fs'); - const manifest = JSON.parse(fs.readFileSync(process.argv[2], 'utf8')); - const packageName = process.argv[3]; - const record = manifest.packages && manifest.packages[packageName]; - if (manifest.schemaVersion !== 1 || !record) { - process.exit(1); - } - process.stdout.write([ - manifest.version, - manifest.tag, - record.filename, - record.shasum, - record.integrity, - ].join('\t')); - NODE - ) - IFS=$'\t' read -r version tag filename expected_shasum expected_integrity <<<"$metadata" - if [[ -z "$version" || -z "$tag" || -z "$filename" || - "$filename" != "${filename##*/}" ]]; then - echo "invalid npm package manifest" >&2 - exit 1 - fi - - tarball="$RUNNER_TEMP/npm-packages/$filename" - test -f "$tarball" - node - "$tarball" "$expected_shasum" "$expected_integrity" <<'NODE' - const crypto = require('crypto'); - const fs = require('fs'); - const bytes = fs.readFileSync(process.argv[2]); - const expectedShasum = process.argv[3]; - const expectedIntegrity = process.argv[4]; - const shasum = crypto.createHash('sha1').update(bytes).digest('hex'); - const integrity = `sha512-${crypto.createHash('sha512').update(bytes).digest('base64')}`; - if (shasum !== expectedShasum || integrity !== expectedIntegrity) { - throw new Error('npm tarball integrity mismatch'); - } - NODE - tar -xOzf "$tarball" package/package.json | jq -e \ - --arg name "$package_name" \ - --arg version "$version" \ - '.name == $name and .version == $version and .publishConfig.access == "public"' \ - >/dev/null - - npm stage publish "$tarball" --tag "$tag" diff --git a/.github/workflows/plantuml-metrics-build.yml b/.github/workflows/plantuml-metrics-build.yml index 27a8373954..6cec6eadb3 100644 --- a/.github/workflows/plantuml-metrics-build.yml +++ b/.github/workflows/plantuml-metrics-build.yml @@ -6,7 +6,7 @@ name: plantuml-metrics build on: push: - branches: [plantuml-metrics] + branches: [master] tags: ['v*-plantuml-metrics*'] workflow_dispatch: diff --git a/.github/workflows/release-archives.yml b/.github/workflows/release-archives.yml deleted file mode 100644 index 84cd74e598..0000000000 --- a/.github/workflows/release-archives.yml +++ /dev/null @@ -1,310 +0,0 @@ -name: Release archives - -on: - pull_request: - paths: - - .github/workflows/release-archives.yml - - .github/workflows/ci.yml - - .github/workflows/windows-msi.yml - - ci/release/** - - d2layouts/d2talalayout/** - - ci/sub - - install.sh - - go.mod - - go.sum - push: - tags: - - v* - -permissions: - contents: read - -concurrency: - group: release-archives-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} - -jobs: - tests: - if: github.event_name == 'push' - permissions: - actions: read - contents: read - uses: ./.github/workflows/ci.yml - - build: - runs-on: ubuntu-24.04 - timeout-minutes: 30 - env: - RELEASE_BUILD_SERIAL: "1" - outputs: - version: ${{ steps.version.outputs.version }} - commit: ${{ steps.version.outputs.commit }} - windows_sha256: ${{ steps.archives.outputs.windows_sha256 }} - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - persist-credentials: false - submodules: true - - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 - with: - go-version-file: ./go.mod - cache: true - - - name: Select release version - id: version - env: - PR_NUMBER: ${{ github.event.pull_request.number }} - run: | - set -euo pipefail - if [[ "$GITHUB_EVENT_NAME" == push ]]; then - version=$GITHUB_REF_NAME - if ! printf '%s\n' "$version" | grep -Eq '^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?$'; then - echo "release tag must be a v-prefixed semantic version" >&2 - exit 1 - fi - else - version="v0.0.0-pr.$PR_NUMBER" - fi - echo "version=$version" >>"$GITHUB_OUTPUT" - echo "commit=$(git rev-parse HEAD)" >>"$GITHUB_OUTPUT" - - - name: Verify modules - env: - GOEXPERIMENT: "" - GOFLAGS: "" - GOTOOLCHAIN: local - GOWORK: "off" - run: go mod verify - - - name: Build and verify all release archives - env: - RELEASE: "1" - RELEASE_BUILD_IN_CI: "1" - VERSION: ${{ steps.version.outputs.version }} - run: ./ci/release/build.sh --rebuild --version="$VERSION" - - - name: Prove archive reproducibility - env: - RELEASE: "1" - RELEASE_BUILD_IN_CI: "1" - VERSION: ${{ steps.version.outputs.version }} - run: | - set -euo pipefail - cp "ci/release/build/$VERSION/SHA256SUMS" "$RUNNER_TEMP/first-SHA256SUMS" - ./ci/release/build.sh --rebuild --version="$VERSION" - diff -u "$RUNNER_TEMP/first-SHA256SUMS" "ci/release/build/$VERSION/SHA256SUMS" - git diff --exit-code - - - name: Record Windows archive checksum - id: archives - env: - VERSION: ${{ steps.version.outputs.version }} - run: | - digest=$(sha256sum "ci/release/build/$VERSION/d2-$VERSION-windows-amd64.tar.gz" | cut -d ' ' -f 1) - echo "windows_sha256=$digest" >>"$GITHUB_OUTPUT" - - - name: Generate release SBOM - env: - VERSION: ${{ steps.version.outputs.version }} - run: | - GOTOOLCHAIN=local GOWORK=off GOFLAGS= GOEXPERIMENT= \ - go run -mod=readonly ./ci/release/releasetool sbom \ - --binary "ci/release/build/$VERSION/linux-amd64/d2-$VERSION/bin/d2" \ - --output "$RUNNER_TEMP/d2.spdx.json" \ - --name "d2-$VERSION" \ - --version "$VERSION" \ - --epoch "$(git show -s --format=%ct HEAD)" - - - name: Upload immutable release archives - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: release-archives - path: | - ci/release/build/${{ steps.version.outputs.version }}/*.tar.gz - ci/release/build/${{ steps.version.outputs.version }}/SHA256SUMS - if-no-files-found: error - compression-level: 0 - retention-days: 30 - - - name: Upload release SBOM - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: release-sbom - path: ${{ runner.temp }}/d2.spdx.json - if-no-files-found: error - retention-days: 30 - - smoke: - needs: build - strategy: - fail-fast: false - matrix: - include: - - runner: ubuntu-24.04 - os: linux - arch: amd64 - extension: "" - - runner: ubuntu-24.04-arm - os: linux - arch: arm64 - extension: "" - - runner: macos-15-intel - os: macos - arch: amd64 - extension: "" - - runner: macos-15 - os: macos - arch: arm64 - extension: "" - - runner: windows-2025 - os: windows - arch: amd64 - extension: .exe - - runner: windows-11-arm - os: windows - arch: arm64 - extension: .exe - runs-on: ${{ matrix.runner }} - timeout-minutes: 10 - steps: - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: release-archives - path: ${{ runner.temp }}/release-archives - - - name: Run native archive smoke test - shell: pwsh - env: - ARCH: ${{ matrix.arch }} - BINARY_EXTENSION: ${{ matrix.extension }} - OS: ${{ matrix.os }} - VERSION: ${{ needs.build.outputs.version }} - run: | - $archive = Join-Path $env:RUNNER_TEMP "release-archives/d2-$env:VERSION-$env:OS-$env:ARCH.tar.gz" - if (-not (Test-Path -LiteralPath $archive -PathType Leaf)) { - throw "missing $archive" - } - $destination = Join-Path $env:RUNNER_TEMP "d2-release-smoke" - New-Item -ItemType Directory -Force -Path $destination | Out-Null - tar -C $destination -xzf $archive - if ($LASTEXITCODE -ne 0) { - throw "unable to extract $archive" - } - $binary = Join-Path $destination "d2-$env:VERSION/bin/d2$env:BINARY_EXTENSION" - $actualVersion = (& $binary --version).Trim() - if ($LASTEXITCODE -ne 0 -or $actualVersion -cne $env:VERSION) { - throw "d2 --version returned '$actualVersion', expected '$env:VERSION'" - } - $inputFile = Join-Path $destination "smoke.d2" - $outputFile = Join-Path $destination "smoke.svg" - "x -> y" | Set-Content -LiteralPath $inputFile - & $binary $inputFile $outputFile - if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath $outputFile -PathType Leaf)) { - throw "d2 SVG smoke render failed" - } - if (-not (Select-String -LiteralPath $outputFile -Pattern ' b +``` + +A D2 reserved keyword used as a shape name, reachable only through an edge, +in a file that also has a glob. `Field.LastPrimaryKey()` returns a nil +`*d2ast.Key` for a field created that way rather than written as +`KEY: value`; `compileReserved` passes it to `errorf`, and +`d2parser.Errorf` calls `GetRange()` on the nil pointer. The crash happens +inside the code that was trying to report an ordinary compile error, and +about sixty call sites in `d2compiler` pass `LastPrimaryKey()` the same way. + +`errorf` now substitutes a zero-range node when handed a nil one, covering +every call site, and the site in the traceback falls back to +`LastRef().AST()` so the message keeps an accurate source position. The +panic becomes `reserved field LINK does not accept composite`. Covered by a +regression test in `d2compiler/compile_test.go`; `go vet` is clean. + +This is independent of the metrics change and is worth sending upstream. + ## Binaries The `plantuml-metrics build` workflow cross-compiles Windows, Linux and macOS @@ -73,21 +101,47 @@ Check what is actually being used with `d2 --version`. This fork reports ## Tracking upstream -`master` is untouched and tracks upstream, so a new release is: +Everything lives on `master`; there is no separate patch branch. Upstream's +tags are mirrored here, so absorbing a new D2 release is: ``` git remote add upstream https://github.com/d2lang/d2.git # once git fetch upstream --tags -git rebase v0.10.0 plantuml-metrics +git rebase v0.10.0 master +``` + +Four one-line constant changes and one compiler fix rarely conflict. The +build workflow re-checks all three metric constants after every build and +fails loudly if a rebase drops one. `README.md` carries a three-line banner +at the top, which is the one file likely to conflict; keep it or drop it, it +has no effect on the build. + +To see exactly what this fork changes, diff against the release it is based +on rather than against a mirror branch: + +``` +git diff v0.9.0..master -- lib/shape lib/textmeasure d2renderers/d2fonts d2compiler ``` -Upstream's tags are mirrored into this fork, so `v0.10.0` resolves without -the extra remote once it has been fetched here. +## Upstream CI is removed + +This fork deliberately changes the geometry of every rendered diagram, so +upstream's end-to-end tests, which compare against golden files built for +D2's original proportions, cannot pass. Verified: they fail on exactly the +coordinates the metrics change moves. Keeping those workflows would mean a +permanently red `master` and a thirty-minute job burning Actions minutes on +every push to prove something already known. + +So upstream's workflows are deleted here (`ci.yml`, `release-archives.yml`, +`weekly-race.yml`, `tala-fuzz.yml`, the docker and npm staging jobs, and +`windows-msi.yml`). Only `plantuml-metrics-build.yml` remains. Removing +`release-archives.yml` also stops a second, differently named set of +binaries being produced for every `v*` tag, which was a source of confusion +about which download to install. -Four one-line constant changes rarely conflict. The workflow re-checks all -three values after every build and fails loudly if a rebase drops one. The -one file that can conflict is `README.md`, which carries a four-line banner -at the very top; keep it or drop it, it has no effect on the build. +What still guards correctness: the build workflow asserts the three +constants on every run, and targeted Go tests remain runnable by hand. +`go test ./d2compiler/` covers the panic fix and passes. ## Releases @@ -99,5 +153,5 @@ git tag v0.9.0-plantuml-metrics.1 git push origin v0.9.0-plantuml-metrics.1 ``` -Untagged pushes still produce artifacts, but those expire with the repo's -retention policy, so tag anything you actually install. +Untagged pushes to `master` still produce artifacts, but those expire with +the repo's retention policy, so tag anything you actually install. diff --git a/README.md b/README.md index edf3ac36f9..722b19f5b2 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ -> **This is a patched fork.** The `plantuml-metrics` branch makes shapes as -> tight around their text as PlantUML's are, via three constants and no -> syntax changes. See **[PLANTUML-METRICS.md](./PLANTUML-METRICS.md)**. -> `master` is untouched and tracks upstream. +> **This is a patched fork.** It makes shapes as tight around their text as +> PlantUML's are, via three constants and no syntax changes, and fixes an +> upstream compiler panic. See **[PLANTUML-METRICS.md](./PLANTUML-METRICS.md)**.
D2