Skip to content

Breaking Changes

Each release that can break an existing project is listed here, newest first, with the steps to upgrade. The Changelog lists every change.

0.6.0

Version 0.6.0 changes how asgard names a project on disk. Every scheduled task and every watcher installed by an earlier version is filed under the old name, and asgard 0.6.0 does not migrate them. If you use schedule or watch, follow the steps on this page: stop everything under the old version, upgrade, then start everything again.

sh and shebang changed too, and two of those changes can break a .loki: the exec: keyword is gone, and a script given no shell name now runs under the shell set with default_shell, one line or many. A .loki that never calls default_shell gets bash, which must then be on PATH. Both are covered at the end of this page, starting at sh and shebang: exec: removed. Methods named after asgard's own, such as invoke_command, are now refused when .loki loads; see Reserved names. The other changes in the release are listed in the Changelog.


What Changed

Each project's scheduled jobs and watcher state are kept outside the repository, under a name derived from the project. Before 0.6.0 that name was the basename of the directory holding .loki, slugified: a project at ~/work/api was simply api.

Before 0.6.0 From 0.6.0
Project slug api api-9f3a1c2b
launchd label com.madbomber.asgard.api.<name> com.madbomber.asgard.api-9f3a1c2b.<name>
systemd units asgard.api.<name>.{service,timer} asgard.api-9f3a1c2b.<name>.{service,timer}
Watcher state ~/.local/state/asgard/watch/api/ ~/.local/state/asgard/watch/api-9f3a1c2b/

The suffix is the CRC32 of the project's full real path, written as eight hex digits. It is stable for as long as the project stays where it is, and it is the same whether you reach the project through a symlink or not.


Why It Was Necessary

A directory name is not unique on a machine. Two checkouts called api, a repository and its examples/ directory alongside another repository's examples/, a fork next to its upstream: any of these gave two projects the same slug, and the slug was the only thing that told them apart.

The consequences were silent:

  • asgard schedule install in one project removed the other's jobs. Install prunes every installed entry under the project's slug that the current .loki no longer declares. With a shared slug, the other project's entries looked like entries of this project that had been removed from .loki, so they were uninstalled.
  • The two watchers fought over one state directory. Both wrote their root into the same root file, both used the same pidfile, so starting the second watcher was refused because "the watcher is already running", and watcher list showed one project's entries under the other's name.

Hashing the full path into the slug makes every project's name unique without making it unreadable: the basename still comes first, and asgard schedule list --all and asgard watcher list --all show the full path next to each slug.


Before You Upgrade

Do this with the old version still installed, so its commands find the entries under their old names.

1. Find every project with schedules or watches. Both commands work from any directory:

asgard schedule list --all
asgard watcher list --all

Each group is titled Project: <slug> (<path>). The path is where to go next.

2. In each of those projects, remove the schedules and stop the watcher:

cd /path/to/project
asgard schedule remove      # unloads and deletes every job of this project
asgard watcher stop         # a task already running finishes first

schedule remove prints one removed <name> line per entry. watcher stop waits for any running task, then prints stopped watcher for <slug>.

3. Check that nothing is left:

asgard schedule list --all   # No scheduled tasks installed (launchd).
asgard watcher list --all    # every project shows DAEMON stopped

Note

A watcher that shows as stopped may still own a state directory under ~/.local/state/asgard/watch/<old slug>/. It holds only the log and the last-run records and is harmless, but you can delete it once the watcher is stopped if you want a clean slate.


Upgrade

gem update asgard
asgard --version     # 0.6.0

After You Upgrade

In each project, install the schedules and start the watcher again. The declarations in .loki are unchanged; only the name they are filed under is new.

cd /path/to/project
asgard schedule install
asgard watcher start

Confirm the new slugs:

asgard schedule list --all
asgard watcher list --all

Every project title now ends in a hash, for example Project: api-9f3a1c2b (/Users/you/work/api).


If You Already Upgraded

Entries installed by the old version are still there and still run. The new version sees them as belonging to another project, because their slug has no hash, so they appear in schedule list --all and watcher list --all under the old name while your project's own entries, once reinstalled, appear under the new one.

Scheduled tasks. Deal with the leftovers before you run asgard schedule install under the new version. Until then a leftover entry can be stopped, or removed along with its job files, by its old slug:

asgard schedule stop api/nightly      # the old slug was the bare basename
asgard schedule remove api/nightly    # unload it and delete its files

Once the project's new entries are installed, api/nightly matches both the old slug api and the new api-9f3a1c2b, and the command stops with api/nightly is in several projects (api, api-9f3a1c2b); name one as project/nightly. The new entry can still be named by its whole slug, api-9f3a1c2b/nightly, but the old one no longer can. In that case delete the old job by hand:

launchctl bootout gui/$(id -u)/com.madbomber.asgard.api.nightly
rm ~/Library/LaunchAgents/com.madbomber.asgard.api.nightly.plist
systemctl --user disable --now asgard.api.nightly.timer
rm ~/.config/systemd/user/asgard.api.nightly.{service,timer}
systemctl --user daemon-reload

Watchers. A leftover daemon keeps running under the old slug. Its pidfile names it:

kill $(cat ~/.local/state/asgard/watch/api/watcher.pid)
rm -r ~/.local/state/asgard/watch/api

The daemon handles TERM the same way watcher stop does: it stops listening, lets a running task finish, and exits.

Then install and start under the new version as described above.


