From e4fdab192aa5d742e282d4ebcde19424bf6aa0ca Mon Sep 17 00:00:00 2001 From: Imani Manyara Date: Fri, 13 Nov 2026 12:12:25 -0500 Subject: [PATCH] Let a caller supply a credential for private cross-org dependencies --- .github/workflows/security.yml | 97 ++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 3073091..d73b6b0 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -9,6 +9,20 @@ on: php-extensions: type: string default: 'mbstring, xml, ctype, iconv, intl' + private-dependency-owner: + description: >- + The GitHub owner holding private repositories this package requires through a `vcs` + repository -- `simtabi`, say. Leave empty when every dependency is publicly resolvable, + which is the common case. Setting it turns on the credential block below, and a caller + that sets it without supplying the key fails loudly rather than clone-failing later. + type: string + default: '' + private-dependency-repos: + description: >- + Optional comma-separated repository names under that owner, to narrow the installation + token. Empty means every repository the app can see there. + type: string + default: '' secrets: OP_SERVICE_ACCOUNT_TOKEN: description: >- @@ -19,6 +33,13 @@ on: job that needs a credential cannot redden every caller before the credential exists. A caller must pass `secrets: inherit` for it to arrive at all. required: false + REFRESH_APP_PRIVATE_KEY: + description: >- + Private key of the GitHub App installed on `private-dependency-owner`, used to mint a + per-run installation token so composer can clone that owner's private repositories. + Nothing long-lived is stored. Required only when `private-dependency-owner` is set; the + client id comes from the caller's `vars.REFRESH_APP_CLIENT_ID`. + required: false # Least privilege. Without this the jobs inherit the calling repository's default token scope, # which on an older repository is still read/write on everything. Every job here only reads the @@ -40,6 +61,44 @@ jobs: extensions: ${{ inputs.php-extensions }} tools: composer:v2 + # A package may require a PRIVATE repository in ANOTHER owner through a `vcs` repository. + # A workflow's GITHUB_TOKEN is scoped to the CALLING repository, so composer's clone fails + # with "Invalid username or token" -- an auth error that reads like a dependency problem, six + # steps away from its cause. laranail/sis-wrapper hit exactly this against simtabi/sis-sdk. + - name: Mint a token for private dependencies + id: dep-token + if: inputs.private-dependency-owner != '' + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.REFRESH_APP_CLIENT_ID }} + private-key: ${{ secrets.REFRESH_APP_PRIVATE_KEY }} + owner: ${{ inputs.private-dependency-owner }} + repositories: ${{ inputs.private-dependency-repos }} + permission-contents: read + + # Fail here, loudly, rather than let composer fall through to GITHUB_TOKEN and report a + # confusing clone failure later. A missing credential is a configuration fault and says so. + - name: Require that token + if: inputs.private-dependency-owner != '' + env: + TOKEN: ${{ steps.dep-token.outputs.token }} + run: | + if [ -z "${TOKEN}" ]; then + echo "::error title=no app token::private-dependency-owner is set to '${{ inputs.private-dependency-owner }}' but no installation token was minted. The caller needs vars.REFRESH_APP_CLIENT_ID and secrets.REFRESH_APP_PRIVATE_KEY, and must pass secrets: inherit. A PRIVATE repository on GitHub Free cannot read org-level values, so it needs repository copies." + exit 1 + fi + + # Composer clones the plain https URL and offers the token as `:x-oauth-basic`, which + # GitHub REFUSES for an app installation token. This rewrite supplies the documented form, + # `x-access-token:`, so the clone is attempted correctly the first time. + - name: Let git clone that owner's private repositories + if: inputs.private-dependency-owner != '' + env: + TOKEN: ${{ steps.dep-token.outputs.token }} + OWNER: ${{ inputs.private-dependency-owner }} + run: | + git config --global url."https://x-access-token:${TOKEN}@github.com/${OWNER}".insteadOf "https://github.com/${OWNER}" + - name: Install dependencies # `composer install` will create composer.lock for libraries that # don't commit one — required by `composer audit --locked`. @@ -70,6 +129,44 @@ jobs: extensions: ${{ inputs.php-extensions }} tools: composer:v2 + # A package may require a PRIVATE repository in ANOTHER owner through a `vcs` repository. + # A workflow's GITHUB_TOKEN is scoped to the CALLING repository, so composer's clone fails + # with "Invalid username or token" -- an auth error that reads like a dependency problem, six + # steps away from its cause. laranail/sis-wrapper hit exactly this against simtabi/sis-sdk. + - name: Mint a token for private dependencies + id: dep-token + if: inputs.private-dependency-owner != '' + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.REFRESH_APP_CLIENT_ID }} + private-key: ${{ secrets.REFRESH_APP_PRIVATE_KEY }} + owner: ${{ inputs.private-dependency-owner }} + repositories: ${{ inputs.private-dependency-repos }} + permission-contents: read + + # Fail here, loudly, rather than let composer fall through to GITHUB_TOKEN and report a + # confusing clone failure later. A missing credential is a configuration fault and says so. + - name: Require that token + if: inputs.private-dependency-owner != '' + env: + TOKEN: ${{ steps.dep-token.outputs.token }} + run: | + if [ -z "${TOKEN}" ]; then + echo "::error title=no app token::private-dependency-owner is set to '${{ inputs.private-dependency-owner }}' but no installation token was minted. The caller needs vars.REFRESH_APP_CLIENT_ID and secrets.REFRESH_APP_PRIVATE_KEY, and must pass secrets: inherit. A PRIVATE repository on GitHub Free cannot read org-level values, so it needs repository copies." + exit 1 + fi + + # Composer clones the plain https URL and offers the token as `:x-oauth-basic`, which + # GitHub REFUSES for an app installation token. This rewrite supplies the documented form, + # `x-access-token:`, so the clone is attempted correctly the first time. + - name: Let git clone that owner's private repositories + if: inputs.private-dependency-owner != '' + env: + TOKEN: ${{ steps.dep-token.outputs.token }} + OWNER: ${{ inputs.private-dependency-owner }} + run: | + git config --global url."https://x-access-token:${TOKEN}@github.com/${OWNER}".insteadOf "https://github.com/${OWNER}" + - name: composer install run: composer install --no-interaction --no-progress --prefer-dist