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 new file mode 100644 index 0000000000..6cec6eadb3 --- /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 +# like "v0.9.0-plantuml-metrics.1" additionally publishes a release. + +on: + push: + branches: [master] + tags: ['v*-plantuml-metrics*'] + 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}" >> "$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/.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 +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 + +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 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 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. + +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 + +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 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 4496236d36..722b19f5b2 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +> **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

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/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]+") 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" + } + ] + } +}