From 02c31c136ddce3a235035a9654637b0e432ae392 Mon Sep 17 00:00:00 2001 From: David Seveloff Date: Tue, 20 Oct 2026 16:37:00 +0300 Subject: [PATCH 1/2] feat: add release-beta-lock action [ED-25903] Lock or unlock release/beta for merges by toggling a dedicated "Release beta lock" ruleset that holds an "update" rule. The ruleset is created on first run. API failures emit an error annotation and fail the step. Ref: ED-25903 Co-authored-by: Cursor --- actions/release-beta-lock/action.yml | 33 +++ actions/release-beta-lock/package.json | 9 + .../release-beta-lock/release-beta-lock.sh | 81 ++++++++ .../release-beta-lock.test.sh | 196 ++++++++++++++++++ 4 files changed, 319 insertions(+) create mode 100644 actions/release-beta-lock/action.yml create mode 100644 actions/release-beta-lock/package.json create mode 100644 actions/release-beta-lock/release-beta-lock.sh create mode 100644 actions/release-beta-lock/release-beta-lock.test.sh diff --git a/actions/release-beta-lock/action.yml b/actions/release-beta-lock/action.yml new file mode 100644 index 000000000..4c4ae26ad --- /dev/null +++ b/actions/release-beta-lock/action.yml @@ -0,0 +1,33 @@ +name: Release Beta Lock +description: >- + Locks or unlocks the beta release branch for merges by toggling a dedicated repository ruleset + with an "update" rule. The ruleset is created on first run. + Requires a token with repository Administration write permission; GITHUB_TOKEN is not sufficient. + +inputs: + token: + description: Token with repository Administration write permission (PAT or GitHub App token) + required: true + state: + description: 'Target state: "locked" or "unlocked"' + required: true + branch: + description: Branch to lock or unlock + required: false + default: release/beta + ruleset-name: + description: Name of the ruleset that holds the lock + required: false + default: Release beta lock + +runs: + using: composite + steps: + - name: Apply lock state + shell: bash + env: + GH_TOKEN: ${{ inputs.token }} + STATE: ${{ inputs.state }} + BRANCH: ${{ inputs.branch }} + RULESET_NAME: ${{ inputs.ruleset-name }} + run: bash "${{ github.action_path }}/release-beta-lock.sh" diff --git a/actions/release-beta-lock/package.json b/actions/release-beta-lock/package.json new file mode 100644 index 000000000..060d7fc12 --- /dev/null +++ b/actions/release-beta-lock/package.json @@ -0,0 +1,9 @@ +{ + "name": "@elementor/release-beta-lock", + "author": "Elementor Team", + "license": "GPL-3.0-or-later", + "private": true, + "scripts": { + "test": "bash release-beta-lock.test.sh" + } +} diff --git a/actions/release-beta-lock/release-beta-lock.sh b/actions/release-beta-lock/release-beta-lock.sh new file mode 100644 index 000000000..c582a30f9 --- /dev/null +++ b/actions/release-beta-lock/release-beta-lock.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# Locks or unlocks a release branch by toggling the enforcement of a dedicated +# repository ruleset holding a single "update" rule. Creates the ruleset when missing. + +set -euo pipefail + +REPO="${GITHUB_REPOSITORY:-}" +STATE="${STATE:-}" +BRANCH="${BRANCH:-release/beta}" +export RULESET_NAME="${RULESET_NAME:-Release beta lock}" + +resolve_enforcement() { + case "$1" in + locked) echo "active" ;; + unlocked) echo "disabled" ;; + *) return 1 ;; + esac +} + +find_ruleset_id() { + gh api "repos/${REPO}/rulesets" --paginate --jq '.[] | select(.name == env.RULESET_NAME) | .id' | head -n 1 +} + +build_ruleset_payload() { + local enforcement="$1" + + jq -n \ + --arg name "$RULESET_NAME" \ + --arg ref "refs/heads/${BRANCH}" \ + --arg enforcement "$enforcement" \ + '{ + name: $name, + target: "branch", + enforcement: $enforcement, + conditions: { ref_name: { include: [$ref], exclude: [] } }, + rules: [{ type: "update" }] + }' +} + +apply_lock_state() { + local enforcement ruleset_id + + if [ -z "$REPO" ]; then + echo "::error::GITHUB_REPOSITORY is not set" + return 1 + fi + + if ! enforcement="$(resolve_enforcement "$STATE")"; then + echo "::error::Invalid state '${STATE}'. Expected 'locked' or 'unlocked'" + return 1 + fi + + if ! ruleset_id="$(find_ruleset_id)"; then + report_api_failure "list rulesets" + return 1 + fi + + if [ -z "$ruleset_id" ]; then + if ! build_ruleset_payload "$enforcement" | gh api --method POST "repos/${REPO}/rulesets" --input - >/dev/null; then + report_api_failure "create ruleset '${RULESET_NAME}'" + return 1 + fi + echo "::notice::Created ruleset '${RULESET_NAME}' - ${BRANCH} is now ${STATE}" + else + if ! build_ruleset_payload "$enforcement" | gh api --method PUT "repos/${REPO}/rulesets/${ruleset_id}" --input - >/dev/null; then + report_api_failure "update ruleset '${RULESET_NAME}' (#${ruleset_id})" + return 1 + fi + echo "::notice::Updated ruleset '${RULESET_NAME}' (#${ruleset_id}) - ${BRANCH} is now ${STATE}" + fi +} + +report_api_failure() { + local operation="$1" + + echo "::error title=Release beta lock failed::Could not ${operation}, so ${BRANCH} was NOT ${STATE}. The token needs repository Administration write permission." +} + +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then + apply_lock_state +fi diff --git a/actions/release-beta-lock/release-beta-lock.test.sh b/actions/release-beta-lock/release-beta-lock.test.sh new file mode 100644 index 000000000..7b8da4b42 --- /dev/null +++ b/actions/release-beta-lock/release-beta-lock.test.sh @@ -0,0 +1,196 @@ +#!/usr/bin/env bash +# Test suite for release-beta-lock.sh +# Run with: bash release-beta-lock.test.sh + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +FAILURES=0 +TESTS_RUN=0 +STUB_DIR="$(mktemp -d)" +trap 'rm -rf "$STUB_DIR"' EXIT + +assert_equals() { + local expected="$1" + local actual="$2" + local message="${3:-}" + + TESTS_RUN=$((TESTS_RUN + 1)) + + if [ "$expected" = "$actual" ]; then + echo " ✓ ${message}" + else + echo " ✗ ${message}" + echo " Expected: ${expected}" + echo " Got: ${actual}" + FAILURES=$((FAILURES + 1)) + fi +} + +stub_gh() { + STUB_RULESET_ID="$1" + : >"${STUB_DIR}/calls" + : >"${STUB_DIR}/payload" + + gh() { + echo "$*" >>"${STUB_DIR}/calls" + if [[ "$*" == *"--paginate"* ]]; then + [ -n "$STUB_RULESET_ID" ] && echo "$STUB_RULESET_ID" + return 0 + fi + if [[ "$*" == *"--input -"* ]]; then + cat >"${STUB_DIR}/payload" + fi + } +} + +last_call() { + tail -n 1 "${STUB_DIR}/calls" +} + +test_resolve_enforcement() { + echo "Testing resolve_enforcement:" + + source "${SCRIPT_DIR}/release-beta-lock.sh" + + assert_equals "active" "$(resolve_enforcement locked)" "locked -> active" + assert_equals "disabled" "$(resolve_enforcement unlocked)" "unlocked -> disabled" + + if resolve_enforcement "open" >/dev/null; then + assert_equals "failure" "success" "invalid state is rejected" + else + assert_equals "failure" "failure" "invalid state is rejected" + fi +} + +test_build_ruleset_payload() { + echo "Testing build_ruleset_payload:" + + BRANCH="release/beta" + source "${SCRIPT_DIR}/release-beta-lock.sh" + + local payload + payload="$(build_ruleset_payload active)" + + assert_equals "active" "$(jq -r '.enforcement' <<<"$payload")" "enforcement is set" + assert_equals "refs/heads/release/beta" "$(jq -r '.conditions.ref_name.include[0]' <<<"$payload")" "targets the branch ref" + assert_equals "update" "$(jq -r '.rules | map(.type) | join(",")' <<<"$payload")" "holds only the update rule" +} + +test_creates_ruleset_when_missing() { + echo "Testing apply_lock_state without an existing ruleset:" + + # Arrange + GITHUB_REPOSITORY="elementor/example" + STATE="locked" + source "${SCRIPT_DIR}/release-beta-lock.sh" + stub_gh "" + + # Act + apply_lock_state >/dev/null + + # Assert + assert_equals "api --method POST repos/elementor/example/rulesets --input -" "$(last_call)" "creates the ruleset" + assert_equals "active" "$(jq -r '.enforcement' "${STUB_DIR}/payload")" "created ruleset is enforced" +} + +test_updates_existing_ruleset() { + echo "Testing apply_lock_state with an existing ruleset:" + + # Arrange + GITHUB_REPOSITORY="elementor/example" + STATE="unlocked" + source "${SCRIPT_DIR}/release-beta-lock.sh" + stub_gh "42" + + # Act + apply_lock_state >/dev/null + + # Assert + assert_equals "api --method PUT repos/elementor/example/rulesets/42 --input -" "$(last_call)" "updates the existing ruleset" + assert_equals "disabled" "$(jq -r '.enforcement' "${STUB_DIR}/payload")" "ruleset enforcement is disabled" +} + +test_rejects_invalid_state() { + echo "Testing apply_lock_state with an invalid state:" + + # Arrange + GITHUB_REPOSITORY="elementor/example" + STATE="open" + source "${SCRIPT_DIR}/release-beta-lock.sh" + stub_gh "" + + # Act + local result="success" + apply_lock_state >/dev/null || result="failure" + + # Assert + assert_equals "failure" "$result" "fails on invalid state" + assert_equals "" "$(cat "${STUB_DIR}/calls")" "does not call the GitHub API" +} + +stub_failing_gh() { + FAILING_CALL="$1" + STUB_RULESET_ID="${2:-}" + + gh() { + if [[ "$*" == *"--input -"* ]]; then + cat >/dev/null + fi + if [[ "$*" == *"$FAILING_CALL"* ]]; then + return 1 + fi + if [[ "$*" == *"--paginate"* ]]; then + echo "$STUB_RULESET_ID" + fi + } +} + +test_reports_list_failure() { + echo "Testing apply_lock_state when listing rulesets fails:" + + # Arrange + GITHUB_REPOSITORY="elementor/example" + STATE="locked" + source "${SCRIPT_DIR}/release-beta-lock.sh" + stub_failing_gh "--paginate" + + # Act + local output result="success" + output="$(apply_lock_state)" || result="failure" + + # Assert + assert_equals "failure" "$result" "fails when the API call fails" + assert_equals "true" "$([[ "$output" == "::error title=Release beta lock failed::Could not list rulesets"* ]] && echo true || echo false)" "emits an error annotation" +} + +test_reports_update_failure() { + echo "Testing apply_lock_state when updating the ruleset fails:" + + # Arrange + GITHUB_REPOSITORY="elementor/example" + STATE="locked" + source "${SCRIPT_DIR}/release-beta-lock.sh" + stub_failing_gh "--method PUT" "42" + + # Act + local output result="success" + output="$(apply_lock_state)" || result="failure" + + # Assert + assert_equals "failure" "$result" "fails when the update call fails" + assert_equals "true" "$([[ "$output" == *"update ruleset 'Release beta lock' (#42), so release/beta was NOT locked"* ]] && echo true || echo false)" "error annotation names the operation and state" +} + +test_resolve_enforcement +test_build_ruleset_payload +test_creates_ruleset_when_missing +test_updates_existing_ruleset +test_rejects_invalid_state +test_reports_list_failure +test_reports_update_failure + +echo "" +echo "Tests run: ${TESTS_RUN}, Failures: ${FAILURES}" + +[ "$FAILURES" -eq 0 ] From f3a9fe2885d3ac568b6abdf0092b8ebf7ae6ec22 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 21 Oct 2026 11:18:30 +0000 Subject: [PATCH 2/2] fix: address review findings for release-beta-lock action [ED-25903] - Fix lockfile: regenerate package-lock.json with npm install to include @elementor/release-beta-lock - Fix CodeQL code injection: pass github.action_path through env var instead of inline interpolation - Fix SIGPIPE: capture gh output first, then take first line to avoid pipeline failure - Fix org-level rulesets: filter for source_type == 'Repository' to exclude org rulesets - Fix test isolation: run tests in clean subshells, prevent global variable leakage - Add new tests: default branch, missing repo, disabled enforcement, org filtering, duplicates - Fix error propagation: explicitly return gh failure code in find_ruleset_id All tests pass locally. Resolves review items 1-5. Co-authored-by: Davids Seveloff --- actions/release-beta-lock/action.yml | 3 +- .../release-beta-lock/release-beta-lock.sh | 4 +- .../release-beta-lock.test.sh | 304 ++++++++++++------ package-lock.json | 8 + 4 files changed, 213 insertions(+), 106 deletions(-) diff --git a/actions/release-beta-lock/action.yml b/actions/release-beta-lock/action.yml index 4c4ae26ad..d777abb29 100644 --- a/actions/release-beta-lock/action.yml +++ b/actions/release-beta-lock/action.yml @@ -30,4 +30,5 @@ runs: STATE: ${{ inputs.state }} BRANCH: ${{ inputs.branch }} RULESET_NAME: ${{ inputs.ruleset-name }} - run: bash "${{ github.action_path }}/release-beta-lock.sh" + ACTION_PATH: ${{ github.action_path }} + run: bash "$ACTION_PATH/release-beta-lock.sh" diff --git a/actions/release-beta-lock/release-beta-lock.sh b/actions/release-beta-lock/release-beta-lock.sh index c582a30f9..1fc747493 100644 --- a/actions/release-beta-lock/release-beta-lock.sh +++ b/actions/release-beta-lock/release-beta-lock.sh @@ -18,7 +18,9 @@ resolve_enforcement() { } find_ruleset_id() { - gh api "repos/${REPO}/rulesets" --paginate --jq '.[] | select(.name == env.RULESET_NAME) | .id' | head -n 1 + local ids + ids="$(gh api "repos/${REPO}/rulesets" --paginate --jq '.[] | select(.name == env.RULESET_NAME and .source_type == "Repository") | .id')" || return $? + echo "$ids" | head -n 1 } build_ruleset_payload() { diff --git a/actions/release-beta-lock/release-beta-lock.test.sh b/actions/release-beta-lock/release-beta-lock.test.sh index 7b8da4b42..74562f469 100644 --- a/actions/release-beta-lock/release-beta-lock.test.sh +++ b/actions/release-beta-lock/release-beta-lock.test.sh @@ -5,6 +5,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SCRIPT_PATH="${SCRIPT_DIR}/release-beta-lock.sh" FAILURES=0 TESTS_RUN=0 STUB_DIR="$(mktemp -d)" @@ -19,29 +20,89 @@ assert_equals() { if [ "$expected" = "$actual" ]; then echo " ✓ ${message}" + return 0 else echo " ✗ ${message}" echo " Expected: ${expected}" echo " Got: ${actual}" FAILURES=$((FAILURES + 1)) + return 1 fi } -stub_gh() { - STUB_RULESET_ID="$1" +# Run test in clean subshell with environment variables passed as arguments +run_isolated() { + env -i PATH="$PATH" HOME="$HOME" STUB_DIR="$STUB_DIR" SCRIPT_PATH="$SCRIPT_PATH" "$@" bash -c ' + source "$SCRIPT_PATH" + [ -f "$STUB_DIR/stub.sh" ] && source "$STUB_DIR/stub.sh" + apply_lock_state + ' +} + +# Create stub file that defines gh function +create_stub_gh() { + local stub_ruleset_id="$1" : >"${STUB_DIR}/calls" : >"${STUB_DIR}/payload" + + cat >"${STUB_DIR}/stub.sh" <<'EOF' +gh() { + echo "$*" >>"${STUB_DIR}/calls" + if [[ "$*" == *"--paginate"* ]]; then + [ -n "${STUB_RULESET_ID:-}" ] && echo "$STUB_RULESET_ID" + return 0 + fi + if [[ "$*" == *"--input -"* ]]; then + cat >"${STUB_DIR}/payload" + fi +} +EOF + echo "STUB_RULESET_ID='$stub_ruleset_id'" >>"${STUB_DIR}/stub.sh" +} - gh() { - echo "$*" >>"${STUB_DIR}/calls" - if [[ "$*" == *"--paginate"* ]]; then - [ -n "$STUB_RULESET_ID" ] && echo "$STUB_RULESET_ID" - return 0 - fi - if [[ "$*" == *"--input -"* ]]; then - cat >"${STUB_DIR}/payload" - fi - } +create_stub_gh_with_json() { + local json_response="$1" + : >"${STUB_DIR}/calls" + : >"${STUB_DIR}/payload" + + cat >"${STUB_DIR}/stub.sh" <<'EOF' +gh() { + echo "$*" >>"${STUB_DIR}/calls" + if [[ "$*" == *"--paginate"* ]]; then + echo "${STUB_JSON}" | jq -r '.[] | select(.name == env.RULESET_NAME and .source_type == "Repository") | .id' + return 0 + fi + if [[ "$*" == *"--input -"* ]]; then + cat >"${STUB_DIR}/payload" + fi +} +EOF + # Properly escape the JSON for embedding + printf 'STUB_JSON=%q\n' "$json_response" >>"${STUB_DIR}/stub.sh" +} + +create_stub_failing_gh() { + local failing_call="$1" + local stub_ruleset_id="${2:-}" + : >"${STUB_DIR}/calls" + : >"${STUB_DIR}/payload" + + cat >"${STUB_DIR}/stub.sh" </dev/null + fi + if [[ "\$*" == *"\${FAILING_CALL}"* ]]; then + return 1 + fi + if [[ "\$*" == *"--paginate"* ]]; then + [ -n "\${STUB_RULESET_ID:-}" ] && echo "\$STUB_RULESET_ID" + fi +} +EOF } last_call() { @@ -50,142 +111,177 @@ last_call() { test_resolve_enforcement() { echo "Testing resolve_enforcement:" - - source "${SCRIPT_DIR}/release-beta-lock.sh" - - assert_equals "active" "$(resolve_enforcement locked)" "locked -> active" - assert_equals "disabled" "$(resolve_enforcement unlocked)" "unlocked -> disabled" - - if resolve_enforcement "open" >/dev/null; then - assert_equals "failure" "success" "invalid state is rejected" - else - assert_equals "failure" "failure" "invalid state is rejected" - fi + + ( + unset BRANCH STATE GITHUB_REPOSITORY RULESET_NAME + source "${SCRIPT_PATH}" + + assert_equals "active" "$(resolve_enforcement locked)" "locked -> active" + assert_equals "disabled" "$(resolve_enforcement unlocked)" "unlocked -> disabled" + + if resolve_enforcement "open" >/dev/null 2>&1; then + assert_equals "failure" "success" "invalid state is rejected" + else + assert_equals "failure" "failure" "invalid state is rejected" + fi + ) } test_build_ruleset_payload() { echo "Testing build_ruleset_payload:" + + ( + unset BRANCH STATE GITHUB_REPOSITORY RULESET_NAME + BRANCH="release/beta" + source "${SCRIPT_PATH}" + + local payload + payload="$(build_ruleset_payload active)" + + assert_equals "active" "$(jq -r '.enforcement' <<<"$payload")" "enforcement is set" + assert_equals "refs/heads/release/beta" "$(jq -r '.conditions.ref_name.include[0]' <<<"$payload")" "targets the branch ref" + assert_equals "update" "$(jq -r '.rules | map(.type) | join(",")' <<<"$payload")" "holds only the update rule" + ) +} - BRANCH="release/beta" - source "${SCRIPT_DIR}/release-beta-lock.sh" - - local payload - payload="$(build_ruleset_payload active)" +test_default_branch() { + echo "Testing default branch is release/beta when BRANCH unset:" + + ( + unset BRANCH STATE GITHUB_REPOSITORY RULESET_NAME + source "${SCRIPT_PATH}" + + assert_equals "release/beta" "$BRANCH" "BRANCH defaults to release/beta" + ) +} - assert_equals "active" "$(jq -r '.enforcement' <<<"$payload")" "enforcement is set" - assert_equals "refs/heads/release/beta" "$(jq -r '.conditions.ref_name.include[0]' <<<"$payload")" "targets the branch ref" - assert_equals "update" "$(jq -r '.rules | map(.type) | join(",")' <<<"$payload")" "holds only the update rule" +test_missing_repository() { + echo "Testing missing GITHUB_REPOSITORY fails without API calls:" + + create_stub_gh "" + + local output result=0 + output="$(run_isolated STATE=locked)" || result=$? + + assert_equals "1" "$result" "fails when GITHUB_REPOSITORY is missing" + assert_equals "" "$(cat "${STUB_DIR}/calls")" "does not call the GitHub API" + [[ "$output" == *"::error::GITHUB_REPOSITORY is not set"* ]] + assert_equals "0" "$?" "emits correct error" } test_creates_ruleset_when_missing() { - echo "Testing apply_lock_state without an existing ruleset:" + echo "Testing creates ruleset when missing:" + + create_stub_gh "" - # Arrange - GITHUB_REPOSITORY="elementor/example" - STATE="locked" - source "${SCRIPT_DIR}/release-beta-lock.sh" - stub_gh "" + run_isolated GITHUB_REPOSITORY=elementor/example STATE=locked >/dev/null - # Act - apply_lock_state >/dev/null - - # Assert assert_equals "api --method POST repos/elementor/example/rulesets --input -" "$(last_call)" "creates the ruleset" assert_equals "active" "$(jq -r '.enforcement' "${STUB_DIR}/payload")" "created ruleset is enforced" } -test_updates_existing_ruleset() { - echo "Testing apply_lock_state with an existing ruleset:" +test_unlocked_creates_disabled_ruleset() { + echo "Testing unlocked with no ruleset POSTs enforcement disabled:" + + create_stub_gh "" + + run_isolated GITHUB_REPOSITORY=elementor/example STATE=unlocked >/dev/null + + assert_equals "api --method POST repos/elementor/example/rulesets --input -" "$(last_call)" "creates the ruleset" + assert_equals "disabled" "$(jq -r '.enforcement' "${STUB_DIR}/payload")" "created ruleset has enforcement disabled" +} - # Arrange - GITHUB_REPOSITORY="elementor/example" - STATE="unlocked" - source "${SCRIPT_DIR}/release-beta-lock.sh" - stub_gh "42" +test_updates_existing_ruleset() { + echo "Testing updates existing ruleset:" + + create_stub_gh "42" - # Act - apply_lock_state >/dev/null + run_isolated GITHUB_REPOSITORY=elementor/example STATE=unlocked >/dev/null - # Assert assert_equals "api --method PUT repos/elementor/example/rulesets/42 --input -" "$(last_call)" "updates the existing ruleset" assert_equals "disabled" "$(jq -r '.enforcement' "${STUB_DIR}/payload")" "ruleset enforcement is disabled" } -test_rejects_invalid_state() { - echo "Testing apply_lock_state with an invalid state:" +test_filters_org_level_rulesets() { + echo "Testing filters out non-Repository rulesets:" + + # Mock response with both org-level and repo-level rulesets with same name + local json='[ + {"id": 1, "name": "Release beta lock", "source_type": "Organization"}, + {"id": 2, "name": "Release beta lock", "source_type": "Repository"} + ]' + create_stub_gh_with_json "$json" - # Arrange - GITHUB_REPOSITORY="elementor/example" - STATE="open" - source "${SCRIPT_DIR}/release-beta-lock.sh" - stub_gh "" + run_isolated GITHUB_REPOSITORY=elementor/example STATE=locked >/dev/null - # Act - local result="success" - apply_lock_state >/dev/null || result="failure" + assert_equals "api --method PUT repos/elementor/example/rulesets/2 --input -" "$(last_call)" "uses only repo-level ruleset (id 2)" +} - # Assert - assert_equals "failure" "$result" "fails on invalid state" - assert_equals "" "$(cat "${STUB_DIR}/calls")" "does not call the GitHub API" +test_duplicate_matches_uses_first() { + echo "Testing duplicate matches use first without failing:" + + # Multiple repo-level rulesets with same name + local json='[ + {"id": 10, "name": "Release beta lock", "source_type": "Repository"}, + {"id": 11, "name": "Release beta lock", "source_type": "Repository"} + ]' + create_stub_gh_with_json "$json" + + local result=0 + run_isolated GITHUB_REPOSITORY=elementor/example STATE=locked >/dev/null || result=$? + + assert_equals "0" "$result" "does not fail on duplicate matches" + assert_equals "api --method PUT repos/elementor/example/rulesets/10 --input -" "$(last_call)" "uses first matching ruleset (id 10)" } -stub_failing_gh() { - FAILING_CALL="$1" - STUB_RULESET_ID="${2:-}" +test_rejects_invalid_state() { + echo "Testing rejects invalid state:" + + create_stub_gh "" - gh() { - if [[ "$*" == *"--input -"* ]]; then - cat >/dev/null - fi - if [[ "$*" == *"$FAILING_CALL"* ]]; then - return 1 - fi - if [[ "$*" == *"--paginate"* ]]; then - echo "$STUB_RULESET_ID" - fi - } + local result=0 + run_isolated GITHUB_REPOSITORY=elementor/example STATE=open >/dev/null 2>&1 || result=$? + + assert_equals "1" "$result" "fails on invalid state" + assert_equals "" "$(cat "${STUB_DIR}/calls")" "does not call the GitHub API" } test_reports_list_failure() { - echo "Testing apply_lock_state when listing rulesets fails:" - - # Arrange - GITHUB_REPOSITORY="elementor/example" - STATE="locked" - source "${SCRIPT_DIR}/release-beta-lock.sh" - stub_failing_gh "--paginate" + echo "Testing reports list failure:" + + create_stub_failing_gh "--paginate" - # Act - local output result="success" - output="$(apply_lock_state)" || result="failure" + local output result=0 + output="$(run_isolated GITHUB_REPOSITORY=elementor/example STATE=locked)" || result=$? - # Assert - assert_equals "failure" "$result" "fails when the API call fails" - assert_equals "true" "$([[ "$output" == "::error title=Release beta lock failed::Could not list rulesets"* ]] && echo true || echo false)" "emits an error annotation" + assert_equals "1" "$result" "fails when the API call fails" + [[ "$output" == "::error title=Release beta lock failed::Could not list rulesets"* ]] + assert_equals "0" "$?" "emits an error annotation" } test_reports_update_failure() { - echo "Testing apply_lock_state when updating the ruleset fails:" - - # Arrange - GITHUB_REPOSITORY="elementor/example" - STATE="locked" - source "${SCRIPT_DIR}/release-beta-lock.sh" - stub_failing_gh "--method PUT" "42" + echo "Testing reports update failure:" + + create_stub_failing_gh "--method PUT" "42" - # Act - local output result="success" - output="$(apply_lock_state)" || result="failure" + local output result=0 + output="$(run_isolated GITHUB_REPOSITORY=elementor/example STATE=locked)" || result=$? - # Assert - assert_equals "failure" "$result" "fails when the update call fails" - assert_equals "true" "$([[ "$output" == *"update ruleset 'Release beta lock' (#42), so release/beta was NOT locked"* ]] && echo true || echo false)" "error annotation names the operation and state" + assert_equals "1" "$result" "fails when the update call fails" + [[ "$output" == *"update ruleset 'Release beta lock' (#42), so release/beta was NOT locked"* ]] + assert_equals "0" "$?" "error annotation names the operation and state" } +# Run all tests test_resolve_enforcement test_build_ruleset_payload +test_default_branch +test_missing_repository test_creates_ruleset_when_missing +test_unlocked_creates_disabled_ruleset test_updates_existing_ruleset +test_filters_org_level_rulesets +test_duplicate_matches_uses_first test_rejects_invalid_state test_reports_list_failure test_reports_update_failure diff --git a/package-lock.json b/package-lock.json index 54fbcf43c..44d742d52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,6 +53,10 @@ "tsup": "^8.5.0" } }, + "actions/release-beta-lock": { + "name": "@elementor/release-beta-lock", + "license": "GPL-3.0-or-later" + }, "actions/release-tag-creation": { "name": "@elementor/release-tag-creation", "license": "GPL-3.0-or-later", @@ -727,6 +731,10 @@ "resolved": "actions/post-release-verify", "link": true }, + "node_modules/@elementor/release-beta-lock": { + "resolved": "actions/release-beta-lock", + "link": true + }, "node_modules/@elementor/release-tag-creation": { "resolved": "actions/release-tag-creation", "link": true