diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d0c417e..9511a1a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -12,6 +12,11 @@ updates: # Its ref is the Rust toolchain (`@stable`, `@1.85` for the MSRV job), # not an action version, so "upgrading" it would change what we test. - dependency-name: dtolnay/rust-toolchain + # Generated by `dist generate`; its action versions are set in + # dist-workspace.toml ([dist.github-action-commits]), and dist's CI check + # fails if the file is edited by hand. + exclude-paths: + - .github/workflows/release.yml - package-ecosystem: npm # also covers pnpm-lock.yaml directory: / schedule: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f0c66fd..0fca8a7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,6 +46,45 @@ jobs: - uses: Swatinem/rust-cache@v2 - run: cargo test --workspace --locked + deny: + name: Licenses & advisories (cargo-deny) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: EmbarkStudios/cargo-deny-action@v2 + with: + command: check # deny.toml: licenses, advisories, bans, sources + + coverage: + name: Coverage + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: dtolnay/rust-toolchain@stable + with: + components: llvm-tools-preview + - uses: Swatinem/rust-cache@v2 + - uses: taiki-e/install-action@v2 + with: + tool: cargo-llvm-cov + # The wasm and node wrappers are tested from JavaScript (jobs below). + - run: cargo llvm-cov --workspace --exclude trustgraph-wasm --exclude trustgraph-node --locked --lcov --output-path lcov.info + - run: cargo llvm-cov report --html --output-dir coverage + - name: Summary + run: | + { + echo '## Coverage' + echo '```' + cargo llvm-cov report --summary-only + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + - uses: actions/upload-artifact@v7 + with: + name: coverage + path: | + lcov.info + coverage/html/ + msrv: name: MSRV (1.85) runs-on: ubuntu-latest diff --git a/.github/workflows/npm.yml b/.github/workflows/npm.yml new file mode 100644 index 0000000..26cc31c --- /dev/null +++ b/.github/workflows/npm.yml @@ -0,0 +1,245 @@ +# npm packages. See doc/releasing.md. +# +# @trustgraph/trustgraph native addon (napi-rs): one small package per +# platform plus a root package that picks the +# right one through optionalDependencies +# @trustgraph/trustgraph-wasm WebAssembly (scripts/build-wasm-package.sh) +# +# A `vX.Y.Z` tag builds, tests and publishes them with provenance. Pull +# requests that touch this pipeline, and manual runs, do everything except the +# upload (`pnpm publish --dry-run`), so they need no secrets. +name: npm + +on: + push: + tags: ["v[0-9]+.[0-9]+.[0-9]+*"] + pull_request: + paths: + - .github/workflows/npm.yml + - scripts/npm-publish.sh + - scripts/build-wasm-package.sh + - crates/trustgraph-node/package.json + - crates/trustgraph-wasm/package.json + workflow_dispatch: + +permissions: + contents: read + +env: + CARGO_TERM_COLOR: always + # napi-rs builds macOS binaries for this minimum version. + MACOSX_DEPLOYMENT_TARGET: "11.0" + +jobs: + version: + name: Version + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + dry-run: ${{ steps.version.outputs.dry-run }} + steps: + - uses: actions/checkout@v7 + - name: Version from Cargo.toml, checked against the tag + id: version + run: | + version=$(sed -n 's/^version = "\(.*\)"$/\1/p' Cargo.toml | head -n 1) + if [ "$GITHUB_REF_TYPE" = tag ]; then + [ "$GITHUB_REF_NAME" = "v$version" ] || { + echo "::error::tag $GITHUB_REF_NAME does not match Cargo.toml version $version" + exit 1 + } + echo "dry-run=" >> "$GITHUB_OUTPUT" + else + echo "dry-run=--dry-run" >> "$GITHUB_OUTPUT" + fi + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "npm version $version ${GITHUB_REF_TYPE}" + + native: + name: Native ${{ matrix.target }} + runs-on: ${{ matrix.host }} + strategy: + fail-fast: false + matrix: + include: + - { host: macos-latest, target: aarch64-apple-darwin, test: true } + - { host: macos-latest, target: x86_64-apple-darwin } + - { host: windows-latest, target: x86_64-pc-windows-msvc, test: true } + - { host: windows-latest, target: aarch64-pc-windows-msvc } + # napi-cross: an old glibc, so the binary runs on older distros. + - { host: ubuntu-latest, target: x86_64-unknown-linux-gnu, flags: --use-napi-cross } + - { host: ubuntu-latest, target: aarch64-unknown-linux-gnu, flags: --use-napi-cross } + # -x: cargo-zigbuild. + - { host: ubuntu-latest, target: x86_64-unknown-linux-musl, flags: -x } + - { host: ubuntu-latest, target: aarch64-unknown-linux-musl, flags: -x } + steps: + - uses: actions/checkout@v7 + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + - uses: Swatinem/rust-cache@v2 + with: + key: ${{ matrix.target }} + - uses: mlugg/setup-zig@v2 + if: contains(matrix.target, 'musl') + with: + version: 0.15.2 + - uses: taiki-e/install-action@v2 + if: contains(matrix.target, 'musl') + with: + tool: cargo-zigbuild + - uses: pnpm/action-setup@v6 # version from packageManager in package.json + - uses: actions/setup-node@v7 + with: + node-version-file: .nvmrc # latest LTS + cache: pnpm + - run: pnpm install --frozen-lockfile + - name: Build + working-directory: crates/trustgraph-node + shell: bash + run: pnpm exec napi build --platform --release --target ${{ matrix.target }} ${{ matrix.flags }} + - name: Smoke test + if: matrix.test + run: node tests/js/smoke.mjs crates/trustgraph-node/index.js + - uses: actions/upload-artifact@v7 + with: + name: bindings-${{ matrix.target }} + path: crates/trustgraph-node/*.node + if-no-files-found: error + # The loader is the same for every target; keep one copy. + - uses: actions/upload-artifact@v7 + if: matrix.target == 'x86_64-unknown-linux-gnu' + with: + name: bindings-loader + path: crates/trustgraph-node/index.js + if-no-files-found: error + + native-test-linux: + name: Test ${{ matrix.target }} (${{ matrix.image }}) + needs: native + runs-on: ${{ startsWith(matrix.target, 'aarch64') && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} + strategy: + fail-fast: false + matrix: + include: + - { target: x86_64-unknown-linux-gnu, image: "node:lts-slim" } + - { target: aarch64-unknown-linux-gnu, image: "node:lts-slim" } + - { target: x86_64-unknown-linux-musl, image: "node:lts-alpine" } + - { target: aarch64-unknown-linux-musl, image: "node:lts-alpine" } + # Oldest supported Node. + - { target: x86_64-unknown-linux-gnu, image: "node:22-slim" } + steps: + - uses: actions/checkout@v7 + - uses: actions/download-artifact@v8 + with: + pattern: "bindings-{${{ matrix.target }},loader}" + path: crates/trustgraph-node + merge-multiple: true + - name: Smoke test in ${{ matrix.image }} + run: | + docker run --rm -v "$PWD:/w" -w /w "${{ matrix.image }}" \ + node tests/js/smoke.mjs crates/trustgraph-node/index.js + + native-publish: + name: Publish @trustgraph/trustgraph + needs: [version, native, native-test-linux] + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # npm provenance and trusted publishing + env: + VERSION: ${{ needs.version.outputs.version }} + DRY_RUN: ${{ needs.version.outputs.dry-run }} + steps: + - uses: actions/checkout@v7 + - uses: pnpm/action-setup@v6 + - uses: actions/setup-node@v7 + with: + node-version-file: .nvmrc + cache: pnpm + - run: pnpm install --frozen-lockfile + - uses: actions/download-artifact@v8 + with: + pattern: bindings-* + path: crates/trustgraph-node/artifacts + merge-multiple: true + - name: Assemble the root and per-platform packages + working-directory: crates/trustgraph-node + run: | + pnpm pkg set version="$VERSION" + mv artifacts/index.js . + cp ../../bindings/trustgraph.d.ts ../../LICENSE . + pnpm exec napi create-npm-dirs + pnpm exec napi artifacts --output-dir artifacts + # The root package depends on every platform package; npm installs + # only the one whose os/cpu/libc match. + node -e ' + const fs = require("fs"); + const root = JSON.parse(fs.readFileSync("package.json")); + root.optionalDependencies = {}; + for (const dir of fs.readdirSync("npm").sort()) { + const pkg = JSON.parse(fs.readFileSync(`npm/${dir}/package.json`)); + const node = pkg.files.find((f) => f.endsWith(".node")); + if (!fs.existsSync(`npm/${dir}/${node}`)) throw new Error(`missing npm/${dir}/${node}`); + fs.copyFileSync("LICENSE", `npm/${dir}/LICENSE`); + root.optionalDependencies[pkg.name] = root.version; + } + fs.writeFileSync("package.json", JSON.stringify(root, null, 2) + "\n"); + ' + ls -lR npm + cat package.json + - name: npm token (optional; trusted publishing needs none) + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + if [ -n "$NPM_TOKEN" ]; then + echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >> ~/.npmrc + fi + - name: Publish per-platform packages, then the root package + working-directory: crates/trustgraph-node + run: | + for dir in npm/*/; do ../../scripts/npm-publish.sh "$dir" $DRY_RUN; done + ../../scripts/npm-publish.sh . $DRY_RUN + + wasm: + name: Publish @trustgraph/trustgraph-wasm + needs: version + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # npm provenance and trusted publishing + env: + VERSION: ${{ needs.version.outputs.version }} + DRY_RUN: ${{ needs.version.outputs.dry-run }} + steps: + - uses: actions/checkout@v7 + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + - uses: Swatinem/rust-cache@v2 + - name: wasm-bindgen version from Cargo.lock + id: wasm-bindgen + run: echo "version=$(grep -A1 '^name = "wasm-bindgen"$' Cargo.lock | sed -n 's/^version = "\(.*\)"/\1/p')" >> "$GITHUB_OUTPUT" + - uses: taiki-e/install-action@v2 + with: + tool: wasm-bindgen@${{ steps.wasm-bindgen.outputs.version }} + - uses: pnpm/action-setup@v6 + - uses: actions/setup-node@v7 + with: + node-version-file: .nvmrc + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: scripts/build-wasm-package.sh + - run: node tests/js/smoke.mjs target/npm/trustgraph-wasm/node/trustgraph_wasm.cjs + - run: node tests/js/smoke.mjs target/npm/trustgraph-wasm/web/trustgraph_wasm.js + - name: Set the version + working-directory: target/npm/trustgraph-wasm + run: pnpm pkg set version="$VERSION" + - name: npm token (optional; trusted publishing needs none) + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + if [ -n "$NPM_TOKEN" ]; then + echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >> ~/.npmrc + fi + - run: scripts/npm-publish.sh target/npm/trustgraph-wasm $DRY_RUN diff --git a/.github/workflows/release-plz.yml b/.github/workflows/release-plz.yml new file mode 100644 index 0000000..8782d8c --- /dev/null +++ b/.github/workflows/release-plz.yml @@ -0,0 +1,93 @@ +# Versioning, CHANGELOG and crates.io. See doc/releasing.md. +# +# On every push to master, release-plz opens (or updates) a release PR that +# bumps the workspace version and CHANGELOG.md. Merging that PR publishes +# trustgraph-core and trustgraph-cli to crates.io and pushes the `vX.Y.Z` tag, +# which starts release.yml (binaries, installers, Homebrew) and npm.yml. +# +# Needs the RELEASE_PLZ_TOKEN secret (a token whose pushes trigger workflows; +# the default GITHUB_TOKEN's do not). Until it exists, both jobs skip. +name: Release-plz + +on: + push: + branches: [master] + workflow_dispatch: + +permissions: {} + +jobs: + release-plz-release: + name: Release-plz release + if: github.repository_owner == 'trustgraph' + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: read + id-token: write # crates.io trusted publishing + steps: + - name: Check configuration + id: config + env: + RELEASE_PLZ_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN }} + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: | + if [ -n "$RELEASE_PLZ_TOKEN" ]; then + echo "enabled=true" >> "$GITHUB_OUTPUT" + else + echo "::notice::RELEASE_PLZ_TOKEN is not set, skipping (see doc/releasing.md)" + fi + # Optional. Without it, release-plz uses crates.io trusted publishing + # (an empty CARGO_REGISTRY_TOKEN would be an error, so set it only + # when the secret exists). + if [ -n "$CARGO_REGISTRY_TOKEN" ]; then + echo "CARGO_REGISTRY_TOKEN=$CARGO_REGISTRY_TOKEN" >> "$GITHUB_ENV" + fi + - uses: actions/checkout@v7 + if: steps.config.outputs.enabled + with: + fetch-depth: 0 + persist-credentials: false + - uses: dtolnay/rust-toolchain@stable + if: steps.config.outputs.enabled + - uses: release-plz/action@v0.5 + if: steps.config.outputs.enabled + with: + command: release + env: + GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN }} + + release-plz-pr: + name: Release-plz PR + if: github.repository_owner == 'trustgraph' + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + concurrency: + group: release-plz-${{ github.ref }} + cancel-in-progress: false + steps: + - name: Check configuration + id: config + env: + RELEASE_PLZ_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN }} + run: | + if [ -n "$RELEASE_PLZ_TOKEN" ]; then + echo "enabled=true" >> "$GITHUB_OUTPUT" + else + echo "::notice::RELEASE_PLZ_TOKEN is not set, skipping (see doc/releasing.md)" + fi + - uses: actions/checkout@v7 + if: steps.config.outputs.enabled + with: + fetch-depth: 0 + persist-credentials: false + - uses: dtolnay/rust-toolchain@stable + if: steps.config.outputs.enabled + - uses: release-plz/action@v0.5 + if: steps.config.outputs.enabled + with: + command: release-pr + env: + GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..c6c6513 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,351 @@ +# This file was autogenerated by dist: https://axodotdev.github.io/cargo-dist +# +# Copyright 2022-2024, axodotdev +# SPDX-License-Identifier: MIT or Apache-2.0 +# +# CI that: +# +# * checks for a Git Tag that looks like a release +# * builds artifacts with dist (archives, installers, hashes) +# * uploads those artifacts to temporary workflow zip +# * on success, uploads the artifacts to a GitHub Release +# +# Note that the GitHub Release will be created with a generated +# title/body based on your changelogs. + +name: Release +permissions: + "contents": "write" + +# This task will run whenever you push a git tag that looks like a version +# like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc. +# Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where +# PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION +# must be a Cargo-style SemVer Version (must have at least major.minor.patch). +# +# If PACKAGE_NAME is specified, then the announcement will be for that +# package (erroring out if it doesn't have the given version or isn't dist-able). +# +# If PACKAGE_NAME isn't specified, then the announcement will be for all +# (dist-able) packages in the workspace with that version (this mode is +# intended for workspaces with only one dist-able package, or with all dist-able +# packages versioned/released in lockstep). +# +# If you push multiple tags at once, separate instances of this workflow will +# spin up, creating an independent announcement for each one. However, GitHub +# will hard limit this to 3 tags per commit, as it will assume more tags is a +# mistake. +# +# If there's a prerelease-style suffix to the version, then the release(s) +# will be marked as a prerelease. +on: + pull_request: + push: + tags: + - '**[0-9]+.[0-9]+.[0-9]+*' + +jobs: + # Run 'dist plan' (or host) to determine what tasks we need to do + plan: + runs-on: "ubuntu-22.04" + outputs: + val: ${{ steps.plan.outputs.manifest }} + tag: ${{ !github.event.pull_request && github.ref_name || '' }} + tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} + publishing: ${{ !github.event.pull_request }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + submodules: recursive + - name: Install dist + # we specify bash to get pipefail; it guards against the `curl` command + # failing. otherwise `sh` won't catch that `curl` returned non-0 + shell: bash + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.33.0/cargo-dist-installer.sh | sh" + - name: Cache dist + uses: actions/upload-artifact@v7 + with: + name: cargo-dist-cache + path: ~/.cargo/bin/dist + # sure would be cool if github gave us proper conditionals... + # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible + # functionality based on whether this is a pull_request, and whether it's from a fork. + # (PRs run on the *source* but secrets are usually on the *target* -- that's *good* + # but also really annoying to build CI around when it needs secrets to work right.) + - id: plan + run: | + dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json + echo "dist ran successfully" + cat plan-dist-manifest.json + echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" + - name: "Upload dist-manifest.json" + uses: actions/upload-artifact@v7 + with: + name: artifacts-plan-dist-manifest + path: plan-dist-manifest.json + + # Build and packages all the platform-specific things + build-local-artifacts: + name: build-local-artifacts (${{ join(matrix.targets, ', ') }}) + # Let the initial task tell us to not run (currently very blunt) + needs: + - plan + if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }} + strategy: + fail-fast: false + # Target platforms/runners are computed by dist in create-release. + # Each member of the matrix has the following arguments: + # + # - runner: the github runner + # - dist-args: cli flags to pass to dist + # - install-dist: expression to run to install dist on the runner + # + # Typically there will be: + # - 1 "global" task that builds universal installers + # - N "local" tasks that build each platform's binaries and platform-specific installers + matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }} + runs-on: ${{ matrix.runner }} + container: ${{ matrix.container && matrix.container.image || null }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json + permissions: + "attestations": "write" + "contents": "read" + "id-token": "write" + steps: + - name: enable windows longpaths + run: | + git config --global core.longpaths true + - uses: actions/checkout@v7 + with: + persist-credentials: false + submodules: recursive + - name: Install Rust non-interactively if not already installed + if: ${{ matrix.container }} + run: | + if ! command -v cargo > /dev/null 2>&1; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + fi + - name: Install dist + run: ${{ matrix.install_dist.run }} + # Get the dist-manifest + - name: Fetch local artifacts + uses: actions/download-artifact@v8 + with: + pattern: artifacts-* + path: target/distrib/ + merge-multiple: true + - name: Install dependencies + run: | + ${{ matrix.packages_install }} + - name: Build artifacts + run: | + # Actually do builds and make zips and whatnot + dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json + echo "dist ran successfully" + - name: Attest + uses: actions/attest@v4 + with: + subject-path: "target/distrib/*${{ join(matrix.targets, ', ') }}*" + - id: cargo-dist + name: Post-build + # We force bash here just because github makes it really hard to get values up + # to "real" actions without writing to env-vars, and writing to env-vars has + # inconsistent syntax between shell and powershell. + shell: bash + run: | + # Parse out what we just built and upload it to scratch storage + echo "paths<> "$GITHUB_OUTPUT" + dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + + cp dist-manifest.json "$BUILD_MANIFEST_NAME" + - name: "Upload artifacts" + uses: actions/upload-artifact@v7 + with: + name: artifacts-build-local-${{ join(matrix.targets, '_') }} + path: | + ${{ steps.cargo-dist.outputs.paths }} + ${{ env.BUILD_MANIFEST_NAME }} + + # Build and package all the platform-agnostic(ish) things + build-global-artifacts: + needs: + - plan + - build-local-artifacts + runs-on: "ubuntu-22.04" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + submodules: recursive + - name: Install cached dist + uses: actions/download-artifact@v8 + with: + name: cargo-dist-cache + path: ~/.cargo/bin/ + - run: chmod +x ~/.cargo/bin/dist + # Get all the local artifacts for the global tasks to use (for e.g. checksums) + - name: Fetch local artifacts + uses: actions/download-artifact@v8 + with: + pattern: artifacts-* + path: target/distrib/ + merge-multiple: true + - id: cargo-dist + shell: bash + run: | + dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json + echo "dist ran successfully" + + # Parse out what we just built and upload it to scratch storage + echo "paths<> "$GITHUB_OUTPUT" + jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + + cp dist-manifest.json "$BUILD_MANIFEST_NAME" + - name: "Upload artifacts" + uses: actions/upload-artifact@v7 + with: + name: artifacts-build-global + path: | + ${{ steps.cargo-dist.outputs.paths }} + ${{ env.BUILD_MANIFEST_NAME }} + # Determines if we should publish/announce + host: + needs: + - plan + - build-local-artifacts + - build-global-artifacts + # Only run if we're "publishing", and only if plan, local and global didn't fail (skipped is fine) + if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + runs-on: "ubuntu-22.04" + outputs: + val: ${{ steps.host.outputs.manifest }} + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + submodules: recursive + - name: Install cached dist + uses: actions/download-artifact@v8 + with: + name: cargo-dist-cache + path: ~/.cargo/bin/ + - run: chmod +x ~/.cargo/bin/dist + # Fetch artifacts from scratch-storage + - name: Fetch artifacts + uses: actions/download-artifact@v8 + with: + pattern: artifacts-* + path: target/distrib/ + merge-multiple: true + - id: host + shell: bash + run: | + dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json + echo "artifacts uploaded and released successfully" + cat dist-manifest.json + echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" + - name: "Upload dist-manifest.json" + uses: actions/upload-artifact@v7 + with: + # Overwrite the previous copy + name: artifacts-dist-manifest + path: dist-manifest.json + # Create a GitHub Release while uploading all files to it + - name: "Download GitHub Artifacts" + uses: actions/download-artifact@v8 + with: + pattern: artifacts-* + path: artifacts + merge-multiple: true + - name: Cleanup + run: | + # Remove the granular manifests + rm -f artifacts/*-dist-manifest.json + - name: Create GitHub Release + env: + PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}" + ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}" + ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}" + RELEASE_COMMIT: "${{ github.sha }}" + run: | + # Write and read notes from a file to avoid quoting breaking things + echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt + + gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/* + + publish-homebrew-formula: + needs: + - plan + - host + runs-on: "ubuntu-22.04" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PLAN: ${{ needs.plan.outputs.val }} + GITHUB_USER: "axo bot" + GITHUB_EMAIL: "admin+bot@axo.dev" + if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }} + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: true + repository: "trustgraph/homebrew-tap" + token: ${{ secrets.HOMEBREW_TAP_TOKEN }} + # So we have access to the formula + - name: Fetch homebrew formulae + uses: actions/download-artifact@v8 + with: + pattern: artifacts-* + path: Formula/ + merge-multiple: true + # This is extra complex because you can make your Formula name not match your app name + # so we need to find releases with a *.rb file, and publish with that filename. + - name: Commit formula files + run: | + git config --global user.name "${GITHUB_USER}" + git config --global user.email "${GITHUB_EMAIL}" + + for release in $(echo "$PLAN" | jq --compact-output '.releases[] | select([.artifacts[] | endswith(".rb")] | any)'); do + filename=$(echo "$release" | jq '.artifacts[] | select(endswith(".rb"))' --raw-output) + name=$(echo "$filename" | sed "s/\.rb$//") + version=$(echo "$release" | jq .app_version --raw-output) + + export PATH="/home/linuxbrew/.linuxbrew/bin:$PATH" + brew update + # We avoid reformatting user-provided data such as the app description and homepage. + brew style --except-cops FormulaAudit/Homepage,FormulaAudit/Desc,FormulaAuditStrict --fix "Formula/${filename}" || true + + git add "Formula/${filename}" + git commit -m "${name} ${version}" + done + git push + + announce: + needs: + - plan + - host + - publish-homebrew-formula + # use "always() && ..." to allow us to wait for all publish jobs while + # still allowing individual publish jobs to skip themselves (for prereleases). + # "host" however must run to completion, no skipping allowed! + if: ${{ always() && needs.host.result == 'success' && (needs.publish-homebrew-formula.result == 'skipped' || needs.publish-homebrew-formula.result == 'success') }} + runs-on: "ubuntu-22.04" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + submodules: recursive diff --git a/Cargo.toml b/Cargo.toml index 494f1e1..12725d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,6 +56,10 @@ lto = "thin" codegen-units = 1 strip = true +# The profile that `dist` builds release binaries with (dist-workspace.toml). +[profile.dist] +inherits = "release" + # Small, fast-loading WebAssembly: `cargo build --profile wasm --target wasm32-unknown-unknown -p trustgraph-wasm` [profile.wasm] inherits = "release" diff --git a/README.md b/README.md index 5d4244d..87e3125 100644 --- a/README.md +++ b/README.md @@ -41,21 +41,37 @@ curl -fsSL https://raw.githubusercontent.com/trustgraph/trustgraph-rust-cli/refs ``` This puts `trust` in `~/.local/bin` (set `TRUST_INSTALL_DIR` to change it). It -uses a prebuilt binary when the release has one for your platform, and otherwise +runs the installer from the latest [release](https://github.com/trustgraph/trustgraph-rust-cli/releases), +which picks the prebuilt binary for your platform (Linux glibc or musl, macOS, +on x86_64 or Arm), checks its SHA-256 and adds `~/.local/bin` to your PATH +(`| sh -s -- --no-modify-path` skips that). With no release for your platform it builds from source, which needs [Rust](https://rustup.rs) 1.85 or newer. [Read the script](install.sh) before you run it if you like. -Or install with cargo directly: +On Windows (PowerShell): + +```powershell +powershell -ExecutionPolicy Bypass -c "irm https://github.com/trustgraph/trustgraph-rust-cli/releases/latest/download/trustgraph-cli-installer.ps1 | iex" +``` + +With Homebrew (macOS, Linux): `brew install trustgraph/tap/trustgraph`. + +Or install with cargo: ```sh -cargo install --git https://github.com/trustgraph/trustgraph-rust-cli trustgraph-cli +cargo install trustgraph-cli # from crates.io +cargo install --git https://github.com/trustgraph/trustgraph-rust-cli trustgraph-cli # latest master ``` Or from a checkout: `cargo install --path crates/trustgraph-cli`. -The JavaScript packages, `@trustgraph/trustgraph-wasm` (WebAssembly) and -`@trustgraph/trustgraph` (native), are not on npm yet. To build them locally, see -[architecture](doc/architecture.md#building). +The JavaScript packages are `@trustgraph/trustgraph-wasm` (WebAssembly) and +`@trustgraph/trustgraph` (native, prebuilt for Linux, macOS and Windows) on +npm: `pnpm add @trustgraph/trustgraph-wasm`. To build them locally, see +[architecture](doc/architecture.md#building). Releases are cut as described in +[doc/releasing.md](doc/releasing.md); until the first one, the prebuilt +binaries, Homebrew, crates.io and npm routes are not live yet and the script +builds from source. ## Quick start diff --git a/crates/trustgraph-cli/Cargo.toml b/crates/trustgraph-cli/Cargo.toml index 4e3f921..7561521 100644 --- a/crates/trustgraph-cli/Cargo.toml +++ b/crates/trustgraph-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "trustgraph-cli" -description = "`trust`: the Trust Graph command line interface" +description = "Trust Graph command line interface (`trust`): sign trust atoms and see the world through your agent lens" readme = "../../README.md" keywords = ["trust", "reputation", "did", "cli", "trustgraph"] categories = ["command-line-utilities", "cryptography"] @@ -11,7 +11,6 @@ license.workspace = true repository.workspace = true homepage.workspace = true authors.workspace = true -publish = false [[bin]] name = "trust" diff --git a/crates/trustgraph-core/Cargo.toml b/crates/trustgraph-core/Cargo.toml index 84bcd54..b8bbe0a 100644 --- a/crates/trustgraph-core/Cargo.toml +++ b/crates/trustgraph-core/Cargo.toml @@ -11,7 +11,6 @@ license.workspace = true repository.workspace = true homepage.workspace = true authors.workspace = true -publish = false [dependencies] bs58.workspace = true diff --git a/crates/trustgraph-core/README.md b/crates/trustgraph-core/README.md index 75b1ad1..cb6d161 100644 --- a/crates/trustgraph-core/README.md +++ b/crates/trustgraph-core/README.md @@ -9,4 +9,4 @@ The [Trust Graph](https://trustgraph.net) protocol in pure Rust, with **no I/O** - `Record` / `Query`: verified atoms and filters - `api`: the JSON-shaped API shared by the CLI, WebAssembly and Node wrappers -See the [architecture](../../doc/architecture.md). +See the [architecture](https://github.com/trustgraph/trustgraph-rust-cli/blob/master/doc/architecture.md). diff --git a/crates/trustgraph-node/.gitignore b/crates/trustgraph-node/.gitignore index 10b710a..fb9fb33 100644 --- a/crates/trustgraph-node/.gitignore +++ b/crates/trustgraph-node/.gitignore @@ -3,3 +3,7 @@ index.js # Generated by `npm run build`: index.d.ts trustgraph.d.ts +# Assembled by .github/workflows/npm.yml when publishing: +/npm/ +/artifacts/ +/LICENSE diff --git a/crates/trustgraph-node/README.md b/crates/trustgraph-node/README.md index 351f264..02c64e8 100644 --- a/crates/trustgraph-node/README.md +++ b/crates/trustgraph-node/README.md @@ -3,7 +3,7 @@ [Trust Graph](https://trustgraph.net) as a native Node.js module (napi-rs). Use it for heavy work in Node, including Convex Node actions via `externalPackages`. Same API as `@trustgraph/trustgraph-wasm`; see the -[types](../../bindings/trustgraph.d.ts) and the [architecture](../../doc/architecture.md). +[types](https://github.com/trustgraph/trustgraph-rust-cli/blob/master/bindings/trustgraph.d.ts) and the [architecture](https://github.com/trustgraph/trustgraph-rust-cli/blob/master/doc/architecture.md). ```sh pnpm install && pnpm run build:node && pnpm run test:node # from the repo root diff --git a/crates/trustgraph-wasm/README.md b/crates/trustgraph-wasm/README.md index 145a2a6..d589249 100644 --- a/crates/trustgraph-wasm/README.md +++ b/crates/trustgraph-wasm/README.md @@ -3,6 +3,6 @@ [Trust Graph](https://trustgraph.net) compiled to WebAssembly. Runs in browsers, Cloudflare Workers, Deno, Node, and inside Convex queries and mutations. Same API as the native `@trustgraph/trustgraph` package; see the -[types](../../bindings/trustgraph.d.ts) and the [architecture](../../doc/architecture.md). +[types](https://github.com/trustgraph/trustgraph-rust-cli/blob/master/bindings/trustgraph.d.ts) and the [architecture](https://github.com/trustgraph/trustgraph-rust-cli/blob/master/doc/architecture.md). Build with `scripts/build-wasm-package.sh` (output in `target/npm/trustgraph-wasm`). diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..e143438 --- /dev/null +++ b/deny.toml @@ -0,0 +1,48 @@ +# cargo-deny: licenses, security advisories, duplicate crates and sources. +# cargo deny check +# Docs: https://embarkstudios.github.io/cargo-deny/ + +[graph] +all-features = true + +[advisories] +# RustSec advisory database. Vulnerabilities and unsound crates fail the +# check; yanked versions do too. +yanked = "deny" +ignore = [] + +[licenses] +# Everything we ship is Apache-2.0, so every dependency must be compatible +# with distributing it under Apache-2.0. +allow = [ + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "BSD-2-Clause", + "BSD-3-Clause", + "BSL-1.0", + "ISC", + "MIT", + "MPL-2.0", # option-ext (via directories); file-level copyleft, unmodified + "Unicode-3.0", + "Unlicense", + "Zlib", +] +confidence-threshold = 0.9 +# The list above is a policy, not an inventory. +unused-allowed-license = "allow" + +[licenses.private] +# Workspace crates that are not published (wasm, node) still have a license. +ignore = false + +[bans] +# Two versions of one crate is worth knowing about, not worth failing CI. +multiple-versions = "warn" +wildcards = "deny" +# Path dependencies inside the workspace have no version requirement. +allow-wildcard-paths = true + +[sources] +unknown-registry = "deny" +unknown-git = "deny" +allow-registry = ["https://github.com/rust-lang/crates.io-index"] diff --git a/dist-workspace.toml b/dist-workspace.toml new file mode 100644 index 0000000..07b0405 --- /dev/null +++ b/dist-workspace.toml @@ -0,0 +1,54 @@ +[workspace] +members = ["cargo:."] + +# Config for 'dist' (prebuilt `trust` binaries, installers, Homebrew). +# See doc/releasing.md and https://axodotdev.github.io/cargo-dist/book/. +# After editing, run `dist generate` to update .github/workflows/release.yml. +[dist] +# The preferred dist version to use in CI (Cargo.toml SemVer syntax) +cargo-dist-version = "0.33.0" +# CI backends to support +ci = "github" +# The installers to generate for each app +installers = ["shell", "powershell", "homebrew"] +# A GitHub repo to push Homebrew formulas to +tap = "trustgraph/homebrew-tap" +# Publish jobs to run in CI +publish-jobs = ["homebrew"] +# Target platforms to build apps for (Rust target-triple syntax) +targets = [ + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "aarch64-unknown-linux-gnu", + "x86_64-unknown-linux-gnu", + "aarch64-unknown-linux-musl", + "x86_64-unknown-linux-musl", + "x86_64-pc-windows-msvc", +] +# Build only trustgraph-cli, not the whole workspace (the napi and wasm +# cdylibs do not build for every target). +precise-builds = true +# `brew install trustgraph/tap/trustgraph` (the binary is still `trust`) +formula = "trustgraph" +# Where the shell and PowerShell installers put `trust`: $XDG_BIN_HOME, or +# ~/.local/bin (the same place as install.sh's source-build fallback). +install-path = ["$XDG_BIN_HOME/", "~/.local/bin"] +# Whether to install a standalone updater program +install-updater = false +# Verifiable build provenance for every archive (`gh attestation verify`). +github-attestations = true +# Which actions to run on pull requests: check the release plan only. +pr-run-mode = "plan" +# Keep the generated workflow on the same action majors as the rest of CI. +# Dependabot skips release.yml (see .github/dependabot.yml): bump these here +# and run `dist generate` instead. +[dist.github-action-commits] +"actions/checkout" = "v7" +"actions/upload-artifact" = "v7" +"actions/download-artifact" = "v8" +"actions/attest" = "v4" + +# aarch64 Linux builds natively on GitHub's Arm runners. +[dist.github-custom-runners] +aarch64-unknown-linux-gnu = "ubuntu-22.04-arm" +aarch64-unknown-linux-musl = "ubuntu-22.04-arm" diff --git a/doc/architecture.md b/doc/architecture.md index 0defdea..7532bc6 100644 --- a/doc/architecture.md +++ b/doc/architecture.md @@ -168,15 +168,17 @@ The native package uses napi-rs's per-platform layout (one small npm package per OS/CPU, installed as optional dependencies, chosen by the generated `index.js`), the same pattern as `@resvg/resvg-js`. Targets are listed in [`crates/trustgraph-node/package.json`](../crates/trustgraph-node/package.json). -CI builds and tests it on Linux, macOS and Windows. +CI builds and tests it on Linux, macOS and Windows. Releases build all eight +targets and publish both npm packages from `.github/workflows/npm.yml`; see +[releasing](releasing.md). ## Open items 1. **Publishing.** Convex's `externalPackages` installs from npm at deploy time, so `@trustgraph/trustgraph` must be published. Decide whether the `@trustgraph` scope is public; if it must be private, first test that Convex - can install from a private registry. Then add the napi-rs cross-compile and - publish matrix to the release workflow. + can install from a private registry. The napi-rs cross-compile and publish + matrix is in place ([releasing](releasing.md)); it publishes publicly. 2. **A real Convex spike.** The numbers above come from Node. Before relying on it, deploy a small query that imports `@trustgraph/trustgraph-wasm` and runs `lens`, and confirm the bundle size and cold-start time in Convex diff --git a/doc/plan/README.md b/doc/plan/README.md index ef8a4cf..1ba2519 100644 --- a/doc/plan/README.md +++ b/doc/plan/README.md @@ -105,6 +105,7 @@ Everything below this line is supporting detail. | Shared TypeScript types; one smoke test across all JS builds ([PR #15](https://github.com/trustgraph/trustgraph-rust-cli/pull/15)) | ✅ | | `lens` on 100k atoms (Node 24 LTS): 0.54 s WebAssembly, 0.43 s native ([PR #15](https://github.com/trustgraph/trustgraph-rust-cli/pull/15)) | ✅ | | CI: purity check, WebAssembly size budget, native addon on 3 OSes, latest Node LTS plus Node 22 ([PR #15](https://github.com/trustgraph/trustgraph-rust-cli/pull/15)) | ✅ | +| Release pipeline: dist binaries and installers, Homebrew tap, release-plz, npm publishing (native + WebAssembly), cargo-deny, coverage ([PR #24](https://github.com/trustgraph/trustgraph-rust-cli/pull/24)) | ✅ ready; first release waits on secrets ([releasing](../releasing.md)) | | Old repos imported with full history: protocol README → `doc/protocol/`, 2017 JSON-LD → `schema/legacy-2017/` | ✅ | ## Design principles diff --git a/doc/releasing.md b/doc/releasing.md new file mode 100644 index 0000000..67fd297 --- /dev/null +++ b/doc/releasing.md @@ -0,0 +1,146 @@ +# Releasing + +One version covers everything: `workspace.package.version` in `Cargo.toml`. +A release is one git tag, `vX.Y.Z`, and ships: + +| What | Where | Built by | +|---|---|---| +| `trustgraph-core`, `trustgraph-cli` | crates.io | release-plz ([`release-plz.yml`](../.github/workflows/release-plz.yml), [`release-plz.toml`](../release-plz.toml)) | +| `trust` binaries for Linux (glibc and musl), macOS (x86_64, Arm), Windows (x86_64); shell and PowerShell installers; checksums and build attestations | GitHub Release | dist ([`release.yml`](../.github/workflows/release.yml), generated from [`dist-workspace.toml`](../dist-workspace.toml)) | +| Homebrew formula `trustgraph` | [`trustgraph/homebrew-tap`](https://github.com/trustgraph/homebrew-tap) | dist | +| `@trustgraph/trustgraph` (native, 8 per-platform packages plus the root) and `@trustgraph/trustgraph-wasm` | npm, with provenance | [`npm.yml`](../.github/workflows/npm.yml) | +| CHANGELOG.md, version bump | release PR | release-plz | + +`trustgraph-wasm` and `trustgraph-node` are `publish = false` on crates.io; +they ship only through npm. The versions in their `package.json` files are +overwritten from the tag when publishing, so they never need editing. + +## Cutting a release + +1. Merge work into `master` with [Conventional Commits](https://www.conventionalcommits.org) + titles (`feat: …`, `fix: …`, `feat!: …` for a breaking change). release-plz + uses them to pick the next version and write the CHANGELOG. Before 1.0 a + breaking change bumps the minor version. +2. On every push to `master`, release-plz opens or updates a release PR + (label `release`) that bumps the version and adds a CHANGELOG entry. Read the CHANGELOG in it, edit it + if you like, and wait for CI. +3. Merge the release PR. release-plz then publishes `trustgraph-core` and + `trustgraph-cli` to crates.io and pushes the tag `vX.Y.Z`. +4. The tag starts two workflows: + - **Release** (dist): builds the seven `trust` archives, the installers and + the Homebrew formula, creates the GitHub Release with notes from + CHANGELOG.md, and pushes the formula to the tap. + - **npm**: builds the native addon for eight targets, smoke-tests it on + glibc and musl Linux (x86_64 and Arm), macOS and Windows, builds the + WebAssembly package, and publishes all ten npm packages. +5. Check the [release page](https://github.com/trustgraph/trustgraph-rust-cli/releases), + [crates.io](https://crates.io/crates/trustgraph-cli) and + [npm](https://www.npmjs.com/package/@trustgraph/trustgraph). + +A version with a suffix (`v1.0.0-rc.1`) is a prerelease: the GitHub Release is +marked as one, Homebrew is skipped, and npm publishes under the `next` tag. + +Every publish step skips what is already published, so a release that failed +half way can be re-run from the Actions tab. + +### Without release-plz + +Bump `workspace.package.version`, commit, then: + +```sh +cargo publish -p trustgraph-core -p trustgraph-cli +git tag vX.Y.Z && git push origin vX.Y.Z +``` + +## One-time setup + +None of this exists yet. Until it does, the release workflows skip (release-plz) +or never run (they need a tag); pull requests need no secrets. + +1. **Package names.** Confirm `trustgraph-core` / `trustgraph-cli` on crates.io + and the `@trustgraph` scope on npm (an open question in the + [roadmap](plan/README.md)). Create the `trustgraph` organization on + [npmjs.com](https://www.npmjs.com/org/create) to own the scope. + +2. **release-plz token** (`RELEASE_PLZ_TOKEN`). Tags and PRs created with the + default `GITHUB_TOKEN` do not start other workflows, so release-plz needs + its own token. Create a + [fine-grained personal access token](https://github.com/settings/personal-access-tokens/new) + (ideally on a bot account, which becomes the PR author) for + `trustgraph/trustgraph-rust-cli` with **Contents** and **Pull requests**: + read and write. Save it as the repository secret `RELEASE_PLZ_TOKEN` + (Settings → Secrets and variables → Actions). A GitHub App token works too; + see the [release-plz docs](https://release-plz.dev/docs/github/token). + +3. **crates.io.** New crates cannot use trusted publishing, so the first + release needs a token: + - Create an [API token](https://crates.io/settings/tokens) with the + `publish-new` and `publish-update` scopes, limited to `trustgraph-*`, and + save it as the secret `CARGO_REGISTRY_TOKEN`. + - After the first release, open each crate's settings on crates.io + (`trustgraph-core`, `trustgraph-cli`) → Trusted Publishing → add GitHub: + owner `trustgraph`, repository `trustgraph-rust-cli`, workflow + `release-plz.yml`. Then delete the `CARGO_REGISTRY_TOKEN` secret and the + token; release-plz switches to trusted publishing on its own. + +4. **Homebrew tap.** Create the public repository + [`trustgraph/homebrew-tap`](https://github.com/organizations/trustgraph/repositories/new) + with a README; dist manages `Formula/` in it. Create a fine-grained token + for that repository with **Contents**: read and write, and save it in this + repository as `HOMEBREW_TAP_TOKEN`. Without it, the Homebrew job fails and + dist does not publish the GitHub Release. + +5. **npm.** New packages cannot use trusted publishing either: + - Create a [granular access token](https://www.npmjs.com/settings/~/tokens/granular-access-tokens/new) + with read and write access to the `@trustgraph` scope (and "bypass + two-factor authentication" for publishing) and save it as `NPM_TOKEN`. + - After the first release, for each of the ten packages + (`@trustgraph/trustgraph`, `@trustgraph/trustgraph-wasm`, and the eight + `@trustgraph/trustgraph-` packages) open Settings → Trusted + Publisher → GitHub Actions: organization `trustgraph`, repository + `trustgraph-rust-cli`, workflow `npm.yml`. Then delete `NPM_TOKEN` and the + token, and set each package to "require two-factor authentication and + disallow tokens". Provenance is published either way. + +6. **Optional:** protect `v*` tags and the `master` branch so that only the + release-plz token (or admins) can create release tags. + +When the repository is renamed to `trustgraph/trustgraph`, update the +`repository` URLs in `Cargo.toml` and the `package.json` files, the trusted +publisher settings on crates.io and npm, and the URLs in `install.sh` and +README.md (GitHub redirects the old ones in the meantime). + +## Checking the pipeline without publishing + +- **Every pull request** runs `dist plan` (the Release workflow), and + `cargo-deny` and coverage (CI). +- **Pull requests that touch `npm.yml`** or the npm build, and manual runs + (Actions → npm → Run workflow), build and test every npm package and run + `pnpm publish --dry-run` for each. +- Locally: + + ```sh + cargo publish --dry-run -p trustgraph-core -p trustgraph-cli + dist plan # what a release would contain + dist build --artifacts=all # archives and installers for this machine, in target/distrib + cargo deny check + ``` + + Install dist with `cargo install --locked cargo-dist` or from its + [releases](https://github.com/axodotdev/cargo-dist/releases), at the version + in `dist-workspace.toml`. + +## Changing the release setup + +- **dist:** edit `dist-workspace.toml`, then run `dist generate` to rewrite + `.github/workflows/release.yml`. Never edit `release.yml` by hand: the + Release workflow fails on pull requests when it is out of date. That is also + why Dependabot skips it; bump its actions in `[dist.github-action-commits]`. + To upgrade dist, change `cargo-dist-version` and run `dist generate` with + that version. +- **Targets:** the `trust` binaries are listed in `dist-workspace.toml`; the + native npm targets in `crates/trustgraph-node/package.json` (`napi.targets`) + and the matrix in `npm.yml`. +- **install.sh** is the stable `curl | sh` entry point. It downloads and runs + `trustgraph-cli-installer.sh` from the release (dist's installer: platform + detection, checksums, PATH setup) and falls back to building with cargo. diff --git a/install.sh b/install.sh index 007d009..2beb1b2 100755 --- a/install.sh +++ b/install.sh @@ -3,59 +3,54 @@ # # curl -fsSL https://raw.githubusercontent.com/trustgraph/trustgraph-rust-cli/refs/heads/master/install.sh | sh # -# Sketch: uses a prebuilt binary from the latest GitHub release when one exists -# (none yet, see doc/plan), otherwise builds from source with cargo. +# This is a stable entry point that defers to the installer `dist` attaches to +# each GitHub release (trustgraph-cli-installer.sh, see doc/releasing.md). That +# installer picks the right prebuilt binary (glibc or musl on Linux, Intel or +# Apple silicon on macOS), checks its SHA-256 and adds it to your PATH. When +# there is no release yet, or none for your platform, this builds from source +# with cargo instead. +# +# Arguments are passed to the release installer, for example: +# curl -fsSL .../install.sh | sh -s -- --no-modify-path # # Environment: -# TRUST_INSTALL_DIR where to put the binary (default: ~/.local/bin) -# TRUST_VERSION release tag to install (default: latest) +# TRUST_INSTALL_DIR where to put the binary (default: $XDG_BIN_HOME or ~/.local/bin) +# TRUST_VERSION release tag to install, e.g. v0.2.0 (default: latest) +# TRUST_FROM_SOURCE set to 1 to always build from source set -eu REPO="trustgraph/trustgraph-rust-cli" -INSTALL_DIR="${TRUST_INSTALL_DIR:-$HOME/.local/bin}" VERSION="${TRUST_VERSION:-latest}" say() { printf 'trust-install: %s\n' "$*" >&2; } die() { say "error: $*"; exit 1; } has() { command -v "$1" >/dev/null 2>&1; } -target() { - os=$(uname -s) arch=$(uname -m) - case "$arch" in - x86_64 | amd64) arch=x86_64 ;; - arm64 | aarch64) arch=aarch64 ;; - *) return 1 ;; - esac - case "$os" in - Linux) echo "$arch-unknown-linux-gnu" ;; - Darwin) echo "$arch-apple-darwin" ;; - *) return 1 ;; - esac -} - install_release() { - has curl && has tar || return 1 - t=$(target) || return 1 + [ "${TRUST_FROM_SOURCE:-0}" = 1 ] && return 1 + has curl || return 1 + case "$(uname -s)" in Linux | Darwin) ;; *) return 1 ;; esac if [ "$VERSION" = latest ]; then - url="https://github.com/$REPO/releases/latest/download/trustgraph-cli-$t.tar.xz" + url="https://github.com/$REPO/releases/latest/download/trustgraph-cli-installer.sh" else - url="https://github.com/$REPO/releases/download/$VERSION/trustgraph-cli-$t.tar.xz" + url="https://github.com/$REPO/releases/download/$VERSION/trustgraph-cli-installer.sh" fi tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT - curl -fsSL "$url" -o "$tmp/trust.tar.xz" 2>/dev/null || return 1 - say "downloaded $url" - tar -xJf "$tmp/trust.tar.xz" -C "$tmp" - bin=$(find "$tmp" -type f -name trust | head -n 1) - [ -n "$bin" ] || die "release archive has no 'trust' binary" - mkdir -p "$INSTALL_DIR" - install -m 755 "$bin" "$INSTALL_DIR/trust" + curl -fsSL "$url" -o "$tmp/installer.sh" 2>/dev/null || return 1 + say "running the release installer from $url" + if [ -n "${TRUST_INSTALL_DIR:-}" ]; then + TRUSTGRAPH_CLI_INSTALL_DIR="$TRUST_INSTALL_DIR" sh "$tmp/installer.sh" "$@" + else + sh "$tmp/installer.sh" "$@" + fi } install_source() { has cargo || die "no prebuilt binary for this platform and no cargo found. Install Rust (https://rustup.rs) and run this again." + dir="${TRUST_INSTALL_DIR:-${XDG_BIN_HOME:-$HOME/.local/bin}}" say "no prebuilt binary; building from source with cargo (takes a minute)" ref="--branch master" [ "$VERSION" = latest ] || ref="--tag $VERSION" @@ -64,17 +59,17 @@ install_source() { # shellcheck disable=SC2086 # $ref is two words on purpose cargo install --locked --git "https://github.com/$REPO" $ref \ --root "$tmp" trustgraph-cli - mkdir -p "$INSTALL_DIR" - install -m 755 "$tmp/bin/trust" "$INSTALL_DIR/trust" + mkdir -p "$dir" + install -m 755 "$tmp/bin/trust" "$dir/trust" + say "installed $dir/trust" + case ":$PATH:" in + *":$dir:"*) ;; + *) say "add $dir to your PATH: export PATH=\"$dir:\$PATH\"" ;; + esac } main() { - if install_release; then :; else install_source; fi - say "installed $INSTALL_DIR/trust" - case ":$PATH:" in - *":$INSTALL_DIR:"*) ;; - *) say "add $INSTALL_DIR to your PATH: export PATH=\"$INSTALL_DIR:\$PATH\"" ;; - esac + if install_release "$@"; then :; else install_source; fi say "next: trust key new" } diff --git a/release-plz.toml b/release-plz.toml new file mode 100644 index 0000000..7eba489 --- /dev/null +++ b/release-plz.toml @@ -0,0 +1,34 @@ +# release-plz: opens a release PR (version bump + CHANGELOG.md) on every push +# to master, and publishes to crates.io when that PR is merged. +# See doc/releasing.md and https://release-plz.dev/docs/config. +# +# Every crate shares one version (workspace.package.version), and a release +# is one tag, `vX.Y.Z`. Pushing that tag starts the binary build (dist, +# .github/workflows/release.yml) and the npm packages (.github/workflows/npm.yml). + +[workspace] +# One CHANGELOG.md at the repo root, written for trustgraph-cli (below). +changelog_update = false +git_tag_enable = false +# dist creates the GitHub Release (with binaries and installers) from the tag. +git_release_enable = false +# Merge the release PR yourself; never release on an unrelated push. +release_always = false +pr_labels = ["release"] + +[[package]] +name = "trustgraph-cli" +# The one CHANGELOG and the one tag for the whole release live here. +changelog_path = "./CHANGELOG.md" +changelog_update = true +changelog_include = ["trustgraph-core"] +git_tag_enable = true +git_tag_name = "v{{ version }}" +version_group = "trustgraph" + +[[package]] +name = "trustgraph-core" +version_group = "trustgraph" + +[changelog] +protect_breaking_commits = true diff --git a/scripts/build-wasm-package.sh b/scripts/build-wasm-package.sh index cbd233d..0062932 100755 --- a/scripts/build-wasm-package.sh +++ b/scripts/build-wasm-package.sh @@ -19,7 +19,7 @@ wasm-bindgen --target web --out-dir "$out/web" "$wasm" wasm-bindgen --target nodejs --out-dir "$out/node" "$wasm" # The package is "type": "module"; the Node build is CommonJS. mv "$out/node/trustgraph_wasm.js" "$out/node/trustgraph_wasm.cjs" -cp crates/trustgraph-wasm/package.json bindings/trustgraph.d.ts "$out/" +cp crates/trustgraph-wasm/package.json crates/trustgraph-wasm/README.md LICENSE bindings/trustgraph.d.ts "$out/" bytes=$(wc -c <"$out/web/trustgraph_wasm_bg.wasm") gzipped=$(gzip -9 -c "$out/web/trustgraph_wasm_bg.wasm" | wc -c) diff --git a/scripts/npm-publish.sh b/scripts/npm-publish.sh new file mode 100755 index 0000000..a34f0ef --- /dev/null +++ b/scripts/npm-publish.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# Publishes one npm package directory with pnpm, with provenance. Used by +# .github/workflows/npm.yml; see doc/releasing.md. +# +# scripts/npm-publish.sh DIR [--dry-run] +# +# A version that is already on the registry is skipped, so a release that +# failed half way can simply be re-run. Authentication is npm trusted +# publishing (OIDC), or an NPM_TOKEN written to ~/.npmrc by the workflow. +set -euo pipefail + +dir=${1:?usage: scripts/npm-publish.sh DIR [--dry-run]} +dry_run=${2:-} +manifest="$(cd "$dir" && pwd)/package.json" +name=$(node -p 'require(process.argv[1]).name' "$manifest") +version=$(node -p 'require(process.argv[1]).version' "$manifest") +spec="$name@$version" + +if pnpm view "$spec" version >/dev/null 2>&1; then + echo "$spec is already published, skipping" + exit 0 +fi + +flags=(--access public --no-git-checks) +# Prereleases (1.0.0-rc.1) go to the `next` dist-tag, so `latest` stays stable. +case "$version" in *-*) flags+=(--tag next) ;; esac +if [ "$dry_run" = --dry-run ]; then + flags+=(--dry-run) +else + flags+=(--provenance) +fi + +echo "publishing $spec from $dir ${dry_run}" +(cd "$dir" && pnpm publish "${flags[@]}")