From 3b812fe6144bdce62771adfb2d05d6a427774ad8 Mon Sep 17 00:00:00 2001 From: Jolien Trog Date: Tue, 17 Jun 2025 13:35:56 +0200 Subject: [PATCH 1/4] scripts for distribution and README for Repo --- README.md | 44 +++++++++++ list-repos.sh | 107 +++++++++++++++++++++++++++ sync.sh | 200 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 351 insertions(+) create mode 100644 README.md create mode 100755 list-repos.sh create mode 100755 sync.sh diff --git a/README.md b/README.md new file mode 100644 index 0000000..38dc92c --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# Icinga GitHub Actions + +This repository contains workflow files for centralized management and scripts for distribution of GitHub Actions to all relevant Icinga repositories. + +### Management Scripts +The two scripts can be used to distribute GitHub Actions workflow files to multiple repositories. +It works with a local repository set up and creates automatically PRs. + +### Prerequisites +- GitHub CLI (`gh`) installed and authenticated +- Write permissions for target repositories + +### Setup +```bash +# Create local repository +mkdir github-actions-deploy +cd github-actions-deploy +git init +git checkout -b main + +# Set up workflow structure +git checkout -b +mkdir -p .github/workflows +cp /path/to/action-file.yml .github/workflows/action-file.yml + +# Create commit +git add -A +git commit -m "Commit Message" +``` + +### Execution +```bash +# All Icinga repositories +./list-repos.sh -p "Icinga/*" | ./sync.sh + +# Specific repository patterns +./list-repos.sh -p "Icinga/icingaweb*" | ./sync.sh + +# Dry-run +./list-repos.sh -p "Icinga/*" | ./sync.sh --dry-run + +#show help +./sync.sh -h +``` diff --git a/list-repos.sh b/list-repos.sh new file mode 100755 index 0000000..97e474c --- /dev/null +++ b/list-repos.sh @@ -0,0 +1,107 @@ +#!/bin/bash + +# Creates a list of repositories based on patterns and filters all public, non-archived repositories + +PATTERNS=() + +RED='\033[0;31m' +NC='\033[0m' # No Color (Reset) + +show_help() { + cat << EOF +GitHub Repository List Generator + +Usage: $0 [OPTIONS] + +OPTIONS: + -p, --pattern PATTERN Repository pattern (required) + Examples: "Icinga/icingaweb*", "MyOrg/*", "Icinga/icinga2" + -h, --help Show this help + +EXAMPLES: + # All public Icinga repos + $0 -p "Icinga/*" + + # Only icingaweb repos in Icinga organization + $0 -p "Icinga/icingaweb*" + +EOF +} + +# Parameter parsen +while [[ $# -gt 0 ]]; do + case $1 in + -p|--pattern) + PATTERNS+=("$2") + shift 2 + ;; + -h|--help) + show_help >&2 + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + show_help >&2 + exit 1 + ;; + esac +done + +# Check if patterns contain organization info +if [[ "${PATTERNS[0]}" != *"/"* ]]; then + echo -e "${RED}Error: Pattern must include organization (e.g., 'Icinga/icingaweb*')${NC}" >&2 + show_help >&2 + exit 1 +fi + +get_repositories() { + local org="${PATTERNS[0]%%/*}" + + # List all public, non-archived repositories for the organization + gh repo list "$org" --limit 1000 --no-archived --visibility public --json name,owner | \ + jq -r '.[] | "\(.owner.login)/\(.name)"' +} + +matches_pattern() { + local repo="$1" + local pattern="$2" + + # Bash pattern matching + case "$repo" in + $pattern) return 0 ;; + *) return 1 ;; + esac +} + +# Pattern filtering +filter_repositories() { + local repos=() + + while IFS= read -r repo; do + for pattern in "${PATTERNS[@]}"; do + if matches_pattern "$repo" "$pattern"; then + repos+=("$repo") + break + fi + done + done + + printf '%s\n' "${repos[@]}" +} + +main() { + + # Get and filter repositories + mapfile -t repositories < <(get_repositories | filter_repositories) + + if [[ ${#repositories[@]} -eq 0 ]]; then + echo -e "${RED}No repositories found matching the patterns${NC}" >&2 + exit 1 + fi + + # Output repositories to stdout + printf '%s\n' "${repositories[@]}" + } + +# Script execution +main diff --git a/sync.sh b/sync.sh new file mode 100755 index 0000000..98a6a7d --- /dev/null +++ b/sync.sh @@ -0,0 +1,200 @@ +#!/bin/bash + +# GitHub Action Deployment: +# 1. Receives repository list from `list-repos.sh` +# 2. Creates temporary branches for each target repository +# 3. Cherry-picks local branch into target branch +# 4. Pushes changes and creates pull requests with auto-generated title and body from commits +# 5. Cleans up local environment (deletes branches/remotes) + +# Configuration +DRY_RUN=false + +# Color codes +RED='\033[0;31m' +GREEN='\033[0;32m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color (Reset) + +show_help() { + cat << EOF +GitHub Action Deployment - Push Branch to Multiple Repos + +Usage: $0 [OPTIONS] + +This script usees stdin to push the current branch to multiple repositories and creates PRs with automated message from commits. + +OPTIONS: + --dry-run Only show what would be done, without changes + -h, --help Show this help + +EXAMPLES: + # Use with pipe from list-repos.sh + ./list-repos.sh -p "Icinga/*" | sync.sh + + # Dry-run to see what would happen + ./list-repos.sh -p "Icinga/*" | $0 --dry-run +EOF +} + +# Parameter parsen +while [[ $# -gt 0 ]]; do + case $1 in + --dry-run) + DRY_RUN=true + shift + ;; + -h|--help) + show_help >&2 + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + show_help >&2 + exit 1 + ;; + esac +done + +# Get current branch +BRANCH_TO_DISTRIBUTE=$(git branch --show-current) +if [[ -z "$BRANCH_TO_DISTRIBUTE" ]]; then + exit 1 +fi + +#global arrays +created_branches=() +updated_repositories=() +failed_repositories=() + +deploy_to_repo() { + local repo="$1" + + # Create remote name from repo (replace / with -) + local remote_name="${repo//\//-}" + WORKING_BRUNCH="$remote_name/$BRANCH_TO_DISTRIBUTE" + + if [[ "$DRY_RUN" == "true" ]]; then + echo "[DRY-RUN] $repo - would cherry-pick branch '$BRANCH_TO_DISTRIBUTE' to $WORKING_BRUNCH" >&2 + return 0 + fi + + echo -e "${BLUE}Processing $repo...${NC}" >&2 + + created_branches+=("$WORKING_BRUNCH") + remote_repos+=("$remote_name") + local expected_url="https://github.com/${repo}.git" + + #get and add remote repository url + if ! git remote get-url "$remote_name"; then + git remote add "$remote_name" "$expected_url" + elif [[ "$(git remote get-url "$remote_name")" != "$expected_url" ]]; then + echo -e "${RED}Error: Remote $remote_name URL mismatch${NC}" >&2 + return 1 + fi + + # Fetch the remote repository + if ! git fetch "$remote_name"; then + echo -e "${RED}$repo (fetch failed)${NC}" >&2 + return 1 + fi + + # Create new branch from remote main + if ! git checkout -b "$WORKING_BRUNCH" "$remote_name/main"; then + echo -e "${RED}Can not create new branch $WORKING_BRUNCH${NC}" >&2 + return 1 + fi + + #cherry-pick branch + if ! git cherry-pick "$BRANCH_TO_DISTRIBUTE"; then + echo -e "${RED}$repo (cherry-pick failed for branch $lworking_branch)${NC}" >&2 + git cherry-pick --abort || true + return 1 + fi + + # Push to remote repository + if ! git push "$remote_name" "HEAD:$BRANCH_TO_DISTRIBUTE"; then + echo -e "${RED}$repo (push failed)${NC}" >&2 + return 1 + fi + + # Create pull request + if gh pr create --repo "$repo" --fill --head "$BRANCH_TO_DISTRIBUTE" --base main; then + echo -e "${GREEN}$repo (PR created)${NC}" >&2 + else + echo -e "${RED}$repo (PR creation failed)${NC}" >&2 + return 1 + fi + + return 0 +} + +#set trap to clean up + cleanup() { + git checkout "$BRANCH_TO_DISTRIBUTE" || git checkout main + + for branch in "${created_branches[@]}"; do + git branch -D "$branch" + done + + for remote_repo in "${remote_repos[@]}"; do + git remote remove "$remote_repo" + done + + } + +print_results() { + echo >&2 + echo "=== RESULTS ===" >&2 + + if [[ ${#updated_repositories[@]} -gt 0 ]]; then + for repo in "${updated_repositories[@]}"; do + echo -e "${GREEN}${repo} updated ${NC}" + done + fi + + if [[ ${#failed_repositories[@]} -gt 0 ]]; then + for repo in "${failed_repositories[@]}"; do + echo -e "${RED}${repo} failed ${NC}" + done + fi +} + + #trap to clean up + trap 'print_results; cleanup' EXIT INT TERM + +main() { + + # Read repository list from file or stdin + local repositories=() + mapfile -t repositories + + # Remove empty lines from stdin + local filtered_repos=() + for repo in "${repositories[@]}"; do + [[ -n "$repo" ]] && filtered_repos+=("$repo") + done + + repositories=("${filtered_repos[@]}") + + + if [[ "$DRY_RUN" == "true" ]]; then + for repo in "${repositories[@]}"; do + deploy_to_repo "$repo" + done + return 0 + fi + + for repo in "${repositories[@]}"; do + if ! deploy_to_repo "$repo"; then + failed_repositories+=("$repo") + exit 1; + else + updated_repositories+=("$repo") + fi + echo >&2 + done +} + +# Script execution +main From 609ce2c882df62cd394371287628ff2c3490c041 Mon Sep 17 00:00:00 2001 From: Jolien Trog Date: Tue, 17 Jun 2025 14:32:35 +0200 Subject: [PATCH 2/4] add stale action configuration --- .github/workflows/stale.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .github/workflows/stale.yml diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..fd8c9e9 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,11 @@ +name: stale action + +on: + push: + branches: + - main + +jobs: + update: + uses: icinga/github-actions/.github/workflows/stale.yml@main + secrets: inherit From e9d1892ee5059457a748bc73ead413854bf4bbe0 Mon Sep 17 00:00:00 2001 From: Jolien Trog Date: Tue, 17 Jun 2025 14:43:37 +0200 Subject: [PATCH 3/4] add stale-template --- .github/workflows/stale-template.sh | 27 +++++++++++++++++++++++++++ stale.yml | 11 +++++++++++ 2 files changed, 38 insertions(+) create mode 100644 .github/workflows/stale-template.sh create mode 100644 stale.yml diff --git a/.github/workflows/stale-template.sh b/.github/workflows/stale-template.sh new file mode 100644 index 0000000..0996e4f --- /dev/null +++ b/.github/workflows/stale-template.sh @@ -0,0 +1,27 @@ +# Stale Bot Action for Icinga +name: 'stale' + +permissions: + issues: write + +on: + schedule: + - cron: 0 13 * * 1-5 + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + close-issue-message: 'This issue has been automatically closed due to age/inactivity. If still relevant with current software version, feel free to create a new issue with updated details. ' + stale-issue-label: 'stale' + exempt-issue-labels: 'ref/IP, ref/NP' + exempt-all-issue-milestones: true + days-before-issue-stale: 1780 + days-before-issue-close: 0 + operations-per-run: 15 + enable-statistics: true + ignore-issue-updates: true + ascending: true + debug-only: true diff --git a/stale.yml b/stale.yml new file mode 100644 index 0000000..fd8c9e9 --- /dev/null +++ b/stale.yml @@ -0,0 +1,11 @@ +name: stale action + +on: + push: + branches: + - main + +jobs: + update: + uses: icinga/github-actions/.github/workflows/stale.yml@main + secrets: inherit From 7cee10a60227972bd7962e998d323ad7b20dbfc6 Mon Sep 17 00:00:00 2001 From: Jolien Trog Date: Wed, 2 Jul 2025 14:37:01 +0200 Subject: [PATCH 4/4] correct jobname and directory for stale.yml --- .github/workflows/stale.yml | 11 ----------- stale.yml | 2 +- 2 files changed, 1 insertion(+), 12 deletions(-) delete mode 100644 .github/workflows/stale.yml diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml deleted file mode 100644 index fd8c9e9..0000000 --- a/.github/workflows/stale.yml +++ /dev/null @@ -1,11 +0,0 @@ -name: stale action - -on: - push: - branches: - - main - -jobs: - update: - uses: icinga/github-actions/.github/workflows/stale.yml@main - secrets: inherit diff --git a/stale.yml b/stale.yml index fd8c9e9..58a4ecd 100644 --- a/stale.yml +++ b/stale.yml @@ -6,6 +6,6 @@ on: - main jobs: - update: + stale: uses: icinga/github-actions/.github/workflows/stale.yml@main secrets: inherit