Motivation
Suppose you have a GitHub Actions workflow called your-workflow
, within a repository called your-repo
.
Then suppose that for some reason (see Why would I ever need to do this?), within that workflow, you need to get hold of another repo - let’s call it your-extra-repo
.
That’s easy enough if your-extra-repo
is a public repo: you can “just” use the GitHub-provided actions/checkout
action multiple times within your workflow.
But what if it’s not a public repo?
There are a few possible approaches1 - I’m going to explain my preferred one here.
Deploy keys
A deploy key is an SSH key that you can attach to a single GitHub repository, and which provides access to just that repository.
We can use a deploy key as the core part of our solution. It’s an ideal choice here because it allows us to create a very specific access route:
- From the GitHub Actions runner which executes
your-workflow
- To the non-public
your-extra-repo
- With read-only permissions (you can add write permissions, but we shouldn’t for this particular purpose)
Here’s how I usually set things up:
Create a new SSH keypair - it doesn’t matter what method you use to create it. I’d suggest using the following command in a terminal (a Linux terminal, or the RStudio Terminal, or Windows PowerShell…):
ssh-keygen -t ed25519 -f deploy
If you’re prompted, don’t set a passphrase. This will create two files in your current working called
deploy.pub
anddeploy
, containing the public and private parts respectively of a new SSH key.In GitHub, navigate to
your-extra-repo
. In theSettings
tab, findSecurity > Deploy keys
. Create a new deploy key:Title
- up to you, but I tend to call it something likeyour-repo-your-workflow
2Key
- this must be the public part of the SSH key you just created (you can opendeploy.pub
and copy the entire contents)
Now navigate to the GitHub page for
your-repo
. In theSettings
tab, findSecurity > Secrets and variables > Actions
. Add a repository secret:Name
- again up to you, but I tend to use the patternYOUR_EXTRA_REPO_DEPLOY_KEY
Value
- this must be the private part of the SSH key you just created (you can opendeploy
and copy the entire contents)
Delete both parts of the key from wherever you created it (e.g. delete
deploy.pub
anddeploy
) - we don’t need these any more!
You can follow these same steps to make more than one private repo accessible from your-workflow
- if you do, you should create & use a different deploy key for each one.
Why would I ever need to do this?
So far, I’ve come across two different cases where this trick can be handy!
Case 1: git submodules
Suppose your-repo
contains a git submodule which lives in a private repo your-extra-repo
, and that you need to get to something provided by that submodule within your-workflow
.
(For a concrete example of this, see my previous post about modular R code with {box}.)
The GitHub-provided actions/checkout
action is typically used to check out the current repo within a GitHub Actions workflow. And at first glance, the solution looks simple.
The actions/checkout
action takes some optional parameters:
submodules
- whether to check out submodules (defaultfalse
)
So can we just specify submodules: true
?
Unfortunately not, because the submodule we want is in a private repo; we’ll need to provide some way of verifying that we’re allowed to access it.
Ah-ha! we say, look, here’s another handy optional parameter:
ssh-key
- if provided, it is used to fetch the specified repository (instead of fetching via HTTPS with thegithub.token
generated for the workflow run)
So can we just pass our shiny new deploy key to this parameter via a GitHub secret?
Again, we quickly hit a problem: it seems that if you provide ssh-key
, it is used for all git operations within the checkout action. So if we pass in a deploy key, we end up trying to use that deploy key to clone your-repo
too, leading to failure (remember the whole point of a deploy key is that it allows access to one single repo, your-extra-repo
in this case).
So ideally, we’d like to do a “normal” checkout for your-repo
, but a special SSH checkout for your-extra-repo
…
The trick isn’t too complicated - in fact, it’s adapted from a scenario anticipated by the actions/checkout
repo’s README file:
- Use an
actions/checkout
step to check outyour-repo
as usual - Use another
actions/checkout
step to check out theyour-extra-repo
submodule, taking advantage of some more optional parameters:repository
- which repository to check out (default is the repo which the workflow belongs to, but we’ll ask foryour-extra-repo
instead)path
- the location to check out to withinyour-repo
(the default is.
, but a submodule typically lives within a subdirectory, i.e. you probably don’t want to check outyour-extra-repo
right on top ofyour-repo
)ssh-key
- we’ve met this already! We’ll use it here to pass through the private half of the deploy key we set up previously
jobs:
some-job-name:
runs-on: ubuntu-latest
steps:
- name: Checkout this repo
uses: actions/checkout@v4
with:
submodules: false
- name: Checkout your-extra-repo submodule
uses: actions/checkout@v4
with:
repository: your-user-or-org/your-extra-repo
ssh-key: ${{ secrets.YOUR_EXTRA_REPO_DEPLOY_KEY }}
path: ./path/to/submodule
# More steps...
Note: the explicit submodules: false
isn’t required since false
is the default, but I think it suggests to the casual reader that there’s something funky and submodule-related going on…)
Case 2: pre-commit hooks
We use pre-commit in several of our team’s repos. For these repos, we also set up a GitHub Actions workflow to run pre-commit whenever a pull request is opened or updated. I won’t go into too much detail here, as that’s probably worthy of its own post sometime!
As well as using some hooks from public repos, we have a handful of custom “team hooks” in an internal3 repo within our GitHub organisation.
The problem is that under the hood, pre-commit uses git to get hold of the various hook-supplying repos. So once again, we need some way of using a “regular” checkout for public repos, and then a “non-regular” checkout for our internal repo.
This time, the trick is in two parts:
In
your-repo
, in the.pre-commit-config.yaml
file, use HTTPS-format repo URLs for public repos, and an SSH-format URL for the internal repo:repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - repo: https://github.com/lorenzwalthert/precommit rev: v0.4.1 hooks: - id: parsable-R - repo: git@github.com:your-user-or-org/your-extra-repo rev: v0.0.1 hooks: - id: your-first-hook - id: your-second-hook
In your GitHub Actions workflow, copy the private half of the deploy key from the relevant GitHub secret into a keyfile, and then tell pre-commit to use that SSH key for all SSH operations executed by git:
jobs: run: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-python@v5 - name: Install dependencies run: | python -m pip install --upgrade pip pre-commit # Set up SSH access for some-private-repo mkdir -p ~/.ssh/ echo "${{ secrets.SOME_PRIVATE_REPO_DEPLOY_KEY }}" > ~/.ssh/deploy-key chmod 600 ~/.ssh/deploy-key ssh-keyscan -H github.com >> ~/.ssh/known_hosts - name: Run pre-commit run: | GIT_SSH_COMMAND='ssh -i ~/.ssh/deploy-key -o IdentitiesOnly=yes' \ pre-commit run \ --from-ref ${{ github.event.pull_request.base.sha }} \ --to-ref ${{ github.event.pull_request.head.sha }}
Footnotes
At the time of writing, this mammoth GitHub issue contains an ongoing discussion: https://github.com/actions/checkout/issues/287↩︎
This naming pattern helps to make it clear, if you are ever tidying up your deploy keys, which key was used where.↩︎
You can interchange “private” and “internal” throughout this post - the key thing is that both are “non-public”.↩︎