Inception style builds with private GitHub dependencies
Or “The build within a build within …”
Recently my colleagues at SCND and myself found ourselves facing an interesting challenge: building a Docker image of an application written in Rust and depending on Rust libraries in private GitHub repositories; of course not locally, but via GitHub Actions.
So this can be broken down into a couple of smaller challenges:
- Build a Docker image of a Rust application in GitHub Actions.
- Depend on a library in a private GitHub repository in GitHub Actions.
- See how to integrate the two above.
Docker build
There are fantastic publicly available GitHub Actions to build (and publish) a Docker image, e.g. docker/metadata-action
, docker/login-action
and in particular build-push-action
. Given a Dockerfile
and the necessary GitHub permissions, a partial workflow could look like this:
1 | name: release |
And a Dockerfile
could look like this:
1 | ARG RUST_VERSION=1.75.0 |
Unless the application depends on libraries in private GitHub repositories, the release
job will execute successfully.
Dependencies on libraries in private GitHub repository
Locally we can easily depend on libraries in private GitHub repositories using Cargo’s git
dependency feature with ssh URLs:
1 | foo = { git = "ssh://git@github.com/<<ORGANIZATION>>/foo" } |
For this to work, we just need to have our personal public SSH key registered at GitHub, what most every developer has. We also have to tweak a specific Cargo setting via the .cargo/config.toml
file:
1 | [net] |
If we want CI to be able to build and test applications with such ssh dependencies, we have to put in some more effort. First we need to create a pair of SSH keys. Then we add the public one as deploy key to the GitHub repository which hosts the library dependency. Next we add the private key as action secret to the GitHub repository of the application. And finally we add the fantastic public webfactory/ssh-agent
GitHub Action to our build:
1 | - name: Install SSH agent |
With all that in place, we can write workflows with jobs/steps to compile, test, etc. our application. But all of that takes place within the GitHub runner. It is still not possible to execute the above release
job.
Why? Because the webfactory/ssh-agent
GitHub Action does the “SSH magic”, but only within the context of the GitHub runner. When GitHub Actions starts the Docker build which then starts the build of the Rust application – notice the build within a build within … – neither the SSH public key of the GitHub server hosting the library nor the private key for accessing that repository are available.
Integration
Luckily it is quite easy to solve the resulting challenge. There are numerous somewhat relevant examples in the internet, but none could be found for our exact use case. Yet we were able to put the pieces together.
First the SSH agent socket needs to be passed down to the Docker build, which can be achieved via the ssh option of the docker/build-push-action
GitHub action:
1 | - name: Docker build and push |
This essentially makes the private SSH key for the library in the private GitHub repository available within the Docker build. In order to also make it available one dream build level deeper, i.e. in the Rust build, the respective RUN
needs to be given the --mount=type=ssh
option:
1 | RUN \ |
Now we are almost there. The last missing piece is to add the public key of the GitHub server to the known_hosts
file in the builder layer in the Dockerfile, of course before the above build:
1 | RUN \ |
Phew, we are finally there! So it turns out that the essence of making this work is to pass the private SSH key down from the “top level” build, where it is defined as an action secret, to the Docker build from where it needs to be passed down to the Rust build. Inception at its best! Of course we also must not forget to add the public key of the GitHub server to known_hosts
.