Skip to content

Sync Actions from gh-aw #9

Sync Actions from gh-aw

Sync Actions from gh-aw #9

Workflow file for this run

name: Sync Actions from gh-aw
on:
workflow_dispatch:
inputs:
ref:
description: 'Ref to sync from gh-aw (tag, branch, SHA, or "latest"). Defaults to latest release.'
required: false
default: 'latest'
type: string
jobs:
sync:
name: Sync Actions
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Log workflow context
run: |
echo "::group::Workflow Context"
echo "Repository: ${{ github.repository }}"
echo "Actor: $ACTOR"
echo "Event: ${{ github.event_name }}"
echo "Ref: $GH_REF"
echo "Run ID: ${{ github.run_id }}"
echo "Run number: ${{ github.run_number }}"
echo "Workflow dispatch input ref: $INPUT_REF"
echo "::endgroup::"
env:
GH_REF: ${{ github.ref }}
ACTOR: ${{ github.actor }}
INPUT_REF: ${{ inputs.ref }}
- name: Check repository is not a fork
run: |
echo "::group::Fork Check"
REPO="${{ github.repository }}"
echo "Checking if '$REPO' is a fork..."
IS_FORK=$(gh api "repos/$REPO" --jq '.fork')
echo "Is fork: $IS_FORK"
if [[ "$IS_FORK" == "true" ]]; then
echo "::error::This workflow is disabled on forks. Repository '$REPO' is a fork."
exit 1
fi
echo "Repository '$REPO' is not a fork. ✓"
echo "::endgroup::"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Check actor has admin or maintainer role
run: |
echo "::group::Permission Check"
ACTOR="$GITHUB_ACTOR"
REPO="${{ github.repository }}"
echo "Checking permissions for actor '$ACTOR' on '$REPO'..."
ROLE=$(gh api "repos/$REPO/collaborators/$ACTOR/permission" --jq '.role_name' 2>/dev/null || echo "none")
echo "Actor role: $ROLE"
if [[ "$ROLE" != "admin" && "$ROLE" != "maintain" ]]; then
echo "::error::Actor '$ACTOR' does not have the required role (role: '$ROLE'). This workflow requires admin or maintainer access."
exit 1
fi
echo "Actor '$ACTOR' has required permissions (role: $ROLE). ✓"
echo "::endgroup::"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Resolve ref
id: resolve-ref
run: |
echo "::group::Resolving Ref"
# Determine raw ref from event inputs
RAW_REF="${INPUT_REF}"
# Default to 'latest' if empty
if [[ -z "$RAW_REF" ]]; then
RAW_REF="latest"
fi
echo "Raw ref: $RAW_REF"
# Validate ref format: must be a semver (vMAJOR.MINOR.PATCH), a 40-char SHA, or "latest"
SEMVER_PATTERN='^v[0-9]+\.[0-9]+\.[0-9]+$'
if [[ "$RAW_REF" != "latest" && ! "$RAW_REF" =~ $SEMVER_PATTERN && ! "$RAW_REF" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "::error::Invalid ref '$RAW_REF'. Must be a semver version (vMAJOR.MINOR.PATCH), a 40-character SHA, or 'latest'."
exit 1
fi
# Verify the ref exists in github/gh-aw and is reachable from the main branch
# (skip for 'latest', which is resolved to the newest release below)
if [[ "$RAW_REF" != "latest" ]]; then
echo "Verifying '$RAW_REF' exists in github/gh-aw..."
API_ERROR=$(mktemp)
COMMIT_SHA=$(gh api "repos/github/gh-aw/commits/$RAW_REF" --jq '.sha' 2>"$API_ERROR" || echo "")
if [[ -z "$COMMIT_SHA" ]]; then
if [[ -s "$API_ERROR" ]]; then
echo "::error::Failed to verify ref '$RAW_REF' in github/gh-aw: $(cat "$API_ERROR")"
else
echo "::error::Ref '$RAW_REF' was not found in the github/gh-aw repository."
fi
rm -f "$API_ERROR"
exit 1
fi
rm -f "$API_ERROR"
echo "Ref '$RAW_REF' resolved to commit $COMMIT_SHA."
# Confirm the commit is reachable from the main branch.
# compare/main...<sha>: status "behind" or "identical" means the commit
# is an ancestor of (or equal to) main HEAD.
echo "Checking that $COMMIT_SHA is in the main branch history of github/gh-aw..."
API_ERROR=$(mktemp)
COMPARE_STATUS=$(gh api "repos/github/gh-aw/compare/main...$COMMIT_SHA" --jq '.status' 2>"$API_ERROR" || echo "")
if [[ -z "$COMPARE_STATUS" ]]; then
echo "::error::Failed to compare ref '$RAW_REF' against main in github/gh-aw: $(cat "$API_ERROR")"
rm -f "$API_ERROR"
exit 1
fi
rm -f "$API_ERROR"
if [[ "$COMPARE_STATUS" != "behind" && "$COMPARE_STATUS" != "identical" ]]; then
echo "::error::Ref '$RAW_REF' (commit $COMMIT_SHA) is not in the main branch history of github/gh-aw (compare status: '$COMPARE_STATUS')."
exit 1
fi
echo "Ref '$RAW_REF' is in the main branch history of github/gh-aw. ✓"
fi
if [[ "$RAW_REF" == "latest" ]]; then
echo "Resolving 'latest' to the most recent gh-aw release..."
LATEST_TAG=$(gh api repos/github/gh-aw/releases/latest --jq '.tag_name' 2>/dev/null || echo "")
if [[ -n "$LATEST_TAG" ]]; then
RESOLVED_REF="$LATEST_TAG"
echo "Latest release tag resolved to: $RESOLVED_REF"
else
echo "::warning::No releases found in gh-aw. Falling back to main branch HEAD."
RESOLVED_REF=$(gh api repos/github/gh-aw/commits/main --jq '.sha')
echo "Using main HEAD SHA: $RESOLVED_REF"
fi
else
RESOLVED_REF="$RAW_REF"
echo "Using provided ref: $RESOLVED_REF"
fi
# Determine whether to create a tag after syncing.
# Create a tag when the ref is not a full 40-character SHA and not "latest".
if [[ "$RAW_REF" =~ ^[0-9a-fA-F]{40}$ ]]; then
IS_LONG_SHA="true"
else
IS_LONG_SHA="false"
fi
if [[ "$RAW_REF" =~ $SEMVER_PATTERN ]]; then
IS_SEMVER="true"
TAG_REF="$RAW_REF"
elif [[ "$RAW_REF" == "latest" && "$RESOLVED_REF" =~ $SEMVER_PATTERN ]]; then
IS_SEMVER="true"
TAG_REF="$RESOLVED_REF"
else
IS_SEMVER="false"
TAG_REF="$RAW_REF"
fi
if [[ "$IS_SEMVER" == "true" ]]; then
SHOULD_CREATE_TAG="true"
else
SHOULD_CREATE_TAG="false"
fi
echo "Resolved ref: $RESOLVED_REF"
echo "Is long SHA: $IS_LONG_SHA"
echo "Is semver: $IS_SEMVER"
echo "Should create tag: $SHOULD_CREATE_TAG"
echo "Tag ref: $TAG_REF"
echo "resolved_ref=$RESOLVED_REF" >> "$GITHUB_OUTPUT"
echo "raw_ref=$RAW_REF" >> "$GITHUB_OUTPUT"
echo "should_create_tag=$SHOULD_CREATE_TAG" >> "$GITHUB_OUTPUT"
echo "is_semver=$IS_SEMVER" >> "$GITHUB_OUTPUT"
echo "tag_ref=$TAG_REF" >> "$GITHUB_OUTPUT"
echo "::endgroup::"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
INPUT_REF: ${{ inputs.ref }}
- name: Checkout gh-aw-actions (this repository)
uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v4.2.2
with:
ref: main
token: ${{ secrets.GITHUB_TOKEN }}
path: gh-aw-actions
- name: Checkout gh-aw at resolved ref (actions/ only)
uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v4.2.2
with:
repository: github/gh-aw
ref: ${{ steps.resolve-ref.outputs.resolved_ref }}
token: ${{ secrets.GITHUB_TOKEN }}
path: gh-aw
sparse-checkout: actions
sparse-checkout-cone-mode: true
- name: Log source actions directory
run: |
echo "::group::Source — gh-aw/actions/ at ref '${{ steps.resolve-ref.outputs.resolved_ref }}'"
find gh-aw/actions -type f | sort
echo "::endgroup::"
- name: Log destination before sync
run: |
echo "::group::Destination — gh-aw-actions/ (action folders) before sync"
for dir in gh-aw/actions/*/; do
name=$(basename "$dir")
echo "--- $name/ ---"
find "gh-aw-actions/$name" -type f 2>/dev/null | sort || echo "(does not exist yet)"
done
echo "::endgroup::"
- name: Sync action folders (remote wins, local README preserved)
run: |
echo "::group::Syncing action folders from gh-aw/actions/ to repo root"
# --archive preserves permissions/timestamps; --delete removes files absent in source (remote wins)
# --exclude='README*' preserves any local README files in each destination folder
for dir in gh-aw/actions/*/; do
name=$(basename "$dir")
echo "Syncing: gh-aw/actions/$name/ -> gh-aw-actions/$name/"
rsync --archive --verbose --delete --exclude='README*' "gh-aw/actions/$name/" "gh-aw-actions/$name/"
done
echo "::endgroup::"
- name: Log destination after sync
run: |
echo "::group::Destination — gh-aw-actions/ (action folders) after sync"
for dir in gh-aw/actions/*/; do
name=$(basename "$dir")
echo "--- $name/ ---"
find "gh-aw-actions/$name" -type f | sort
done
echo "::endgroup::"
- name: Create branch, commit, and open pull request
id: create-pr
run: |
echo "::group::Git Commit"
cd gh-aw-actions
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
for dir in ../gh-aw/actions/*/; do
name=$(basename "$dir")
git add -A "$name/"
done
if git diff --staged --quiet; then
echo "No changes detected — nothing to commit."
echo "changed=false" >> "$GITHUB_OUTPUT"
echo "pr_number=" >> "$GITHUB_OUTPUT"
else
echo "Changes staged for commit:"
git diff --staged --stat
echo ""
BRANCH="sync/gh-aw-${{ steps.resolve-ref.outputs.resolved_ref }}"
git checkout -b "$BRANCH"
COMMIT_MSG="chore: sync actions from gh-aw@${{ steps.resolve-ref.outputs.resolved_ref }}"
git commit -m "$COMMIT_MSG"
git push origin "$BRANCH"
PR_URL=$(gh pr create \
--title "$COMMIT_MSG" \
--body "Automated sync of actions from [gh-aw](https://github.com/github/gh-aw) at \`${{ steps.resolve-ref.outputs.resolved_ref }}\`." \
--base main \
--head "$BRANCH")
PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$')
echo ""
echo "Pull request #$PR_NUMBER created: $PR_URL ✓"
echo "changed=true" >> "$GITHUB_OUTPUT"
echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
fi
echo "::endgroup::"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Poll pull request until merged
if: steps.create-pr.outputs.changed == 'true'
run: |
echo "::group::Polling PR #${{ steps.create-pr.outputs.pr_number }}"
PR_NUMBER="${{ steps.create-pr.outputs.pr_number }}"
TIMEOUT=1200
INTERVAL=30
ELAPSED=0
while [[ $ELAPSED -lt $TIMEOUT ]]; do
if ! STATE=$(gh pr view "$PR_NUMBER" --repo "${{ github.repository }}" --json state --jq '.state' 2>&1); then
echo "::warning::Failed to query PR state (elapsed: ${ELAPSED}s): $STATE"
else
echo "PR #$PR_NUMBER state: $STATE (elapsed: ${ELAPSED}s / ${TIMEOUT}s)"
if [[ "$STATE" == "MERGED" ]]; then
echo "Pull request #$PR_NUMBER has been merged. ✓"
echo "::endgroup::"
exit 0
fi
if [[ "$STATE" == "CLOSED" ]]; then
echo "::error::Pull request #$PR_NUMBER was closed without merging."
echo "::endgroup::"
exit 1
fi
fi
sleep $INTERVAL
ELAPSED=$((ELAPSED + INTERVAL))
done
echo "::error::Timed out after ${TIMEOUT}s waiting for pull request #$PR_NUMBER to be merged."
echo "::endgroup::"
exit 1
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create tag
if: steps.resolve-ref.outputs.should_create_tag == 'true'
run: |
echo "::group::Creating Tag"
cd gh-aw-actions
# When a PR was created and merged, we need to pull the merge commit from main
# because the working directory is still on the sync branch.
if [[ "${{ steps.create-pr.outputs.changed }}" == "true" ]]; then
echo "PR was merged — fetching main to tag the merge commit..."
git fetch origin main
git checkout -B main origin/main
fi
TAG="${{ steps.resolve-ref.outputs.tag_ref }}"
echo "Creating tag: $TAG"
# If the tag already exists, delete and re-create it so it points to the
# new commit produced by this sync run. Re-running a sync for the same
# named ref (e.g. a branch name or a short alias) is an explicit request
# to advance the tag, so force-updating is the expected behaviour.
if git tag -d "$TAG" 2>/dev/null; then
echo "Deleted existing local tag '$TAG' (will re-create at new HEAD)."
fi
if git push origin ":refs/tags/$TAG" 2>/dev/null; then
echo "Deleted existing remote tag '$TAG' (will re-create at new HEAD)."
fi
git tag -a "$TAG" -m "Sync from gh-aw@$TAG"
git push origin "$TAG"
echo "Tag '$TAG' created and pushed. ✓"
# If the ref is a semver, also update the vMAJOR and vMAJOR.MINOR floating tags
if [[ "${{ steps.resolve-ref.outputs.is_semver }}" == "true" ]]; then
# These sed patterns are safe: the format was already validated as vMAJOR.MINOR.PATCH above
MAJOR_TAG="v$(echo "$TAG" | sed 's/^v\([0-9]*\)\..*/\1/')"
MINOR_TAG="v$(echo "$TAG" | sed 's/^v\([0-9]*\.[0-9]*\)\..*/\1/')"
for FLOATING_TAG in "$MAJOR_TAG" "$MINOR_TAG"; do
echo "Updating floating tag: $FLOATING_TAG"
if git tag -d "$FLOATING_TAG" 2>/dev/null; then
echo "Deleted existing local tag '$FLOATING_TAG'."
fi
if git push origin ":refs/tags/$FLOATING_TAG" 2>/dev/null; then
echo "Deleted existing remote tag '$FLOATING_TAG'."
fi
git tag -a "$FLOATING_TAG" -m "Sync from gh-aw@$TAG"
git push origin "$FLOATING_TAG"
echo "Floating tag '$FLOATING_TAG' updated. ✓"
done
fi
echo "::endgroup::"