diff --git a/.github/workflows/setup-new-repo.yml b/.github/workflows/setup-new-repo.yml new file mode 100644 index 0000000..085f364 --- /dev/null +++ b/.github/workflows/setup-new-repo.yml @@ -0,0 +1,341 @@ +# .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 + + env: + TEMPLATE_REPO_NAME: Template_API_Repository + + steps: + + - name: Checkout template repository + uses: actions/checkout@v4 + with: + repository: camaraproject/${{ env.TEMPLATE_REPO_NAME }} + token: ${{ secrets.GH_REPO_CREATE_TOKEN }} + + - 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 template repository as a template + gh repo create "$OWNER/$REPO_NAME" --public --template "$OWNER/${{ env.TEMPLATE_REPO_NAME }}" + + # 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: Update issue template config placeholders + run: | + # Update .github/ISSUE_TEMPLATE/config.yml placeholders + CONFIG_FILE=".github/ISSUE_TEMPLATE/config.yml" + if [ -f "$CONFIG_FILE" ]; then + echo "Updating $CONFIG_FILE placeholders..." + sed -i "s/{{repo_name}}/$REPO_NAME/g" $CONFIG_FILE + + # Wait for the config.yml file to be available in the new repository + CONFIG_SHA="" + for i in {1..5}; do + CONFIG_SHA=$(gh api repos/$OWNER/$REPO_NAME/contents/$CONFIG_FILE 2>/dev/null | jq -r '.sha') + if [ "$CONFIG_SHA" != "null" ] && [ -n "$CONFIG_SHA" ]; then + echo "Found $CONFIG_FILE sha: $CONFIG_SHA" + break + else + echo "$CONFIG_FILE not yet available, retrying in 2s..." + sleep 2 + fi + done + + if [ "$CONFIG_SHA" != "null" ] && [ -n "$CONFIG_SHA" ]; then + gh api repos/$OWNER/$REPO_NAME/contents/$CONFIG_FILE \ + -X PUT \ + -F message="Update $CONFIG_FILE with project metadata" \ + -F content="$(base64 -w 0 $CONFIG_FILE)" \ + -F sha="$CONFIG_SHA" + echo "Successfully updated $CONFIG_FILE" + else + echo "::warning::Could not find $CONFIG_FILE in the repository. Skipping update." + fi + else + echo "::warning::$CONFIG_FILE not found in template. Skipping update." + fi + + - 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="${{ env.TEMPLATE_REPO_NAME }}" + 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 template files from new repository + run: | + # Explicit list of files to remove after setup from the newly created repository + FILES_TO_DELETE=( + "templates/CODEOWNERS_TEMPLATE" + "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 template file $file from new 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 \ No newline at end of file