Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 175 additions & 0 deletions .github/workflows/bootstrap-copilot-sync.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
name: Bootstrap Copilot Sync
# One-time bootstrap workflow that creates PRs in all Cratis repositories to add
# thin wrapper workflows that delegate to the reusable workflows in this repository.
# Trigger this workflow manually once to set up all repositories.
# Requires PAT_DOCUMENTATION secret with permissions:
# - repo (to create branches, push files, and open pull requests)

on:
workflow_dispatch:

permissions:
contents: read

jobs:
bootstrap:
runs-on: ubuntu-latest

steps:
- name: Get all Cratis repositories
id: get-repos
env:
GH_TOKEN: ${{ secrets.PAT_DOCUMENTATION }}
run: |
# List all non-archived repos (limit 1000 covers foreseeable growth)
repos=$(gh repo list Cratis --limit 1000 --json name,isArchived \
--jq '[.[] | select(.isArchived == false) | .name]')
echo "repos=$repos" >> $GITHUB_OUTPUT

- name: Create PRs in all repositories
env:
GH_TOKEN: ${{ secrets.PAT_DOCUMENTATION }}
run: |
repos='${{ steps.get-repos.outputs.repos }}'

echo "$repos" | jq -r '.[]' | while read -r repo; do
# Skip this repository (Workflows) — it holds the reusable workflows
if [ "$repo" = "Workflows" ]; then
echo "Skipping Workflows (this repository)"
continue
fi

echo "Processing Cratis/$repo..."

# Get default branch SHA
default_branch=$(gh api repos/Cratis/$repo --jq '.default_branch' 2>/dev/null)
if [ -z "$default_branch" ]; then
echo " ⚠ Could not get default branch for $repo, skipping"
continue
fi

sha=$(gh api repos/Cratis/$repo/git/ref/heads/$default_branch \
--jq '.object.sha' 2>/dev/null)
if [ -z "$sha" ]; then
echo " ⚠ Could not get SHA for $repo, skipping"
continue
fi

branch="add-copilot-sync-workflows"

# Create the branch (ignore error if it already exists)
gh api --method POST repos/Cratis/$repo/git/refs \
-f ref="refs/heads/$branch" \
-f sha="$sha" 2>/dev/null || true

# Content for sync wrapper workflow
sync_content=$(cat <<'SYNC_EOF'
name: Sync Copilot Instructions

on:
workflow_dispatch:
inputs:
source_repository:
description: 'Source repository (owner/repo format)'
required: true
type: string

jobs:
sync:
uses: Cratis/Workflows/.github/workflows/sync-copilot-instructions.yml@main
with:
source_repository: ${{ inputs.source_repository }}
secrets: inherit
SYNC_EOF
)

# Content for propagate wrapper workflow
propagate_content=$(cat <<'PROPAGATE_EOF'
name: Propagate Copilot Instructions

on:
push:
branches: ["main"]
paths:
- ".github/copilot-instructions.md"
- ".github/instructions/**"
- ".github/agents/**"

jobs:
propagate:
uses: Cratis/Workflows/.github/workflows/propagate-copilot-instructions.yml@main
secrets: inherit
PROPAGATE_EOF
)

# Ensure .github/workflows directory path
sync_path=".github/workflows/sync-copilot-instructions.yml"
propagate_path=".github/workflows/propagate-copilot-instructions.yml"

sync_b64=$(echo "$sync_content" | base64 -w 0)
propagate_b64=$(echo "$propagate_content" | base64 -w 0)

# Push sync workflow file (update if exists, create if not)
existing_sha=$(gh api repos/Cratis/$repo/contents/$sync_path \
--jq '.sha' 2>/dev/null || true)

if [ -n "$existing_sha" ]; then
gh api --method PUT repos/Cratis/$repo/contents/$sync_path \
-f message="Add sync-copilot-instructions workflow" \
-f content="$sync_b64" \
-f sha="$existing_sha" \
-f branch="$branch" 2>/dev/null || \
echo " ⚠ Could not update sync workflow in $repo"
else
gh api --method PUT repos/Cratis/$repo/contents/$sync_path \
-f message="Add sync-copilot-instructions workflow" \
-f content="$sync_b64" \
-f branch="$branch" 2>/dev/null || \
echo " ⚠ Could not create sync workflow in $repo"
fi

# Push propagate workflow file
existing_sha=$(gh api repos/Cratis/$repo/contents/$propagate_path \
--jq '.sha' 2>/dev/null || true)

if [ -n "$existing_sha" ]; then
gh api --method PUT repos/Cratis/$repo/contents/$propagate_path \
-f message="Add propagate-copilot-instructions workflow" \
-f content="$propagate_b64" \
-f sha="$existing_sha" \
-f branch="$branch" 2>/dev/null || \
echo " ⚠ Could not update propagate workflow in $repo"
else
gh api --method PUT repos/Cratis/$repo/contents/$propagate_path \
-f message="Add propagate-copilot-instructions workflow" \
-f content="$propagate_b64" \
-f branch="$branch" 2>/dev/null || \
echo " ⚠ Could not create propagate workflow in $repo"
fi

# Create PR
existing_pr=$(gh pr list --repo Cratis/$repo \
--head "$branch" --json number --jq '.[0].number' 2>/dev/null || true)

if [ -z "$existing_pr" ]; then
if gh pr create \
--repo "Cratis/$repo" \
--title "Add Copilot sync workflows" \
--body "Adds thin wrapper workflows that delegate to the reusable workflows in [Cratis/Workflows](https://github.com/Cratis/Workflows).

### Workflows added

