new_repository_created #44
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
| name: 'Onboard New Repository with SAST' | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| organization: | |
| description: 'Organization name (e.g., MetaMask)' | |
| required: true | |
| type: string | |
| repository: | |
| description: 'Repository name (e.g., snaps)' | |
| required: true | |
| type: string | |
| repository_dispatch: | |
| types: [new_repository_created] | |
| jobs: | |
| create-sast-pr: | |
| runs-on: ubuntu-latest | |
| environment: onboarding | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout scanner action repository | |
| uses: actions/checkout@v4 | |
| with: | |
| path: scanner-repo | |
| - name: Parse target repository inputs | |
| id: target | |
| run: | | |
| validate_name() { | |
| local value="$1" | |
| local label="$2" | |
| local max_len="$3" | |
| if [ -z "$value" ]; then | |
| echo "::error::$label is empty" | |
| exit 1 | |
| fi | |
| if [ "${#value}" -gt "$max_len" ]; then | |
| echo "::error::$label exceeds maximum length of $max_len characters" | |
| exit 1 | |
| fi | |
| if ! echo "$value" | grep -qE '^[a-zA-Z0-9._-]+$'; then | |
| echo "::error::$label contains invalid characters (only alphanumeric, dots, hyphens, and underscores are allowed)" | |
| exit 1 | |
| fi | |
| } | |
| if [ "$EVENT_NAME" = "repository_dispatch" ]; then | |
| ORG="$EVENT_ORG" | |
| REPO_NAME="$EVENT_REPO" | |
| else | |
| ORG="$INPUT_ORG" | |
| REPO_NAME="$INPUT_REPO" | |
| fi | |
| validate_name "$ORG" "Organization" 39 | |
| validate_name "$REPO_NAME" "Repository" 100 | |
| { | |
| echo "organization=$ORG" | |
| echo "repo_name=$REPO_NAME" | |
| echo "repository=$ORG/$REPO_NAME" | |
| } >> "$GITHUB_OUTPUT" | |
| shell: bash | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| EVENT_ORG: ${{ github.event.client_payload.organization }} | |
| EVENT_REPO: ${{ github.event.client_payload.repository }} | |
| INPUT_ORG: ${{ inputs.organization }} | |
| INPUT_REPO: ${{ inputs.repository }} | |
| - name: Generate GitHub App token | |
| id: app_token | |
| uses: actions/create-github-app-token@v2 | |
| with: | |
| app-id: ${{ secrets.ONBOARDING_APP_ID }} | |
| private-key: ${{ secrets.ONBOARDING_APP_PRIVATE_KEY }} | |
| owner: ${{ steps.target.outputs.organization }} | |
| repositories: ${{ steps.target.outputs.repo_name }} | |
| - name: Detect default branch | |
| id: detect_branch | |
| run: | | |
| echo "Detecting default branch for $REPO..." | |
| BASE_BRANCH=$(gh api "repos/$REPO" --jq '.default_branch' 2>/dev/null) || BASE_BRANCH="" | |
| if [ -z "$BASE_BRANCH" ] || [ "$BASE_BRANCH" = "null" ]; then | |
| echo "Repository is empty or default branch not found. Defaulting to 'main'" | |
| BASE_BRANCH="main" | |
| fi | |
| if ! echo "$BASE_BRANCH" | grep -qE '^[a-zA-Z0-9._/-]+$'; then | |
| echo "::error::Branch name contains invalid characters (only alphanumeric, dots, hyphens, slashes, and underscores are allowed)" | |
| exit 1 | |
| fi | |
| echo "base_branch=$BASE_BRANCH" >> "$GITHUB_OUTPUT" | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ steps.app_token.outputs.token }} | |
| REPO: ${{ steps.target.outputs.repository }} | |
| - name: Check for opt-out file | |
| id: check_opt_out | |
| run: | | |
| if gh api "repos/$REPO/contents/.github/no-security-scanner?ref=$BASE_BRANCH" > /dev/null 2>&1; then | |
| echo "Repository has opted out via .github/no-security-scanner" | |
| echo "opted_out=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "opted_out=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ steps.app_token.outputs.token }} | |
| REPO: ${{ steps.target.outputs.repository }} | |
| BASE_BRANCH: ${{ steps.detect_branch.outputs.base_branch }} | |
| - name: Skip onboarding (repository opted out) | |
| if: steps.check_opt_out.outputs.opted_out == 'true' | |
| run: | | |
| echo "::notice::Skipping onboarding — repository $REPO has a .github/no-security-scanner opt-out file" | |
| env: | |
| REPO: ${{ steps.target.outputs.repository }} | |
| - name: Check if target repository is empty | |
| if: steps.check_opt_out.outputs.opted_out != 'true' | |
| id: check_empty | |
| run: | | |
| if ! BRANCHES=$(gh api "repos/$REPO/branches" --jq 'length' 2>&1); then | |
| echo "::error::GitHub API call failed while listing branches for $REPO: $BRANCHES" | |
| exit 1 | |
| fi | |
| if ! echo "$BRANCHES" | grep -qE '^[0-9]+$'; then | |
| echo "::error::Unexpected API response when listing branches for $REPO: $BRANCHES" | |
| exit 1 | |
| fi | |
| if [ "$BRANCHES" = "0" ]; then | |
| IS_EMPTY="true" | |
| echo "Repository is empty (no branches found)" | |
| else | |
| IS_EMPTY="false" | |
| echo "Repository has $BRANCHES branch(es)" | |
| fi | |
| echo "is_empty=$IS_EMPTY" >> "$GITHUB_OUTPUT" | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ steps.app_token.outputs.token }} | |
| REPO: ${{ steps.target.outputs.repository }} | |
| - name: Checkout target repository | |
| if: steps.check_opt_out.outputs.opted_out != 'true' && steps.check_empty.outputs.is_empty == 'false' | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: ${{ steps.target.outputs.repository }} | |
| token: ${{ steps.app_token.outputs.token }} | |
| path: target-repo | |
| ref: ${{ steps.detect_branch.outputs.base_branch }} | |
| - name: Initialize empty repository locally | |
| if: steps.check_opt_out.outputs.opted_out != 'true' && steps.check_empty.outputs.is_empty == 'true' | |
| run: | | |
| mkdir -p target-repo | |
| cd target-repo | |
| git init | |
| git remote add origin "https://x-access-token:${APP_TOKEN}@github.com/${REPO}.git" | |
| shell: bash | |
| env: | |
| APP_TOKEN: ${{ steps.app_token.outputs.token }} | |
| REPO: ${{ steps.target.outputs.repository }} | |
| - name: Create branch and add SAST workflow | |
| id: create_branch | |
| if: steps.check_opt_out.outputs.opted_out != 'true' | |
| working-directory: target-repo | |
| env: | |
| IS_EMPTY: ${{ steps.check_empty.outputs.is_empty }} | |
| BASE_BRANCH: ${{ steps.detect_branch.outputs.base_branch }} | |
| run: | | |
| git config user.name "MetaMask Security Bot" | |
| git config user.email "security-bot@metamask.io" | |
| if [ "$IS_EMPTY" = "true" ]; then | |
| # For empty repos, create initial commit on main | |
| BRANCH_NAME="$BASE_BRANCH" | |
| else | |
| # For existing repos, create a feature branch | |
| BRANCH_NAME="security/add-sast-scanner" | |
| git checkout -b "$BRANCH_NAME" | |
| fi | |
| mkdir -p .github/workflows | |
| sed "s|{ DEFAULT_BRANCH }|$BASE_BRANCH|g" \ | |
| ../scanner-repo/.github/templates/security-code-scanner.yml \ | |
| > .github/workflows/security-code-scanner.yml | |
| git add .github/workflows/security-code-scanner.yml | |
| if git diff --cached --quiet; then | |
| echo "::notice::Workflow file already exists and matches — nothing to commit" | |
| echo "skipped=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| git commit -m "chore: add MetaMask Security Code Scanner workflow | |
| This PR adds the MetaMask Security Code Scanner workflow to enable | |
| automated security scanning of the codebase. | |
| The scanner will run on: | |
| - Push to $BASE_BRANCH branch | |
| - Pull requests to $BASE_BRANCH branch | |
| - Manual workflow dispatch | |
| To configure the scanner for your repository's specific needs, | |
| please review the workflow file and adjust as necessary." | |
| if [ "$IS_EMPTY" = "true" ]; then | |
| git branch -M "$BRANCH_NAME" | |
| fi | |
| git push -u origin "$BRANCH_NAME" | |
| echo "skipped=false" >> "$GITHUB_OUTPUT" | |
| shell: bash | |
| - name: Create Pull Request | |
| if: steps.check_opt_out.outputs.opted_out != 'true' && steps.check_empty.outputs.is_empty == 'false' && steps.create_branch.outputs.skipped != 'true' | |
| working-directory: target-repo | |
| env: | |
| GH_TOKEN: ${{ steps.app_token.outputs.token }} | |
| REPO_NAME: ${{ steps.target.outputs.repository }} | |
| BASE_BRANCH: ${{ steps.detect_branch.outputs.base_branch }} | |
| run: | | |
| # Extract owner and repo name for URL construction | |
| OWNER=$(echo "$REPO_NAME" | cut -d'/' -f1) | |
| REPO=$(echo "$REPO_NAME" | cut -d'/' -f2) | |
| SECURITY_URL="https://github.com/${OWNER}/${REPO}/security/code-scanning" | |
| # Read PR body template and substitute variables | |
| PR_BODY=$(cat ../scanner-repo/.github/templates/onboarding-pr-body-automated.md) | |
| PR_BODY="${PR_BODY//\{\{SECURITY_SCANNING_URL\}\}/$SECURITY_URL}" | |
| gh pr create \ | |
| --title "🔒 Add MetaMask Security Code Scanner" \ | |
| --body "$PR_BODY" \ | |
| --base "$BASE_BRANCH" \ | |
| --head "security/add-sast-scanner" | |
| shell: bash | |
| - name: Output PR URL | |
| if: steps.check_opt_out.outputs.opted_out != 'true' && steps.check_empty.outputs.is_empty == 'false' && steps.create_branch.outputs.skipped != 'true' | |
| working-directory: target-repo | |
| env: | |
| GH_TOKEN: ${{ steps.app_token.outputs.token }} | |
| run: | | |
| PR_URL=$(gh pr view security/add-sast-scanner --json url -q .url) | |
| echo "✅ Pull Request created: $PR_URL" | |
| echo "PR_URL=$PR_URL" >> "$GITHUB_OUTPUT" | |
| shell: bash | |
| - name: Output commit info for empty repo | |
| if: steps.check_opt_out.outputs.opted_out != 'true' && steps.check_empty.outputs.is_empty == 'true' && steps.create_branch.outputs.skipped != 'true' | |
| run: | | |
| echo "✅ Initial commit pushed to https://github.com/$REPO/tree/$BASE_BRANCH" | |
| echo "Repository was empty - workflow file added directly to $BASE_BRANCH branch" | |
| shell: bash | |
| env: | |
| REPO: ${{ steps.target.outputs.repository }} | |
| BASE_BRANCH: ${{ steps.detect_branch.outputs.base_branch }} | |
| - name: Post to Slack channel on failure | |
| if: ${{ failure() && env.SLACK_WEBHOOK_URL != '' }} | |
| uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844 | |
| with: | |
| payload: | | |
| { | |
| "text": "Onboarding failed for ${{ steps.target.outputs.repository }} - Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| } | |
| env: | |
| SLACK_WEBHOOK_URL: ${{ secrets.APPSEC_BOT_SLACK_WEBHOOK }} | |
| SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK |