mirror of
https://github.com/kucherenko/jscpd.git
synced 2026-09-19 08:11:03 +08:00
4c3f007d0b
`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
427 lines
17 KiB
YAML
427 lines
17 KiB
YAML
name: basta-release
|
|
|
|
# basta is released on its own trigger, deliberately not the one that
|
|
# releases jscpd. jscpd goes out on every push to master that leaves a
|
|
# version unpublished; basta goes out only when a `basta-v*` tag is pushed,
|
|
# so its release is an explicit act and a jscpd release never drags it along.
|
|
#
|
|
# The one place the two cannot be separated is crates.io: `jscpd` depends on
|
|
# `basta`, so cargo refuses to publish jscpd until the matching basta version
|
|
# exists. The jscpd crates job therefore still publishes basta when it is
|
|
# missing. Both paths skip a version that is already there, so whichever runs
|
|
# first wins and the other is a no-op.
|
|
|
|
on:
|
|
push:
|
|
tags:
|
|
- "basta-v*"
|
|
workflow_dispatch:
|
|
inputs:
|
|
version:
|
|
description: "Version to release (defaults to rust/basta/package.json)"
|
|
required: false
|
|
type: string
|
|
|
|
concurrency:
|
|
group: basta-release
|
|
cancel-in-progress: false
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
env:
|
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
|
|
|
defaults:
|
|
run:
|
|
shell: bash
|
|
|
|
jobs:
|
|
version:
|
|
name: resolve and check version
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
version: ${{ steps.version.outputs.version }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
with:
|
|
node-version: "22"
|
|
package-manager-cache: false
|
|
|
|
# One version, three places: the crate, the npm wrapper and the tag.
|
|
# A release that disagrees with itself ships a wrapper resolving an
|
|
# engine it does not name, which is how jscpd@5.1.0 went out pinned to
|
|
# the 5.0.16 binaries. Fail here instead.
|
|
- name: Resolve version and check it agrees everywhere
|
|
id: version
|
|
working-directory: rust
|
|
run: |
|
|
node scripts/sync-version.mjs
|
|
manifest=$(node -p "require('./basta/package.json').version")
|
|
crate=$(grep -m1 '^version = ' crates/basta/Cargo.toml | cut -d'"' -f2)
|
|
[ "$manifest" = "$crate" ] || {
|
|
echo "::error::basta/package.json says ${manifest}, crates/basta/Cargo.toml says ${crate}"
|
|
exit 1
|
|
}
|
|
|
|
requested="${{ inputs.version }}"
|
|
if [ -z "$requested" ] && [ "${{ github.ref_type }}" = "tag" ]; then
|
|
requested="${GITHUB_REF_NAME#basta-v}"
|
|
fi
|
|
if [ -n "$requested" ] && [ "$requested" != "$manifest" ]; then
|
|
echo "::error::asked for ${requested} but the tree says ${manifest}"
|
|
exit 1
|
|
fi
|
|
|
|
if ! git diff --quiet; then
|
|
echo "::error::sync-version.mjs changed tracked files; commit the sync before tagging"
|
|
git diff --stat
|
|
exit 1
|
|
fi
|
|
|
|
echo "version=${manifest}" >> "$GITHUB_OUTPUT"
|
|
echo "Releasing basta ${manifest}"
|
|
|
|
crates:
|
|
name: publish crates.io
|
|
needs: version
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
permissions:
|
|
contents: read
|
|
id-token: write # crates.io trusted publishing
|
|
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: Test
|
|
working-directory: rust
|
|
run: cargo test --workspace --locked
|
|
|
|
- name: Remove publish = false
|
|
working-directory: rust
|
|
run: find crates -name Cargo.toml -exec sed -i '/^publish = false$/d' {} \;
|
|
|
|
# Trusted Publishing is configured per crate and per workflow filename,
|
|
# and it can only be configured on a crate that already exists. The very
|
|
# first publish of a new crate therefore has to fall back to the token —
|
|
# and this step must not be fatal, or the job dies before reaching the
|
|
# fallback it already has.
|
|
- name: Authenticate to crates.io (Trusted Publishing)
|
|
id: crates-io-auth
|
|
continue-on-error: true
|
|
uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1.0.5
|
|
|
|
- name: Check a crates.io credential is available
|
|
run: |
|
|
if [ -z "${{ steps.crates-io-auth.outputs.token }}" ] && [ -z "${{ secrets.CARGO_REGISTRY_TOKEN }}" ]; then
|
|
echo "::error::no crates.io credential: Trusted Publishing did not issue a token and CARGO_REGISTRY_TOKEN is unset"
|
|
exit 1
|
|
fi
|
|
|
|
# `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 currently on
|
|
# crates.io — so basta cannot go out until those do. Rather than wait
|
|
# for a jscpd release, publish whatever basta needs, in dependency
|
|
# order, skipping anything already there.
|
|
- name: Publish basta and any dependency it needs
|
|
working-directory: rust
|
|
env:
|
|
CARGO_REGISTRY_TOKEN: ${{ steps.crates-io-auth.outputs.token || secrets.CARGO_REGISTRY_TOKEN }}
|
|
run: |
|
|
order=$(cargo metadata --format-version 1 --no-deps | node -e "
|
|
const meta = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8'));
|
|
const ws = meta.packages.filter(p => !p.id.includes('fuzz'));
|
|
const byName = new Map(ws.map(p => [p.name, p]));
|
|
const seen = new Set(), out = [];
|
|
// Only what basta actually reaches: jscpd is a sibling, not a
|
|
// dependency, and releasing it is not this workflow's business.
|
|
(function visit(name) {
|
|
if (seen.has(name) || !byName.has(name)) return;
|
|
seen.add(name);
|
|
for (const d of byName.get(name).dependencies) {
|
|
if (d.path && byName.has(d.name)) visit(d.name);
|
|
}
|
|
out.push(name + ':' + byName.get(name).version);
|
|
})('basta');
|
|
out.forEach(x => console.log(x));
|
|
")
|
|
echo "Publish order:"
|
|
echo "$order"
|
|
|
|
for entry in $order; do
|
|
crate="${entry%%:*}"
|
|
crate_version="${entry##*:}"
|
|
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" ]; then
|
|
echo "${crate}@${crate_version} already on crates.io; skipping"
|
|
continue
|
|
fi
|
|
echo "--- publishing ${crate}@${crate_version} ---"
|
|
cargo publish --locked -p "${crate}" --allow-dirty
|
|
|
|
# The index has to carry the new version before the next crate
|
|
# can build against it.
|
|
for i in $(seq 1 30); do
|
|
cargo info "${crate}@${crate_version}" >/dev/null 2>&1 && break
|
|
echo "waiting for the index to show ${crate}@${crate_version} ($i/30)"
|
|
sleep 10
|
|
done
|
|
done
|
|
|
|
build:
|
|
name: build ${{ matrix.target.key }}
|
|
needs: version
|
|
runs-on: ${{ matrix.target.runner }}
|
|
timeout-minutes: 30
|
|
permissions:
|
|
contents: read
|
|
id-token: write # npm publish --provenance
|
|
strategy:
|
|
fail-fast: false
|
|
max-parallel: 6
|
|
matrix:
|
|
target:
|
|
- key: linux-x64-gnu
|
|
os: linux
|
|
cpu: x64
|
|
libc: glibc
|
|
rustTarget: x86_64-unknown-linux-gnu
|
|
runner: ubuntu-22.04
|
|
- key: linux-arm64-gnu
|
|
os: linux
|
|
cpu: arm64
|
|
libc: glibc
|
|
rustTarget: aarch64-unknown-linux-gnu
|
|
runner: ubuntu-22.04-arm
|
|
- key: linux-x64-musl
|
|
os: linux
|
|
cpu: x64
|
|
libc: musl
|
|
rustTarget: x86_64-unknown-linux-musl
|
|
runner: ubuntu-latest
|
|
- key: linux-arm64-musl
|
|
os: linux
|
|
cpu: arm64
|
|
libc: musl
|
|
rustTarget: aarch64-unknown-linux-musl
|
|
runner: ubuntu-22.04-arm
|
|
- key: darwin-arm64
|
|
os: darwin
|
|
cpu: arm64
|
|
rustTarget: aarch64-apple-darwin
|
|
runner: macos-latest
|
|
- key: darwin-x64
|
|
os: darwin
|
|
cpu: x64
|
|
rustTarget: x86_64-apple-darwin
|
|
runner: macos-latest
|
|
- key: windows-x64-msvc
|
|
os: win32
|
|
cpu: x64
|
|
rustTarget: x86_64-pc-windows-msvc
|
|
runner: windows-latest
|
|
- key: windows-arm64-msvc
|
|
os: win32
|
|
cpu: arm64
|
|
rustTarget: aarch64-pc-windows-msvc
|
|
runner: windows-11-arm
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
|
|
- name: Install Node
|
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
with:
|
|
node-version: "22"
|
|
registry-url: "https://registry.npmjs.org"
|
|
package-manager-cache: false
|
|
|
|
- name: Check if platform package is already published
|
|
id: platform-package
|
|
run: |
|
|
version="${{ needs.version.outputs.version }}"
|
|
package_name="basta-${{ matrix.target.key }}"
|
|
if npm view "${package_name}@${version}" version >/dev/null 2>&1; then
|
|
echo "published=true" >> "$GITHUB_OUTPUT"
|
|
echo "${package_name}@${version} is already published; skipping build"
|
|
else
|
|
echo "published=false" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
- name: Install Rust toolchain
|
|
if: steps.platform-package.outputs.published != 'true'
|
|
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
|
|
with:
|
|
toolchain: "1.97.1"
|
|
targets: ${{ matrix.target.rustTarget }}
|
|
|
|
- name: Cache Cargo registry and target
|
|
if: steps.platform-package.outputs.published != 'true'
|
|
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
|
|
with:
|
|
workspaces: "rust -> rust/target"
|
|
cache-on-failure: true
|
|
|
|
- name: Install cross-compilation tools (Linux ARM64)
|
|
if: steps.platform-package.outputs.published != 'true' && matrix.target.key == 'linux-arm64-gnu'
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y gcc-aarch64-linux-gnu
|
|
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV
|
|
|
|
- name: Install musl tools
|
|
if: steps.platform-package.outputs.published != 'true' && matrix.target.libc == 'musl'
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y musl-tools
|
|
|
|
# The workspace test suite runs once, in the `crates` job of this same
|
|
# workflow; repeating it per target would multiply CI time for no new
|
|
# signal beyond the per-target build below.
|
|
- name: Build basta
|
|
if: steps.platform-package.outputs.published != 'true'
|
|
working-directory: rust
|
|
run: cargo build --release --locked --target ${{ matrix.target.rustTarget }} -p basta
|
|
|
|
- name: Verify the built binary reports the expected version
|
|
if: steps.platform-package.outputs.published != 'true' && matrix.target.key != 'darwin-x64' && matrix.target.os != 'win32'
|
|
working-directory: rust
|
|
run: |
|
|
expected="${{ needs.version.outputs.version }}"
|
|
built=$(./target/${{ matrix.target.rustTarget }}/release/basta --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
|
|
[ "$built" = "$expected" ] || { echo "::error::built basta $built, expected $expected"; exit 1; }
|
|
|
|
- name: Package platform package
|
|
id: package
|
|
if: steps.platform-package.outputs.published != 'true'
|
|
working-directory: rust
|
|
run: |
|
|
package_dir=$(
|
|
node scripts/npm-prebuilt-package.mjs \
|
|
--product basta \
|
|
--target "${{ matrix.target.key }}" \
|
|
--bin-dir "target/${{ matrix.target.rustTarget }}/release" \
|
|
--out-dir "target/npm-prebuilt"
|
|
)
|
|
echo "package_dir=${package_dir}" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Publish platform package
|
|
if: steps.platform-package.outputs.published != 'true'
|
|
env:
|
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
working-directory: rust
|
|
run: |
|
|
version="${{ needs.version.outputs.version }}"
|
|
package_name="basta-${{ matrix.target.key }}"
|
|
if npm view "${package_name}@${version}" version >/dev/null 2>&1; then
|
|
echo "${package_name}@${version} is already published; skipping"
|
|
exit 0
|
|
fi
|
|
npm publish "${{ steps.package.outputs.package_dir }}" --access public --provenance
|
|
|
|
publish:
|
|
name: publish basta wrapper
|
|
needs:
|
|
- version
|
|
- build
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
permissions:
|
|
contents: read
|
|
id-token: write # npm publish --provenance
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
|
|
- name: Install Node
|
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
with:
|
|
node-version: "22"
|
|
registry-url: "https://registry.npmjs.org"
|
|
package-manager-cache: false
|
|
|
|
# npm reads are eventually consistent: a platform package published
|
|
# seconds earlier can still be invisible here. Re-check only what is
|
|
# still missing, against one shared deadline, as the jscpd gate does.
|
|
- name: Verify all platform packages are published
|
|
working-directory: rust
|
|
run: |
|
|
version="${{ needs.version.outputs.version }}"
|
|
missing=$(node -e "
|
|
const t = require('./npm/prebuilt-targets.json');
|
|
Object.keys(t).forEach(k => console.log('basta-' + k));
|
|
")
|
|
deadline=$(( $(date +%s) + 300 ))
|
|
while [ -n "$missing" ]; do
|
|
still=""
|
|
for package_name in $missing; do
|
|
npm view "${package_name}@${version}" version >/dev/null 2>&1 || still="$still $package_name"
|
|
done
|
|
missing=$(echo $still)
|
|
[ -z "$missing" ] && break
|
|
if [ "$(date +%s)" -ge "$deadline" ]; then
|
|
echo "::error::platform packages never appeared on npm:$missing"
|
|
exit 1
|
|
fi
|
|
echo "waiting for:$missing"
|
|
sleep 15
|
|
done
|
|
echo "all basta platform packages are visible at ${version}"
|
|
|
|
- name: Publish basta package
|
|
working-directory: rust/basta
|
|
env:
|
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
run: |
|
|
version="${{ needs.version.outputs.version }}"
|
|
if npm view "basta@${version}" version >/dev/null 2>&1; then
|
|
echo "basta@${version} is already published; skipping"
|
|
exit 0
|
|
fi
|
|
npm publish --access public --provenance
|
|
|
|
# `npm publish` tags what it publishes as `latest`, but the package
|
|
# carries 1.x versions from an unrelated tool, and a dist-tag that
|
|
# silently stayed on those would hand every `npm i basta` the wrong
|
|
# program. Assert it rather than assume it.
|
|
# npm reads are eventually consistent, and a publish says so itself:
|
|
# "Your package is being processed and may take a few minutes to become
|
|
# available." Reading the dist-tag immediately after publishing returns
|
|
# the *previous* one, so this polls to a deadline instead of trusting a
|
|
# single read — the same way the platform-package gate above does.
|
|
- name: Check the latest tag points at this release
|
|
run: |
|
|
version="${{ needs.version.outputs.version }}"
|
|
deadline=$(( $(date +%s) + 300 ))
|
|
while :; do
|
|
latest=$(npm view basta dist-tags.latest 2>/dev/null || echo "")
|
|
[ "$latest" = "$version" ] && break
|
|
if [ "$(date +%s)" -ge "$deadline" ]; then
|
|
echo "::error::basta dist-tag latest is ${latest:-unset}, expected ${version} after 5 minutes;"
|
|
echo "::error::fix with: npm dist-tag add basta@${version} latest"
|
|
exit 1
|
|
fi
|
|
echo "dist-tag still ${latest:-unset}; waiting for the registry to catch up"
|
|
sleep 15
|
|
done
|
|
echo "basta latest -> ${version}"
|