From aaa7cd4b99978d9a6d4b3587cb0b94d51697ae6c Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 15 Sep 2026 18:25:32 -0300 Subject: [PATCH 01/24] refactor: harden ruleset synchronization --- scripts/sync-rulesets.sh | 228 +++++++++++++++++++++++++++++++-------- 1 file changed, 182 insertions(+), 46 deletions(-) diff --git a/scripts/sync-rulesets.sh b/scripts/sync-rulesets.sh index 06967c1..8d9046b 100755 --- a/scripts/sync-rulesets.sh +++ b/scripts/sync-rulesets.sh @@ -1,67 +1,203 @@ #!/usr/bin/env bash set -euo pipefail -ORG="${ORG:?ORG must be set}" -RULESET_FILE="${RULESET_FILE:-.github/rulesets/default-branches.json}" -RULESET_NAME="$(jq -r '.name' "$RULESET_FILE")" -NEXTCLOUD_BOT="${NEXTCLOUD_BOT:-nextcloud-bot}" -NEXTCLOUD_BOT_ID="$(gh api "users/$NEXTCLOUD_BOT" --jq '.id')" - -gh api \ - --paginate \ - '/installation/repositories?per_page=100' \ - --jq '.repositories[] | - select(.owner.login == "'"$ORG"'") | - select(.archived == false) | - select(.visibility == "public") | - .name' | -while read -r repo; do - echo "=== $ORG/$repo ===" +DEFAULT_RULESET_FILE=".github/rulesets/default-branches.json" +NEXTCLOUD_BOT="nextcloud-bot" +NEXTCLOUD_BOT_ID="20296731" +CHECK_ONLY=false +TARGET_REPOSITORY="" + +usage() { + cat <<'EOF' +Usage: sync-rulesets.sh [--check] [--repo OWNER/REPO] + +Options: + --check Report drift without changing repository rulesets. + --repo OWNER/REPO Sync only one repository instead of all public repositories. + -h, --help Show this help. +EOF +} + +parse_args() { + while [ "$#" -gt 0 ]; do + case "$1" in + --check) + CHECK_ONLY=true + ;; + --repo) + [ "$#" -ge 2 ] || { echo "--repo requires OWNER/REPO" >&2; return 2; } + TARGET_REPOSITORY="$2" + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown argument: $1" >&2 + usage >&2 + return 2 + ;; + esac + shift + done +} + +list_repositories() { + if [ -n "$TARGET_REPOSITORY" ]; then + case "$TARGET_REPOSITORY" in + "$ORG"/*) printf '%s\n' "${TARGET_REPOSITORY#*/}" ;; + *) echo "Repository must belong to $ORG: $TARGET_REPOSITORY" >&2; return 2 ;; + esac + return + fi + + gh api \ + --paginate \ + '/installation/repositories?per_page=100' \ + --jq '.repositories[] | + select(.owner.login == "'"$ORG"'") | + select(.archived == false) | + select(.visibility == "public") | + .name' +} - effective_ruleset_file="$RULESET_FILE" - temporary_ruleset_file="" +is_nextcloud_app() { + local repo="$1" + local error_file + error_file="$(mktemp)" - if gh api "repos/$ORG/$repo/contents/appinfo/info.xml" --silent >/dev/null 2>&1; then - echo "Nextcloud app detected; allowing $NEXTCLOUD_BOT to bypass the ruleset" + if gh api "repos/$ORG/$repo/contents/appinfo/info.xml" --silent >/dev/null 2>"$error_file"; then + rm -f "$error_file" + return 0 + fi + + if grep -q 'HTTP 404' "$error_file"; then + rm -f "$error_file" + return 1 + fi - temporary_ruleset_file="$(mktemp)" + echo "Failed to detect whether $ORG/$repo is a Nextcloud app:" >&2 + cat "$error_file" >&2 + rm -f "$error_file" + return 2 +} + +build_ruleset() { + local repo="$1" + local detection_status=0 + + if is_nextcloud_app "$repo"; then + echo "Nextcloud app detected; allowing $NEXTCLOUD_BOT to bypass the ruleset" >&2 jq \ --argjson bot_id "$NEXTCLOUD_BOT_ID" \ - '.bypass_actors += [{ - actor_id: $bot_id, - actor_type: "User", - bypass_mode: "always" - }]' \ - "$RULESET_FILE" > "$temporary_ruleset_file" - effective_ruleset_file="$temporary_ruleset_file" + '.bypass_actors = ( + .bypass_actors + + [{actor_id: $bot_id, actor_type: "User", bypass_mode: "always"}] + | unique_by([.actor_type, .actor_id]) + )' \ + "$RULESET_FILE" + return + else + detection_status=$? + fi + + if [ "$detection_status" -eq 1 ]; then + cat "$RULESET_FILE" + return fi - ruleset_id="$( - gh api "repos/$ORG/$repo/rulesets" \ - --jq ".[] | select(.name == \"$RULESET_NAME\") | .id" \ - 2>/dev/null | - head -n 1 - )" + return "$detection_status" +} + +find_ruleset_id() { + local repo="$1" + gh api "repos/$ORG/$repo/rulesets" \ + --jq '.[] | select(.name == $name) | .id' \ + -f name="$RULESET_NAME" \ + 2>/dev/null | + head -n 1 +} + +normalize_ruleset() { + jq -S '{name, target, enforcement, bypass_actors, conditions, rules}' +} + +ruleset_has_drift() { + local repo="$1" + local ruleset_id="$2" + local desired_file="$3" + local current desired + + current="$(gh api "repos/$ORG/$repo/rulesets/$ruleset_id" | normalize_ruleset)" + desired="$(normalize_ruleset < "$desired_file")" + + [ "$current" != "$desired" ] +} + +sync_repository() { + local repo="$1" + local desired_file ruleset_id + + echo "=== $ORG/$repo ===" + + desired_file="$(mktemp)" + trap 'rm -f "$desired_file"' RETURN + build_ruleset "$repo" > "$desired_file" + + ruleset_id="$(find_ruleset_id "$repo")" if [ -z "$ruleset_id" ]; then - echo "Creating ruleset" + if [ "$CHECK_ONLY" = true ]; then + echo "DRIFT: ruleset is missing" + return 1 + fi + echo "Creating ruleset" gh api \ --method POST \ "repos/$ORG/$repo/rulesets" \ -H 'Accept: application/vnd.github+json' \ - --input "$effective_ruleset_file" - else - echo "Updating ruleset $ruleset_id" + --input "$desired_file" + return + fi - gh api \ - --method PUT \ - "repos/$ORG/$repo/rulesets/$ruleset_id" \ - -H 'Accept: application/vnd.github+json' \ - --input "$effective_ruleset_file" + if ! ruleset_has_drift "$repo" "$ruleset_id" "$desired_file"; then + echo "OK: ruleset is up to date" + return fi - if [ -n "$temporary_ruleset_file" ]; then - rm -f "$temporary_ruleset_file" + if [ "$CHECK_ONLY" = true ]; then + echo "DRIFT: ruleset differs from desired configuration" + return 1 fi -done + + echo "Updating ruleset $ruleset_id" + gh api \ + --method PUT \ + "repos/$ORG/$repo/rulesets/$ruleset_id" \ + -H 'Accept: application/vnd.github+json' \ + --input "$desired_file" +} + +main() { + ORG="${ORG:?ORG must be set}" + RULESET_FILE="${RULESET_FILE:-$DEFAULT_RULESET_FILE}" + RULESET_NAME="$(jq -r '.name' "$RULESET_FILE")" + + parse_args "$@" + + local failed=0 repo + while read -r repo; do + [ -n "$repo" ] || continue + if ! sync_repository "$repo"; then + failed=1 + fi + done < <(list_repositories) + + return "$failed" +} + +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then + main "$@" +fi From aef59672df56ef538a72fbf0c089e7aee6b41430 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 15 Sep 2026 18:25:56 -0300 Subject: [PATCH 02/24] fix: compare effective ruleset configuration --- scripts/sync-rulesets.sh | 55 +++++++++++++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/scripts/sync-rulesets.sh b/scripts/sync-rulesets.sh index 8d9046b..5730b91 100755 --- a/scripts/sync-rulesets.sh +++ b/scripts/sync-rulesets.sh @@ -6,6 +6,14 @@ NEXTCLOUD_BOT="nextcloud-bot" NEXTCLOUD_BOT_ID="20296731" CHECK_ONLY=false TARGET_REPOSITORY="" +TEMP_FILES=() + +cleanup() { + if [ "${#TEMP_FILES[@]}" -gt 0 ]; then + rm -f "${TEMP_FILES[@]}" + fi +} +trap cleanup EXIT usage() { cat <<'EOF' @@ -66,20 +74,18 @@ is_nextcloud_app() { local repo="$1" local error_file error_file="$(mktemp)" + TEMP_FILES+=("$error_file") if gh api "repos/$ORG/$repo/contents/appinfo/info.xml" --silent >/dev/null 2>"$error_file"; then - rm -f "$error_file" return 0 fi if grep -q 'HTTP 404' "$error_file"; then - rm -f "$error_file" return 1 fi echo "Failed to detect whether $ORG/$repo is a Nextcloud app:" >&2 cat "$error_file" >&2 - rm -f "$error_file" return 2 } @@ -112,15 +118,46 @@ build_ruleset() { find_ruleset_id() { local repo="$1" - gh api "repos/$ORG/$repo/rulesets" \ - --jq '.[] | select(.name == $name) | .id' \ - -f name="$RULESET_NAME" \ - 2>/dev/null | + gh api "repos/$ORG/$repo/rulesets" 2>/dev/null | + jq -r --arg name "$RULESET_NAME" '.[] | select(.name == $name) | .id' | head -n 1 } normalize_ruleset() { - jq -S '{name, target, enforcement, bypass_actors, conditions, rules}' + jq -S ' + { + name, + target, + enforcement, + bypass_actors: [ + .bypass_actors[] | + { + actor_id: (if .actor_type == "OrganizationAdmin" then null else .actor_id end), + actor_type, + bypass_mode + } + ] | sort_by([.actor_type, .actor_id]), + conditions, + rules: [ + .rules[] | + if .type == "pull_request" then + { + type, + parameters: { + allowed_merge_methods: .parameters.allowed_merge_methods, + dismiss_stale_reviews_on_push: .parameters.dismiss_stale_reviews_on_push, + require_code_owner_review: .parameters.require_code_owner_review, + require_last_push_approval: .parameters.require_last_push_approval, + required_approving_review_count: .parameters.required_approving_review_count, + required_review_thread_resolution: .parameters.required_review_thread_resolution + } + } + else + {type} + end + ] + } + ' } ruleset_has_drift() { @@ -142,7 +179,7 @@ sync_repository() { echo "=== $ORG/$repo ===" desired_file="$(mktemp)" - trap 'rm -f "$desired_file"' RETURN + TEMP_FILES+=("$desired_file") build_ruleset "$repo" > "$desired_file" ruleset_id="$(find_ruleset_id "$repo")" From 097994ad85343990f97a1568088161d393199ae5 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 15 Sep 2026 18:26:26 -0300 Subject: [PATCH 03/24] fix: fail closed on repository API errors --- scripts/sync-rulesets.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/sync-rulesets.sh b/scripts/sync-rulesets.sh index 5730b91..941f7a4 100755 --- a/scripts/sync-rulesets.sh +++ b/scripts/sync-rulesets.sh @@ -118,7 +118,7 @@ build_ruleset() { find_ruleset_id() { local repo="$1" - gh api "repos/$ORG/$repo/rulesets" 2>/dev/null | + gh api "repos/$ORG/$repo/rulesets" | jq -r --arg name "$RULESET_NAME" '.[] | select(.name == $name) | .id' | head -n 1 } @@ -224,13 +224,15 @@ main() { parse_args "$@" - local failed=0 repo + local failed=0 repo repositories + repositories="$(list_repositories)" + while read -r repo; do [ -n "$repo" ] || continue if ! sync_repository "$repo"; then failed=1 fi - done < <(list_repositories) + done <<< "$repositories" return "$failed" } From 12d17ce137978a50125bb870e362f1f1b7a25aac Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 15 Sep 2026 18:26:42 -0300 Subject: [PATCH 04/24] test: cover ruleset synchronization behavior --- tests/sync-rulesets.bats | 108 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 tests/sync-rulesets.bats diff --git a/tests/sync-rulesets.bats b/tests/sync-rulesets.bats new file mode 100644 index 0000000..0041c81 --- /dev/null +++ b/tests/sync-rulesets.bats @@ -0,0 +1,108 @@ +#!/usr/bin/env bats + +setup() { + REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/.." && pwd)" + # shellcheck source=../scripts/sync-rulesets.sh + source "$REPO_ROOT/scripts/sync-rulesets.sh" + + ORG="LibreSign" + RULESET_FILE="$REPO_ROOT/.github/rulesets/default-branches.json" + RULESET_NAME="$(jq -r '.name' "$RULESET_FILE")" + CHECK_ONLY=false + TARGET_REPOSITORY="" + TEMP_FILES=() +} + +@test "Nextcloud apps receive the pinned nextcloud-bot bypass" { + gh() { + return 0 + } + + result="$(build_ruleset libresign 2>/dev/null)" + + [ "$(jq '[.bypass_actors[] | select(.actor_type == "User" and .actor_id == 20296731 and .bypass_mode == "always")] | length' <<< "$result")" -eq 1 ] +} + +@test "non-Nextcloud repositories do not receive the bot bypass" { + gh() { + echo "gh: Not Found (HTTP 404)" >&2 + return 1 + } + + result="$(build_ruleset docs)" + + [ "$(jq '[.bypass_actors[] | select(.actor_type == "User" and .actor_id == 20296731)] | length' <<< "$result")" -eq 0 ] +} + +@test "unexpected errors while detecting a Nextcloud app fail closed" { + gh() { + echo "gh: Resource not accessible (HTTP 403)" >&2 + return 1 + } + + run build_ruleset libresign + + [ "$status" -eq 2 ] + [[ "$output" == *"Failed to detect whether LibreSign/libresign is a Nextcloud app"* ]] +} + +@test "server errors while detecting a Nextcloud app fail closed" { + gh() { + echo "gh: Server Error (HTTP 500)" >&2 + return 1 + } + + run build_ruleset libresign + + [ "$status" -eq 2 ] +} + +@test "adding the bot bypass is idempotent" { + fixture="$(mktemp)" + TEMP_FILES+=("$fixture") + jq '.bypass_actors += [{actor_id: 20296731, actor_type: "User", bypass_mode: "always"}]' \ + "$RULESET_FILE" > "$fixture" + RULESET_FILE="$fixture" + + gh() { + return 0 + } + + result="$(build_ruleset libresign 2>/dev/null)" + + [ "$(jq '[.bypass_actors[] | select(.actor_type == "User" and .actor_id == 20296731)] | length' <<< "$result")" -eq 1 ] +} + +@test "repository targeting only accepts repositories from the selected organization" { + TARGET_REPOSITORY="LibreCodeCoop/profile_fields" + + run list_repositories + + [ "$status" -eq 2 ] + [[ "$output" == *"Repository must belong to LibreSign"* ]] +} + +@test "repository targeting returns only the selected repository name" { + TARGET_REPOSITORY="LibreSign/libresign" + + run list_repositories + + [ "$status" -eq 0 ] + [ "$output" = "libresign" ] +} + +@test "normalization ignores GitHub API defaults not managed by this policy" { + desired="$(normalize_ruleset < "$RULESET_FILE")" + current="$( + jq ' + .bypass_actors[0].actor_id = null | + (.rules[] | select(.type == "pull_request") | .parameters) += { + required_reviewers: [], + dismissal_restriction: {enabled: false, allowed_actors: []}, + require_extra_approval_for_unattributed_changes: true + } + ' "$RULESET_FILE" | normalize_ruleset + )" + + [ "$current" = "$desired" ] +} From 9bd7972a3120348881ed0b3eba3c15e6e35a6842 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 15 Sep 2026 18:26:50 -0300 Subject: [PATCH 05/24] test: assert ruleset policy invariants --- tests/ruleset-policy.bats | 52 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 tests/ruleset-policy.bats diff --git a/tests/ruleset-policy.bats b/tests/ruleset-policy.bats new file mode 100644 index 0000000..02e7609 --- /dev/null +++ b/tests/ruleset-policy.bats @@ -0,0 +1,52 @@ +#!/usr/bin/env bats + +setup() { + REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/.." && pwd)" + RULESET_FILE="$REPO_ROOT/.github/rulesets/default-branches.json" +} + +@test "ruleset JSON is valid" { + run jq -e . "$RULESET_FILE" + [ "$status" -eq 0 ] +} + +@test "ruleset is active and protects default and stable branches" { + run jq -e ' + .enforcement == "active" and + (.conditions.ref_name.include | index("~DEFAULT_BRANCH") != null) and + (.conditions.ref_name.include | index("refs/heads/stable*") != null) + ' "$RULESET_FILE" + + [ "$status" -eq 0 ] +} + +@test "ruleset prevents deletion and non-fast-forward updates" { + run jq -e ' + ([.rules[].type] | index("deletion") != null) and + ([.rules[].type] | index("non_fast_forward") != null) + ' "$RULESET_FILE" + + [ "$status" -eq 0 ] +} + +@test "pull requests require approval, CODEOWNERS and resolved threads" { + run jq -e ' + .rules[] | + select(.type == "pull_request") | + .parameters.required_approving_review_count >= 1 and + .parameters.require_code_owner_review == true and + .parameters.required_review_thread_resolution == true + ' "$RULESET_FILE" + + [ "$status" -eq 0 ] +} + +@test "organization admins can bypass only through pull requests" { + run jq -e ' + [.bypass_actors[] | + select(.actor_type == "OrganizationAdmin" and .bypass_mode == "pull_request") + ] | length == 1 + ' "$RULESET_FILE" + + [ "$status" -eq 0 ] +} From 2e39dd301fc8f71168e3069cbf9a7afcf11e6888 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 15 Sep 2026 18:27:15 -0300 Subject: [PATCH 06/24] fix: propagate synchronization failures explicitly --- scripts/sync-rulesets.sh | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/scripts/sync-rulesets.sh b/scripts/sync-rulesets.sh index 941f7a4..79a116f 100755 --- a/scripts/sync-rulesets.sh +++ b/scripts/sync-rulesets.sh @@ -166,23 +166,32 @@ ruleset_has_drift() { local desired_file="$3" local current desired - current="$(gh api "repos/$ORG/$repo/rulesets/$ruleset_id" | normalize_ruleset)" - desired="$(normalize_ruleset < "$desired_file")" + if ! current="$(gh api "repos/$ORG/$repo/rulesets/$ruleset_id" | normalize_ruleset)"; then + return 2 + fi + if ! desired="$(normalize_ruleset < "$desired_file")"; then + return 2 + fi [ "$current" != "$desired" ] } sync_repository() { local repo="$1" - local desired_file ruleset_id + local desired_file ruleset_id drift_status echo "=== $ORG/$repo ===" desired_file="$(mktemp)" TEMP_FILES+=("$desired_file") - build_ruleset "$repo" > "$desired_file" + if ! build_ruleset "$repo" > "$desired_file"; then + return $? + fi - ruleset_id="$(find_ruleset_id "$repo")" + if ! ruleset_id="$(find_ruleset_id "$repo")"; then + echo "Failed to read rulesets for $ORG/$repo" >&2 + return 2 + fi if [ -z "$ruleset_id" ]; then if [ "$CHECK_ONLY" = true ]; then @@ -199,7 +208,18 @@ sync_repository() { return fi - if ! ruleset_has_drift "$repo" "$ruleset_id" "$desired_file"; then + if ruleset_has_drift "$repo" "$ruleset_id" "$desired_file"; then + drift_status=0 + else + drift_status=$? + fi + + if [ "$drift_status" -eq 2 ]; then + echo "Failed to compare ruleset for $ORG/$repo" >&2 + return 2 + fi + + if [ "$drift_status" -eq 1 ]; then echo "OK: ruleset is up to date" return fi @@ -225,7 +245,9 @@ main() { parse_args "$@" local failed=0 repo repositories - repositories="$(list_repositories)" + if ! repositories="$(list_repositories)"; then + return $? + fi while read -r repo; do [ -n "$repo" ] || continue From 69e2ce16fbd7ef85f62a460ac1994bc10d54ec28 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 15 Sep 2026 18:27:32 -0300 Subject: [PATCH 07/24] fix: preserve command failure status --- scripts/sync-rulesets.sh | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/scripts/sync-rulesets.sh b/scripts/sync-rulesets.sh index 79a116f..6ded420 100755 --- a/scripts/sync-rulesets.sh +++ b/scripts/sync-rulesets.sh @@ -178,19 +178,25 @@ ruleset_has_drift() { sync_repository() { local repo="$1" - local desired_file ruleset_id drift_status + local desired_file ruleset_id drift_status status echo "=== $ORG/$repo ===" desired_file="$(mktemp)" TEMP_FILES+=("$desired_file") - if ! build_ruleset "$repo" > "$desired_file"; then - return $? + if build_ruleset "$repo" > "$desired_file"; then + : + else + status=$? + return "$status" fi - if ! ruleset_id="$(find_ruleset_id "$repo")"; then + if ruleset_id="$(find_ruleset_id "$repo")"; then + : + else + status=$? echo "Failed to read rulesets for $ORG/$repo" >&2 - return 2 + return "$status" fi if [ -z "$ruleset_id" ]; then @@ -244,9 +250,12 @@ main() { parse_args "$@" - local failed=0 repo repositories - if ! repositories="$(list_repositories)"; then - return $? + local failed=0 repo repositories status + if repositories="$(list_repositories)"; then + : + else + status=$? + return "$status" fi while read -r repo; do From e32c4bdbf0f134d4d7eb81719b9e9f12202a4eca Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 15 Sep 2026 18:27:44 -0300 Subject: [PATCH 08/24] ci: add tests and security checks --- .github/workflows/quality.yml | 66 +++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 .github/workflows/quality.yml diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 0000000..e8fb3c1 --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,66 @@ +name: Quality checks + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + tests: + name: Bash tests and policy checks + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + with: + persist-credentials: false + + - name: Install test dependencies + run: | + sudo apt-get update + sudo apt-get install --yes bats shellcheck + + - name: ShellCheck + run: shellcheck scripts/*.sh + + - name: Bats + run: bats tests + + actionlint: + name: GitHub Actions lint + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + with: + persist-credentials: false + + - name: Run actionlint + uses: raven-actions/actionlint@3d39aea434753780c3b3d4a1a31c854b4dbf49d7 # v2.2.0 + with: + shellcheck: true + + zizmor: + name: GitHub Actions security + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + with: + persist-credentials: false + + - name: Run zizmor + uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4 + with: + advanced-security: false + online-audits: false From 15375df9103de654671596b0c3bcf51f98d51634 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 15 Sep 2026 18:27:52 -0300 Subject: [PATCH 09/24] ci: serialize privileged ruleset syncs --- .github/workflows/sync-rulesets.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/sync-rulesets.yml b/.github/workflows/sync-rulesets.yml index e4443a8..9374ba1 100644 --- a/.github/workflows/sync-rulesets.yml +++ b/.github/workflows/sync-rulesets.yml @@ -8,10 +8,15 @@ on: permissions: contents: read +concurrency: + group: sync-rulesets + cancel-in-progress: false + jobs: sync: name: Sync rulesets for ${{ matrix.organization }} runs-on: ubuntu-latest + timeout-minutes: 15 environment: ruleset-sync strategy: From d21b3fab88d3b5c9c26673eba7ae9b7a129f6d35 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 15 Sep 2026 18:27:59 -0300 Subject: [PATCH 10/24] ci: enable Dependabot for GitHub Actions --- .github/dependabot.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..6c5049e --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + groups: + github-actions: + patterns: + - "*" From e3877bc97d5beef4c0ddf392e71b67dd2065e0a6 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 15 Sep 2026 18:28:12 -0300 Subject: [PATCH 11/24] docs: document ruleset automation --- README.md | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a46ae92..4d286fe 100644 --- a/README.md +++ b/README.md @@ -1 +1,55 @@ -# .github \ No newline at end of file +# LibreSign organization automation + +This repository contains shared organization metadata and automation used to keep repository rulesets consistent across LibreSign and LibreCodeCoop public repositories. + +## Ruleset synchronization + +`.github/rulesets/default-branches.json` is the source of truth for the default repository ruleset. The scheduled workflow `.github/workflows/sync-rulesets.yml` applies it to public, non-archived repositories that are available to the ruleset GitHub App installation. + +The ruleset protects the default branch and `stable*` branches. It prevents deletion and non-fast-forward updates and requires pull requests, one approval, CODEOWNERS approval and resolved review threads. + +### Nextcloud apps + +A repository is considered a Nextcloud app when `appinfo/info.xml` exists in its default branch. + +For these repositories, the sync script adds the `nextcloud-bot` GitHub user to the ruleset bypass actors with `bypass_mode: always`. The actor is pinned by the stable GitHub user ID `20296731`; the username is used only for documentation and log messages. + +A `404` while checking `appinfo/info.xml` means the repository is not a Nextcloud app. Other API errors abort synchronization for that repository so a temporary authorization or GitHub API failure cannot silently remove the bot bypass. + +## Running the synchronization + +The normal command synchronizes every public, non-archived repository for the selected organization: + +```bash +ORG=LibreSign ./scripts/sync-rulesets.sh +``` + +To inspect drift without modifying any repository: + +```bash +ORG=LibreSign ./scripts/sync-rulesets.sh --check +``` + +To limit the operation to one repository: + +```bash +ORG=LibreSign ./scripts/sync-rulesets.sh --repo LibreSign/libresign +``` + +The command requires `gh`, `jq` and a GitHub token with repository administration permission. The scheduled workflow generates a short-lived GitHub App token with only `administration: write` in addition to the workflow's read-only contents permission. + +## Tests + +The shell behavior and policy invariants are tested with Bats: + +```bash +bats tests +``` + +Shell scripts are checked with ShellCheck. GitHub Actions workflows are checked with actionlint and zizmor in `.github/workflows/quality.yml`. + +The pull request quality workflow does not use the ruleset GitHub App credentials and has only `contents: read` permission. + +## GitHub Actions dependencies + +Third-party actions are pinned to full commit SHAs. Dependabot is configured to propose weekly GitHub Actions updates so immutable pins can stay current. From 9ad421840dad304006d843ef07e2e722e41c86f4 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 15 Sep 2026 18:29:06 -0300 Subject: [PATCH 12/24] security: scope ruleset tokens per repository --- .github/workflows/sync-rulesets.yml | 48 ++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/.github/workflows/sync-rulesets.yml b/.github/workflows/sync-rulesets.yml index 9374ba1..912c8c6 100644 --- a/.github/workflows/sync-rulesets.yml +++ b/.github/workflows/sync-rulesets.yml @@ -13,18 +13,48 @@ concurrency: cancel-in-progress: false jobs: + discover: + name: Discover public repositories + runs-on: ubuntu-latest + timeout-minutes: 10 + outputs: + repositories: ${{ steps.repositories.outputs.repositories }} + + steps: + - name: List repositories + id: repositories + env: + GH_TOKEN: ${{ github.token }} + run: | + repositories="$( + for organization in LibreSign LibreCodeCoop; do + gh api \ + --paginate \ + "orgs/$organization/repos?type=public&per_page=100" \ + --jq '.[] | select(.archived == false) | [.owner.login, .name] | @tsv' + done | + jq -Rsc ' + split("\n") | + map( + select(length > 0) | + split("\t") | + {organization: .[0], repository: .[1]} + ) + ' + )" + echo "repositories=$repositories" >> "$GITHUB_OUTPUT" + sync: - name: Sync rulesets for ${{ matrix.organization }} + name: Sync ${{ matrix.organization }}/${{ matrix.repository }} + needs: discover runs-on: ubuntu-latest - timeout-minutes: 15 + timeout-minutes: 10 environment: ruleset-sync strategy: fail-fast: false matrix: - organization: - - LibreSign - - LibreCodeCoop + include: ${{ fromJSON(needs.discover.outputs.repositories) }} steps: - name: Checkout @@ -32,17 +62,19 @@ jobs: with: persist-credentials: false - - name: Generate GitHub App token + - name: Generate repository-scoped GitHub App token id: app-token uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3 with: client-id: ${{ vars.RULESET_APP_CLIENT_ID }} private-key: ${{ secrets.RULESET_APP_PRIVATE_KEY }} owner: ${{ matrix.organization }} + repositories: ${{ matrix.repository }} permission-administration: write - - name: Sync rulesets + - name: Sync ruleset env: GH_TOKEN: ${{ steps.app-token.outputs.token }} ORG: ${{ matrix.organization }} - run: ./scripts/sync-rulesets.sh + TARGET_REPOSITORY: ${{ format('{0}/{1}', matrix.organization, matrix.repository) }} + run: ./scripts/sync-rulesets.sh --repo "$TARGET_REPOSITORY" From abb801a913e65359e1e0e9b244c993a6e035fd28 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 15 Sep 2026 18:29:17 -0300 Subject: [PATCH 13/24] security: add Dependabot update cooldown --- .github/dependabot.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 6c5049e..7bb64f3 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,6 +4,8 @@ updates: directory: / schedule: interval: weekly + cooldown: + default-days: 7 groups: github-actions: patterns: From 3241a21652ae588f032dae916248814de5703502 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 15 Sep 2026 18:29:33 -0300 Subject: [PATCH 14/24] security: pin workflow analysis tool versions --- .github/workflows/quality.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index e8fb3c1..057d446 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -46,6 +46,7 @@ jobs: - name: Run actionlint uses: raven-actions/actionlint@3d39aea434753780c3b3d4a1a31c854b4dbf49d7 # v2.2.0 with: + version: 1.7.12 shellcheck: true zizmor: @@ -62,5 +63,6 @@ jobs: - name: Run zizmor uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4 with: + version: 1.30.1 advanced-security: false online-audits: false From 87634972c1244063d10e9586dc9159b35238141b Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 15 Sep 2026 18:36:33 -0300 Subject: [PATCH 15/24] ci: split quality checks by concern --- .github/workflows/quality.yml | 68 ----------------------------------- 1 file changed, 68 deletions(-) delete mode 100644 .github/workflows/quality.yml diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml deleted file mode 100644 index 057d446..0000000 --- a/.github/workflows/quality.yml +++ /dev/null @@ -1,68 +0,0 @@ -name: Quality checks - -on: - pull_request: - push: - branches: - - main - -permissions: - contents: read - -jobs: - tests: - name: Bash tests and policy checks - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - name: Checkout - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 - with: - persist-credentials: false - - - name: Install test dependencies - run: | - sudo apt-get update - sudo apt-get install --yes bats shellcheck - - - name: ShellCheck - run: shellcheck scripts/*.sh - - - name: Bats - run: bats tests - - actionlint: - name: GitHub Actions lint - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - name: Checkout - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 - with: - persist-credentials: false - - - name: Run actionlint - uses: raven-actions/actionlint@3d39aea434753780c3b3d4a1a31c854b4dbf49d7 # v2.2.0 - with: - version: 1.7.12 - shellcheck: true - - zizmor: - name: GitHub Actions security - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - name: Checkout - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 - with: - persist-credentials: false - - - name: Run zizmor - uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4 - with: - version: 1.30.1 - advanced-security: false - online-audits: false From 00f1e68cc6140bae3937b6bebda89297a1ceec9f Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 15 Sep 2026 18:36:41 -0300 Subject: [PATCH 16/24] ci: add dedicated test workflow --- .github/workflows/test.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..e7ba076 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,30 @@ +name: Tests + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + bats: + name: Bash tests and policy checks + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + with: + persist-credentials: false + + - name: Install Bats + run: | + sudo apt-get update + sudo apt-get install --yes bats + + - name: Run Bats + run: bats tests From 88bef9ac4ac4ab490e70ba8d26ea9946e9f2e97b Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 15 Sep 2026 18:36:46 -0300 Subject: [PATCH 17/24] ci: add dedicated shellcheck workflow --- .github/workflows/shellcheck.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/shellcheck.yml diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml new file mode 100644 index 0000000..464b7f4 --- /dev/null +++ b/.github/workflows/shellcheck.yml @@ -0,0 +1,25 @@ +name: ShellCheck + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + shellcheck: + name: ShellCheck + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + with: + persist-credentials: false + + - name: Run ShellCheck + run: shellcheck scripts/*.sh From 61d29b1e2b90b8919a7777af2dcbdbae27b20b15 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 15 Sep 2026 18:36:50 -0300 Subject: [PATCH 18/24] ci: add dedicated actionlint workflow --- .github/workflows/actionlint.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/actionlint.yml diff --git a/.github/workflows/actionlint.yml b/.github/workflows/actionlint.yml new file mode 100644 index 0000000..7a92aab --- /dev/null +++ b/.github/workflows/actionlint.yml @@ -0,0 +1,28 @@ +name: actionlint + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + actionlint: + name: actionlint + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + with: + persist-credentials: false + + - name: Run actionlint + uses: raven-actions/actionlint@3d39aea434753780c3b3d4a1a31c854b4dbf49d7 # v2.2.0 + with: + version: 1.7.12 + shellcheck: true From 53500d549b722672bc2c5e2906dabb0160e19993 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 15 Sep 2026 18:36:56 -0300 Subject: [PATCH 19/24] ci: add dedicated zizmor workflow --- .github/workflows/zizmor.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/zizmor.yml diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 0000000..23a6742 --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,29 @@ +name: zizmor + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + zizmor: + name: zizmor + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + with: + persist-credentials: false + + - name: Run zizmor + uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4 + with: + version: 1.30.1 + advanced-security: false + online-audits: false From bda38d8bb0fd92d02b9dbf5a8442315b19c6bbeb Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 15 Sep 2026 18:37:03 -0300 Subject: [PATCH 20/24] docs: move ruleset sync details out of README --- docs/ruleset-sync.md | 45 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 docs/ruleset-sync.md diff --git a/docs/ruleset-sync.md b/docs/ruleset-sync.md new file mode 100644 index 0000000..dcf75fe --- /dev/null +++ b/docs/ruleset-sync.md @@ -0,0 +1,45 @@ +# Ruleset synchronization + +This repository keeps branch protection rulesets consistent across public repositories in the LibreSign and LibreCodeCoop organizations. + +## Source of truth + +`.github/rulesets/default-branches.json` defines the desired ruleset for default branches and `stable*` branches. + +The synchronization script applies this policy to public, non-archived repositories available to the ruleset GitHub App installation. + +## Nextcloud apps + +A repository is treated as a Nextcloud app when `appinfo/info.xml` exists in its default branch. + +For these repositories, the synchronization adds `nextcloud-bot` as a bypass actor with `bypass_mode: always`. The GitHub actor is pinned by user ID `20296731`. + +A `404` while checking `appinfo/info.xml` means the repository is not a Nextcloud app. Other API errors abort synchronization so transient failures cannot silently remove the bot bypass. + +## Running locally + +Synchronize all public, non-archived repositories in an organization: + +```bash +ORG=LibreSign ./scripts/sync-rulesets.sh +``` + +Check for drift without modifying repositories: + +```bash +ORG=LibreSign ./scripts/sync-rulesets.sh --check +``` + +Limit synchronization to one repository: + +```bash +ORG=LibreSign ./scripts/sync-rulesets.sh --repo LibreSign/libresign +``` + +The command requires `gh`, `jq`, and a GitHub token with repository administration permission. + +## Privileged workflow + +`.github/workflows/sync-rulesets.yml` discovers public repositories with the read-only workflow token. It then creates one short-lived GitHub App token per repository, scoped to that repository with `administration: write`, and runs the sync using `--repo`. + +The workflow serializes synchronization runs and has execution timeouts to reduce the risk of concurrent administrative writes or stuck privileged jobs. From 56009d9fb51ff7f83657d979af3d734dd679ccc1 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 15 Sep 2026 18:37:10 -0300 Subject: [PATCH 21/24] docs: document testing and validation --- docs/testing.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 docs/testing.md diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000..513550c --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,35 @@ +# Testing and validation + +The repository uses focused checks so each validation type has its own setup and failure signal. + +## Bats + +Bats tests cover synchronization behavior and ruleset policy invariants. + +Run locally with: + +```bash +bats tests +``` + +The test suite covers Nextcloud and non-Nextcloud repositories, API error handling, bypass idempotency, repository targeting, ruleset normalization, and policy invariants. + +## ShellCheck + +Shell scripts are analyzed independently with ShellCheck: + +```bash +shellcheck scripts/*.sh +``` + +## actionlint + +GitHub Actions workflow syntax and expressions are validated by the dedicated `actionlint` workflow. + +## zizmor + +GitHub Actions security is audited by the dedicated `zizmor` workflow. Analysis runs without access to the ruleset GitHub App credentials. + +## Dependency updates + +Third-party Actions are pinned to full commit SHAs. Dependabot checks GitHub Actions dependencies weekly and applies a seven-day cooldown before proposing updates. From ebaf4132c92c01529a3a38923689ba46304a33e1 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 15 Sep 2026 18:37:16 -0300 Subject: [PATCH 22/24] docs: keep README focused on repository purpose --- README.md | 58 ++++++++++--------------------------------------------- 1 file changed, 10 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 4d286fe..f933749 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,17 @@ # LibreSign organization automation -This repository contains shared organization metadata and automation used to keep repository rulesets consistent across LibreSign and LibreCodeCoop public repositories. +This repository centralizes shared GitHub organization automation for LibreSign and LibreCodeCoop. -## Ruleset synchronization +It helps keep repository governance consistent as the project grows, including branch protection policies, controlled exceptions required by project workflows, and automated validation of the configuration that manages those policies. -`.github/rulesets/default-branches.json` is the source of truth for the default repository ruleset. The scheduled workflow `.github/workflows/sync-rulesets.yml` applies it to public, non-archived repositories that are available to the ruleset GitHub App installation. +The repository currently delivers: -The ruleset protects the default branch and `stable*` branches. It prevents deletion and non-fast-forward updates and requires pull requests, one approval, CODEOWNERS approval and resolved review threads. +- consistent branch protection rules across public repositories; +- automatic support for Nextcloud translation workflows where required; +- reduced administrative access scope for automation; +- automated checks that help prevent regressions in repository governance. -### Nextcloud apps +Technical and operational details are kept in [`docs/`](docs/): -A repository is considered a Nextcloud app when `appinfo/info.xml` exists in its default branch. - -For these repositories, the sync script adds the `nextcloud-bot` GitHub user to the ruleset bypass actors with `bypass_mode: always`. The actor is pinned by the stable GitHub user ID `20296731`; the username is used only for documentation and log messages. - -A `404` while checking `appinfo/info.xml` means the repository is not a Nextcloud app. Other API errors abort synchronization for that repository so a temporary authorization or GitHub API failure cannot silently remove the bot bypass. - -## Running the synchronization - -The normal command synchronizes every public, non-archived repository for the selected organization: - -```bash -ORG=LibreSign ./scripts/sync-rulesets.sh -``` - -To inspect drift without modifying any repository: - -```bash -ORG=LibreSign ./scripts/sync-rulesets.sh --check -``` - -To limit the operation to one repository: - -```bash -ORG=LibreSign ./scripts/sync-rulesets.sh --repo LibreSign/libresign -``` - -The command requires `gh`, `jq` and a GitHub token with repository administration permission. The scheduled workflow generates a short-lived GitHub App token with only `administration: write` in addition to the workflow's read-only contents permission. - -## Tests - -The shell behavior and policy invariants are tested with Bats: - -```bash -bats tests -``` - -Shell scripts are checked with ShellCheck. GitHub Actions workflows are checked with actionlint and zizmor in `.github/workflows/quality.yml`. - -The pull request quality workflow does not use the ruleset GitHub App credentials and has only `contents: read` permission. - -## GitHub Actions dependencies - -Third-party actions are pinned to full commit SHAs. Dependabot is configured to propose weekly GitHub Actions updates so immutable pins can stay current. +- [Ruleset synchronization](docs/ruleset-sync.md) +- [Testing and validation](docs/testing.md) From af00b697349d8bf0aa7fc774e5f4a77d829f0d76 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 15 Sep 2026 19:03:14 -0300 Subject: [PATCH 23/24] ci: name Bats ruleset sync workflow explicitly --- .github/workflows/bats-ruleset-sync.yml | 30 +++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/bats-ruleset-sync.yml diff --git a/.github/workflows/bats-ruleset-sync.yml b/.github/workflows/bats-ruleset-sync.yml new file mode 100644 index 0000000..3d5d375 --- /dev/null +++ b/.github/workflows/bats-ruleset-sync.yml @@ -0,0 +1,30 @@ +name: Bats ruleset sync tests + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + bats: + name: Ruleset sync behavior and policy + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + with: + persist-credentials: false + + - name: Install Bats + run: | + sudo apt-get update + sudo apt-get install --yes bats + + - name: Run Bats + run: bats tests From 744d8946eaa5c4c503da1df4b3f36fe578f8cb90 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 15 Sep 2026 19:03:22 -0300 Subject: [PATCH 24/24] ci: replace generic test workflow name --- .github/workflows/test.yml | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index e7ba076..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Tests - -on: - pull_request: - push: - branches: - - main - -permissions: - contents: read - -jobs: - bats: - name: Bash tests and policy checks - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - name: Checkout - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 - with: - persist-credentials: false - - - name: Install Bats - run: | - sudo apt-get update - sudo apt-get install --yes bats - - - name: Run Bats - run: bats tests