Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ jobs:
fi
read -ra packages <<<"$APT_PACKAGES"
sudo apt-get update
sudo apt-get install -y "${packages[@]}"
sudo apt-get install -y ${packages[@]+"${packages[@]}"}
- name: 🐍 Install uv and Python
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
with:
Expand All @@ -91,8 +91,8 @@ jobs:
while IFS= read -r line; do
if [[ -n "$line" ]]; then options+=(--exclude-newer-package "$line"); fi
done <<<"$EXCLUDE_NEWER_PACKAGE"
echo "Installing ${spec[*]} with uploads older than $EXCLUDE_NEWER"
uv tool install --python-preference only-managed --python 3.15 "${options[@]}" "${spec[@]}"
echo "Installing ${spec[*]+"${spec[*]}"} with uploads older than $EXCLUDE_NEWER"
uv tool install --python-preference only-managed --python 3.15 "${options[@]}" ${spec[@]+"${spec[@]}"}
- name: 🐍 Install Python
id: python
background: true
Expand Down
3 changes: 3 additions & 0 deletions changelog.d/+bash32-empty-arrays.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Actions and the `check` workflow no longer fail with `unbound variable` on macOS runners when an input leaves an array
empty, such as the `targets` input of `install-locked-tools`. macOS runs `shell: bash` steps with Bash 3.2, which
rejects `"${array[@]}"` on an empty array under `set -u`.
2 changes: 1 addition & 1 deletion cooled-prek/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ runs:
run: |
set -euo pipefail
read -ra arguments <<<"$ARGS"
"$GITHUB_ACTION_PATH/../tools/cooled_prek.sh" "${arguments[@]}"
"$GITHUB_ACTION_PATH/../tools/cooled_prek.sh" ${arguments[@]+"${arguments[@]}"}
6 changes: 3 additions & 3 deletions install-locked-tools/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ runs:
groups=()
while IFS= read -r line; do
read -ra selected <<<"$line"
groups+=("${selected[@]}")
groups+=(${selected[@]+"${selected[@]}"})
done <<<"$DEPENDENCY_GROUPS"
if ((${#groups[@]})); then
echo "Selected lock groups: ${groups[*]} (from the groups input), installing into $VENV from pylock.toml"
Expand Down Expand Up @@ -82,15 +82,15 @@ runs:
fi
digest=$(
sha256sum "$BUILD_LOCK"
printf '%s\n' "$TARGET_PLATFORM" "$TARGETS" "${args[@]}"
printf '%s\n' "$TARGET_PLATFORM" "$TARGETS" ${args[@]+"${args[@]}"}
)
digest=$(sha256sum <<<"$digest")
output="$RUNNER_TEMP/build-constraints-${digest%% *}.txt"
if [[ -f "$output" ]]; then
echo "Reusing build constraints from an earlier step (same lock and targets): $output"
else
uv run --no-project --python "$RUNNER_TEMP/lock-reader" python \
"$GITHUB_ACTION_PATH/../tools/build_constraints.py" "${args[@]}" "$output" "$BUILD_LOCK"
"$GITHUB_ACTION_PATH/../tools/build_constraints.py" ${args[@]+"${args[@]}"} "$output" "$BUILD_LOCK"
fi
echo "path=$output" >>"$GITHUB_OUTPUT"
- name: 📦 Install selected groups
Expand Down
5 changes: 3 additions & 2 deletions publish-post-release-pr/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ runs:
while IFS= read -r path || [[ -n "$path" ]]; do
trusted_paths+=("$path")
done < <(printf '%s' "$ALLOWED_PATHS")
printf '%s\n' "${trusted_paths[@]}" | LC_ALL=C sort -u >"$RUNNER_TEMP/allowed-paths"
printf '%s\n' ${trusted_paths[@]+"${trusted_paths[@]}"} | LC_ALL=C sort -u >"$RUNNER_TEMP/allowed-paths"
paths=()
while IFS=$'\t' read -r path blob; do
case "$path" in
Expand All @@ -51,7 +51,8 @@ runs:
;;
esac
if ! grep -Fqx -- "$path" "$RUNNER_TEMP/allowed-paths"; then
echo "::error::prepared path $path is not in the writer's allowed-paths: ${trusted_paths[*]}" >&2
echo "::error::prepared path $path is not in the writer's allowed-paths:" \
"${trusted_paths[*]+"${trusted_paths[*]}"}" >&2
exit 1
fi
case "$blob" in
Expand Down
22 changes: 12 additions & 10 deletions refresh-release-lock/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ runs:
;;
esac
cp pylock.toml "$RUNNER_TEMP/lock-before.toml"
uv run --no-project --python "$RUNNER_TEMP/lock-tools" nab lock --all-groups --all-extras --no-cache "${refresh[@]}"
uv run --no-project --python "$RUNNER_TEMP/lock-tools" nab lock --all-groups --all-extras --no-cache \
${refresh[@]+"${refresh[@]}"}
if [[ "$MODE" == check ]]; then
python3 "$GITHUB_ACTION_PATH/../tools/check_nab_lock.py" "$RUNNER_TEMP/lock-before.toml" pylock.toml
fi
Expand All @@ -110,15 +111,15 @@ runs:
selected_groups=()
while IFS= read -r line; do
read -ra words <<<"$line"
platforms+=("${words[@]}")
platforms+=(${words[@]+"${words[@]}"})
done <<<"$MATRIX_PLATFORMS"
while IFS= read -r line; do
read -ra words <<<"$line"
implementations+=("${words[@]}")
implementations+=(${words[@]+"${words[@]}"})
done <<<"$MATRIX_IMPLEMENTATIONS"
while IFS= read -r line; do
read -ra words <<<"$line"
selected_groups+=("${words[@]}")
selected_groups+=(${words[@]+"${words[@]}"})
done <<<"$ADDITIONAL_GROUPS"
case "$ADDITIONAL_SELECTION" in
project)
Expand All @@ -134,14 +135,15 @@ runs:
fi
;;
esac
echo "Secondary lock $ADDITIONAL_LOCK: $description for Python $MATRIX_PYTHON on ${platforms[*]}" \
"with ${implementations[*]}, ${MODE} mode"
echo "Secondary lock $ADDITIONAL_LOCK: $description for Python $MATRIX_PYTHON" \
"on ${platforms[*]+"${platforms[*]}"} with ${implementations[*]+"${implementations[*]}"}, ${MODE} mode"
cp "$ADDITIONAL_LOCK" "$RUNNER_TEMP/additional-lock-before.toml"
uv run --no-project --python "$RUNNER_TEMP/lock-tools" nab lock --no-cache pyproject.toml "${selection[@]}" \
uv run --no-project --python "$RUNNER_TEMP/lock-tools" nab lock --no-cache pyproject.toml \
${selection[@]+"${selection[@]}"} \
--project-matrix-python "$MATRIX_PYTHON" \
--project-matrix-platforms "${platforms[@]}" \
--project-matrix-implementations "${implementations[@]}" \
--output "$ADDITIONAL_LOCK" "${refresh[@]}"
--project-matrix-platforms ${platforms[@]+"${platforms[@]}"} \
--project-matrix-implementations ${implementations[@]+"${implementations[@]}"} \
--output "$ADDITIONAL_LOCK" ${refresh[@]+"${refresh[@]}"}
if [[ "$MODE" == check ]]; then
python3 "$GITHUB_ACTION_PATH/../tools/check_nab_lock.py" \
"$RUNNER_TEMP/additional-lock-before.toml" "$ADDITIONAL_LOCK"
Expand Down
2 changes: 1 addition & 1 deletion select-previous-tag/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ runs:
set -euo pipefail
args=()
if [[ "$TAG" == */* ]]; then args+=(--package "${TAG%%/*}"); fi
previous=$(git tag --list | release-creator tags "$TAG" --previous "${args[@]}")
previous=$(git tag --list | release-creator tags "$TAG" --previous ${args[@]+"${args[@]}"})
if [[ -n "$previous" ]]; then
echo "Previous release tag: $previous (newest release tag before $TAG${args[*]:+ for package ${TAG%%/*}})"
else
Expand Down
50 changes: 50 additions & 0 deletions tests/bash32_arrays/test_empty_arrays.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import re
from collections.abc import Iterator
from pathlib import Path

from conftest import WORKFLOWS, at, load_yaml, mapping, step_list, text


def test_unguarded_array_expansions_are_provably_non_empty() -> None:
# Bash 3.2, the /bin/bash of macOS runners, aborts on "${arr[@]}" of an empty array under set -u. Bash 5 does
# not, so CI only sees this through the scan. Each allowed name is an array that cannot be empty where it expands.
expected = {
(".github/workflows/check.yaml", "options"): "starts with --exclude-newer",
("check-nab-exceptions/action.yml", "files"): "starts with pyproject.toml",
("install-locked-tools/action.yml", "groups"): "filled only when the selection found a group",
("prepare-towncrier/action.yml", "args"): "set to three options before the call",
("publish-github-release/action.yml", "request"): "starts with two -f pairs",
("publish-post-release-pr/action.yml", "paths"): "checked non-empty before any other use",
("refresh-release-lock/action.yml", "paths"): "starts with pylock.toml",
("refresh-release-lock/action.yml", "requirements"): "set to one requirement in every mode",
("refresh-release-lock/action.yml", "selected_groups"): "used only inside the non-empty check",
("render-release-notes/action.yml", "args"): "starts with three options",
("stage-jetbrains-marketplace/action.yml", "args"): "starts with the marketplace subcommand",
("tools/install_live_tools.sh", "options"): "starts with --exclude-newer",
("tools/install_live_tools.sh", "overrides"): "expanded only inside the :+ alternative",
}
found = {(path, name) for path, script in _run_scripts() for name in _unguarded_names(script)}

assert found == set(expected)


def _run_scripts() -> Iterator[tuple[str, str]]:
root = Path(__file__).resolve().parents[2]
for action in sorted(root.glob("*/action.yml")):
for step in step_list(at(load_yaml(action), "runs", "steps")):
if "run" in step:
yield action.relative_to(root).as_posix(), text(step["run"])
for script in sorted((root / "tools").glob("*.sh")):
yield script.relative_to(root).as_posix(), script.read_text()
for workflow in sorted(WORKFLOWS.glob("*.y*ml")):
for job_value in mapping(at(load_yaml(workflow), "jobs")).values():
job = mapping(job_value)
if "${{" in text(job.get("runs-on", "")) and "steps" in job:
for step in step_list(job["steps"]):
if "run" in step:
yield workflow.relative_to(root).as_posix(), text(step["run"])


def _unguarded_names(script: str) -> set[str]:
without_guards = re.sub(r'\$\{(\w+)\[[@*]\]\+"\$\{\1\[[@*]\]\}"\}', "", script)
return {match.group(1) for match in re.finditer(r"\$\{(\w+)\[[@*]\]\}", without_guards)}
7 changes: 7 additions & 0 deletions tests/check_workflow/test_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,13 @@ def test_gate_requires_test_through_required_checks() -> None:
+ ["--exclude-newer", "7 days", "tox", "--with", "tox-uv"],
id="default",
),
pytest.param(
"",
"",
["tool", "install", "--python-preference", "only-managed", "--python", "3.15"]
+ ["--exclude-newer", "7 days"],
id="empty-tox-install",
),
pytest.param(
"tox --with .",
"",
Expand Down
2 changes: 1 addition & 1 deletion validate-release-patch/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ runs:
if [[ -n "$ALLOWED_PATHS" ]]; then
echo "Release patch path: $status $path"
matched=false
for pattern in "${patterns[@]}"; do
for pattern in ${patterns[@]+"${patterns[@]}"}; do
# shellcheck disable=SC2053 # The patterns are globs, matched on purpose.
if [[ "$path" == $pattern ]]; then
matched=true
Expand Down