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
17 changes: 17 additions & 0 deletions .github/workflows/shellcheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,28 @@ on:
- main
- renovate/*

# The shellcheck version is pinned so CI and a developer's machine agree.
# The runner's own apt shellcheck drifts (it was 0.9.0 while a current Homebrew
# is 0.11.0), and the two disagree on findings — 0.9 flags SC2015/SC2002 that
# 0.11 does not — so an unpinned job passes or fails by accident. Match it
# locally with the same release, or `brew install shellcheck@0.10` where pinned.
env:
SHELLCHECK_VERSION: "0.10.0"

jobs:
shellcheck:
name: ShellCheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Install the pinned ShellCheck
run: |
set -euo pipefail
url="https://github.com/koalaman/shellcheck/releases/download/v${SHELLCHECK_VERSION}/shellcheck-v${SHELLCHECK_VERSION}.linux.x86_64.tar.xz"
curl -fsSL "$url" | tar -xJ
sudo install "shellcheck-v${SHELLCHECK_VERSION}/shellcheck" /usr/local/bin/shellcheck
shellcheck --version

- name: Run ShellCheck
run: find . -type f -name "*.sh" -not -path "./.git/*" -exec shellcheck {} +
52 changes: 52 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,58 @@ jobs:
GD_TEST_MIN_COMPOSE: ${{ runner.temp }}/min-compose/docker-compose
run: node --test --test-timeout=120000 tests/*.test.mjs

# macOS is a supported host for local development (`install.sh --local`), so
# the shell helpers run on its BSD userland and bash 3.2, where a GNU-only
# construct that passes on Linux breaks. This job exercises them there.
#
# GitHub's macOS runners have no Docker daemon, so the daemon-backed tests
# (mode matrix, Caddy, ingress, the installer end to end) skip themselves
# through `dockerAvailable()`. The Docker *CLI* is installed anyway — no
# daemon — so the tests that only parse configuration (`docker compose
# config`) or probe for an unreachable daemon still run and are meaningful.
# Production ingress stays a Linux concern and is not run here.
helpers-macos:
name: Helpers on macOS
runs-on: macos-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: "22"

- name: Install the Docker CLI and Compose plugin (no daemon)
env:
# A current Compose is enough: it is only used to *parse* configuration
# (`docker compose config`), never to reach a daemon.
COMPOSE_PLUGIN_VERSION: "2.29.7"
run: |
set -euo pipefail
# The runner has no Docker at all. `brew install docker` gives the CLI
# (client only, no daemon); the Homebrew compose formula does not wire
# itself up as a `docker compose` subcommand, so the plugin is dropped
# into cli-plugins directly instead.
brew install docker
mkdir -p ~/.docker/cli-plugins
curl -fsSL \
"https://github.com/docker/compose/releases/download/v${COMPOSE_PLUGIN_VERSION}/docker-compose-darwin-aarch64" \
-o ~/.docker/cli-plugins/docker-compose
chmod +x ~/.docker/cli-plugins/docker-compose
docker --version
docker compose version
jq --version

- name: Run the helper tests under bash 3.2
env:
# macOS's system bash is 3.2, which the helpers target; the runner's
# own bash on PATH is newer and would hide a 3.2 incompatibility, so
# the suite is pointed at /bin/bash explicitly.
GD_TEST_BASH: /bin/bash
run: |
set -eu
/bin/bash --version | head -1
node --test --test-timeout=120000 tests/*.test.mjs

ingress:
name: Ingress smoke tests
runs-on: ubuntu-latest
Expand Down
11 changes: 9 additions & 2 deletions scripts/lib/env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,15 @@ env_get() {
printf 'error: %s spans several lines; edit it by hand\n' "$2" >&2
return 1
;;
# Single quoted: literal, and a backslash before a quote is the only escape.
s) printf '%s\n' "${found_body//\\\'/\'}" ;;
# Single quoted: literal, and a backslash before a quote is the only
# escape. The substitution is done in an unquoted assignment rather than
# inside the printf's double quotes: bash 3.2 (macOS's system bash) and
# bash 4+ parse the backslashes in a double-quoted `${v//\\\'/\'}`
# pattern differently, and only the unquoted form agrees on both.
s)
local unescaped=${found_body//\\\'/\'}
printf '%s\n' "$unescaped"
;;
*)
_gd_env_unescape "$found_body"
printf '\n'
Expand Down
7 changes: 6 additions & 1 deletion tests/helpers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,14 @@ const LIBS = ['fs', 'env', 'compose', 'config', 'caddy', 'meta', 'preflight', 'i
* Returns { stdout, stderr, status }. Throws only if the shell itself cannot
* be started.
*/
// The helpers target bash 3.2 (macOS's system bash). GD_TEST_BASH points the
// suite at a specific interpreter — the macOS CI job sets it to /bin/bash so a
// bash-4+-ism that a newer Homebrew bash would tolerate is caught.
const BASH = process.env.GD_TEST_BASH || 'bash';

export function sh(script, { cwd = REPO_DIR, env = {}, input } = {}) {
const preamble = LIBS.map((l) => `. "${REPO_DIR}/scripts/lib/${l}.sh"`).join('\n');
const result = spawnSync('bash', ['-c', `${preamble}\n${script}`], {
const result = spawnSync(BASH, ['-c', `${preamble}\n${script}`], {
cwd,
input,
env: { ...process.env, ...env },
Expand Down