From 76971e4b9151c3afb279e7cc025d47df901cc3b2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 8 Mar 2026 20:18:57 +0000 Subject: [PATCH] feat: add checkout-as-app action and enhance github-app-auth Add a new `checkout-as-app` composite action that combines GitHub App authentication with repository checkout in a single step. This replaces the previous pattern of checkout -> auth -> re-checkout by doing auth first (via github-app-auth) then a single checkout with the app token baked into the remote URL, so subsequent git operations work seamlessly. Changes to github-app-auth: - Add `skip-checkout` input (default: false) for backward compatibility while allowing checkout-as-app to handle checkout separately - Add `user-email` output - Add debug output step for authentication info - Use shell variables for cleaner output computation https://claude.ai/code/session_013AzQ3VqAJ2sGSyUFDRBS2X --- .github/actions/checkout-as-app/action.yml | 163 +++++++++++++++++++++ .github/actions/github-app-auth/action.yml | 34 ++++- 2 files changed, 193 insertions(+), 4 deletions(-) create mode 100644 .github/actions/checkout-as-app/action.yml diff --git a/.github/actions/checkout-as-app/action.yml b/.github/actions/checkout-as-app/action.yml new file mode 100644 index 0000000..de2ee49 --- /dev/null +++ b/.github/actions/checkout-as-app/action.yml @@ -0,0 +1,163 @@ +name: 'Checkout as GitHub App' +description: 'Authenticate as a GitHub App and checkout the repository with the app token baked into the remote URL, so subsequent git operations use the app credentials.' + +branding: + icon: 'download' + color: 'blue' + +inputs: + # ── GitHub App Auth inputs ── + app-id: + description: 'GitHub App ID' + required: true + + private-key: + description: 'GitHub App private key' + required: true + + owner: + description: 'Repository owner for the GitHub App token (defaults to current repository owner)' + required: false + default: ${{ github.repository_owner }} + + # ── Checkout inputs (passthrough to actions/checkout) ── + repository: + description: 'Repository name with owner. For example, actions/checkout' + required: false + default: ${{ github.repository }} + + ref: + description: 'The branch, tag or SHA to checkout. When checking out the repository that triggered a workflow, this defaults to the reference or SHA for that event. Otherwise, uses the default branch.' + required: false + default: '' + + path: + description: 'Relative path under $GITHUB_WORKSPACE to place the repository' + required: false + default: '' + + clean: + description: 'Whether to execute git clean -ffdx && git reset --hard HEAD before fetching' + required: false + default: 'true' + + filter: + description: 'Partially clone against a given filter. Overrides sparse-checkout if set.' + required: false + default: '' + + sparse-checkout: + description: 'Do a sparse checkout on given patterns. Each pattern should be separated with new lines.' + required: false + default: '' + + sparse-checkout-cone-mode: + description: 'Specifies whether to use cone-mode when doing a sparse checkout.' + required: false + default: 'true' + + fetch-depth: + description: 'Number of commits to fetch. 0 indicates all history for all branches and tags.' + required: false + default: '1' + + fetch-tags: + description: 'Whether to fetch tags, even if fetch-depth > 0.' + required: false + default: 'false' + + show-progress: + description: 'Whether to show progress status output when fetching.' + required: false + default: 'true' + + lfs: + description: 'Whether to download Git-LFS files' + required: false + default: 'false' + + submodules: + description: 'Whether to checkout submodules: true to checkout submodules or recursive to recursively checkout submodules.' + required: false + default: 'false' + + set-safe-directory: + description: 'Add repository path as safe.directory for the checkout.' + required: false + default: 'true' + + github-server-url: + description: 'The base URL for the GitHub instance that you are trying to clone from, it will use environment defaults to fetch from the same instance that the workflow is running from unless specified.' + required: false + default: '' + + persist-credentials: + description: 'Whether to configure the token or SSH key with the local git config. Defaults to true so the app token persists for subsequent git operations.' + required: false + default: 'true' + +outputs: + # ── Auth outputs (from github-app-auth) ── + token: + description: 'GitHub App token' + value: ${{ steps.auth.outputs.token }} + + app-slug: + description: 'GitHub App slug name' + value: ${{ steps.auth.outputs.app-slug }} + + user-id: + description: 'GitHub App bot user ID' + value: ${{ steps.auth.outputs.user-id }} + + user-name: + description: 'GitHub App bot user name (app slug with [bot] suffix)' + value: ${{ steps.auth.outputs.user-name }} + + user-email: + description: 'GitHub App bot email address' + value: ${{ steps.auth.outputs.user-email }} + + # ── Checkout outputs ── + ref: + description: 'The branch, tag or SHA that was checked out' + value: ${{ steps.checkout.outputs.ref }} + + commit: + description: 'The commit SHA that was checked out' + value: ${{ steps.checkout.outputs.commit }} + +runs: + using: 'composite' + steps: + # ── Step 1: Authenticate as GitHub App (skip its built-in checkout) ── + - name: Authenticate as GitHub App + id: auth + uses: ./.github/actions/github-app-auth + with: + app-id: ${{ inputs.app-id }} + private-key: ${{ inputs.private-key }} + owner: ${{ inputs.owner }} + skip-checkout: 'true' + + # ── Step 2: Checkout with app token baked into the remote URL ── + - name: Checkout repository as GitHub App + id: checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 + with: + token: ${{ steps.auth.outputs.token }} + repository: ${{ inputs.repository }} + ref: ${{ inputs.ref }} + path: ${{ inputs.path }} + clean: ${{ inputs.clean }} + filter: ${{ inputs.filter }} + sparse-checkout: ${{ inputs.sparse-checkout }} + sparse-checkout-cone-mode: ${{ inputs.sparse-checkout-cone-mode }} + fetch-depth: ${{ inputs.fetch-depth }} + fetch-tags: ${{ inputs.fetch-tags }} + show-progress: ${{ inputs.show-progress }} + lfs: ${{ inputs.lfs }} + submodules: ${{ inputs.submodules }} + set-safe-directory: ${{ inputs.set-safe-directory }} + github-server-url: ${{ inputs.github-server-url }} + persist-credentials: ${{ inputs.persist-credentials }} diff --git a/.github/actions/github-app-auth/action.yml b/.github/actions/github-app-auth/action.yml index 23e869a..6df8390 100644 --- a/.github/actions/github-app-auth/action.yml +++ b/.github/actions/github-app-auth/action.yml @@ -19,6 +19,11 @@ inputs: required: false default: ${{ github.repository_owner }} + skip-checkout: + description: 'Skip the checkout step (useful when called from checkout-as-app)' + required: false + default: 'false' + outputs: token: description: 'GitHub App token' @@ -36,6 +41,10 @@ outputs: description: 'GitHub App bot user name (app slug with [bot] suffix)' value: ${{ steps.get-user-id.outputs.user-name }} + user-email: + description: 'GitHub App bot email address' + value: ${{ steps.get-user-id.outputs.user-email }} + runs: using: 'composite' steps: @@ -51,8 +60,12 @@ runs: id: get-user-id shell: bash run: | - echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT" - echo "user-name=${{ steps.app-token.outputs.app-slug }}[bot]" >> "$GITHUB_OUTPUT" + USER_ID=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id) + USER_NAME="${{ steps.app-token.outputs.app-slug }}[bot]" + USER_EMAIL="${USER_ID}+${USER_NAME}@users.noreply.github.com" + echo "user-id=${USER_ID}" >> "$GITHUB_OUTPUT" + echo "user-name=${USER_NAME}" >> "$GITHUB_OUTPUT" + echo "user-email=${USER_EMAIL}" >> "$GITHUB_OUTPUT" env: GH_TOKEN: ${{ steps.app-token.outputs.token }} @@ -63,13 +76,26 @@ runs: run: | gh auth setup-git echo "GH_TOKEN=${GH_TOKEN}" >> $GITHUB_ENV + echo "Wrote GH_TOKEN to GITHUB_ENV" echo "GITHUB_TOKEN=${GH_TOKEN}" >> $GITHUB_ENV + echo "Wrote GITHUB_TOKEN to GITHUB_ENV" git config --global user.name '${{ steps.get-user-id.outputs.user-name }}' git config user.name '${{ steps.get-user-id.outputs.user-name }}' - git config --global user.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.get-user-id.outputs.user-name }}@users.noreply.github.com' - git config user.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.get-user-id.outputs.user-name }}@users.noreply.github.com' + git config --global user.email '${{ steps.get-user-id.outputs.user-email }}' + git config user.email '${{ steps.get-user-id.outputs.user-email }}' + + - name: Output GitHub App authentication info + shell: bash + run: | + echo "GitHub App Authentication Info:" + echo "-------------------------------" + echo "- User Name: ${{ steps.get-user-id.outputs.user-name }}" + echo "- User ID: ${{ steps.get-user-id.outputs.user-id }}" + echo "- User Email: ${{ steps.get-user-id.outputs.user-email }}" + echo "- App Slug: ${{ steps.app-token.outputs.app-slug }}" - name: Configure repo checkout token + if: inputs.skip-checkout != 'true' uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 with: token: ${{ steps.app-token.outputs.token }}