Repository navigation
93 lines (89 loc) · 4.25 KB
/
Copy pathcompose-validation.yml
File metadata and controls
93 lines (89 loc) · 4.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
name: Validate Compose files
on:
workflow_dispatch:
push:
branches:
- main
pull_request:
branches:
- main
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
validate:
name: Compose files
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Clone this repo
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# Some stacks require the user to set their own secrets and stop with
# "required variable X is missing". Supply a dummy value for those and
# retry, so that the rest of the file is still checked. The template
# leaves SERVICE and IMAGE_URL empty, so it starts with dummy values for
# both. A variable that compose.yaml uses but .env does not define
# ("The X variable is not set") is an error, and so is a required
# variable that .env does not list or a Serve configuration that is not
# valid JSON. Compose leaves a config that no service uses out of its
# output, so a second pass without interpolation reads those.
- name: Run docker compose config
shell: bash
run: |
docker compose version
status=0
for dir in services/*/ templates/service-template/; do
vars=()
if [ "$dir" = templates/service-template/ ]; then
vars=(SERVICE IMAGE_URL)
fi
ok=0
for _ in $(seq 1 30); do
if out=$(cd "$dir" && env "${vars[@]/%/=dummy}" docker compose config --quiet 2>&1); then
ok=1
unset_vars=$(sed -nE 's/.*The \\?"([A-Za-z0-9_]+)\\?" variable is not set.*/\1/p' <<<"$out" | sort -u | paste -sd' ' -)
if [ -n "$unset_vars" ]; then
echo "::error file=${dir}compose.yaml::Used in compose.yaml but not defined in ${dir}.env: ${unset_vars}"
status=1
fi
for v in "${vars[@]}"; do
grep -qE "^${v}=" "${dir}.env" || { echo "::error file=${dir}compose.yaml::${v} is required but not listed in ${dir}.env"; status=1; }
done
if ! (cd "$dir" && env "${vars[@]/%/=dummy}" docker compose config --format json 2>/dev/null \
| jq -r '(.configs // {}) | to_entries[] | select(.key | startswith("ts-serve")) | .value.content' \
| jq empty); then
echo "::error file=${dir}compose.yaml::The Tailscale Serve configuration is not valid JSON"
status=1
fi
used=$(cd "$dir" && env "${vars[@]/%/=dummy}" docker compose config --format json 2>/dev/null \
| jq -c '[(.configs // {}) | keys[] | select(startswith("ts-serve"))]') || used='[]'
raw=$(cd "$dir" && env "${vars[@]/%/=dummy}" docker compose config --no-interpolate --format json 2>/dev/null) || raw='{}'
while read -r name; do
if ! jq -r --arg n "$name" '.configs[$n].content' <<<"$raw" | jq empty; then
echo "::error file=${dir}compose.yaml::The Tailscale Serve configuration ${name}, which the stack does not use by default, is not valid JSON"
status=1
fi
done < <(jq -r --argjson used "$used" '(.configs // {}) | keys[] | select(startswith("ts-serve")) | select(. as $k | $used | index($k) | not)' <<<"$raw")
break
fi
missing=$(grep -oE 'required variable [A-Za-z0-9_]+' <<<"$out" | awk '{print $3}' | sort -u | grep -vxFf <(printf '%s\n' "${vars[@]}") || true)
if [ -z "$missing" ]; then
echo "::error file=${dir}compose.yaml::docker compose config failed in ${dir}"
echo "$out"
status=1
ok=2
break
fi
mapfile -t new <<<"$missing"
vars+=("${new[@]}")
done
if [ "$ok" = 0 ]; then
echo "::error file=${dir}compose.yaml::too many required variables in ${dir}"
status=1
fi
done
exit $status