Files
kucherenko__jscpd/.github/workflows/crates-publish.yml
apk b718753839 chore(basta): ship basta on its own trigger, separate from the jscpd release
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
2026-09-16 11:01:06 +02:00

209 lines
8.0 KiB
YAML

name: crates-publish
on:
workflow_call:
inputs:
version:
required: true
type: string
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
permissions:
contents: read
defaults:
run:
shell: bash
working-directory: rust
jobs:
version:
name: resolve version
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
skip: ${{ steps.skip.outputs.skip }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Read version
id: version
working-directory: rust
run: |
if [ -n "${{ inputs.version }}" ]; then
version="${{ inputs.version }}"
else
version=$(node -p "require('./package.json').version")
fi
echo "version=${version}" >> "$GITHUB_OUTPUT"
- name: Check if already published
id: skip
working-directory: rust
run: |
version="${{ steps.version.outputs.version }}"
# Use crates.io HTTP API for exact version match.
# cargo info jscpd@ver exits 0 if the crate exists at ANY version.
max_ver=$(curl -sfH "Accept: application/json" -H "User-Agent: jscpd-ci (github.com/kucherenko/jscpd)" "https://crates.io/api/v1/crates/jscpd" 2>/dev/null \
| jq -r '.crate.max_version // empty' || echo "")
if [ "$max_ver" = "$version" ] && [ -n "$max_ver" ]; then
echo "jscpd@${version} is already published on crates.io (max_version=${max_ver}); skipping"
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "jscpd@${version} not yet on crates.io (max_version=${max_ver:-none}); will publish"
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
publish:
name: publish crates.io packages
needs: version
if: needs.version.outputs.skip != 'true'
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # crates.io trusted publishing (crates-io-auth-action)
checks: write # dorny/test-reporter posts check runs
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
with:
toolchain: "1.97.1"
- name: Cache Cargo registry and target
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
with:
workspaces: "rust -> rust/target"
cache-on-failure: true
- name: Install Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "22"
package-manager-cache: false
- name: Sync version from package.json
run: node scripts/sync-version.mjs
- name: Install cargo-nextest
uses: taiki-e/install-action@d438492cf8a250514fa2d34b30bc3c0dc37c65ff # v2.87.8
with:
tool: nextest
- name: Test
run: cargo nextest run --workspace --locked --profile ci
- name: Upload test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: nextest-results-crates-publish
path: rust/target/nextest/ci/nextest.xml
if-no-files-found: warn
- name: Test Report
if: always()
uses: dorny/test-reporter@a43b3a5f7366b97d083190328d2c652e1a8b6aa2 # v3.0.0
with:
name: Rust Tests (crates-publish)
path: rust/target/nextest/ci/nextest.xml
reporter: java-junit
fail-on-error: false
- name: Remove publish = false from all crates
run: |
find crates -name Cargo.toml -exec sed -i '/^publish = false$/d' {} \;
sed -i '/^publish = false$/d' Cargo.toml
echo "Removed publish = false from all crates"
- name: Smoke test (build and version check)
run: |
cargo build --release --locked -p jscpd
BUILT_VERSION=$(./target/release/cpd --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "NOT_FOUND")
echo "Built version: ${BUILT_VERSION}"
echo "Expected version: ${{ needs.version.outputs.version }}"
[ "$BUILT_VERSION" = "${{ needs.version.outputs.version }}" ] || { echo "Version mismatch!"; exit 1; }
- name: Authenticate to crates.io (Trusted Publishing)
id: crates-io-auth
uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1.0.5
- name: Publish crates in dependency order
working-directory: rust
env:
CARGO_REGISTRY_TOKEN: ${{ steps.crates-io-auth.outputs.token || secrets.CARGO_REGISTRY_TOKEN }}
run: |
# Discover workspace members in topological order (dependencies first).
#
# `basta` is released on its own trigger (basta-release.yml) and is
# deliberately not part of the jscpd release. It still appears here
# because `jscpd` depends on it, and cargo refuses to publish a crate
# whose dependency is not on crates.io — so a jscpd release must be
# able to put a missing basta there first. The version check below
# makes that a no-op whenever basta is already published, which is
# the normal case.
ORDER=$(cargo metadata --format-version 1 --no-deps 2>/dev/null | node -e "
const meta = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8'));
const wsPkgs = meta.packages.filter(p => !p.id.includes('fuzz'));
const byName = new Map(wsPkgs.map(p => [p.name, p]));
const graph = new Map();
for (const p of wsPkgs) {
graph.set(p.name, p.dependencies
.filter(d => d.path && byName.has(d.name))
.map(d => d.name));
}
const sorted = [], visited = new Set();
function visit(n) {
if (visited.has(n)) return;
visited.add(n);
for (const dep of (graph.get(n) || [])) visit(dep);
sorted.push(n);
}
for (const n of graph.keys()) visit(n);
for (const name of sorted) {
const pkg = byName.get(name);
console.log(name + ':' + pkg.version);
}
")
echo "Publish order:"
echo "$ORDER"
for entry in $ORDER; do
crate="${entry%%:*}"
crate_version="${entry##*:}"
echo "--- Publishing ${crate}@${crate_version} ---"
# Use crates.io HTTP API for exact version check before publishing.
# cargo info crate@ver exits 0 if the crate exists at ANY version,
# not just the specified one — so we must compare max_version instead.
max_ver=$(curl -sfH "Accept: application/json" -H "User-Agent: jscpd-ci (github.com/kucherenko/jscpd)" "https://crates.io/api/v1/crates/${crate}" 2>/dev/null \
| jq -r '.crate.max_version // empty' || echo "")
if [ "$max_ver" = "$crate_version" ] && [ -n "$max_ver" ]; then
echo "${crate}@${crate_version} already published on crates.io (max_version=${max_ver}); skipping"
continue
fi
if ! cargo publish --locked -p "${crate}" --allow-dirty; then
echo "⚠ Publish failed for ${crate}@${crate_version} (may already exist); continuing"
continue
fi
echo "Waiting for crates.io index to update for ${crate}@${crate_version}..."
for i in $(seq 1 30); do
# After publish, cargo info can verify the exact version since it now exists.
if cargo info "${crate}@${crate_version}" >/dev/null 2>&1; then
echo "${crate}@${crate_version} is available on crates.io"
break
fi
echo "Attempt $i/30: waiting for index..."
sleep 10
done
done