Run-Parts Hook Directories
One small runner turns each lifecycle event into a directory of .sh hooks, run in lexical order and stopped by the first failure.
- Kind
- Orchestration
- Domain
- Environment & Tooling
- Applies at
.devcontainer/lifecycle/- Status
- Stable
- Source · managed
.devcontainer/lifecycle/run-parts.sh
01 Defines
The contract of run-parts.sh: given one lifecycle event name, it runs every .sh file in the matching <event>.d/ directory with bash, in lexical order.
02 Applies when
- A single configured command needs to grow into several independent setup steps.
- Steps are added or removed by adding or deleting files, without editing configuration.
- The same hooks are run by the Dev Containers tooling and by hand while debugging.
03 Boundaries
- The runner only sequences scripts; what each hook does is up to the hook.
- It does not run hooks in parallel or skip failing ones.
- Files other than
*.shin a hook directory are ignored.
Expected behaviour
- 01The runner takes exactly one argument, the event name, and exits with usage text otherwise.
- 02A missing
<event>.d/directory is an error that names the expected path. - 03An empty directory is not an error; the runner says no scripts were found.
- 04Hooks run in lexical order, each invoked with
bashand announced by name. - 05The runner uses
set -eu, so a failing hook stops the event and fails the lifecycle command.
Behaviour#
The name comes from the classic Unix run-parts utility, which runs every
script in a directory. Here the directory is chosen by event: post-start
resolves to post-start.d/, on-create to on-create.d/, and so on. Adding
a hook is adding a file; the devcontainer.json command never changes.
- 01 Check arguments Exactly one non-empty event name, or usage text and exit 1
- 02
Resolve directory
<event>.d/beside the runner; missing directory exits 1 - 03
Announce
Running lifecycle event: <event> - 04
Run hooks
Each
*.shin lexical order, asbash <script>, after==> Running <name> - 05
Finish
Report
No lifecycle scripts foundwhen the glob matched nothing
The canonical event names are initialize, on-create, update-content,
post-create, post-start, and post-attach, one per phase of the
Devcontainer Lifecycle
. The runner enforces them through the
directory check: an event with no matching directory is refused.
Ordering and failure#
Because the shell glob expands in lexical order, numeric prefixes make the
sequence visible in a directory listing: 10-setup-devcontainer-tools.sh runs
before 20-configure-zsh-profile.sh. Scripts that don’t depend on each other
can share a prefix or use none.
Each hook is invoked as bash "$script", so it doesn’t need its executable bit
set and doesn’t depend on its shebang. The runner itself runs under set -eu;
when a hook exits non-zero, the runner exits at that point and the remaining
hooks in the directory do not run.
Examples#
hooks_dir="$script_dir/$event.d"
if [ ! -d "$hooks_dir" ]; then
echo "Lifecycle directory is missing for event: $event" >&2
echo "Expected directory: $hooks_dir" >&2
exit 1
fi
found_script=0
for script in "$hooks_dir"/*.sh; do
if [ ! -f "$script" ]; then
continue
fi
found_script=1
script_name=${script##*/}
echo "==> Running $script_name"
bash "$script"
done
Running a phase by hand uses the same entry point as the container:
bash .devcontainer/lifecycle/run-parts.sh update-content
Connections