diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2750c9a6..cdbfd7af 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -38,11 +38,31 @@ jobs: - name: Checkout ${{ github.repository }} uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: + fetch-depth: 0 persist-credentials: false - name: Run pre-commit + env: + FEATURE_VERSION_BASE_SHA: ${{ github.event.pull_request.base.sha }} run: pip install pre-commit && pre-commit run --all-files + feature-version-check-tests: + name: Feature version check tests + permissions: + contents: read + runs-on: ubuntu-latest + steps: + - name: Checkout ${{ github.repository }} + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Run feature version check tests + run: ci/check-feature-version-bumps-tests.sh + + - name: Shellcheck feature version check scripts + run: shellcheck ci/check-feature-version-bumps.sh ci/check-feature-version-bumps-tests.sh + build-all-rapids-repos: if: needs.check-event.outputs.ok == 'true' && github.repository_owner == 'rapidsai' name: Build diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c3260a0c..1b7858bf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,3 +4,13 @@ repos: rev: v1.25.2 hooks: - id: zizmor + - repo: local + hooks: + # Opt in to automatic feature version updates in this checkout with: + # git config devcontainers.auto-bump-feature-versions true + - id: check-feature-version-bumps + name: check feature version bumps + entry: ci/check-feature-version-bumps.sh + language: system + always_run: true + pass_filenames: false diff --git a/ci/check-feature-version-bumps-tests.sh b/ci/check-feature-version-bumps-tests.sh new file mode 100755 index 00000000..31f8aec9 --- /dev/null +++ b/ci/check-feature-version-bumps-tests.sh @@ -0,0 +1,290 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +checker="${repo_root}/ci/check-feature-version-bumps.sh" +test_root="$(mktemp -d)" +trap 'rm -rf "${test_root}"' EXIT + +# A CI base SHA from the outer repository is meaningless inside the fixtures. +unset FEATURE_VERSION_BASE_SHA + +fixture_dir="" +output_file="" + +make_fixture() { + local name="$1" + + fixture_dir="${test_root}/${name}" + output_file="${fixture_dir}/checker-output" + mkdir -p \ + "${fixture_dir}/ci" \ + "${fixture_dir}/features/common" \ + "${fixture_dir}/features/src/alpha" \ + "${fixture_dir}/features/src/beta" + cp "${checker}" "${fixture_dir}/ci/check-feature-version-bumps.sh" + printf '%s\n' '#!/usr/bin/env bash' 'echo alpha' > "${fixture_dir}/features/src/alpha/install.sh" + printf '%s\n' '#!/usr/bin/env bash' 'echo beta' > "${fixture_dir}/features/src/beta/install.sh" + printf '%s\n' '# Alpha' > "${fixture_dir}/features/src/alpha/README.md" + printf '%s\n' 'echo common' > "${fixture_dir}/features/common/utilities.sh" + printf '%s\n' \ + '{"id":"alpha","version":"1.0.0","description":"Alpha"}' \ + > "${fixture_dir}/features/src/alpha/devcontainer-feature.json" + printf '%s\n' \ + '{"id":"beta","version":"1.0.0","description":"Beta"}' \ + > "${fixture_dir}/features/src/beta/devcontainer-feature.json" + + git -C "${fixture_dir}" init -q + git -C "${fixture_dir}" config user.email test@example.com + git -C "${fixture_dir}" config user.name "Feature version check tests" + git -C "${fixture_dir}" config commit.gpgsign false + git -C "${fixture_dir}" add . + git -C "${fixture_dir}" commit -qm baseline +} + +bump_feature_to() { + local feature="$1" + local version="$2" + local manifest="${fixture_dir}/features/src/${feature}/devcontainer-feature.json" + local temporary="${manifest}.tmp" + + jq --arg version "${version}" '.version = $version' "${manifest}" > "${temporary}" + mv "${temporary}" "${manifest}" + git -C "${fixture_dir}" add "features/src/${feature}/devcontainer-feature.json" +} + +bump_feature() { + bump_feature_to "$1" 1.0.1 +} + +run_checker() { + ( + cd "${fixture_dir}" + ./ci/check-feature-version-bumps.sh "$@" + ) > "${output_file}" 2>&1 +} + +run_checker_with_auto_bump() { + ( + cd "${fixture_dir}" + FEATURE_VERSION_AUTO_BUMP=1 ./ci/check-feature-version-bumps.sh "$@" + ) > "${output_file}" 2>&1 +} + +feature_version() { + jq -er '.version' "${fixture_dir}/features/src/$1/devcontainer-feature.json" +} + +expect_success() { + local name="$1" + shift + if ! run_checker "$@"; then + echo "FAIL: ${name} should have passed" >&2 + sed 's/^/ /' "${output_file}" >&2 + exit 1 + fi +} + +expect_failure() { + local name="$1" + shift + if run_checker "$@"; then + echo "FAIL: ${name} should have failed" >&2 + exit 1 + fi +} + +expect_auto_bump() { + local name="$1" + shift + if run_checker_with_auto_bump "$@"; then + echo "FAIL: ${name} should have stopped the commit after updating files" >&2 + exit 1 + fi +} + +expect_success_with_auto_bump() { + local name="$1" + shift + if ! run_checker_with_auto_bump "$@"; then + echo "FAIL: ${name} should have passed without another version bump" >&2 + sed 's/^/ /' "${output_file}" >&2 + exit 1 + fi +} + +make_fixture payload_without_bump +printf '%s\n' 'echo changed' >> "${fixture_dir}/features/src/alpha/install.sh" +git -C "${fixture_dir}" add features/src/alpha/install.sh +expect_failure payload_without_bump +grep -q 'alpha:.*still 1.0.0' "${output_file}" +grep -q 'git config devcontainers.auto-bump-feature-versions true' "${output_file}" + +make_fixture payload_with_auto_bump +printf '%s\n' 'echo changed' >> "${fixture_dir}/features/src/alpha/install.sh" +git -C "${fixture_dir}" add features/src/alpha/install.sh +expect_auto_bump payload_with_auto_bump +[[ "$(feature_version alpha)" == 1.0.1 ]] +grep -q 'alpha: 1.0.0 -> 1.0.1' "${output_file}" +git -C "${fixture_dir}" add features/src/alpha/devcontainer-feature.json +expect_success payload_with_auto_bump_retry + +make_fixture payload_with_auto_bump_from_git_config +git -C "${fixture_dir}" config devcontainers.auto-bump-feature-versions true +printf '%s\n' 'echo changed' >> "${fixture_dir}/features/src/alpha/install.sh" +git -C "${fixture_dir}" add features/src/alpha/install.sh +expect_failure payload_with_auto_bump_from_git_config +[[ "$(feature_version alpha)" == 1.0.1 ]] + +make_fixture payload_with_manual_bump_and_auto_enabled +printf '%s\n' 'echo changed' >> "${fixture_dir}/features/src/alpha/install.sh" +git -C "${fixture_dir}" add features/src/alpha/install.sh +bump_feature alpha +expect_success_with_auto_bump payload_with_manual_bump_and_auto_enabled +[[ "$(feature_version alpha)" == 1.0.1 ]] + +make_fixture descriptor_payload_without_bump +manifest="${fixture_dir}/features/src/alpha/devcontainer-feature.json" +jq '.description = "Changed"' "${manifest}" > "${manifest}.tmp" +mv "${manifest}.tmp" "${manifest}" +git -C "${fixture_dir}" add features/src/alpha/devcontainer-feature.json +expect_failure descriptor_payload_without_bump + +make_fixture version_only +bump_feature alpha +expect_success version_only + +make_fixture historical_version_reuse +bump_feature_to alpha 1.0.5 +git -C "${fixture_dir}" commit -qm high_water_mark +bump_feature_to alpha 1.0.0 +git -C "${fixture_dir}" commit -qm reset +base_sha="$(git -C "${fixture_dir}" rev-parse HEAD)" +bump_feature_to alpha 1.0.1 +expect_failure historical_version_reuse +grep -q 'alpha: 1.0.1 is not newer than 1.0.5' "${output_file}" +git -C "${fixture_dir}" commit -qm reused_version +expect_failure historical_version_reuse_in_pull_request "${base_sha}" +bump_feature_to alpha 1.0.6 +git -C "${fixture_dir}" commit -qm version_above_high_water_mark +expect_success version_above_high_water_mark_in_pull_request "${base_sha}" + +make_fixture historical_version_reset +bump_feature_to alpha 1.0.5 +git -C "${fixture_dir}" commit -qm high_water_mark +bump_feature_to alpha 1.0.0 +expect_failure historical_version_reset +grep -q 'alpha: 1.0.0 is not newer than 1.0.5' "${output_file}" + +make_fixture payload_auto_bump_above_high_water_mark +bump_feature_to alpha 1.0.5 +git -C "${fixture_dir}" commit -qm high_water_mark +bump_feature_to alpha 1.0.0 +git -C "${fixture_dir}" commit -qm reset +printf '%s\n' 'echo changed' >> "${fixture_dir}/features/src/alpha/install.sh" +git -C "${fixture_dir}" add features/src/alpha/install.sh +expect_auto_bump payload_auto_bump_above_high_water_mark +[[ "$(feature_version alpha)" == 1.0.6 ]] +grep -q 'alpha: 1.0.0 -> 1.0.6' "${output_file}" + +make_fixture other_version_series_ignored +bump_feature_to alpha 1.1.5 +git -C "${fixture_dir}" commit -qm other_series_high_water_mark +bump_feature_to alpha 1.0.0 +git -C "${fixture_dir}" commit -qm reset_original_series +bump_feature_to alpha 1.0.1 +expect_success other_version_series_ignored + +make_fixture auto_bump_ignores_other_version_series +bump_feature_to alpha 1.1.5 +git -C "${fixture_dir}" commit -qm other_series_high_water_mark +bump_feature_to alpha 1.0.0 +git -C "${fixture_dir}" commit -qm reset_original_series +printf '%s\n' 'echo changed' >> "${fixture_dir}/features/src/alpha/install.sh" +git -C "${fixture_dir}" add features/src/alpha/install.sh +expect_auto_bump auto_bump_ignores_other_version_series +[[ "$(feature_version alpha)" == 1.0.1 ]] + +make_fixture generated_docs_only +printf '%s\n' 'Generated details' >> "${fixture_dir}/features/src/alpha/README.md" +git -C "${fixture_dir}" add features/src/alpha/README.md +expect_success generated_docs_only + +make_fixture shared_payload_without_bumps +printf '%s\n' 'echo changed' >> "${fixture_dir}/features/common/utilities.sh" +git -C "${fixture_dir}" add features/common/utilities.sh +expect_failure shared_payload_without_bumps +grep -q 'alpha:.*still 1.0.0' "${output_file}" +grep -q 'beta:.*still 1.0.0' "${output_file}" + +make_fixture shared_payload_with_auto_bumps +printf '%s\n' 'echo changed' >> "${fixture_dir}/features/common/utilities.sh" +git -C "${fixture_dir}" add features/common/utilities.sh +expect_auto_bump shared_payload_with_auto_bumps +[[ "$(feature_version alpha)" == 1.0.1 ]] +[[ "$(feature_version beta)" == 1.0.1 ]] + +make_fixture auto_bump_refuses_unstaged_manifest_changes +printf '%s\n' 'echo changed' >> "${fixture_dir}/features/src/alpha/install.sh" +git -C "${fixture_dir}" add features/src/alpha/install.sh +manifest="${fixture_dir}/features/src/alpha/devcontainer-feature.json" +jq '.description = "Unstaged change"' "${manifest}" > "${manifest}.tmp" +mv "${manifest}.tmp" "${manifest}" +expect_auto_bump auto_bump_refuses_unstaged_manifest_changes +[[ "$(feature_version alpha)" == 1.0.0 ]] +grep -q 'has unstaged manifest changes; refusing to overwrite them' "${output_file}" + +make_fixture shared_auto_bump_is_atomic +printf '%s\n' 'echo changed' >> "${fixture_dir}/features/common/utilities.sh" +git -C "${fixture_dir}" add features/common/utilities.sh +manifest="${fixture_dir}/features/src/beta/devcontainer-feature.json" +jq '.description = "Unstaged change"' "${manifest}" > "${manifest}.tmp" +mv "${manifest}.tmp" "${manifest}" +expect_auto_bump shared_auto_bump_is_atomic +[[ "$(feature_version alpha)" == 1.0.0 ]] +[[ "$(feature_version beta)" == 1.0.0 ]] + +make_fixture shared_payload_with_bumps +printf '%s\n' 'echo changed' >> "${fixture_dir}/features/common/utilities.sh" +git -C "${fixture_dir}" add features/common/utilities.sh +bump_feature alpha +bump_feature beta +expect_success shared_payload_with_bumps + +make_fixture deleted_payload_without_bump +git -C "${fixture_dir}" rm -q features/src/alpha/install.sh +expect_failure deleted_payload_without_bump + +make_fixture new_feature +mkdir -p "${fixture_dir}/features/src/gamma" +printf '%s\n' '#!/usr/bin/env bash' 'echo gamma' > "${fixture_dir}/features/src/gamma/install.sh" +printf '%s\n' \ + '{"id":"gamma","version":"1.0.0","description":"Gamma"}' \ + > "${fixture_dir}/features/src/gamma/devcontainer-feature.json" +git -C "${fixture_dir}" add features/src/gamma +expect_success new_feature + +make_fixture pull_request_range +base_sha="$(git -C "${fixture_dir}" rev-parse HEAD)" +printf '%s\n' 'echo changed' >> "${fixture_dir}/features/src/alpha/install.sh" +git -C "${fixture_dir}" add features/src/alpha/install.sh +git -C "${fixture_dir}" commit -qm payload +expect_failure pull_request_range_without_bump "${base_sha}" +bump_feature alpha +git -C "${fixture_dir}" commit -qm bump +expect_success pull_request_range_with_bump "${base_sha}" + +make_fixture pull_request_range_is_always_check_only +base_sha="$(git -C "${fixture_dir}" rev-parse HEAD)" +printf '%s\n' 'echo changed' >> "${fixture_dir}/features/src/alpha/install.sh" +git -C "${fixture_dir}" add features/src/alpha/install.sh +git -C "${fixture_dir}" commit -qm payload +expect_auto_bump pull_request_range_is_always_check_only "${base_sha}" +[[ "$(feature_version alpha)" == 1.0.0 ]] +if grep -q 'Automatically updated required feature versions' "${output_file}"; then + echo "FAIL: pull-request checking must not modify feature versions" >&2 + exit 1 +fi + +echo "All feature version bump checks passed." diff --git a/ci/check-feature-version-bumps.sh b/ci/check-feature-version-bumps.sh new file mode 100755 index 00000000..8a2ecf7d --- /dev/null +++ b/ci/check-feature-version-bumps.sh @@ -0,0 +1,319 @@ +#!/usr/bin/env bash + +set -euo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")/.." + +base_ref="${FEATURE_VERSION_BASE_SHA:-${1:-}}" +auto_bump=false +if [[ -z "${base_ref}" ]]; then + auto_bump_value="${FEATURE_VERSION_AUTO_BUMP:-}" + if [[ -z "${auto_bump_value}" ]]; then + auto_bump_value="$(git config --get devcontainers.auto-bump-feature-versions 2>/dev/null || true)" + fi + + case "${auto_bump_value}" in + "" | 0 | false | no | off) + ;; + 1 | true | yes | on) + auto_bump=true + ;; + *) + echo "FEATURE_VERSION_AUTO_BUMP must be a boolean value, got '${auto_bump_value}'." >&2 + exit 2 + ;; + esac +fi + +if [[ -n "${base_ref}" ]]; then + if ! git cat-file -e "${base_ref}^{commit}" 2>/dev/null; then + echo "Feature version check cannot find base commit '${base_ref}'." >&2 + echo "Fetch the base branch history before running this check." >&2 + exit 2 + fi + old_prefix="${base_ref}:" + new_prefix="HEAD:" + history_ref="${base_ref}" + diff_command=( + git diff --name-only -z --diff-filter=ACDMRTUXB + "${base_ref}" HEAD -- features/common features/src + ) +else + if git diff --cached --quiet -- features/common features/src; then + exit 0 + fi + old_prefix="HEAD:" + new_prefix=":" + history_ref="HEAD" + diff_command=( + git diff --cached --name-only -z --diff-filter=ACDMRTUXB -- + features/common features/src + ) +fi + +affected_features="$(mktemp)" +changed_version_features="$(mktemp)" +unchanged_version_failures="$(mktemp)" +historical_version_failures="$(mktemp)" +auto_bump_features="$(mktemp)" +auto_bump_plan="$(mktemp)" +auto_bump_errors="$(mktemp)" +trap 'rm -f \ + "${affected_features}" \ + "${changed_version_features}" \ + "${unchanged_version_failures}" \ + "${historical_version_failures}" \ + "${auto_bump_features}" \ + "${auto_bump_plan}" \ + "${auto_bump_errors}"' EXIT + +object_exists() { + git cat-file -e "${1}${2}" 2>/dev/null +} + +read_object() { + git show "${1}${2}" +} + +mark_all_features() { + local manifest feature + for manifest in features/src/*/devcontainer-feature.json; do + [[ -f "${manifest}" ]] || continue + feature="${manifest#features/src/}" + printf '%s\n' "${feature%%/*}" >> "${affected_features}" + done +} + +descriptor_payload_changed() { + local manifest="$1" + local old_payload new_payload + + # A new or removed feature has no prior version that can be bumped. + object_exists "${old_prefix}" "${manifest}" || return 1 + object_exists "${new_prefix}" "${manifest}" || return 1 + + old_payload="$(read_object "${old_prefix}" "${manifest}" | jq -cS 'del(.version)')" + new_payload="$(read_object "${new_prefix}" "${manifest}" | jq -cS 'del(.version)')" + [[ "${old_payload}" != "${new_payload}" ]] +} + +is_numeric_version() { + [[ "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] +} + +version_is_greater() { + local candidate="$1" + local previous="$2" + local candidate_major candidate_minor candidate_patch candidate_rest + local previous_major previous_minor previous_patch previous_rest + + is_numeric_version "${candidate}" || return 2 + is_numeric_version "${previous}" || return 2 + + candidate_major="${candidate%%.*}" + candidate_rest="${candidate#*.}" + candidate_minor="${candidate_rest%%.*}" + candidate_patch="${candidate_rest#*.}" + previous_major="${previous%%.*}" + previous_rest="${previous#*.}" + previous_minor="${previous_rest%%.*}" + previous_patch="${previous_rest#*.}" + + if ((10#${candidate_major} != 10#${previous_major})); then + ((10#${candidate_major} > 10#${previous_major})) + elif ((10#${candidate_minor} != 10#${previous_minor})); then + ((10#${candidate_minor} > 10#${previous_minor})) + else + ((10#${candidate_patch} > 10#${previous_patch})) + fi +} + +highest_historical_version() { + local manifest="$1" + local candidate="$2" + local candidate_series="${candidate%.*}" + local version highest="" + + # Read both sides of every historical version-field change. This retains + # high-water marks that a later release-preparation commit may have reset. + while IFS= read -r version; do + [[ "${version%.*}" == "${candidate_series}" ]] || continue + if [[ -z "${highest}" ]] || version_is_greater "${version}" "${highest}"; then + highest="${version}" + fi + done < <( + git log -p --format= "${history_ref}" -- "${manifest}" \ + | sed -nE 's/^[+-].*"version"[[:space:]]*:[[:space:]]*"([0-9]+\.[0-9]+\.[0-9]+)".*/\1/p' + ) + + printf '%s\n' "${highest}" +} + +next_patch_version() { + local version="$1" + local patch="${version##*.}" + + printf '%s.%d\n' "${version%.*}" "$((10#${patch} + 1))" +} + +rewrite_manifest_version() { + local manifest="$1" + local old_version="$2" + local new_version="$3" + + OLD_VERSION="${old_version}" NEW_VERSION="${new_version}" perl -pi -e \ + '$count += s{("version"\s*:\s*)"\Q$ENV{OLD_VERSION}\E"}{$1 . qq{"$ENV{NEW_VERSION}"}}e; END { die "version replacement failed\n" unless $count == 1; }' \ + "${manifest}" +} + +while IFS= read -r -d '' path; do + if [[ "${path}" == features/common/* ]]; then + mark_all_features + continue + fi + + [[ "${path}" == features/src/*/* ]] || continue + feature="${path#features/src/}" + feature="${feature%%/*}" + relative_path="${path#features/src/"${feature}"/}" + + # These files are generated or explanatory and are not feature payload. + if [[ "${relative_path}" == "README.md" || "${relative_path}" == "NOTES.md" ]]; then + continue + fi + + if [[ "${relative_path}" == "devcontainer-feature.json" ]]; then + if object_exists "${old_prefix}" "${path}" && object_exists "${new_prefix}" "${path}"; then + old_version="$(read_object "${old_prefix}" "${path}" | jq -er '.version | strings')" + new_version="$(read_object "${new_prefix}" "${path}" | jq -er '.version | strings')" + if [[ "${old_version}" != "${new_version}" ]]; then + printf '%s\n' "${feature}" >> "${changed_version_features}" + fi + fi + if descriptor_payload_changed "${path}"; then + printf '%s\n' "${feature}" >> "${affected_features}" + fi + else + printf '%s\n' "${feature}" >> "${affected_features}" + fi +done < <("${diff_command[@]}") + +while IFS= read -r feature; do + manifest="features/src/${feature}/devcontainer-feature.json" + + # Adding or removing a complete feature does not reuse an existing version. + object_exists "${old_prefix}" "${manifest}" || continue + object_exists "${new_prefix}" "${manifest}" || continue + + old_version="$(read_object "${old_prefix}" "${manifest}" | jq -er '.version | strings')" + new_version="$(read_object "${new_prefix}" "${manifest}" | jq -er '.version | strings')" + if [[ "${old_version}" == "${new_version}" ]]; then + printf '%s\t%s\n' "${feature}" "${new_version}" >> "${unchanged_version_failures}" + fi +done < <(sort -u "${affected_features}") + +while IFS= read -r feature; do + manifest="features/src/${feature}/devcontainer-feature.json" + new_version="$(read_object "${new_prefix}" "${manifest}" | jq -er '.version | strings')" + + if ! is_numeric_version "${new_version}"; then + printf '%s\t%s\t%s\n' "${feature}" "${new_version}" "numeric MAJOR.MINOR.PATCH" \ + >> "${historical_version_failures}" + else + highest_version="$(highest_historical_version "${manifest}" "${new_version}")" + if [[ -n "${highest_version}" ]] && ! version_is_greater "${new_version}" "${highest_version}"; then + printf '%s\t%s\t%s\n' "${feature}" "${new_version}" "${highest_version}" \ + >> "${historical_version_failures}" + fi + fi +done < <(sort -u "${changed_version_features}") + +if [[ "${auto_bump}" == true ]]; then + { + cut -f1 "${unchanged_version_failures}" + cut -f1 "${historical_version_failures}" + } | sort -u > "${auto_bump_features}" + + while IFS= read -r feature; do + [[ -n "${feature}" ]] || continue + manifest="features/src/${feature}/devcontainer-feature.json" + current_version="$(read_object "${new_prefix}" "${manifest}" | jq -er '.version | strings')" + + if ! is_numeric_version "${current_version}"; then + printf '%s\t%s\n' "${feature}" \ + "cannot auto-bump non-numeric version ${current_version}" >> "${auto_bump_errors}" + continue + fi + + if ! git diff --quiet -- "${manifest}"; then + printf '%s\t%s\n' "${feature}" \ + "has unstaged manifest changes; refusing to overwrite them" >> "${auto_bump_errors}" + continue + fi + + highest_version="$(highest_historical_version "${manifest}" "${current_version}")" + bump_from="${current_version}" + if [[ -n "${highest_version}" ]] && version_is_greater "${highest_version}" "${bump_from}"; then + bump_from="${highest_version}" + fi + next_version="$(next_patch_version "${bump_from}")" + printf '%s\t%s\t%s\t%s\n' \ + "${feature}" "${current_version}" "${next_version}" "${manifest}" >> "${auto_bump_plan}" + done < "${auto_bump_features}" + + if [[ ! -s "${auto_bump_errors}" && -s "${auto_bump_plan}" ]]; then + while IFS=$'\t' read -r feature current_version next_version manifest; do + rewrite_manifest_version "${manifest}" "${current_version}" "${next_version}" + done < "${auto_bump_plan}" + + echo "Automatically updated required feature versions:" >&2 + while IFS=$'\t' read -r feature current_version next_version manifest; do + echo " - ${feature}: ${current_version} -> ${next_version}" >&2 + done < "${auto_bump_plan}" + echo >&2 + echo "Review and stage the updated manifests, then retry the commit." >&2 + exit 1 + fi + + if [[ -s "${auto_bump_errors}" ]]; then + echo "Feature versions could not be updated automatically:" >&2 + while IFS=$'\t' read -r feature message; do + echo " - ${feature}: ${message}" >&2 + done < "${auto_bump_errors}" + echo >&2 + fi +fi + +status=0 +if [[ -s "${unchanged_version_failures}" ]]; then + echo "Feature contents changed without changing the published version:" >&2 + while IFS=$'\t' read -r feature version; do + echo " - ${feature}: features/src/${feature}/devcontainer-feature.json is still ${version}" >&2 + done < "${unchanged_version_failures}" + echo >&2 + echo "Change each listed manifest's 'version' value and stage it with the feature changes." >&2 + echo "Files under features/common are copied into every feature, so they require every feature version to change." >&2 + status=1 +fi + +if [[ -s "${historical_version_failures}" ]]; then + if ((status != 0)); then + echo >&2 + fi + echo "Feature versions must be newer than every version previously used in the target history:" >&2 + while IFS=$'\t' read -r feature version highest_version; do + echo " - ${feature}: ${version} is not newer than ${highest_version}" >&2 + done < "${historical_version_failures}" + echo >&2 + echo "Choose a version above the historical high-water mark; the current file may contain an earlier reset value." >&2 + status=1 +fi + +if ((status != 0)) && [[ -z "${base_ref}" && "${auto_bump}" == false ]]; then + echo >&2 + echo "For formatter-style automatic bumps in this checkout, opt in with:" >&2 + echo " git config devcontainers.auto-bump-feature-versions true" >&2 + echo "Or enable it for one command with FEATURE_VERSION_AUTO_BUMP=1." >&2 +fi + +exit "${status}" diff --git a/features/src/rapids-build-utils/devcontainer-feature.json b/features/src/rapids-build-utils/devcontainer-feature.json index a12703ae..cc24e9f0 100644 --- a/features/src/rapids-build-utils/devcontainer-feature.json +++ b/features/src/rapids-build-utils/devcontainer-feature.json @@ -1,7 +1,7 @@ { "name": "NVIDIA RAPIDS devcontainer build utilities", "id": "rapids-build-utils", - "version": "26.12.0", + "version": "26.12.1", "description": "A feature to install the RAPIDS devcontainer build utilities", "containerEnv": { "BASH_ENV": "/etc/bash.bash_env"