Skip to content

♻️ Update common DevOps tooling #33

♻️ Update common DevOps tooling

♻️ Update common DevOps tooling #33

Workflow file for this run

---
# 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