Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
0f7f00f
feat: implement issue #1193 — SonarCloud: shell script hygiene
don-petry Sep 29, 2026
743d538
fix(reviews): address review comments [skip ci-relay]
don-petry Sep 29, 2026
d1e8d82
fix(bot): address bot feedback [skip ci-relay]
don-petry Sep 30, 2026
50e72fd
fix(reviews): address review comments [skip ci-relay]
don-petry Sep 30, 2026
26076ff
fix(bot): address bot feedback [skip ci-relay]
don-petry Oct 2, 2026
41ff371
fix: address octal parsing and owner validation issues in shell scripts
don-petry Oct 2, 2026
38474b5
fix: avoid arithmetic wraparound in sweep.sh positive-cap check
don-petry Oct 2, 2026
775ad08
fix(reviews): address review comments [skip ci-relay]
don-petry Oct 2, 2026
ddd2302
fix: normalize version comparison in ring_highest_channel_major
don-petry Oct 3, 2026
79f9431
fix: normalize comparison in amcl_clean_cycles_met
don-petry Oct 3, 2026
943e537
fix: normalize required parameter comparison in agents-md-cycle-log
don-petry Oct 3, 2026
030518f
fix: prevent --override from advancing AWAITING_CONFIRMATION state
don-petry Oct 3, 2026
707cda1
fix: address CodeRabbit review findings for range validation and erro…
don-petry Oct 3, 2026
f7f70fa
fix: add range check to amcl_clean_cycles_met validation
don-petry Oct 3, 2026
8f18acb
fix(reviews): address review comments [skip ci-relay]
don-petry Oct 3, 2026
0fa8ce7
fix(reviews): address review comments [skip ci-relay]
don-petry Oct 3, 2026
8e3443a
fix: normalize numeric values to base 10 before [[ comparisons
don-petry Oct 3, 2026
433e946
fix(canary-rollout): normalize CANARY_MAX_COMMIT_PAGES to base 10
don-petry Oct 3, 2026
fa1067a
fix(reviews): address review comments [skip ci-relay]
don-petry Oct 3, 2026
5d72c99
fix(compliance-audit): define constants for frequently-used literal s…
don-petry Oct 3, 2026
d53e0e0
fix(compliance-audit): use jq --arg for safe variable passing
don-petry Oct 3, 2026
23dea8e
fix(agents-md-cycle-log): normalize to base 10 before numeric compari…
don-petry Oct 3, 2026
898b5b3
fix(agents-md-cycle-log): add 18-digit range check before 10# conversion
don-petry Oct 11, 2026
26d18c6
fix(sonarcloud): address S1066 and S1192 code quality issues in compl…
don-petry Oct 14, 2026
0d1964d
fix(reviews): use [[ ]] in ruleset-list tests; reject nonpositive rou…
don-petry Oct 14, 2026
7c6fd33
fix(reviews): use [[ ]] in list_workflow_files tests
don-petry Oct 17, 2026
118dcfc
fix(bot): strip leading plus sign in clamp and round_div functions
don-petry Oct 17, 2026
80d5a4d
fix(bootstrap): revert unrelated argument-validation behavior change
don-petry Oct 18, 2026
f1c2b36
fix(sonarcloud): simplify TARGET_REPO parsing, remove owner validation
don-petry Oct 18, 2026
be0649c
fix(pinned-version-report): remove retry-on-failure logic for tag loads
don-petry Oct 18, 2026
f0190f9
fix(agents-md-cycle-log): remove redundant base-10 conversions
don-petry Oct 28, 2026
64d2a7e
fix(sonarcloud-audit): validate target_name after org-prefix removal
don-petry Oct 28, 2026
2a465c5
fix(canary-rollout): improve denominator validation in round_div
don-petry Oct 28, 2026
533c804
fix(sonarcloud): convert remaining single brackets to double brackets…
don-petry Oct 29, 2026
e7d3c6d
Merge branch 'main' into dev-lead/issue-1193-20260929-2126
don-petry Nov 8, 2026
a00090e
Merge branch 'main' into dev-lead/issue-1193-20260929-2126
don-petry Nov 8, 2026
ae0559d
Merge branch 'main' into dev-lead/issue-1193-20260929-2126
don-petry Nov 9, 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 @@ -281,14 +281,14 @@ sweep_project() {
local log_title
log_title=$(atp_log_safe "${title}")

if [ -z "${init}" ]; then
if [[ -z "${init}" ]]; then
unmatched=$((unmatched + 1))
printf 'UNMATCHED %-20s «%s»\n' "${repo:-draft}" "${log_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}" "${log_title}" >&2
skipped=$((skipped + 1))
continue
Expand All @@ -310,9 +310,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}" "${log_title}" >&2
fi
fi
Expand All @@ -321,6 +321,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 Down Expand Up @@ -60,13 +60,13 @@ atp_log_safe() {
# "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 @@ -79,7 +79,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 @@ -109,7 +109,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 @@ -155,7 +155,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 @@ -198,8 +198,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 @@ -210,15 +210,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 @@ -234,7 +234,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 @@ -253,14 +253,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' "$(atp_log_safe "${title}")"
return 0
fi
Expand All @@ -278,13 +278,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 @@ -323,15 +323,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