dynoxide-rs 0.11.1

A lightweight, embeddable DynamoDB emulator backed by SQLite
Documentation
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
name: Release

# Tag-triggered release pipeline. Push a v* tag on a commit where:
#   - Cargo.toml version matches the tag
#   - CHANGELOG.md has a matching ## [X.Y.Z] entry
#   - CI has gone green
# then let this workflow do the rest.
#
# Flow: validate -> build (5 targets) -> github-release -> publish-crate
# (review gate) -> publish-homebrew + publish-npm + publish-docker in parallel.

on:
  push:
    tags:
      - 'v*'

concurrency:
  group: release-${{ github.ref_name }}
  cancel-in-progress: false

permissions:
  contents: write
  id-token: write

env:
  CARGO_TERM_COLOR: always
  # Cross-compile builds fetch a lot (toolchain, full index, crate sources)
  # and GitHub runners intermittently drop crates.io downloads with an HTTP/2
  # framing reset. Force HTTP/1.1 and retry hard so a transient blip does not
  # fail a release build.
  CARGO_NET_RETRY: "10"
  CARGO_HTTP_MULTIPLEXING: "false"

jobs:
  # -------------------------------------------------------------------
  # Validate Cargo.toml and CHANGELOG.md line up with the tag.
  # -------------------------------------------------------------------
  validate:
    name: Validate tag
    runs-on: ubuntu-latest
    timeout-minutes: 5

    outputs:
      version: ${{ steps.resolve.outputs.version }}
      tag: ${{ steps.resolve.outputs.tag }}

    steps:
      - uses: actions/checkout@v4

      - name: Resolve version from tag
        id: resolve
        run: |
          TAG="${GITHUB_REF_NAME}"
          if ! [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
            echo "::error::Invalid tag format: $TAG (expected vX.Y.Z or vX.Y.Z-pre.N)"
            exit 1
          fi
          VERSION="${TAG#v}"
          echo "tag=$TAG" >> "$GITHUB_OUTPUT"
          echo "version=$VERSION" >> "$GITHUB_OUTPUT"
          echo "Resolved tag=$TAG version=$VERSION"

      - name: Check Cargo.toml version matches tag
        env:
          EXPECTED: ${{ steps.resolve.outputs.version }}
        run: |
          ACTUAL=$(grep -m1 '^version = ' Cargo.toml | cut -d'"' -f2)
          echo "Cargo.toml version: $ACTUAL"
          echo "Expected: $EXPECTED"
          if [ "$ACTUAL" != "$EXPECTED" ]; then
            echo "::error::Cargo.toml version ($ACTUAL) does not match tag ($EXPECTED)"
            exit 1
          fi

      - name: Check CHANGELOG has entry for this version
        env:
          VERSION: ${{ steps.resolve.outputs.version }}
        run: scripts/extract-changelog.sh "$VERSION" CHANGELOG.md > /dev/null

  # -------------------------------------------------------------------
  # Build release binaries for all targets.
  # -------------------------------------------------------------------
  build:
    name: Build (${{ matrix.target }})
    needs: validate
    runs-on: ${{ matrix.os }}
    timeout-minutes: 30

    strategy:
      fail-fast: true
      matrix:
        include:
          - target: x86_64-unknown-linux-musl
            os: ubuntu-latest
            archive: tar.gz
          - target: aarch64-unknown-linux-musl
            os: ubuntu-latest
            archive: tar.gz
          - target: x86_64-apple-darwin
            os: macos-latest
            archive: tar.gz
          - target: aarch64-apple-darwin
            os: macos-latest
            archive: tar.gz
          - target: x86_64-pc-windows-msvc
            os: windows-latest
            archive: zip

    steps:
      - uses: actions/checkout@v4

      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}

      - name: Install musl tools (x86_64)
        if: matrix.target == 'x86_64-unknown-linux-musl'
        run: |
          sudo apt-get update
          sudo apt-get install -y musl-tools

      # The zig download mirror occasionally fails to fetch; one retry covers it.
      - name: Install zig (aarch64-musl)
        id: setup-zig
        if: matrix.target == 'aarch64-unknown-linux-musl'
        continue-on-error: true
        uses: mlugg/setup-zig@v2

      - name: Install zig (aarch64-musl, retry)
        if: matrix.target == 'aarch64-unknown-linux-musl' && steps.setup-zig.outcome == 'failure'
        uses: mlugg/setup-zig@v2

      - name: Install cargo-zigbuild (aarch64-musl)
        if: matrix.target == 'aarch64-unknown-linux-musl'
        uses: taiki-e/install-action@cargo-zigbuild

      - name: Build
        if: matrix.target != 'aarch64-unknown-linux-musl'
        run: cargo build --release --target ${{ matrix.target }} --features full

      - name: Build (zigbuild)
        if: matrix.target == 'aarch64-unknown-linux-musl'
        run: cargo zigbuild --release --target ${{ matrix.target }} --features full

      - name: Package (Unix)
        if: matrix.archive == 'tar.gz'
        run: |
          cd target/${{ matrix.target }}/release
          tar czf ../../../dynoxide-${{ matrix.target }}.tar.gz dynoxide
          cd ../../..

      - name: Package (Windows)
        if: matrix.archive == 'zip'
        shell: pwsh
        run: |
          Compress-Archive -Path "target/${{ matrix.target }}/release/dynoxide.exe" -DestinationPath "dynoxide-${{ matrix.target }}.zip"

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: dynoxide-${{ matrix.target }}
          path: dynoxide-${{ matrix.target }}.${{ matrix.archive }}

  # -------------------------------------------------------------------
  # Create the GitHub Release with artefacts and a changelog body.
  # -------------------------------------------------------------------
  github-release:
    name: Create GitHub Release
    needs: [validate, build]
    runs-on: ubuntu-latest
    timeout-minutes: 10

    steps:
      - uses: actions/checkout@v4

      - name: Download all artifacts
        uses: actions/download-artifact@v4
        with:
          path: artifacts

      - name: Create checksums
        run: |
          cd artifacts
          find . -type f \( -name "*.tar.gz" -o -name "*.zip" \) -exec mv {} . \;
          sha256sum *.tar.gz *.zip > sha256sums.txt
          cat sha256sums.txt

      - name: Extract changelog body
        env:
          VERSION: ${{ needs.validate.outputs.version }}
        run: scripts/extract-changelog.sh "$VERSION" CHANGELOG.md > /tmp/release_body.md

      - name: Create Release
        uses: softprops/action-gh-release@v2
        with:
          tag_name: ${{ needs.validate.outputs.tag }}
          name: ${{ needs.validate.outputs.tag }}
          body_path: /tmp/release_body.md
          files: |
            artifacts/*.tar.gz
            artifacts/*.zip
            artifacts/sha256sums.txt

  # -------------------------------------------------------------------
  # Publish to crates.io. Protected by the production-publish environment
  # so a human must approve before anything is pushed to an external
  # registry. Approval also unblocks the Homebrew and npm jobs.
  # -------------------------------------------------------------------
  publish-crate:
    name: Publish to crates.io
    needs: [validate, github-release]
    environment: production-publish
    runs-on: ubuntu-latest
    timeout-minutes: 15

    steps:
      - uses: actions/checkout@v4

      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2

      - name: cargo publish --dry-run
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        run: cargo publish --dry-run

      - name: cargo publish
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        run: cargo publish

  # -------------------------------------------------------------------
  # Update the Homebrew tap formula. Runs only after crates.io succeeds.
  # -------------------------------------------------------------------
  publish-homebrew:
    name: Update Homebrew tap
    needs: [validate, publish-crate]
    runs-on: ubuntu-latest
    timeout-minutes: 10

    steps:
      - name: Download checksums from release
        env:
          GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
          TAG: ${{ needs.validate.outputs.tag }}
        run: |
          gh release download "$TAG" \
            --repo nubo-db/dynoxide \
            --pattern 'sha256sums.txt' \
            --output sha256sums.txt

      - name: Build formula
        env:
          VERSION: ${{ needs.validate.outputs.version }}
        run: |
          SHA_DARWIN_ARM64=$(grep 'aarch64-apple-darwin' sha256sums.txt | awk '{print $1}')
          SHA_DARWIN_X64=$(grep 'x86_64-apple-darwin' sha256sums.txt | awk '{print $1}')
          SHA_LINUX_ARM64=$(grep 'aarch64-unknown-linux-musl' sha256sums.txt | awk '{print $1}')
          SHA_LINUX_X64=$(grep 'x86_64-unknown-linux-musl' sha256sums.txt | awk '{print $1}')
          for var in SHA_DARWIN_ARM64 SHA_DARWIN_X64 SHA_LINUX_ARM64 SHA_LINUX_X64; do
            if [[ -z "${!var}" ]]; then
              echo "::error::Missing checksum for $var"
              exit 1
            fi
          done
          BASE="https://github.com/nubo-db/dynoxide/releases/download/v${VERSION}"
          cat <<FORMULA | sed 's/^  //' > formula.rb
            class Dynoxide < Formula
              desc "Fast, lightweight drop-in replacement for DynamoDB Local, backed by SQLite"
              homepage "https://dynoxide.dev"
              version "${VERSION}"
              license any_of: ["MIT", "Apache-2.0"]
              on_macos do
                on_arm do
                  url "${BASE}/dynoxide-aarch64-apple-darwin.tar.gz"
                  sha256 "${SHA_DARWIN_ARM64}"
                end
                on_intel do
                  url "${BASE}/dynoxide-x86_64-apple-darwin.tar.gz"
                  sha256 "${SHA_DARWIN_X64}"
                end
              end
              on_linux do
                on_arm do
                  url "${BASE}/dynoxide-aarch64-unknown-linux-musl.tar.gz"
                  sha256 "${SHA_LINUX_ARM64}"
                end
                on_intel do
                  url "${BASE}/dynoxide-x86_64-unknown-linux-musl.tar.gz"
                  sha256 "${SHA_LINUX_X64}"
                end
              end
              def install
                bin.install "dynoxide"
              end
              test do
                assert_match version.to_s, shell_output("#{bin}/dynoxide --version")
              end
            end
          FORMULA

      - name: Push formula to tap
        env:
          GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
          VERSION: ${{ needs.validate.outputs.version }}
        run: |
          CURRENT_SHA=$(gh api repos/nubo-db/homebrew-tap/contents/Formula/dynoxide.rb --jq '.sha')
          gh api repos/nubo-db/homebrew-tap/contents/Formula/dynoxide.rb \
            -X PUT \
            --field message="dynoxide $VERSION" \
            --field content="$(base64 -w0 formula.rb)" \
            --field sha="$CURRENT_SHA" \
            --jq '.commit.html_url'

  # -------------------------------------------------------------------
  # Trigger the npm publish workflow. npm.yml owns the OIDC trusted
  # publisher configuration on npmjs.com, so the actual publish must run
  # inside that workflow rather than inline here.
  # -------------------------------------------------------------------
  publish-npm:
    name: Trigger npm publish
    needs: [validate, publish-crate]
    runs-on: ubuntu-latest
    timeout-minutes: 5

    permissions:
      actions: write
      contents: read

    steps:
      - name: Dispatch npm.yml with the release tag
        env:
          GH_TOKEN: ${{ github.token }}
          TAG: ${{ needs.validate.outputs.tag }}
        run: |
          gh api "repos/${{ github.repository }}/actions/workflows/npm.yml/dispatches" \
            -f ref=main \
            -f "inputs[tag]=$TAG"
          echo "Dispatched npm.yml with tag $TAG"

  # -------------------------------------------------------------------
  # Build and push multi-arch Docker images. GHCR is canonical and
  # gating; Docker Hub and ECR Public are best-effort mirrors.
  # -------------------------------------------------------------------
  publish-docker:
    name: Publish Docker image
    needs: [validate, publish-crate]
    runs-on: ubuntu-latest
    timeout-minutes: 20

    permissions:
      contents: read
      packages: write
      id-token: write
      attestations: write

    steps:
      - uses: actions/checkout@v4

      - name: Stage prebuilt linux-musl binaries
        env:
          GH_TOKEN: ${{ github.token }}
          TAG: ${{ needs.validate.outputs.tag }}
        run: |
          set -euo pipefail
          rm -rf dist staging
          mkdir -p dist/amd64 dist/arm64 staging
          gh release download "$TAG" \
            --repo nubo-db/dynoxide \
            --pattern '*linux-musl.tar.gz' \
            --dir staging/
          tar -xzf staging/dynoxide-x86_64-unknown-linux-musl.tar.gz -C dist/amd64/
          tar -xzf staging/dynoxide-aarch64-unknown-linux-musl.tar.gz -C dist/arm64/
          chmod +x dist/amd64/dynoxide dist/arm64/dynoxide
          # Fail loud if either slot holds the wrong arch.
          file dist/amd64/dynoxide
          file dist/arm64/dynoxide
          file dist/amd64/dynoxide | grep -q 'x86-64'
          file dist/arm64/dynoxide | grep -q 'aarch64'

      # QEMU is for the post-push verification step that runs the arm64
      # image on the amd64 runner. The build itself is pure COPY.
      - uses: docker/setup-qemu-action@v3
      - uses: docker/setup-buildx-action@v3

      - name: Login to GHCR
        id: ghcr-login
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Login to Docker Hub
        id: dockerhub-login
        continue-on-error: true
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Configure AWS credentials for ECR Public
        id: aws-creds
        continue-on-error: true
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ vars.ECR_PUBLIC_PUBLISH_ROLE_ARN }}
          aws-region: us-east-1

      - name: Login to ECR Public
        id: ecr-public-login
        continue-on-error: true
        if: steps.aws-creds.outcome == 'success'
        uses: aws-actions/amazon-ecr-login@v2
        with:
          registry-type: public

      - name: Compute image tags and labels
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ghcr.io/${{ github.repository }}
          flavor: |
            latest=auto
          tags: |
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=semver,pattern={{major}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.') }}

      - name: Build and push to GHCR
        id: build
        uses: docker/build-push-action@v6
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          provenance: mode=max
          sbom: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}

      - name: Verify published image runs on both architectures
        env:
          IMAGE: ghcr.io/${{ github.repository }}:${{ needs.validate.outputs.version }}
          EXPECTED: ${{ needs.validate.outputs.version }}
        run: |
          set -euo pipefail
          for arch in linux/amd64 linux/arm64; do
            echo "Probing $IMAGE on $arch"
            actual=$(docker run --rm --pull=always --platform "$arch" "$IMAGE" --version | awk '{print $NF}')
            echo "  reported: $actual"
            if [ "$actual" != "$EXPECTED" ]; then
              echo "::error::$arch reported version $actual, expected $EXPECTED"
              exit 1
            fi
          done

      - name: Attest build provenance
        id: attest
        continue-on-error: true
        uses: actions/attest-build-provenance@v2
        with:
          subject-name: ghcr.io/${{ github.repository }}
          subject-digest: ${{ steps.build.outputs.digest }}
          push-to-registry: true

      - name: Mirror to Docker Hub
        id: mirror-dockerhub
        if: ${{ !cancelled() && steps.dockerhub-login.outcome == 'success' }}
        continue-on-error: true
        env:
          META_JSON: ${{ steps.meta.outputs.json }}
        run: |
          set -euo pipefail
          echo "$META_JSON" | jq -r '.tags[]' | while read -r src_tag; do
            tag="${src_tag##*:}"
            echo "Mirroring $src_tag -> docker.io/nubodb/dynoxide:${tag}"
            docker buildx imagetools create \
              -t "docker.io/nubodb/dynoxide:${tag}" \
              "$src_tag"
          done

      - name: Mirror to ECR Public
        id: mirror-ecr
        if: ${{ !cancelled() && steps.ecr-public-login.outcome == 'success' }}
        continue-on-error: true
        env:
          META_JSON: ${{ steps.meta.outputs.json }}
          ECR_ALIAS: ${{ vars.ECR_PUBLIC_ALIAS }}
        run: |
          set -euo pipefail
          if [ -z "${ECR_ALIAS:-}" ]; then
            echo "::warning::ECR_PUBLIC_ALIAS is not set; skipping ECR Public mirror"
            exit 0
          fi
          echo "$META_JSON" | jq -r '.tags[]' | while read -r src_tag; do
            tag="${src_tag##*:}"
            echo "Mirroring $src_tag -> public.ecr.aws/${ECR_ALIAS}/dynoxide:${tag}"
            docker buildx imagetools create \
              -t "public.ecr.aws/${ECR_ALIAS}/dynoxide:${tag}" \
              "$src_tag"
          done

      - name: Publish summary
        if: ${{ !cancelled() }}
        env:
          META_JSON: ${{ steps.meta.outputs.json }}
          GHCR_OUTCOME: ${{ steps.build.outcome }}
          ATTEST_OUTCOME: ${{ steps.attest.outcome }}
          DOCKERHUB_OUTCOME: ${{ steps.mirror-dockerhub.outcome }}
          ECR_OUTCOME: ${{ steps.mirror-ecr.outcome }}
        run: |
          {
            echo "## Docker publish summary"
            echo
            echo "| Surface | Outcome |"
            echo "| --- | --- |"
            echo "| GHCR push | ${GHCR_OUTCOME} |"
            echo "| Provenance attestation | ${ATTEST_OUTCOME} |"
            echo "| Docker Hub mirror | ${DOCKERHUB_OUTCOME:-skipped} |"
            echo "| ECR Public mirror | ${ECR_OUTCOME:-skipped} |"
            echo
            echo "### Pushed tags"
            echo
            echo "$META_JSON" | jq -r '.tags[] | "- `\(.)`"'
          } >> "$GITHUB_STEP_SUMMARY"

  # -------------------------------------------------------------------
  # Notify dynoxide.dev so the marketing site rebuilds and the banner /
  # changelog pick up the new version. The site reads CHANGELOG.md from
  # this repo at build time, so it only needs a kick.
  # -------------------------------------------------------------------
  notify-site:
    name: Trigger dynoxide.dev rebuild
    needs: [validate, publish-crate, publish-homebrew, publish-npm]
    runs-on: ubuntu-latest
    timeout-minutes: 5

    steps:
      - name: Dispatch dynoxide.dev deploy
        env:
          GH_TOKEN: ${{ secrets.DYNOXIDE_SITE_DISPATCH_TOKEN }}
          VERSION: ${{ needs.validate.outputs.version }}
          TAG: ${{ needs.validate.outputs.tag }}
        run: |
          gh api repos/nubo-db/dynoxide.dev/dispatches \
            -X POST \
            -f event_type=dynoxide-release \
            -f "client_payload[version]=$VERSION" \
            -f "client_payload[tag]=$TAG"
          echo "Dispatched dynoxide-release event for $TAG"