diff --git a/.github/workflows/test-pack-dist-dynamic.yaml b/.github/workflows/test-pack-dist-dynamic.yaml new file mode 100644 index 0000000..cd2f8ed --- /dev/null +++ b/.github/workflows/test-pack-dist-dynamic.yaml @@ -0,0 +1,25 @@ +name: Test pack-dist-dynamic +on: + push: + paths: + - export-dynamic/** + - .github/workflows/test-pack-dist-dynamic.yaml + pull_request: + paths: + - export-dynamic/** + - .github/workflows/test-pack-dist-dynamic.yaml + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Setup Node.js 24.x + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version: 24.x + + - name: Run pack-dist-dynamic tests + run: bash export-dynamic/pack-dist-dynamic.test.sh diff --git a/export-dynamic/export-dynamic.sh b/export-dynamic/export-dynamic.sh index 5c1d886..ac33f6f 100755 --- a/export-dynamic/export-dynamic.sh +++ b/export-dynamic/export-dynamic.sh @@ -37,6 +37,10 @@ fi # export INPUTS_CLI_CALLER=/path/to/node_modules/.bin/rhdh-cli INPUTS_CLI_CALLER=${INPUTS_CLI_CALLER:-"npx --yes ${INPUTS_CLI_PACKAGE}@${INPUTS_CLI_VERSION}"} +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=export-dynamic/pack-dist-dynamic.sh +source "${SCRIPT_DIR}/pack-dist-dynamic.sh" + # Check local installation first, then fall back to npx --yes (requires network) run_cli() { local cli_args=("$@") @@ -208,8 +212,8 @@ else packDestination=${INPUTS_DESTINATION} mkdir -pv "${packDestination}" - echo " running npm pack on the exported './dist-dynamic' sub-folder" - if ! json=$(npm pack ./dist-dynamic --pack-destination "$packDestination" --json); then + echo " running npm pack on a hardlink-free copy of './dist-dynamic'" + if ! json=$(pack_dist_dynamic "$(pwd)/dist-dynamic" "$packDestination"); then errors+=("${pluginPath}") set -e popd > /dev/null diff --git a/export-dynamic/pack-dist-dynamic.sh b/export-dynamic/pack-dist-dynamic.sh new file mode 100755 index 0000000..a994e2b --- /dev/null +++ b/export-dynamic/pack-dist-dynamic.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Stage dist-dynamic to a short, hardlink-free copy, then npm pack. +# +# Yarn nmMode: hardlinks-local trees crash npm 11 (`Exit handler never called!`). +# `cp -r` (not `cp -a`) copies file contents and breaks those hardlinks. +# A short /tmp prefix also avoids npm pack failures on very long paths (RHDHBUGS-3556). + +copy_dist_dynamic_without_hardlinks() { + local src="$1" + local dest="$2" + + if [[ ! -d "$src" ]]; then + echo " dist-dynamic directory not found: ${src}" >&2 + return 1 + fi + mkdir -p "$dest" || return 1 + # Copy package contents into dest (not a nested dist-dynamic folder). + # Do not use cp -a / --preserve=links: that keeps Yarn hardlinks. + cp -r "${src}/." "${dest}/" +} + +pack_dist_dynamic() { + local dist_dynamic="$1" + local pack_destination="$2" + local stage_dir + local output + local pack_status=0 + + mkdir -p "$pack_destination" || return 1 + + stage_dir=$(mktemp -d /tmp/dist-XXXXXX) || return 1 + if ! copy_dist_dynamic_without_hardlinks "$dist_dynamic" "$stage_dir"; then + rm -rf "$stage_dir" + return 1 + fi + + # Pack the staged package root (equivalent to `npm pack .` in dist-dynamic). + output=$( + cd "$stage_dir" && npm pack --pack-destination "$pack_destination" --json --foreground-scripts=false + ) || pack_status=$? + + rm -rf "$stage_dir" + + if [[ "$pack_status" -ne 0 ]]; then + return "$pack_status" + fi + printf '%s' "$output" +} diff --git a/export-dynamic/pack-dist-dynamic.test.sh b/export-dynamic/pack-dist-dynamic.test.sh new file mode 100755 index 0000000..c78034e --- /dev/null +++ b/export-dynamic/pack-dist-dynamic.test.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=export-dynamic/pack-dist-dynamic.sh +source "${SCRIPT_DIR}/pack-dist-dynamic.sh" + +assert_eq() { + local description="$1" + local expected="$2" + local actual="$3" + if [[ "$expected" == "$actual" ]]; then + echo "PASS: ${description}" + else + echo "FAIL: ${description} (expected '${expected}', got '${actual}')" >&2 + exit 1 + fi +} + +assert_true() { + local description="$1" + shift + if "$@"; then + echo "PASS: ${description}" + else + echo "FAIL: ${description}" >&2 + exit 1 + fi +} + +WORKDIR=$(mktemp -d /tmp/pack-dist-dynamic-test-XXXXXX) +trap 'rm -rf "$WORKDIR"' EXIT + +FIXTURE="${WORKDIR}/dist-dynamic" +DEST="${WORKDIR}/archives" +STAGE="${WORKDIR}/staged" + +mkdir -p "${FIXTURE}/node_modules/dup" +cat > "${FIXTURE}/package.json" <<'EOF' +{ + "name": "test-hardlink-pack", + "version": "1.0.0", + "bundleDependencies": true +} +EOF +printf 'payload\n' > "${FIXTURE}/payload.txt" +# Same inode in two paths, matching Yarn hardlinks-local under node_modules. +ln "${FIXTURE}/payload.txt" "${FIXTURE}/node_modules/dup/payload.txt" + +orig_hardlinks=$(find "$FIXTURE" -type f -links +1 | wc -l) +assert_true "fixture has hardlinked files" test "${orig_hardlinks}" -gt 0 + +copy_dist_dynamic_without_hardlinks "$FIXTURE" "$STAGE" +staged_hardlinks=$(find "$STAGE" -type f -links +1 | wc -l) +assert_eq "staged copy has no hardlinked files" "0" "${staged_hardlinks}" + +after_copy_orig=$(find "$FIXTURE" -type f -links +1 | wc -l) +assert_eq "original tree still has hardlinks after copy" "${orig_hardlinks}" "${after_copy_orig}" + +json=$(pack_dist_dynamic "$FIXTURE" "$DEST") +filename=$(echo "$json" | jq -r '.[0].filename') +integrity=$(echo "$json" | jq -r '.[0].integrity') + +assert_true "npm pack returned a filename" test -n "$filename" +assert_true "tgz exists in destination" test -f "${DEST}/${filename}" +assert_true "integrity is non-empty" test -n "$integrity" + +echo "$integrity" > "${DEST}/${filename}.integrity" +assert_true "integrity sidecar is written" test -f "${DEST}/${filename}.integrity" + +after_pack_orig=$(find "$FIXTURE" -type f -links +1 | wc -l) +assert_eq "original tree still has hardlinks after pack" "${orig_hardlinks}" "${after_pack_orig}" + +echo "All pack-dist-dynamic tests passed."