`cargo publish` verifies by building the packaged crate against the
*published* versions of its dependencies, not the local paths. basta uses
`cpd_core::deadcode`, `cpd_reporter::deadcode` and a public `LineIndex` —
none of which exist in the versions on crates.io — so every attempt failed
with "failed to verify package tarball", and basta could not reach crates.io
until a jscpd release happened to carry those three crates along.
That is the coupling this workflow exists to avoid. It now walks basta's own
dependency graph, publishes in topological order whatever is missing, and
waits for the index between crates. jscpd is excluded by construction: it is
a sibling, not a dependency, and releasing it is not this workflow's job.
Bumps the three crates that actually changed: cpd-core 0.1.13 -> 0.1.14
(the `deadcode` module), cpd-tokenizer 0.1.15 -> 0.1.16 (`LineIndex` made
public, `.pyi`), cpd-reporter 0.1.14 -> 0.1.15 (the dead-code reporters).
cpd-finder is untouched and stays where it is.
Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
Deprecating needs npm credentials, and this project's live in repository
secrets rather than on a laptop. Takes a spec and a message; an empty
message un-deprecates, which is npm's own convention and what makes a
wrong message recoverable.
Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
The publish step's own output says it: "Your package is being processed and
may take a few minutes to become available." Reading `dist-tags.latest`
seconds later returns the previous tag, so the guard failed a release that
had in fact succeeded — basta@0.1.0 was published, signed and tagged
correctly, and the job went red anyway.
Polls to a five-minute deadline, the same shape as the platform-package gate
directly above it.
Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
Trusted Publishing is configured per crate and per workflow filename, and
it can only be configured on a crate that already exists — so the first
publish of a new crate cannot use it. The job already had
`secrets.CARGO_REGISTRY_TOKEN` as a fallback, but the auth step failed the
job before anything could reach it. It is now non-fatal, with an explicit
check that at least one credential is present.
Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
basta is a separate product from jscpd — a different question about the
same tree — so it gets its own version, its own prebuilt packages and its
own trigger. jscpd goes out on every push to master that leaves a version
unpublished; basta goes out only when a `basta-v*` tag is pushed, or by
hand through workflow_dispatch. Neither release can drag the other along
or hold it up.
`basta-release.yml` owns the whole thing: it checks that the crate, the npm
manifest and the tag all name the same version and that the working tree is
already synced, publishes the crate, builds the eight targets and publishes
`basta-<platform>` plus the `basta` wrapper. It ends by asserting that npm's
`latest` really points at this release — the package carries 1.x versions
from an unrelated tool of the same name, and a dist-tag that quietly stayed
on those would hand every `npm i basta` the wrong program.
The one place the two cannot be separated is crates.io: `jscpd` depends on
`basta`, so cargo refuses to publish jscpd until a matching basta exists.
The jscpd crates job therefore still publishes basta when it is missing, and
that is now commented as the dependency requirement it is. Both paths skip a
version already on the registry, so whichever runs first wins.
npm needed the wrapper pattern `jscpd` and `cpd` already use: `rust/basta/`
holds the manifest, a platform map naming `basta-<platform>`, and a launcher
that falls back to a local `cargo build` when no platform package is
installed. `npm-prebuilt-package.mjs` grew a `--product` flag; its output for
jscpd is byte-identical to before. `prebuilt-targets.json` now carries a
platform label rather than a jscpd-specific blurb, so one table serves both
products. `sync-version.mjs` syncs and verifies the basta wrapper against the
basta crate version — the same independent version/pin update the jscpd block
does, for the same reason it does it.
Nothing is published by this commit.
Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
Closes#1037.
Adds rust/scripts/build-pypi-wheels.py, which repacks the eight
jscpd-<platform>.tar.gz release tarballs into platform wheels: the same
binary under <pkg>.data/scripts/ as both `jscpd` and `cpd`, one wheel
per target, stdlib only, no maturin and no recompilation. glibc wheels
take their manylinux floor from the GLIBC_x.y symbols in the binary
(manylinux_2_34 today), musl builds are static so they tag as
musllinux_1_2, macOS and Windows use the Rust minimums. Semver
pre-releases map to PEP 440 (5.3.0-beta.1 -> 5.3.0b1); other labels
fail rather than guess.
release.yml gains a rust-publish-pypi job after the npm publish: it
takes the binary artifacts from this run, refills any missing target
from the published npm platform package (resumed releases, or a version
already on npm), builds the wheels, pip-installs the matching one and
checks `jscpd --version` and `cpd --version`, then uploads with PyPI
trusted publishing (environment `pypi`, no token, skip-existing). The
detect job treats a version missing from PyPI as pending, so the wheels
for the current release publish on the next push to master without a
new release commit. The GitHub release lists the PyPI package and waits
on the upload the same way it waits on crates.io and npm.
Docs: PyPI in the README install table and packages table, rust/README
and docs/rust.md install sections, and a `language: python` pre-commit
option in docs/ci-and-hooks.md so the hook no longer needs Node.js.
Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
The "Verify all platform packages are published" gate checked npm once,
seconds after the matrix jobs published. npm reads are eventually
consistent, and jscpd-windows-x64-msvc took about three minutes to become
visible on both 5.1.2 and 5.2.0, failing the gate and needing a manual
rerun each time. Re-check only the packages still missing against one
shared 5-minute deadline, as the release-notes step already does (#982).
Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
Everything belonging to the v4 TypeScript engine moves to the master-v4
branch: apps/, packages/, build-utils/, changesets, pnpm workspace, turbo,
the Node.js CI workflow and the ts-* release jobs. What remains is the
Rust workspace under rust/, the npm wrapper and platform packages, the
GitHub Action, Docker image, flake, skills, benchmark and fixtures/.
- rust.yml gains a smoke job that runs the release binary against
fixtures/ (json, sarif, html) and asserts clones and reporters
- release.yml keeps only the Rust jobs; pnpm removed from every workflow;
audit.yml keeps cargo audit + cargo deny; dependabot drops the root npm entry
- root package.json becomes a thin private package depending on the exact
jscpd@5 version so the pre-commit hook keeps installing the binary;
sync-version.mjs keeps it in step
- .pre-commit-hooks.yaml used v4-only flags that the v5 CLI rejects; fixed
- FORMATS.md is now generated from the Rust tokenizer (224 formats)
- README, docs, CONTRIBUTING, SECURITY, package and crate descriptions,
benchmark and skills rewritten for v5 on its own terms: no v5-vs-v4 speed
multipliers anywhere, a single "Looking for v4?" pointer to master-v4
Rust / crates.io
- report the invoked binary name in --version/--help (jscpd vs cpd)
- add repository, documentation, keywords, categories to all crates
- exclude tests/ from the jscpd crate package; add cargo-binstall metadata
- expand the crates.io README and render it on docs.rs
npm
- add funding field to jscpd, cpd and jscpd@4 packages
- add jscpd-linux-arm64-musl platform package and clearer unsupported-platform error
- enable provenance for the v4 changeset publish
Pipelines
- rust.yml: windows-latest in the matrix, drop dead schedule guard
- release.yml: cosign-sign release assets, floating v5 tag, LICENSE in tarballs,
invoke the new docker.yml (GHCR multi-arch image) after each release
- audit.yml: nightly cargo audit, cargo deny and pnpm audit
- Dockerfile: multi-stage image from release binaries (distroless), mcp target for Glama
Docs / community
- README: shields badges, single primary install per OS, GitHub Action snippet,
link to jscpd.dev, v5.1 features, 15 reporters, 224 formats, 8 platforms
- docs: @v5 action tag, Docker usage, baseline section, current crate versions
- YAML issue forms with engine selector, config.yml, CODEOWNERS
- remove stale .github/CONTRIBUTING.md stub, custom.md, TODO.md; untrack rust/jscpd-report.json
- examples: mark as v4 API, add a Rust cpd-finder example
The workspace declared rust-version = "1.87", set once when the Rust
workspace was added (0ddf611, June 2026) and never revisited. crates.io
publishes that field verbatim, so the jscpd crate page advertised a floor
the crate cannot actually build on:
$ cargo +1.93 check -p jscpd
error: rustc 1.93.1 is not supported by the following packages:
oxc_allocator@0.147.0 requires rustc 1.96.0
...
The real floor is 1.96.0, forced by the 13 oxc_* crates that reach the
published crate through the mandatory oxc_parser -> cpd-tokenizer -> jscpd
chain. ignore, globset and askama independently require 1.88. The number
was probably accurate at the time — oxc was pinned to 0.133 then — and
drifted silently as oxc raised its own MSRV across later bumps.
Nothing caught the drift because every workflow pins the toolchain to
1.97.1, so no job ever built at the declared MSRV. This adds an `msrv` job
that reads rust-version straight out of Cargo.toml and runs cargo check
against exactly that toolchain, so the next oxc bump fails loudly instead
of quietly falsifying published metadata.
The check uses `cargo +<version>` rather than relying on the toolchain
installed by the action: rust-toolchain.toml pins 1.97.1 for development
and would otherwise take precedence, making the job test the wrong
compiler.
rust/README.md claimed 1.87+ and pointed at rust-toolchain.toml, which
actually pins 1.97.1 — the two never agreed. It now states the MSRV and
explains what the pin is for.
Verified: cargo +1.96 check --workspace --all-targets --locked passes, so
1.96 is achievable including dev-dependencies.
The npm account enforces 2FA on writes, so dist-tag removal and version
deprecation fail with EOTP from a laptop and need an interactive OTP per
operation. The CI automation token is exempt, so run them here instead.
Adds two optional workflow_dispatch inputs: remove_tags (comma-separated
dist-tags to delete, with latest/latest-4 refused as a guard) and
deprecate_range (semver range plus message). Both skip when empty, so
the existing retag-only behaviour is unchanged.
The release-notes package check ran a single 'npm view' per package
immediately after publishing. npm reads are eventually consistent, so a
package published seconds earlier can still be invisible — v5.1.0 shipped
with a stale 'Not Yet Published: jscpd@5.1.0 (published: 5.0.16)' section
even though the publish had succeeded.
Re-check only the packages still missing, against one shared deadline
(5 min) so a genuinely unpublished package can't stretch the wait, and
report the results in stable package order. Same fix in the TypeScript
release job, which had the identical single-shot check.
Pinning rust-toolchain.toml to 1.97.1 made rustup auto-install a bare
1.97.1 toolchain in CI instead of using the dtolnay-installed 1.97 one
that carried clippy and rustfmt, failing the Clippy step. Declare the
components in the toolchain file so any auto-install includes them, and
align the workflow toolchain pins to 1.97.1 so CI installs the exact
toolchain cargo resolves.
Claude-Session: https://claude.ai/code/session_01VvWzWfEK6oV4HTe6EoGkff
Improves the OpenSSF Scorecard results (currently 7.2):
- Signed-Releases (0/10): release jobs now generate a sha256
checksums.txt and signed SLSA build provenance for the binary
tarballs via actions/attest-build-provenance (keyless, OIDC), and
upload the Sigstore bundle as a multiple.intoto.jsonl release asset.
Users can verify assets with
`gh attestation verify <asset> --repo kucherenko/jscpd`.
- Pinned-Dependencies (8/10): the five `curl | node -e` pipelines that
parse the crates.io API response are `curl | jq` now — jq consumes
the download as data, so nothing downloaded is piped into an
interpreter. The remaining npm warning is the post-publish smoke
test installing the just-published exact version from the live
registry, which is the point of that step.
- README: add Socket.dev and OpenSSF Scorecard badges.
release-rust.yml is currently uncalled (release.yml creates the
releases) but gets the same signing steps so it stays correct if
revived.
- pin every GitHub Action to a full commit SHA (with version comments);
add github-actions ecosystem to Dependabot to keep the pins fresh
- follow least privilege for workflow tokens: top-level contents:read
everywhere, checks/id-token/contents write grants scoped to the jobs
that need them (test-reporter, npm/crates trusted publishing,
gh release create)
- add SECURITY.md with private disclosure channels and supported versions
Also enabled outside the repo: master branch ruleset (no force-push or
deletion) and GitHub private vulnerability reporting.
- release.yml: mark the Rust v5 release --latest, the legacy TS v4 and
cpd releases --latest=false so v4 tags never steal the Latest badge
- rust/jscpd/README.md: use absolute GitHub URLs so links work on npmjs.com
- rust/jscpd/package.json: richer description and search keywords
The jscpd package ships from two release lines: 4.x (TypeScript) and
5.x (Rust wrapper). changeset publish always passes --tag latest (it
ignores publishConfig.tag), so the v4.3.0 maintenance release moved
'latest' from 5.0.15 back to 4.3.0 and 'npx jscpd' started installing
v4 again.
Add a guard step after the TS publish that re-points 'latest' at the
newest 5.x and keeps a 'v4' dist-tag on the newest 4.x, plus a
manually-dispatchable npm-dist-tags workflow with the same logic for
one-off repairs.
When a release run partially fails (e.g. v5.0.15's Windows build), the
retry skips platforms whose npm packages already published — but that
skipped the release-asset archive/upload steps too, and the GitHub
release job only attaches artifacts from its own run. v5.0.15 shipped
with 1 of 6 binaries attached (recovered manually from the first run's
artifacts, which expire after 1 day).
On resumed runs, repack the release-asset tarball from the already
published npm platform package — it contains the exact shipped binary —
so the release job always sees a complete artifact set.
The security overrides lived in package.json's "pnpm.overrides", which
pnpm 10 silently stopped reading (settings moved to pnpm-workspace.yaml)
— so none of them applied and transitive-dependency alerts piled up.
- Move all overrides to pnpm-workspace.yaml and update them to the
patched minimums for every open alert: fast-uri >=3.1.5, hono
>=4.12.34, @hono/node-server >=1.19.15, js-yaml >=3.15.1/4.3.1,
brace-expansion >=1.1.16/5.0.9, nanoid >=3.3.17, postcss >=8.5.23,
ip-address >=10.3.1, body-parser >=2.3.0 (each annotated with its
GHSA id).
- Regenerate pnpm-lock.yaml; pnpm dedupe cleared a stale postcss peer
resolution. All affected packages now resolve to patched versions and
--frozen-lockfile passes.
- Drop the now-ignored pnpm block from package.json so dead config
can't mask itself as protection again.
- CI: install with --frozen-lockfile in nodejs.yml and release.yml so
lockfile corruption or drift fails loudly instead of silently
re-resolving dependencies from the registry (follow-up from #901).
Verified: pnpm build (10/10 tasks), pnpm test (5/5 suites), and
jscpd-server tests (43/43) all pass on the updated resolutions.
Renames the compiled binary from cpd(.exe) to jscpd(.exe) and the
platform-specific optional-dependency npm packages from cpd-* to
jscpd-* to avoid collision with an antivirus-flagged executable name.
Fixes#826
The publish-jscpd job failed on first run and passed on re-run because
npm install -g cpd@${version} ran before the registry CDN had propagated
the freshly published version. Poll npm view for up to 5 minutes before
installing.
Fixes release note step failing with "cpd-core@0.1.5: command not found" because markdown backticks in the published/failed package lists were interpreted as command substitution when assigned inside double quotes.