Skip to content

Commit a30ddf2

Browse files
committed
feat: add GitHub App auth for installing private repository dependencies
intent(private-repo-app): integrate the standalone app.yml prototype into action.yml so users can pull private GitHub deps via this action's inputs decision(input-naming): named the inputs private-repo-app-* so the purpose (private repo access via a GitHub App) is obvious from the input list rejected(input-naming): bare app-* / owner / repositories from the prototype — too generic, no hint that they enable private repo access decision(rewrite-mechanism): GIT_CONFIG_COUNT / GIT_CONFIG_KEY_<n> / GIT_CONFIG_VALUE_<n> env vars to layer url.*.insteadOf on top of the existing global git config rejected(rewrite-mechanism): replacing GIT_CONFIG_GLOBAL with a temp gitconfig — drops the runner's existing global settings and hardcodes ~/.gitconfig while git also reads ~/.config/git/config decision(rewrite-scope): emit per-repository url.*.insteadOf entries so the token only attaches to repos that are in the App installation scope rejected(rewrite-scope): owner-wide rewrite — would catch other repos under the same owner and 404 because the installation token is scoped to specific repos constraint(poetry-git-client): Poetry's default dulwich client ignores GIT_CONFIG_* env vars; POETRY_SYSTEM_GIT_CLIENT / POETRY_EXPERIMENTAL_SYSTEM_GIT_CLIENT are forced so the rewrite applies constraint(cleanup-position): cleanup runs after pre-commit/prek (not right after Setup environment) so private hook repos can still be fetched while the rewrite is active constraint(cleanup-failure-mode): cleanup uses always() so the rewrite is dropped even when poetry install / uv sync fails, preventing the token-bearing env vars from leaking into later if: always() steps learned(actionlint-client-id): actionlint < rhysd/actionlint#652 does not yet recognize client-id on actions/create-github-app-token, hence the .github/actionlint.yaml ignore entries
1 parent ccbb0c8 commit a30ddf2

3 files changed

Lines changed: 147 additions & 0 deletions

File tree

