Every program an app runs is written out in its artifact.yaml, argument by argument, and its settings reach it as
command-line flags. This page is the reference for those commands: how a command is run, the values it can name, what
its environment holds, and how an app is stopped. The rest of the package is in Authoring apps.
Commands
A phase runs something only when it declares a command, and nothing else: a script the package ships runs because a
command names it. A command is a list, one element per argument, run directly with no shell in between:
start:
service: github-runner
command:
unix: ["./start-github-runner.sh", "--url=${url}", "--token-file=${token_file}", "--labels=${labels}"]
windows: ["start-github-runner.ps1", "-Url:${url}", "-TokenFile:${token_file}", "-Labels:${labels}"]
The first element is the program:
- Linux and macOS.
./nameis the package's filename, which must be executable and start with a shebang (#!/bin/sh). A bare name, such aspython3, is looked up inPATH. - Windows. A packaged
.ps1runs aspowershell -NoProfile -ExecutionPolicy Bypass -File <app dir>\name.ps1, followed by the rest of the command. A packaged.exeruns directly. Anything else is looked up inPATH, sopwshcan still be spelled out.
How a script names its flags is up to you. The convention is each platform's own: --kebab-case=value on Linux and
macOS, -PascalCase:value in PowerShell, always one element per flag, never a flag and its value as two.
A script that runs a long-lived program ends by execing it (exec ./bin/server --port=8080), so the stop signal, the
process id and the exit status belong to the program, not to the script that started it.
Tokens
${name} in a command is replaced by a value. Substitution never splits or joins elements: an element stays one
argument whatever its value holds, spaces and quotes included.
| Written | Becomes |
|---|---|
${name} | The value of name. |
${name:-text} | The value, or text when it is unset. ${name:-} is the empty string. |
$${name} | A literal ${name}. |
$$ | A literal $. Any other $ is itself. |
A name is letters, digits and _. A default runs to the first } and is taken as written.
Tokens are filled in two passes. orc build fills ${os}, ${arch} and each platform's vars, anywhere in the
recipe. The node fills settings and context when the command runs, and only in commands, probe commands and the
start.service name. No two values share a name, so you never need to know which pass fills a token.
Settings
${url} is the value of the setting url, as a string. A setting marked sensitive, or with a contentMediaType, has
no value token: ${token_file} (its name plus _file) is the path of a 0600 file holding it. A sensitive value
never appears in a command line, an environment or a log; only its path does. Its lifetime decides when the file is
removed: a startup file is gone once the app has started, so stop, stopped and probes need runtime.
Unset values
An element that names an unset value with no default is dropped whole. --pool=${pool} does not appear at all
when pool is unset, and --name=${a}-${b} is dropped when either is. A value the platform fills in that comes out
empty counts as unset. Your script sees no flag, and uses its own default. Two cases fail instead, naming the token:
- an unset first element, since a command without its program cannot run;
- an unset token in
start.service, since a service name cannot lose a piece and keep its meaning.
On Windows, an element that is -Name: followed only by tokens, such as -Dir:${dir}, is also dropped when they
come out as the empty string. PowerShell would otherwise take the next element as its value. The script sees its own
param() default, so if a script needs "empty", make that its default. An element with text beside its tokens
(-Dir:a${x}) is kept. Linux and macOS keep --dir= with an empty value: there an empty value is a value.
Context
Besides settings, a command can name what the node knows about the run:
| Token | Phases | Value |
|---|---|---|
${version} | all | The upstream version installed; unset on the default tag. |
${instance} | all | This copy of the app: its name, or name:version when several versions run. |
${service} | all, service apps | The platform's service name for start.service. |
${persist_state} | install, start | empty, restored or present, for apps that keep data. |
${pid} | stop, stopped | The app's process id. |
${stop_reason} | stop, stopped | Why it is stopping; see below. |
${stop_status} | stopped | How stop went: ok, failed, timeout or skipped. |
${exit_code}, ${exit_signal} | stopped | How the app ended; exactly one is set. |
${run_duration} | stopped | Seconds the app ran. |
A probe command takes the tokens of start. A persistence hook takes settings, ${version} and ${instance}. The
start.service name takes settings by value, ${version} and ${instance}. Because ${version} is unset on the
default tag, --version=${version} is simply absent there.
The environment
A phase runs in the app's working directory, with a normal login environment: the PATH, HOME and LANG a person
at a shell would have. Nothing in it comes from the app's settings: no setting is exported as a variable, and nothing
the app starts — a job, a plugin, a shell — inherits its configuration.
So a program reads its settings from its arguments alone. When a flag is absent, the setting is unset: never fall back to an environment variable of the same or a similar name, which the host or another tool may have set for its own use. A program should refuse a flag it does not take, so a manifest that names a wrong one fails loudly at start.
Writing the scripts
Write .sh scripts in POSIX sh, so they run under dash, busybox, bash and macOS alike, and make each answer --help
with its usage. Since every flag arrives as the single argument the manifest wrote, a case loop parses them with no
option library:
#!/bin/sh
set -eu
url= token_file= stop_reason=exit
for arg; do
case $arg in
--url=*) url=${arg#--url=} ;;
--token-file=*) token_file=${arg#--token-file=} ;;
--stop-reason=*) stop_reason=${arg#--stop-reason=} ;;
--help) echo "Usage: stopped.sh [--url=URL] [--token-file=PATH] [--stop-reason=REASON]"; exit 0 ;;
*) echo "unknown argument $arg" >&2; exit 2 ;;
esac
done
In PowerShell, the param() block is the parser, and its defaults are what a dropped flag leaves. A comment-based help
block (.SYNOPSIS, .PARAMETER) answers Get-Help:
[CmdletBinding()]
param(
[string]$Url = '',
[string]$TokenFile = '',
[string]$StopReason = 'exit'
)
Stopping an app cleanly
Shutting an app down happens in four steps, in this order:
stopruns while the app is still alive. Make the app safe to signal: take it out of rotation, unregister it, let in-flight work finish. Do not kill it here. Exiting0means "go ahead".- The app is asked to stop —
SIGTERMunless you named anothersignal— or, for a service, stopped through the service manager. A Windows service receives Ctrl+C forSIGINTand Ctrl+Break for any other signal, which .NET raises asConsole.CancelKeyPress; a plain Windows subprocess receives nothing, so there the ask has to come fromstop. - Once
timeouthas run out, whatever is still alive is killed. The timeout counts from the start of the stop and covers bothstopand the app's own exit, so an app that finishes its work after the ask can use whatstopleft of it. However late the ask comes, the app gets at leastgracebefore the kill. stoppedruns once the app has exited, after a clean shutdown, a kill or a crash alike.
A slow or failing stop never blocks the shutdown: a timeout or a non-zero exit is recorded, the flow carries on, and
stopped reads what happened in ${stop_status}. Only a forced stop kills at once: an operator can force one at any
moment — terminating a node that is already terminating does — and it jumps to the kill.
stopped is where you release anything that lives outside the node — a runner registration, a lease, a DNS entry.
What to release depends on ${stop_reason}:
${stop_reason} | Means | Usually |
|---|---|---|
restart | The same install starts again (an edited setting, a renewed certificate). | Keep external state. |
stop | The install stays on the node and may start again. | Keep external state. |
terminate | The node is going away for good. | Release everything external. |
shutdown | The agent is exiting, for an upgrade or a reboot. | Keep everything; the app comes back. |
exit | The app ended on its own. | Usually nothing. |
Write both commands so they can be cut short and run again: a timeout can interrupt them, and they may run after a
crash in which an earlier attempt got nowhere. Neither may reboot the machine; install, start and uninstall may.
Related pages
- Authoring apps — the package: files, platforms, settings, phases and publishing.
- Serving traffic from an app — endpoints, probes, and values the platform fills in.
- App versions — install exactly the version
${version}names.