From 80691d5128ed42476c08392738199ba8269983ce Mon Sep 17 00:00:00 2001 From: 0xisk <0xisk@proton.me> Date: Tue, 15 Sep 2026 11:30:39 +0200 Subject: [PATCH 1/3] build(compact-lint): ship prebuilt binaries through npm Adds the @openzeppelin/compact-lint npm package, a Node bin that fetches the prebuilt compact-lint binary for the host from the GitHub release of the same version, verifies it against checksums.txt, caches it inside the package, and passes arguments and exit codes through. COMPACT_LINT_BINARY skips the download for source builds and air-gapped CI. * release.yml: compact-lint bumps package.json and the crate version together and refreshes Cargo.lock. * release-publish.yml: detection becomes its own job; a four-runner matrix builds the Linux and macOS binaries and the publish job creates the GitHub release with the assets before npm publish, so the package never points at a release that is not there yet. * Cargo.toml: stripped, thin-LTO release profile (3.3 MiB binary). * tests: target mapping, checksum verification, the download path against a local server, pass-through, and a guard that the npm and crate versions agree. --- .github/workflows/release-publish.yml | 175 +++++++++++++-- .github/workflows/release.yml | 40 ++++ .gitignore | 3 + Cargo.toml | 7 + README.md | 6 +- RELEASING.md | 40 +++- crates/compact-lint/README.md | 7 + packages/lint/CHANGELOG.md | 13 ++ packages/lint/LICENSE | 21 ++ packages/lint/README.md | 45 ++++ packages/lint/package.json | 43 ++++ packages/lint/src/binary.ts | 143 +++++++++++++ packages/lint/src/compactLint.ts | 52 +++++ packages/lint/test/binary.test.ts | 293 ++++++++++++++++++++++++++ packages/lint/test/version.test.ts | 22 ++ packages/lint/tsconfig.json | 21 ++ packages/lint/turbo.json | 11 + packages/lint/vitest.config.ts | 22 ++ yarn.lock | 13 ++ 19 files changed, 955 insertions(+), 22 deletions(-) create mode 100644 packages/lint/CHANGELOG.md create mode 100644 packages/lint/LICENSE create mode 100644 packages/lint/README.md create mode 100644 packages/lint/package.json create mode 100644 packages/lint/src/binary.ts create mode 100644 packages/lint/src/compactLint.ts create mode 100644 packages/lint/test/binary.test.ts create mode 100644 packages/lint/test/version.test.ts create mode 100644 packages/lint/tsconfig.json create mode 100644 packages/lint/turbo.json create mode 100644 packages/lint/vitest.config.ts diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index 09a3060..2e34bc0 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -19,30 +19,30 @@ on: - compact-builder - compact-cli - compact-deployer + - compact-lint - compact-simulator jobs: - publish: - name: Publish merged release + # Detection is its own job so the binary matrix and the publish both read the + # same answer. + detect: + name: Detect released package if: >- github.event_name == 'workflow_dispatch' || (github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release')) runs-on: ubuntu-24.04 - environment: compact-npm-prod # Final approval gate before npm publish permissions: - contents: write # push the version tag - id-token: write # mint the npm OIDC credential and the provenance statement + contents: read - steps: - - name: Get github app token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 - id: gh-app-token - with: - app-id: ${{ vars.GH_APP_ID }} - private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} + outputs: + dir: ${{ steps.pkg.outputs.dir }} + name: ${{ steps.pkg.outputs.name }} + version: ${{ steps.pkg.outputs.version }} + dist_tag: ${{ steps.pkg.outputs.dist_tag }} + steps: # For pull_request runs: checkout the merge commit (deterministic view of # what was just merged). For manual dispatch: checkout the dispatched ref # (defaults to main but can be any branch — hotfix, release-prep, etc.). @@ -53,7 +53,6 @@ jobs: with: fetch-depth: 0 ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.merge_commit_sha || github.ref }} - token: ${{ steps.gh-app-token.outputs.token }} - name: Detect released package id: pkg @@ -69,6 +68,7 @@ jobs: compact-builder) DIR="builder" ;; compact-cli) DIR="cli" ;; compact-deployer) DIR="deployer" ;; + compact-lint) DIR="lint" ;; compact-simulator) DIR="simulator" ;; *) echo "::error::unknown package: $PKG" @@ -88,6 +88,7 @@ jobs: builder) PKG="compact-builder" ;; cli) PKG="compact-cli" ;; deployer) PKG="compact-deployer" ;; + lint) PKG="compact-lint" ;; simulator) PKG="compact-simulator" ;; *) echo "::error::unknown package directory: $DIR" @@ -135,6 +136,98 @@ jobs: echo "- Trigger: $EVENT" } >> $GITHUB_STEP_SUMMARY + # compact-lint ships a native binary per target, built on its own runner. The + # npm package downloads them, so they must reach the GitHub release first. + binaries: + name: Build compact-lint for ${{ matrix.target }} + needs: detect + if: needs.detect.outputs.name == 'compact-lint' + runs-on: ${{ matrix.runner }} + + permissions: + contents: read + + strategy: + fail-fast: false + matrix: + include: + - target: x86_64-unknown-linux-gnu + runner: ubuntu-24.04 + - target: aarch64-unknown-linux-gnu + runner: ubuntu-24.04-arm + - target: x86_64-apple-darwin + runner: macos-15-intel + - target: aarch64-apple-darwin + runner: macos-15 + + steps: + - name: Harden Runner + uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0 + with: + egress-policy: audit + + - name: Check out target ref + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.merge_commit_sha || github.ref }} + + - name: Install the pinned toolchain + run: | + CHANNEL=$(grep -m1 '^channel' rust-toolchain.toml | cut -d'"' -f2) + rustup toolchain install "$CHANNEL" --profile minimal + rustup target add ${{ matrix.target }} + rustup show active-toolchain + + - name: Build + run: cargo build --release -p compact-lint --target ${{ matrix.target }} + + # Every target is built on its own native runner, so the binary runs here. + - name: Smoke test + run: target/${{ matrix.target }}/release/compact-lint --version + + - name: Stage the release asset + run: | + mkdir -p dist + cp target/${{ matrix.target }}/release/compact-lint dist/compact-lint-${{ matrix.target }} + + - name: Upload the release asset + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: compact-lint-${{ matrix.target }} + path: dist/compact-lint-${{ matrix.target }} + if-no-files-found: error + + publish: + name: Publish merged release + needs: [detect, binaries] + # `binaries` is skipped for every package but compact-lint, and a skipped + # dependency must not skip the publish. + if: >- + always() && + needs.detect.result == 'success' && + (needs.binaries.result == 'success' || needs.binaries.result == 'skipped') + runs-on: ubuntu-24.04 + environment: compact-npm-prod # Final approval gate before npm publish + + permissions: + contents: write # push the version tag and create the GitHub release + id-token: write # mint the npm OIDC credential and the provenance statement + + steps: + - name: Get github app token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + id: gh-app-token + with: + app-id: ${{ vars.GH_APP_ID }} + private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} + + - name: Check out target ref + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.merge_commit_sha || github.ref }} + token: ${{ steps.gh-app-token.outputs.token }} + - name: Setup Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: @@ -148,11 +241,11 @@ jobs: run: yarn install --immutable - name: Build package - run: yarn build --filter=@openzeppelin/${{ steps.pkg.outputs.name }} + run: yarn build --filter=@openzeppelin/${{ needs.detect.outputs.name }} - name: Create and push tag env: - TAG: ${{ steps.pkg.outputs.name }}/v${{ steps.pkg.outputs.version }} + TAG: ${{ needs.detect.outputs.name }}/v${{ needs.detect.outputs.version }} run: | if git ls-remote --tags --exit-code origin "refs/tags/$TAG" >/dev/null; then echo "tag $TAG already on origin, leaving it in place" @@ -161,7 +254,55 @@ jobs: git push origin "$TAG" fi + - name: Download the release binaries + if: needs.detect.outputs.name == 'compact-lint' + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + path: dist + pattern: compact-lint-* + merge-multiple: true + + # Asset names only: the installed package looks each digest up by the name + # it downloads. + - name: Write the checksums + if: needs.detect.outputs.name == 'compact-lint' + working-directory: dist + run: | + sha256sum compact-lint-* > checksums.txt + cat checksums.txt + + # The npm package downloads these assets on first run, so the release has + # to carry them before the package reaches npm. + - name: Create the GitHub release + if: needs.detect.outputs.name == 'compact-lint' + env: + GH_TOKEN: ${{ steps.gh-app-token.outputs.token }} + TAG: ${{ needs.detect.outputs.name }}/v${{ needs.detect.outputs.version }} + VERSION: ${{ needs.detect.outputs.version }} + DIST_TAG: ${{ needs.detect.outputs.dist_tag }} + run: | + NOTES=$(awk -v v="$VERSION" ' + $0 ~ "^## " v "( |$)" { found = 1; next } + found && /^## / { exit } + found { print } + ' packages/lint/CHANGELOG.md) + if [[ -z "${NOTES//[[:space:]]/}" ]]; then + NOTES="$TAG" + fi + if gh release view "$TAG" >/dev/null 2>&1; then + gh release upload "$TAG" dist/* --clobber + else + ARGS=() + if [[ "$DIST_TAG" == "beta" ]]; then + ARGS+=(--prerelease) + fi + gh release create "$TAG" dist/* \ + --title "compact-lint v$VERSION" \ + --notes "$NOTES" \ + "${ARGS[@]}" + fi + - name: Publish to npm run: | - cd packages/${{ steps.pkg.outputs.dir }} - yarn npm publish --access public --provenance --tag ${{ steps.pkg.outputs.dist_tag }} + cd packages/${{ needs.detect.outputs.dir }} + yarn npm publish --access public --provenance --tag ${{ needs.detect.outputs.dist_tag }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8ca6340..13360df 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,6 +11,7 @@ on: - compact-builder - compact-cli - compact-deployer + - compact-lint - compact-simulator version_bump: description: "Version bump type (pre* strategies are beta-only)" @@ -89,6 +90,9 @@ jobs: "compact-deployer") echo "dir=deployer" >> $GITHUB_OUTPUT ;; + "compact-lint") + echo "dir=lint" >> $GITHUB_OUTPUT + ;; "compact-simulator") echo "dir=simulator" >> $GITHUB_OUTPUT ;; @@ -97,6 +101,15 @@ jobs: - name: Setup Environment uses: ./.github/actions/setup + # compact-lint's version lives in the crate too, and bumping it there + # rewrites Cargo.lock. + - name: Install the pinned toolchain + if: inputs.package == 'compact-lint' + run: | + CHANNEL=$(grep -m1 '^channel' rust-toolchain.toml | cut -d'"' -f2) + rustup toolchain install "$CHANNEL" --profile minimal + rustup show active-toolchain + - name: Run tests for package run: yarn test --filter=@openzeppelin/${{ inputs.package }} @@ -134,6 +147,20 @@ jobs: echo "- npm dist-tag: $DIST_TAG" } >> $GITHUB_STEP_SUMMARY + - name: Sync the crate version + if: inputs.package == 'compact-lint' + env: + NEW_VERSION: ${{ steps.version.outputs.new }} + run: | + sed -i "0,/^version = .*/s//version = \"$NEW_VERSION\"/" crates/compact-lint/Cargo.toml + # Resolving the workspace rewrites the crate's own Cargo.lock entry. + cargo metadata --format-version 1 > /tmp/cargo-metadata.json + ACTUAL=$(node -p "JSON.parse(require('fs').readFileSync('/tmp/cargo-metadata.json','utf8')).packages.find((p) => p.name === 'compact-lint').version") + if [[ "$ACTUAL" != "$NEW_VERSION" ]]; then + echo "::error::crates/compact-lint/Cargo.toml is at $ACTUAL, expected $NEW_VERSION" + exit 1 + fi + - name: Verify package contents run: | cd packages/${{ steps.pkg.outputs.dir }} @@ -156,6 +183,7 @@ jobs: fi - name: Commit version bump + if: inputs.package != 'compact-lint' uses: iarekylew00t/verified-bot-commit@42a042b0407248f3c0acac00c389e729e3476e86 # v2.3.5 with: message: "release: ${{ inputs.package }} v${{ steps.version.outputs.new }}" @@ -164,6 +192,18 @@ jobs: files: | packages/${{ steps.pkg.outputs.dir }}/package.json + - name: Commit version bump with the crate + if: inputs.package == 'compact-lint' + uses: iarekylew00t/verified-bot-commit@42a042b0407248f3c0acac00c389e729e3476e86 # v2.3.5 + with: + message: "release: ${{ inputs.package }} v${{ steps.version.outputs.new }}" + token: ${{ steps.gh-app-token.outputs.token }} + ref: ${{ steps.version.outputs.branch }} + files: | + packages/lint/package.json + crates/compact-lint/Cargo.toml + Cargo.lock + - name: Ensure release label exists env: GH_TOKEN: ${{ steps.gh-app-token.outputs.token }} diff --git a/.gitignore b/.gitignore index a66d7c3..757ea36 100644 --- a/.gitignore +++ b/.gitignore @@ -66,6 +66,9 @@ vendor/ target/ .toolkit-cache/ +# compact-lint's release binary, cached inside the installed npm package. +packages/lint/.cache/ + coverage **/reports diff --git a/Cargo.toml b/Cargo.toml index 3e63a37..a235a01 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,13 @@ license = "MIT" repository = "https://github.com/OpenZeppelin/compact-tools" rust-version = "1.96" +# Release binaries ship as GitHub release assets, so size and speed both matter. +# Unwinding stays on: the error paths depend on it. +[profile.release] +strip = true +lto = "thin" +codegen-units = 1 + [workspace.lints.clippy] pedantic = { level = "warn", priority = -1 } diff --git a/README.md b/README.md index 4bbe406..6ae796e 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Tools for compiling, building, testing, and deploying Compact smart contracts. T - [`packages/builder`](./packages/builder) — programmatic library that drives the Compact compiler + builder - [`packages/cli`](./packages/cli) — thin bin wrapper around the builder and deployer libraries (`compact-compiler`, `compact-builder`, `compact-deploy`) - [`packages/deployer`](./packages/deployer) — deployer library that submits a compiled contract to a Midnight network. Pins one Midnight stack: see [Supported stack](./packages/deployer/README.md#supported-stack) +- [`packages/lint`](./packages/lint) — bin wrapper around the [`compact-lint`](./crates/compact-lint) crate, the doc-comment linter for Compact sources - [`packages/simulator`](./packages/simulator) — TypeScript simulator to run and test Compact contracts locally See each package's README for usage, options, and examples. @@ -38,6 +39,9 @@ yarn add --dev @openzeppelin/compact-cli # Simulator — test Compact contracts locally yarn add --dev @openzeppelin/compact-simulator +# Linter — check doc comments in .compact sources +yarn add --dev @openzeppelin/compact-lint + # Deployer — deploy a compiled contract from TypeScript yarn add --dev @openzeppelin/compact-deployer ``` @@ -59,7 +63,7 @@ import { createSimulator } from '@openzeppelin/compact-simulator'; ## Requirements -- Node.js >= 22 for `packages/builder` and `packages/simulator`, >= 24 for `packages/cli` and `packages/deployer` +- Node.js >= 22 for `packages/builder` and `packages/simulator`, >= 24 for `packages/cli`, `packages/deployer` and `packages/lint` - Yarn 4 (Berry) - Turbo - Optional: Midnight Compact toolchain installed and available in `PATH` diff --git a/RELEASING.md b/RELEASING.md index a8ddbbe..ed5e496 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -49,7 +49,8 @@ release notes agree. - Create a git tag. - Publish the package to npm under the channel's dist-tag. 7. Once published, go to "Releases" and create a GitHub release using the - generated tag. Mark beta tags as prereleases. + generated tag. Mark beta tags as prereleases. `compact-lint` is the + exception: the workflow creates its release itself (see below). ## Graduating a beta to stable @@ -80,7 +81,7 @@ breaking change. After that, only `prerelease` moves the counter. Running ## First-release order -There's a one-step dependency chain across the four published packages: +There's a one-step dependency chain across the five published packages: ```text compact-cli (bin wrapper) @@ -88,6 +89,7 @@ compact-cli (bin wrapper) └─ depends on compact-deployer compact-builder (library) compact-deployer (library) +compact-lint (bin wrapper around the compact-lint crate) compact-simulator (library) ``` @@ -98,8 +100,38 @@ each dependent finds its deps already on npm: 1. `compact-builder` (no internal deps) 2. `compact-simulator` (no internal deps) 3. `compact-deployer` (no internal deps) -4. `compact-cli` (depends on `compact-builder` and `compact-deployer`; pull +4. `compact-lint` (no internal deps) +5. `compact-cli` (depends on `compact-builder` and `compact-deployer`; pull `main` first so both bump commits are present locally before triggering) -After the first release, the four packages version independently — bump any +After the first release, the five packages version independently — bump any one of them in isolation without re-publishing the others. + +## compact-lint + +`compact-lint` is a bin wrapper around the `compact-lint` crate, so its release +carries a native binary as well as an npm package. + +- The version lives in two files: `packages/lint/package.json` and + `crates/compact-lint/Cargo.toml`. The release workflow bumps the package, mirrors + the value into the crate, refreshes `Cargo.lock`, and commits all three. A unit + test in `packages/lint` fails when the two drift. +- The publish workflow builds four binaries before publishing, one per target, each + on its own runner: + + | Target | Runner | + | --- | --- | + | `x86_64-unknown-linux-gnu` | `ubuntu-24.04` | + | `aarch64-unknown-linux-gnu` | `ubuntu-24.04-arm` | + | `x86_64-apple-darwin` | `macos-15-intel` | + | `aarch64-apple-darwin` | `macos-15` | + +- It then creates the GitHub release `compact-lint/v` with the four assets + and a `checksums.txt`, and only then publishes to npm. The installed package + downloads its binary from that release, so the assets must be there first. For + every other package the GitHub release is still created by hand (step 7 above). +- Release notes come from the `## ` section of `packages/lint/CHANGELOG.md`, + falling back to the tag name. A `beta` dist-tag marks the release a prerelease. +- Before the first release, add `@openzeppelin/compact-lint` as a Trusted Publisher + on npmjs.org, naming this repo and `.github/workflows/release-publish.yml`. Every + new npm package needs this once, or the publish step cannot authenticate. diff --git a/crates/compact-lint/README.md b/crates/compact-lint/README.md index d85a415..aa30399 100644 --- a/crates/compact-lint/README.md +++ b/crates/compact-lint/README.md @@ -6,6 +6,13 @@ per-kind template in `compact-lint.toml`, and prints one finding per line. `fix` rewrites the comments the repairable rules report. `fill-constraints` compiles the contracts and writes the measured `k` and `rows` into the annotations. +## Install + +- npm, no Rust toolchain needed: `yarn add -D @openzeppelin/compact-lint`, or + `npx @openzeppelin/compact-lint check`. The package downloads the release binary for + the host on first use. See [`packages/lint`](../../packages/lint). +- From source: `cargo install --path crates/compact-lint`. + ## Usage ```sh diff --git a/packages/lint/CHANGELOG.md b/packages/lint/CHANGELOG.md new file mode 100644 index 0000000..a6cdf5c --- /dev/null +++ b/packages/lint/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog + +All notable changes to `@openzeppelin/compact-lint` are documented in this +file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this package adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Unreleased + +### Added + +- `compact-lint` bin, a launcher that downloads the [`compact-lint`](../../crates/compact-lint) release binary for the host, verifies its SHA-256, and runs it diff --git a/packages/lint/LICENSE b/packages/lint/LICENSE new file mode 100644 index 0000000..e22ac90 --- /dev/null +++ b/packages/lint/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Zeppelin Group Ltd + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/lint/README.md b/packages/lint/README.md new file mode 100644 index 0000000..22f62d3 --- /dev/null +++ b/packages/lint/README.md @@ -0,0 +1,45 @@ +# @openzeppelin/compact-lint + +Bin wrapper around the [`compact-lint`](../../crates/compact-lint) Rust crate, the +doc-comment linter for Compact sources. The package itself is a thin launcher: it +resolves the native binary for the host and runs it with your arguments. + +## Install + +```bash +yarn add --dev @openzeppelin/compact-lint +``` + +```bash +npx @openzeppelin/compact-lint check +yarn compact-lint check contracts/src +yarn compact-lint fix --dry-run +``` + +Commands, flags, exit codes and `compact-lint.toml` are documented in the +[crate README](../../crates/compact-lint/README.md). + +## Where the binary comes from + +- Prebuilt binaries are attached to the `compact-lint/v` GitHub release, one + per target: `x86_64-unknown-linux-gnu`, `aarch64-unknown-linux-gnu`, + `x86_64-apple-darwin`, `aarch64-apple-darwin`. +- The first run downloads the one matching the host, checks its SHA-256 against the + release's `checksums.txt`, and caches it at `.cache/compact-lint--` + inside the installed package. Later runs print nothing and start it directly. +- Uninstalling the package removes the cache with it. +- No prebuilt binary for the host is a hard error: build the crate with + `cargo install --path crates/compact-lint` and point `COMPACT_LINT_BINARY` at it. + +## Environment + +| Variable | Effect | +| --- | --- | +| `COMPACT_LINT_BINARY` | Run this executable and skip the download. For local crate builds and air-gapped CI. | +| `COMPACT_LINT_DOWNLOAD_BASE` | Fetch the asset and `checksums.txt` from this base URL instead of the GitHub release. | +| `COMPACT_LINT_COMPACT_BIN` | Read by the linter itself: path to the `compact` binary. | + +## Requirements + +- Node.js >= 24 +- Linux or macOS, x86_64 or aarch64 diff --git a/packages/lint/package.json b/packages/lint/package.json new file mode 100644 index 0000000..d1451fa --- /dev/null +++ b/packages/lint/package.json @@ -0,0 +1,43 @@ +{ + "name": "@openzeppelin/compact-lint", + "description": "Doc-comment linter for Compact sources", + "version": "0.1.0", + "keywords": [ + "compact", + "lint", + "linter", + "doc-comments", + "midnight" + ], + "author": "OpenZeppelin Community ", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/OpenZeppelin/compact-tools.git", + "directory": "packages/lint" + }, + "type": "module", + "files": [ + "dist", + "README.md", + "CHANGELOG.md", + "LICENSE" + ], + "engines": { + "node": ">=24" + }, + "bin": "dist/compactLint.js", + "scripts": { + "build": "tsc -p .", + "types": "tsc -p tsconfig.json --noEmit", + "test": "yarn vitest run", + "coverage": "yarn vitest run --coverage", + "clean": "git clean -fXd" + }, + "devDependencies": { + "@tsconfig/node24": "^24.0.3", + "@types/node": "26.1.2", + "typescript": "^6.0.3", + "vitest": "^4.1.9" + } +} diff --git a/packages/lint/src/binary.ts b/packages/lint/src/binary.ts new file mode 100644 index 0000000..02cbaa8 --- /dev/null +++ b/packages/lint/src/binary.ts @@ -0,0 +1,143 @@ +import { createHash, randomUUID } from 'node:crypto'; +import { + chmodSync, + existsSync, + mkdirSync, + renameSync, + writeFileSync, +} from 'node:fs'; +import { join } from 'node:path'; + +/** npm `platform-arch` pair to the Rust target triple the release assets are named after. */ +const TARGETS: Record = { + 'linux-x64': 'x86_64-unknown-linux-gnu', + 'linux-arm64': 'aarch64-unknown-linux-gnu', + 'darwin-x64': 'x86_64-apple-darwin', + 'darwin-arm64': 'aarch64-apple-darwin', +}; + +const RELEASE_BASE = + 'https://github.com/OpenZeppelin/compact-tools/releases/download'; + +/** A stalled release download fails instead of hanging the lint run. */ +const DOWNLOAD_TIMEOUT_MS = 120_000; + +/** Where the binary is fetched from and cached, and who is told about a download. */ +export interface ResolveOptions { + /** Root of the installed package; the cache lives under `.cache/` inside it. */ + packageDir: string; + version: string; + platform: string; + arch: string; + env: Record; + /** Called once, before the first download of a run. */ + notify: (message: string) => void; +} + +export function resolveTarget(platform: string, arch: string): string | null { + return TARGETS[`${platform}-${arch}`] ?? null; +} + +export function assetName(target: string): string { + return `compact-lint-${target}`; +} + +export function sha256(bytes: Uint8Array): string { + return createHash('sha256').update(bytes).digest('hex'); +} + +/** ` ` lines to an asset-keyed map; CRLF and the `*` binary marker are tolerated. */ +export function parseChecksums(text: string): Map { + const digests = new Map(); + for (const line of text.split('\n')) { + const entry = /^([0-9a-f]{64})\s+\*?(\S+)$/.exec(line.trim()); + if (entry) { + digests.set(entry[2], entry[1]); + } + } + return digests; +} + +export function expectedChecksum(text: string, asset: string): string { + const digest = parseChecksums(text).get(asset); + if (digest === undefined) { + throw new Error(`checksums.txt has no entry for ${asset}`); + } + return digest; +} + +/** + * Path to the `compact-lint` executable, downloading it into the package cache when missing. + * + * Messages carry no `compact-lint:` prefix; the bin adds it. + */ +export async function resolveBinary(options: ResolveOptions): Promise { + const override = options.env.COMPACT_LINT_BINARY; + if (override) { + return override; + } + + const target = resolveTarget(options.platform, options.arch); + if (target === null) { + throw new Error( + `no prebuilt binary for ${options.platform}-${options.arch}; build from source with cargo (crates/compact-lint) and set COMPACT_LINT_BINARY`, + ); + } + + const cacheDir = join(options.packageDir, '.cache'); + const cached = join(cacheDir, `compact-lint-${options.version}-${target}`); + if (existsSync(cached)) { + return cached; + } + + const asset = assetName(target); + const base = + options.env.COMPACT_LINT_DOWNLOAD_BASE ?? + `${RELEASE_BASE}/compact-lint/v${options.version}`; + options.notify(`downloading ${asset} (v${options.version})`); + + const [binary, checksums] = await Promise.all([ + fetchBytes(`${base}/${asset}`), + fetchText(`${base}/checksums.txt`), + ]); + + const expected = expectedChecksum(checksums, asset); + const actual = sha256(binary); + if (actual !== expected) { + throw new Error( + `checksum mismatch for ${asset}: expected ${expected}, got ${actual}; delete ${cached} and retry`, + ); + } + + // Staged under a unique name so concurrent runs never read a half-written file. + mkdirSync(cacheDir, { recursive: true }); + const staged = `${cached}.${randomUUID()}.tmp`; + writeFileSync(staged, binary); + chmodSync(staged, 0o755); + renameSync(staged, cached); + return cached; +} + +async function fetchOk(url: string): Promise { + let response: Response; + try { + response = await fetch(url, { + signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS), + }); + } catch (cause) { + const reason = cause instanceof Error ? cause.message : String(cause); + throw new Error(`download failed for ${url}: ${reason}`); + } + if (!response.ok) { + throw new Error(`download failed for ${url}: HTTP ${response.status}`); + } + return response; +} + +async function fetchBytes(url: string): Promise { + return new Uint8Array(await (await fetchOk(url)).arrayBuffer()); +} + +async function fetchText(url: string): Promise { + return (await fetchOk(url)).text(); +} diff --git a/packages/lint/src/compactLint.ts b/packages/lint/src/compactLint.ts new file mode 100644 index 0000000..a03ae70 --- /dev/null +++ b/packages/lint/src/compactLint.ts @@ -0,0 +1,52 @@ +#!/usr/bin/env node + +import { spawnSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import { constants } from 'node:os'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { resolveBinary } from './binary.ts'; + +// One level up from src/ and from dist/ alike, so both entry points find the manifest. +const packageDir = dirname(dirname(fileURLToPath(import.meta.url))); + +function packageVersion(): string { + const manifest = readFileSync(join(packageDir, 'package.json'), 'utf8'); + return (JSON.parse(manifest) as { version: string }).version; +} + +function fail(message: string): never { + process.stderr.write(`compact-lint: ${message}\n`); + process.exit(2); +} + +async function runLint(): Promise { + let binary: string; + try { + binary = await resolveBinary({ + packageDir, + version: packageVersion(), + platform: process.platform, + arch: process.arch, + env: process.env, + notify: (message) => process.stderr.write(`compact-lint: ${message}\n`), + }); + } catch (error) { + fail(error instanceof Error ? error.message : String(error)); + } + + // argv0 keeps the cache file name out of the linter's own usage output. + const result = spawnSync(binary, process.argv.slice(2), { + argv0: 'compact-lint', + stdio: 'inherit', + }); + if (result.error) { + fail(`cannot run ${binary}: ${result.error.message}`); + } + if (result.signal) { + process.exit(128 + (constants.signals[result.signal] ?? 0)); + } + process.exit(result.status ?? 0); +} + +await runLint(); diff --git a/packages/lint/test/binary.test.ts b/packages/lint/test/binary.test.ts new file mode 100644 index 0000000..c467344 --- /dev/null +++ b/packages/lint/test/binary.test.ts @@ -0,0 +1,293 @@ +import { spawnSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import { + chmodSync, + mkdtempSync, + rmSync, + statSync, + writeFileSync, +} from 'node:fs'; +import { createServer, type Server } from 'node:http'; +import type { AddressInfo } from 'node:net'; +import { tmpdir } from 'node:os'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { + expectedChecksum, + parseChecksums, + resolveBinary, + resolveTarget, + sha256, +} from '../src/binary.ts'; + +const packageDir = dirname(dirname(fileURLToPath(import.meta.url))); +const FAKE_BINARY = '#!/bin/sh\necho fake-lint "$@"\n'; +const ASSET = 'compact-lint-x86_64-unknown-linux-gnu'; + +interface Origin { + base: string; + requests: string[]; + close: () => Promise; +} + +/** A release-shaped origin: one asset, one `checksums.txt`, every request path recorded. */ +async function startOrigin(files: Record): Promise { + const requests: string[] = []; + const server: Server = createServer((request, response) => { + const path = (request.url ?? '/').slice(1); + requests.push(path); + const body = files[path]; + if (body === undefined) { + response.writeHead(404).end('not found'); + return; + } + response.writeHead(200).end(body); + }); + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); + const { port } = server.address() as AddressInfo; + return { + base: `http://127.0.0.1:${port}`, + requests, + close: () => + new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())), + ), + }; +} + +function checksumsFor(asset: string, body: string): string { + const digest = createHash('sha256').update(body).digest('hex'); + return [ + `${'0'.repeat(64)} compact-lint-aarch64-apple-darwin`, + `${digest} ${asset}`, + `${'1'.repeat(64)} compact-lint-aarch64-unknown-linux-gnu`, + ].join('\n'); +} + +let install: string; +let notices: string[]; + +function options( + overrides: Partial[0]> = {}, +): Parameters[0] { + return { + packageDir: install, + version: '0.1.0', + platform: 'linux', + arch: 'x64', + env: {}, + notify: (message) => notices.push(message), + ...overrides, + }; +} + +beforeEach(() => { + install = mkdtempSync(join(tmpdir(), 'compact-lint-pkg-')); + notices = []; +}); + +afterEach(() => { + rmSync(install, { recursive: true, force: true }); +}); + +describe('resolveTarget', () => { + it('maps every supported platform and architecture to its rust target', () => { + expect(resolveTarget('linux', 'x64')).toBe('x86_64-unknown-linux-gnu'); + expect(resolveTarget('linux', 'arm64')).toBe('aarch64-unknown-linux-gnu'); + expect(resolveTarget('darwin', 'x64')).toBe('x86_64-apple-darwin'); + expect(resolveTarget('darwin', 'arm64')).toBe('aarch64-apple-darwin'); + }); + + it('returns null for a platform with no release asset', () => { + expect(resolveTarget('win32', 'x64')).toBeNull(); + expect(resolveTarget('linux', 'ia32')).toBeNull(); + }); +}); + +describe('parseChecksums', () => { + it('reads every entry of a multi-asset file', () => { + const digests = parseChecksums(checksumsFor(ASSET, FAKE_BINARY)); + expect(digests.size).toBe(3); + expect(digests.get(ASSET)).toBe(sha256(Buffer.from(FAKE_BINARY))); + }); + + it('reads entries written with CRLF line endings', () => { + const digests = parseChecksums( + checksumsFor(ASSET, FAKE_BINARY).replaceAll('\n', '\r\n'), + ); + expect(digests.get(ASSET)).toBe(sha256(Buffer.from(FAKE_BINARY))); + }); + + it('reads the entry of a file marked binary with an asterisk', () => { + const digests = parseChecksums(`${'a'.repeat(64)} *${ASSET}`); + expect(digests.get(ASSET)).toBe('a'.repeat(64)); + }); + + it('ignores blank lines and lines that hold no digest', () => { + const digests = parseChecksums('\n# a comment\nnot a checksum line\n'); + expect(digests.size).toBe(0); + }); + + it('rejects a file with no entry for the asset', () => { + expect(() => expectedChecksum('', ASSET)).toThrow( + `checksums.txt has no entry for ${ASSET}`, + ); + }); +}); + +describe('resolveBinary', () => { + it('reports a platform with no release asset', async () => { + await expect( + resolveBinary(options({ platform: 'win32', arch: 'x64' })), + ).rejects.toThrow( + 'no prebuilt binary for win32-x64; build from source with cargo (crates/compact-lint) and set COMPACT_LINT_BINARY', + ); + }); + + it('returns COMPACT_LINT_BINARY without contacting the origin', async () => { + const origin = await startOrigin({}); + try { + const binary = await resolveBinary( + options({ + env: { + COMPACT_LINT_BINARY: '/opt/compact-lint', + COMPACT_LINT_DOWNLOAD_BASE: origin.base, + }, + }), + ); + expect(binary).toBe('/opt/compact-lint'); + expect(origin.requests).toStrictEqual([]); + expect(notices).toStrictEqual([]); + } finally { + await origin.close(); + } + }); + + it('downloads, verifies and caches the release asset', async () => { + const origin = await startOrigin({ + [ASSET]: FAKE_BINARY, + 'checksums.txt': checksumsFor(ASSET, FAKE_BINARY), + }); + try { + const binary = await resolveBinary( + options({ env: { COMPACT_LINT_DOWNLOAD_BASE: origin.base } }), + ); + expect(binary).toBe( + join(install, '.cache', 'compact-lint-0.1.0-x86_64-unknown-linux-gnu'), + ); + expect(statSync(binary).mode & 0o777).toBe(0o755); + expect(spawnSync(binary, ['check'], { encoding: 'utf8' }).stdout).toBe( + 'fake-lint check\n', + ); + expect(origin.requests.toSorted()).toStrictEqual([ + 'checksums.txt', + ASSET, + ]); + expect(notices).toStrictEqual([ + 'downloading compact-lint-x86_64-unknown-linux-gnu (v0.1.0)', + ]); + } finally { + await origin.close(); + } + }); + + it('serves a second run from the cache', async () => { + const origin = await startOrigin({ + [ASSET]: FAKE_BINARY, + 'checksums.txt': checksumsFor(ASSET, FAKE_BINARY), + }); + try { + const first = await resolveBinary( + options({ env: { COMPACT_LINT_DOWNLOAD_BASE: origin.base } }), + ); + const second = await resolveBinary( + options({ env: { COMPACT_LINT_DOWNLOAD_BASE: origin.base } }), + ); + expect(second).toBe(first); + expect(origin.requests).toHaveLength(2); + expect(notices).toHaveLength(1); + } finally { + await origin.close(); + } + }); + + it('rejects an asset whose digest does not match checksums.txt', async () => { + const origin = await startOrigin({ + [ASSET]: FAKE_BINARY, + 'checksums.txt': checksumsFor(ASSET, 'a different binary'), + }); + try { + const expected = sha256(Buffer.from('a different binary')); + const actual = sha256(Buffer.from(FAKE_BINARY)); + await expect( + resolveBinary( + options({ env: { COMPACT_LINT_DOWNLOAD_BASE: origin.base } }), + ), + ).rejects.toThrow( + `checksum mismatch for ${ASSET}: expected ${expected}, got ${actual}; delete ${join(install, '.cache', 'compact-lint-0.1.0-x86_64-unknown-linux-gnu')} and retry`, + ); + } finally { + await origin.close(); + } + }); + + it('rejects an origin that answers with an http error', async () => { + const origin = await startOrigin({}); + try { + await expect( + resolveBinary( + options({ env: { COMPACT_LINT_DOWNLOAD_BASE: origin.base } }), + ), + ).rejects.toThrow( + `download failed for ${origin.base}/${ASSET}: HTTP 404`, + ); + } finally { + await origin.close(); + } + }); + + it('rejects an origin that cannot be reached', async () => { + await expect( + resolveBinary( + options({ + env: { COMPACT_LINT_DOWNLOAD_BASE: 'http://127.0.0.1:1/download' }, + }), + ), + ).rejects.toThrow('download failed for http://127.0.0.1:1/download/'); + }); +}); + +describe('the compact-lint bin', () => { + it('passes its arguments to the binary and forwards the exit code', () => { + const stub = join(install, 'stub-lint'); + writeFileSync(stub, '#!/bin/sh\necho "args: $*"\nexit 7\n'); + chmodSync(stub, 0o755); + + const run = spawnSync( + process.execPath, + [join(packageDir, 'src', 'compactLint.ts'), 'check', '--strict', 'src'], + { encoding: 'utf8', env: { ...process.env, COMPACT_LINT_BINARY: stub } }, + ); + + expect(run.stdout).toBe('args: check --strict src\n'); + expect(run.status).toBe(7); + }); + + it('exits 2 when no binary can be resolved', () => { + const run = spawnSync( + process.execPath, + [join(packageDir, 'src', 'compactLint.ts'), 'check'], + { + encoding: 'utf8', + env: { + ...process.env, + COMPACT_LINT_BINARY: join(install, 'not-installed'), + }, + }, + ); + + expect(run.status).toBe(2); + expect(run.stderr).toContain('compact-lint: cannot run'); + }); +}); diff --git a/packages/lint/test/version.test.ts b/packages/lint/test/version.test.ts new file mode 100644 index 0000000..75c37ae --- /dev/null +++ b/packages/lint/test/version.test.ts @@ -0,0 +1,22 @@ +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; + +const packageDir = dirname(dirname(fileURLToPath(import.meta.url))); + +function read(...segments: string[]): string { + return readFileSync(join(packageDir, ...segments), 'utf8'); +} + +describe('the published version', () => { + it('matches the crate the package downloads', () => { + const published = (JSON.parse(read('package.json')) as { version: string }) + .version; + const crate = /^version = "(.+)"$/m.exec( + read('..', '..', 'crates', 'compact-lint', 'Cargo.toml'), + ); + + expect(crate?.[1]).toBe(published); + }); +}); diff --git a/packages/lint/tsconfig.json b/packages/lint/tsconfig.json new file mode 100644 index 0000000..f4bcef1 --- /dev/null +++ b/packages/lint/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "@tsconfig/node24/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "types": ["node"], + "declaration": true, + "skipLibCheck": true, + "sourceMap": true, + "rewriteRelativeImportExtensions": true, + "erasableSyntaxOnly": true, + "verbatimModuleSyntax": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist" + ] +} \ No newline at end of file diff --git a/packages/lint/turbo.json b/packages/lint/turbo.json new file mode 100644 index 0000000..d671009 --- /dev/null +++ b/packages/lint/turbo.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "build": { + "outputs": ["dist/**"], + "inputs": ["src/**/*.ts", "tsconfig.json"], + "cache": true + } + } +} diff --git a/packages/lint/vitest.config.ts b/packages/lint/vitest.config.ts new file mode 100644 index 0000000..a07a60b --- /dev/null +++ b/packages/lint/vitest.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['test/**/*.test.ts'], + reporters: 'verbose', + coverage: { + provider: 'v8', + reporter: ['text', 'html', 'json-summary'], + // The bin runs in a subprocess, which v8 coverage does not observe. + include: ['src/binary.ts'], + thresholds: { + statements: 95, + branches: 90, + functions: 100, + lines: 95, + }, + }, + }, +}); diff --git a/yarn.lock b/yarn.lock index 583cabb..65d5c32 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1180,6 +1180,19 @@ __metadata: languageName: unknown linkType: soft +"@openzeppelin/compact-lint@workspace:packages/lint": + version: 0.0.0-use.local + resolution: "@openzeppelin/compact-lint@workspace:packages/lint" + dependencies: + "@tsconfig/node24": "npm:^24.0.3" + "@types/node": "npm:26.1.2" + typescript: "npm:^6.0.3" + vitest: "npm:^4.1.9" + bin: + compact-lint: dist/compactLint.js + languageName: unknown + linkType: soft + "@openzeppelin/compact-simulator@workspace:packages/simulator": version: 0.0.0-use.local resolution: "@openzeppelin/compact-simulator@workspace:packages/simulator" From b3c6c8c89507d4fe861d357546413123bb83566e Mon Sep 17 00:00:00 2001 From: 0xisk <0xisk@proton.me> Date: Tue, 15 Sep 2026 11:45:24 +0200 Subject: [PATCH 2/3] build(compact-linter): name the npm package compact-linter The published packages are compact-builder, compact-cli, compact-deployer and compact-simulator, so the wrapper follows the same shape as @openzeppelin/compact-linter under packages/linter, with a compact-linter bin and a compact-linter/v release tag. The crate, its binary and the release asset names stay compact-lint. --- packages/{lint => linter}/CHANGELOG.md | 0 packages/{lint => linter}/LICENSE | 0 packages/{lint => linter}/README.md | 0 packages/{lint => linter}/package.json | 0 packages/{lint => linter}/src/binary.ts | 0 packages/{lint => linter}/src/compactLint.ts | 0 packages/{lint => linter}/test/binary.test.ts | 0 packages/{lint => linter}/test/version.test.ts | 0 packages/{lint => linter}/tsconfig.json | 0 packages/{lint => linter}/turbo.json | 0 packages/{lint => linter}/vitest.config.ts | 0 11 files changed, 0 insertions(+), 0 deletions(-) rename packages/{lint => linter}/CHANGELOG.md (100%) rename packages/{lint => linter}/LICENSE (100%) rename packages/{lint => linter}/README.md (100%) rename packages/{lint => linter}/package.json (100%) rename packages/{lint => linter}/src/binary.ts (100%) rename packages/{lint => linter}/src/compactLint.ts (100%) rename packages/{lint => linter}/test/binary.test.ts (100%) rename packages/{lint => linter}/test/version.test.ts (100%) rename packages/{lint => linter}/tsconfig.json (100%) rename packages/{lint => linter}/turbo.json (100%) rename packages/{lint => linter}/vitest.config.ts (100%) diff --git a/packages/lint/CHANGELOG.md b/packages/linter/CHANGELOG.md similarity index 100% rename from packages/lint/CHANGELOG.md rename to packages/linter/CHANGELOG.md diff --git a/packages/lint/LICENSE b/packages/linter/LICENSE similarity index 100% rename from packages/lint/LICENSE rename to packages/linter/LICENSE diff --git a/packages/lint/README.md b/packages/linter/README.md similarity index 100% rename from packages/lint/README.md rename to packages/linter/README.md diff --git a/packages/lint/package.json b/packages/linter/package.json similarity index 100% rename from packages/lint/package.json rename to packages/linter/package.json diff --git a/packages/lint/src/binary.ts b/packages/linter/src/binary.ts similarity index 100% rename from packages/lint/src/binary.ts rename to packages/linter/src/binary.ts diff --git a/packages/lint/src/compactLint.ts b/packages/linter/src/compactLint.ts similarity index 100% rename from packages/lint/src/compactLint.ts rename to packages/linter/src/compactLint.ts diff --git a/packages/lint/test/binary.test.ts b/packages/linter/test/binary.test.ts similarity index 100% rename from packages/lint/test/binary.test.ts rename to packages/linter/test/binary.test.ts diff --git a/packages/lint/test/version.test.ts b/packages/linter/test/version.test.ts similarity index 100% rename from packages/lint/test/version.test.ts rename to packages/linter/test/version.test.ts diff --git a/packages/lint/tsconfig.json b/packages/linter/tsconfig.json similarity index 100% rename from packages/lint/tsconfig.json rename to packages/linter/tsconfig.json diff --git a/packages/lint/turbo.json b/packages/linter/turbo.json similarity index 100% rename from packages/lint/turbo.json rename to packages/linter/turbo.json diff --git a/packages/lint/vitest.config.ts b/packages/linter/vitest.config.ts similarity index 100% rename from packages/lint/vitest.config.ts rename to packages/linter/vitest.config.ts From 71db1e87e5af158e53c57dfd63713de3cbc5b94d Mon Sep 17 00:00:00 2001 From: 0xisk <0xisk@proton.me> Date: Tue, 15 Sep 2026 11:46:27 +0200 Subject: [PATCH 3/3] build(compact-linter): finish the compact-linter rename Carries the content half of the rename: manifest name and bin, release tag path, stderr prefix, workflows, docs and the lockfile. The previous commit only moved the directory. --- .github/workflows/release-publish.yml | 22 +++++++++---------- .github/workflows/release.yml | 18 +++++++-------- .gitignore | 2 +- README.md | 6 ++--- RELEASING.md | 20 ++++++++--------- crates/compact-lint/README.md | 6 ++--- packages/linter/CHANGELOG.md | 4 ++-- packages/linter/README.md | 12 +++++----- packages/linter/package.json | 6 ++--- packages/linter/src/binary.ts | 2 +- .../src/{compactLint.ts => compactLinter.ts} | 9 +++++--- packages/linter/test/binary.test.ts | 8 +++---- yarn.lock | 6 ++--- 13 files changed, 62 insertions(+), 59 deletions(-) rename packages/linter/src/{compactLint.ts => compactLinter.ts} (89%) diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index 2e34bc0..af9452e 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -19,7 +19,7 @@ on: - compact-builder - compact-cli - compact-deployer - - compact-lint + - compact-linter - compact-simulator jobs: @@ -68,7 +68,7 @@ jobs: compact-builder) DIR="builder" ;; compact-cli) DIR="cli" ;; compact-deployer) DIR="deployer" ;; - compact-lint) DIR="lint" ;; + compact-linter) DIR="linter" ;; compact-simulator) DIR="simulator" ;; *) echo "::error::unknown package: $PKG" @@ -88,7 +88,7 @@ jobs: builder) PKG="compact-builder" ;; cli) PKG="compact-cli" ;; deployer) PKG="compact-deployer" ;; - lint) PKG="compact-lint" ;; + linter) PKG="compact-linter" ;; simulator) PKG="compact-simulator" ;; *) echo "::error::unknown package directory: $DIR" @@ -136,12 +136,12 @@ jobs: echo "- Trigger: $EVENT" } >> $GITHUB_STEP_SUMMARY - # compact-lint ships a native binary per target, built on its own runner. The + # compact-linter ships a native binary per target, built on its own runner. The # npm package downloads them, so they must reach the GitHub release first. binaries: name: Build compact-lint for ${{ matrix.target }} needs: detect - if: needs.detect.outputs.name == 'compact-lint' + if: needs.detect.outputs.name == 'compact-linter' runs-on: ${{ matrix.runner }} permissions: @@ -200,7 +200,7 @@ jobs: publish: name: Publish merged release needs: [detect, binaries] - # `binaries` is skipped for every package but compact-lint, and a skipped + # `binaries` is skipped for every package but compact-linter, and a skipped # dependency must not skip the publish. if: >- always() && @@ -255,7 +255,7 @@ jobs: fi - name: Download the release binaries - if: needs.detect.outputs.name == 'compact-lint' + if: needs.detect.outputs.name == 'compact-linter' uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: path: dist @@ -265,7 +265,7 @@ jobs: # Asset names only: the installed package looks each digest up by the name # it downloads. - name: Write the checksums - if: needs.detect.outputs.name == 'compact-lint' + if: needs.detect.outputs.name == 'compact-linter' working-directory: dist run: | sha256sum compact-lint-* > checksums.txt @@ -274,7 +274,7 @@ jobs: # The npm package downloads these assets on first run, so the release has # to carry them before the package reaches npm. - name: Create the GitHub release - if: needs.detect.outputs.name == 'compact-lint' + if: needs.detect.outputs.name == 'compact-linter' env: GH_TOKEN: ${{ steps.gh-app-token.outputs.token }} TAG: ${{ needs.detect.outputs.name }}/v${{ needs.detect.outputs.version }} @@ -285,7 +285,7 @@ jobs: $0 ~ "^## " v "( |$)" { found = 1; next } found && /^## / { exit } found { print } - ' packages/lint/CHANGELOG.md) + ' packages/linter/CHANGELOG.md) if [[ -z "${NOTES//[[:space:]]/}" ]]; then NOTES="$TAG" fi @@ -297,7 +297,7 @@ jobs: ARGS+=(--prerelease) fi gh release create "$TAG" dist/* \ - --title "compact-lint v$VERSION" \ + --title "compact-linter v$VERSION" \ --notes "$NOTES" \ "${ARGS[@]}" fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 13360df..b9522bb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,7 @@ on: - compact-builder - compact-cli - compact-deployer - - compact-lint + - compact-linter - compact-simulator version_bump: description: "Version bump type (pre* strategies are beta-only)" @@ -90,8 +90,8 @@ jobs: "compact-deployer") echo "dir=deployer" >> $GITHUB_OUTPUT ;; - "compact-lint") - echo "dir=lint" >> $GITHUB_OUTPUT + "compact-linter") + echo "dir=linter" >> $GITHUB_OUTPUT ;; "compact-simulator") echo "dir=simulator" >> $GITHUB_OUTPUT @@ -101,10 +101,10 @@ jobs: - name: Setup Environment uses: ./.github/actions/setup - # compact-lint's version lives in the crate too, and bumping it there + # compact-linter's version lives in the crate too, and bumping it there # rewrites Cargo.lock. - name: Install the pinned toolchain - if: inputs.package == 'compact-lint' + if: inputs.package == 'compact-linter' run: | CHANNEL=$(grep -m1 '^channel' rust-toolchain.toml | cut -d'"' -f2) rustup toolchain install "$CHANNEL" --profile minimal @@ -148,7 +148,7 @@ jobs: } >> $GITHUB_STEP_SUMMARY - name: Sync the crate version - if: inputs.package == 'compact-lint' + if: inputs.package == 'compact-linter' env: NEW_VERSION: ${{ steps.version.outputs.new }} run: | @@ -183,7 +183,7 @@ jobs: fi - name: Commit version bump - if: inputs.package != 'compact-lint' + if: inputs.package != 'compact-linter' uses: iarekylew00t/verified-bot-commit@42a042b0407248f3c0acac00c389e729e3476e86 # v2.3.5 with: message: "release: ${{ inputs.package }} v${{ steps.version.outputs.new }}" @@ -193,14 +193,14 @@ jobs: packages/${{ steps.pkg.outputs.dir }}/package.json - name: Commit version bump with the crate - if: inputs.package == 'compact-lint' + if: inputs.package == 'compact-linter' uses: iarekylew00t/verified-bot-commit@42a042b0407248f3c0acac00c389e729e3476e86 # v2.3.5 with: message: "release: ${{ inputs.package }} v${{ steps.version.outputs.new }}" token: ${{ steps.gh-app-token.outputs.token }} ref: ${{ steps.version.outputs.branch }} files: | - packages/lint/package.json + packages/linter/package.json crates/compact-lint/Cargo.toml Cargo.lock diff --git a/.gitignore b/.gitignore index 757ea36..48f64fe 100644 --- a/.gitignore +++ b/.gitignore @@ -67,7 +67,7 @@ target/ .toolkit-cache/ # compact-lint's release binary, cached inside the installed npm package. -packages/lint/.cache/ +packages/linter/.cache/ coverage **/reports diff --git a/README.md b/README.md index 6ae796e..5d39deb 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Tools for compiling, building, testing, and deploying Compact smart contracts. T - [`packages/builder`](./packages/builder) — programmatic library that drives the Compact compiler + builder - [`packages/cli`](./packages/cli) — thin bin wrapper around the builder and deployer libraries (`compact-compiler`, `compact-builder`, `compact-deploy`) - [`packages/deployer`](./packages/deployer) — deployer library that submits a compiled contract to a Midnight network. Pins one Midnight stack: see [Supported stack](./packages/deployer/README.md#supported-stack) -- [`packages/lint`](./packages/lint) — bin wrapper around the [`compact-lint`](./crates/compact-lint) crate, the doc-comment linter for Compact sources +- [`packages/linter`](./packages/linter) — bin wrapper around the [`compact-lint`](./crates/compact-lint) crate, the doc-comment linter for Compact sources - [`packages/simulator`](./packages/simulator) — TypeScript simulator to run and test Compact contracts locally See each package's README for usage, options, and examples. @@ -40,7 +40,7 @@ yarn add --dev @openzeppelin/compact-cli yarn add --dev @openzeppelin/compact-simulator # Linter — check doc comments in .compact sources -yarn add --dev @openzeppelin/compact-lint +yarn add --dev @openzeppelin/compact-linter # Deployer — deploy a compiled contract from TypeScript yarn add --dev @openzeppelin/compact-deployer @@ -63,7 +63,7 @@ import { createSimulator } from '@openzeppelin/compact-simulator'; ## Requirements -- Node.js >= 22 for `packages/builder` and `packages/simulator`, >= 24 for `packages/cli`, `packages/deployer` and `packages/lint` +- Node.js >= 22 for `packages/builder` and `packages/simulator`, >= 24 for `packages/cli`, `packages/deployer` and `packages/linter` - Yarn 4 (Berry) - Turbo - Optional: Midnight Compact toolchain installed and available in `PATH` diff --git a/RELEASING.md b/RELEASING.md index ed5e496..fd3494e 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -49,7 +49,7 @@ release notes agree. - Create a git tag. - Publish the package to npm under the channel's dist-tag. 7. Once published, go to "Releases" and create a GitHub release using the - generated tag. Mark beta tags as prereleases. `compact-lint` is the + generated tag. Mark beta tags as prereleases. `compact-linter` is the exception: the workflow creates its release itself (see below). ## Graduating a beta to stable @@ -89,7 +89,7 @@ compact-cli (bin wrapper) └─ depends on compact-deployer compact-builder (library) compact-deployer (library) -compact-lint (bin wrapper around the compact-lint crate) +compact-linter (bin wrapper around the compact-lint crate) compact-simulator (library) ``` @@ -100,22 +100,22 @@ each dependent finds its deps already on npm: 1. `compact-builder` (no internal deps) 2. `compact-simulator` (no internal deps) 3. `compact-deployer` (no internal deps) -4. `compact-lint` (no internal deps) +4. `compact-linter` (no internal deps) 5. `compact-cli` (depends on `compact-builder` and `compact-deployer`; pull `main` first so both bump commits are present locally before triggering) After the first release, the five packages version independently — bump any one of them in isolation without re-publishing the others. -## compact-lint +## compact-linter -`compact-lint` is a bin wrapper around the `compact-lint` crate, so its release +`compact-linter` is a bin wrapper around the `compact-lint` crate, so its release carries a native binary as well as an npm package. -- The version lives in two files: `packages/lint/package.json` and +- The version lives in two files: `packages/linter/package.json` and `crates/compact-lint/Cargo.toml`. The release workflow bumps the package, mirrors the value into the crate, refreshes `Cargo.lock`, and commits all three. A unit - test in `packages/lint` fails when the two drift. + test in `packages/linter` fails when the two drift. - The publish workflow builds four binaries before publishing, one per target, each on its own runner: @@ -126,12 +126,12 @@ carries a native binary as well as an npm package. | `x86_64-apple-darwin` | `macos-15-intel` | | `aarch64-apple-darwin` | `macos-15` | -- It then creates the GitHub release `compact-lint/v` with the four assets +- It then creates the GitHub release `compact-linter/v` with the four assets and a `checksums.txt`, and only then publishes to npm. The installed package downloads its binary from that release, so the assets must be there first. For every other package the GitHub release is still created by hand (step 7 above). -- Release notes come from the `## ` section of `packages/lint/CHANGELOG.md`, +- Release notes come from the `## ` section of `packages/linter/CHANGELOG.md`, falling back to the tag name. A `beta` dist-tag marks the release a prerelease. -- Before the first release, add `@openzeppelin/compact-lint` as a Trusted Publisher +- Before the first release, add `@openzeppelin/compact-linter` as a Trusted Publisher on npmjs.org, naming this repo and `.github/workflows/release-publish.yml`. Every new npm package needs this once, or the publish step cannot authenticate. diff --git a/crates/compact-lint/README.md b/crates/compact-lint/README.md index aa30399..434efe1 100644 --- a/crates/compact-lint/README.md +++ b/crates/compact-lint/README.md @@ -8,9 +8,9 @@ contracts and writes the measured `k` and `rows` into the annotations. ## Install -- npm, no Rust toolchain needed: `yarn add -D @openzeppelin/compact-lint`, or - `npx @openzeppelin/compact-lint check`. The package downloads the release binary for - the host on first use. See [`packages/lint`](../../packages/lint). +- npm, no Rust toolchain needed: `yarn add -D @openzeppelin/compact-linter`, or + `npx @openzeppelin/compact-linter check`. The package downloads the release binary for + the host on first use. See [`packages/linter`](../../packages/linter). - From source: `cargo install --path crates/compact-lint`. ## Usage diff --git a/packages/linter/CHANGELOG.md b/packages/linter/CHANGELOG.md index a6cdf5c..5d88638 100644 --- a/packages/linter/CHANGELOG.md +++ b/packages/linter/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -All notable changes to `@openzeppelin/compact-lint` are documented in this +All notable changes to `@openzeppelin/compact-linter` are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), @@ -10,4 +10,4 @@ and this package adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- `compact-lint` bin, a launcher that downloads the [`compact-lint`](../../crates/compact-lint) release binary for the host, verifies its SHA-256, and runs it +- `compact-linter` bin, a launcher that downloads the [`compact-lint`](../../crates/compact-lint) release binary for the host, verifies its SHA-256, and runs it diff --git a/packages/linter/README.md b/packages/linter/README.md index 22f62d3..6bf577b 100644 --- a/packages/linter/README.md +++ b/packages/linter/README.md @@ -1,4 +1,4 @@ -# @openzeppelin/compact-lint +# @openzeppelin/compact-linter Bin wrapper around the [`compact-lint`](../../crates/compact-lint) Rust crate, the doc-comment linter for Compact sources. The package itself is a thin launcher: it @@ -7,13 +7,13 @@ resolves the native binary for the host and runs it with your arguments. ## Install ```bash -yarn add --dev @openzeppelin/compact-lint +yarn add --dev @openzeppelin/compact-linter ``` ```bash -npx @openzeppelin/compact-lint check -yarn compact-lint check contracts/src -yarn compact-lint fix --dry-run +npx @openzeppelin/compact-linter check +yarn compact-linter check contracts/src +yarn compact-linter fix --dry-run ``` Commands, flags, exit codes and `compact-lint.toml` are documented in the @@ -21,7 +21,7 @@ Commands, flags, exit codes and `compact-lint.toml` are documented in the ## Where the binary comes from -- Prebuilt binaries are attached to the `compact-lint/v` GitHub release, one +- Prebuilt binaries are attached to the `compact-linter/v` GitHub release, one per target: `x86_64-unknown-linux-gnu`, `aarch64-unknown-linux-gnu`, `x86_64-apple-darwin`, `aarch64-apple-darwin`. - The first run downloads the one matching the host, checks its SHA-256 against the diff --git a/packages/linter/package.json b/packages/linter/package.json index d1451fa..81cdf57 100644 --- a/packages/linter/package.json +++ b/packages/linter/package.json @@ -1,5 +1,5 @@ { - "name": "@openzeppelin/compact-lint", + "name": "@openzeppelin/compact-linter", "description": "Doc-comment linter for Compact sources", "version": "0.1.0", "keywords": [ @@ -14,7 +14,7 @@ "repository": { "type": "git", "url": "git+https://github.com/OpenZeppelin/compact-tools.git", - "directory": "packages/lint" + "directory": "packages/linter" }, "type": "module", "files": [ @@ -26,7 +26,7 @@ "engines": { "node": ">=24" }, - "bin": "dist/compactLint.js", + "bin": "dist/compactLinter.js", "scripts": { "build": "tsc -p .", "types": "tsc -p tsconfig.json --noEmit", diff --git a/packages/linter/src/binary.ts b/packages/linter/src/binary.ts index 02cbaa8..8c73bd4 100644 --- a/packages/linter/src/binary.ts +++ b/packages/linter/src/binary.ts @@ -93,7 +93,7 @@ export async function resolveBinary(options: ResolveOptions): Promise { const asset = assetName(target); const base = options.env.COMPACT_LINT_DOWNLOAD_BASE ?? - `${RELEASE_BASE}/compact-lint/v${options.version}`; + `${RELEASE_BASE}/compact-linter/v${options.version}`; options.notify(`downloading ${asset} (v${options.version})`); const [binary, checksums] = await Promise.all([ diff --git a/packages/linter/src/compactLint.ts b/packages/linter/src/compactLinter.ts similarity index 89% rename from packages/linter/src/compactLint.ts rename to packages/linter/src/compactLinter.ts index a03ae70..3300348 100644 --- a/packages/linter/src/compactLint.ts +++ b/packages/linter/src/compactLinter.ts @@ -16,7 +16,8 @@ function packageVersion(): string { } function fail(message: string): never { - process.stderr.write(`compact-lint: ${message}\n`); + process.stderr.write(`compact-linter: ${message} +`); process.exit(2); } @@ -29,7 +30,9 @@ async function runLint(): Promise { platform: process.platform, arch: process.arch, env: process.env, - notify: (message) => process.stderr.write(`compact-lint: ${message}\n`), + notify: (message) => + process.stderr.write(`compact-linter: ${message} +`), }); } catch (error) { fail(error instanceof Error ? error.message : String(error)); @@ -37,7 +40,7 @@ async function runLint(): Promise { // argv0 keeps the cache file name out of the linter's own usage output. const result = spawnSync(binary, process.argv.slice(2), { - argv0: 'compact-lint', + argv0: 'compact-linter', stdio: 'inherit', }); if (result.error) { diff --git a/packages/linter/test/binary.test.ts b/packages/linter/test/binary.test.ts index c467344..18de25c 100644 --- a/packages/linter/test/binary.test.ts +++ b/packages/linter/test/binary.test.ts @@ -258,7 +258,7 @@ describe('resolveBinary', () => { }); }); -describe('the compact-lint bin', () => { +describe('the compact-linter bin', () => { it('passes its arguments to the binary and forwards the exit code', () => { const stub = join(install, 'stub-lint'); writeFileSync(stub, '#!/bin/sh\necho "args: $*"\nexit 7\n'); @@ -266,7 +266,7 @@ describe('the compact-lint bin', () => { const run = spawnSync( process.execPath, - [join(packageDir, 'src', 'compactLint.ts'), 'check', '--strict', 'src'], + [join(packageDir, 'src', 'compactLinter.ts'), 'check', '--strict', 'src'], { encoding: 'utf8', env: { ...process.env, COMPACT_LINT_BINARY: stub } }, ); @@ -277,7 +277,7 @@ describe('the compact-lint bin', () => { it('exits 2 when no binary can be resolved', () => { const run = spawnSync( process.execPath, - [join(packageDir, 'src', 'compactLint.ts'), 'check'], + [join(packageDir, 'src', 'compactLinter.ts'), 'check'], { encoding: 'utf8', env: { @@ -288,6 +288,6 @@ describe('the compact-lint bin', () => { ); expect(run.status).toBe(2); - expect(run.stderr).toContain('compact-lint: cannot run'); + expect(run.stderr).toContain('compact-linter: cannot run'); }); }); diff --git a/yarn.lock b/yarn.lock index 65d5c32..6a5f237 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1180,16 +1180,16 @@ __metadata: languageName: unknown linkType: soft -"@openzeppelin/compact-lint@workspace:packages/lint": +"@openzeppelin/compact-linter@workspace:packages/linter": version: 0.0.0-use.local - resolution: "@openzeppelin/compact-lint@workspace:packages/lint" + resolution: "@openzeppelin/compact-linter@workspace:packages/linter" dependencies: "@tsconfig/node24": "npm:^24.0.3" "@types/node": "npm:26.1.2" typescript: "npm:^6.0.3" vitest: "npm:^4.1.9" bin: - compact-lint: dist/compactLint.js + compact-linter: dist/compactLinter.js languageName: unknown linkType: soft