From 0c73419efc8891154e1b01c369cf40816951eee2 Mon Sep 17 00:00:00 2001 From: Derek Roberts Date: Tue, 20 Oct 2026 13:50:51 -0700 Subject: [PATCH 1/3] feat!: take cert-expiry hosts and .pr-close.yml packages one per line --- .github/tests/cert-expiry/check.bats | 14 +++++++++++--- .github/workflows/.pr-close.yml | 16 ++++++++++------ .github/workflows/cert-expiry-tests.yml | 1 + cert-expiry/README.md | 2 +- cert-expiry/action.yml | 2 +- cert-expiry/check.sh | 22 ++++++++++++++-------- 6 files changed, 38 insertions(+), 19 deletions(-) diff --git a/.github/tests/cert-expiry/check.bats b/.github/tests/cert-expiry/check.bats index f230fa8..80c010c 100644 --- a/.github/tests/cert-expiry/check.bats +++ b/.github/tests/cert-expiry/check.bats @@ -84,14 +84,14 @@ findings() { } @test "fail_on_findings false reports findings without failing" { - INPUT_HOSTS="localhost:${OK_PORT} localhost:${SOON_PORT}" INPUT_FAIL_ON_FINDINGS=false run bash "$SCRIPT" + INPUT_HOSTS=$'localhost:'"${OK_PORT}"$'\nlocalhost:'"${SOON_PORT}" INPUT_FAIL_ON_FINDINGS=false run bash "$SCRIPT" [ "$status" -eq 0 ] [ "$(findings | jq length)" -eq 1 ] grep -qx "checked=2" "$GITHUB_OUTPUT" } @test "never prints certificate or key material" { - INPUT_HOSTS="localhost:${OK_PORT} localhost:${SOON_PORT} localhost:${OLD_PORT}" run bash "$SCRIPT" + INPUT_HOSTS=$'localhost:'"${OK_PORT}"$'\nlocalhost:'"${SOON_PORT}"$'\nlocalhost:'"${OLD_PORT}" run bash "$SCRIPT" [[ "$output" != *"BEGIN"* ]] ! grep -q "BEGIN" "$GITHUB_OUTPUT" "$GITHUB_STEP_SUMMARY" } @@ -129,7 +129,15 @@ findings() { } @test "multi-line hosts: blank lines and comments skipped, duplicates removed" { - INPUT_HOSTS=$'localhost:'"${OK_PORT}"$'\n\n# vanity host unset\nLOCALHOST:'"${OK_PORT}"$', localhost:'"${SOON_PORT}" INPUT_DAYS=1 run bash "$SCRIPT" + INPUT_HOSTS=$'localhost:'"${OK_PORT}"$'\n\n# vanity host unset\nLOCALHOST:'"${OK_PORT}"$'\nlocalhost:'"${SOON_PORT}" INPUT_DAYS=1 run bash "$SCRIPT" [ "$status" -eq 0 ] grep -qx "checked=2" "$GITHUB_OUTPUT" } + +@test "comma or space separated hosts fail with the one-per-line format" { + for h in "localhost:${OK_PORT}, localhost:${SOON_PORT}" "localhost:${OK_PORT} localhost:${SOON_PORT}"; do + INPUT_HOSTS="$h" run bash "$SCRIPT" + [ "$status" -eq 1 ] + [[ "$output" == *"hosts takes one item per line, or a single value."* ]] + done +} diff --git a/.github/workflows/.pr-close.yml b/.github/workflows/.pr-close.yml index ebdb4f1..754096c 100644 --- a/.github/workflows/.pr-close.yml +++ b/.github/workflows/.pr-close.yml @@ -13,7 +13,7 @@ on: required: false type: string packages: - description: "Space-separated list of packages to promote" + description: "Packages to promote, one per line or a single package" required: false type: string remove_pvc: @@ -78,9 +78,14 @@ jobs: env: INPUT_PACKAGES: ${{ inputs.packages }} run: | - # Convert packages to JSON array for job matrix - read -r -a pkgs <<< "$INPUT_PACKAGES" - echo "packages=$(jq -cn '$ARGS.positional' --args "${pkgs[@]}")" >> "$GITHUB_OUTPUT" + # One package per line, or a single package (same rules as shared/inputs.sh; + # a reusable workflow can't read files from this repository) + packages=$(jq -cRn '[inputs | gsub("^\\s+|\\s+$"; "") | select(. != "")]' <<< "$INPUT_PACKAGES") + if jq -e 'any(.[]; test("[\\s,;\"'"'"'()\\[\\]]"))' <<< "$packages" > /dev/null; then + echo "::error title=packages: list format::Invalid packages input: ${INPUT_PACKAGES//$'\n'/ }%0Apackages takes one item per line, or a single value.%0AUse:%0A packages: |%0A backend%0A frontend%0AMultiline input syntax: https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idstepswith" + exit 1 + fi + echo "packages=${packages}" >> "$GITHUB_OUTPUT" - env: INPUT_PACKAGES: ${{ inputs.packages }} @@ -89,8 +94,7 @@ jobs: MERGE_BRANCH: ${{ inputs.merge_branch }} run: | # Diagnostic vars - read -r -a pkgs <<< "$INPUT_PACKAGES" - echo "packages=$(jq -cn '$ARGS.positional' --args "${pkgs[@]}")" + printf 'packages=%s\n' "${INPUT_PACKAGES//$'\n'/ }" echo "github.event.pull_request.merged=$PR_MERGED" echo "github.event.pull_request.base.ref=$PR_BASE_REF" echo "inputs.merge_branch=$MERGE_BRANCH" diff --git a/.github/workflows/cert-expiry-tests.yml b/.github/workflows/cert-expiry-tests.yml index cd0a2ab..740e465 100644 --- a/.github/workflows/cert-expiry-tests.yml +++ b/.github/workflows/cert-expiry-tests.yml @@ -51,6 +51,7 @@ jobs: ref: ${{ steps.base.outputs.sha || github.event.before }} triggers: | cert-expiry/ + shared/inputs.sh .github/tests/cert-expiry/ .github/workflows/cert-expiry-tests.yml !*.md diff --git a/cert-expiry/README.md b/cert-expiry/README.md index c82e555..da36922 100644 --- a/cert-expiry/README.md +++ b/cert-expiry/README.md @@ -65,7 +65,7 @@ To notify only on certificate findings (not on an input error), give the check s | Input | Default | Description | | --- | --- | --- | -| `hosts` | required | Hostnames, one per line (commas or spaces also work). `host` or `host:port`, no scheme; port defaults to 443. Blank lines and `#` comments are skipped. | +| `hosts` | required | Hostnames, one per line, or a single host. `host` or `host:port`, no scheme; port defaults to 443. Blank lines and `#` comments are skipped. | | `days` | `30` | Warn when a certificate expires within this many days. Whole number, 1 to 3650. | | `timeout` | `10` | Seconds to wait for each handshake. Whole number, 1 to 120. | | `fail_on_findings` | `true` | Fail the step when any host needs attention, so the next step can notify with `if: failure()`. Outputs are written either way. | diff --git a/cert-expiry/action.yml b/cert-expiry/action.yml index aa2e56b..2c83051 100644 --- a/cert-expiry/action.yml +++ b/cert-expiry/action.yml @@ -3,7 +3,7 @@ name: "Cert Expiry" description: "Read-only TLS expiry check: reads each host's public certificate with openssl s_client and reports any that expire within a number of days." inputs: hosts: - description: "Hostnames to check, one per line (commas or spaces also work). host or host:port, no scheme. Port defaults to 443. Blank lines and # comments are ignored." + description: "Hostnames to check, one per line, or a single host. host or host:port, no scheme. Port defaults to 443. Blank lines and # comments are ignored." required: true days: description: "Warn when a certificate expires within this many days (whole number, 1 to 3650)." diff --git a/cert-expiry/check.sh b/cert-expiry/check.sh index 4be45be..22ca2f3 100755 --- a/cert-expiry/check.sh +++ b/cert-expiry/check.sh @@ -38,17 +38,23 @@ case "$FAIL_ON_FINDINGS" in *) fail "fail_on_findings must be true or false, got '${FAIL_ON_FINDINGS}'." "Set fail_on_findings: true or fail_on_findings: false." ;; esac -LIST="$HOSTS" +# Shared list rules (shared/inputs.sh): one host per line, or a single host. +# '# comments' are dropped first. +# shellcheck source=../shared/inputs.sh +source "$(dirname "${BASH_SOURCE[0]}")/../shared/inputs.sh" +COMMENTLESS="" +while IFS= read -r line; do + COMMENTLESS+="${line%%#*}"$'\n' +done <<< "$HOSTS" +LIST="$(input_list hosts "$COMMENTLESS")" || { echo "$LIST"; exit 1; } -# Parse: drop comments, split on commas and whitespace, validate, de-duplicate +# Validate and de-duplicate LABEL='[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?' ENTRIES=() declare -A SEEN=() -while IFS= read -r line; do - line="${line%%#*}" - # read -a splits without pathname expansion, so '*' is rejected, not globbed - IFS=$' \t,' read -r -a parts <<< "$line" - for entry in "${parts[@]}"; do +while IFS= read -r entry; do + [ -n "$entry" ] || continue + { if [[ "$entry" == *"://"* || "$entry" == */* ]]; then fail "'${entry}' is a URL, not a hostname." "List the hostname only, for example myapp.gov.bc.ca or myapp.gov.bc.ca:8443." fi @@ -63,7 +69,7 @@ while IFS= read -r line; do [[ -n "${SEEN[$key]:-}" ]] && continue SEEN[$key]=1 ENTRIES+=("$key") - done + } done <<< "$LIST" ((${#ENTRIES[@]} > 0)) || fail "No hostnames to check." "Set hosts to one or more hostnames, one per line." From 8f832cd149a85e588c08c354894084c0c48edd94 Mon Sep 17 00:00:00 2001 From: Derek Roberts Date: Tue, 20 Oct 2026 14:22:04 -0700 Subject: [PATCH 2/3] fix: keep quotes in list errors and take .pr-close.yml remove_pvc and cleanup_name one per line --- .github/tests/shared/inputs.bats | 7 +++++++ .github/workflows/.pr-close.yml | 4 ++-- cleanup-pr/README.md | 4 ++-- cleanup-pr/action.yml | 4 ++-- shared/inputs.sh | 12 +++++++++--- sysdig-monitor/README.md | 2 +- sysdig-monitor/action.yml | 2 +- 7 files changed, 24 insertions(+), 11 deletions(-) diff --git a/.github/tests/shared/inputs.bats b/.github/tests/shared/inputs.bats index 4f15a26..1bf6208 100644 --- a/.github/tests/shared/inputs.bats +++ b/.github/tests/shared/inputs.bats @@ -31,6 +31,13 @@ setup() { done } +@test "list: error keeps quotes on items with spaces" { + run input_list triggers '["my path/", "other path/"]' + [ "$status" -eq 1 ] + [[ "$output" == *'%0A triggers: |%0A "my path/"%0A "other path/"%0A'* ]] + [ "$(input_list triggers $'"my path/"\n"other path/"')" = $'my path/\nother path/' ] +} + @test "bool, enum and debug" { [ "$(input_bool dry_run TRUE false)" = true ] [ "$(input_bool dry_run '' true)" = true ] diff --git a/.github/workflows/.pr-close.yml b/.github/workflows/.pr-close.yml index 754096c..16c06f1 100644 --- a/.github/workflows/.pr-close.yml +++ b/.github/workflows/.pr-close.yml @@ -20,7 +20,7 @@ on: required: false default: data-${{ github.event.repository.name }}-${{ github.event.number }}-bitnami-pg-0 type: string - description: "Comma separated list of PVCs to remove" + description: "PVCs to remove, one per line or a single PVC" ### Usually a bad idea / not recommended merge_branch: @@ -29,7 +29,7 @@ on: type: string default: ${{ github.event.repository.default_branch }} cleanup_name: - description: "Use if template/cleanup name differs from repo; space-separated for multiple names" + description: "Use if template/cleanup name differs from repo; one name per line, or a single name" type: string default: "" organization: diff --git a/cleanup-pr/README.md b/cleanup-pr/README.md index 752733a..fb72009 100644 --- a/cleanup-pr/README.md +++ b/cleanup-pr/README.md @@ -7,10 +7,10 @@ This composite action cleans up resources in a target OpenShift namespace on pul | Input | Description | Required | Default | |---|---|---|---| | `cleanup` | Type of cleanup to run (`helm` or `label`) | No | `label` | -| `cleanup_name` | App labels to delete: one per line, comma separated, or a JSON array (if `cleanup` is `label`). Defaults to `repository-target` | No | `""` | +| `cleanup_name` | App labels to delete: one per line, or a single label (if `cleanup` is `label`). Defaults to `repository-target` | No | `""` | | `repository` | The GitHub repository name | No | `${{ github.event.repository.name }}` | | `target` | Environment target identifier (e.g. PR number) | No | `${{ github.event.number }}` | -| `remove_pvc` | PVCs to delete: one per line, comma separated, or a JSON array | No | `""` | +| `remove_pvc` | PVCs to delete: one per line, or a single PVC | No | `""` | | `oc_server` | OpenShift API server URL with port | Yes | | | `oc_namespace` | Target OpenShift namespace | **Yes** | N/A | | `oc_token` | OpenShift Service Account token | **Yes** | N/A | diff --git a/cleanup-pr/action.yml b/cleanup-pr/action.yml index 920cd56..ce3bcea 100644 --- a/cleanup-pr/action.yml +++ b/cleanup-pr/action.yml @@ -7,7 +7,7 @@ inputs: required: false default: "label" cleanup_name: - description: "app label values to clean up: one per line, comma separated, or a JSON array (defaults to repository-target)" + description: "app label values to clean up: one per line, or a single value (defaults to repository-target)" required: false default: "" repository: @@ -19,7 +19,7 @@ inputs: required: false default: ${{ github.event.number }} remove_pvc: - description: "PVCs to remove: one per line, comma separated, or a JSON array" + description: "PVCs to remove: one per line, or a single PVC" required: false default: "" oc_server: diff --git a/shared/inputs.sh b/shared/inputs.sh index 6c10718..f633fa5 100644 --- a/shared/inputs.sh +++ b/shared/inputs.sh @@ -27,15 +27,21 @@ _input_list_error() { local name="$1" raw="$2" inner="$2" item local -a items=() [[ "$raw" == \(*\) || "$raw" == \[*\] ]] && inner="${raw:1:${#raw}-2}" - for item in ${inner//[,;]/ }; do - item="$(_input_strip_quotes "$item")" + # Tokens: '...' or "..." (spaces kept), else runs without spaces, commas or semicolons + local re="^[[:space:],;]*('[^']*'|\"[^\"]*\"|[^[:space:],;]+)(.*)$" + while [[ "$inner" =~ $re ]]; do + item="$(_input_strip_quotes "${BASH_REMATCH[1]}")" + inner="${BASH_REMATCH[2]}" [ -n "$item" ] && items+=("$item") done [ "${#items[@]}" -gt 0 ] || items=(backend/ frontend/) local msg="::error title=${name}: list format::Invalid ${name} input: ${raw}" msg+="%0A${name} takes one item per line, or a single value." msg+="%0AGot:%0A ${name}: ${raw}%0AUse:%0A ${name}: |" - for item in "${items[@]}"; do msg+="%0A ${item}"; done + # Items with spaces keep their quotes, so the suggested block parses + for item in "${items[@]}"; do + if [[ "$item" =~ [[:space:]] ]]; then msg+="%0A \"${item}\""; else msg+="%0A ${item}"; fi + done # GitHub's docs on step inputs (with:), including multiline values msg+="%0AMultiline input syntax: https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idstepswith" echo "$msg" diff --git a/sysdig-monitor/README.md b/sysdig-monitor/README.md index a931362..7d4765c 100644 --- a/sysdig-monitor/README.md +++ b/sysdig-monitor/README.md @@ -95,7 +95,7 @@ Adding/removing alert recipients is done by editing `spec.team.users[]` in the S # Default: monitoring/alerts alerts_dir: monitoring/alerts - # Deployment names: one per line, comma separated, or a JSON array. One alert per file per component. + # Deployment names: one per line, or a single name. One alert per file per component. # Default: frontend,backend components: frontend,backend diff --git a/sysdig-monitor/action.yml b/sysdig-monitor/action.yml index c1cae25..dfa8187 100644 --- a/sysdig-monitor/action.yml +++ b/sysdig-monitor/action.yml @@ -39,7 +39,7 @@ inputs: default: 'monitoring/alerts' components: description: | - Deployment names to monitor: one per line, comma separated, or a JSON array. + Deployment names to monitor: one per line, or a single name. Each component gets one alert per file in alerts_dir. Example: frontend,backend default: 'frontend,backend' From aa5672ab60471df8443b653450c4bd4fe380acf4 Mon Sep 17 00:00:00 2001 From: Derek Roberts Date: Tue, 20 Oct 2026 14:30:02 -0700 Subject: [PATCH 3/3] fix: strip outer quotes from .pr-close.yml packages --- .github/workflows/.pr-close.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/.pr-close.yml b/.github/workflows/.pr-close.yml index 16c06f1..d0af21f 100644 --- a/.github/workflows/.pr-close.yml +++ b/.github/workflows/.pr-close.yml @@ -78,9 +78,9 @@ jobs: env: INPUT_PACKAGES: ${{ inputs.packages }} run: | - # One package per line, or a single package (same rules as shared/inputs.sh; + # One package per line, or a single package; outer quotes stripped (same rules as shared/inputs.sh; # a reusable workflow can't read files from this repository) - packages=$(jq -cRn '[inputs | gsub("^\\s+|\\s+$"; "") | select(. != "")]' <<< "$INPUT_PACKAGES") + packages=$(jq -cRn '[inputs | gsub("^\\s+|\\s+$"; "") | sub("^(?[\"'"'"'])(?.*)\\k$"; "\(.v)") | select(. != "")]' <<< "$INPUT_PACKAGES") if jq -e 'any(.[]; test("[\\s,;\"'"'"'()\\[\\]]"))' <<< "$packages" > /dev/null; then echo "::error title=packages: list format::Invalid packages input: ${INPUT_PACKAGES//$'\n'/ }%0Apackages takes one item per line, or a single value.%0AUse:%0A packages: |%0A backend%0A frontend%0AMultiline input syntax: https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idstepswith" exit 1