diff --git a/bash/aws.sh b/bash/aws.sh index 9e0e9fc..ae3bf73 100644 --- a/bash/aws.sh +++ b/bash/aws.sh @@ -46,10 +46,26 @@ function aws::get_tags { } function aws::scan { + @doc Iterate over every ECR repository and refresh the scan for the \ + given tag, skipping any repository named in AWS_SCAN_SKIP_REPOS. \ + AWS_SCAN_SKIP_REPOS is comma-separated and defaults to \ + blockchaintp/busybox, preserving the historical BTP behaviour. \ + SUR-2832. + @arg _1_ tag to scan - when empty every tag is rescanned local tag=$1 + local skip_list=${AWS_SCAN_SKIP_REPOS:-blockchaintp/busybox} for repository in $(aws::get_repositories); do - if [ "$repository" = "blockchaintp/busybox" ]; then - log::info "Skipping busybox repository" + local skip + local match=false + IFS=',' read -ra skip <<<"$skip_list" + for skip_entry in "${skip[@]}"; do + if [ "$repository" = "$skip_entry" ]; then + match=true + break + fi + done + if [ "$match" = "true" ]; then + log::info "Skipping $repository repository (AWS_SCAN_SKIP_REPOS)" continue fi log::info "Scanning $repository $tag" diff --git a/bash/docker.sh b/bash/docker.sh index 2018154..b2a1091 100644 --- a/bash/docker.sh +++ b/bash/docker.sh @@ -25,7 +25,11 @@ source "$(dirname "${BASH_SOURCE[0]}")/includer.sh" @package docker function docker::cmd() { - @doc Smart command for docker. + @doc Smart command for docker. \ + When the SIMULATE environment variable is non-empty every docker \ + invocation is echoed to stdout instead of executed. release-images -d \ + sets SIMULATE=true to provide a dry-run mode and callers that want \ + the same behaviour for ad-hoc scripts can export SIMULATE themselves. if [ -z "$SIMULATE" ]; then $(commands::use docker) "$@" else @@ -209,18 +213,37 @@ function docker::list_tags { } function docker::list_versions { + @doc List repository tags matching the version pattern, sorted by \ + semver. The pattern defaults to the historical BTP regex. \ + Override by passing an explicit pattern argument or by setting \ + DOCKER_VERSION_PATTERN in the environment. SUR-2832. + @arg _1_ repository name + @arg _2_ registry url + @arg _3_ optional ERE pattern - defaults to historical BTP regex or DOCKER_VERSION_PATTERN local repository=${1:?} local registry=${2?} + local default_pattern='BTP[0-9]+.[0-9]+.[0-9]+(rc[0-9]+)?(-[0-9]+-[a-z0-9]{8,10})?(-[0-9]+.[0-9]+.[0-9]+(p[0-9]+(-[0-9]+-[a-z0-9]{8,10})?)?)?' + local pattern=${3:-${DOCKER_VERSION_PATTERN:-$default_pattern}} docker::list_tags "$repository" "$registry" | - grep -E 'BTP[0-9]+.[0-9]+.[0-9]+(rc[0-9]+)?(-[0-9]+-[a-z0-9]{8,10})?(-[0-9]+.[0-9]+.[0-9]+(p[0-9]+(-[0-9]+-[a-z0-9]{8,10})?)?)?' | + grep -E "$pattern" | sort -V } function docker::list_official_versions { + @doc List repository tags matching the official-release pattern, \ + sorted by semver. The pattern defaults to the historical BTP \ + anchored regex. Override by passing an explicit pattern argument or \ + by setting DOCKER_OFFICIAL_VERSION_PATTERN in the environment. \ + SUR-2832. + @arg _1_ repository name + @arg _2_ registry url + @arg _3_ optional ERE pattern - defaults to anchored BTP regex or DOCKER_OFFICIAL_VERSION_PATTERN local repository=${1:?} local registry=${2?} - docker::list_tags "$repository" "$registry" | grep -E \ - '^BTP[0-9]+.[0-9]+.[0-9]+(rc[0-9]+)?(-[0-9]+.[0-9]+.[0-9]+(p[0-9]+)?)?$' | + local default_pattern='^BTP[0-9]+.[0-9]+.[0-9]+(rc[0-9]+)?(-[0-9]+.[0-9]+.[0-9]+(p[0-9]+)?)?$' + local pattern=${3:-${DOCKER_OFFICIAL_VERSION_PATTERN:-$default_pattern}} + docker::list_tags "$repository" "$registry" | + grep -E "$pattern" | sort -V } diff --git a/bash/find-squashable b/bash/find-squashable index e210c6e..ef8d34f 100755 --- a/bash/find-squashable +++ b/bash/find-squashable @@ -29,25 +29,42 @@ options::parse "$@" previous_cmt= previous_files= -first_squash="true" +group_open=false +group_first= +# SUR-2840: separate `group_open` (bool) from `group_first` (SHA) so the +# state isn't overloaded onto one sentinel. Cache git::files_changed | +# cksum once per iteration and flush a trailing same-files group after +# the loop — pre-fix, a group that ran to the oldest commit in range +# never printed because the emit branch only fired on a different +# following commit. for cmt in $(git::commits "$START_FROM" "$END_AT"); do + these_files=$(git::files_changed "$cmt" | cksum) if [ -n "$previous_cmt" ]; then - these_files=$(git::files_changed "$cmt" | cksum) if [ "$these_files" = "$previous_files" ]; then - if [ "$first_squash" = "true" ]; then - first_squash=$previous_cmt + if [ "$group_open" = "false" ]; then + group_first=$previous_cmt + group_open=true fi else - if [ "$first_squash" != "true" ]; then + if [ "$group_open" = "true" ]; then echo - echo "$first_squash" to "$previous_cmt" could be squashed - git::log_fromto "${previous_cmt}~1" "$first_squash" + echo "$group_first" to "$previous_cmt" could be squashed + git::log_fromto "${previous_cmt}~1" "$group_first" echo git::files_changed "$previous_cmt" + group_open=false + group_first= fi - first_squash="true" fi fi previous_cmt=$cmt - previous_files=$(git::files_changed "$cmt" | cksum) + previous_files=$these_files done + +if [ "$group_open" = "true" ]; then + echo + echo "$group_first" to "$previous_cmt" could be squashed + git::log_fromto "${previous_cmt}~1" "$group_first" + echo + git::files_changed "$previous_cmt" +fi diff --git a/bash/git-check b/bash/git-check index 260f1e5..19364fe 100755 --- a/bash/git-check +++ b/bash/git-check @@ -37,7 +37,12 @@ function set_client() { options::standard options::description "Check all the repositories according to the\ - parameters and update keep them in sync with the origin if possible" + parameters and update keep them in sync with the origin if possible.\ + GH_PR_CHECK is only consulted for orgs in GIT_CHECK_GH_ORGS\ + (space-separated; default \"blockchaintp catenasys scealiontach\");\ + BitBucket PR counts are only fetched for orgs in GIT_CHECK_BB_ORGS\ + (default \"TASE\"). Override either to extend the toolkit beyond the\ + historical BTP setup." options::add -o o -d "organization to scan" -a -f set_organization options::add -o b -d "base directory to scan" -a -e BASE options::add -o p -d "check for pull requests" -x GH_PR_CHECK @@ -92,8 +97,16 @@ function get_gh_pr_count { return fi if command -v gh >/dev/null; then - if [ "$orgname" = "blockchaintp" ] || - [ "$orgname" = "catenasys" ] || [ "$orgname" = "scealiontach" ]; then + local gh_orgs=${GIT_CHECK_GH_ORGS:-"blockchaintp catenasys scealiontach"} + local org + local match=false + for org in $gh_orgs; do + if [ "$orgname" = "$org" ]; then + match=true + break + fi + done + if [ "$match" = "true" ]; then log::debug "Checking pull requests for $base_name" gh pr list -R "$base_name" --draft=false | grep -cv "no open" else @@ -114,7 +127,16 @@ function get_bb_pr_count { return fi if command -v bb >/dev/null; then - if [ "$orgname" = "TASE" ]; then + local bb_orgs=${GIT_CHECK_BB_ORGS:-TASE} + local org + local match=false + for org in $bb_orgs; do + if [ "$orgname" = "$org" ]; then + match=true + break + fi + done + if [ "$match" = "true" ]; then log::debug "Checking pull requests for $base_name" bb -c "$orgname" pr-count "$base_name" else diff --git a/bash/log.sh b/bash/log.sh index 85cc5e8..99541d3 100644 --- a/bash/log.sh +++ b/bash/log.sh @@ -86,29 +86,16 @@ function log::level() { LOG_LEVEL=$level } -# Initialise the LOG_DISABLE_* flags from LOG_LEVEL at source time so -# scripts that don't pass -v still get deterministic gating. Each flag is -# initialised independently: SUR-2347 — pre-fix, if a caller pre-set any -# single flag (e.g. LOG_DISABLE_INFO=false) the entire block was skipped, -# leaving the other three unset (empty), and `[ "$LOG_DISABLE_TRACE" = -# "false" ]` evaluated false, silently disabling output. Snapshot the -# caller's explicit pre-sets, let log::level recompute defaults from -# LOG_LEVEL, then restore the snapshots so explicit values win. -__log_preset_trace=${LOG_DISABLE_TRACE+x} -__log_preset_debug=${LOG_DISABLE_DEBUG+x} -__log_preset_info=${LOG_DISABLE_INFO+x} -__log_preset_warning=${LOG_DISABLE_WARNING+x} -__log_value_trace=${LOG_DISABLE_TRACE-} -__log_value_debug=${LOG_DISABLE_DEBUG-} -__log_value_info=${LOG_DISABLE_INFO-} -__log_value_warning=${LOG_DISABLE_WARNING-} -log::level "$LOG_LEVEL" -[ "$__log_preset_trace" = x ] && LOG_DISABLE_TRACE=$__log_value_trace -[ "$__log_preset_debug" = x ] && LOG_DISABLE_DEBUG=$__log_value_debug -[ "$__log_preset_info" = x ] && LOG_DISABLE_INFO=$__log_value_info -[ "$__log_preset_warning" = x ] && LOG_DISABLE_WARNING=$__log_value_warning -unset __log_preset_trace __log_preset_debug __log_preset_info __log_preset_warning -unset __log_value_trace __log_value_debug __log_value_info __log_value_warning +# Initialise each LOG_DISABLE_* flag from LOG_LEVEL at source time, +# honouring any caller pre-set. The ${VAR+x} guard treats an explicit +# pre-set as authoritative; only unset flags are computed from +# LOG_LEVEL. log::level (above) still recomputes unconditionally, which +# is what log::level_increase / log::level_decrease rely on. +# (SUR-2347; simplified SUR-2843) +[ -z "${LOG_DISABLE_TRACE+x}" ] && { ((LOG_LEVEL > 3)) && LOG_DISABLE_TRACE=false || LOG_DISABLE_TRACE=true; } +[ -z "${LOG_DISABLE_DEBUG+x}" ] && { ((LOG_LEVEL > 2)) && LOG_DISABLE_DEBUG=false || LOG_DISABLE_DEBUG=true; } +[ -z "${LOG_DISABLE_INFO+x}" ] && { ((LOG_LEVEL > 1)) && LOG_DISABLE_INFO=false || LOG_DISABLE_INFO=true; } +[ -z "${LOG_DISABLE_WARNING+x}" ] && { ((LOG_LEVEL > 0)) && LOG_DISABLE_WARNING=false || LOG_DISABLE_WARNING=true; } function log::level_increase() { @doc Increase the LOG_LEVEL diff --git a/bash/pagerduty-alert b/bash/pagerduty-alert index a34e226..ae60ef4 100755 --- a/bash/pagerduty-alert +++ b/bash/pagerduty-alert @@ -25,7 +25,9 @@ source "$(dirname "${BASH_SOURCE[0]}")/includer.sh" log::level 2 options::standard -options::description "Creates PagerDuty alert (incident)" +options::description "Creates PagerDuty alert (incident). When -f is not\ + supplied the From address defaults to the value of PAGERDUTY_FROM_DEFAULT\ + (historical default: \"no-reply@blockchaintp.com\")." options::add -o a -d "Alert API Token" -a -m -e ALERT_TOKEN options::add -o s -d "PagerDuty ServiceID" -a -m -e SERVICE_ID options::add -o i -d "Alert Type (incident)" -a -m -e ALERT_TYPE @@ -35,7 +37,7 @@ options::add -o f -d "Alert From Email Address" -a -e ALERT_FROM options::parse_available "$@" ALERT_TITLE=${ALERT_TITLE:="Test Alert"} -ALERT_FROM=${ALERT_FROM:="no-reply@blockchaintp.com"} +ALERT_FROM=${ALERT_FROM:="${PAGERDUTY_FROM_DEFAULT:-no-reply@blockchaintp.com}"} case $ALERT_TYPE in incident) diff --git a/bash/release-images b/bash/release-images index b293b8e..9abb44c 100755 --- a/bash/release-images +++ b/bash/release-images @@ -27,7 +27,11 @@ function _add_additional_registry { } options::standard -options::description "various tools to do with releasing and publishing images" +options::description "various tools to do with releasing and publishing\ + images. The source organization filtered against the registry catalogue\ + defaults to \"blockchaintp\" for historical compatibility; override via\ + the RELEASE_IMAGES_ORG env var. -d sets SIMULATE=true so docker::cmd\ + echoes commands instead of executing them." options::add -o t -d "Target tag pattern to copy" -a -m -e target_tag options::add -o r -d "Source registry to find images" -a -m -e target_registry options::add -o a -d "target registry url to copy to, may be repeated" -a -m -f _add_additional_registry @@ -40,12 +44,14 @@ if [ "$DRY_RUN" = "true" ]; then export SIMULATE=true fi +RELEASE_IMAGES_ORG=${RELEASE_IMAGES_ORG:-blockchaintp} + if [ -z "$IMAGES_FILE" ]; then # docker::promote_latest accepts variadic extras after shift 3, so call # it once with every additional_registry instead of looping — this # collapses N redundant pulls per repo into one (60 pulls -> 20 across # a 30-repo / 3-extra-registry org). - docker::promote_latest blockchaintp "${target_registry:?}" \ + docker::promote_latest "$RELEASE_IMAGES_ORG" "${target_registry:?}" \ "${target_tag:?}" \ "${additional_registries[@]}" else diff --git a/bash/review-prs b/bash/review-prs index 853320f..7ba0458 100755 --- a/bash/review-prs +++ b/bash/review-prs @@ -22,7 +22,11 @@ function usage() { true } options::standard -options::description "Opens a browser to review all pull requests for the specified organizations." +options::description "Opens a browser to review all pull requests for the\ + specified organizations. If no -o orgs are given, the default organization\ + list comes from REVIEW_PRS_ORGS (space-separated; historical default is\ + \"391agency btpworks blockchaintp catenasys\"). REVIEW_PRS_INTEREST_ORGS\ + plays the same role for -i." declare -a -g ORGANIZATIONS function local::add_organization { @@ -47,12 +51,19 @@ shift $((OPTIND - 1)) OPEN_BROWSER=${OPEN_BROWSER:-false} GIT_HOME=${GIT_HOME:-$HOME/git} +# SUR-2832: defaults preserved for the BTP-era invocation; override +# REVIEW_PRS_ORGS / REVIEW_PRS_INTEREST_ORGS (space-separated) to scan +# different organizations without editing the script. +REVIEW_PRS_ORGS=${REVIEW_PRS_ORGS:-"391agency btpworks blockchaintp catenasys"} +REVIEW_PRS_INTEREST_ORGS=${REVIEW_PRS_INTEREST_ORGS:-hyperledger} if [ -z "${INTERESTED_ORGANIZATIONS[*]}" ]; then - INTERESTED_ORGANIZATIONS+=(hyperledger) + # shellcheck disable=SC2206 # intentional word-splitting on space-separated env var + INTERESTED_ORGANIZATIONS+=(${REVIEW_PRS_INTEREST_ORGS}) fi if [ -z "${ORGANIZATIONS[*]}" ]; then - ORGANIZATIONS+=(391agency btpworks blockchaintp catenasys) + # shellcheck disable=SC2206 # intentional word-splitting on space-separated env var + ORGANIZATIONS+=(${REVIEW_PRS_ORGS}) fi function review_prs { diff --git a/bash/update-repo-tags b/bash/update-repo-tags index 60e895d..b9ed3e9 100755 --- a/bash/update-repo-tags +++ b/bash/update-repo-tags @@ -24,7 +24,9 @@ source "$(dirname "${BASH_SOURCE[0]}")/includer.sh" options::standard options::description "Add a new tag to the repository based on semver and\ - conventional commits." + conventional commits. Annotated tags are GPG-signed (git tag -s), which\ + requires a configured signing key (user.signingkey + gpg-agent or\ + equivalent); pass -U to write an unsigned annotated tag instead." options::add -o t -d "target directory to examine and update" -a -m \ -e TARGET_DIR options::add -o p -d "Patch tag" \ @@ -33,6 +35,8 @@ options::add -o b -d "proceed to bump minor version if breaking changes are foun -x ALLOW_BREAKING options::add -o c -d "generate and commit change before tagging" \ -x GENERATE_CHANGELOG +options::add -o U -d "skip GPG signing of the annotated tag" \ + -x NO_SIGN_TAG options::parse "$@" pushd "$TARGET_DIR" >/dev/null || exit 1 @@ -128,7 +132,11 @@ if [ "$skip" = "false" ]; then log::notice "No prior changelog found. Will not generate a new one." fi fi - git::cmd tag -a -s "$TAG" -m "Auto Tagging $TAG" + if [ "$NO_SIGN_TAG" = "true" ]; then + git::cmd tag -a "$TAG" -m "Auto Tagging $TAG" + else + git::cmd tag -a -s "$TAG" -m "Auto Tagging $TAG" + fi fi fi popd >/dev/null || exit 1 diff --git a/tests/find-squashable.bats b/tests/find-squashable.bats index 263e2c6..104974d 100644 --- a/tests/find-squashable.bats +++ b/tests/find-squashable.bats @@ -51,3 +51,34 @@ teardown() { [ "$status" -eq 0 ] [[ "$output" == *"could be squashed"* ]] } + +# SUR-2840: when a same-files run extends to the oldest commit in the +# iteration range, the historical emit branch never fired because it +# required a different following commit. Build a history where every +# in-range commit touches the same file and assert the trailing group is +# still reported. +@test "find-squashable flushes a same-files group that extends to the last commit (SUR-2840)" { + TAIL_FIXTURE=$(mktemp -d) + ( + cd "$TAIL_FIXTURE" || exit 1 + git init -q -b main . + helpers::set_git_identity + echo "initial" >file_a.txt + git add file_a.txt + git -c commit.gpgsign=false commit -q -m "feat: initial" + echo "c1" >>file_a.txt + git add file_a.txt + git -c commit.gpgsign=false commit -q -m "feat: change file_a 1" + echo "c2" >>file_a.txt + git add file_a.txt + git -c commit.gpgsign=false commit -q -m "feat: change file_a 2" + echo "c3" >>file_a.txt + git add file_a.txt + git -c commit.gpgsign=false commit -q -m "feat: change file_a 3" + ) + TAIL_START=$(git -C "$TAIL_FIXTURE" rev-list --reverse HEAD | head -1) + run bash -c "cd '$TAIL_FIXTURE' && '$SQUASHABLE' -s '$TAIL_START'" + rm -rf "$TAIL_FIXTURE" + [ "$status" -eq 0 ] + [[ "$output" == *"could be squashed"* ]] +} diff --git a/tests/sur-2832-btp-defaults.sh b/tests/sur-2832-btp-defaults.sh new file mode 100644 index 0000000..6dd12c0 --- /dev/null +++ b/tests/sur-2832-btp-defaults.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +# SUR-2832: hardcoded BTP / blockchaintp identifiers have been promoted +# to overridable environment variables. Lock down both contracts: +# 1. The default of each env var matches the historical literal so the +# refactor cannot silently regress callers that relied on the +# previous behaviour. +# 2. The promoted env var actually takes effect when set (covers the +# AWS_SCAN_SKIP_REPOS path through aws::scan). + +TEST_DIR="$(cd -P "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd -P "$TEST_DIR/.." && pwd)" +# shellcheck source=lib/assert.sh +source "$TEST_DIR/lib/assert.sh" + +failures=0 + +# --- Default literals ---------------------------------------------------- + +# release-images: source organization default is "blockchaintp". +out=$(grep -E '^RELEASE_IMAGES_ORG=' "$REPO_ROOT/bash/release-images") +assert_contains "$out" "blockchaintp" \ + "release-images RELEASE_IMAGES_ORG default preserves blockchaintp" || + failures=$((failures + 1)) + +# review-prs: org list default is "391agency btpworks blockchaintp catenasys". +out=$(grep -E '^REVIEW_PRS_ORGS=' "$REPO_ROOT/bash/review-prs") +assert_contains "$out" "391agency btpworks blockchaintp catenasys" \ + "review-prs REVIEW_PRS_ORGS default preserves BTP-era org list" || + failures=$((failures + 1)) + +out=$(grep -E '^REVIEW_PRS_INTEREST_ORGS=' "$REPO_ROOT/bash/review-prs") +assert_contains "$out" "hyperledger" \ + "review-prs REVIEW_PRS_INTEREST_ORGS default preserves hyperledger" || + failures=$((failures + 1)) + +# git-check: GH org default is "blockchaintp catenasys scealiontach". +out=$(grep -E 'GIT_CHECK_GH_ORGS:-' "$REPO_ROOT/bash/git-check") +assert_contains "$out" "blockchaintp catenasys scealiontach" \ + "git-check GIT_CHECK_GH_ORGS default preserves BTP-era org list" || + failures=$((failures + 1)) + +out=$(grep -E 'GIT_CHECK_BB_ORGS:-' "$REPO_ROOT/bash/git-check") +assert_contains "$out" "TASE" \ + "git-check GIT_CHECK_BB_ORGS default preserves TASE" || + failures=$((failures + 1)) + +# pagerduty-alert: default From address is no-reply@blockchaintp.com. +out=$(grep -E 'PAGERDUTY_FROM_DEFAULT:-' "$REPO_ROOT/bash/pagerduty-alert") +assert_contains "$out" "no-reply@blockchaintp.com" \ + "pagerduty-alert PAGERDUTY_FROM_DEFAULT preserves BTP From address" || + failures=$((failures + 1)) + +# aws.sh: default skip list is blockchaintp/busybox. +out=$(grep -E 'AWS_SCAN_SKIP_REPOS:-' "$REPO_ROOT/bash/aws.sh") +assert_contains "$out" "blockchaintp/busybox" \ + "aws.sh AWS_SCAN_SKIP_REPOS default preserves blockchaintp/busybox" || + failures=$((failures + 1)) + +# docker.sh: default version patterns retain the historical BTP regex. +out=$(grep -E "default_pattern='BTP" "$REPO_ROOT/bash/docker.sh") +assert_contains "$out" "BTP" \ + "docker::list_versions default_pattern preserves the BTP regex" || + failures=$((failures + 1)) + +out=$(grep -E "default_pattern='\^BTP" "$REPO_ROOT/bash/docker.sh") +assert_contains "$out" "^BTP" \ + "docker::list_official_versions default_pattern preserves the anchored BTP regex" || + failures=$((failures + 1)) + +# --- AWS_SCAN_SKIP_REPOS override actually takes effect ----------------- + +# Drive aws::scan with stubbed aws::get_repositories and aws::scan_repository +# to assert the override skips additional repos. +override_out=$( + # shellcheck source=/dev/null + source "$REPO_ROOT/bash/includer.sh" + @include aws + + # Stub out the AWS calls so the test runs without aws on PATH. + # shellcheck disable=SC2317,SC2329 # invoked indirectly by sourced aws::scan + aws::get_repositories() { printf 'blockchaintp/busybox\nblockchaintp/foo\nacme/bar\n'; } + # shellcheck disable=SC2317,SC2329 # invoked indirectly by sourced aws::scan + aws::scan_repository() { echo "SCAN $1 $2"; } + + AWS_SCAN_SKIP_REPOS="blockchaintp/busybox,blockchaintp/foo" aws::scan some-tag 2>/dev/null +) +# Only acme/bar should be scanned. +assert_contains "$override_out" "SCAN acme/bar some-tag" \ + "AWS_SCAN_SKIP_REPOS override leaves non-skipped repos scannable" || + failures=$((failures + 1)) +case "$override_out" in + *"SCAN blockchaintp/foo"*) + echo "FAIL: AWS_SCAN_SKIP_REPOS did not skip blockchaintp/foo" >&2 + failures=$((failures + 1)) + ;; +esac + +if [ "$failures" -ne 0 ]; then + echo "sur-2832: $failures assertion(s) failed" >&2 + exit 1 +fi +exit 0