Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
8bf5c68
feat: implement issue #1193 — SonarCloud: shell script hygiene
don-petry Sep 29, 2026
d51dc3f
fix(reviews): address review comments [skip ci-relay]
don-petry Sep 29, 2026
e8ccfbd
fix(bot): address bot feedback [skip ci-relay]
don-petry Sep 30, 2026
d10a5c8
fix(reviews): address review comments [skip ci-relay]
don-petry Sep 30, 2026
d9d053e
fix(bot): address bot feedback [skip ci-relay]
don-petry Oct 2, 2026
228caa2
fix: address octal parsing and owner validation issues in shell scripts
don-petry Oct 2, 2026
86f1d3b
fix: avoid arithmetic wraparound in sweep.sh positive-cap check
don-petry Oct 2, 2026
8d27a3b
fix(reviews): address review comments [skip ci-relay]
don-petry Oct 2, 2026
981d80a
fix: normalize version comparison in ring_highest_channel_major
don-petry Oct 3, 2026
5e59949
fix: normalize comparison in amcl_clean_cycles_met
don-petry Oct 3, 2026
5499433
fix: normalize required parameter comparison in agents-md-cycle-log
don-petry Oct 3, 2026
d838e17
fix: prevent --override from advancing AWAITING_CONFIRMATION state
don-petry Oct 3, 2026
56a8d1c
fix: address CodeRabbit review findings for range validation and erro…
don-petry Oct 3, 2026
f6b4cab
fix: add range check to amcl_clean_cycles_met validation
don-petry Oct 3, 2026
9ed233a
fix(reviews): address review comments [skip ci-relay]
don-petry Oct 3, 2026
03c247b
fix(reviews): address review comments [skip ci-relay]
don-petry Oct 3, 2026
e4e9f8e
fix: normalize numeric values to base 10 before [[ comparisons
don-petry Oct 3, 2026
42cf55b
fix(canary-rollout): normalize CANARY_MAX_COMMIT_PAGES to base 10
don-petry Oct 3, 2026
76c7f50
fix(reviews): address review comments [skip ci-relay]
don-petry Oct 3, 2026
63c10d4
fix(compliance-audit): define constants for frequently-used literal s…
don-petry Oct 3, 2026
7fcf281
fix(compliance-audit): use jq --arg for safe variable passing
don-petry Oct 3, 2026
bcb8e04
fix(agents-md-cycle-log): normalize to base 10 before numeric compari…
don-petry Oct 3, 2026
097dc0b
fix(agents-md-cycle-log): add 18-digit range check before 10# conversion
don-petry Oct 11, 2026
9ab1ca6
fix(sonarcloud): address S1066 and S1192 code quality issues in compl…
don-petry Oct 14, 2026
56c3e71
fix(reviews): use [[ ]] in ruleset-list tests; reject nonpositive rou…
don-petry Oct 14, 2026
b8cc897
fix(reviews): use [[ ]] in list_workflow_files tests
don-petry Oct 17, 2026
bf774fc
fix(bot): strip leading plus sign in clamp and round_div functions
don-petry Oct 17, 2026
d8f9383
fix(bootstrap): revert unrelated argument-validation behavior change
don-petry Oct 18, 2026
7c9aaff
fix(sonarcloud): simplify TARGET_REPO parsing, remove owner validation
don-petry Oct 18, 2026
74f20dd
fix(pinned-version-report): remove retry-on-failure logic for tag loads
don-petry Oct 18, 2026
f43d4c2
Merge branch 'main' into dev-lead/issue-1193-20260929-2126
don-petry Oct 21, 2026
87aadf3
Merge branch 'main' into dev-lead/issue-1193-20260929-2126
don-petry Oct 22, 2026
d991f81
Merge branch 'main' into dev-lead/issue-1193-20260929-2126
don-petry Oct 25, 2026
b7f997c
Merge branch 'main' into dev-lead/issue-1193-20260929-2126
don-petry Oct 25, 2026
b1483c2
Merge branch 'main' into dev-lead/issue-1193-20260929-2126
don-petry Oct 25, 2026
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
10 changes: 5 additions & 5 deletions .github/scripts/add-to-project/add-issue-or-pr.sh
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ _atp_lib_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
. "${_atp_lib_dir}/lib.sh"

evaluate_noise_gate() {
if [ "$#" -ne 1 ]; then
if [[ "$#" -ne 1 ]]; then
printf '[evaluate_noise_gate] expected 1 arg (labels_json), got %d\n' "$#" >&2
return 64
fi
Expand Down Expand Up @@ -111,15 +111,15 @@ evaluate_noise_gate() {
# Thin wrapper over the shared lookup: find the project item linked to a
# given issue/PR node id (or empty string if it isn't on the board).
find_content_item_id() {
if [ "$#" -ne 1 ]; then
if [[ "$#" -ne 1 ]]; then
printf '[find_content_item_id] expected 1 arg (content_node_id), got %d\n' "$#" >&2
return 64
fi
find_project_item content-id "$1"
}

reconcile_content_with_project() {
if [ "$#" -ne 3 ]; then
if [[ "$#" -ne 3 ]]; then
printf '[reconcile_content_with_project] expected 3 args (content_node_id content_url labels_json), got %d\n' "$#" >&2
return 64
fi
Expand Down Expand Up @@ -155,7 +155,7 @@ reconcile_content_with_project() {
fi
local existing
existing=$(find_content_item_id "${content_node_id}")
if [ -n "${existing}" ]; then
if [[ -n "${existing}" ]]; then
printf 'Removing %s from %s (no longer qualifies: %s); item %s\n' \
"${content_url}" "${PROJECT_URL:-the project}" "${reason}" "${existing}"
delete_project_item "${existing}"
Expand All @@ -170,7 +170,7 @@ reconcile_content_with_project() {
esac
}

if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
if [[ "${BASH_SOURCE[0]}" = "${0}" ]]; then
reconcile_content_with_project \
"${CONTENT_NODE_ID:?CONTENT_NODE_ID is required}" \
"${CONTENT_URL:?CONTENT_URL is required}" \
Expand Down
48 changes: 24 additions & 24 deletions .github/scripts/add-to-project/classify-initiative.sh
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ CI_THEME_FIELD_ID=""
# would make the `dev-lead agent` rule match every single item. Only labels
# that actually signal an initiative survive into the signature.
normalize_signature() {
if [ "$#" -ne 3 ]; then
if [[ "$#" -ne 3 ]]; then
printf '[normalize_signature] expected 3 args (title labels_json repo), got %d\n' "$#" >&2
return 64
fi
Expand Down Expand Up @@ -97,21 +97,21 @@ normalize_signature() {

# classify_by_rules <signature> → first-matching Initiative name, or "".
classify_by_rules() {
if [ "$#" -ne 1 ]; then
if [[ "$#" -ne 1 ]]; then
printf '[classify_by_rules] expected 1 arg (signature), got %d\n' "$#" >&2
return 64
fi
local sig="$1" name rx
local rules="${RULES_FILE:-${_ci_dir}/initiative-rules.tsv}"
if [ ! -f "${rules}" ]; then
if [[ ! -f "${rules}" ]]; then
printf '[classify_by_rules] rules file not found: %s\n' "${rules}" >&2
return 65
fi
while IFS=$'\t' read -r name rx || [ -n "${name}" ]; do
while IFS=$'\t' read -r name rx || [[ -n "${name}" ]]; do
name="${name%$'\r'}"
rx="${rx%$'\r'}"
case "${name}" in ''|'#'*) continue ;; esac
[ -n "${rx}" ] || continue
[[ -n "${rx}" ]] || continue
if printf '%s' "${sig}" | grep -Eiq -- "${rx}"; then
printf '%s' "${name}"
return 0
Expand All @@ -122,21 +122,21 @@ classify_by_rules() {

# theme_for <initiative> → the Theme it rolls up to, or "".
theme_for() {
if [ "$#" -ne 1 ]; then
if [[ "$#" -ne 1 ]]; then
printf '[theme_for] expected 1 arg (initiative), got %d\n' "$#" >&2
return 64
fi
local want="$1" init theme
local tax="${TAXONOMY_FILE:-${_ci_dir}/initiative-taxonomy.tsv}"
if [ ! -f "${tax}" ]; then
if [[ ! -f "${tax}" ]]; then
printf '[theme_for] taxonomy file not found: %s\n' "${tax}" >&2
return 65
fi
while IFS=$'\t' read -r init theme || [ -n "${init}" ]; do
while IFS=$'\t' read -r init theme || [[ -n "${init}" ]]; do
init="${init%$'\r'}"
theme="${theme%$'\r'}"
case "${init}" in ''|'#'*) continue ;; esac
if [ "${init}" = "${want}" ]; then
if [[ "${init}" = "${want}" ]]; then
printf '%s' "${theme}"
return 0
fi
Expand All @@ -147,13 +147,13 @@ theme_for() {
# decide_for_signature <signature> → "<initiative>\t<theme>" (theme may be
# empty), or "" when no rule matches.
decide_for_signature() {
if [ "$#" -ne 1 ]; then
if [[ "$#" -ne 1 ]]; then
printf '[decide_for_signature] expected 1 arg (signature), got %d\n' "$#" >&2
return 64
fi
local sig="$1" init theme
init=$(classify_by_rules "${sig}") || return $?
[ -n "${init}" ] || return 0
[[ -n "${init}" ]] || return 0
theme=$(theme_for "${init}") || return $?
printf '%s\t%s' "${init}" "${theme}"
}
Expand All @@ -179,38 +179,38 @@ resolve_fields() {
}
}')

if [ "$(printf '%s' "${json}" | jq -r '.data.node?')" = "null" ]; then
if [[ "$(printf '%s' "${json}" | jq -r '.data.node?')" = "null" ]]; then
printf '[resolve_fields] GraphQL returned data.node:null for PROJECT_ID=%s — token may lack access, or the project was deleted.\n' "${PROJECT_ID}" >&2
return 75
fi
CI_INIT_FIELD_ID=$(printf '%s' "${json}" | jq -r '.data.node.initiative.id? // ""')
CI_THEME_FIELD_ID=$(printf '%s' "${json}" | jq -r '.data.node.theme.id? // ""')
if [ -z "${CI_INIT_FIELD_ID}" ]; then
if [[ -z "${CI_INIT_FIELD_ID}" ]]; then
printf '[resolve_fields] Initiative single-select field %q not found on the project.\n' "${INITIATIVE_FIELD:-Initiative}" >&2
return 65
fi

local id name
while IFS=$'\t' read -r id name; do
[ -n "${id}" ] && CI_INIT_OPT["${name}"]="${id}"
[[ -n "${id}" ]] && CI_INIT_OPT["${name}"]="${id}"
done < <(printf '%s' "${json}" | jq -r '.data.node.initiative.options?[]? | "\(.id)\t\(.name)"')
while IFS=$'\t' read -r id name; do
[ -n "${id}" ] && CI_THEME_OPT["${name}"]="${id}"
[[ -n "${id}" ]] && CI_THEME_OPT["${name}"]="${id}"
done < <(printf '%s' "${json}" | jq -r '.data.node.theme.options?[]? | "\(.id)\t\(.name)"')
}

# _ci_report <total> <already> <matched> <unmatched> <skipped> <failed>
_ci_report() {
local total="$1" already="$2" matched="$3" unmatched="$4" skipped="$5" failed="${6:-0}"
local mode="apply"; [ "${DRY_RUN:-}" = "1" ] && mode="dry-run"
local mode="apply"; [[ "${DRY_RUN:-}" = "1" ]] && mode="dry-run"
printf '\n=== classify-initiative summary (%s) ===\n' "${mode}"
printf ' board items scanned : %d\n' "${total}"
printf ' already associated : %d (skipped; RECLASSIFY=all to re-evaluate)\n' "${already}"
printf ' newly matched : %d\n' "${matched}"
printf ' unmatched (blank) : %d\n' "${unmatched}"
printf ' option-missing skip : %d\n' "${skipped}"
printf ' transient set fails : %d (left blank; refilled next sweep)\n' "${failed}"
if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then
{
printf '### classify-initiative (%s)\n\n' "${mode}"
printf '| scanned | already | matched | unmatched | skipped | failed |\n'
Expand Down Expand Up @@ -257,7 +257,7 @@ sweep_project() {
local total=0 already=0 matched=0 unmatched=0 skipped=0 failed=0
local node
while IFS= read -r node; do
[ -n "${node}" ] || continue
[[ -n "${node}" ]] || continue
total=$((total + 1))

local item_id cur title labels repo sig decided init theme optid
Expand All @@ -267,7 +267,7 @@ sweep_project() {
labels=$(printf '%s' "${node}" | jq -c '.content.labels.nodes? // []')
repo=$(printf '%s' "${node}" | jq -r '.content.repository.nameWithOwner? // ""')

if [ -n "${cur}" ] && [ "${RECLASSIFY:-}" != "all" ]; then
if [[ -n "${cur}" ]] && [[ "${RECLASSIFY:-}" != "all" ]]; then
already=$((already + 1))
continue
fi
Expand All @@ -277,14 +277,14 @@ sweep_project() {
init="${decided%%$'\t'*}"
theme="${decided#*$'\t'}"

if [ -z "${init}" ]; then
if [[ -z "${init}" ]]; then
unmatched=$((unmatched + 1))
printf 'UNMATCHED %-20s «%s»\n' "${repo:-draft}" "${title}"
continue
fi

optid="${CI_INIT_OPT[${init}]:-}"
if [ -z "${optid}" ]; then
if [[ -z "${optid}" ]]; then
printf '::warning::rule matched Initiative %q which is not a live project option; skipping «%s»\n' "${init}" "${title}" >&2
skipped=$((skipped + 1))
continue
Expand All @@ -306,9 +306,9 @@ sweep_project() {
# Theme is best-effort: co-assign only when the field and matching option
# both exist live. A missing Theme field/option is silently tolerated, and
# a transient set failure is non-fatal (Initiative already landed).
if [ -n "${theme}" ] && [ -n "${CI_THEME_FIELD_ID}" ]; then
if [[ -n "${theme}" ]] && [[ -n "${CI_THEME_FIELD_ID}" ]]; then
local topt="${CI_THEME_OPT[${theme}]:-}"
if [ -n "${topt}" ] && ! set_item_single_select_value "${item_id}" "${CI_THEME_FIELD_ID}" "${topt}"; then
if [[ -n "${topt}" ]] && ! set_item_single_select_value "${item_id}" "${CI_THEME_FIELD_ID}" "${topt}"; then
printf '::warning::failed to set Theme %q on «%s» (transient?); retry requires RECLASSIFY=all (Initiative already set)\n' "${theme}" "${title}" >&2
fi
fi
Expand All @@ -317,6 +317,6 @@ sweep_project() {
_ci_report "${total}" "${already}" "${matched}" "${unmatched}" "${skipped}" "${failed}"
}

if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
if [[ "${BASH_SOURCE[0]}" = "${0}" ]]; then
sweep_project
fi
36 changes: 18 additions & 18 deletions .github/scripts/add-to-project/lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@
set -euo pipefail

_atp_require_env() {
if [ -z "${PROJECT_ID:-}" ]; then
if [[ -z "${PROJECT_ID:-}" ]]; then
printf '[%s] PROJECT_ID env var is required\n' "$1" >&2
return 64
fi
if [ -z "${GH_TOKEN:-}" ]; then
if [[ -z "${GH_TOKEN:-}" ]]; then
printf '::error::[%s] GH_TOKEN is empty. INITIATIVES_APP_ID / INITIATIVES_APP_PRIVATE_KEY are likely unset or stale. See combsmith/.github#387.\n' "$1" >&2
return 64
fi
Expand All @@ -40,13 +40,13 @@ _atp_require_env() {
# "is it already on the board?" from memory and skip the API call unless a
# genuine change is needed. The event path leaves both unset, so it always
# falls through to the network and its behavior is unchanged.
_atp_membership_ready() { [ "${_ATP_MEMBERSHIP_READY:-}" = "1" ]; }
_atp_membership_ready() { [[ "${_ATP_MEMBERSHIP_READY:-}" = "1" ]]; }

# _atp_on_board <content_node_id>
# Returns 0 if the cache says the content IS on the board, 1 if the cache
# says it is NOT. MUST be gated by `_atp_membership_ready` — when no cache
# has been populated the result is meaningless (there is nothing to consult).
_atp_on_board() { [ -n "${_ATP_ON_BOARD[$1]:-}" ]; }
_atp_on_board() { [[ -n "${_ATP_ON_BOARD[$1]:-}" ]]; }

# find_project_item <kind> <value>
# kind=title-prefix → match items whose content.title startswith <value>
Expand All @@ -59,7 +59,7 @@ _atp_on_board() { [ -n "${_ATP_ON_BOARD[$1]:-}" ]; }
# caller add duplicates. Lookup is paginated so the project can grow past
# the first page without silently missing matches.
find_project_item() {
if [ "$#" -ne 2 ]; then
if [[ "$#" -ne 2 ]]; then
printf '[find_project_item] expected 2 args (kind value), got %d\n' "$#" >&2
return 64
fi
Expand Down Expand Up @@ -89,7 +89,7 @@ find_project_item() {
while true; do
local json
# shellcheck disable=SC2016 # $projectId/$pageSize/$cursor are GraphQL variables
if [ -n "${cursor}" ]; then
if [[ -n "${cursor}" ]]; then
json=$(gh api graphql \
-F projectId="${PROJECT_ID}" \
-F pageSize="${page_size}" \
Expand Down Expand Up @@ -135,7 +135,7 @@ find_project_item() {
}')
fi

if [ "$(printf '%s' "${json}" | jq -r '.data.node')" = "null" ]; then
if [[ "$(printf '%s' "${json}" | jq -r '.data.node')" = "null" ]]; then
printf '[find_project_item] GraphQL returned data.node:null. PROJECT_ID=%s — token may lack access, or the project was deleted.\n' "${PROJECT_ID}" >&2
return 75
fi
Expand Down Expand Up @@ -178,8 +178,8 @@ find_project_item() {
esac
done <<< "${parsed}"

if [ -n "${match}" ]; then
if [ "${match_count}" != "1" ]; then
if [[ -n "${match}" ]]; then
if [[ "${match_count}" != "1" ]]; then
# Multi-match on a single page indicates inconsistent state (manual
# drafts shadowing automation). Warn so an operator can clean up,
# but proceed with the first match so the state machine progresses.
Expand All @@ -190,15 +190,15 @@ find_project_item() {
return 0
fi

if [ "${has_next}" != "true" ]; then
if [[ "${has_next}" != "true" ]]; then
return 0
fi
cursor="${end_cursor}"
done
}

add_content_to_project() {
if [ "$#" -ne 1 ]; then
if [[ "$#" -ne 1 ]]; then
printf '[add_content_to_project] expected 1 arg (content_node_id), got %d\n' "$#" >&2
return 64
fi
Expand All @@ -214,7 +214,7 @@ add_content_to_project() {
return 0
fi

if [ "${DRY_RUN:-}" = "1" ]; then
if [[ "${DRY_RUN:-}" = "1" ]]; then
printf '[dry-run] would add content %s to project\n' "${content_node_id}"
return 0
fi
Expand All @@ -233,14 +233,14 @@ add_content_to_project() {
}

add_draft_item() {
if [ "$#" -ne 2 ]; then
if [[ "$#" -ne 2 ]]; then
printf '[add_draft_item] expected 2 args (title body), got %d\n' "$#" >&2
return 64
fi
local title="$1"
local body="$2"

if [ "${DRY_RUN:-}" = "1" ]; then
if [[ "${DRY_RUN:-}" = "1" ]]; then
printf '[dry-run] would add draft: %s\n' "${title}"
return 0
fi
Expand All @@ -258,13 +258,13 @@ add_draft_item() {
}

delete_project_item() {
if [ "$#" -ne 1 ]; then
if [[ "$#" -ne 1 ]]; then
printf '[delete_project_item] expected 1 arg (item_id), got %d\n' "$#" >&2
return 64
fi
local item_id="$1"

if [ "${DRY_RUN:-}" = "1" ]; then
if [[ "${DRY_RUN:-}" = "1" ]]; then
printf '[dry-run] would delete item %s from project\n' "${item_id}"
return 0
fi
Expand Down Expand Up @@ -303,15 +303,15 @@ delete_project_item() {
# Exit 0 on success; 64 on bad args. DRY_RUN=1 logs the intended write and
# mutates nothing.
set_item_single_select_value() {
if [ "$#" -ne 3 ]; then
if [[ "$#" -ne 3 ]]; then
printf '[set_item_single_select_value] expected 3 args (item_id field_id option_id), got %d\n' "$#" >&2
return 64
fi
local item_id="$1"
local field_id="$2"
local option_id="$3"

if [ "${DRY_RUN:-}" = "1" ]; then
if [[ "${DRY_RUN:-}" = "1" ]]; then
printf '[dry-run] would set item %s field %s = option %s\n' "${item_id}" "${field_id}" "${option_id}"
return 0
fi
Expand Down
Loading
Loading