Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,8 @@ Dockerfile
docs
Makefile
README.md
Dockerfile.test
tests
CHANGELOG.md
news
towncrier.toml
33 changes: 33 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
version: 2
updates:

- package-ecosystem: "github-actions"
directory: "/"
schedule:
# Check for updates to GitHub Actions every week
interval: "weekly"
# No `cooldown` here: it is not supported for the github-actions
# ecosystem, so the weekly interval is the only pacing available.
groups:
# One pull request for all action bumps, so CI validates them together.
github-actions:
patterns:
- "*"
# Must already exist in the repository, otherwise it is silently ignored.
# "skip changelog" is what exempts these pull requests from the changelog
# workflow, which dependabot has no way of satisfying itself.
labels:
- "dependencies"
- "skip changelog"

- package-ecosystem: "docker"
# Keeps the pinned base image in Dockerfile current.
directory: "/"
schedule:
interval: "weekly"
cooldown:
# Supported for the docker ecosystem: ignore releases younger than this.
default-days: 7
labels:
- "dependencies"
- "skip changelog"
48 changes: 48 additions & 0 deletions .github/workflows/changelog.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: "Changelog"
on:
pull_request:
types: [assigned, opened, synchronize, reopened, labeled, unlabeled]
branches:
- main

env:
base-branch: main

permissions:
contents: read
pull-requests: read

jobs:

checks:
runs-on: ubuntu-latest
if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip changelog') }}
steps:
- uses: actions/checkout@v7
with:
# Fetch all history
fetch-depth: '0'

- name: Setup uv
uses: plone/meta/.github/actions/setup_uv@2.x
with:
python-version: "3.14"
working-directory: '.'

- name: "Fetch base branch"
run: |
# Reference: https://github.com/actions/checkout/#fetch-all-branches.
git fetch --no-tags origin ${{ env.base-branch }}

- name: "Repository: Check"
id: repository-changelog
run: |
uvx towncrier check --compare-with origin/${{ env.base-branch }} --config ./towncrier.toml --dir ./

- name: "Report check"
if: ${{ always() }}
run: |
echo '# Workflow Report' >> $GITHUB_STEP_SUMMARY
echo '| Job ID | Conclusion |' >> $GITHUB_STEP_SUMMARY
echo '| --- | --- |' >> $GITHUB_STEP_SUMMARY
echo '| repository |${{ steps.repository-changelog.conclusion }} |' >> $GITHUB_STEP_SUMMARY
35 changes: 35 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: CI

on:
push:
branches:
- 'main'
pull_request:
workflow_dispatch:

jobs:

lint:
name: Shellcheck
runs-on: ubuntu-latest
steps:

- name: Checkout
uses: actions/checkout@v7

- name: Lint shell scripts
run: make lint

test:
name: Bats
runs-on: ubuntu-latest
steps:

- name: Checkout
uses: actions/checkout@v7

# Builds the action image, then the test runner on top of it, so the
# suite runs against the same environment the action ships. This also
# means a broken Dockerfile fails here instead of at release time.
- name: Run test suite
run: make test
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
steps:

- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v7

- name: Docker meta
id: meta
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Change log

<!-- You should *NOT* be adding new change log entries to this file.
You should create a file in the news directory instead.
See the Development section of README.md for the fragment types and
naming convention.
-->

<!-- towncrier release notes start -->
14 changes: 14 additions & 0 deletions Dockerfile.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Test runner image.
#
# It is built FROM the action image on purpose: the scripts depend on what that
# image provides (bash, and GNU xargs from findutils rather than the busybox
# one), so the suite has to run in the same environment the action ships.
ARG BASE_IMAGE=ghcr.io/kitconcept/docker-stack-deploy:test
FROM ${BASE_IMAGE}

RUN apk add --no-cache bats

WORKDIR /code

ENTRYPOINT [ "bats" ]
CMD [ "tests/" ]
29 changes: 29 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ MAKEFLAGS+=--no-builtin-rules
BASE_NAME=docker-stack-deploy
IMAGE_NAME=ghcr.io/kitconcept/$(BASE_NAME)