- \`.github/workflows/sync-copilot-instructions.yml\` — triggered via \`workflow_dispatch\` to pull Copilot instructions from a source repository and open a PR with the changes.
- \`.github/workflows/propagate-copilot-instructions.yml\` — triggered on push to \`main\` when Copilot instruction files change, propagating updates to all Cratis repositories.

The actual logic lives in [Cratis/Workflows](https://github.com/Cratis/Workflows) so it can be maintained in one place." \
--head "$branch" \
--base "$default_branch" 2>/dev/null; then
echo " ✓ Created PR for $repo"
else
echo " ⚠ Could not create PR for $repo"
fi
else
echo " ℹ PR already exists for $repo (#$existing_pr)"
fi
done
59 changes: 59 additions & 0 deletions .github/workflows/propagate-copilot-instructions.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Propagate Copilot Instructions
# Reusable workflow that propagates Copilot instruction changes from the calling
# repository to all other repositories in the Cratis organization.
# This workflow requires PAT_DOCUMENTATION secret with permissions:
# - repo (to list repositories and trigger workflows)

on:
workflow_call:
secrets:
PAT_DOCUMENTATION:
required: true

permissions:
contents: read

jobs:
propagate:
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'Cratis' }}

steps:
- name: Get all Cratis repositories
id: get-repos
env:
GH_TOKEN: ${{ secrets.PAT_DOCUMENTATION }}
run: |
# Get all repositories in the Cratis organization (limit 1000 covers foreseeable growth)
repos=$(gh repo list Cratis --limit 1000 --json name --jq '.[].name')

# Convert to JSON array
echo "repos<<EOF" >> $GITHUB_OUTPUT
echo "$repos" | jq -R -s -c 'split("\n") | map(select(length > 0))' >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT

- name: Trigger sync workflow for each repository
env:
GH_TOKEN: ${{ secrets.PAT_DOCUMENTATION }}
run: |
repos='${{ steps.get-repos.outputs.repos }}'
source_repo="${{ github.repository }}"
source_repo_name="${{ github.event.repository.name }}"

echo "Triggering sync for repositories in Cratis organization (source: $source_repo)..."
echo "$repos" | jq -r '.[]' | while read -r repo; do
# Skip the source repository to avoid infinite loop
if [ "$repo" = "$source_repo_name" ]; then
echo "Skipping $repo (source repository)"
continue
fi

echo "Triggering sync for Cratis/$repo..."
if gh workflow run "sync-copilot-instructions.yml" \
--repo "Cratis/$repo" \
--raw-field source_repository="$source_repo" 2>&1; then
echo " ✓ Successfully triggered workflow for $repo"
else
echo " ⚠ Could not trigger workflow for $repo (workflow may not exist)"
fi
done
102 changes: 102 additions & 0 deletions .github/workflows/sync-copilot-instructions.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
name: Sync Copilot Instructions
# Reusable workflow that synchronizes Copilot instructions from a source repository.
# This workflow requires PAT_DOCUMENTATION secret with permissions:
# - contents: write (to checkout and modify files)
# - pull-requests: write (to create pull requests)

on:
workflow_call:
inputs:
source_repository:
description: 'Source repository (owner/repo format)'
required: true
type: string
secrets:
PAT_DOCUMENTATION:
required: true

permissions:
contents: write
pull-requests: write

jobs:
sync-instructions:
runs-on: ubuntu-latest

steps:
- name: Checkout current repository
uses: actions/checkout@v4
with:
ref: main
token: ${{ secrets.PAT_DOCUMENTATION }}

- name: Validate source repository input
run: |
input="${{ inputs.source_repository }}"
if ! echo "$input" | grep -qE '^[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+$'; then
echo "Error: Invalid repository format. Expected 'owner/repo'"
exit 1
fi
owner="${input%%/*}"
if [ "$owner" != "Cratis" ]; then
echo "Error: Source repository must belong to the Cratis organization (got '$owner')"
exit 1
fi

- name: Clone source repository
env:
GH_TOKEN: ${{ secrets.PAT_DOCUMENTATION }}
run: |
gh repo clone "${{ inputs.source_repository }}" /tmp/source-repo

- name: Extract and copy Copilot instructions
run: |
mkdir -p .github

# Copy root Copilot instructions file if it exists
if [ -f "/tmp/source-repo/.github/copilot-instructions.md" ]; then
cp /tmp/source-repo/.github/copilot-instructions.md .github/copilot-instructions.md
echo "Copied copilot-instructions.md"
fi

# Copy instructions folder if it exists in source
if [ -d "/tmp/source-repo/.github/instructions" ]; then
rm -rf .github/instructions
cp -r /tmp/source-repo/.github/instructions .github/instructions
echo "Copied instructions folder"
fi

# Copy agents folder if it exists in source
if [ -d "/tmp/source-repo/.github/agents" ]; then
rm -rf .github/agents
cp -r /tmp/source-repo/.github/agents .github/agents
echo "Copied agents folder"
fi

- name: Configure git
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"

- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.PAT_DOCUMENTATION }}
commit-message: "Copilot Instructions synchronization from ${{ inputs.source_repository }}"
branch: "copilot-sync/${{ github.run_id }}-${{ github.run_attempt }}"
delete-branch: true
title: "Sync Copilot Instructions from ${{ inputs.source_repository }}"
body: |
This PR synchronizes Copilot instructions from [${{ inputs.source_repository }}](https://github.com/${{ inputs.source_repository }}).

### Changes include:
- Updated `.github/copilot-instructions.md` (if present)
- Updated `.github/instructions/` folder (if present)
- Updated `.github/agents/` folder (if present)

**Source repository:** ${{ inputs.source_repository }}
**Triggered by workflow run:** ${{ github.run_id }}
**Run attempt:** ${{ github.run_attempt }}
labels: |
automated
copilot-sync
Loading