Skip to content

Repository Update

Repository Update #20

Workflow file for this run

# ============================================================================
# Repository Update Workflow Template (update.yml)
# ============================================================================
#
# ORIGIN: https://github.com/XAOSTECH/dev-control
# This workflow template is part of the dev-control toolkit.
# See: https://github.com/XAOSTECH/dev-control/blob/main/workflows-templates/
#
# ============================================================================
#
# Automates repository maintenance tasks including:
# - Detecting accumulated changes and auto-triggering releases
# - Updating submodules that have fallen behind
# - Keeping monorepo dependencies synchronised
#
# TRIGGERS:
# - Schedule: Weekly on Monday at 00:00 UTC
# - Manual dispatch with customisable options
#
# WHAT IT DOES:
# 1. Checks commits since last release
# 2. If 5+ commits found, triggers re-release (or version bump if 30+)
# 3. Scans submodules for updates (5+ commits behind main)
# 4. Updates outdated submodules and creates PR
# 5. Generates summary report
#
# SAFEGUARDS:
# ✓ Dry-run mode for testing
# ✓ Minimum threshold checks (configurable)
# ✓ Creates PR instead of direct commit for review
# ✓ Detailed summary with change counts
#
# CONFIGURATION:
# Adjust thresholds via workflow inputs:
# - release_threshold: Minimum commits to trigger release (default: 5)
# - new_release_threshold: Commits ceiling before forcing version bump (default: 30)
# - submodule_threshold: Minimum commits behind to update (default: 5)
#
# BEST PRACTICES:
# - Review the generated PR before merging
# - Use dry-run mode first to preview changes
# - Adjust thresholds based on your release cadence
# - Check submodule references match intended versions
#
# CUSTOMISATION:
# - Edit schedule cron for different timing
# - Modify thresholds for your workflow
# - Add additional checks in the detection steps
# - Customise PR template and labels
#
# ============================================================================
name: Repository Update
on:
schedule:
- cron: '0 0 * * 1' # Weekly on Monday at midnight UTC
workflow_dispatch:
inputs:
check_release:
description: 'Check for accumulated changes and trigger release'
required: false
type: boolean
default: true
check_submodules:
description: 'Check and update submodules'
required: false
type: boolean
default: true
release_threshold:
description: 'Minimum commits since last release to trigger re-release'
required: false
type: number
default: 5
new_release_threshold:
description: 'Commits ceiling - above this, bump version instead of re-release'
required: false
type: number
default: 30
submodule_threshold:
description: 'Minimum commits behind to update submodule'
required: false
type: number
default: 5
dry_run:
description: 'Dry run - report only, no changes'
required: false
type: boolean
default: false
normalize_version:
description: 'Normalise v0.0.x to v0.1.0 (uncheck to keep v0.0.x scheme)'
required: false
type: boolean
default: true
check_dependencies:
description: 'Check and update dependencies (package.json, requirements.txt)'
required: false
type: boolean
default: true
cleanup_branches:
description: 'Delete stale merged branches (>5 days old)'
required: false
type: boolean
default: true
branch_age_days:
description: 'Days after merge to consider branch stale'
required: false
type: number
default: 5
check_stale_issues:
description: 'Check and comment on stale issues/PRs'
required: false
type: boolean
default: true
stale_days:
description: 'Days of inactivity to consider issue/PR stale'
required: false
type: number
default: 10
dummy_commit:
description: 'Amend HEAD for serverless deployment (gcda-style, no SHA change)'
required: false
type: boolean
default: false
update_tree_viz:
description: 'Regenerate git tree visualisation if threshold met'
required: false
type: boolean
default: true
tree_viz_threshold:
description: 'Minimum new commits since last tree-viz to regenerate'
required: false
type: number
default: 30
permissions:
contents: write
pull-requests: write
actions: write
issues: write
concurrency:
group: update-${{ github.ref }}
cancel-in-progress: false
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
update:
name: Check Repository Updates
runs-on: ubuntu-latest
steps:
- name: Generate App Token
id: app_token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets['XB_AI'] }}
private-key: ${{ secrets['XB_PK'] }}
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0 # Full history for commit counting
submodules: recursive
token: ${{ steps.app_token.outputs.token }}
- name: Setup bot identity
uses: XAOSTECH/dev-control/.github/actions/identity@b067f72ca7f849b734a45634f23581a739d5146f # v2.0.0
with:
gpg-private-key: ${{ secrets['XB_GK'] }}
gpg-passphrase: ${{ secrets['XB_GP'] }}
user-token: ${{ secrets['XB_UT'] }}
bot-name: ${{ vars.BOT_NAME || 'xaos-bot' }}
- name: Check for release trigger
id: check_release
if: ${{ github.event.inputs.check_release != 'false' }}
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
RELEASE_THRESHOLD: ${{ github.event.inputs.release_threshold || '5' }}
NEW_RELEASE_THRESHOLD: ${{ github.event.inputs.new_release_threshold || '30' }}
DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }}
run: |
echo "## 📦 Release Check" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Get latest release tag
LATEST_TAG=$(gh api repos/${{ github.repository }}/releases/latest --jq '.tag_name' 2>/dev/null || \
git tag --list 'v*' --sort=-version:refname | grep -vE 'latest|alpha|beta|rc' | head -1 || echo "")
if [[ -z "$LATEST_TAG" ]]; then
echo "ℹ️ No previous release found - skipping release check"
echo "ℹ️ **No previous release found**" >> $GITHUB_STEP_SUMMARY
echo "trigger_release=false" >> $GITHUB_OUTPUT
exit 0
fi
# Count commits since last release (for minimum threshold)
COMMIT_COUNT=$(git log "${LATEST_TAG}..HEAD" --oneline | wc -l)
# Cumulative count: commits since the PRIOR different version tag
# Re-releases move the tag to HEAD, resetting the above count.
# To accumulate across re-releases, find the tag before the current version.
LATEST_VERSION="${LATEST_TAG#v}"
PRIOR_TAG=$(git tag -l 'v*' --sort=-version:refname | grep -vE 'latest|alpha|beta|rc' | \
while read -r t; do
v="${t#v}"
[[ "$v" != "$LATEST_VERSION" ]] && echo "$t" && break
done) || true
if [[ -n "$PRIOR_TAG" ]]; then
CUMULATIVE_COUNT=$(git log "${PRIOR_TAG}..HEAD" --oneline | wc -l)
else
CUMULATIVE_COUNT=$COMMIT_COUNT
fi
echo "📊 Latest release: $LATEST_TAG" | tee -a $GITHUB_STEP_SUMMARY
echo "📊 Commits since last release: $COMMIT_COUNT" | tee -a $GITHUB_STEP_SUMMARY
echo "📊 Cumulative commits since prior version (${PRIOR_TAG:-none}): $CUMULATIVE_COUNT" | tee -a $GITHUB_STEP_SUMMARY
echo "🎯 Release threshold: $RELEASE_THRESHOLD" | tee -a $GITHUB_STEP_SUMMARY
echo "🎯 New release ceiling: $NEW_RELEASE_THRESHOLD" | tee -a $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [[ $COMMIT_COUNT -ge $RELEASE_THRESHOLD ]]; then
echo "✅ Threshold met - release should be triggered" | tee -a $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Recent Changes" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
git log "${LATEST_TAG}..HEAD" --oneline | head -20 >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
echo "trigger_release=true" >> $GITHUB_OUTPUT
echo "commit_count=$COMMIT_COUNT" >> $GITHUB_OUTPUT
echo "cumulative_count=$CUMULATIVE_COUNT" >> $GITHUB_OUTPUT
echo "latest_tag=$LATEST_TAG" >> $GITHUB_OUTPUT
else
echo "ℹ️ Not enough changes - skipping release ($COMMIT_COUNT < $RELEASE_THRESHOLD)" | tee -a $GITHUB_STEP_SUMMARY
echo "trigger_release=false" >> $GITHUB_OUTPUT
fi
- name: Trigger release workflow
if: steps.check_release.outputs.trigger_release == 'true' && github.event.inputs.dry_run != 'true'
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
NEW_RELEASE_THRESHOLD: ${{ github.event.inputs.new_release_threshold || '30' }}
CUMULATIVE_COUNT: ${{ steps.check_release.outputs.cumulative_count }}
COMMIT_COUNT: ${{ steps.check_release.outputs.commit_count }}
run: |
CURRENT_VERSION=$(gh api repos/${{ github.repository }}/releases/latest --jq '.tag_name' 2>/dev/null || \
git tag --list 'v*' --sort=-version:refname | grep -vE 'latest|alpha|beta|rc' | head -1)
CURRENT_VERSION=${CURRENT_VERSION#v}
# Normalise v0.0.x → v0.1.0 (standard semver starting point)
if [[ "${{ github.event.inputs.normalize_version }}" != 'false' && "$CURRENT_VERSION" =~ ^0\.0\.[0-9]+$ ]]; then
echo "📐 Normalising v${CURRENT_VERSION} → v0.1.0 (standard semver convention)"
RELEASE_VERSION="0.1.0"
RELEASE_TYPE="new release"
elif [[ $CUMULATIVE_COUNT -ge $NEW_RELEASE_THRESHOLD ]]; then
# Cumulative changes across re-releases exceed ceiling — bump patch version
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION"
NEXT_VERSION="${MAJOR}.${MINOR}.$(( PATCH + 1 ))"
echo "📈 Cumulative commits ($CUMULATIVE_COUNT) exceeds ceiling ($NEW_RELEASE_THRESHOLD) — bumping version"
echo "📌 ${CURRENT_VERSION} → ${NEXT_VERSION}"
RELEASE_VERSION="$NEXT_VERSION"
RELEASE_TYPE="new release"
else
echo "🔄 Cumulative commits ($CUMULATIVE_COUNT) within ceiling ($NEW_RELEASE_THRESHOLD) — re-releasing"
echo "📌 Current version: ${CURRENT_VERSION}"
RELEASE_VERSION="$CURRENT_VERSION"
RELEASE_TYPE="re-release"
fi
gh workflow run release.yml \
--ref ${{ github.ref_name }} \
-f version="${RELEASE_VERSION}" \
-f normalize_version="${{ github.event.inputs.normalize_version || 'true' }}" \
-f prerelease=false \
-f draft=false
echo "✅ Release workflow triggered successfully"
echo "" >> $GITHUB_STEP_SUMMARY
echo "🚀 **Release workflow triggered** (${RELEASE_TYPE}: v${RELEASE_VERSION} — ${COMMIT_COUNT} new, ${CUMULATIVE_COUNT} cumulative)" >> $GITHUB_STEP_SUMMARY
- name: Check submodules
id: check_submodules
if: ${{ github.event.inputs.check_submodules != 'false' }}
env:
SUBMODULE_THRESHOLD: ${{ github.event.inputs.submodule_threshold || '5' }}
run: |
echo "## 📦 Submodule Check" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# For manual (workflow_dispatch) runs, override threshold to 0 so all
# behind submodules are flagged regardless of how far behind they are.
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
EFFECTIVE_THRESHOLD=0
else
EFFECTIVE_THRESHOLD=$SUBMODULE_THRESHOLD
fi
# Check if .gitmodules exists
if [[ ! -f .gitmodules ]]; then
echo "ℹ️ No submodules found in repository"
echo "ℹ️ **No submodules configured**" >> $GITHUB_STEP_SUMMARY
echo "has_updates=false" >> $GITHUB_OUTPUT
exit 0
fi
UPDATES_NEEDED=0
UPDATES_LIST=""
# Parse submodules (process substitution avoids subshell variable scope loss)
while read -r key path; do
name=$(echo "$key" | sed 's/^submodule\.\(.*\)\.path$/\1/')
url=$(git config --file .gitmodules --get "submodule.$name.url" || echo "")
branch=$(git config --file .gitmodules --get "submodule.$name.branch" || echo "main")
if [[ -z "$url" || ! -d "$path" ]]; then
continue
fi
echo "🔍 Checking: $name ($path)"
# Get current commit in submodule
cd "$path"
CURRENT_COMMIT=$(git rev-parse HEAD 2>/dev/null || echo "")
if [[ -z "$CURRENT_COMMIT" ]]; then
echo " ⚠️ Failed to get current commit"
cd "$GITHUB_WORKSPACE"
continue
fi
# Fetch latest from remote
# Fetch latest from remote — try configured branch then common fallbacks
RESOLVED_BRANCH=""
for try_branch in "$branch" main Main master Master; do
if git fetch origin "$try_branch" --quiet 2>/dev/null; then
RESOLVED_BRANCH="$try_branch"
break
fi
done
if [[ -z "$RESOLVED_BRANCH" ]]; then
echo " ⚠️ Failed to fetch any branch for $name — skipping"
cd "$GITHUB_WORKSPACE"
continue
fi
branch="$RESOLVED_BRANCH"
LATEST_COMMIT=$(git rev-parse "origin/$branch" 2>/dev/null || echo "")
if [[ -z "$LATEST_COMMIT" ]]; then
echo " ⚠️ Failed to resolve remote branch: $branch"
cd "$GITHUB_WORKSPACE"
continue
fi
# Count commits behind
if [[ "$CURRENT_COMMIT" != "$LATEST_COMMIT" ]]; then
COMMITS_BEHIND=$(git rev-list --count "${CURRENT_COMMIT}..${LATEST_COMMIT}" 2>/dev/null || echo "0")
if [[ $COMMITS_BEHIND -ge $EFFECTIVE_THRESHOLD ]]; then
echo " ⚠️ $COMMITS_BEHIND commits behind (threshold: $EFFECTIVE_THRESHOLD)"
UPDATES_NEEDED=$((UPDATES_NEEDED + 1))
UPDATES_LIST="${UPDATES_LIST}${name}:${path}:${COMMITS_BEHIND};"
else
echo " ✅ Up to date ($COMMITS_BEHIND commits behind, below threshold)"
fi
else
echo " ✅ Up to date"
fi
cd "$GITHUB_WORKSPACE"
done < <(git config --file .gitmodules --get-regexp '^submodule\..*\.path$')
echo "updates_needed=$UPDATES_NEEDED" >> $GITHUB_OUTPUT
echo "updates_list=$UPDATES_LIST" >> $GITHUB_OUTPUT
if [[ $UPDATES_NEEDED -gt 0 ]]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "⚠️ **$UPDATES_NEEDED submodule(s) need updates**" >> $GITHUB_STEP_SUMMARY
echo "has_updates=true" >> $GITHUB_OUTPUT
else
echo "✅ **All submodules up to date**" >> $GITHUB_STEP_SUMMARY
echo "has_updates=false" >> $GITHUB_OUTPUT
fi
- name: Update submodules
if: steps.check_submodules.outputs.has_updates == 'true' && github.event.inputs.dry_run != 'true'
run: |
echo "## 🔄 Updating Submodules" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
UPDATES_LIST="${{ steps.check_submodules.outputs.updates_list }}"
IFS=';' read -ra UPDATES <<< "$UPDATES_LIST"
for update in "${UPDATES[@]}"; do
if [[ -z "$update" ]]; then continue; fi
IFS=':' read -r name path commits_behind <<< "$update"
echo "⬆️ Updating $name ($commits_behind commits)"
echo "- **$name** ($path): $commits_behind commits" >> $GITHUB_STEP_SUMMARY
cd "$path"
# Get branch to update to
# Get branch to update to — try configured branch then common fallbacks
branch=$(git config --file "$GITHUB_WORKSPACE/.gitmodules" --get "submodule.$name.branch" 2>/dev/null || echo "main")
RESOLVED_BRANCH=""
for try_branch in "$branch" main Main master Master; do
if git fetch origin "$try_branch" --quiet 2>/dev/null; then
RESOLVED_BRANCH="$try_branch"
break
fi
done
if [[ -z "$RESOLVED_BRANCH" ]]; then
echo " ⚠️ Could not fetch any branch for $name — skipping"
echo "- ⚠️ **$name**: skipped (no fetchable branch)" >> $GITHUB_STEP_SUMMARY
cd "$GITHUB_WORKSPACE"
continue
fi
if ! git checkout "origin/$RESOLVED_BRANCH" --quiet 2>/dev/null; then
echo " ⚠️ checkout failed for $name — skipping"
echo "- ⚠️ **$name**: skipped (checkout failed)" >> $GITHUB_STEP_SUMMARY
cd "$GITHUB_WORKSPACE"
continue
fi
cd "$GITHUB_WORKSPACE"
done
echo "" >> $GITHUB_STEP_SUMMARY
echo "✅ Submodules update complete" >> $GITHUB_STEP_SUMMARY
- name: Create Pull Request
if: steps.check_submodules.outputs.has_updates == 'true' && github.event.inputs.dry_run != 'true'
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
run: |
# Check if there are any changes to commit
if git diff --cached --quiet && git diff --quiet; then
echo "ℹ️ No changes to commit"
exit 0
fi
BRANCH_NAME="chore/update-submodules-$(date +%Y%m%d-%H%M%S)"
git checkout -b "$BRANCH_NAME"
# Stage all submodule updates
git add .gitmodules
git add -A
# Create commit
UPDATES_COUNT="${{ steps.check_submodules.outputs.updates_needed }}"
git commit -m "chore: update ${UPDATES_COUNT} submodule(s) to latest" \
-m "Automated update of submodules that were 5+ commits behind." \
-m "See workflow run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
git push origin "$BRANCH_NAME"
# Ensure automerge label exists before creating PR
gh label create automerge \
--description "Automatically merge this PR" \
--color "3BEF67" \
2>/dev/null || true
# Create PR
gh pr create \
--title "chore: Update ${UPDATES_COUNT} submodule(s) to latest" \
--body "## 🔄 Automated Submodule Update\n\nThis PR updates submodules that have fallen behind by 5 or more commits.\n\n**Updated Submodules:** ${{ steps.check_submodules.outputs.updates_list }}\n\n**Triggered by:** [Repository Update Workflow](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})\n\n**What to Check:**\n- Review each submodule's changes between old and new commit\n- Ensure no breaking changes were introduced\n- Verify monorepo compatibility\n- Run tests locally if needed\n\nOnce approved, this will bring submodules up to date with their respective main branches." \
--base "${{ github.ref_name }}" \
--head "$BRANCH_NAME" \
--label "automerge"
echo "✅ Pull request created successfully"
echo "" >> $GITHUB_STEP_SUMMARY
echo "📝 **Pull request created** for submodule updates" >> $GITHUB_STEP_SUMMARY
- name: Check dependencies
id: check_dependencies
if: ${{ github.event.inputs.check_dependencies != 'false' }}
run: |
echo "## 📦 Dependency Check" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
UPDATES_FOUND=0
UPDATES_FILES=""
# Check package.json (npm)
if [[ -f package.json ]]; then
echo "🔍 Checking npm dependencies..."
if command -v npm &>/dev/null; then
npm outdated --json > outdated.json || true
if [[ -s outdated.json ]] && [[ $(cat outdated.json) != "{}" ]]; then
COUNT=$(jq 'length' outdated.json 2>/dev/null || echo "0")
if [[ $COUNT -gt 0 ]]; then
echo " ⚠️ Found $COUNT outdated npm package(s)"
UPDATES_FOUND=$((UPDATES_FOUND + COUNT))
UPDATES_FILES="${UPDATES_FILES}npm:${COUNT};"
echo "- **npm**: $COUNT package(s)" >> $GITHUB_STEP_SUMMARY
fi
else
echo " ✅ All npm packages up to date"
fi
rm -f outdated.json
fi
fi
# Check requirements.txt (pip)
if [[ -f requirements.txt ]]; then
echo "🔍 Checking pip dependencies..."
if command -v pip &>/dev/null; then
pip list --outdated --format=json > pip-outdated.json 2>/dev/null || true
if [[ -s pip-outdated.json ]]; then
COUNT=$(jq 'length' pip-outdated.json 2>/dev/null || echo "0")
if [[ $COUNT -gt 0 ]]; then
echo " ⚠️ Found $COUNT outdated pip package(s)"
UPDATES_FOUND=$((UPDATES_FOUND + COUNT))
UPDATES_FILES="${UPDATES_FILES}pip:${COUNT};"
echo "- **pip**: $COUNT package(s)" >> $GITHUB_STEP_SUMMARY
fi
else
echo " ✅ All pip packages up to date"
fi
rm -f pip-outdated.json
fi
fi
# Check go.mod (Go)
if [[ -f go.mod ]]; then
echo "🔍 Checking Go dependencies..."
if command -v go &>/dev/null; then
go list -u -m -json all 2>/dev/null | jq -s '[.[] | select(.Update)] | length' > go-updates 2>/dev/null || echo "0" > go-updates
COUNT=$(cat go-updates)
if [[ $COUNT -gt 0 ]]; then
echo " ⚠️ Found $COUNT outdated Go module(s)"
UPDATES_FOUND=$((UPDATES_FOUND + COUNT))
UPDATES_FILES="${UPDATES_FILES}go:${COUNT};"
echo "- **Go**: $COUNT module(s)" >> $GITHUB_STEP_SUMMARY
else
echo " ✅ All Go modules up to date"
fi
rm -f go-updates
fi
fi
echo "has_updates=$([[ $UPDATES_FOUND -gt 0 ]] && echo 'true' || echo 'false')" >> $GITHUB_OUTPUT
echo "updates_count=$UPDATES_FOUND" >> $GITHUB_OUTPUT
echo "updates_files=$UPDATES_FILES" >> $GITHUB_OUTPUT
if [[ $UPDATES_FOUND -eq 0 ]]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "✅ **All dependencies up to date**" >> $GITHUB_STEP_SUMMARY
fi
- name: Update dependencies
if: steps.check_dependencies.outputs.has_updates == 'true' && github.event.inputs.dry_run != 'true'
run: |
echo "## 🔄 Updating Dependencies" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Update npm
if [[ -f package.json ]] && command -v npm &>/dev/null; then
echo "⬆️ Updating npm packages to latest..."
npm update 2>/dev/null || true
npm outdated 2>/dev/null || echo "All up to date"
fi
# Update pip
if [[ -f requirements.txt ]] && command -v pip &>/dev/null; then
echo "⬆️ Updating pip packages to latest..."
pip list --outdated --format=json 2>/dev/null | jq -r '.[] | .name' | xargs -n1 pip install -U 2>/dev/null || true
pip freeze > requirements.txt 2>/dev/null || true
fi
# Update Go
if [[ -f go.mod ]] && command -v go &>/dev/null; then
echo "⬆️ Updating Go modules to latest..."
go get -u ./... 2>/dev/null || true
go mod tidy 2>/dev/null || true
fi
echo "✅ Dependencies updated" >> $GITHUB_STEP_SUMMARY
- name: Create PR for dependency updates
if: steps.check_dependencies.outputs.has_updates == 'true' && github.event.inputs.dry_run != 'true'
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
run: |
# Check if there are changes
if git diff --quiet && git diff --cached --quiet; then
echo "ℹ️ No dependency changes to commit"
exit 0
fi
BRANCH_NAME="chore/update-dependencies-$(date +%Y%m%d-%H%M%S)"
git checkout -b "$BRANCH_NAME"
git add -A
UPDATES_COUNT="${{ steps.check_dependencies.outputs.updates_count }}"
git commit -m "chore: update $UPDATES_COUNT dependencies to latest" \
-m "Automated dependency updates." \
-m "See workflow run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
git push origin "$BRANCH_NAME"
# Ensure automerge label exists before creating PR
gh label create automerge \
--description "Automatically merge this PR" \
--color "3BEF67" \
2>/dev/null || true
gh pr create \
--title "chore: Update $UPDATES_COUNT dependencies to latest" \
--body "## 📦 Automated Dependency Updates\n\nThis PR updates outdated dependencies to their latest versions.\n\n**Updated Files:** ${{ steps.check_dependencies.outputs.updates_files }}\n\n**Triggered by:** [Repository Update Workflow](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})\n\n**Review Checklist:**\n- Check CHANGELOG/release notes for breaking changes\n- Review major version bumps carefully\n- Run tests locally\n- Verify compatibility with existing code\n\nOnce approved, dependencies will be updated to latest stable versions." \
--base "${{ github.ref_name }}" \
--head "$BRANCH_NAME" \
--label "automerge"
echo "" >> $GITHUB_STEP_SUMMARY
echo "📝 **Pull request created** for dependency updates" >> $GITHUB_STEP_SUMMARY
- name: Cleanup stale branches
id: cleanup_branches
if: ${{ github.event.inputs.cleanup_branches != 'false' }}
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
BRANCH_AGE_DAYS: ${{ github.event.inputs.branch_age_days || '5' }}
DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }}
run: |
echo "## 🧹 Stale Branch Cleanup" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
CUTOFF_DATE=$(date -d "$BRANCH_AGE_DAYS days ago" +%s)
DELETED=0
# Get all merged branches (excluding main/master and protected branches)
gh api repos/${{ github.repository }}/branches --paginate | jq -r '.[] | select(.protected == false) | .name' | while read -r branch; do
# Skip main branches
if [[ "$branch" =~ ^(main|master|develop|production)$ ]]; then
continue
fi
# Check if branch is merged
MERGE_STATUS=$(git branch -r --merged origin/${{ github.ref_name }} | grep -c "origin/$branch" || echo "0")
if [[ $MERGE_STATUS -gt 0 ]]; then
# Get last commit date
LAST_COMMIT_DATE=$(git log -1 --format=%ct "origin/$branch" 2>/dev/null || echo "0")
if [[ $LAST_COMMIT_DATE -lt $CUTOFF_DATE ]]; then
DAYS_OLD=$(( ($(date +%s) - LAST_COMMIT_DATE) / 86400 ))
echo "🗑️ Stale branch: $branch (merged $DAYS_OLD days ago)"
if [[ "$DRY_RUN" != "true" ]]; then
git push origin --delete "$branch" 2>/dev/null || true
DELETED=$((DELETED + 1))
echo " ✅ Deleted"
else
echo " 🔍 Would delete (dry-run)"
DELETED=$((DELETED + 1))
fi
echo "- \`$branch\` - $DAYS_OLD days old" >> $GITHUB_STEP_SUMMARY
fi
fi
done
if [[ $DELETED -eq 0 ]]; then
echo "✅ **No stale branches found**" >> $GITHUB_STEP_SUMMARY
else
echo "" >> $GITHUB_STEP_SUMMARY
echo "🗑️ **$DELETED branch(es) cleaned up**" >> $GITHUB_STEP_SUMMARY
fi
- name: Check stale issues and PRs
id: check_stale
if: ${{ github.event.inputs.check_stale_issues != 'false' }}
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
STALE_DAYS: ${{ github.event.inputs.stale_days || '10' }}
DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }}
run: |
echo "## 📝 Stale Issues/PRs Check" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
CUTOFF_DATE=$(date -d "$STALE_DAYS days ago" --iso-8601)
COMMENTED=0
# Check stale issues
echo "🔍 Checking stale issues..."
gh issue list --state open --json number,title,updatedAt --limit 100 | \
jq -r ".[] | select(.updatedAt < \"$CUTOFF_DATE\") | \"\(.number):\(.title)\"" | \
while IFS=':' read -r number title; do
echo " ⚠️ Issue #$number is stale: $title"
if [[ "$DRY_RUN" != "true" ]]; then
gh issue comment "$number" --body "This issue has been inactive for over $STALE_DAYS days. Please provide an update or it may be closed." 2>/dev/null || true
gh issue edit "$number" --add-label "stale" 2>/dev/null || true
COMMENTED=$((COMMENTED + 1))
else
COMMENTED=$((COMMENTED + 1))
fi
echo "- Issue [#$number](${{ github.server_url }}/${{ github.repository }}/issues/$number): $title" >> $GITHUB_STEP_SUMMARY
done
# Check stale PRs
echo "🔍 Checking stale PRs..."
gh pr list --state open --json number,title,updatedAt --limit 100 | \
jq -r ".[] | select(.updatedAt < \"$CUTOFF_DATE\") | \"\(.number):\(.title)\"" | \
while IFS=':' read -r number title; do
echo " ⚠️ PR #$number is stale: $title"
if [[ "$DRY_RUN" != "true" ]]; then
gh pr comment "$number" --body "This PR has been inactive for over $STALE_DAYS days. Please rebase and resolve conflicts, or it may be closed." 2>/dev/null || true
gh pr edit "$number" --add-label "stale" 2>/dev/null || true
COMMENTED=$((COMMENTED + 1))
else
COMMENTED=$((COMMENTED + 1))
fi
echo "- PR [#$number](${{ github.server_url }}/${{ github.repository }}/pull/$number): $title" >> $GITHUB_STEP_SUMMARY
done
if [[ $COMMENTED -eq 0 ]]; then
echo "✅ **No stale issues or PRs found**" >> $GITHUB_STEP_SUMMARY
else
echo "" >> $GITHUB_STEP_SUMMARY
echo "📌 **$COMMENTED item(s) marked as stale**" >> $GITHUB_STEP_SUMMARY
fi
- name: Check and trigger tree visualisation
id: check_tree_viz
if: ${{ github.event.inputs.update_tree_viz != 'false' && github.event.inputs.dry_run != 'true' }}
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
TREE_VIZ_THRESHOLD: ${{ github.event.inputs.tree_viz_threshold || '30' }}
run: |
echo "## 🌳 Tree Visualization Check" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Find the last tree-viz commit
LAST_VIZ_SHA=$(git log --all --oneline --format='%H' -- '.github/static/tree-viz/' | head -1 || echo "")
if [[ -z "$LAST_VIZ_SHA" ]]; then
COMMITS_SINCE=999
echo "ℹ️ No previous tree-viz found — will generate" | tee -a $GITHUB_STEP_SUMMARY
else
COMMITS_SINCE=$(git rev-list --count "${LAST_VIZ_SHA}..HEAD" 2>/dev/null || echo "0")
echo "📊 Commits since last tree-viz: $COMMITS_SINCE (threshold: $TREE_VIZ_THRESHOLD)" | tee -a $GITHUB_STEP_SUMMARY
fi
if [[ $COMMITS_SINCE -ge $TREE_VIZ_THRESHOLD ]]; then
echo "✅ Threshold met — triggering tree-viz workflow" | tee -a $GITHUB_STEP_SUMMARY
gh workflow run tree-viz.yml --ref ${{ github.ref_name }}
echo "triggered=true" >> $GITHUB_OUTPUT
else
echo "ℹ️ Below threshold — skipping ($COMMITS_SINCE < $TREE_VIZ_THRESHOLD)" | tee -a $GITHUB_STEP_SUMMARY
echo "triggered=false" >> $GITHUB_OUTPUT
fi
- name: Amend HEAD for serverless deployment
if: ${{ github.event.inputs.dummy_commit == 'true' && github.event.inputs.dry_run != 'true' }}
run: |
echo "## 🤖 Amending HEAD for Serverless Deployment" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Get original author date from HEAD
AUTHOR_DATE=$(git show -s --format=%aD HEAD)
# Amend HEAD with preserved author date (gcda approach)
# This triggers deployments without changing the commit SHA
GIT_COMMITTER_DATE="$AUTHOR_DATE" git commit --amend --no-edit --date="$AUTHOR_DATE"
git push --force-with-lease origin ${{ github.ref_name }}
echo "✅ **HEAD amended for deployment** (no SHA change)" >> $GITHUB_STEP_SUMMARY
echo "- Original author date preserved" >> $GITHUB_STEP_SUMMARY
echo "- Push timestamp updated (triggers serverless)" >> $GITHUB_STEP_SUMMARY
echo "- Method: \`git commit --amend\` (gcda-style)" >> $GITHUB_STEP_SUMMARY
- name: Summary
if: always()
run: |
echo "" >> $GITHUB_STEP_SUMMARY
echo "---" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Workflow Configuration" >> $GITHUB_STEP_SUMMARY
echo "- **Release check:** ${{ github.event.inputs.check_release != 'false' && '✅ Enabled' || '❌ Disabled' }}" >> $GITHUB_STEP_SUMMARY
echo "- **Submodule check:** ${{ github.event.inputs.check_submodules != 'false' && '✅ Enabled' || '❌ Disabled' }}" >> $GITHUB_STEP_SUMMARY
echo "- **Dependency check:** ${{ github.event.inputs.check_dependencies != 'false' && '✅ Enabled' || '❌ Disabled' }}" >> $GITHUB_STEP_SUMMARY
echo "- **Branch cleanup:** ${{ github.event.inputs.cleanup_branches != 'false' && '✅ Enabled' || '❌ Disabled' }}" >> $GITHUB_STEP_SUMMARY
echo "- **Stale check:** ${{ github.event.inputs.check_stale_issues != 'false' && '✅ Enabled' || '❌ Disabled' }}" >> $GITHUB_STEP_SUMMARY
echo "- **Dummy commit:** ${{ github.event.inputs.dummy_commit == 'true' && '✅ Enabled' || '❌ Disabled' }}" >> $GITHUB_STEP_SUMMARY
echo "- **Tree viz:** ${{ github.event.inputs.update_tree_viz != 'false' && '✅ Enabled' || '❌ Disabled' }}" >> $GITHUB_STEP_SUMMARY
echo "- **Release threshold:** ${{ github.event.inputs.release_threshold || '5' }} commits" >> $GITHUB_STEP_SUMMARY
echo "- **New release ceiling:** ${{ github.event.inputs.new_release_threshold || '30' }} commits" >> $GITHUB_STEP_SUMMARY
echo "- **Tree viz threshold:** ${{ github.event.inputs.tree_viz_threshold || '30' }} commits" >> $GITHUB_STEP_SUMMARY
echo "- **Submodule threshold:** ${{ github.event.inputs.submodule_threshold || '5' }} commits" >> $GITHUB_STEP_SUMMARY
echo "- **Branch age:** ${{ github.event.inputs.branch_age_days || '5' }} days" >> $GITHUB_STEP_SUMMARY
echo "- **Stale threshold:** ${{ github.event.inputs.stale_days || '10' }} days" >> $GITHUB_STEP_SUMMARY
echo "- **Dry run:** ${{ github.event.inputs.dry_run == 'true' && '🔍 Yes' || '❌ No' }}" >> $GITHUB_STEP_SUMMARY