# Images used by the test suite, never pushed.
TEST_IMAGE=$(IMAGE_NAME):test
TEST_RUNNER_IMAGE=$(IMAGE_NAME):test-runner
SHELLCHECK_IMAGE=koalaman/shellcheck:stable

# Everything shellcheck should look at.
SHELL_SOURCES=scripts/docker-entrypoint.sh scripts/stack-wait.sh tests/helpers/bin/docker

# We like colors
# From: https://coderwall.com/p/izxssa/colored-makefile-for-golang-projects
RED=`tput setaf 1`
Expand All @@ -36,6 +44,26 @@ build-image: # Build Docker Image
@echo "Building $(IMAGE_NAME)"
docker build . -t $(IMAGE_NAME)

.PHONY: lint
lint: # Lint the shell scripts with shellcheck
docker run --rm -v "$(PWD)":/code -w /code $(SHELLCHECK_IMAGE) $(SHELL_SOURCES)

.PHONY: build-test-image
build-test-image: # Build the bats test runner image
docker build . -t $(TEST_IMAGE)
docker build . -f Dockerfile.test --build-arg BASE_IMAGE=$(TEST_IMAGE) -t $(TEST_RUNNER_IMAGE)

.PHONY: test
test: build-test-image # Run the bats test suite
docker run --rm -v "$(PWD)":/code -w /code $(TEST_RUNNER_IMAGE) tests/

# Changelog
.PHONY: draft-changelog
draft-changelog: # Display the draft of the changelog
@uvx towncrier build --draft --version unreleased --config towncrier.toml

# Release
.PHONY: create-tag
create-tag: # Create a new tag using git
@test -n "$(VERSION)"
if git show-ref --tags v$(VERSION) --quiet; \
Expand All @@ -44,6 +72,7 @@ create-tag: # Create a new tag using git
else \
echo "Creating new tag $(VERSION)"; \
sed -i 's/$(BASE_NAME):latest/$(BASE_NAME):$(VERSION)/' action.yml; \
uvx towncrier build --yes --version $(VERSION) --config towncrier.toml; \
git commit -am "Prepare release $(VERSION)"; \
git tag -a v$(VERSION) -m "Release $(VERSION)"; \
git push && git push --tags; \
Expand Down
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,51 @@ Please **DO NOT** commit to version branches directly. Even for the smallest and

**ALWAYS** open a pull request and ask somebody else to merge your code. **NEVER** merge it yourself.

### Development

Both commands need Docker, and nothing else — there is no local toolchain to install.

```shell
make lint # shellcheck the shell scripts
make test # run the bats suite
```

`make test` builds the action image and then builds the test runner on top of
it, so the suite runs in the same environment the action ships. That matters:
the scripts rely on GNU `xargs` from `findutils`, and against the busybox
`xargs` in a plain Alpine image the tests fail for reasons that have nothing to
do with the code.

Tests live in `tests/` and use a fake `docker` CLI (`tests/helpers/bin/docker`)
that replays a scenario from `tests/fixtures/*.services`, so no Swarm is
needed. Each fixture describes one poll of the wait loop per section.

Tests that document a currently open bug call `skip` with a link to the issue.
They are written to assert the *desired* behaviour, so fixing the bug means
deleting the `skip` line rather than writing a new test.

### Change log

