Skip to content

Commit 790bd44

Browse files
authored
Merge pull request #26 from kitconcept/issues-2-3-env-file
Add an env_file_path input, and document how values reach the stack file
2 parents cc1c3e1 + b3df532 commit 790bd44

6 files changed

Lines changed: 248 additions & 19 deletions

File tree

‎README.md‎

Lines changed: 86 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ GitHub Action and Docker image used to deploy a Docker stack on a Docker Swarm.
3737
| `prune` | `PRUNE` | Prune services that are not defined in the stack file | | **0** |
3838
| `stack_file` | `STACK_FILE` | Path to the stack file used in the deploy. | ✅ | |
3939
| `stack_name` | `STACK_NAME` | Name of the stack to be deployed. | ✅ | |
40-
| `stack_param` | `STACK_PARAM` | Additional parameter (env var) to be passed to the stack. | | |
41-
| `env_file` | `ENV_FILE` | Additional environment variables to be passed to the stack. | | |
40+
| `stack_param` | `STACK_PARAM` | A single additional value, available to the stack file as `${STACK_PARAM}`. Superseded by `env_file`, see [Passing values into the stack file](#passing-values-into-the-stack-file). | | |
41+
| `env_file` | `ENV_FILE` | Additional environment variables **as content**, one `VAR=VALUE` per line. Mutually exclusive with `env_file_path`. | | |
42+
| `env_file_path` | `ENV_FILE_PATH` | **Path** to a file of additional environment variables, one `VAR=VALUE` per line. Mutually exclusive with `env_file`. | | |
4243
| `debug` | `DEBUG` | Verbose logging | | **0** |
4344
| `scale_after` | `SCALE_AFTER` | Scale a service after a deployment has converged successfully. Example: servicename=1 | | |
4445

@@ -60,6 +61,85 @@ repository UI, and used to surface much later as an opaque
6061
structural in a PEM key, and whitespace can be a legitimate part of a token.
6162

6263

64+
## Passing values into the stack file
65+
66+
Your stack file can reference environment variables, and `docker stack deploy`
67+
substitutes them as it reads the file. This action gives you three ways to set
68+
those variables, all of which work through that same substitution.
69+
70+
### `env_file` — the variables themselves
71+
72+
Pass the content directly. Each line is a `VAR=VALUE` pair:
73+
74+
```yaml
75+
- name: Deploy
76+
uses: kitconcept/docker-stack-deploy@v1.4.0
77+
with:
78+
# ...
79+
env_file: |
80+
BACKEND_REPLICAS=2
81+
FRONTEND_REPLICAS=3
82+
SOLR_JAVA_MEM=-Xms1536m -Xmx1536m
83+
```
84+
85+
```yaml
86+
# stacks/plone.yml
87+
services:
88+
backend:
89+
deploy:
90+
replicas: ${BACKEND_REPLICAS}
91+
frontend:
92+
deploy:
93+
replicas: ${FRONTEND_REPLICAS}
94+
```
95+
96+
Values are taken **verbatim**, matching `docker --env-file`: a value may
97+
contain spaces, and quotes around it become part of the value rather than
98+
being stripped. So `GREETING="hello"` sets `GREETING` to `"hello"`, quotes
99+
included.
100+
101+
Lines that are blank or start with `#` are ignored. Anything else that is not
102+
`NAME=VALUE` is an error naming the offending line, rather than a silent
103+
failure to export.
104+
105+
### `env_file_path` — a file to read them from
106+
107+
If the variables already live in a file in your repository, point at it
108+
instead. Relative paths resolve against the workspace, the same as `stack_file`:
109+
110+
```yaml
111+
with:
112+
# ...
113+
env_file_path: "stacks/production.env"
114+
```
115+
116+
The file format and the verbatim semantics are identical to `env_file`. The two
117+
inputs are mutually exclusive — supplying both is an error, because the
118+
precedence between them would otherwise be arbitrary.
119+
120+
### `stack_param` — a single value (discouraged)
121+
122+
`stack_param` sets one variable, always named `STACK_PARAM`:
123+
124+
```yaml
125+
with:
126+
# ...
127+
stack_param: "2"
128+
```
129+
130+
```yaml
131+
# stacks/plone.yml
132+
services:
133+
backend:
134+
deploy:
135+
replicas: ${STACK_PARAM}
136+
```
137+
138+
It works, and it is kept for compatibility, but it is a one-variable special
139+
case of `env_file` with a name you cannot choose. Prefer `env_file` or
140+
`env_file_path` for anything new.
141+
142+
63143
## Using the GitHub Action
64144

65145
Add, or edit an existing, `yaml` file inside `.github/actions` and use the configuration options listed above.
@@ -83,10 +163,10 @@ jobs:
83163
runs-on: ubuntu-latest
84164
steps:
85165
- name: Checkout codebase
86-
uses: actions/checkout@v2
166+
uses: actions/checkout@v7
87167
88168
- name: Deploy
89-
uses: kitconcept/docker-stack-deploy@v1.0.1
169+
uses: kitconcept/docker-stack-deploy@v1.4.0
90170
with:
91171
remote_host: ${{ secrets.REMOTE_HOST }}
92172
remote_user: ${{ secrets.REMOTE_USER }}
@@ -113,10 +193,10 @@ jobs:
113193
runs-on: ubuntu-latest
114194
steps:
115195
- name: Checkout codebase
116-
uses: actions/checkout@v2
196+
uses: actions/checkout@v7
117197
118198
- name: Deploy
119-
uses: kitconcept/docker-stack-deploy@v1.0.1
199+
uses: kitconcept/docker-stack-deploy@v1.4.0
120200
with:
121201
registry: "ghcr.io"
122202
username: ${{ secrets.GHCR_USERNAME }}
@@ -126,7 +206,6 @@ jobs:
126206
remote_private_key: ${{ secrets.REMOTE_PRIVATE_KEY }}
127207
stack_file: "stacks/plone.yml"
128208
stack_name: "plone-live"
129-
stack_param: "foo"
130209
```
131210

132211
## Using the Docker Image

‎action.yml‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,11 @@ inputs:
5252
required: false
5353
default: ""
5454
env_file:
55-
description: "Additional environment variables in the format VAR_01=VALUE\nVAR_02=VALUE"
55+
description: "Additional environment variables, as content, in the format VAR_01=VALUE\nVAR_02=VALUE. Mutually exclusive with env_file_path."
56+
required: false
57+
default: ""
58+
env_file_path:
59+
description: "Path to a file containing additional environment variables, one VAR=VALUE per line. Mutually exclusive with env_file."
5660
required: false
5761
default: ""
5862
debug:
@@ -80,5 +84,6 @@ runs:
8084
STACK_NAME: ${{ inputs.stack_name }}
8185
STACK_PARAM: ${{ inputs.stack_param }}
8286
ENV_FILE: ${{ inputs.env_file }}
87+
ENV_FILE_PATH: ${{ inputs.env_file_path }}
8388
DEBUG: ${{ inputs.debug }}
8489
SCALE_AFTER: ${{ inputs.scale_after }}

‎news/2.documentation‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Documented how values reach the stack file, with worked examples for `env_file`, `env_file_path` and `stack_param`, and noted that `stack_param` is a one-variable special case of `env_file`. @ericof

‎news/3.feature‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added an `env_file_path` input, which reads the additional environment variables from a file instead of taking them as content. `env_file` keeps its current meaning, so nothing breaks; supplying both is an error. @ericof

‎scripts/docker-entrypoint.sh‎

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ set -e
55
SSH_DIR="/root/.ssh"
66
SSH_KEY="${SSH_DIR}/docker"
77
KNOWN_HOSTS="${SSH_DIR}/known_hosts"
8-
ENV_FILE_PATH="/root/.env"
8+
# Where the `env_file` input's content is materialised before being parsed.
9+
# Distinct from the ENV_FILE_PATH input, which is a path supplied by the user.
10+
ENV_FILE_DEST="/root/.env"
911

1012
OPTS=("--with-registry-auth" "--resolve-image=${RESOLVE_IMAGE:-always}")
1113
[ "${PRUNE:-0}" = "1" ] && OPTS+=("--prune")
@@ -55,9 +57,10 @@ configure_ssh_key() {
5557
ssh-add "${SSH_KEY}"
5658
}
5759

58-
configure_env_file() {
59-
printf '%s' "$ENV_FILE" > "${ENV_FILE_PATH}"
60-
env_file_len=$(grep -cv -e '^#' -e '^$' "${ENV_FILE_PATH}" || true)
60+
# Parse a NAME=VALUE file and export every entry. $1 is the file to read.
61+
load_env_file() {
62+
local source_file="$1" env_file_len line
63+
env_file_len=$(grep -cv -e '^#' -e '^$' "${source_file}" || true)
6164
if [[ ${env_file_len} -gt 0 ]]; then
6265
echo "Environment Variables: Additional values"
6366
if [ "${DEBUG}" != "0" ]; then
@@ -70,8 +73,9 @@ configure_env_file() {
7073
# Values are taken verbatim, matching `docker --env-file`: quotes in the
7174
# file are part of the value, not delimiters around it.
7275
#
73-
# ENV_FILE has no trailing newline, so the `-n` test is what keeps the
74-
# final line from being dropped by read's non-zero exit.
76+
# The ENV_FILE input has no trailing newline, so the `-n` test is what
77+
# keeps the final line from being dropped by read's non-zero exit. A file
78+
# read through env_file_path usually does end in a newline; both work.
7579
while IFS= read -r line || [ -n "${line}" ]; do
7680
case "${line}" in
7781
''|\#*) continue ;;
@@ -81,13 +85,32 @@ configure_env_file() {
8185
exit 1
8286
fi
8387
export "${line?}"
84-
done < "${ENV_FILE_PATH}"
88+
done < "${source_file}"
8589
if [ "${DEBUG}" != "0" ]; then
8690
echo "Environment vars after: $(env|wc -l)"
8791
fi
8892
fi
8993
}
9094

95+
# The env_file input carries the variables themselves, so materialise them
96+
# before parsing.
97+
configure_env_file() {
98+
printf '%s' "$ENV_FILE" > "${ENV_FILE_DEST}"
99+
load_env_file "${ENV_FILE_DEST}"
100+
}
101+
102+
# The env_file_path input names a file to read instead. Relative paths resolve
103+
# against the workspace, which is the working directory of a docker action --
104+
# the same way the stack_file input works.
105+
configure_env_file_path() {
106+
if [ ! -f "${ENV_FILE_PATH}" ]; then
107+
echo "${ENV_FILE_PATH} does not exist."
108+
exit 1
109+
fi
110+
echo "Environment Variables: Reading ${ENV_FILE_PATH}"
111+
load_env_file "${ENV_FILE_PATH}"
112+
}
113+
91114
configure_ssh_host() {
92115
ssh-keyscan -p "${REMOTE_PORT}" "${REMOTE_HOST}" > "${KNOWN_HOSTS}"
93116
chmod 600 "${KNOWN_HOSTS}"
@@ -133,7 +156,16 @@ fi
133156
[ -z ${DEBUG+x} ] && export DEBUG="0"
134157

135158
# ADDITIONAL ENV VARIABLES
136-
if [[ -z "${ENV_FILE}" ]]; then
159+
# env_file carries the variables themselves; env_file_path names a file to read
160+
# them from. Accepting both at once would leave the precedence undefined, so it
161+
# is rejected rather than guessed at.
162+
if [[ -n "${ENV_FILE}" ]] && [[ -n "${ENV_FILE_PATH}" ]]; then
163+
echo "Inputs env_file and env_file_path are mutually exclusive!"
164+
exit 1
165+
fi
166+
if [[ -n "${ENV_FILE_PATH}" ]]; then
167+
configure_env_file_path;
168+
elif [[ -z "${ENV_FILE}" ]]; then
137169
export ENV_FILE=""
138170
else
139171
configure_env_file;

‎tests/entrypoint.bats‎

Lines changed: 114 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ export_from_env_file() {
230230
ENV_FILE="${body}" WANT="${var}" \
231231
bash -c "
232232
source '${ENTRYPOINT}'
233-
ENV_FILE_PATH=\"\${HOME}/.env\"
233+
ENV_FILE_DEST=\"\${HOME}/.env\"
234234
configure_env_file >/dev/null 2>&1
235235
printf '%s' \"\${!WANT}\"
236236
"
@@ -296,7 +296,7 @@ export_from_env_file() {
296296
run env -i PATH="${PATH}" HOME="${BATS_TEST_TMPDIR}" ENV_FILE=".env" \
297297
bash -c "
298298
source '${ENTRYPOINT}'
299-
ENV_FILE_PATH=\"\${HOME}/.env\"
299+
ENV_FILE_DEST=\"\${HOME}/.env\"
300300
configure_env_file
301301
"
302302
[ "$status" -eq 1 ]
@@ -307,13 +307,124 @@ export_from_env_file() {
307307
run env -i PATH="${PATH}" HOME="${BATS_TEST_TMPDIR}" ENV_FILE="MY-VAR=1" \
308308
bash -c "
309309
source '${ENTRYPOINT}'
310-
ENV_FILE_PATH=\"\${HOME}/.env\"
310+
ENV_FILE_DEST=\"\${HOME}/.env\"
311311
configure_env_file
312312
"
313313
[ "$status" -eq 1 ]
314314
[[ "$output" == *"is not in NAME=VALUE format"* ]]
315315
}
316316

317+
# --- env_file_path (issue #3) -------------------------------------------------
318+
319+
# Write a file with the given content, run configure_env_file_path against it,
320+
# and print the resulting value of one variable.
321+
#
322+
# The value is bracketed because bats strips trailing newlines from $output,
323+
# which would hide a value that wrongly kept one.
324+
export_from_env_file_path() {
325+
local body="$1" var="$2"
326+
local file="${BATS_TEST_TMPDIR}/vars.env"
327+
printf '%s\n' "${body}" > "${file}"
328+
run env -i PATH="${PATH}" HOME="${BATS_TEST_TMPDIR}" \
329+
ENV_FILE_PATH="${file}" WANT="${var}" \
330+
bash -c "
331+
source '${ENTRYPOINT}'
332+
configure_env_file_path >/dev/null 2>&1
333+
printf '[%s]' \"\${!WANT}\"
334+
"
335+
}
336+
337+
@test "env_file_path exports a value read from a file (issue #3)" {
338+
export_from_env_file_path 'DB_USER=plone' DB_USER
339+
[ "$status" -eq 0 ]
340+
[ "$output" = "[plone]" ]
341+
}
342+
343+
@test "env_file_path exports every entry in the file (issue #3)" {
344+
export_from_env_file_path $'DB_USER=plone\nDB_NAME=site' DB_NAME
345+
[ "$status" -eq 0 ]
346+
[ "$output" = "[site]" ]
347+
}
348+
349+
@test "env_file_path ignores comments and blank lines (issue #3)" {
350+
export_from_env_file_path $'# a comment\n\nDB_USER=plone' DB_USER
351+
[ "$status" -eq 0 ]
352+
[ "$output" = "[plone]" ]
353+
}
354+
355+
@test "env_file_path takes values verbatim, like env_file (issue #3)" {
356+
# The semantics must not diverge between the two inputs: quotes are part of
357+
# the value, matching `docker --env-file`.
358+
export_from_env_file_path 'GREETING="hello world"' GREETING
359+
[ "$status" -eq 0 ]
360+
[ "$output" = '["hello world"]' ]
361+
}
362+
363+
@test "env_file_path keeps a value containing spaces (issue #3)" {
364+
export_from_env_file_path 'SOLR_JAVA_MEM=-Xms1536m -Xmx1536m' SOLR_JAVA_MEM
365+
[ "$status" -eq 0 ]
366+
[ "$output" = "[-Xms1536m -Xmx1536m]" ]
367+
}
368+
369+
@test "env_file_path does not drop the last line of the file (issue #3)" {
370+
# A real file ends in a newline, unlike the env_file input; the read loop
371+
# has to handle both without losing or duplicating the final entry.
372+
export_from_env_file_path $'DB_USER=plone\nDB_NAME=site\nLAST=here' LAST
373+
[ "$status" -eq 0 ]
374+
[ "$output" = "[here]" ]
375+
}
376+
377+
@test "env_file_path rejects a malformed line (issue #3)" {
378+
printf '%s\n' 'not-a-pair' > "${BATS_TEST_TMPDIR}/bad.env"
379+
run env -i PATH="${PATH}" HOME="${BATS_TEST_TMPDIR}" \
380+
ENV_FILE_PATH="${BATS_TEST_TMPDIR}/bad.env" \
381+
bash -c "source '${ENTRYPOINT}'; configure_env_file_path"
382+
[ "$status" -eq 1 ]
383+
[[ "$output" == *"'not-a-pair' is not in NAME=VALUE format"* ]]
384+
}
385+
386+
@test "env_file_path says which file it read (issue #3)" {
387+
printf '%s\n' 'DB_USER=plone' > "${BATS_TEST_TMPDIR}/vars.env"
388+
run env -i PATH="${PATH}" HOME="${BATS_TEST_TMPDIR}" \
389+
ENV_FILE_PATH="${BATS_TEST_TMPDIR}/vars.env" \
390+
bash -c "source '${ENTRYPOINT}'; configure_env_file_path"
391+
[ "$status" -eq 0 ]
392+
[[ "$output" == *"Environment Variables: Reading ${BATS_TEST_TMPDIR}/vars.env"* ]]
393+
}
394+
395+
@test "env_file_path fails when the file does not exist (issue #3)" {
396+
run_entrypoint ENV_FILE_PATH=/nope/missing.env
397+
[ "$status" -eq 1 ]
398+
[[ "$output" == *"/nope/missing.env does not exist."* ]]
399+
}
400+
401+
@test "env_file and env_file_path together are rejected (issue #3)" {
402+
printf '%s\n' 'DB_USER=plone' > "${BATS_TEST_TMPDIR}/vars.env"
403+
run_entrypoint ENV_FILE="DB_NAME=site" \
404+
ENV_FILE_PATH="${BATS_TEST_TMPDIR}/vars.env"
405+
[ "$status" -eq 1 ]
406+
[[ "$output" == *"env_file and env_file_path are mutually exclusive"* ]]
407+
}
408+
409+
@test "env_file_path is read as part of the deploy flow (issue #3)" {
410+
# The tests above call configure_env_file_path directly; this one proves the
411+
# input is actually wired into the script's flow.
412+
printf '%s\n' 'DB_USER=plone' > "${BATS_TEST_TMPDIR}/vars.env"
413+
run_entrypoint ENV_FILE_PATH="${BATS_TEST_TMPDIR}/vars.env"
414+
[ "$status" -eq 1 ]
415+
[[ "$output" == *"Environment Variables: Reading ${BATS_TEST_TMPDIR}/vars.env"* ]]
416+
[[ "$output" == *"Input remote_host is required!"* ]]
417+
}
418+
419+
@test "env_file still works when env_file_path is unset (issue #3)" {
420+
# Backwards compatibility is the whole point of adding an input rather than
421+
# changing the meaning of the existing one.
422+
run_entrypoint ENV_FILE="DB_USER=plone"
423+
[ "$status" -eq 1 ]
424+
[[ "$output" == *"Environment Variables: Additional values"* ]]
425+
[[ "$output" == *"Input remote_host is required!"* ]]
426+
}
427+
317428
# --- scale_after --------------------------------------------------------------
318429

319430
@test "scale_after does nothing when unset" {

0 commit comments

Comments
 (0)