♻️ Update shared DevOps tooling #54
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--- | |
name: "♻️ Update shared DevOps tooling" | |
# yamllint disable-line rule:truthy | |
on: | |
workflow_dispatch: | |
schedule: | |
- cron: "0 8 * * MON" | |
jobs: | |
update-actions: | |
name: "Update DevOps tooling" | |
runs-on: ubuntu-latest | |
permissions: | |
# IMPORTANT: mandatory to create or update content/actions/pr | |
contents: write | |
actions: write | |
pull-requests: write | |
steps: | |
- name: "Checkout primary repository" | |
uses: actions/checkout@v4 | |
- name: "Pull devops content from repository" | |
uses: actions/checkout@v4 | |
with: | |
repository: "os-climate/devops-toolkit" | |
path: ".devops" | |
- name: "Update repository workflows and create PR" | |
id: update-repository | |
env: | |
GH_TOKEN: ${{ github.token }} | |
# yamllint disable rule:line-length | |
run: | | |
#SHELLCODESTART | |
set -euo pipefail | |
# set -x | |
# Define variables | |
DEVOPS_DIR=".devops" | |
AUTOMATION_BRANCH="update-devops-tooling" | |
REPO_DIR=$(git rev-parse --show-toplevel) | |
GIT_ORIGIN=$(git config --get remote.origin.url) | |
REPO_NAME=$(basename -s .git "$GIT_ORIGIN") | |
EXCLUDE_FILE=".devops-exclusions" | |
DEVOPS_REPO='git@github.com:os-climate/devops-toolkit.git' | |
HEAD_BRANCH=$(git rev-parse --abbrev-ref HEAD) | |
# Content folder defines the files and folders to update | |
FILES="$DEVOPS_DIR/content/files.txt" | |
FOLDERS="$DEVOPS_DIR/content/folders.txt" | |
# Define functions | |
perform_folder_operation() { | |
FS_PATH="$1" | |
if [ -d "$DEVOPS_DIR"/"$FS_PATH" ]; then | |
echo "Scanning target folder content at: $FS_PATH" | |
return 0 | |
else | |
echo "Upstream folder NOT found: $FS_PATH [skipping]" | |
return 1 | |
fi | |
} | |
# Allows for selective opt-out components on a per-path basis | |
perform_operation() { | |
FS_PATH="$1" | |
if [ ! -f "$DEVOPS_DIR"/"$FS_PATH" ]; then | |
echo "Skipping missing upstream file at: $FS_PATH" | |
return 1 | |
fi | |
# Elements excluded from processing return exit status 1 | |
if [ ! -f "$EXCLUDE_FILE" ]; then | |
return 0 | |
elif [ "$FS_PATH" = "$EXCLUDE_FILE" ]; then | |
# The exclusion file itself is never updated by automation | |
return 1 | |
elif (grep -Fxq "$FS_PATH" "$EXCLUDE_FILE" > /dev/null); then | |
# Element listed; exclude from processing | |
return 1 | |
else | |
# Element not found in exclusion file; process it | |
return 0 | |
fi | |
} | |
# Only updates file if it has changed | |
selective_file_copy() { | |
# Receives a single file path as argument | |
# SHA_SRC=$(sha1sum "$DEVOPS_DIR/$1" | awk '{print $1}') | |
# SHA_DST=$(sha1sum "$1" 2>/dev/null | awk '{print $1}' || :) | |
# if [ "$SHA_SRC" != "$SHA_DST" ]; then | |
if ! (cmp "$DEVOPS_DIR/$1" "$1"); then | |
echo "Copying: $1" | |
cp "$DEVOPS_DIR/$1" "$1" | |
git add "$1" | |
fi | |
} | |
check_pr_for_author() { | |
AUTHOR="$1" | |
printf "Checking for pull requests by: %s" "$AUTHOR" | |
# Capture the existing PR number | |
PR_NUM=$(gh pr list --state open -L 1 \ | |
--author "$AUTHOR" --json number | \ | |
grep "number" | sed "s/:/ /g" | awk '{print $2}' | \ | |
sed "s/}//g" | sed "s/]//g") | |
if [ -z "$PR_NUM" ]; then | |
echo " [none]" | |
return 1 | |
else | |
echo " [$PR_NUM]" | |
echo "Running: gh pr checkout $PR_NUM" | |
if (gh pr checkout "$PR_NUM"); then | |
return 0 | |
else | |
echo "Failed to checkout GitHub pull request" | |
echo "Check errors/output for the cause" | |
return 2 | |
fi | |
fi | |
} | |
check_prs() { | |
# Define users to check for pre-existing pull requests | |
AUTOMATION_USER="github-actions[bot]" | |
if [[ -n ${GH_TOKEN+x} ]]; then | |
GITHUB_USERS="$AUTOMATION_USER" | |
else | |
GITHUB_USERS=$(gh api user | jq -r '.login') | |
# Check local user account first, if enumerated | |
GITHUB_USERS+=" $AUTOMATION_USER" | |
fi | |
# Check for existing pull requests opened by this automation | |
for USER in $GITHUB_USERS; do | |
if (check_pr_for_author "$USER"); then | |
return 0 | |
else | |
STATUS="$?" | |
fi | |
if [ "$STATUS" -eq 1 ]; then | |
continue | |
elif [ "$STATUS" -eq 2 ]; then | |
echo "Failed to checkout pull request"; exit 1 | |
fi | |
done | |
return 1 | |
} | |
# Check if script is running in GHA workflow | |
in_github() { | |
if [ -z ${GITHUB_RUN_ID+x} ]; then | |
echo "Script is NOT running in GitHub" | |
return 1 | |
else | |
echo "Script is running in GitHub" | |
return 0 | |
fi | |
} | |
# Check if user is logged into GitHub | |
logged_in_github() { | |
if (gh auth status); then | |
echo "Logged in and authenticated to GitHb" | |
return 0 | |
else | |
echo "Not logged into GitHub, some script operations unavailable" | |
return 1 | |
fi | |
} | |
# Main script entry point | |
echo "Repository name and HEAD branch: $REPO_NAME [$HEAD_BRANCH]" | |
# Ensure working from top-level of GIT repository | |
CURRENT_DIR=$(pwd) | |
if [ "$REPO_DIR" != "$CURRENT_DIR" ]; then | |
echo "Changing directory to: $REPO_DIR" | |
if ! (cd "$REPO_DIR"); then | |
echo "Error: unable to change directory"; exit 1 | |
fi | |
fi | |
# Stashing only used during development/testing | |
# Check if there are unstaged changes | |
# if ! (git diff --exit-code --quiet); then | |
# echo "Stashing unstaged changes in current repository" | |
# git stash -q | |
# fi | |
# Configure GIT environment only if NOT already configured | |
# i.e. when running in a GitHub Actions workflow | |
TEST=$(git config -l > /dev/null 2>&1) | |
if [ -n "$TEST" ]; then | |
git config user.name "github-actions[bot]" | |
git config user.email \ | |
"41898282+github-actions[bot]@users.noreply.github.com" | |
fi | |
if ! (check_prs); then | |
# No existing open pull requests found for this repository | |
# Remove remote branch if it exists | |
git push origin --delete "$AUTOMATION_BRANCH" > /dev/null 2>&1 || : | |
git branch -D "$AUTOMATION_BRANCH" || : | |
git checkout -b "$AUTOMATION_BRANCH" | |
else | |
# The -B flag swaps branch and creates it if NOT present | |
git checkout -B "$AUTOMATION_BRANCH" | |
fi | |
# Only if NOT running in GitHub | |
# (checkout is otherwise performed by earlier steps) | |
if ! (in_github); then | |
# Remove any stale local copy of the upstream repository | |
if [ -d "$DEVOPS_DIR" ]; then | |
rm -Rf "$DEVOPS_DIR" | |
fi | |
printf "Cloning DevOps repository into: %s" "$DEVOPS_DIR" | |
if (git clone "$DEVOPS_REPO" "$DEVOPS_DIR" > /dev/null 2>&1); then | |
echo " [success]" | |
else | |
echo " [failed]"; exit 1 | |
fi | |
fi | |
# Process upstream DevOps repository content and update | |
LOCATIONS="" | |
# Populate list of files to be updated/sourced | |
while read -ra LINE; | |
do | |
for FILE in "${LINE[@]}"; | |
do | |
LOCATIONS+="$FILE " | |
done | |
done < "$FILES" | |
# Gather files from specified folders and append to locations list | |
while read -ra LINE; | |
do | |
for FOLDER in "${LINE[@]}"; | |
do | |
# Check to see if this folder should be skipped | |
if (perform_folder_operation "$FOLDER"); then | |
# If necessary, create target folder | |
if [ ! -d "$FOLDER" ]; then | |
echo "Creating target folder: $FOLDER" | |
mkdir "$FOLDER" | |
fi | |
# Add folder contents to list of file LOCATIONS | |
FILES=$(cd "$DEVOPS_DIR/$FOLDER"; find . -maxdepth 1 -type f -exec basename {} \;) | |
for LOCATION in $FILES; do | |
# Also check if individual files in the folder are excluded | |
if (perform_operation "$FOLDER/$LOCATION"); then | |
LOCATIONS+=" $FOLDER/$LOCATION" | |
fi | |
done | |
else | |
echo "Opted out of folder: $FOLDER" | |
continue | |
fi | |
done; | |
done < "$FOLDERS" | |
# Copy specified files into repository root | |
for LOCATION in ${LOCATIONS}; do | |
if (perform_operation "$LOCATION"); then | |
selective_file_copy "$LOCATION" | |
else | |
echo "Not updating: $LOCATION" | |
fi | |
done | |
# If no changes required, do not throw an error | |
if [ -z "$(git status --porcelain)" ]; then | |
echo "No updates/changes to commit"; exit 0 | |
fi | |
# Temporarily disable exit on unbound variable | |
set +eu +o pipefail | |
# Next step is only performed if running as GitHub Action | |
if [[ -n ${GH_TOKEN+x} ]]; then | |
# Script is running in a GitHub actions workflow | |
# Set outputs for use by the next actions/steps | |
# shellcheck disable=SC2129 | |
echo "changed=true" >> "$GITHUB_OUTPUT" | |
echo "branchname=$AUTOMATION_BRANCH" >> "$GITHUB_OUTPUT" | |
echo "headbranch=$HEAD_BRANCH" >> "$GITHUB_OUTPUT" | |
# Move to the next workflow step to raise the PR | |
git push --set-upstream origin "$AUTOMATION_BRANCH" | |
exit 0 | |
fi | |
# If running shell code locally, continue to raise the PR | |
# Reinstate exit on unbound variables | |
set -euo pipefail | |
git status | |
if ! (git commit -as -S -m "Chore: Update DevOps tooling from central repository [skip ci]" \ | |
-m "This commit created by automation/scripting" --no-verify); then | |
echo "Commit failed; aborting"; exit 1 | |
else | |
# Push branch to remote repository | |
git push --set-upstream origin "$AUTOMATION_BRANCH" | |
# Create PR request | |
gh pr create \ | |
--title "Chore: Pull DevOps tooling from upstream repository" \ | |
--body 'Automated by a GitHub workflow: bootstrap.yaml' | |
fi | |
# echo "Unstashing unstaged changes, if any exist" | |
# git stash pop -q || : | |
#SHELLCODEEND | |
- name: Create Pull Request | |
if: steps.update-repository.outputs.changed == 'true' | |
uses: peter-evans/create-pull-request@v6 | |
# env: | |
# GITHUB_TOKEN: ${{ github.token }} | |
with: | |
# Note: Requires a specific/defined Personal Access Token | |
token: ${{ secrets.ACTIONS_WORKFLOW }} | |
commit-message: "Chore: Update DevOps tooling from central repository [skip ci]" | |
signoff: "true" | |
base: ${{ steps.update-repository.outputs.headbranch }} | |
branch: ${{ steps.update-repository.outputs.branchname }} | |
delete-branch: true | |
title: "Chore: Update DevOps tooling from central repository [skip ci]" | |
body: | | |
Update repository with content from upstream: os-climate/devops-toolkit | |
labels: | | |
automated pr | |
draft: false |