Skip to content

Commit e2e156b

Browse files
authored
Initial implementation (#1)
1 parent 152cb14 commit e2e156b

11 files changed

+630
-1
lines changed

.editorconfig

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# https://editorconfig.org/
2+
3+
# https://manpages.debian.org/testing/shfmt/shfmt.1.en.html#EXAMPLES
4+
[*.sh]
5+
indent_style = space
6+
indent_size = 4
7+
shell_variant = bash # --language-variant
8+
binary_next_line = false
9+
switch_case_indent = true # --case-indent
10+
space_redirects = false
11+
keep_padding = false
12+
function_next_line = false # --func-next-line

.github/workflows/gha.yaml

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
name: GitHub Actions
3+
on:
4+
pull_request:
5+
paths:
6+
- ".github/workflows/*"
7+
8+
jobs:
9+
lint:
10+
name: Lint
11+
# These permissions are needed to:
12+
# - Checkout the Git repo (`contents: read`)
13+
permissions:
14+
contents: read
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v4
18+
# https://github.com/rhysd/actionlint/blob/v1.7.6/docs/usage.md#use-actionlint-on-github-actions
19+
# https://github.com/rhysd/actionlint/blob/v1.7.6/docs/usage.md#reviewdog
20+
# https://github.com/reviewdog/reviewdog#filter-mode
21+
# No support for non-workflows yet: https://github.com/rhysd/actionlint/issues/46
22+
- uses: reviewdog/action-actionlint@a1b7ce56be870acfe94b83ce5f6da076aecc6d8c # v1.62.0
23+
with:
24+
fail_level: error
25+
filter_mode: nofilter # Post results on all results and not just changed files
+196
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
---
2+
name: Integration Tests
3+
on:
4+
pull_request:
5+
paths:
6+
- "action.yaml"
7+
- ".github/workflows/integration-tests.yaml"
8+
push:
9+
paths:
10+
- "action.yaml"
11+
- ".github/workflows/integration-tests.yaml"
12+
13+
concurrency:
14+
group: integration-tests-${{ github.event_name == 'pull_request' && 'pr' || 'push' }}-${{ github.event.pull_request.head.sha || github.sha }}
15+
cancel-in-progress: true
16+
17+
jobs:
18+
filter-matrix:
19+
name: Filter Matrix
20+
runs-on: ubuntu-latest
21+
outputs:
22+
test-json: ${{ steps.filter.outputs.test-json }}
23+
cleanup-json: ${{ steps.filter.outputs.cleanup-json }}
24+
steps:
25+
- name: Filter Matrix
26+
id: filter
27+
shell: bash
28+
run: |
29+
# Remove any entries with keys containing `null` values.
30+
test_yaml="$(yq 'map(select(to_entries | map(.value != null) | all))' <<<"${matrix:?}")"
31+
32+
# Validate we do not accidentally test against the same package and commit SHA.
33+
yq -o=json <<<"$test_yaml" | jq -e '(map({package, "commit-sha"}) | unique | length) == length' || exit 1
34+
35+
# Automatically cleanup the `cache-sha-*` tags for the specific test commits.
36+
cleanup_yaml="$(yq 'group_by(.package) | map({"package": .[0].package, "tags" : map(.commit-sha) | unique | map("cache-sha-" + .) | join(",")})' <<<"$test_yaml")"
37+
38+
# Output our multiline YAML document using GH action flavored heredoc
39+
# https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings
40+
{
41+
echo "test-json<<EOF"
42+
yq -o json <<<"$test_yaml"
43+
echo "EOF"
44+
45+
echo "cleanup-json<<EOF"
46+
yq -o json <<<"$cleanup_yaml"
47+
echo "EOF"
48+
} | tee -a "$GITHUB_OUTPUT"
49+
env:
50+
# We need to avoid running concurrent tests using the same commit SHA and
51+
# writing to the same image-repository. If we do not then we could see false
52+
# positive test results if one of them doesn't actually push cache layers. We
53+
# address this problem by:
54+
#
55+
# 1. Ensuring tests which run in parallel either use separate image repositories
56+
# or different Git commit SHAs.
57+
# 2. Utilizing concurrency groups to avoid having multiple instances of this
58+
# workflow run in parallel when triggered on the same commit SHA.
59+
# 3. Deleting the `cache-sha-*` tags to ensure our running workflow produced
60+
# those images. Ideally, we'd delete these before the tests run but attempting
61+
# to delete images from non-existing packages causes failures so this works
62+
# well enough.
63+
#
64+
# I also considered revising the action to avoid pushing images entirely.
65+
# Doing this may be challenging in otherways as pushing the image is a
66+
# requirement for getting the digests in some contexts:
67+
# https://github.com/docker/build-push-action/issues/906#issuecomment-1674567311
68+
matrix: |
69+
- title: ${{ github.event_name == 'pull_request' && 'Merge Commit' || '' }}
70+
package : temporary/whalesay-pr
71+
commit-sha: ${{ github.sha }}
72+
from-scratch: false
73+
- title: Head Commit
74+
package: temporary/whalesay-${{ github.event_name == 'pull_request' && 'pr' || 'push' }}
75+
commit-sha: ${{ github.event.pull_request.head.sha || github.sha }}
76+
from-scratch: false
77+
- title: ${{ github.event_name == 'pull_request' && 'Merge Commit From Scratch' || '' }}
78+
package: temporary/whalesay-pr-from-scratch
79+
commit-sha: ${{ github.sha }}
80+
from-scratch: true
81+
- title: Head Commit From Scratch
82+
package: temporary/whalesay-${{ github.event_name == 'pull_request' && 'pr' || 'push' }}-from-scratch
83+
commit-sha: ${{ github.event.pull_request.head.sha || github.sha }}
84+
from-scratch: true
85+
86+
test:
87+
name: Test ${{ matrix.test.title }}
88+
needs: filter-matrix
89+
# These permissions are needed to:
90+
# - Checkout the repo
91+
permissions:
92+
contents: read
93+
packages: write
94+
runs-on: ubuntu-latest
95+
strategy:
96+
fail-fast: false
97+
matrix:
98+
test: ${{ fromJSON(needs.filter-matrix.outputs.test-json) }}
99+
steps:
100+
- name: Job started at
101+
id: job-started
102+
run: |
103+
job_started_at="$(date --utc --iso-8601=seconds)"
104+
echo "at=$job_started_at" | tee -a "$GITHUB_OUTPUT"
105+
- uses: actions/checkout@v4
106+
with:
107+
ref: ${{ matrix.test.commit-sha }}
108+
- name: Log in to the Container registry
109+
uses: docker/login-action@v3
110+
with:
111+
registry: ghcr.io
112+
username: ${{ github.actor }}
113+
password: ${{ github.token }}
114+
- uses: ./
115+
id: build
116+
with:
117+
image-repository: ghcr.io/beacon-biosignals/${{ matrix.test.package }}
118+
context: test
119+
build-args: |
120+
DEBIAN_VERSION=12.9
121+
from-scratch: ${{ matrix.test.from-scratch || 'false' }}
122+
- name: Validate image works
123+
run: |
124+
docker pull "${{ steps.build.outputs.image }}"
125+
output="$(docker run "${{ steps.build.outputs.image }}")"
126+
if [[ "$(wc -l <<<"$output")" -lt 14 ]]; then
127+
echo "$output"
128+
exit 1
129+
fi
130+
debian_version="$(docker run --entrypoint=/bin/cat "${{ steps.build.outputs.image }}" /etc/debian_version)"
131+
[[ "$debian_version" == "12.9" ]] || exit 2
132+
- name: Layer created at
133+
id: layer-created
134+
run: |
135+
layer_created_at="$(docker run --entrypoint=/bin/cat "${{ steps.build.outputs.image }}" /etc/layer-created-at)"
136+
echo "at=$layer_created_at" | tee -a "$GITHUB_OUTPUT"
137+
# Test will fail if this is the first time the image was build in the image-repository
138+
- name: Validate layer caching
139+
if: ${{ matrix.test.from-scratch == false }}
140+
run: |
141+
[[ "$(date -d "$layer_created_at" +%s)" -lt "$(date -d "$job_started_at" +%s)" ]] || exit 1
142+
env:
143+
job_started_at: ${{ steps.job-started.outputs.at }}
144+
layer_created_at: ${{ steps.layer-created.outputs.at }}
145+
- name: Validate no layer caching
146+
if: ${{ matrix.test.from-scratch == true }}
147+
run: |
148+
[[ "$(date -d "$layer_created_at" +%s)" -gt "$(date -d "$job_started_at" +%s)" ]] || exit 1
149+
env:
150+
job_started_at: ${{ steps.job-started.outputs.at }}
151+
layer_created_at: ${{ steps.layer-created.outputs.at }}
152+
- name: Validate cache images
153+
run: |
154+
docker manifest inspect "${{ steps.build.outputs.image-repository }}:cache-sha-${{ matrix.test.commit-sha }}"
155+
156+
# Should only be skipped when workflow is triggered by a tag push
157+
if [[ -n "$branch" ]]; then
158+
docker manifest inspect "${{ steps.build.outputs.image-repository }}:cache-branch-${branch//[^[:alnum:]]/-}"
159+
fi
160+
env:
161+
branch: ${{ github.head_ref || (github.ref_type == 'branch' && github.ref_name) }}
162+
- name: Validate annotations
163+
run: |
164+
set -x
165+
json="$(docker manifest inspect "${{ steps.build.outputs.image }}")"
166+
[[ "$(jq -r '.annotations."org.opencontainers.image.revision"' <<<"$json")" == "${{ matrix.test.commit-sha }}" ]] || exit 1
167+
- name: Validate docker/metadata-output environment variables are overwritten
168+
shell: bash
169+
run: |
170+
if [[ "$(printenv | grep '^DOCKER_METADATA_OUTPUT_' | grep -c '[^=]$')" -ne 0 ]]; then
171+
printenv | grep '^DOCKER_METADATA_OUTPUT_'
172+
exit 1
173+
fi
174+
175+
cleanup:
176+
name: Cleanup (${{ matrix.cleanup.package }})
177+
needs:
178+
- filter-matrix
179+
- test
180+
if: ${{ !cancelled() }}
181+
runs-on: ubuntu-latest
182+
strategy:
183+
fail-fast: false
184+
matrix:
185+
cleanup: ${{ fromJSON(needs.filter-matrix.outputs.cleanup-json || '[]') }}
186+
steps:
187+
- uses: dataaxiom/ghcr-cleanup-action@cd0cdb900b5dbf3a6f2cc869f0dbb0b8211f50c4 # v1.0.16
188+
with:
189+
package: ${{ matrix.cleanup.package }}
190+
delete-tags: ${{ matrix.cleanup.tags }}
191+
- uses: dataaxiom/ghcr-cleanup-action@cd0cdb900b5dbf3a6f2cc869f0dbb0b8211f50c4 # v1.0.16
192+
with:
193+
package: ${{ matrix.cleanup.package }}
194+
older-than: 1 day
195+
keep-n-tagged: 0
196+
exclude-tags: branch-main,cache-branch-main

.github/workflows/shell.yaml

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
name: Shell
3+
on:
4+
pull_request:
5+
paths:
6+
- "**.sh"
7+
- ".github/workflows/*"
8+
9+
jobs:
10+
lint-format:
11+
name: Lint & Format
12+
# These permissions are needed to:
13+
# - Checkout the Git repo (`contents: read`)
14+
# - Post a comments on PRs: https://github.com/luizm/action-sh-checker#secrets
15+
permissions:
16+
contents: read
17+
pull-requests: write
18+
runs-on: ubuntu-latest
19+
steps:
20+
- uses: actions/checkout@v4
21+
- name: Extract workflow shell scripts
22+
id: extract
23+
uses: beacon-biosignals/gha-extract-shell-scripts@v1
24+
- uses: luizm/action-sh-checker@c6edb3de93e904488b413636d96c6a56e3ad671a # v0.8.0
25+
env:
26+
GITHUB_TOKEN: ${{ github.token }}
27+
with:
28+
sh_checker_comment: true
29+
# Support investigating linting/formatting errors
30+
- uses: actions/upload-artifact@v4
31+
if: ${{ failure() }}
32+
with:
33+
name: workflow-scripts
34+
path: ${{ steps.extract.outputs.output-dir }}

.github/workflows/yaml.yaml

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
# https://yamllint.readthedocs.io/en/stable/integration.html#integration-with-github-actions
3+
name: YAML
4+
on:
5+
pull_request:
6+
paths:
7+
- "**/*.yaml"
8+
- "**/*.yml"
9+
jobs:
10+
lint:
11+
name: Lint
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v4
15+
- name: Install yamllint
16+
run: pip install yamllint
17+
- name: Lint YAML files
18+
run: yamllint . --format=github

.yamllint.yaml

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
rules:
3+
indentation:
4+
spaces: 2
5+
indent-sequences: true
6+
document-start:
7+
present: true
8+
new-line-at-end-of-file: enable

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 Beacon Biosignals
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

+69-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,69 @@
1-
# docker-build
1+
# Docker Build
2+
3+
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.
4+
5+
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.
6+
7+
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.
8+
9+
## Example
10+
11+
```yaml
12+
---
13+
on:
14+
pull_request: {}
15+
# Trigger this build workflow on "main". See `from-scratch`
16+
push:
17+
branches:
18+
- main
19+
jobs:
20+
example:
21+
# These permissions are needed to:
22+
# - Get the workflow run: https://github.com/beacon-biosignals/docker-build#permissions
23+
permissions: {}
24+
runs-on: ubuntu-latest
25+
steps:
26+
- name: Build image
27+
uses: beacon-biosignals/docker-build@v1
28+
with:
29+
image-repository: temporary/my-image
30+
context: .
31+
# Example of passing in Docker `--build-arg`
32+
build-args: |
33+
JULIA_VERSION=1.10
34+
PYTHON_VERSION=3.10
35+
# Example of passing in Docker `--secret`
36+
build-secrets: |
37+
github-token=${{ secrets.token || github.token }}
38+
# Build images from scratch on "main". Ensures that caching doesn't result in using insecure system packages.
39+
from-scratch: ${{ github.ref == 'refs/heads/main' }}
40+
```
41+
42+
## Inputs
43+
44+
| Name | Description | Required | Example |
45+
|:---------------------|:------------|:---------|:--------|
46+
| `image-repository` | The Docker image repository to push the build image and cached layers. | Yes | `temporary/my-image` |
47+
| `context` | The Docker build context directory. Defaults to `.`. | No | `./my-image` |
48+
| `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> |
49+
| `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` |
50+
| `from-scratch` | Do not use cache when building the image. Defaults to `false`. | No | `false` |
51+
52+
## Outputs
53+
54+
| Name | Description | Example |
55+
|:-------------------|:------------|:--------|
56+
| `image` | Reference to the build image including the digest. | `temporary/my-image@sha256:37782d4e1c24d8f12047039a0d3512d1b6059e306a80d5b66a1d9ff60247a8cb` |
57+
| `image-repository` | The Docker image repository where the image was pushed to. | `temporary/my-image` |
58+
| `digest` | The built Docker image digest. | `sha256:37782d4e1c24d8f12047039a0d3512d1b6059e306a80d5b66a1d9ff60247a8cb` |
59+
| `tags` | JSON list of tags associated with the built Docker image. | `branch-main`, `sha-152cb14` |
60+
| `commit-sha` | The Git commit SHA used to build the image. | `152cb14643b50529b229930d6124e6bbef48668d` |
61+
62+
## Permissions
63+
64+
The follow [job permissions](https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs) are required to run this action:
65+
66+
```yaml
67+
permissions:
68+
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
69+
```

0 commit comments

Comments
 (0)