`CHANGELOG.md` is generated by [towncrier](https://towncrier.readthedocs.io/) —
do not edit it directly. Add a file to `news/` instead, named for the issue it
addresses, and CI will check that every pull request has one.

```
news/21.bugfix # an issue number, when there is an issue
news/+test-harness.internal # a short slug, when there is not
```

Types are `breaking`, `feature`, `bugfix`, `internal` and `documentation`. Write
one sentence in the past tense, describing the change from the user's side, and
sign it with your GitHub handle:

```
Fixed `env_file` values containing spaces being truncated. @ericof
```

Preview the result with `uvx towncrier build --draft --version <next>`. A pull
request that genuinely needs no entry can carry the `skip changelog` label.


## Credits

Expand Down
1 change: 1 addition & 0 deletions news/+test-harness.internal
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added a bats test suite for the shell scripts, run in CI together with shellcheck. @ericof
1 change: 1 addition & 0 deletions news/+towncrier.internal
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added towncrier to manage the change log, with a CI check that every pull request carries a news fragment. @ericof
15 changes: 15 additions & 0 deletions news/.changelog_template.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{% if sections[""] %}
{% for category, val in definitions.items() if category in sections[""] %}

### {{ definitions[category]['name'] }}

{% for text, values in sections[""][category].items() %}
- {{ text }} {{ values|join(', ') }}
{% endfor %}

{% endfor %}
{% else %}
No significant changes.


{% endif %}
1 change: 1 addition & 0 deletions news/13.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed the deploy waiting until it timed out on a `mode: replicated-job` service that had already run to completion. @ericof
1 change: 1 addition & 0 deletions news/19.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed the deploy waiting until it timed out on services the stack file scales to zero, and added the `docker stack ps` task list to the output when a deploy fails. @ericof
1 change: 1 addition & 0 deletions news/21.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed `env_file` values containing spaces being truncated to their first word. Each line is now read whole and the value taken verbatim, matching `docker --env-file`. @ericof
35 changes: 31 additions & 4 deletions scripts/docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,31 @@ configure_ssh_key() {

configure_env_file() {
printf '%s' "$ENV_FILE" > "${ENV_FILE_PATH}"
env_file_len=$(grep -v '^#' ${ENV_FILE_PATH}|grep -v '^$' -c)
if [[ $env_file_len -gt 0 ]]; then
env_file_len=$(grep -cv -e '^#' -e '^$' "${ENV_FILE_PATH}" || true)
if [[ ${env_file_len} -gt 0 ]]; then
echo "Environment Variables: Additional values"
if [ "${DEBUG}" != "0" ]; then
echo "Environment vars before: $(env|wc -l)"
fi
# shellcheck disable=SC2046
export $(grep -v '^#' ${ENV_FILE_PATH} | grep -v '^$' | xargs -d '\n')
# Read one line at a time and hand export a single quoted argument, so a
# value containing spaces survives. Passing the whole file through an
# unquoted $(...) word-split it before export ever ran.
#
# Values are taken verbatim, matching `docker --env-file`: quotes in the
# file are part of the value, not delimiters around it.
#
# ENV_FILE has no trailing newline, so the `-n` test is what keeps the
# final line from being dropped by read's non-zero exit.
while IFS= read -r line || [ -n "${line}" ]; do
case "${line}" in
''|\#*) continue ;;
esac
if [[ ! "${line}" =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; then
echo "Environment Variables: '${line}' is not in NAME=VALUE format"
exit 1
fi
export "${line?}"
done < "${ENV_FILE_PATH}"
if [ "${DEBUG}" != "0" ]; then
echo "Environment vars after: $(env|wc -l)"
fi
Expand Down Expand Up @@ -76,10 +93,20 @@ check_deploy() {
scale_after() {
if [[ -n "$SCALE_AFTER" ]]; then
echo "Scaling services: $SCALE_AFTER"
# Unquoted on purpose: SCALE_AFTER may hold several "service=n" pairs,
# and `docker service scale` expects them as separate arguments.
# shellcheck disable=SC2086
docker service scale $SCALE_AFTER
fi
}

# Everything above is a function definition; everything below is the deploy
# flow. Sourcing this script (as the test suite does) stops here, so the
# functions can be exercised individually without running a deploy.
if [ "${BASH_SOURCE[0]}" != "${0}" ]; then
return 0
fi

[ -z ${DEBUG+x} ] && export DEBUG="0"

# ADDITIONAL ENV VARIABLES
Expand Down
Loading
Loading