Two smaller changes came with the slug and are worth knowing:

  • In a project/name argument to schedule stop, start, trigger, log or remove, the project may be the whole slug or just the directory name when that is enough to pick one project out: asgard schedule stop api/nightly reaches api-9f3a1c2b/nightly when no leftover api entry remains.
  • schedule list --all and watcher list --all mark a project whose directory no longer exists with [root missing]. Its jobs were left behind by a move or a delete, and the hash will not match the project at its new location, so remove them the same way as the leftovers above and reinstall from the new location.

sh and shebang: exec: removed

sh and shebang no longer take an exec: keyword. sh is shebang with the default shell, so this is one change, not two. A .loki that still passes it fails on the first call:

ArgumentError: unknown keyword: :exec

What to do: delete the keyword. The task keeps working the same way.

# before
def doc_server = sh "mkdocs serve", exec: true

# from 0.6.0
def doc_server = sh "mkdocs serve"

Why it was removed. exec: true promised to replace the asgard process with the command, so that nothing idle sat in memory beside a dev server and a signal sent to the pid reached the command directly. Since 0.6.0 every script runs as a file under its shell, and a shell does not hand its pid to the last command of a script file. With exec: true the pid was bash, the command was its child, and a kill of that pid stopped bash and left the command running on its own. The keyword no longer did what the documentation said.

Without it, the shell is the unit of work: when the script finishes the shell exits, asgard exits with its status, and Ctrl-C reaches asgard, the shell and the command together because they share the terminal's process group. That is the behaviour exec: true was meant to give, and it is now the only behaviour.


sh and shebang: every script runs under the default shell

Before 0.6.0 a one-line sh "..." went through Ruby's system, which hands a single string to /bin/sh, while a multi-line script went through bash. The same sh call therefore ran under two different shells depending on line count, and bash syntax passed or failed with it. From 0.6.0 sh and shebang are the same method, and a script given no shell name, one line or many, is written to a tempfile and run by the shell set with default_shell. A .loki that never calls default_shell gets bash, the seeded :bash entry of the shells registry. Three things follow.

bash must be on PATH unless you set another default

The seeded :bash entry is the bare word bash, found on PATH at run time. With no default_shell set, a machine or container with only /bin/sh fails on the first sh:

asgard: shell :bash not found: bash ...

and asgard exits 127. Every desktop system, and any image that installs bash, is unaffected.

What to do, if you cannot install bash:

class Tasks
  default_shell :sh           # sh and a nameless shebang run /bin/sh's `sh`
end

or keep :bash as the name and point it somewhere else:

class Tasks
  shell :bash, "/bin/sh"      # every sh and shebang :bash runs this instead
end

The first form is right when your scripts are POSIX sh already. The second is a quick way to get a container running; scripts that use bash features will then fail under /bin/sh the way they did before 0.6.0.

One-liners now run under the default shell, not /bin/sh

With the bash default, this means one-liners now run under bash. On macOS /bin/sh is bash in POSIX mode, so one-liners behave almost exactly as before. On Debian and Ubuntu /bin/sh is dash, so a one-line script that ran under dash now runs under bash. Bash accepts nearly everything dash does, so this mostly adds features rather than removing them, but a few builtins differ:

dash (/bin/sh on Debian) bash
echo "a\tb" interprets \t prints \t literally (use printf)
[ a == b ] error: unexpected operator works

What to do: nothing, unless a one-liner depended on dash-only behaviour. A script that needs /bin/sh exactly can ask for it by name: shebang :sh, "...". The :sh entry is seeded and runs sh from PATH.

$0 is a tempfile path

A script that printed $0, typically in a usage message, used to show bash or the command itself. It now shows the tempfile the script was written to, such as /var/folders/.../T/asgard_20261024-80545-bmbloc.sh. The file is deleted when the script finishes.

What to do: print a name you choose instead of $0. Nothing else reads it.

Also changed, not breaking

  • sh and shebang return true; they used to return nil.
  • A command that ends on a signal exits 128 plus the signal number, as a shell would. It used to raise a TypeError from a nil exit status.
  • A missing interpreter is named in the error message and exits 127 instead of exiting 127 silently.

Reserved names: invoke_command and asgard's other methods

Before 0.6.0 a .loki could define a method with the same name as one asgard uses itself, and the .loki version silently replaced asgard's. From 0.6.0 a task or helper with such a name is refused while the .loki loads, and asgard exits 1 before running anything:

asgard: 'invoke_command' at /proj/.loki:6 is an asgard helper and cannot be a task or helper; rename it

This applies whether the method is a task or a helper inside no_commands. The most common case in older .loki files is invoke_command, defined to run code around every task (timing, logging, a banner) and then call super. That worked in 0.5 and is refused in 0.6.0.

Besides the names Thor reserves (run, shell, options, invoke, args and the rest listed in Reserved Names), these names are now refused:

Kind Names
Helpers a task body calls sh, shebang, env, import, import_up, loki_up, changes, debug?, verbose?, dep_result, dep_results, current_task
The method Thor runs every task through invoke_command

asgard's other internal methods start with _, a prefix that is already asgard's own, so they never clash with a name in your .loki.

What to do: rename the method. The error names the file and line. A helper of your own that happened to share a name, such as env or changes, only needs a new name and its callers updated.

An invoke_command that wrapped every task has no direct replacement. Move the work it did into the tasks that need it, or into a task of its own that the others name with depends_on:

# before: ran before every task
no_commands do
  def invoke_command(command, *args)
    puts "== #{command.name}"
    super
  end
end

# from 0.6.0
desc "banner", "Print the run banner"
def banner = puts "== #{ARGV.first}"

depends_on :banner
desc "build", "Build the project"
def build = sh "make"

A dependency runs once per asgard invocation, so banner prints once however many tasks depend on it.