.github/actionlint.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
paths:
2+
.github/workflows/**/*.{yml,yaml}:
3+
ignore:
4+
# Ignore the following errors for actions/create-github-app-token, until https://github.com/rhysd/actionlint/pull/652 is merged and released.
5+
- 'missing input "app-id" which is required by action'
6+
- 'input "client-id" is not defined in action'

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ coverage-push-condition | Condition to push the coverage. This will be shown in
2626
github_token | Token to push `coverage` branch.| `${{ github.token }}` | No
2727
pre-commit | Set `1` to run pre-commit. | `0` | No
2828
prek | Set `1` to run prek. | `0` | No
29+
private-repo-app-client-id | GitHub App Client ID for accessing private GitHub repositories. When set together with `private-repo-app-private-key`, `private-repo-app-owner`, and `private-repo-app-repositories`, a GitHub App installation token is generated and `url.*.insteadOf` rewrites are layered on the existing git config via `GIT_CONFIG_COUNT` so that the dependency installation step (`uv sync` / `poetry install` / `pip install`) can pull from the listed private repositories. | `''` | No
30+
private-repo-app-private-key | Private key of the GitHub App used to access private GitHub repositories. Used together with `private-repo-app-client-id`. | `''` | No
31+
private-repo-app-owner | GitHub organization or user where the GitHub App used for private repository access is installed. Used to scope the generated token and the `insteadOf` rewrites. | `''` | No
32+
private-repo-app-repositories | Private repositories to scope the generated token and `insteadOf` rewrites to. One repository name per line. | `''` | No
33+
private-repo-app-debug | Set `1` to print the resolved (token-redacted) `url.*.insteadOf` git config and run `git ls-remote` against each listed repository after the rewrite is configured. | `0` | No
2934
tmate | Set `1` to run [tmate](https://mxschmitt.github.io/action-tmate/). | `0` | No
3035

3136
If you want to enable `coverage`,
@@ -154,3 +159,21 @@ jobs:
154159
* Set `IS_MAIN` to run `prek` and `push` only for the main combination of the matrix of `os` and `python-version`.
155160

156161
Ref: https://github.com/rcmdnk/python-action-test/actions
162+
163+
### Installing private GitHub dependencies via a GitHub App
164+
165+
If your project depends on private repositories on GitHub, register a GitHub App
166+
installed on the relevant repositories with `Contents: read` permission, then
167+
pass its credentials to the action:
168+
169+
```yaml
170+
- uses: rcmdnk/python-action@v1
171+
with:
172+
setup-type: uv
173+
private-repo-app-client-id: ${{ vars.MY_APP_CLIENT_ID }}
174+
private-repo-app-private-key: ${{ secrets.MY_APP_PRIVATE_KEY }}
175+
private-repo-app-owner: my-org
176+
private-repo-app-repositories: |
177+
my-private-lib
178+
another-private-lib
179+
```

action.yml

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,26 @@ inputs:
1414
description: 'Optional shell command(s) to run right after checkout. Multiple commands can be passed as a multi-line string.'
1515
default: ''
1616
required: false
17+
private-repo-app-client-id:
18+
description: 'GitHub App Client ID for accessing private GitHub repositories. If set together with private-repo-app-private-key, private-repo-app-owner, and private-repo-app-repositories, a GitHub App token is generated and used to rewrite git URLs so that private GitHub repositories can be accessed during dependency installation.'
19+
default: ''
20+
required: false
21+
private-repo-app-private-key:
22+
description: 'Private key of the GitHub App used to access private GitHub repositories. Used together with private-repo-app-client-id.'
23+
default: ''
24+
required: false
25+
private-repo-app-owner:
26+
description: 'GitHub organization or user where the GitHub App used for private repository access is installed. Used to scope the generated GitHub App token.'
27+
default: ''
28+
required: false
29+
private-repo-app-repositories:
30+
description: 'Private GitHub repositories to scope the generated GitHub App token to. One repository name per line.'
31+
default: ''
32+
required: false
33+
private-repo-app-debug:
34+
description: 'Set 1 to print sanitized git config and run git ls-remote tests after configuring the GitHub App token for private repository access.'
35+
default: '0'
36+
required: false
1737
setup-python:
1838
description: 'Set 1 to run setup-python.'
1939
default: '1'
@@ -137,6 +157,79 @@ runs:
137157
${{ inputs.post-checkout-command }}
138158
echo "::endgroup::"
139159
shell: bash
160+
- name: Generate GitHub App token for private repository access
161+
id: private-repo-app-token
162+
if: ${{ inputs.private-repo-app-client-id != '' && inputs.private-repo-app-private-key != '' && inputs.private-repo-app-owner != '' && inputs.private-repo-app-repositories != '' }}
163+
uses: actions/create-github-app-token@v3
164+
with:
165+
client-id: ${{ inputs.private-repo-app-client-id }}
166+
private-key: ${{ inputs.private-repo-app-private-key }}
167+
owner: ${{ inputs.private-repo-app-owner }}
168+
repositories: ${{ inputs.private-repo-app-repositories }}
169+
permission-contents: read
170+
- name: Configure Git URL rewrite for private repositories
171+
id: private-repo-app-config
172+
if: ${{ inputs.private-repo-app-client-id != '' && inputs.private-repo-app-private-key != '' && inputs.private-repo-app-owner != '' && inputs.private-repo-app-repositories != '' }}
173+
shell: bash
174+
env:
175+
APP_TOKEN: ${{ steps.private-repo-app-token.outputs.token }}
176+
APP_OWNER: ${{ inputs.private-repo-app-owner }}
177+
APP_REPOS: ${{ inputs.private-repo-app-repositories }}
178+
run: |
179+
set -euo pipefail
180+
181+
# Use GIT_CONFIG_COUNT/GIT_CONFIG_KEY_<n>/GIT_CONFIG_VALUE_<n> to add
182+
# url.*.insteadOf entries on top of the runner's existing global git
183+
# config (~/.gitconfig or $XDG_CONFIG_HOME/git/config). This avoids
184+
# replacing the global config and assuming a particular path.
185+
# Per-repository entries keep the token attached only to the listed
186+
# repositories.
187+
count=0
188+
while IFS= read -r repo; do
189+
repo="$(echo "$repo" | xargs)"
190+
[ -z "$repo" ] && continue
191+
target="https://x-access-token:${APP_TOKEN}@github.com/${APP_OWNER}/${repo}"
192+
for src in \
193+
"ssh://git@github.com/${APP_OWNER}/${repo}" \
194+
"git@github.com:${APP_OWNER}/${repo}" \
195+
"https://github.com/${APP_OWNER}/${repo}"
196+
do
197+
key_name="GIT_CONFIG_KEY_${count}"
198+
val_name="GIT_CONFIG_VALUE_${count}"
199+
key_val="url.${target}.insteadOf"
200+
export "${key_name}=${key_val}"
201+
export "${val_name}=${src}"
202+
{
203+
echo "${key_name}=${key_val}"
204+
echo "${val_name}=${src}"
205+
} >> "$GITHUB_ENV"
206+
count=$((count + 1))
207+
done
208+
done <<< "$APP_REPOS"
209+
export GIT_CONFIG_COUNT="${count}"
210+
echo "GIT_CONFIG_COUNT=${count}" >> "$GITHUB_ENV"
211+
echo "count=${count}" >> "$GITHUB_OUTPUT"
212+
213+
# Poetry uses dulwich by default, which does not honor GIT_CONFIG_*
214+
# env vars. Force the system git client so the rewrite applies.
215+
echo "POETRY_SYSTEM_GIT_CLIENT=true" >> "$GITHUB_ENV"
216+
echo "POETRY_EXPERIMENTAL_SYSTEM_GIT_CLIENT=true" >> "$GITHUB_ENV"
217+
218+
if [ "${{ inputs.private-repo-app-debug }}" = "1" ]; then
219+
echo "::group::git config (effective)"
220+
git config --get-regexp '^url\..*\.insteadOf$' \
221+
| sed -E 's#x-access-token:[^@]+@#x-access-token:***@#g' || true
222+
echo "::endgroup::"
223+
224+
echo "::group::git ls-remote tests"
225+
while IFS= read -r repo; do
226+
repo="$(echo "$repo" | xargs)"
227+
[ -z "$repo" ] && continue
228+
echo "Testing ${APP_OWNER}/${repo}"
229+
git ls-remote "ssh://git@github.com/${APP_OWNER}/${repo}" HEAD
230+
done <<< "$APP_REPOS"
231+
echo "::endgroup::"
232+
fi
140233
- name: Install poetry
141234
if: ${{ inputs.setup-type == 'poetry' }}
142235
run: |
@@ -276,6 +369,31 @@ runs:
276369
git checkout .
277370
echo "::endgroup::"
278371
shell: bash
372+
- name: Clear GitHub App token git environment
373+
# Use always() so the rewrite is dropped even when an earlier step
374+
# (e.g. poetry install / uv sync) fails, preventing the tokenized git
375+
# access from leaking into later workflow steps that use if: always().
376+
if: ${{ always() && inputs.private-repo-app-client-id != '' && inputs.private-repo-app-private-key != '' && inputs.private-repo-app-owner != '' && inputs.private-repo-app-repositories != '' }}
377+
shell: bash
378+
env:
379+
COUNT: ${{ steps.private-repo-app-config.outputs.count }}
380+
run: |
381+
set -euo pipefail
382+
# Disable the env-based git config layer so subsequent git operations
383+
# see only the runner's normal global config.
384+
echo "GIT_CONFIG_COUNT=0" >> "$GITHUB_ENV"
385+
# Scrub the token-bearing GIT_CONFIG_KEY_<n>/VALUE_<n> values so the
386+
# app token does not appear in `printenv` for later steps.
387+
if [ -n "${COUNT:-}" ] && [ "${COUNT}" -gt 0 ] 2>/dev/null; then
388+
i=0
389+
while [ "${i}" -lt "${COUNT}" ]; do
390+
echo "GIT_CONFIG_KEY_${i}=" >> "$GITHUB_ENV"
391+
echo "GIT_CONFIG_VALUE_${i}=" >> "$GITHUB_ENV"
392+
i=$((i + 1))
393+
done
394+
fi
395+
echo "POETRY_SYSTEM_GIT_CLIENT=" >> "$GITHUB_ENV"
396+
echo "POETRY_EXPERIMENTAL_SYSTEM_GIT_CLIENT=" >> "$GITHUB_ENV"
279397
- name: Pytest coverage comment
280398
id: coverageComment
281399
if: ${{ inputs.coverage == '1' }}

0 commit comments

Comments
 (0)