Setup New Repository #26
This file contains hidden or 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
# .github/workflows/setup-new-repo.yml | |
# | |
# Prerequisites: | |
# - A GitHub environment named `repo-setup` must exist. | |
# - The environment must contain a secret `GH_REPO_CREATE_TOKEN`. | |
# - This token must be a fine-grained personal access token (FGPAT) with the following: | |
# • Repository access: Allow access to the template repo and target org repos. | |
# • Permissions: | |
# - Contents: Read and write | |
# - Issues: Read and write | |
# - Metadata: Read-only | |
# - Administration: Read and write (for repo settings, team setup) | |
# | |
# See: https://github.com/settings/tokens for generating tokens | |
# | |
name: Setup New Repository | |
on: | |
workflow_dispatch: | |
inputs: | |
repo_name: | |
description: 'Name of the new repository to create' | |
required: true | |
subproject_name: | |
description: 'Optional subproject/working group name (leave empty for independent sandbox repo)' | |
required: false | |
repo_wiki_page: | |
description: 'URL of the repository wiki page' | |
required: true | |
subproject_wiki_page: | |
description: 'Optional URL of the subproject wiki page' | |
required: false | |
mailinglist_name: | |
description: 'Mailing list name' | |
required: true | |
initial_codeowners: | |
description: 'Space-separated GitHub usernames (with @) for initial CODEOWNERS' | |
required: true | |
jobs: | |
setup: | |
runs-on: ubuntu-latest | |
environment: repo-setup | |
permissions: | |
issues: write | |
contents: write | |
actions: write | |
pull-requests: write | |
steps: | |
- name: Checkout template repository | |
uses: actions/checkout@v4 | |
- name: Set up GitHub CLI token with personal fine grained access token | |
run: | | |
# Use GH_REPO_CREATE_TOKEN as the authentication token for all GitHub CLI commands | |
echo "GH_TOKEN=${{ secrets.GH_REPO_CREATE_TOKEN }}" >> $GITHUB_ENV | |
if [ -z "${{ secrets.GH_REPO_CREATE_TOKEN }}" ]; then | |
echo "::error::GH_REPO_CREATE_TOKEN is not set. Please configure the secret in the 'repo-setup' environment." | |
exit 1 | |
fi | |
- name: Create new repository and set variables | |
run: | | |
REPO_NAME=${{ github.event.inputs.repo_name }} | |
# Extract the owner (user or organization) part of the current repository (before the slash) | |
OWNER=$(echo '${{ github.repository }}' | cut -d'/' -f1) | |
echo "Creating new repository: https://github.com/$OWNER/$REPO_NAME" | |
# Creates a new public repository using the current repository as a template | |
gh repo create "$OWNER/$REPO_NAME" --public --template "$OWNER/$(basename '${{ github.repository }}')" | |
# Wait until the repository is fully accessible | |
for i in {1..5}; do | |
if gh api repos/$OWNER/$REPO_NAME > /dev/null 2>&1; then | |
echo "::notice::Repository $OWNER/$REPO_NAME is now available." | |
break | |
else | |
echo "Waiting for repository $OWNER/$REPO_NAME to become available..." | |
sleep 4 | |
fi | |
done | |
if ! gh api repos/$OWNER/$REPO_NAME > /dev/null 2>&1; then | |
echo "::warning::Repository $OWNER/$REPO_NAME did not become available after 5 attempts. Continuing anyway." | |
fi | |
echo "REPO_NAME=$REPO_NAME" >> $GITHUB_ENV | |
echo "OWNER=$OWNER" >> $GITHUB_ENV | |
echo "MAINTAINERS_TEAM=${REPO_NAME}_maintainers" >> $GITHUB_ENV | |
echo "CODEOWNERS_TEAM=${REPO_NAME}_codeowners" >> $GITHUB_ENV | |
echo "CODEOWNERS_LIST=${{ github.event.inputs.initial_codeowners }}" >> $GITHUB_ENV | |
- name: Create teams | |
run: | | |
if gh api orgs/$OWNER/teams > /dev/null 2>&1; then | |
if ! gh api orgs/$OWNER/teams/$MAINTAINERS_TEAM > /dev/null 2>&1; then | |
# Retrieve the numeric ID of the 'maintainers' team to use as the parent_team_id | |
MAINTAINERS_PARENT_ID=$(gh api orgs/$OWNER/teams/maintainers | jq -r '.id') | |
MAINTAINERS_PARENT_ID_NUM=$(echo "$MAINTAINERS_PARENT_ID" | grep -o '[0-9]*') | |
echo "Debug: MAINTAINERS_PARENT_ID_NUM=$MAINTAINERS_PARENT_ID_NUM" | |
if [ -z "$MAINTAINERS_PARENT_ID_NUM" ]; then | |
echo "::error::Invalid team ID format for maintainers. Expected numeric ID." | |
exit 1 | |
fi | |
echo "{\"name\": \"$MAINTAINERS_TEAM\", \"description\": \"Maintainers for $REPO_NAME repository\", \"parent_team_id\": $MAINTAINERS_PARENT_ID_NUM}" > maintainer_payload.json | |
gh api orgs/$OWNER/teams \ | |
-X POST \ | |
-H "Accept: application/vnd.github+json" \ | |
--input maintainer_payload.json | |
else | |
echo "Team $MAINTAINERS_TEAM already exists. Skipping creation." | |
fi | |
if ! gh api orgs/$OWNER/teams/$CODEOWNERS_TEAM > /dev/null 2>&1; then | |
CODEOWNERS_PARENT_ID=$(gh api orgs/$OWNER/teams/codeowners | jq -r '.id') | |
CODEOWNERS_PARENT_ID_NUM=$(echo "$CODEOWNERS_PARENT_ID" | grep -o '[0-9]*') | |
echo "Debug: CODEOWNERS_PARENT_ID_NUM=$CODEOWNERS_PARENT_ID_NUM" | |
if [ -z "$CODEOWNERS_PARENT_ID_NUM" ]; then | |
echo "::error::Invalid team ID format for codeowners. Expected numeric ID." | |
exit 1 | |
fi | |
echo "{\"name\": \"$CODEOWNERS_TEAM\", \"description\": \"Codeowners for $REPO_NAME repository\", \"parent_team_id\": $CODEOWNERS_PARENT_ID_NUM}" > codeowners_payload.json | |
gh api orgs/$OWNER/teams \ | |
-X POST \ | |
-H "Accept: application/vnd.github+json" \ | |
--input codeowners_payload.json | |
else | |
echo "Team $CODEOWNERS_TEAM already exists. Skipping creation." | |
fi | |
CODEOWNERS_LIST=$(echo "$CODEOWNERS_LIST" | xargs) | |
# Loop through each provided GitHub username and invite them to the codeowners team | |
# Note: users who are not yet members of the organization will be invited to join | |
# and will need to accept the invitation before they are part of the team and the CODEOWNERS file get fully valid | |
echo "Inviting users to team $CODEOWNERS_TEAM: [$CODEOWNERS_LIST]" | |
for username in $CODEOWNERS_LIST; do | |
clean_user=$(echo "$username" | sed 's/^@//') | |
echo "Checking if @$clean_user is a valid GitHub user..." | |
if gh api users/$clean_user > /dev/null 2>&1; then | |
echo "Inviting @$clean_user to team $CODEOWNERS_TEAM" | |
gh api orgs/$OWNER/teams/$CODEOWNERS_TEAM/memberships/$clean_user -X PUT -f role=member || echo "Failed to invite $clean_user" | |
else | |
echo "::warning::User @$clean_user does not exist or cannot be looked up. Skipping." | |
fi | |
done | |
else | |
echo "Skipping team creation — not running in an organization." | |
fi | |
- name: Configure repository settings | |
run: | | |
gh repo edit $OWNER/$REPO_NAME \ | |
--description "Sandbox API Repository for $REPO_NAME API(s)" \ | |
--homepage "${{ github.event.inputs.repo_wiki_page }}" \ | |
--add-topic sandbox-api-repository | |
gh api -X PATCH repos/$OWNER/$REPO_NAME \ | |
-F has_discussions=true \ | |
-F has_issues=true \ | |
-F has_wiki=false | |
- name: Update README.md placeholders | |
run: | | |
# Replace placeholders in the template README.md and push the updated version to the new repository | |
# Note: the README.md file is expected to be in the root of the repository | |
sed -i "s/{{repo_name}}/$REPO_NAME/g" README.md | |
sed -i "s|{{repo_wiki_page}}|${{ github.event.inputs.repo_wiki_page }}|g" README.md | |
sed -i "s|{{subproject_name}}|${{ github.event.inputs.subproject_name }}|g" README.md | |
sed -i "s|{{subproject_wiki_page}}|${{ github.event.inputs.subproject_wiki_page }}|g" README.md | |
sed -i "s|{{mailinglist_name}}|${{ github.event.inputs.mailinglist_name }}|g" README.md | |
sed -i "s|{{initial_codeowners}}|${{ github.event.inputs.initial_codeowners }}|g" README.md | |
# Retry loop: waits for README.md to appear in the new repo (max 5 attempts) | |
SHA="" | |
for i in {1..5}; do | |
SHA=$(gh api repos/$OWNER/$REPO_NAME/contents/README.md 2>/dev/null | jq -r '.sha') | |
if [ "$SHA" != "null" ] && [ -n "$SHA" ]; then | |
echo "Found README.md sha: $SHA" | |
break | |
else | |
echo "README.md not yet available, retrying in 2s..." | |
sleep 2 | |
fi | |
done | |
gh api repos/$OWNER/$REPO_NAME/contents/README.md \ | |
-X PUT \ | |
-F message='Update README.md with project metadata' \ | |
-F content="$(base64 -w 0 README.md)" \ | |
-F sha="$SHA" | |
- name: Set team permissions | |
run: | | |
if gh api orgs/$OWNER/teams > /dev/null 2>&1; then | |
if gh api orgs/$OWNER/teams/$MAINTAINERS_TEAM > /dev/null 2>&1; then | |
gh api orgs/$OWNER/teams/$MAINTAINERS_TEAM/repos/$OWNER/$REPO_NAME \ | |
-X PUT -H "Accept: application/vnd.github+json" -f permission=triage || \ | |
echo "::error::Failed to set permissions for $MAINTAINERS_TEAM. Please check team and repository availability." | |
else | |
echo "::error::Team $MAINTAINERS_TEAM does not exist. Cannot assign permissions." | |
exit 1 | |
fi | |
if gh api orgs/$OWNER/teams/$CODEOWNERS_TEAM > /dev/null 2>&1; then | |
echo "Assigning permissions to CODEOWNERS_TEAM: [$CODEOWNERS_TEAM]" | |
gh api orgs/$OWNER/teams/$CODEOWNERS_TEAM/repos/$OWNER/$REPO_NAME \ | |
-X PUT -H "Accept: application/vnd.github+json" -f permission=push || \ | |
echo "::error::Failed to set permissions for $CODEOWNERS_TEAM. Please check team and repository availability." | |
else | |
echo "::error::Team $CODEOWNERS_TEAM does not exist. Cannot assign permissions." | |
exit 1 | |
fi | |
gh api orgs/$OWNER/teams/admins/repos/$OWNER/$REPO_NAME \ | |
-X PUT -H "Accept: application/vnd.github+json" -f permission=maintain || \ | |
echo "::error::Failed to set permissions for admin team. Please check team and repository availability." | |
else | |
echo "Skipping team permission assignment — not running in an organization." | |
fi | |
- name: Update CODEOWNERS file | |
run: | | |
# Replace placeholder in CODEOWNERS template with actual list of codeowners | |
# Note: also users who are skipped during team invitation will be added to the CODEOWNERS file and | |
# might need to be corrected manually later and invited manually to the CODEOWNERS team | |
sed "s|{{initial_codeowners}}|$CODEOWNERS_LIST|g" templates/CODEOWNERS_TEMPLATE > CODEOWNERS | |
CODEOWNERS_SHA=$(gh api repos/$OWNER/$REPO_NAME/contents/CODEOWNERS | jq -r '.sha') | |
gh api repos/$OWNER/$REPO_NAME/contents/CODEOWNERS \ | |
-X PUT \ | |
-F message='Update CODEOWNERS from template' \ | |
-F content="$(base64 -w 0 CODEOWNERS)" \ | |
-F sha="$CODEOWNERS_SHA" | |
- name: Sync rulesets from template repository | |
run: | | |
TEMPLATE_REPO=$(basename "${{ github.repository }}") | |
echo "Fetching rulesets from $OWNER/$TEMPLATE_REPO" | |
# Fetch all rulesets defined in the template repository for later replication | |
RULESETS=$(gh api repos/$OWNER/$TEMPLATE_REPO/rulesets \ | |
-H "Accept: application/vnd.github+json" 2>/dev/null || echo "[]") | |
if ! echo "$RULESETS" | jq -e 'type == "array" and length > 0' > /dev/null; then | |
echo "No valid rulesets array found in template repository. Skipping." | |
exit 0 | |
fi | |
echo "$RULESETS" | jq -r '.[].id' | while read -r ruleset_id; do | |
RULESET=$(gh api repos/$OWNER/$TEMPLATE_REPO/rulesets/$ruleset_id \ | |
-H "Accept: application/vnd.github+json") | |
NAME=$(echo "$RULESET" | jq -r '.name') | |
echo "Syncing full ruleset: $NAME" | |
PAYLOAD=$(echo "$RULESET" | jq 'del(.id, .repository_id, .creator, .created_at, .updated_at)') | |
echo "$PAYLOAD" > ruleset.json | |
gh api repos/$OWNER/$REPO_NAME/rulesets \ | |
-X POST \ | |
-H "Accept: application/vnd.github+json" \ | |
--input ruleset.json || echo "Warning: Failed to apply ruleset $NAME" | |
done | |
- name: Create initial issues | |
run: | | |
ADMIN_ISSUE_URL=$(gh issue create --repo $OWNER/$REPO_NAME \ | |
--title "New Repository - Initial administrative tasks" \ | |
--body "$(cat templates/issues/initial-admin.md)") | |
gh issue create --repo $OWNER/$REPO_NAME \ | |
--title "New Repository - Initial tasks for codeowners" \ | |
--body "$(cat templates/issues/initial-codeowners.md)" | |
gh issue comment "$ADMIN_ISSUE_URL" \ | |
--body "✅ Repository setup has been completed by automation. You may now proceed with the checklist." | |
- name: Cleanup setup artifacts from template repository | |
run: | | |
# Explicit list of files to remove after setup (e.g., templates and placeholders) | |
FILES_TO_DELETE=( | |
"templates/CODEOWNERS" | |
"templates/issues/initial-admin.md" | |
"templates/issues/initial-codeowners.md" | |
"templates/README.md" | |
) | |
for file in "${FILES_TO_DELETE[@]}"; do | |
if gh api repos/$OWNER/$REPO_NAME/contents/$file > /dev/null 2>&1; then | |
sha=$(gh api repos/$OWNER/$REPO_NAME/contents/$file | jq -r '.sha') | |
gh api repos/$OWNER/$REPO_NAME/contents/$file \ | |
-X DELETE \ | |
-F message="Remove $file from template repository" \ | |
-F sha="$sha" || echo "::error::Failed to delete $file" | |
echo "::notice::Deleted $file" | |
else | |
echo "::warning::File $file not found during cleanup. Skipping." | |
fi | |
done |