#+title: github continuous integration
# org-publish options
# ^:{} require a_{b} before assuming that b should be subscripted.
# without this option a_b will automatically subscript b.
#+options: ^:{} toc:4
#
# emacs-specific options
#+startup: showall
#
# html exporter options
#+setupfile: ../ext/fniessen/theme-readtheorg.setup
#+language: en
# #+infojs_opt: view:showall mouse:#ffc0c0 toc:nil ltoc:nil path:/web/ext/orginfo/org-info.js
# #+html_head:
# +html_link_home: index.html
# +html_link_up: development-environment.html
* Motivation
We want to use a custom (~docker~) container with ~github~ actions, and prepare it using ~nix~.
Several advantages:
1. control over build software (e.g. gcc version).
2. reproducibility -- nix-to-container pipeline guarantees perfect reproducibility
3. flexibility -- container can include prepared software that isn't available on ubuntu
4. size -- container with no excess baggage
* Contents
repos under https://github.com/Rconybea
| scheme | builder | src-repo | action-repo |
|----------+-----------+-----------------------+------------------------|
| scheme 1 | ubuntu | [[https://github.com/Rconybea/hello-example][hello-example]] | (same) |
| scheme 2 | ubuntu | [[https://github.com/Rconybea/docker-action-example][docker-action-example]] | (same) |
| scheme 3 | container | [[https://github.com/Rconybea/docker-nix-hello][docker-nix-hello]] | [[https://github.com/Rconybea/docker-action-example2][docker-action-example2]] |
| scheme 4 | container | [[https://github.com/Rconybea/docker-cpp-builder][docker-cpp-builder]] | [[https://github.com/rconybea/docker-action-example3][docker-action-example3]] |
* Progressive Implementation
We'll present several progressive CI-with-github examples,
that converge on goals above.
** Scheme 1 - github build using base o/s platform
*** Preliminaries
source for ~Scheme 1~ is on github here: [[https://github.com/Rconybea/hello-example]]
**** Directory structure
#+begin_example
hello-example
+- .github
| \- workflow
| \- main.yml
+- CMakeLists.txt
\- hello.cpp
#+end_example
**** Source
#+begin_src c++
// hello.cpp
#include
using namespace std;
int
main(int argc, char * argv[]) {
std::string subject = "World";
if (argc > 1)
subject = argv[1];
cout << "Hello, " << subject << "!" << std::endl;
} /*main*/
#+end_src
**** Cmake build files
#+begin_src cmake
# CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(hello-example VERSION 1.0)
enable_language(CXX)
# write compile_commands.json for LSP
set(CMAKE_EXPORT_COMPILE_COMMANDS ON CACHE INTERNAL "")
set(SELF_EXE hello-example)
set(SELF_SOURCE_FILES hello.cpp)
add_executable(${SELF_EXE} ${SELF_SOURCE_FILES})
install(TARGETS ${SELF_EXE} DESTINATION bin)
if(CMAKE_EXPORT_COMPILE_COMMANDS)
# include otherwise-omitted system directories in compile_commands.json,
# so LSP knows exactly what compiler is using
set(CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES ${CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES})
endif()
#+end_src
**** Build + Run
#+begin_example
$ cd hello-example
$ mkdir build
$ cmake ..
-- Configuring done
-- Generating done
-- Build files have been written to: /home/roland/proj/hello-example/build
$ make
[ 50%] Building CXX object CMakeFiles/hello-example.dir/hello.cpp.o
[100%] Linking CXX executable hello-example
[100%] Built target hello-example
$ ./hello-example
Hello, World!
#+end_example
**** Continuous Integration with Github
We need to do two things:
1. setup a github repo holding our sources:
#+begin_src
$ cd hello-world
$ git remote -v
origin git@github.com:rconybea/hello-example.git (fetch)
origin git@github.com:rconybea/hello-example.git (push)
#+end_src
2. configure github actions for that repo
We add one file, ~hello-world/.github/workflows/main.yml~
#+begin_src yaml
name: c++ build with cmake
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
env:
BUILD_TYPE: Release
jobs:
build_job:
runs-on: ubuntu-latest
steps:
- name: checkout self (hello-example)
uses: actions/checkout@v3
- name: configure self (hello-example)
run: cmake -B ${{github.workspace}}/build -DCMAKE_INSTALL_PREFIX=${{github.workspace}}/local -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}}
- name: build self (hello-example)
run: cmake --build ${{github.workspace}}/build --verbose
- name: test self (hello-example)
working-directory: ${{github.workspace}}/build
run: ./hello-example
#+end_src
github will automatically trigger a build whenever new sources are pushed.
build will run the contents of ~steps~ in order.
** Scheme 2 - docker build using base o/s platform
Instead of building on github-provided base ubuntu platform,
use github-built docker container.
Initially replacing c++ program with shell script, we'll restore it later.
*** Preliminaries
source for ~Scheme 2~ is on github here: https://github.comt/Rconybea/docker-action-example
Build docker container (using resources from ubuntu base platform).
Container:
- invokes a shell script to say hello
- can be used as a github action
**** Directory Structure
#+begin_example
docker-action-example
+- .github
| \- workflows
| \- main.yml
+- action.yml
\- entrypoint.sh
#+end_example
**** Source
~entrypoint.sh~:
#+begin_src sh
#!/usr/bin/env bash
echo "Hello $1"
time=$(date)
echo "time=$time" >> $GITHUB_OUTPUT
#+end_src
**** Docker Container
~DockerFile~:
#+begin_example
# base container image
FROM ubuntu:22.04
# copy files from repo to container filesystem
COPY entrypoint.sh /entrypoint.sh
# default startup executable
ENTRYPOINT ["/entrypoint.sh"]
#+end_example
**** Use Container as github action
toplevel ~action.yml~ describes a custom github action
that operates by invoking docker image
(automagically built from enclosed ~DockerFile~)
~action.yml~:
#+begin_src yaml
name: 'Hello World'
description: 'Greet someone + record the time'
inputs:
who-to-greet:
description: 'Who to greet'
required: true
default: "World"
outputs:
time:
description: "time when greeting made"
runs:
using: 'docker'
image: 'DockerFile'
args:
- ${{ inputs.who-to-greet }}
#+end_src
**** Continuous Integration with Github
Configure github actions for repo https://github.com/Rconybea/docker-action-example;
File ~.github/workflows/main.yml~
#+begin_src yaml
on: [push]
jobs:
hello_world_job:
runs-on: ubuntu-latest
name: a job to say hello, perhaps fiercely
steps:
- name: hello world action step
id: hello
uses: rconybea/docker-action-example@v1
with:
who-to-greet: 'Iffy! What are you doing??'
# do something with output
- name: get output time
run: echo "The time was ${{ steps.hello.outputs.time }}"
#+end_src
In ~main.yml~ here:
- ~rconybea/docker-action-example~ is our github repo,
- ~v1~ is target github tag
** Scheme 3 - custom docker container via nix (no c++)
*** Preliminaries
Source for ~Scheme 3~ uses two repos:
- Custom container build here: [[https://github.com/Rconybea/docker-nix-hello]]
- Workflow using container here: [[https://github.com/Rconybea/docker-action-example2]]
- See also: https://blog.bitsrc.io/using-github-container-registry-in-practice-295677c6f65e]]
**** Directory structure
#+begin_example
docker-nix-hello
+- flake.nix
\- flake.lock # automatically created by nix
#+end_example
#+begin_example
docker-action-example2
\- .github
\- workflows
\- main.yml
#+end_example
**** Source
Prepare minimal custom docker container using a nix flake.
~flake.nix~:
#+begin_src nix
{
description = "hello world";
# dependencies
inputs = rec {
nixpkgs.url = "github:nixos/nixpkgs/23.05";
};
outputs = { self, nixpkgs } :
let
system = "x86_64-linux";
pkgs = import nixpkgs { inherit system; };
hello_deriv = pkgs.writeShellScriptBin "entrypoint.sh" ''
echo "Hello $1"
time=$(date)
echo "time=$time" >> $GITHUB_OUTPUT
'';
docker_hello_deriv =
pkgs.dockerTools.buildLayeredImage {
name = "docker-nix-hello";
tag = "v1";
contents = [ self.packages.${system}.hello
self.packages.${system}.bash
# for /bin/tail, assumed by github actions when invoking a docker container
self.packages.${system}.coreutils ];
config = {
Cmd = [ "/bin/entrypoint.sh" ];
WorkingDir = "/";
};
};
in rec {
packages.${system} = {
default = docker_hello_deriv;
docker_hello = docker_hello_deriv;
hello = hello_deriv;
bash = pkgs.bash;
# for example, github actions creates container with --entrypoint "tail",
# so container must provide executable with that name in $PATH
#
coreutils = pkgs.coreutils;
};
};
}
#+end_src
**** Build Container + Upload to github
Container will be ~docker-nix-hello~.
Steps:
1. get github personal access token,
so docker can send images to ~ghcr.io~.
on [[https://github.com/Rconybea]]:
- visit profile (upper rhs of ~github~ webpage)
- developer settings (bottom of sidebar)
- personal access token
- tokens (classic)
- generate personal access token with scopes:
~read:packages~ ~write:packages~ ~delete:packages~
2. have docker login to github
#+begin_src sh
export CR_PAT=${token}
echo $CR_PAT | docker login ghcr.io -u rconybea --password-stdin
#+end_src
3. nix builds custom image (using ~docker-nix-hello/flake.nix~)
#+begin_src sh
cd ~/proj/docker-nix-hello
nix build
#+end_src
4. load image into docker
#+begin_src sh
docker load <~/proj/docker-nix-hello/result
#+end_src
5. tag image the way github expects: ~ghcr.io/${username}/${imagename}:${tag}~
#+begin_src sh
docker image tag docker-nix-hello:v1 ghcr.io/rconybea/docker-nix-hello:v1
#+end_src
6. push to github container registry
(will show up at https://github.com/Rconybea?tab=packages)
#+begin_src sh
docker image push ghcr.io/rconybea/docker-nix-hello:v1
#+end_src
7. make package public from package's settings link
**** Workflow Using Custom Container
in ~docker-action-example2/.github/workflows/main.yml~:
#+begin_src yaml
on: [push]
jobs:
hello_world_job:
name: a job to say hello, using separate docker container
runs-on: ubuntu-latest
container:
image: ghcr.io/rconybea/docker-nix-hello:v1
steps:
- name: hello world action step
id: hello
run: /bin/entrypoint.sh 'Xioni!'
- name: get output time
run: echo "The time was ${{ steps.hello.outputs.time }}"
#+end_src
** Scheme 4 - custom docker container via nix (with gcc)
Source for ~Scheme 4~ in two repos:
- Custom container build here: [[https://github.com/Rconybea/docker-cpp-builder]]
- Workflow using container here: [[https://github.com/Rconybea/docker-action-example3]]
**** Directory structure
#+begin_example
docker-cpp-builder
+- flake.nix
\- flake.lock # automatically created by nix
#+end_example
#+begin_example
docker-action-example3
+- .github
| \- workflows
| \- main.yml
+- Makefile
+- hello.cpp
#+end_example
**** Source
Prepare custom docker container to deliver build stack (=gcc=, =cmake=, ...)
~flake.nix~:
#+begin_src nix
{
description = "docker c++ builder (using nix)";
# dependencies
inputs = rec {
nixpkgs.url = "github:nixos/nixpkgs/23.05";
};
outputs = { self, nixpkgs } :
let
system = "x86_64-linux";
pkgs = import nixpkgs { inherit system; };
docker_builder_deriv =
pkgs.dockerTools.buildLayeredImage {
name = "docker-cpp-builder";
tag = "v3";
contents = [ self.packages.${system}.git
self.packages.${system}.cacert
self.packages.${system}.gnumake
self.packages.${system}.gcc
self.packages.${system}.binutils
self.packages.${system}.bash
# for /bin/tail, assumed by github actions when invoking a docker contianer
self.packages.${system}.coreutils ];
};
in rec {
packages.${system} = {
default = docker_builder_deriv;
docker_builder = docker_builder_deriv;
git = pkgs.git;
cacert = pkgs.cacert;
gnumake = pkgs.gnumake;
gcc = pkgs.gcc;
binutils = pkgs.binutils;
bash = pkgs.bash;
coreutils = pkgs.coreutils;
};
};
}
#+end_src
**** Build Container + Upload to github
Instructions are the same as for Scheme 3, but using =~/proj/docker-cpp-builder= instead of =~/proj/docker-nix-hello=
#+begin_src sh
cd ~/proj/docker-cpp-builder
git tag v1
nix build
docker load <~/proj/docker-cpp-builder/result
docker image tag docker-cpp-builder:v1 ghcr.io/rconybea/rconybea/docker-cpp-builder:v1
docker image push ghcr.io/rconybea/docker-cpp-builder:v1
#+end_src
After uploading package (docker image) for gcc builder appears here: [[https://github.com/Rconybea?tab=packages]]:
**** Workflow using Custom Container
in ~docker-action-example3/.github/workflows/main.yml~:
#+begin_src yaml
on: [push]
env:
# personal accesss token (using automatically-supplied GIT_TOKEN) with read access to public repos
GIT_USER: rconybea
jobs:
build_job:
name: compile hello world, using prepared docker container
runs-on: ubuntu-latest
container:
image: ghcr.io/rconybea/docker-cpp-builder:v3
steps:
- name: checkout
run:
# not using usual checkout action: bc complications from within container
GIT_SSL_NO_VERIFY=true git clone https://${{env.GIT_USER}}:${{env.GIT_TOKEN}}@github.com/rconybea/docker-action-example3.git
- name: compile
run:
# make,g++ will run in container..
make -C docker-action-example3
- name: hello
run:
./docker-action-example3/hello Roland
#${{github.workspace}}/hello Roland
#+end_src
Note: in non-container build we can checkout code with something like:
#+begin_src yaml
steps:
- name: checkout
- uses: rconybea/my-repo@my-git-tag
#+end_src
Unfortunately that doesn't work out-of-the-box when we use a container for build,
because of a permissioning problem. Spent some time on various internet-advised workarounds,
before settling on the solution above: including =git= in custom docker image, and checking source
out from inside the container.
We need to set ~GIT_SSL_NO_VERIFY~ to prevent an obscure error (passed along from SSL) mentioning an `unrecognized scheme`.
Presumably there's a problem authenticating github.com's certificate