diff --git a/.github/workflows/build-and-publish-image.yml b/.github/workflows/build-and-publish-image.yml index 7d0014dc2..6c340e19e 100644 --- a/.github/workflows/build-and-publish-image.yml +++ b/.github/workflows/build-and-publish-image.yml @@ -3,6 +3,7 @@ # separate terms of service, privacy policy, and support # documentation. + name: Build and publish Docker image on: @@ -41,7 +42,17 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 - + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + - name: Cache Docker layers + uses: actions/cache@v4 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- - name: Extract version number run: | VER=$(cat VERSION) @@ -74,3 +85,6 @@ jobs: build-args: | CONTAINER_VERSION=${{ env.VERSION }} labels: ${{ steps.meta.outputs.labels }} + platforms: linux/amd64,linux/arm64/v8 + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache diff --git a/.github/workflows/call_jobs.yml b/.github/workflows/call_jobs.yml new file mode 100644 index 000000000..86567b839 --- /dev/null +++ b/.github/workflows/call_jobs.yml @@ -0,0 +1,29 @@ +name: Call reusable workflows + +on: + pull_request: + branches: + - main + - devel + types: + - closed + paths: + - '.github/workflows/reuse-build-and-publish-amd.yml' + - '.devcontainer/devcontainer.json' + - 'Dockerfile' + - 'reinstall-cmake.sh' + - 'VERSION' + +jobs: + call-workflow-amd64: + permissions: + contents: read + packages: write + uses: ./.github/workflows/reuse-build-and-publish.yml + with: + image_name: ${{ github.repository }} + ref_name: ${{ github.ref_name }} + actor: ${{ github.actor }} + registry: ghcr.io + platform: amd64 + secrets: inherit diff --git a/.github/workflows/reuse-build-and-publish.yml b/.github/workflows/reuse-build-and-publish.yml new file mode 100644 index 000000000..9ea4a6a22 --- /dev/null +++ b/.github/workflows/reuse-build-and-publish.yml @@ -0,0 +1,99 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Build and publish amd 64 Docker image + +on: + workflow_call: + inputs: + image_name: + description: image name + required: true + type: string + ref_name: + description: ref name + required: true + type: string + actor: + description: actor + required: true + type: string + registry: + description: registry + required: true + type: string + platform: + description: platform + required: true + type: string +permissions: + contents: read + packages: write + +jobs: + reuse-build-and-push: + + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Extract version number + run: | + VER=$(cat VERSION) + echo "VERSION=$VER" >> $GITHUB_ENV + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ inputs.registry }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ inputs.registry }}/${{ inputs.image_name }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern=${{ env.VERSION }} + type=sha + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + pull: true + tags: ${{ inputs.registry }}/${{ inputs.image_name }}:${{ inputs.ref_name }} + build-args: | + CONTAINER_VERSION=${{ env.VERSION }} + labels: ${{ steps.meta.outputs.labels }} + - name: Write manifest and build history file + run: | + sudo apt-get install r-base -y && sudo Rscript -e "install.packages('optparse')" \ + && Rscript scripts/manifest.R \ + --registry ${{ inputs.registry }} \ + --owner ${{ inputs.actor}} \ + --image ${{ inputs.ref_name }} \ + --historydir /tmp/r-devel/hist_lines \ + --manifestdir /tmp/r-devel/manifests \ + --repository ${{ inputs.image_name }} + shell: bash + - name: Upload manifest file + uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.platform }}-manifest + path: /tmp/r-devel/manifests/manifest-*.md + retention-days: 3 + - name: Upload build history line + uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.platform }}-history_line + path: /tmp/r-devel/hist_lines/history-*.txt + retention-days: 3 diff --git a/scripts/manifest.R b/scripts/manifest.R new file mode 100644 index 000000000..83fbc66cb --- /dev/null +++ b/scripts/manifest.R @@ -0,0 +1,228 @@ +# manifest.R +# +# Files takes the container arguments and creates two files inside the +# given container. These are the build history and manifest files. +# +# The build history file has the locations of the manifest and git history +# files. This is stored in the location given in histlines argument. +# +# The manifest file has the installed apt and R packages written into it +# along with the build time and commit hash for this build. This is is stored +# in the manifestdir location. +# +# Both are stored as markdown files on the container itself. + +library(optparse) + +option_list <- list( + make_option(c("-r", "--registry"), type = "character", default = "ghcr.io", + help = "Registry name"), + make_option(c("-o", "--owner"), type = "character", default = "r-devel", + help = "Current owner of the Repository"), + make_option(c("-i", "--image"), type = "character", default = "devel", + help = "Provide the image name [default %default]"), + make_option(c("-t", "--historydir"), type = "character", + default = "/tmp/hist_lines/", help = "location of history lines"), + make_option(c("-m", "--manifestdir"), type = "character", + default = "/tmp/manifests/", + help = "Manifest file location [default %default]"), + make_option(c("-p", "--repository"), type = "character", + default = "r-dev-env", help = "Repository name [ %default]") +) + +parser <- OptionParser(usage = "%prog [options] file", + option_list = option_list) + +args <- parse_args(parser, positional_arguments = 0) +opt <- args$options + +#set up constants +build_time <- as.numeric(Sys.time()) +tm <- as.POSIXlt(build_time, "UTC") +utc_time <- strftime(tm, "%Y-%m-%dT%H:%M:%S%z") + +manifest_filename <- paste("manifest-", build_time, ".md", collapse = NULL, + sep = "") + +history_filename <- paste("history-", build_time, ".txt", collapse = NULL, + sep = "") + +#set up docker +docker_id <- paste(opt$registry, "/", opt$repository, ":", + opt$image, collapse = NULL, sep = "") + +#container_id = system2("docker", args = c("run", "-i -d", docker_id, "bash"), +# stdout = TRUE) + +write_all <- function() { + write_manifest_details() + write_history_details() +} + +write_manifest_details <- function() { + details <- get_manifest_details() + write_manifest(details, opt$manifestdir, manifest_filename) +} + +write_history_details <- function() { + details <- get_build_history() + write_manifest(details, opt$historydir, history_filename) +} + +write_manifest <- function(manifestdata, filepth, filename) { + + my_directory <- file.path(filepth) + if (!dir.exists(my_directory)) { + dir.create(my_directory, recursive = TRUE) + } + + file_conn <- file(manifest_filename) + writeLines(manifestdata, paste(my_directory, filename, + sep = "/", collapse = NULL)) + close(file_conn) +} + +get_manifest_details <- function() { + + buildregistry <- paste("registry", opt$registry, sep = ":") + buildowner <- paste("owner", opt$owner, sep = ":") + buildimage <- paste("image", opt$image, sep = ":") + buildrepository <- paste("repository", opt$repository, sep = ":") + buildtime <- paste("timestamp", build_time, sep = ":") + + r_ver <- r_version() + rbuildversion <- paste("### R Version: ", r_ver, sep = "\n") + + r_package <- r_packages() + rbuildpackages <- paste("### R Packages: ", r_package, sep = "\n") + + apt_package <- apt_packages() + aptbuildpackages <- paste("### APT Packages: ", apt_package, sep = "\n") + + manifest_d <- manifest_string() + + title <- paste(opt$owner, opt$repository, sep = "/") + build_title <- paste("# Build manifest for image ", title, ":", opt$image, + sep = "") + + builds <- paste(build_title, buildregistry, buildrepository, buildowner, + buildimage, buildtime, manifest_d, + rbuildversion, rbuildpackages, aptbuildpackages, " ", + sep = "\n\n") + + builds +} + +get_build_history <- function() { + + platform <- "amd64" + + tags <- "default" + + commit <- get_commit_hash() + + diff_url <- paste("https://github.com/", opt$repository, collapse = NULL, + sep = "") + + diff <- paste("[Git diff](", diff_url, "/commit/", commit, ")", + collapse = NULL, sep = "") + dockerfile <- paste("[Dockerfile](", diff_url, "/blob/", commit, ")", + collapse = NULL, sep = "") + manif <- paste("[Build manifest] (./", manifest_filename, ")", + collapse = NULL, sep = "") + + build_history <- paste(diff, dockerfile, manif, sep = "
") + + image_manifest <- paste("{", tags, ":", platform, "}", collapse = NULL, + sep = "") + + build_conf <- paste(utc_time, image_manifest, build_history, sep = " | ") + + build_conf +} + + +## Git helpers +get_commit_hash <- function() { + arguments <- c("rev-parse", "HEAD") + hash <- system2("git", args = arguments, stdout = TRUE) + hash +} + +get_commit_hash_tag <- function() { + x <- get_commit_hash() + tag <- substr(x, nchar(x) - 12 + 1, nchar(x)) + tag +} + +##Linux Helper +get_linux_release <- function() { + os <- docker_command(c("lsb_release", "-a")) + release <- paste(os, sep = " ", collapse = "\n") + release +} + +#packages +r_version <- function() { + + r_version <- docker_command(c("R", "--version")) + details <- strsplit(r_version, "\r", fixed = TRUE) + #version <- paste(details[[1]], details[[3]], sep = " ", collapse = "\n\n") + version <- paste(details, sep = " ", collapse = "\n\n") + version +} + +r_packages <- function() { + + # from dockerstack + r <- docker_command(c("R", "--silent", "-e", + "'installed.packages(.Library)[, c(1,3)]'")) + #tidy the response by removing final > and the initial commands + package <- paste(unlist(strsplit(r, "\r", fixed = TRUE)[3:length(r) - 1]), + sep = " ", collapse = "\n") + package +} + +apt_packages <- function() { + e <- docker_command(c("apt", "list", "--installed")) + #remove listing line and then clean up response + install <- paste(strsplit(e, "\r", fixed = TRUE)[2:length(e)], sep = " ", + collapse = " \n") + remove_local <- gsub("\\[installed,local\\]", "", install) + install1 <- gsub("\\[32m", "", remove_local) + installed <- unlist(paste(gsub("\\[0m/now", "", install1), sep = " ", + collapse = " \n")) + installed +} + +# manifest +manifest_string <- function() { + m <- paste("default", get_commit_hash_tag(), utc_time, get_linux_release(), + collapse = NULL, sep = " ") + m +} + +# Docker functions +docker_start <- function() { + args <- system2("docker", args = c("run", "-i -d", docker_id, "bash"), + stdout = TRUE) + args +} + +docker_stop <- function() { + system2("docker", args = c("stop", container_id)) +} + +docker_command <- function(arguments) { + docker <- paste("exec", "-i", container_id, "bash", "-c", + sep = " ", collapse = "") + + a <- paste('"', paste(arguments, sep = " ", collapse = " "), '"', + collapse = NULL) + + cmd <- system2("docker", args = c(docker, a), stdout = TRUE) + cmd +} + +container_id <- docker_start() +write_all()