Orchestration specification

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.

Applies at
.devcontainer/lifecycle/
Status
Stable

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 *.sh in a hook directory are ignored.

Expected behaviour

  1. 01
    The runner takes exactly one argument, the event name, and exits with usage text otherwise.
  2. 02
    A missing <event>.d/ directory is an error that names the expected path.
  3. 03
    An empty directory is not an error; the runner says no scripts were found.
  4. 04
    Hooks run in lexical order, each invoked with bash and announced by name.
  5. 05
    The 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.

FlowOne invocation of run-parts.sh
  1. 01 Check arguments Exactly one non-empty event name, or usage text and exit 1
  2. 02 Resolve directory <event>.d/ beside the runner; missing directory exits 1
  3. 03 Announce Running lifecycle event: <event>
  4. 04 Run hooks Each *.sh in lexical order, as bash <script>, after ==> Running <name>
  5. 05 Finish Report No lifecycle scripts found when 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#

.devcontainer/lifecycle/run-parts.sh (excerpt)
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

How Run-Parts Hook Directories relates to the library

Run-Parts Hook Directories and 1 related specificationsDevcontainer LifecycleDevcontainer Lifecycle
RefinesComposesRelies onFollowed byContrasts with