Skip to content

Commit

Permalink
Initial implementation (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
omus authored Jan 20, 2025
1 parent 152cb14 commit e2e156b
Show file tree
Hide file tree
Showing 11 changed files with 630 additions and 1 deletion.
12 changes: 12 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# https://editorconfig.org/

# https://manpages.debian.org/testing/shfmt/shfmt.1.en.html#EXAMPLES
[*.sh]
indent_style = space
indent_size = 4
shell_variant = bash # --language-variant
binary_next_line = false
switch_case_indent = true # --case-indent
space_redirects = false
keep_padding = false
function_next_line = false # --func-next-line
25 changes: 25 additions & 0 deletions .github/workflows/gha.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
name: GitHub Actions
on:
pull_request:
paths:
- ".github/workflows/*"

jobs:
lint:
name: Lint
# These permissions are needed to:
# - Checkout the Git repo (`contents: read`)
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# https://github.com/rhysd/actionlint/blob/v1.7.6/docs/usage.md#use-actionlint-on-github-actions
# https://github.com/rhysd/actionlint/blob/v1.7.6/docs/usage.md#reviewdog
# https://github.com/reviewdog/reviewdog#filter-mode
# No support for non-workflows yet: https://github.com/rhysd/actionlint/issues/46
- uses: reviewdog/action-actionlint@a1b7ce56be870acfe94b83ce5f6da076aecc6d8c # v1.62.0
with:
fail_level: error
filter_mode: nofilter # Post results on all results and not just changed files
196 changes: 196 additions & 0 deletions .github/workflows/integration-tests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
---
name: Integration Tests
on:
pull_request:
paths:
- "action.yaml"
- ".github/workflows/integration-tests.yaml"
push:
paths:
- "action.yaml"
- ".github/workflows/integration-tests.yaml"

concurrency:
group: integration-tests-${{ github.event_name == 'pull_request' && 'pr' || 'push' }}-${{ github.event.pull_request.head.sha || github.sha }}
cancel-in-progress: true

jobs:
filter-matrix:
name: Filter Matrix
runs-on: ubuntu-latest
outputs:
test-json: ${{ steps.filter.outputs.test-json }}
cleanup-json: ${{ steps.filter.outputs.cleanup-json }}
steps:
- name: Filter Matrix
id: filter
shell: bash
run: |
# Remove any entries with keys containing `null` values.
test_yaml="$(yq 'map(select(to_entries | map(.value != null) | all))' <<<"${matrix:?}")"
# Validate we do not accidentally test against the same package and commit SHA.
yq -o=json <<<"$test_yaml" | jq -e '(map({package, "commit-sha"}) | unique | length) == length' || exit 1
# Automatically cleanup the `cache-sha-*` tags for the specific test commits.
cleanup_yaml="$(yq 'group_by(.package) | map({"package": .[0].package, "tags" : map(.commit-sha) | unique | map("cache-sha-" + .) | join(",")})' <<<"$test_yaml")"
# Output our multiline YAML document using GH action flavored heredoc
# https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings
{
echo "test-json<<EOF"
yq -o json <<<"$test_yaml"
echo "EOF"
echo "cleanup-json<<EOF"
yq -o json <<<"$cleanup_yaml"
echo "EOF"
} | tee -a "$GITHUB_OUTPUT"
env:
# We need to avoid running concurrent tests using the same commit SHA and
# writing to the same image-repository. If we do not then we could see false
# positive test results if one of them doesn't actually push cache layers. We
# address this problem by:
#
# 1. Ensuring tests which run in parallel either use separate image repositories
# or different Git commit SHAs.
# 2. Utilizing concurrency groups to avoid having multiple instances of this
# workflow run in parallel when triggered on the same commit SHA.
# 3. Deleting the `cache-sha-*` tags to ensure our running workflow produced
# those images. Ideally, we'd delete these before the tests run but attempting
# to delete images from non-existing packages causes failures so this works
# well enough.
#
# I also considered revising the action to avoid pushing images entirely.
# Doing this may be challenging in otherways as pushing the image is a
# requirement for getting the digests in some contexts:
# https://github.com/docker/build-push-action/issues/906#issuecomment-1674567311
matrix: |
- title: ${{ github.event_name == 'pull_request' && 'Merge Commit' || '' }}
package : temporary/whalesay-pr
commit-sha: ${{ github.sha }}
from-scratch: false
- title: Head Commit
package: temporary/whalesay-${{ github.event_name == 'pull_request' && 'pr' || 'push' }}
commit-sha: ${{ github.event.pull_request.head.sha || github.sha }}
from-scratch: false
- title: ${{ github.event_name == 'pull_request' && 'Merge Commit From Scratch' || '' }}
package: temporary/whalesay-pr-from-scratch
commit-sha: ${{ github.sha }}
from-scratch: true
- title: Head Commit From Scratch
package: temporary/whalesay-${{ github.event_name == 'pull_request' && 'pr' || 'push' }}-from-scratch
commit-sha: ${{ github.event.pull_request.head.sha || github.sha }}
from-scratch: true
test:
name: Test ${{ matrix.test.title }}
needs: filter-matrix
# These permissions are needed to:
# - Checkout the repo
permissions:
contents: read
packages: write
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
test: ${{ fromJSON(needs.filter-matrix.outputs.test-json) }}
steps:
- name: Job started at
id: job-started
run: |
job_started_at="$(date --utc --iso-8601=seconds)"
echo "at=$job_started_at" | tee -a "$GITHUB_OUTPUT"
- uses: actions/checkout@v4
with:
ref: ${{ matrix.test.commit-sha }}
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- uses: ./
id: build
with:
image-repository: ghcr.io/beacon-biosignals/${{ matrix.test.package }}
context: test
build-args: |
DEBIAN_VERSION=12.9
from-scratch: ${{ matrix.test.from-scratch || 'false' }}
- name: Validate image works
run: |
docker pull "${{ steps.build.outputs.image }}"
output="$(docker run "${{ steps.build.outputs.image }}")"
if [[ "$(wc -l <<<"$output")" -lt 14 ]]; then
echo "$output"
exit 1
fi
debian_version="$(docker run --entrypoint=/bin/cat "${{ steps.build.outputs.image }}" /etc/debian_version)"
[[ "$debian_version" == "12.9" ]] || exit 2
- name: Layer created at
id: layer-created
run: |
layer_created_at="$(docker run --entrypoint=/bin/cat "${{ steps.build.outputs.image }}" /etc/layer-created-at)"
echo "at=$layer_created_at" | tee -a "$GITHUB_OUTPUT"
# Test will fail if this is the first time the image was build in the image-repository
- name: Validate layer caching
if: ${{ matrix.test.from-scratch == false }}
run: |
[[ "$(date -d "$layer_created_at" +%s)" -lt "$(date -d "$job_started_at" +%s)" ]] || exit 1
env:
job_started_at: ${{ steps.job-started.outputs.at }}
layer_created_at: ${{ steps.layer-created.outputs.at }}
- name: Validate no layer caching
if: ${{ matrix.test.from-scratch == true }}
run: |
[[ "$(date -d "$layer_created_at" +%s)" -gt "$(date -d "$job_started_at" +%s)" ]] || exit 1
env:
job_started_at: ${{ steps.job-started.outputs.at }}
layer_created_at: ${{ steps.layer-created.outputs.at }}
- name: Validate cache images
run: |
docker manifest inspect "${{ steps.build.outputs.image-repository }}:cache-sha-${{ matrix.test.commit-sha }}"
# Should only be skipped when workflow is triggered by a tag push
if [[ -n "$branch" ]]; then
docker manifest inspect "${{ steps.build.outputs.image-repository }}:cache-branch-${branch//[^[:alnum:]]/-}"
fi
env:
branch: ${{ github.head_ref || (github.ref_type == 'branch' && github.ref_name) }}
- name: Validate annotations
run: |
set -x
json="$(docker manifest inspect "${{ steps.build.outputs.image }}")"
[[ "$(jq -r '.annotations."org.opencontainers.image.revision"' <<<"$json")" == "${{ matrix.test.commit-sha }}" ]] || exit 1
- name: Validate docker/metadata-output environment variables are overwritten
shell: bash
run: |
if [[ "$(printenv | grep '^DOCKER_METADATA_OUTPUT_' | grep -c '[^=]$')" -ne 0 ]]; then
printenv | grep '^DOCKER_METADATA_OUTPUT_'
exit 1
fi
cleanup:
name: Cleanup (${{ matrix.cleanup.package }})
needs:
- filter-matrix
- test
if: ${{ !cancelled() }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
cleanup: ${{ fromJSON(needs.filter-matrix.outputs.cleanup-json || '[]') }}
steps:
- uses: dataaxiom/ghcr-cleanup-action@cd0cdb900b5dbf3a6f2cc869f0dbb0b8211f50c4 # v1.0.16
with:
package: ${{ matrix.cleanup.package }}
delete-tags: ${{ matrix.cleanup.tags }}
- uses: dataaxiom/ghcr-cleanup-action@cd0cdb900b5dbf3a6f2cc869f0dbb0b8211f50c4 # v1.0.16
with:
package: ${{ matrix.cleanup.package }}
older-than: 1 day
keep-n-tagged: 0
exclude-tags: branch-main,cache-branch-main
34 changes: 34 additions & 0 deletions .github/workflows/shell.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
name: Shell
on:
pull_request:
paths:
- "**.sh"
- ".github/workflows/*"

jobs:
lint-format:
name: Lint & Format
# These permissions are needed to:
# - Checkout the Git repo (`contents: read`)
# - Post a comments on PRs: https://github.com/luizm/action-sh-checker#secrets
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Extract workflow shell scripts
id: extract
uses: beacon-biosignals/gha-extract-shell-scripts@v1
- uses: luizm/action-sh-checker@c6edb3de93e904488b413636d96c6a56e3ad671a # v0.8.0
env:
GITHUB_TOKEN: ${{ github.token }}
with:
sh_checker_comment: true
# Support investigating linting/formatting errors
- uses: actions/upload-artifact@v4
if: ${{ failure() }}
with:
name: workflow-scripts
path: ${{ steps.extract.outputs.output-dir }}
18 changes: 18 additions & 0 deletions .github/workflows/yaml.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
# https://yamllint.readthedocs.io/en/stable/integration.html#integration-with-github-actions
name: YAML
on:
pull_request:
paths:
- "**/*.yaml"
- "**/*.yml"
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install yamllint
run: pip install yamllint
- name: Lint YAML files
run: yamllint . --format=github
8 changes: 8 additions & 0 deletions .yamllint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
rules:
indentation:
spaces: 2
indent-sequences: true
document-start:
present: true
new-line-at-end-of-file: enable
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2025 Beacon Biosignals

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
70 changes: 69 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,69 @@
# docker-build
# Docker Build

Build a Docker image while utilizing [layer caching](https://docs.docker.com/build/cache/) backed from the image repository. Image tags will be automatically created based upon the relevant PR, branch name, and commit SHA.

When using this action we recommend utilizing a separate image repositories for development and production (e.g.`temporary/my-image` and `permanent/my-image`) to make it easier to separate temporary images from permanent images meant for end users. The `beacon-biosignals/docker-build` action is used to build temporary images under development. Once a temporary image is ready for production it can be promoted to be permanent by using `docker tag`/`docker push` or [`regctl image copy --digest-tags`](https://github.com/regclient/regclient/blob/main/docs/regctl.md#registry-commands) (if you want the digest to be identical across registries) to transfer the image.

Note that although [Docker does support using GitHub Actions cache](https://docs.docker.com/build/cache/backends/gha/) as a layer cache backend the GHA cache limit for a repository is 10 GB which is quite limiting for larger Docker images.

## Example

```yaml
---
on:
pull_request: {}
# Trigger this build workflow on "main". See `from-scratch`
push:
branches:
- main
jobs:
example:
# These permissions are needed to:
# - Get the workflow run: https://github.com/beacon-biosignals/docker-build#permissions
permissions: {}
runs-on: ubuntu-latest
steps:
- name: Build image
uses: beacon-biosignals/docker-build@v1
with:
image-repository: temporary/my-image
context: .
# Example of passing in Docker `--build-arg`
build-args: |
JULIA_VERSION=1.10
PYTHON_VERSION=3.10
# Example of passing in Docker `--secret`
build-secrets: |
github-token=${{ secrets.token || github.token }}
# Build images from scratch on "main". Ensures that caching doesn't result in using insecure system packages.
from-scratch: ${{ github.ref == 'refs/heads/main' }}
```
## Inputs
| Name | Description | Required | Example |
|:---------------------|:------------|:---------|:--------|
| `image-repository` | The Docker image repository to push the build image and cached layers. | Yes | `temporary/my-image` |
| `context` | The Docker build context directory. Defaults to `.`. | No | `./my-image` |
| `build-args` | List of [build-time variables](https://docs.docker.com/reference/cli/docker/buildx/build/#build-arg). | No | <pre><code>HTTP_PROXY=http://10.20.30.2:1234&#10;FTP_PROXY=http://40.50.60.5:4567</code></pre> |
| `build-secrets` | List of [secrets](https://docs.docker.com/engine/reference/commandline/buildx_build/#secret) to expose to the build. | No | `GIT_AUTH_TOKEN=mytoken` |
| `from-scratch` | Do not use cache when building the image. Defaults to `false`. | No | `false` |

## Outputs

| Name | Description | Example |
|:-------------------|:------------|:--------|
| `image` | Reference to the build image including the digest. | `temporary/my-image@sha256:37782d4e1c24d8f12047039a0d3512d1b6059e306a80d5b66a1d9ff60247a8cb` |
| `image-repository` | The Docker image repository where the image was pushed to. | `temporary/my-image` |
| `digest` | The built Docker image digest. | `sha256:37782d4e1c24d8f12047039a0d3512d1b6059e306a80d5b66a1d9ff60247a8cb` |
| `tags` | JSON list of tags associated with the built Docker image. | `branch-main`, `sha-152cb14` |
| `commit-sha` | The Git commit SHA used to build the image. | `152cb14643b50529b229930d6124e6bbef48668d` |

## Permissions

The follow [job permissions](https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs) are required to run this action:

```yaml
permissions:
packages: write # Only required when using the GitHub Container registry: https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry
```
Loading

0 comments on commit e2e156b

Please sign in to comment.