diff --git a/.github/actions/Cleanup-PSModulePrereleases/action.yml b/.github/actions/Cleanup-PSModulePrereleases/action.yml index 0e04885e..eaf64ad7 100644 --- a/.github/actions/Cleanup-PSModulePrereleases/action.yml +++ b/.github/actions/Cleanup-PSModulePrereleases/action.yml @@ -31,6 +31,7 @@ runs: shell: pwsh working-directory: ${{ inputs.WorkingDirectory }} env: + GH_TOKEN: ${{ env.GH_TOKEN }} PSMODULE_CLEANUP_PSMODULEPRERELEASES_INPUT_WhatIf: ${{ inputs.WhatIf }} PSMODULE_CLEANUP_PSMODULEPRERELEASES_CONTEXT_ReleaseTag: ${{ inputs.ReleaseTag }} run: ${{ github.action_path }}/src/cleanup.ps1 diff --git a/.github/actions/Get-PSModuleSettings/action.yml b/.github/actions/Get-PSModuleSettings/action.yml index 86a49b3a..1a209701 100644 --- a/.github/actions/Get-PSModuleSettings/action.yml +++ b/.github/actions/Get-PSModuleSettings/action.yml @@ -53,6 +53,7 @@ runs: uses: PSModule/GitHub-Script@8083ec1f733f00357ee4d0db0c6056686e483bc0 # v1.9.0 id: Get-PSModuleSettings env: + GH_TOKEN: ${{ env.GH_TOKEN }} PSMODULE_GET_SETTINGS_INPUT_Name: ${{ inputs.Name }} PSMODULE_GET_SETTINGS_INPUT_SettingsPath: ${{ inputs.SettingsPath }} PSMODULE_GET_SETTINGS_INPUT_Debug: ${{ inputs.Debug }} @@ -63,6 +64,7 @@ runs: PSMODULE_GET_SETTINGS_INPUT_ImportantFilePatterns: ${{ inputs.ImportantFilePatterns }} with: Name: Get-PSModuleSettings + Token: ${{ env.GH_TOKEN }} ShowInfo: false ShowOutput: true Debug: ${{ inputs.Debug }} diff --git a/.github/actions/Publish-PSModule/README.md b/.github/actions/Publish-PSModule/README.md index 1a24f4f4..6b97fed0 100644 --- a/.github/actions/Publish-PSModule/README.md +++ b/.github/actions/Publish-PSModule/README.md @@ -9,7 +9,7 @@ Publishes a pre-versioned PowerShell module artifact to the PowerShell Gallery. | `Name` | Name of the module to publish. | No | Repository name | | `ModulePath` | Path containing the built `/` module directory. | No | `outputs/module` | | `ArtifactName` | Name of the module artifact to download. | No | `module` | -| `APIKey` | PowerShell Gallery API key. | Yes | N/A | +| `PSGALLERY_API_KEY` | PowerShell Gallery API key. | Yes | N/A | | `WhatIf` | Logs publishing operations without publishing the module. | No | `false` | | `WorkingDirectory` | Directory where the publishing script runs. | No | `.` | @@ -26,7 +26,7 @@ This action does not provide outputs. Name: ExampleModule ModulePath: outputs/module ArtifactName: module - APIKey: ${{ secrets.APIKEY }} + PSGALLERY_API_KEY: ${{ secrets.PSGALLERY_API_KEY }} ``` Use [Release-PSModule](../Release-PSModule/README.md) in a separate workflow step to create the GitHub release from the same artifact. diff --git a/.github/actions/Publish-PSModule/action.yml b/.github/actions/Publish-PSModule/action.yml index ea0a6cfb..038ddaea 100644 --- a/.github/actions/Publish-PSModule/action.yml +++ b/.github/actions/Publish-PSModule/action.yml @@ -10,7 +10,7 @@ inputs: description: Path to the folder containing the / module subdirectory from Build-PSModule. required: false default: outputs/module - APIKey: + PSGALLERY_API_KEY: description: PowerShell Gallery API Key. required: true WhatIf: @@ -46,8 +46,9 @@ runs: shell: pwsh working-directory: ${{ inputs.WorkingDirectory }} env: + GH_TOKEN: ${{ env.GH_TOKEN }} PSMODULE_PUBLISH_PSMODULE_INPUT_Name: ${{ inputs.Name }} PSMODULE_PUBLISH_PSMODULE_INPUT_ModulePath: ${{ inputs.ModulePath }} - PSMODULE_PUBLISH_PSMODULE_INPUT_APIKey: ${{ inputs.APIKey }} + PSMODULE_PUBLISH_PSMODULE_INPUT_PSGALLERY_API_KEY: ${{ inputs.PSGALLERY_API_KEY }} PSMODULE_PUBLISH_PSMODULE_INPUT_WhatIf: ${{ inputs.WhatIf }} run: ${{ github.action_path }}/src/publish.ps1 diff --git a/.github/actions/Publish-PSModule/src/publish.ps1 b/.github/actions/Publish-PSModule/src/publish.ps1 index 13c6c92e..dc512c0a 100644 --- a/.github/actions/Publish-PSModule/src/publish.ps1 +++ b/.github/actions/Publish-PSModule/src/publish.ps1 @@ -1,5 +1,5 @@ [Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseDeclaredVarsMoreThanAssignments', 'apiKey', + 'PSUseDeclaredVarsMoreThanAssignments', 'psGalleryApiKey', Justification = 'Variable is used in script blocks.' )] [Diagnostics.CodeAnalysis.SuppressMessageAttribute( @@ -38,7 +38,7 @@ LogGroup 'Load inputs' { exit 1 } $modulePath = Resolve-Path -Path $modulePathCandidate | Select-Object -ExpandProperty Path - $apiKey = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_APIKey + $psGalleryApiKey = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_PSGALLERY_API_KEY $whatIf = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_WhatIf -eq 'true' Write-Host "Module name: [$name]" @@ -138,7 +138,7 @@ LogGroup 'Publish to PSGallery' { Write-Host "Publish-PSResource -Path $modulePath -Repository PSGallery -ApiKey ***" } else { try { - Publish-PSResource -Path $modulePath -Repository PSGallery -ApiKey $apiKey + Publish-PSResource -Path $modulePath -Repository PSGallery -ApiKey $psGalleryApiKey } catch { Write-Error $_.Exception.Message exit 1 diff --git a/.github/actions/Resolve-PSModuleVersion/action.yml b/.github/actions/Resolve-PSModuleVersion/action.yml index 20b41883..5192f28f 100644 --- a/.github/actions/Resolve-PSModuleVersion/action.yml +++ b/.github/actions/Resolve-PSModuleVersion/action.yml @@ -65,7 +65,7 @@ runs: shell: pwsh working-directory: ${{ inputs.WorkingDirectory }} env: - GH_TOKEN: ${{ github.token }} + GH_TOKEN: ${{ env.GH_TOKEN }} PSMODULE_RESOLVE_PSMODULEVERSION_INPUT_Settings: ${{ inputs.Settings }} PSMODULE_RESOLVE_PSMODULEVERSION_INPUT_Name: ${{ inputs.Name }} PSMODULE_RESOLVE_PSMODULEVERSION_INPUT_EventJson: ${{ inputs.EventJson }} diff --git a/.github/workflows/Build-Module.yml b/.github/workflows/Build-Module.yml index e953017f..655e2618 100644 --- a/.github/workflows/Build-Module.yml +++ b/.github/workflows/Build-Module.yml @@ -2,6 +2,13 @@ name: Build-Module on: workflow_call: + secrets: + GitHubAppClientId: + description: The client ID of the GitHub App used for repository API calls. + required: true + GitHubAppPrivateKey: + description: The private key of the GitHub App used for repository API calls. + required: true inputs: Settings: type: string @@ -20,8 +27,6 @@ jobs: Build-Module: name: Build-Module runs-on: ubuntu-latest - env: - GH_TOKEN: ${{ github.token }} steps: - name: Checkout Code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -36,8 +41,19 @@ jobs: path: _wf persist-credentials: false + - name: Create GitHub App token + id: App-Token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ secrets.GitHubAppClientId }} + private-key: ${{ secrets.GitHubAppPrivateKey }} + repositories: ${{ github.event.repository.name }} + permission-metadata: read + - name: Build module uses: ./_wf/.github/actions/Build-PSModule + env: + GH_TOKEN: ${{ steps.App-Token.outputs.token }} with: Name: ${{ fromJson(inputs.Settings).Name }} Version: ${{ fromJson(inputs.Settings).Publish.Module.Resolution.Version != '' && fromJson(inputs.Settings).Publish.Module.Resolution.Version || '999.0.0' }} diff --git a/.github/workflows/Plan.yml b/.github/workflows/Plan.yml index e49bec20..8e9b4657 100644 --- a/.github/workflows/Plan.yml +++ b/.github/workflows/Plan.yml @@ -9,6 +9,13 @@ name: Plan on: workflow_call: + secrets: + GitHubAppClientId: + description: The client ID of the GitHub App used for repository API calls. + required: true + GitHubAppPrivateKey: + description: The private key of the GitHub App used for repository API calls. + required: true inputs: SettingsPath: type: string @@ -79,9 +86,21 @@ jobs: path: _wf persist-credentials: false + - name: Create GitHub App token + id: App-Token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ secrets.GitHubAppClientId }} + private-key: ${{ secrets.GitHubAppPrivateKey }} + repositories: ${{ github.event.repository.name }} + permission-contents: read + permission-pull-requests: write + - name: Get-Settings uses: ./_wf/.github/actions/Get-PSModuleSettings id: Get-Settings + env: + GH_TOKEN: ${{ steps.App-Token.outputs.token }} with: SettingsPath: ${{ inputs.SettingsPath }} Debug: ${{ inputs.Debug }} @@ -95,7 +114,7 @@ jobs: uses: ./_wf/.github/actions/Resolve-PSModuleVersion id: Resolve-Version env: - GH_TOKEN: ${{ github.token }} + GH_TOKEN: ${{ steps.App-Token.outputs.token }} with: Settings: ${{ steps.Get-Settings.outputs.Settings }} Name: ${{ fromJson(steps.Get-Settings.outputs.Settings).Name }} diff --git a/.github/workflows/Publish-Module.yml b/.github/workflows/Publish-Module.yml index f02f2268..149bb78a 100644 --- a/.github/workflows/Publish-Module.yml +++ b/.github/workflows/Publish-Module.yml @@ -3,9 +3,15 @@ name: Publish-Module on: workflow_call: secrets: - APIKey: + PSGALLERY_API_KEY: description: The API key for the PowerShell Gallery. required: true + GitHubAppClientId: + description: The client ID of the GitHub App used for repository API calls. + required: true + GitHubAppPrivateKey: + description: The private key of the GitHub App used for repository API calls. + required: true inputs: Settings: type: string @@ -37,15 +43,25 @@ jobs: path: _wf persist-credentials: false + - name: Create GitHub App token + id: App-Token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ secrets.GitHubAppClientId }} + private-key: ${{ secrets.GitHubAppPrivateKey }} + repositories: ${{ github.event.repository.name }} + permission-contents: write + permission-pull-requests: write + - name: Publish module if: fromJson(inputs.Settings).Publish.Module.Resolution.ReleaseType != 'None' uses: ./_wf/.github/actions/Publish-PSModule env: - GH_TOKEN: ${{ github.token }} + GH_TOKEN: ${{ steps.App-Token.outputs.token }} with: Name: ${{ fromJson(inputs.Settings).Name }} ModulePath: outputs/module - APIKey: ${{ secrets.APIKey }} + PSGALLERY_API_KEY: ${{ secrets.PSGALLERY_API_KEY }} WhatIf: ${{ github.repository == 'PSModule/Process-PSModule' }} WorkingDirectory: ${{ fromJson(inputs.Settings).WorkingDirectory }} @@ -54,7 +70,7 @@ jobs: if: always() && !cancelled() && fromJson(inputs.Settings).Publish.Module.Resolution.ReleaseType != 'None' uses: ./_wf/.github/actions/Release-PSModule env: - GH_TOKEN: ${{ github.token }} + GH_TOKEN: ${{ steps.App-Token.outputs.token }} with: Name: ${{ fromJson(inputs.Settings).Name }} ModulePath: outputs/module @@ -74,7 +90,7 @@ jobs: steps.create-github-release.outcome == 'success') uses: ./_wf/.github/actions/Cleanup-PSModulePrereleases env: - GH_TOKEN: ${{ github.token }} + GH_TOKEN: ${{ steps.App-Token.outputs.token }} with: WhatIf: ${{ github.repository == 'PSModule/Process-PSModule' }} AutoCleanup: ${{ fromJson(inputs.Settings).Publish.Module.AutoCleanup }} diff --git a/.github/workflows/Workflow-Test-Default.yml b/.github/workflows/Workflow-Test-Default.yml index 0806fb62..03ed7cfa 100644 --- a/.github/workflows/Workflow-Test-Default.yml +++ b/.github/workflows/Workflow-Test-Default.yml @@ -30,7 +30,9 @@ jobs: if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} uses: ./.github/workflows/workflow.yml secrets: - APIKey: ${{ secrets.APIKey }} + PSGALLERY_API_KEY: ${{ secrets.PSGALLERY_API_KEY }} + GitHubAppClientId: ${{ secrets.SHELLY_CLIENT_ID }} + GitHubAppPrivateKey: ${{ secrets.SHELLY_PRIVATE_KEY }} TestData: >- { "secrets": { diff --git a/.github/workflows/Workflow-Test-WithManifest.yml b/.github/workflows/Workflow-Test-WithManifest.yml index e4576760..0340f329 100644 --- a/.github/workflows/Workflow-Test-WithManifest.yml +++ b/.github/workflows/Workflow-Test-WithManifest.yml @@ -30,7 +30,9 @@ jobs: if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} uses: ./.github/workflows/workflow.yml secrets: - APIKey: ${{ secrets.APIKey }} + PSGALLERY_API_KEY: ${{ secrets.PSGALLERY_API_KEY }} + GitHubAppClientId: ${{ secrets.SHELLY_CLIENT_ID }} + GitHubAppPrivateKey: ${{ secrets.SHELLY_PRIVATE_KEY }} TestData: >- { "secrets": { diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 099afc34..32de93a0 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -3,9 +3,15 @@ name: Process-PSModule on: workflow_call: secrets: - APIKey: + PSGALLERY_API_KEY: description: The API key for the PowerShell Gallery. required: true + GitHubAppClientId: + description: Generic GitHub App client ID secret for caller mapping. + required: true + GitHubAppPrivateKey: + description: Generic GitHub App private key secret for caller mapping. + required: true TestData: description: | Optional single-line JSON object carrying all data the module test jobs @@ -77,6 +83,9 @@ jobs: # - ✅ Manual run - Always runs to load configuration Plan: uses: ./.github/workflows/Plan.yml + secrets: + GitHubAppClientId: ${{ secrets.GitHubAppClientId }} + GitHubAppPrivateKey: ${{ secrets.GitHubAppPrivateKey }} with: SettingsPath: ${{ inputs.SettingsPath }} Debug: ${{ inputs.Debug }} @@ -107,6 +116,9 @@ jobs: Build-Module: if: fromJson(needs.Plan.outputs.Settings).Build.Module.Enabled uses: ./.github/workflows/Build-Module.yml + secrets: + GitHubAppClientId: ${{ secrets.GitHubAppClientId }} + GitHubAppPrivateKey: ${{ secrets.GitHubAppPrivateKey }} needs: - Plan with: @@ -245,7 +257,9 @@ jobs: if: fromJson(needs.Plan.outputs.Settings).Publish.Module.Enabled && needs.Plan.result == 'success' && !cancelled() && (needs.Get-TestResults.result == 'success' || needs.Get-TestResults.result == 'skipped') && (needs.Get-CodeCoverage.result == 'success' || needs.Get-CodeCoverage.result == 'skipped') && (needs.Build-Site.result == 'success' || needs.Build-Site.result == 'skipped') uses: ./.github/workflows/Publish-Module.yml secrets: - APIKey: ${{ secrets.APIKey }} + PSGALLERY_API_KEY: ${{ secrets.PSGALLERY_API_KEY }} + GitHubAppClientId: ${{ secrets.GitHubAppClientId }} + GitHubAppPrivateKey: ${{ secrets.GitHubAppPrivateKey }} needs: - Plan - Get-TestResults diff --git a/docs/content/get-started/index.md b/docs/content/get-started/index.md index 7b4f0b8b..b641b85d 100644 --- a/docs/content/get-started/index.md +++ b/docs/content/get-started/index.md @@ -11,7 +11,7 @@ Start new modules from the PSModule template repository: ## Quickstart 1. [Create a new repository from the template](https://github.com/new?template_name=Template-PSModule&template_owner=PSModule&description=Add%20a%20description%20(required)&name=%3CModule%20name%3E). -2. [Configure the repository](repository-setup.md) — GitHub Pages, the PowerShell Gallery API key, and the caller workflow. +2. [Configure the repository](repository-setup.md) — GitHub Pages, `PSGALLERY_API_KEY`, and the caller workflow. 3. Replace placeholder metadata and remove scaffold sample files. 4. Add your first public command and tests. 5. Validate `.github/PSModule.yml` defaults for your module. @@ -30,7 +30,7 @@ If the module needs several interdependent commands before it is usable at all, | Page | Description | | --- | --- | -| [Repository setup](repository-setup.md) | GitHub Pages, the PowerShell Gallery API key, permissions, and the caller workflow. | +| [Repository setup](repository-setup.md) | GitHub Pages, `PSGALLERY_API_KEY`, permissions, and the caller workflow. | | [Your first release](your-first-release.md) | The pull request flow, version labels, and what happens on merge. | | [Module bootstrap](module-bootstrap.md) | Getting a brand-new module to its first release with an integration branch. | diff --git a/docs/content/get-started/repository-setup.md b/docs/content/get-started/repository-setup.md index e4ee047c..821dada5 100644 --- a/docs/content/get-started/repository-setup.md +++ b/docs/content/get-started/repository-setup.md @@ -1,6 +1,6 @@ --- title: Repository setup -description: Configure GitHub Pages, the PowerShell Gallery API key, permissions, and the caller workflow so Process-PSModule can build and publish the module. +description: Configure GitHub Pages, `PSGALLERY_API_KEY`, permissions, and the caller workflow so Process-PSModule can build and publish the module. --- # Repository setup @@ -18,14 +18,14 @@ This creates an environment called `github-pages` that GitHub deploys the docume Remove the branch protection on main -## 2. Create a PowerShell Gallery API key +## 2. Create `PSGALLERY_API_KEY` 1. [Create an API key on the PowerShell Gallery](https://www.powershellgallery.com/account/apikeys). Give it permission to manage the module you are working on. -2. Create a repository secret called `APIKEY` and set the API key as its value. +2. Create a repository or organization secret called `PSGALLERY_API_KEY` and set the API key as its value. If you plan to create many modules, use a glob pattern for the API key permissions in the PowerShell Gallery and store -the secret on the organization instead of on each repository. +`PSGALLERY_API_KEY` on the organization instead of on each repository. ## 3. Add the caller workflow @@ -63,7 +63,9 @@ jobs: Process-PSModule: uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@v5 secrets: - APIKey: ${{ secrets.APIKey }} + PSGALLERY_API_KEY: ${{ secrets.PSGALLERY_API_KEY }} + GitHubAppClientId: ${{ secrets.SHELLY_CLIENT_ID }} + GitHubAppPrivateKey: ${{ secrets.SHELLY_PRIVATE_KEY }} ``` Every permission in that block is required. See [Workflow inputs](../reference/workflow-inputs.md) for what each one is diff --git a/docs/content/guides/calling-the-workflow.md b/docs/content/guides/calling-the-workflow.md index b420c742..799da79e 100644 --- a/docs/content/guides/calling-the-workflow.md +++ b/docs/content/guides/calling-the-workflow.md @@ -46,17 +46,20 @@ jobs: Process-PSModule: uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@v5 secrets: - APIKey: ${{ secrets.APIKey }} + PSGALLERY_API_KEY: ${{ secrets.PSGALLERY_API_KEY }} + GitHubAppClientId: ${{ secrets.SHELLY_CLIENT_ID }} + GitHubAppPrivateKey: ${{ secrets.SHELLY_PRIVATE_KEY }} ``` ## Passing test data -The reusable workflow at `.github/workflows/workflow.yml` declares only two workflow-call secrets, +The reusable workflow at `.github/workflows/workflow.yml` declares four workflow-call secrets, which keeps the calling workflow in full control of the credentials that are exposed. -`secrets: inherit` is intentionally not required. `APIKey` publishes to the PowerShell Gallery; `TestData` carries -everything the module's own tests need. +`secrets: inherit` is intentionally not required. `PSGALLERY_API_KEY` publishes to the PowerShell Gallery, +`GitHubAppClientId` and `GitHubAppPrivateKey` authenticate GitHub API operations, and `TestData` +carries everything the module's own tests need. ### Breaking change: fixed test secrets use `TestData` @@ -79,7 +82,9 @@ jobs: Process-PSModule: uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@v5 secrets: - APIKey: ${{ secrets.APIKey }} + PSGALLERY_API_KEY: ${{ secrets.PSGALLERY_API_KEY }} + GitHubAppClientId: ${{ secrets.SHELLY_CLIENT_ID }} + GitHubAppPrivateKey: ${{ secrets.SHELLY_PRIVATE_KEY }} TestData: >- { "secrets": { "TEST_USER_PAT": "${{ secrets.TEST_USER_PAT }}", "TEST_APP_ORG_CLIENT_ID": "${{ secrets.TEST_APP_ORG_CLIENT_ID }}" } } @@ -106,7 +111,9 @@ jobs: Process-PSModule: uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@v5 secrets: - APIKey: ${{ secrets.APIKey }} + PSGALLERY_API_KEY: ${{ secrets.PSGALLERY_API_KEY }} + GitHubAppClientId: ${{ secrets.SHELLY_CLIENT_ID }} + GitHubAppPrivateKey: ${{ secrets.SHELLY_PRIVATE_KEY }} TestData: >- { "secrets": { "CONFLUENCE_API_TOKEN": "${{ secrets.CONFLUENCE_API_TOKEN }}" }, "variables": { "CONFLUENCE_SITE": ${{ toJSON(vars.CONFLUENCE_SITE) }}, diff --git a/docs/content/guides/github-app-authentication.md b/docs/content/guides/github-app-authentication.md new file mode 100644 index 00000000..7be1620b --- /dev/null +++ b/docs/content/guides/github-app-authentication.md @@ -0,0 +1,104 @@ +--- +title: GitHub App authentication +description: Configure the GitHub App secrets and understand token scope and injection in Process-PSModule workflows. +--- + +# GitHub App authentication + +The repository API operations in the Plan, Build-Module, and Publish-Module workflows use short-lived GitHub App +installation tokens. These workflows do not use `github.token` as a fallback for those operations. + +## Caller secret contract + +The reusable workflow declares two required secrets at its `workflow_call` boundary: + +| Name | Purpose | +| --- | --- | +| `GitHubAppClientId` | The GitHub App client ID passed to the token action. | +| `GitHubAppPrivateKey` | The GitHub App private key passed to the token action. | + +The names are the reusable workflow contract, not a requirement for the caller's repository or organization secret +names. Map the caller's secrets explicitly: + +```yaml +jobs: + Process-PSModule: + uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@v5 + secrets: + PSGALLERY_API_KEY: ${{ secrets.PSGALLERY_API_KEY }} + GitHubAppClientId: ${{ secrets.SHELLY_CLIENT_ID }} + GitHubAppPrivateKey: ${{ secrets.SHELLY_PRIVATE_KEY }} +``` + +The root reusable workflow forwards these two values to the Plan, Build-Module, and Publish-Module reusable jobs. +Do not use `secrets: inherit` as a substitute for this mapping. + +Dependabot-triggered workflows cannot read regular Actions secrets. To run Process-PSModule on Dependabot pull +requests, create `SHELLY_CLIENT_ID` and `SHELLY_PRIVATE_KEY` as Dependabot secrets in addition to Actions secrets. +This is a deliberate trust boundary: review the App's installation scope and every dependency update carefully, +because the workflow can mint a Shelly token before human review. + +## GitHub App installation permissions + +Install Shelly only on repositories that the process must manage. The complete permission baseline for the current +Process-PSModule GitHub App path is: + +| Repository permission | Access | Why it is needed | +| --- | --- | --- | +| Contents | Write | Read releases during version resolution; create, upload to, and delete releases during publish and cleanup. | +| Pull requests | Write | Read pull-request files and labels; add process and release comments to pull requests. | +| Metadata | Read | Read repository description, topics, and URL while building the module manifest. This permission is granted automatically to GitHub Apps. | + +Do not grant Shelly Actions, Issues, Statuses, Pages, Workflows, or administration permissions for the current +Process-PSModule GitHub App path. Those permissions are not used by installation tokens minted here. + +The caller workflow's `permissions:` block is separate: it scopes only `github.token` for non-App operations such as +artifact handling, linting, and Pages deployment. It cannot expand or restrict Shelly's installation token. + +## Per-workflow token scope + +Each job mints its own token with the repository that triggered the workflow: +`${{ github.event.repository.name }}`. + +| Workflow | Requested repository permissions | GitHub operations | +| --- | --- | --- | +| Plan | `contents: read`, `pull-requests: write` | Read repository settings and version data, inspect pull-request files and labels, and write planning comments or labels. | +| Build-Module | `metadata: read` | Read repository metadata while building the module manifest. | +| Publish-Module | `contents: write`, `pull-requests: write` | Create and upload releases, write pull-request comments, and clean up prereleases. | + +The GitHub App installation must grant the permissions requested by each job. Keep the installation and token scope +limited to the repository set required by the workflow; add broader repository access only when a workflow explicitly +needs cross-repository operations. + +The scopes have separate ceilings: + +- `permissions:` on the caller workflow controls the default `github.token`; it does not expand an App installation + token. +- The App installation permissions are the maximum permissions any token from that installation can receive. +- The `repositories` input limits the repositories available to the minted token. +- Each `permission-` input requests only the subset needed by that job. + +## Token injection + +Use the latest released version of the token action, resolved to its full immutable commit SHA. Record the exact +release tag in the trailing comment. The token action exposes its output only to the steps that need GitHub API access: + +```yaml +- name: Create GitHub App token + id: App-Token + uses: actions/create-github-app-token@ # vx.y.z + with: + client-id: ${{ secrets.GitHubAppClientId }} + private-key: ${{ secrets.GitHubAppPrivateKey }} + repositories: ${{ github.event.repository.name }} + permission-metadata: read + +- name: Use the token + env: + GH_TOKEN: ${{ steps.App-Token.outputs.token }} + run: gh repo view +``` + +Process-PSModule does not set this token as a job-wide environment variable. It injects `GH_TOKEN` on the Get-Settings +and Resolve-Version steps in Plan, the Build-PSModule step in Build-Module, and the Publish-PSModule and cleanup steps +in Publish-Module. Keep GitHub App tokens step-scoped when adding new API calls. diff --git a/docs/content/index.md b/docs/content/index.md index 8e126e11..0a63f3b5 100644 --- a/docs/content/index.md +++ b/docs/content/index.md @@ -22,7 +22,7 @@ New to Process-PSModule? Work through these in order. | Page | Description | | --- | --- | | [Get started](get-started/index.md) | Create a module repository from the template and get the pipeline running. | -| [Repository setup](get-started/repository-setup.md) | Configure GitHub Pages, the PowerShell Gallery API key, permissions, and the caller workflow. | +| [Repository setup](get-started/repository-setup.md) | Configure GitHub Pages, `PSGALLERY_API_KEY`, permissions, and the caller workflow. | | [Your first release](get-started/your-first-release.md) | The pull request flow, version labels, and what happens on merge. | ## Guides diff --git a/docs/content/reference/repository-standard.md b/docs/content/reference/repository-standard.md index dde5a31c..dbe7a6d0 100644 --- a/docs/content/reference/repository-standard.md +++ b/docs/content/reference/repository-standard.md @@ -124,7 +124,9 @@ jobs: Process-PSModule: uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@ # secrets: - APIKey: ${{ secrets.APIKEY }} + PSGALLERY_API_KEY: ${{ secrets.PSGALLERY_API_KEY }} + GitHubAppClientId: ${{ secrets.SHELLY_CLIENT_ID }} + GitHubAppPrivateKey: ${{ secrets.SHELLY_PRIVATE_KEY }} ``` Name the caller file `Process-PSModule.yml`, matching [`PSModule/Template-PSModule`](https://github.com/PSModule/Template-PSModule) and every existing module repository. `workflow.yml` is the reusable workflow's own filename inside `PSModule/Process-PSModule` and belongs only in the `uses:` reference. Pin the reference to a commit SHA with the version tag in a trailing comment so Dependabot can update it. diff --git a/docs/content/reference/workflow-inputs.md b/docs/content/reference/workflow-inputs.md index 1e433413..dd290c06 100644 --- a/docs/content/reference/workflow-inputs.md +++ b/docs/content/reference/workflow-inputs.md @@ -23,19 +23,22 @@ interface it exposes to a caller workflow. For how to wire it up, see ## Secrets -The workflow declares only two workflow-call secrets, which keeps the calling workflow in full control of the +The workflow declares four workflow-call secrets, which keeps the calling workflow in full control of the credentials that are exposed. `secrets: inherit` is intentionally not required. | Name | Location | Description | Required | | ---- | -------- | ----------- | -------- | -| `APIKey` | GitHub secrets | The API key for the PowerShell Gallery, used to publish the module. | Yes | +| `PSGALLERY_API_KEY` | GitHub secrets | The API key for the PowerShell Gallery, used to publish the module. | Yes | +| `GitHubAppClientId` | GitHub secrets | The GitHub App client ID used to mint scoped installation tokens for GitHub API operations. Map Shelly's `SHELLY_CLIENT_ID` in the caller and Dependabot secret store. | Yes | +| `GitHubAppPrivateKey` | GitHub secrets | The GitHub App private key used to mint scoped installation tokens for GitHub API operations. Map Shelly's `SHELLY_PRIVATE_KEY` in the caller and Dependabot secret store. | Yes | | `TestData` | GitHub secrets | A single-line JSON object with `secrets` and `variables` maps, exposed as environment variables to the module test jobs. Values under `secrets` are masked; values under `variables` are not. | No | See [passing test data](../guides/calling-the-workflow.md#passing-test-data) for how to build the `TestData` value. -## Permissions +## Workflow `github.token` permissions -The following permissions are needed for the workflow to be able to perform all tasks. +The following permissions are needed by the caller workflow's default `github.token` for operations that do not use +Shelly, such as linting and GitHub Pages deployment: ```yaml permissions: @@ -47,3 +50,10 @@ permissions: ``` For more info, see [Deploy GitHub Pages site](https://github.com/marketplace/actions/deploy-github-pages-site). + +## GitHub App permissions + +The `permissions:` block above does not apply to Shelly's installation tokens. Shelly needs only the repository +permissions documented in [GitHub App authentication](../guides/github-app-authentication.md#github-app-installation-permissions): +Contents: write, Pull requests: write, and automatically granted Metadata: read. Each job requests a smaller, +repository-scoped subset when it mints its token. diff --git a/docs/content/specification/design.md b/docs/content/specification/design.md index 2f17fb13..9990831b 100644 --- a/docs/content/specification/design.md +++ b/docs/content/specification/design.md @@ -12,7 +12,7 @@ The behaviour in the [spec](spec.md) is delivered by a **single reusable GitHub ### Single entry point The reusable workflow accepts a caller workflow and minimal caller configuration: a `pull_request`-triggered job that -calls `workflow.yml` and passes the `APIKey` secret. The full caller template is in +calls `workflow.yml` and passes the `PSGALLERY_API_KEY` secret. The full caller template is in [Repository setup](../get-started/repository-setup.md#3-add-the-caller-workflow), and the interface it targets is documented in [Workflow inputs](../reference/workflow-inputs.md).