♻️ Update common DevOps tooling #33
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
--- | |
# SPDX-License-Identifier: Apache-2.0 | |
# Copyright 2024 The Linux Foundation <[email protected]> | |
name: "♻️ Update common 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: | | |
# Updating repository environment | |
#SHELLCODESTART | |
set -euo pipefail | |
# set -vx | |
# Define variables | |
# Inherit from parent, otherwise set off | |
if [[ -z ${DEBUG+x} ]]; then | |
DEBUG="false" | |
fi | |
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='[email protected]: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 [ ! -f "$1" ]; then | |
echo "Adding new file: $1" | |
cp "$DEVOPS_DIR/$1" "$1" | |
git add "$1" | |
elif !(cmp "$DEVOPS_DIR/$1" "$1" > /dev/null 2>&1); then | |
echo "Updating existing file: $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 | |
} | |
change_dir_error() { | |
echo "Could not change directory"; exit 1 | |
} | |
pdm_migrate_config() { | |
python -m venv /tmp/python-venv | |
source /tmp/python-venv/bin/activate | |
cd "$REPO_NAME" || change_dir_error | |
echo "Installing PDM command..." | |
if !(pip install pdm setuptools > /dev/null 2>&1); then | |
echo "PDM installation failed"; exit 1 | |
fi | |
echo -n "Validating PDM version: " | |
pdm self update | |
if [ "$DEBUG" = "true" ]; then | |
echo "Display PDM/project environment:" | |
pdm info --env | |
fi | |
pdm import setup.py | |
cd - >/dev/null || change_dir_error | |
echo "Relocating Pyscaffold output to current folder" | |
mv "$REPO_NAME"/{.[!.],}* . | |
rmdir "$REPO_NAME" | |
rm README.md setup.cfg setup.py LICENSE.txt test.sh pyscaffold.cfg | |
} | |
bootstrap_new_repo() { | |
echo "Bootstrapping new repository" | |
echo "Installing pyscaffold..." | |
if !(pip install pyscaffold > /dev/null 2>&1); then | |
echo "PyScaffold installation failed"; exit 1 | |
fi | |
# Search and replace pyscaffold configuration file | |
sed -i "s/REPO_PLACEHOLDER/$REPO_NAME/g" pyscaffold.cfg | |
if [ -d "$REPO_NAME" ]; then | |
echo "Removing previous pyscaffold run from local disk" | |
rm -Rf "$REPO_NAME" | |
fi | |
echo -n "Creating new project skeleton with Pyscaffold: " | |
putup --force "$REPO_NAME" --config pyscaffold.cfg | |
} | |
# 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 | |
# Configure GIT environment only if NOT already configured | |
# i.e. when running in a GitHub Actions workflow | |
TEST=$(git commit >/dev/null 2>&1; echo $?) | |
if [ "$TEST" -eq 128 ]; then | |
echo "Configuring GIT as github-actions[bot]" | |
git config user.name "github-actions[bot]" | |
git config user.email \ | |
"41898282+github-actions[bot]@users.noreply.github.com" | |
else | |
echo "GIT user is configured as:" | |
git config user.name | |
git config user.email | |
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 | |
# Create template Python repository content | |
# Install and run Pyscaffold | |
if [ ! -d ./src ]; then | |
bootstrap_new_repo | |
pdm_migrate_config | |
fi | |
# Process upstream DevOps repository content and update | |
# 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 | |
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 [skip ci]" \ | |
--body 'Automated by a GitHub workflow: tooling.yaml [skip ci]' | |
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 |