diff --git a/.ai/skills/linear/SKILL.md b/.ai/skills/linear/SKILL.md
new file mode 100644
index 0000000..c70f71a
--- /dev/null
+++ b/.ai/skills/linear/SKILL.md
@@ -0,0 +1,123 @@
+---
+name: linear-cli
+description: Create, update, organize, and comment on Linear CLI repository issues with mix lc, body files, assignment/status setup, and dependency links. Use for Linear work in this repository, not unrelated Linear projects.
+---
+
+# Linear CLI issues
+
+Use this skill for Linear issue work in the `linear-cli` repository.
+
+Use `mix lc` rather than raw `lc`, so the working tree's CLI is exercised.
+If `mix lc` fails, report the failure; do not fall back to raw `lc`.
+
+## Required client and body handling
+
+- Interact with Linear only through `mix lc`. Do not use an MCP connector,
+ browser, direct API request, or another CLI.
+- Always use `--body-file PATH` for issue descriptions and comments: creation,
+ description updates, and comments. The body file preserves text verbatim and
+ avoids shell-quoting failures.
+- Give every create, description-update, and comment operation a dedicated
+ temporary body file. Write the final body to that file, pass it to `mix lc`,
+ and remove it when the operation has finished or is abandoned.
+
+## Issue creation
+
+Before creating issues, establish the requested team, project, assignee,
+status, labels, and direct dependency graph. Read existing issues only when
+needed to avoid duplicates.
+
+For each issue:
+
+1. Write one independently implementable outcome with a concise title.
+2. Put its goal, scope, preserved behavior, exclusions, acceptance criteria,
+ and dependencies in the dedicated body file.
+3. When labels were requested, append `--labels LABELS` to the create command.
+4. Create it unassigned and non-interactively:
+
+ ```sh
+ mix lc issue create --yes --no-take \
+ --team TEAM \
+ --project PROJECT \
+ --title TITLE \
+ --body-file BODY_FILE
+ ```
+
+5. Capture the returned identifier. Do nothing further when neither an
+ assignee nor a status was requested. Otherwise apply exactly what the user
+ requested:
+
+ * assignee and status:
+
+ ```sh
+ mix lc issue assign --assignee ASSIGNEE --status STATUS ISSUE_ID
+ ```
+
+ * assignee only:
+
+ ```sh
+ mix lc issue assign --assignee ASSIGNEE ISSUE_ID
+ ```
+
+ * status only:
+
+ ```sh
+ mix lc issue status ISSUE_ID --status STATUS
+ ```
+
+Do not commit while creating or organizing issues unless the user separately
+asks for a commit.
+
+## Issue updates
+
+First read the issue when its current state matters, then make only the
+requested change. Use `lc issue update` for supported issue fields and
+lifecycle changes. For example, move an issue to a project:
+
+```sh
+mix lc issue update ISSUE_ID --project PROJECT
+```
+
+Or close an issue with a specific workflow status:
+
+```sh
+mix lc issue update ISSUE_ID --close --status "Done"
+```
+
+Do not use an update as an opportunity to change unrelated fields. Update a
+description through its dedicated body file:
+
+```sh
+mix lc issue update ISSUE_ID --body-file BODY_FILE
+```
+
+## Comments
+
+Use `lc issue comment` to add a comment with its dedicated body file. For
+example:
+
+```sh
+mix lc issue comment ISSUE_ID --body-file COMMENT_FILE
+```
+
+Write comments that state the outcome, relevant evidence, and any remaining
+blocker or handoff; do not repeat the issue description.
+
+## Dependencies
+
+Link a dependent issue to each direct prerequisite:
+
+```sh
+mix lc issue relation add DEPENDENT_ID PREREQUISITE_ID --type blocked-by
+```
+
+Use only direct edges; do not add relationships implied transitively. Verify
+the finished graph with `mix lc issue relation list ISSUE_ID` for every
+affected issue, then report the issue identifiers, assignment/status, direct
+blockers, and independent work.
+
+## Authorization
+
+This skill standardizes mechanics only. It does not authorize creating,
+editing, commenting on, assigning, or linking Linear issues; obtain the user's
+request before performing each external change.
diff --git a/.claude/skills b/.claude/skills
new file mode 120000
index 0000000..6838a11
--- /dev/null
+++ b/.claude/skills
@@ -0,0 +1 @@
+../.ai/skills
\ No newline at end of file
diff --git a/.codex/skills b/.codex/skills
new file mode 120000
index 0000000..6838a11
--- /dev/null
+++ b/.codex/skills
@@ -0,0 +1 @@
+../.ai/skills
\ No newline at end of file
diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml
index 0e6e326..34dfbff 100644
--- a/.github/workflows/main.yaml
+++ b/.github/workflows/main.yaml
@@ -350,58 +350,34 @@ jobs:
container:
needs: [burrito-package]
- name: Build and publish container image
- runs-on: ubuntu-latest
+ name: Build and publish ${{ matrix.arch }} container image
+ runs-on: ${{ matrix.runner }}
+ strategy:
+ matrix:
+ include:
+ - target: linux_x86_64
+ arch: amd64
+ runner: ubuntu-latest
+ - target: linux_aarch64
+ arch: arm64
+ runner: ubuntu-24.04-arm
steps:
-
uses: actions/checkout@v7
-
- uses: erlef/setup-beam@v1
- with:
- otp-version: "29.0.3"
- elixir-version: "1.20.3"
- disable_problem_matchers: true
- -
- # Same Burrito build as the `burrito-build` job above, so it needs
- # the same pinned Zig (see that job's comment) - missing here would
- # fail this job's build at `mix release` time even though
- # `burrito-build` succeeds.
- uses: mlugg/setup-zig@v2.2.1
+ # Reuse the matching native binary from burrito-build. Building the
+ # image on a matching runner means Alpine and the packaged executable
+ # have the same architecture without emulation.
+ name: Download the ${{ matrix.target }} container payload
+ uses: actions/download-artifact@v7
with:
- version: "0.16.0"
- -
- run: mix deps.get
- working-directory: app
- -
- # Same two-stage preparation as the Linux burrito-build legs: Syntect
- # must finish compiling with its host NIF before that artifact can be
- # replaced with the musl build.
- name: Compile makeup_syntect with its host NIF
- env:
- MIX_ENV: prod
- run: mix deps.compile castore rustler_precompiled makeup_syntect
- working-directory: app
- -
- name: Pre-compile mdex_native with its musl NIF
- env:
- MIX_ENV: prod
- TARGET_ABI: musl
- run: mix deps.compile mdex_native
- working-directory: app
- -
- # Install Syntect's musl artifact and apply the same libgcc repair to
- # both Rust NIFs as the standalone Linux binaries.
- name: Install and repair the musl NIFs
- run: ../ci/prepare_musl_nifs.sh
- working-directory: app
- -
- name: Build the linux_x86_64 target (container's payload)
- run: MIX_ENV=prod BURRITO_TARGET=linux_x86_64 mix release lc
- working-directory: app
+ name: burrito-${{ matrix.target }}
+ path: app/burrito_out
-
name: Build the image
env:
APP_VERSION: ${{ needs.burrito-package.outputs.tag_name }}
+ BURRITO_TARGET: ${{ matrix.target }}
run: ./ci/build_image.sh "${{ needs.burrito-package.outputs.tag_name }}"
-
# Verify the musl NIF actually loads on Alpine before publishing.
@@ -409,12 +385,9 @@ jobs:
# Markdown renders, forcing the MDEx and Syntect NIFs, their bundled
# libgcc runtimes, Makeup, and the Elixir/Ruby lexers to load. It then
# prints the version and exits 0; no API key or network is needed.
- # ci/build_image.sh prefers
- # Podman, so the image lives in Podman's local storage; we run it
- # directly rather than loading a tarball. LINEAR_CLI_DAEMON is
- # overridden to false (the image bakes in true so the default CMD
- # starts the daemon; without this override the app ignores `version`
- # and stays alive as a daemon, timing out the step). See EXT-7.
+ # Podman owns the image built above, so run it directly. Overriding
+ # LINEAR_CLI_DAEMON lets `lc version` exit instead of starting the
+ # daemon. See EXT-7.
name: Smoke-test the container image Markdown runtime on Alpine/musl
timeout-minutes: 1
run: |
@@ -433,7 +406,7 @@ jobs:
name: Save the image to a tarball for Trivy to scan directly
id: save
run: |
- tarball="$RUNNER_TEMP/linear-cli.tar"
+ tarball="$RUNNER_TEMP/linear-cli-${{ matrix.arch }}.tar"
./ci/save_image.sh "${{ needs.burrito-package.outputs.tag_name }}" "$tarball"
printf 'tarball_path=%s\n' "$tarball" >> "$GITHUB_OUTPUT"
-
@@ -450,12 +423,13 @@ jobs:
input: ${{ steps.save.outputs.tarball_path }}
scan-type: image
format: cyclonedx
- output: container-sbom.cdx.json
+ output: container-sbom-${{ matrix.arch }}.cdx.json
-
- name: Publish the image
+ name: Publish the ${{ matrix.arch }} image
env:
GITHUB_TOKEN: ${{ github.token }}
GITHUB_ACTOR: ${{ github.actor }}
+ REGISTRY_TAG_SUFFIX: -${{ matrix.arch }}
run: ./ci/publish.sh "${{ needs.burrito-package.outputs.tag_name }}"
-
# Can't attach this to the GitHub release the burrito-package job
@@ -465,10 +439,36 @@ jobs:
name: Upload the container SBOM as a workflow artifact
uses: actions/upload-artifact@v7
with:
- name: container-sbom
- path: container-sbom.cdx.json
+ name: container-sbom-${{ matrix.arch }}
+ path: container-sbom-${{ matrix.arch }}.cdx.json
retention-days: 90
+ container-manifest:
+ needs: [burrito-package, container]
+ name: Publish multi-architecture container manifest
+ runs-on: ubuntu-latest
+ steps:
+ -
+ name: Publish the amd64 and arm64 image manifest
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+ GITHUB_ACTOR: ${{ github.actor }}
+ TAG: ${{ needs.burrito-package.outputs.tag_name }}
+ run: |
+ image_repo=ghcr.io/rubyists/linear-cli
+ printf '%s' "$GITHUB_TOKEN" | podman login --username "$GITHUB_ACTOR" --password-stdin ghcr.io
+
+ mapfile -t tags < <(echo "$TAG" | awk -F'.' 'NF==3{print; print $1"."$2; print $1; next} NF==2{print; print $1; next} {print}')
+
+ for published_tag in "${tags[@]}"
+ do
+ manifest="$image_repo:$published_tag"
+ podman manifest create "$manifest"
+ podman manifest add "$manifest" "docker://$image_repo:${published_tag}-amd64"
+ podman manifest add "$manifest" "docker://$image_repo:${published_tag}-arm64"
+ podman manifest push --all "$manifest" "docker://$manifest"
+ done
+
homebrew-tap-bump:
needs: [burrito-package]
name: Bump the Homebrew tap formula
diff --git a/Readme-docinfo-footer.html b/Readme-docinfo-footer.html
new file mode 100644
index 0000000..4a97133
--- /dev/null
+++ b/Readme-docinfo-footer.html
@@ -0,0 +1,39 @@
+
+
diff --git a/Readme.adoc b/Readme.adoc
index 5dacd89..f5335a9 100644
--- a/Readme.adoc
+++ b/Readme.adoc
@@ -7,6 +7,8 @@ Tj Vanderpoel (bougyman)
:tip-caption: 💡
:note-caption: 📝
:experimental:
+:docinfo: private
+:docinfodir: {docdir}
ifdef::env-github[]
:tip-caption: :bulb:
:note-caption: :information_source:
@@ -25,7 +27,20 @@ endif::[]
A command line interface to https://linear.app - list, take, create, develop,
and update issues without leaving your terminal.
-image::cinemas/listings.cinema.gif[]
+[cols="1,1",frame=none,grid=none]
+|===
+|Ruby CLI (The Original)
+|Elixir CLI (This Project)
+
+a|[.cast-preview]
+image::cinemas/listings.cinema.gif[Original Ruby CLI listing demo,link=cinemas/listings.cinema.gif,window=_blank]
+
+a|[.cast-preview]
+image::cinemas/elixir-lcls-demo.gif[Elixir CLI listing demo,link=cinemas/elixir-lcls-demo.gif,window=_blank]
+|===
+
+Click either recording to enlarge it. In the rendered HTML, press kbd:[Esc]
+or select *Close* to return here.
This is an Elixir/OTP port of https://github.com/rubyists/linear-cli-rb[linear-cli-rb]
(the original Ruby CLI): same commands, same wrapper scripts (`lcls`,
@@ -40,6 +55,7 @@ vs ~5s doing it one at a time.
=== I don't want to install
You can run the CLI from the container image without installing anything.
+The image supports Linux x86_64 and ARM64 hosts.
[source,sh,subs="attributes+"]
----
@@ -54,7 +70,39 @@ entrypoint can also run as the long-lived Oban scheduling daemon. Override it
to an empty value (as above) for one-off interactive CLI usage - otherwise
`lc`/the wrapper scripts never see your command at all.
-=== Download a release binary (Most should use this)
+=== Homebrew (Recommended: macOS Apple Silicon, Linux x86_64, Linux ARM64)
+
+[source,sh]
+----
+$ brew tap rubyists/tap
+$ brew install lc
+----
+
+See https://github.com/rubyists/homebrew-tap for the formula itself.
+
+=== install.sh (for machines without Homebrew)
+
+[source,sh]
+----
+$ curl --retry 5 --retry-all-errors -fsSL https://raw.githubusercontent.com/rubyists/linear-cli/main/install.sh | bash
+----
+
+Detects your platform, downloads and checksum-verifies the matching release
+tarball (the same one in the "Download a release binary" section below),
+and installs `lc` plus the `bin/` wrapper scripts onto a directory already on
+your `$PATH` - no Erlang/Elixir/Zig toolchain required. `LC_VERSION` pins a
+specific release tag instead of the latest one; `LC_INSTALL_DIR` pins a
+specific install directory.
+
+[source,sh]
+----
+$ curl --retry 5 --retry-all-errors -fsSL https://raw.githubusercontent.com/rubyists/linear-cli/main/uninstall.sh | bash
+----
+
+Removes exactly what `install.sh` installed, by replaying the manifest it
+wrote - not a guess at where things ended up.
+
+=== Download a release binary
Grab the tarball for your platform from the
https://github.com/rubyists/linear-cli/releases[latest release] - macOS
@@ -87,39 +135,7 @@ so they're unaffected) since it isn't signed/notarized yet:
$ xattr -d com.apple.quarantine /usr/local/bin/lc
----
-=== Homebrew (macOS Apple Silicon, Linux x86_64, Linux ARM64)
-
-[source,sh]
-----
-$ brew tap rubyists/tap
-$ brew install lc
-----
-
-See https://github.com/rubyists/homebrew-tap for the formula itself.
-
-=== install.sh (fetches a release binary, for machines without Homebrew)
-
-[source,sh]
-----
-$ curl --retry 5 --retry-all-errors -fsSL https://raw.githubusercontent.com/rubyists/linear-cli/main/install.sh | bash
-----
-
-Detects your platform, downloads and checksum-verifies the matching release
-tarball (the same one the "Download a release binary" section above uses),
-and installs `lc` plus the `bin/` wrapper scripts onto a directory already on
-your `$PATH` - no Erlang/Elixir/Zig toolchain required. `LC_VERSION` pins a
-specific release tag instead of the latest one; `LC_INSTALL_DIR` pins a
-specific install directory.
-
-[source,sh]
-----
-$ curl --retry 5 --retry-all-errors -fsSL https://raw.githubusercontent.com/rubyists/linear-cli/main/uninstall.sh | bash
-----
-
-Removes exactly what `install.sh` installed, by replaying the manifest it
-wrote - not a guess at where things ended up.
-
-=== From Source (You are obviously a developer)
+=== From Source (Developers only)
[source,sh]
----
@@ -275,10 +291,15 @@ $ lc issue assign CRY-1234 <4>
----
$ lc issue create --title "My new issue" --description "This is a new issue" --labels Bug,Feature --team CRY
$ lc issue create -t "My new issue" -T CRY -l Improvement,Feature
+$ lc issue create --title "Unassigned issue" --description "Ready to triage" --team CRY --yes --no-take
----
NOTE: If you don't provide a title, team, labels, or description, you will be prompted to enter them.
+NOTE: `--yes` creates and self-assigns an issue without prompting. Add
+`--no-take` to keep the new issue unassigned; it cannot be combined with
+`--dev`, which starts development by self-assigning the issue.
+
TIP: When creating an issue, you can use the `--dev` (or `--develop`) option to immediately start development on the issue.
==== Develop an issue
@@ -470,9 +491,10 @@ $ lproj list --mine
== Development
-First, activate the repo's git hooks (enforces conventional-commit subjects
-on every commit and push, and runs a Hex dependency security audit before a
-push):
+In a shell with Mise activated, entering the checkout installs the repo's git
+hooks when needed. Otherwise, activate them manually (they enforce
+conventional-commit subjects on every commit and push, and run a Hex dependency
+security audit before a push):
[source,sh]
----
diff --git a/app/lib/linear_cli/cli.ex b/app/lib/linear_cli/cli.ex
index 0bdb0da..e542802 100644
--- a/app/lib/linear_cli/cli.ex
+++ b/app/lib/linear_cli/cli.ex
@@ -798,7 +798,8 @@ defmodule LinearCli.CLI do
],
flags: [
develop: [long: "--dev", help: "Start development after creating the issue"],
- yes: [short: "-y", long: "--yes", help: "Skip all interactive prompts"]
+ yes: [short: "-y", long: "--yes", help: "Skip all interactive prompts"],
+ no_take: [long: "--no-take", help: "Do not assign the new issue to yourself"]
]
],
develop: [
diff --git a/app/lib/linear_cli/cli/commands.ex b/app/lib/linear_cli/cli/commands.ex
index a0a42c6..3b8e898 100644
--- a/app/lib/linear_cli/cli/commands.ex
+++ b/app/lib/linear_cli/cli/commands.ex
@@ -346,8 +346,9 @@ defmodule LinearCli.CLI.Commands do
@doc """
Ported from commands/issue/create.rb: resolves every field
(`LinearCli.CLI.IssueHelpers.make_da_issue!/1`), optionally self-assigns it
- (`prompt.yes?('Do you want to take this issue?')`), displays it, then, if
- `--dev` was given, chains straight into the same flow as `issue_develop/2`
+ (`prompt.yes?('Do you want to take this issue?')`, unless `--no-take` was
+ given), displays it, then, if `--dev` was given, chains straight into the
+ same flow as `issue_develop/2`
(Ruby: `Rubyists::Linear::CLI::Issue::Develop.new.call(issue_id: issue.id,
**options)`).
@@ -362,7 +363,8 @@ defmodule LinearCli.CLI.Commands do
def issue_create(result, opts \\ [])
def issue_create(%{options: options, flags: flags}, opts) do
- with :ok <- validate_body_file_exclusion(options, :description, "--description"),
+ with :ok <- validate_no_take_develop(flags),
+ :ok <- validate_body_file_exclusion(options, :description, "--description"),
{:ok, description} <- resolve_body_from_file(options, :description),
create_opts = [
title: options.title,
@@ -373,20 +375,27 @@ defmodule LinearCli.CLI.Commands do
yes: flags.yes
],
{:ok, issue} <- IssueHelpers.make_da_issue!(create_opts),
- :ok <- maybe_take(issue, flags.yes, opts) do
+ :ok <- maybe_take(issue, flags, opts) do
Display.show(issue, %{output: options.output})
if flags.develop, do: run_develop(issue.id, opts), else: :ok
end
end
- defp maybe_take(issue, true, opts) do
+ defp validate_no_take_develop(%{no_take: true, develop: true}),
+ do: {:error, {:smells_bad, "--no-take cannot be used with --dev"}}
+
+ defp validate_no_take_develop(_flags), do: :ok
+
+ defp maybe_take(_issue, %{no_take: true}, _opts), do: :ok
+
+ defp maybe_take(issue, %{yes: true}, opts) do
case IssueHelpers.gimme_da_issue!(issue.id, opts) do
{:ok, _updated} -> :ok
{:error, reason} -> {:error, reason}
end
end
- defp maybe_take(issue, _yes, opts) do
+ defp maybe_take(issue, _flags, opts) do
if Prompt.yes?("Do you want to take this issue?") do
case IssueHelpers.gimme_da_issue!(issue.id, opts) do
{:ok, _updated} -> :ok
diff --git a/app/test/linear_cli/cli/issue_commands_test.exs b/app/test/linear_cli/cli/issue_commands_test.exs
index 1cf8983..62a28eb 100644
--- a/app/test/linear_cli/cli/issue_commands_test.exs
+++ b/app/test/linear_cli/cli/issue_commands_test.exs
@@ -1412,6 +1412,75 @@ defmodule LinearCli.CLI.IssueCommandsTest do
assert_received {:halted, _code}
end
+ test "--no-take keeps a -y/--yes-created issue unassigned" do
+ created_issue =
+ issue_map(%{
+ "id" => "i2",
+ "identifier" => "CRY-2",
+ "title" => "New thing",
+ "branchName" => "cry-2-new-thing",
+ "description" => "Some description",
+ "assignee" => nil
+ })
+
+ stub_responses([
+ {"team(id: $id)", %{"data" => %{"team" => team_map()}}},
+ {"projects(first: 100", team_projects([])},
+ {"issueCreate", %{"data" => %{"issueCreate" => %{"issue" => created_issue}}}}
+ ])
+
+ output =
+ capture_io(fn ->
+ assert :ok =
+ LinearCli.CLI.main([
+ "issue",
+ "create",
+ "--title",
+ "New thing",
+ "--description",
+ "Some description",
+ "--team",
+ "ENG",
+ "--yes",
+ "--no-take"
+ ])
+ end)
+
+ refute output =~ "Do you want to take this issue?"
+ refute output =~ "Assigning issue"
+ assert output =~ "CRY-2"
+ end
+
+ test "--no-take cannot be combined with --dev" do
+ test_pid = self()
+ halt = fn code -> send(test_pid, {:halted, code}) end
+
+ Req.Test.stub(LinearCli.Api, fn _conn -> raise "no GraphQL call should happen" end)
+
+ output =
+ capture_io(:stderr, fn ->
+ LinearCli.CLI.main(
+ [
+ "issue",
+ "create",
+ "--title",
+ "New thing",
+ "--description",
+ "Some description",
+ "--team",
+ "ENG",
+ "--yes",
+ "--no-take",
+ "--dev"
+ ],
+ halt
+ )
+ end)
+
+ assert_received {:halted, 22}
+ assert output =~ "--no-take cannot be used with --dev"
+ end
+
test "-y/--yes with all required flags creates and self-assigns without any prompts" do
created_issue =
issue_map(%{
diff --git a/ci/build_image.sh b/ci/build_image.sh
index a49cc40..9b5a4d2 100755
--- a/ci/build_image.sh
+++ b/ci/build_image.sh
@@ -40,6 +40,7 @@ default_image_name=$(printf "%s" "$base_dir" | tr '[:upper:]' '[:lower:]')
: "${IMAGE_NAME:=$default_image_name}"
: "${LICENSE:=Proprietary/All Rights Reserved}"
: "${APP_VERSION:=$(< "$here"/../.version.txt)}"
+: "${BURRITO_TARGET:=linux_x86_64}"
usage() {
cat <<-EOT
@@ -177,5 +178,6 @@ debug "Building $full_tag with $runtime from $CONTAINERFILE"
--label org.opencontainers.image.source="$image_url/tree/$revision" \
--label shortref="$shortref" \
--build-arg APP_VERSION="$APP_VERSION" \
+ --build-arg BURRITO_TARGET="$BURRITO_TARGET" \
-f "$CONTAINERFILE" \
"$BUILD_CONTEXT"
diff --git a/ci/publish.sh b/ci/publish.sh
index b709358..ae34c8f 100755
--- a/ci/publish.sh
+++ b/ci/publish.sh
@@ -39,6 +39,7 @@ base_dir=$(basename "$(pwd)")
: "${REGISTRY:=ghcr.io}"
: "${REGISTRY_TOKEN:=${GITHUB_TOKEN:-}}"
: "${REGISTRY_USERNAME:=${GITHUB_ACTOR:-}}"
+: "${REGISTRY_TAG_SUFFIX:=}"
usage() {
cat <<-EOT
@@ -173,7 +174,9 @@ mapfile -t tags < <(echo "$tag" | awk -F'.' 'NF==3{print; print $1"."$2; print $
for pushed_tag in "${tags[@]}"
do
- remote_image="$image_repo:$pushed_tag"
+ # Release CI publishes one temporary tag per architecture, then combines
+ # them into this public tag's manifest after both native builds succeed.
+ remote_image="$image_repo:$pushed_tag$REGISTRY_TAG_SUFFIX"
debug "Pushing $local_image to $remote_image"
"$runtime" tag "$local_image" "$remote_image" ||
die 9 "Failed to tag '$local_image' as '$remote_image'"
diff --git a/cinemas/elixir-lcls-demo.cast b/cinemas/elixir-lcls-demo.cast
new file mode 100644
index 0000000..ff97c1d
--- /dev/null
+++ b/cinemas/elixir-lcls-demo.cast
@@ -0,0 +1,86 @@
+{"version":3,"term":{"cols":195,"rows":56,"type":"xterm-256color","version":"tmux 3.7c","theme":{"fg":"#d8d8d8","bg":"#181818","palette":"#181818:#ac4242:#90a959:#f4bf75:#6a9fb5:#aa759f:#75b5aa:#d8d8d8:#6b6b6b:#c55555:#aac474:#feca88:#82b8c8:#c28cb8:#93d3c3:#f8f8f8"}},"timestamp":1789165652,"idle_time_limit":1.0,"title":"Elixir linear-cli demo","env":{"SHELL":"/bin/zsh"}}
+[0.028, "o", "\u001b[0m\u001b[49m\u001b[39m\u001b[27m\u001b[24m\r\u001b[K\r\n\r\n\u001b[2A\u001b7\u001b[0m\u001b[49m\u001b[39m\u001b[0m\u001b[49m\u001b[38;5;236m\u001b[0m\u001b[38;5;236m\u001b[48;5;236m\u001b[38;5;255m \u001b[0m\u001b[38;5;255m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;255m \u001b[0m\u001b[38;5;255m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;244m\u001b[0m\u001b[38;5;244m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;31m \u001b]8;;file:///Users/tj.vanderpoel/rubyists/linear-cli\u0007\u001b[1m\u001b[38;5;31m\u001b[48;5;236m\u001b[38;5;39m~\u001b[0m\u001b[38;5;39m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;31m/rubyists/\u001b[1m\u001b[38;5;31m\u001b[48;5;236m\u001b[38;5;39mlinear-cli\u001b[0m\u001b[38;5;39m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;31m\u001b]8;;\u0007\u001b[0m\u001b[38;5;31m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;31m \u001b[0m\u001b[38;5;31m\u001b[48;5;236m\u001b[49m\u001b[38;5;236m\u001b[0m\u001b[38;5;236m\u001b[49m\u001b[39m \u001b[0m\u001b[49m\u001b[39m \u001b[0m\u001b[49m\u001b[38;5;236m\u001b[0m\u001b[38;5;236m\u001b[48;5;236m\u001b[38;5;70m \u001b[0m\u001b[38;5;70m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;70m✔\u001b[0m\u001b[38;5;70m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;70m \u001b[0m\u001b[38;5;70m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;70m\u001b[0m\u001b[38;5;70m\u001b[48;5;236m\u001b[48;5;236m\u001b"]
+[0.000, "o", "[38;5;66m\u001b[38;5;244m\u001b[0m\u001b[38;5;244m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;66m 17:27:32\u001b[0m\u001b[38;5;66m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;66m \u001b[0m\u001b[38;5;66m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;66m \u001b[49m\u001b[38;5;236m\u001b[0m\u001b[38;5;236m\u001b[48;5;236m\u001b[38;5;66m\u001b[0m\u001b[38;5;66m\u001b[48;5;236m\u001b[49m\u001b[39m\r\n\u001b[0m\u001b[49m\u001b[0m\u001b[49m\u001b[39m\u001b[?2004h"]
+[2.696, "o", "\u001b[?25l"]
+[0.001, "o", "\u001b8\u001b[0m\u001b[49m\u001b[39m\u001b[27m\u001b[24m\u001b[J"]
+[0.012, "o", "\u001b]2;tj.vanderpoel@MBP-H14VYY7M75:~/rubyists/linear-cli\u0007\u001b]1;..ts/linear-cli\u0007\u001b]7;file://MBP-H14VYY7M75/Users/tj.vanderpoel/rubyists/linear-cli\u001b\\"]
+[0.000, "o", "\u001b[0m\u001b[38;5;31m\u001b[49m\u001b[39m\u001b[27m\u001b[24m\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r\u001b[0m\u001b[49m\u001b[39m\u001b[27m\u001b[24m\u001b[K"]
+[0.003, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[0m\u001b[49m\u001b[39m\r\n\u001b[A\u001b[0m\u001b[49m\u001b[38;5;236m\u001b[0m\u001b[38;5;236m\u001b[48;5;236m\u001b[38;5;255m \u001b[0m\u001b[38;5;255m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;255m \u001b[0m\u001b[38;5;255m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;244m\u001b[0m\u001b[38;5;244m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;31m \u001b]8;;file:///Users/tj.vanderpoel/rubyists/linear-cli\u0007\u001b[1m\u001b[38;5;31m\u001b[48;5;236m\u001b[38;5;39m~\u001b[0m\u001b[38;5;39m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;31m/rubyists/\u001b[1m\u001b[38;5;31m\u001b[48;5;236m\u001b[38;5;39mlinear-cli\u001b[0m\u001b[38;5;39m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;31m\u001b]8;;\u0007\u001b[0m\u001b[38;5;31m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;31m \u001b[0m\u001b[38;5;31m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;244m\u001b[0m\u001b[38;5;244m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;178m \u001b[0m\u001b[38;5;178m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;76m\u001b[0m\u001b[38;5;76m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;178m \u001b[38;5;76m bougyman/ext…gement-skill \u001b[38;5;178m+6 \u001b[38;5;39m?2\u001b[0m\u001b[38;5;39m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;178m \u001b[0m\u001b[38;5;178m\u001b[48;5;236m\u001b[49m\u001b[38;5;236m\u001b[0m\u001b[38;5;236m\u001b[49m\u001b[39m \u001b[0m\u001b[49m\u001b[39m \u001b[0m\u001b[49m\u001b[38;5;23"]
+[0.000, "o", "6"]
+[0.000, "o", "m\u001b[0m\u001b[38;5;236m\u001b[48;5;236m\u001b[38;5;70m \u001b[0m\u001b[38;5;70m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;70m✔\u001b[0m\u001b[38;5;70m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;70m \u001b[0m\u001b[38;5;70m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;70m\u001b[0m\u001b[38;5;70m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;141m\u001b[38;5;244m\u001b[0m\u001b[38;5;244m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;141m 1.20.3-otp-29\u001b[0m\u001b[38;5;141m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;141m \u001b[0m\u001b[38;5;141m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;141m \u001b[0m\u001b[38;5;141m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;141m\u001b[0m\u001b[38;5;141m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;160m\u001b[38;5;244m\u001b[0m\u001b[38;5;244m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;160m 29.0.3\u001b[0m\u001b[38;5;160m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;160m \u001b[0m\u001b[38;5;160m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;160m \u001b[0m\u001b[38;5;160m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;160m\u001b[0m\u001b[38;5;160m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;39m\u001b[38;5;244m\u001b[0m\u001b[38;5;244m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;39m 3.14.1\u001b[0m\u001b[38;5;39m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;39m \u001b[0m\u001b[38;5;39m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;39m \u001b[0m\u001b[38;5;39m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;39m\u001b[0m\u001b[38;5;39m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;66"]
+[0.000, "o", "m\u001b[38;5;244m\u001b[0m\u001b[38;5;244m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;66m 17:27:32\u001b[0m\u001b[38;5;66m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;66m \u001b[0m\u001b[38;5;66m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;66m \u001b[49m\u001b[38;5;236m\u001b[0m\u001b[38;5;236m\u001b[48;5;236m\u001b[38;5;66m\u001b[0m\u001b[38;5;66m\u001b[48;5;236m\u001b[49m\u001b[39m\r\n\u001b[0m\u001b[49m\u001b[0m\u001b[49m\u001b[39m\u001b[K"]
+[0.001, "o", "\u001b[?1h\u001b="]
+[0.000, "o", "\u001b[?25h"]
+[0.000, "o", "\u001b[?2004h\u001b[K"]
+[0.015, "o", "\u001b[?25l"]
+[0.004, "o", "\r\u001b[A\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[0m\u001b[49m\u001b[39m\r\n\u001b[A\u001b[0m\u001b[49m\u001b[38;5;236m\u001b[0m\u001b[38;5;236m\u001b[48;5;236m\u001b[38;5;255m \u001b[0m\u001b[38;5;255m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;255m \u001b[0m\u001b[38;5;255m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;244m\u001b[0m\u001b[38;5;244m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;31m \u001b]8;;file:///Users/tj.vanderpoel/rubyists/linear-cli\u0007\u001b[1m\u001b[38;5;31m\u001b[48;5;236m\u001b[38;5;39m~\u001b[0m\u001b[38;5;39m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;31m/rubyists/\u001b[1m\u001b[38;5;31m\u001b[48;5;236m\u001b[38;5;39mlinear-cli\u001b[0m\u001b[38;5;39m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;31m\u001b]8;;\u0007\u001b[0m\u001b[38;5;31m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;31m \u001b[0m\u001b[38;5;31m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;244m\u001b[0m\u001b[38;5;244m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;178m \u001b[0m\u001b[38;5;178m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;76m\u001b[0m\u001b[38;5;76m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;178m \u001b[38;5;76m bougyman/ext…gement-skill \u001b[38;5;178m+6 \u001b[38;5;39m?2\u001b[0m\u001b[38;5;39m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;178m \u001b[0m\u001b[38;5;178m\u001b[48;5;236m\u001b[49m\u001b[38;5;236m\u001b[0m\u001b[38;5;236m\u001b[49m\u001b[39m \u001b[0m\u001b[49m\u001b[39m \u001b[0m\u001b[49m\u001b[38;5;236m\u001b["]
+[0.000, "o", "0"]
+[0.000, "o", "m\u001b[38;5;236m\u001b[48;5;236m\u001b[38;5;70m \u001b[0m\u001b[38;5;70m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;70m✔\u001b[0m\u001b[38;5;70m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;70m \u001b[0m\u001b[38;5;70m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;70m\u001b[0m\u001b[38;5;70m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;141m\u001b[38;5;244m\u001b[0m\u001b[38;5;244m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;141m 1.20.3-otp-29\u001b[0m\u001b[38;5;141m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;141m \u001b[0m\u001b[38;5;141m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;141m \u001b[0m\u001b[38;5;141m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;141m\u001b[0m\u001b[38;5;141m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;160m\u001b[38;5;244m\u001b[0m\u001b[38;5;244m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;160m 29.0.3\u001b[0m\u001b[38;5;160m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;160m \u001b[0m\u001b[38;5;160m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;160m \u001b[0m\u001b[38;5;160m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;160m\u001b[0m\u001b[38;5;160m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;39m\u001b[38;5;244m\u001b[0m\u001b[38;5;244m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;39m 3.14.1\u001b[0m\u001b[38;5;39m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;39m \u001b[0m\u001b[38;5;39m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;39m \u001b[0m\u001b[38;5;39m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;39m\u001b[0m\u001b[38;5;39m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;66m\u001b[38;5"]
+[0.000, "o", ";244m\u001b[0m\u001b[38;5;244m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;66m 16.2G\u001b[0m\u001b[38;5;66m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;66m \u001b[0m\u001b[38;5;66m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;66m \u001b[0m\u001b[38;5;66m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;66m\u001b[0m\u001b[38;5;66m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;66m\u001b[38;5;244m\u001b[0m\u001b[38;5;244m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;66m 17:27:32\u001b[0m\u001b[38;5;66m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;66m \u001b[0m\u001b[38;5;66m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;66m \u001b[49m\u001b[38;5;236m\u001b[0m\u001b[38;5;236m\u001b[48;5;236m\u001b[38;5;66m\u001b[0m\u001b[38;5;66m\u001b[48;5;236m\u001b[49m\u001b[39m\r\n\u001b[0m\u001b[49m\u001b[0m\u001b[49m\u001b[39m\u001b[K"]
+[0.000, "o", "\u001b[?25h"]
+[2.873, "o", "f"]
+[0.007, "o", "\r\u001b[1m\u001b[31mf\u001b[0m\u001b[39m"]
+[0.000, "o", "\r\u001b[1m\u001b[31mf\u001b[0m\u001b[39m\u001b[90malse\u001b[39m\b\b\b\b"]
+[0.180, "o", "\r\u001b[1m\u001b[31mf\u001b[1m\u001b[31mo\u001b[0m\u001b[39m\u001b[39m \u001b[39m \u001b[39m \b\b\b"]
+[0.080, "o", "\r\u001b[1m\u001b[31mf\u001b[1m\u001b[31mo\u001b[1m\u001b[31mr\u001b[0m\u001b[39m"]
+[0.003, "o", "\r\u001b[0m\u001b[33mf\u001b[0m\u001b[33mo\u001b[0m\u001b[33mr\u001b[39m"]
+[0.865, "o", "\b\u001b[33mr\u001b[33m \u001b[39m"]
+[0.001, "o", "\b\b\u001b[33mr\u001b[39m\u001b[39m "]
+[3.006, "o", "\b"]
+[0.379, "o", "\r\u001b[33mf\u001b[33mo\u001b[39m\u001b[39m \b"]
+[0.003, "o", "\r\u001b[1m\u001b[31mf\u001b[1m\u001b[31mo\u001b[0m\u001b[39m"]
+[0.172, "o", "\r\u001b[1m\u001b[31mf\u001b[0m\u001b[39m\u001b[0m\u001b[39m \b"]
+[0.003, "o", "\r\u001b[1m\u001b[31mf\u001b[0m\u001b[39m\u001b[90malse\u001b[39m\b\b\b\b"]
+[0.158, "o", "\r\u001b[K"]
+[0.000, "o", "\u001b[K"]
+[1.244, "o", "s"]
+[0.004, "o", "\r\u001b[32ms\u001b[39m"]
+[0.000, "o", "\r\u001b[32ms\u001b[39m\u001b[90meq 1 9 | while read n; do lcls -f CRY-$n; sleep 2; done\u001b[39m\u001b[55D"]
+[0.115, "o", "\r\u001b[32ms\u001b[32me\u001b[39m"]
+[0.004, "o", "\r\u001b[1m\u001b[31ms\u001b[1m\u001b[31me\u001b[0m\u001b[39m"]
+[1.632, "o", "\u001b[39mq\u001b[39m \u001b[39m1\u001b[39m \u001b[39m9\u001b[39m \u001b[39m|\u001b[39m \u001b[39mw\u001b[39mh\u001b[39mi\u001b[39ml\u001b[39me\u001b[39m \u001b[39mr\u001b[39me\u001b[39ma\u001b[39md\u001b[39m \u001b[39mn\u001b[39m;\u001b[39m \u001b[39md\u001b[39mo\u001b[39m \u001b[39ml\u001b[39mc\u001b[39ml\u001b[39ms\u001b[39m \u001b[39m-\u001b[39mf\u001b[39m \u001b[39mC\u001b[39mR\u001b[39mY\u001b[39m-\u001b[39m$\u001b[39mn\u001b[39m;\u001b[39m \u001b[39ms\u001b[39ml\u001b[39me\u001b[39me\u001b[39mp\u001b[39m \u001b[39m2\u001b[39m;\u001b[39m \u001b[39md\u001b[39mo\u001b[39mn\u001b[39me"]
+[0.011, "o", "\r\u001b[0m\u001b[32ms\u001b[0m\u001b[32me\u001b[32mq\u001b[39m\u001b[7C\u001b[33mw\u001b[33mh\u001b[33mi\u001b[33ml\u001b[33me\u001b[39m \u001b[32mr\u001b[32me\u001b[32ma\u001b[32md\u001b[39m\u001b[4C\u001b[33md\u001b[33mo\u001b[39m \u001b[32ml\u001b[32mc\u001b[32ml\u001b[32ms\u001b[39m\u001b[12C\u001b[32ms\u001b[32ml\u001b[32me\u001b[32me\u001b[32mp\u001b[39m\u001b[4C\u001b[33md\u001b[33mo\u001b[33mn\u001b[33me\u001b[39m"]
+[1.404, "o", "\u001b[?1l\u001b>"]
+[0.000, "o", "\u001b[?2004l"]
+[0.004, "o", "\r\r\n"]
+[0.000, "o", "\u001b]2;seq 1 9 | while read n; do; lcls -f CRY-$n; sleep 2; done\u0007\u001b]1;seq\u0007"]
+[3.059, "o", "CRY-1 [Done] Welcome to Linear 👋 (bougyman@hey.com)\r\n----------------------------------------------------------\r\nHi there. Complete these issues to learn how to use Linear and discover ✨\u001b[1mProTips.\u001b[0m When you're done, delete them or move them to another team for others to view.\r\n\r\n\u001b[1;37m\u001b[1mTo start, type\u001b[0m \u001b[32mC\u001b[0m to \u001b[1mcreate your first issue.\u001b[0m\u001b[0m\r\n\r\nCreate issues from any view using \u001b[32mC\u001b[0m or by clicking the \u001b[32mNew issue\u001b[0m button.\r\n\r\n\u001b[4m\u001b[34m1189b618-97f2-4e2c-ae25-4f25467679e7\u001b[0m\u001b[2m (https://uploads.linear.app/fe63b3e2-bf87-46c0-8784-cd7d639287c8/532d146d-bcd6-4602-bf1f-83f674b70fff/1189b618-97f2-4e2c-ae25-4f25467679e7)\u001b[0m\r\n\r\nOur issue editor and comments support Markdown. You can also:\r\n\r\n ▸ @mention a teammate\r\n ▸ Drag & drop images or video (Loom & Youtube embed automatically)\r\n ▸ Use emoji ✅\r\n--- bougyman@hey.com ---\r\nNope\r\n--- bougyman@hey.com ---\r\nNah\r\n--- bougyman@hey.com ---\r\nNo\r\n--- bougyman@hey.com ---\r\nmsdkfiu\r\n"]
+[4.987, "o", "CRY-2 [Done] Try 3 ways to navigate Linear: Command line, keyboard or mouse (bougyman@hey.com)\r\n-----------------------------------------------------------------------------------------------------\r\n\u001b[32mCmd/Ctrl\u001b[0m \u001b[32mK\u001b[0m \u001b[1mis our most powerful feature.\u001b[0m\r\n\r\nUse it to search for or take any action in the app.\r\n\r\n\u001b[4m\u001b[34mcf798178-b3bc-4ae1-b24f-577d4871ae5d\u001b[0m\u001b[2m (https://uploads.linear.app/fe63b3e2-bf87-46c0-8784-cd7d639287c8/f477604b-f0d2-4ade-8a7a-571830a3b5de/cf798178-b3bc-4ae1-b24f-577d4871ae5d)\u001b[0m\r\n\r\nIf you prefer to use a mouse, right click over any issue for \u001b[1mcontextual menus\u001b[0m. They're a great way to learn the keyboard shortcuts, too.\r\n\r\n\u001b[2m[image: CleanShot 2023-07-10 at 16.03.57.png]\u001b[0m\u001b[2m (https://uploads.linear.app/fe63b3e2-bf87-46c0-8784-cd7d639287c8/11668d24-67e7-431d-9c7d-4bb5f409cae8/650d277b-cc1e-498b-aabd-8d04e8f57680)\u001b[0m\r\n\r\nIf you prefer \u001b[1mkeyboard shortcuts\u001b[0m, type \u001b[32m?\u001b[0m to view our full list.\r\n--- bougyman@hey.com ---\r\n\u001b[1;33mNo description for this"]
+[0.000, "o", " issue\u001b[0m\r\n--- bougyman@hey.com ---\r\nSome important reason\r\n--- bougyman@hey.com ---\r\nThis is a\r\n\r\nLong reason\r\n\r\n\u001b[1;33mFor closing\u001b[0m\r\n"]
+[4.989, "o", "CRY-3 [Canceled] Connect to Slack (bougyman@hey.com)\r\n-----------------------------------------------------------\r\nIf your team uses Slack, enable this integration to keep your workflow and communication in sync.\r\n\r\n ▸ See issue updates in a dedicated channel.\r\n ▸ Use the \u001b[32m/linear\u001b[0m command to create new issues from Slack messages.\r\n ▸ Sync comments between Slack and Linear.\r\n ▸ Set up personal Slack notifications.\r\n ▸ Import custom emoji 🥳 to your Linear workspace.\r\n\r\n\u001b[4m\u001b[34mConnect Slack →\u001b[0m\u001b[2m (https://linear.app/settings/integrations/slack)\u001b[0m\r\n--- bougyman@hey.com ---\r\n\u001b[1;33mHey\u001b[0m\r\n\r\n ☐ One\r\n ☐ Two\r\n--- bougyman@hey.com ---\r\nWow, this works, too?\r\n--- bougyman@hey.com ---\r\nHere's a comment\r\n\r\n\u001b[1;36mFort\u001b[0m\r\n\r\n ☐ Checklist 1 - Item 1\r\n ☐ Item 2\r\n\r\n. [ ] Checklist 2 - Item 2 . [ ] Item 2\r\n"]
+[5.060, "o", "CRY-4 [Canceled] Connect GitHub or GitLab (bougyman@hey.com)\r\n-------------------------------------------------------------------\r\nConnect your account to link issues to pull/merge requests and automate your workflow:\r\n\r\n ▸ Link Linear issues to pull requests.\r\n ▸ Automatically update an issue's status when PRs are created or merged.\r\n ▸ Connect one or multiple repos.\r\n\r\n\u001b[4m\u001b[34mConnect GitHub or GitLab →\u001b[0m\u001b[2m (https://linear.app/settings/integrations/github)\u001b[0m\r\n\r\n\u001b[1;36mSetup tips\u001b[0m\r\n\r\n\u001b[1;37mHow to link a Linear issue to a PR\u001b[0m\r\n\r\n ▸ \u001b[1mBranch name\u001b[0m (e.g. \"LIN-123\" or \"username/LIN-123\"). To quickly copy branch name for an issue to your clipboard, press \u001b[32mCmd/Ctrl\u001b[0m \u001b[32mShift\u001b[0m \u001b[32m.\u001b[0m\r\n ▸ \u001b[1mPull request title\u001b[0m (e.g. \"GitHub Workflow LIN-123\")\r\n ▸ \u001b[1mPull request description\u001b[0m (e.g. \u001b[3mFixes LIN-123, Resolves LIN-123\u001b[0m) – it will not work if entered in commits or comments.\r\n\r\n\u001b[1;37mWhen you link a Linear issue to a PR, Linear will:\u001b[0m\r\n\r\n "]
+[0.000, "o", "▸ Create a link to the PR in the Linear issue.\r\n ▸ Comment on the PR with a link back to the Linear issue.\r\n ▸ Once PR has been opened, Linear will change the status of the issue to \"In Progress\".\r\n ▸ Once PR has been merged, Linear will change the status of the issue as \"Done\".\r\n\r\n\u001b[1;37mSuggested Workflow\u001b[0m\r\n\r\n ① Select or create the issue you want to work on next.\r\n ② Open the command menu (\u001b[32mCmd\u001b[0m \u001b[32mK\u001b[0m on Mac, or \u001b[32mCtrl\u001b[0m \u001b[32mK\u001b[0m on Windows) and select \u001b[1mCopy git branch name,\u001b[0m or use the shortcut \u001b[32mCmd/Ctrl\u001b[0m \u001b[32mShift\u001b[0m \u001b[32m.\u001b[0m\r\n ③ This will copy the git branch name to your clipboard (e.g. \u001b[32musername/LIN-123-github-workflow\u001b[0m\r\n ④ Paste the branch name to your git checkout command to create a new branch: \u001b[32mgit checkout -b username/LIN-123-github-workflow\u001b[0m\r\n ⑤ Make your changes and push the branch to GitHub and open a pull request\r\n ⑥ Once the pull request is open, Linear will comment on the PR and change the issue state to \u001b[1mIn"]
+[0.000, "o", " Progress\u001b[0m*.*\r\n ⑦ Once the PR merged, Linear will change the status to Done.\r\n\r\nRead full integration instructions for \u001b[4m\u001b[34mGitHub\u001b[0m\u001b[2m (https://linear.app/docs/github)\u001b[0m and \u001b[4m\u001b[34mGitLab →\u001b[0m\u001b[2m (https://linear.app/docs/gitlab)\u001b[0m\r\n--- bougyman@hey.com ---\r\nSome Comment\r\n\r\n ☐ List\r\n ☐ Item\r\n"]
+[5.021, "o", "CRY-5 [Canceled] Customize settings (bougyman@hey.com)\r\n-------------------------------------------------------------\r\nGo to \u001b[4m\u001b[34msettings\u001b[0m\u001b[2m (https://linear.app/settings)\u001b[0m to enable features such as Roadmap, explore features and add integrations. Use \u001b[32mG\u001b[0m then \u001b[32mS\u001b[0m to get there.\r\n\r\n\u001b[2m[image: image.png]\u001b[0m\u001b[2m (https://uploads.linear.app/fe63b3e2-bf87-46c0-8784-cd7d639287c8/558fcd30-245d-4ddf-b15b-d9f18b1f4626/image.png)\u001b[0m\r\n\r\nVisit individual \u001b[4m\u001b[34mteam settings\u001b[0m\u001b[2m (https://linear.app/settings/teams/)\u001b[0m to customize features on a team level.\r\n\r\n ▸ Configure automated issue workflows for issues linked to PRs\r\n ▸ Add or edit \u001b[3mworkflow statuses\u001b[0m, and \u001b[3mlabels\u001b[0m\r\n ▸ Add team Slack notifications\r\n ▸ Create reusable \u001b[3mtemplates\u001b[0m. Use the keyboard shortcut \u001b[32mAlt\u001b[0m \u001b[32mC\u001b[0m to quickly create new issues from templates\r\n ▸ Set your \u001b[3mestimate\u001b[0m scale and enable \u001b[3mcycles\u001b[0m\r\n ▸ Configure time-saving features such as \u001b[3mAuto-cl"]
+[0.000, "o", "ose\u001b[0m and \u001b[3mAuto-archive\u001b[0m\r\n\r\n\u001b[2m[image: image.png]\u001b[0m\u001b[2m (https://uploads.linear.app/fe63b3e2-bf87-46c0-8784-cd7d639287c8/9fc1632b-893e-422c-939f-0cdc9d009b37/image.png)\u001b[0m\r\n"]
+[5.104, "o", "CRY-6 [Canceled] Use Cycles to focus work over n–weeks\r\n-------------------------------------------------------------\r\nCycles are a practice to keep up your team's momentum. They're similar to agile flavored sprints but help focus your work without tying it to a release date or feature.\r\n\r\n ▸ Customize cycles to last between 1-8 weeks.\r\n ▸ Add issues manually or automatically move issues marked \u001b[3mTodo\u001b[0m to your current cycle.\r\n ▸ Follow progress over time with cycle graphs.\r\n ▸ Any uncompleted issues roll to the next cycle automatically.\r\n\r\n\u001b[2m[image: CleanShot 2023-07-10 at 16.22.23.png]\u001b[0m\u001b[2m (https://uploads.linear.app/fe63b3e2-bf87-46c0-8784-cd7d639287c8/29272f4b-ee87-487f-9191-2f4f2fcc14a7/ad0f609e-d9d4-45f0-870c-a0b1c1f305ab)\u001b[0m\r\n\r\n\u001b[2m │ \u001b[0m\u001b[3m✨\u001b[1mProTip:\u001b[0m Quickly run meetings with the cycles sidebar. Filter issues by assignee and then ask each member to share updates and blockers. At Linear, we run 2-week cycles and follow this format at the beginning of each"]
+[0.000, "o", " cycle.\u001b[0m\r\n\r\nRead more about cycles in the \u001b[4m\u001b[34mLinear Guide →\u001b[0m\u001b[2m (https://linear.app/docs/use-cycles)\u001b[0m\r\n"]
+[4.976, "o", "CRY-7 [Done] Use Projects to organize work for features or releases (bougyman@hey.com)\r\n---------------------------------------------------------------------------------------------\r\nProjects are meant for larger initiatives you have on your roadmap.\r\n\r\n ▸ Share projects with multiple teams.\r\n ▸ Set a target date.\r\n ▸ View progress and estimate completion timeframe with graphs.\r\n\r\n\u001b[2m[image: header-projects.png]\u001b[0m\u001b[2m (https://uploads.linear.app/fe63b3e2-bf87-46c0-8784-cd7d639287c8/3e49c9a1-8794-4740-80d9-4991c6021016/cded48fc-554a-472d-a27b-90df57550087)\u001b[0m\r\n\r\nLearn more about Projects in the \u001b[4m\u001b[34mLinear Guide →\u001b[0m\u001b[2m (https://linear.app/docs/projects)\u001b[0m\r\n--- bougyman@hey.com ---\r\nBecause\r\n"]
+[5.021, "o", "CRY-8 [Canceled] Invite your teammates (bougyman@hey.com)\r\n----------------------------------------------------------------\r\nInvite others to your workspace in \u001b[4m\u001b[34msettings\u001b[0m\u001b[2m (http://linear.app/settings/members)\u001b[0m.\r\n--- bougyman@hey.com ---\r\nComment on issue\r\n"]
+[4.890, "o", "CRY-9 [Canceled] Next steps (bougyman@hey.com)\r\n-----------------------------------------------------\r\nYou're all set to use Linear.\r\n\r\n\u001b[1mWant to learn more?\u001b[0m\r\n\r\n ▸ Check out our \u001b[4m\u001b[34mdocs\u001b[0m\u001b[2m (https://linear.app/docs)\u001b[0m to dive into specific features\r\n ▸ Learn about new features in weekly\u001b[4m\u001b[34m changelogs\u001b[0m\u001b[2m (https://linear.app/changelog)\u001b[0m\r\n ▸ Read the \u001b[4m\u001b[34mLinear Method\u001b[0m\u001b[2m (https://linear.app/linear-method)\u001b[0m, our approach to software building\r\n ▸ Join our Linear Customer Slack at this \u001b[4m\u001b[34mlink\u001b[0m\u001b[2m (http://linear.app/join-slack)\u001b[0m\r\n\r\n\u001b[1mProTip: How to trial Linear on your team\u001b[0m\r\n\r\n ▸ Use our \u001b[4m\u001b[34min-app importer\u001b[0m\u001b[2m (https://linear.app/settings/import-export)\u001b[0m to add issues from Jira, Asana, Shortcut or GitHub\r\n ▸ Trial it on your team. Pick a project or sprint, invite a few coworkers, add key integrations like Slack and GitHub to experience the full workflow.\r\n\r\n\u001b[1;36mHave a question?\u001b[0m\r\n\r\nReach out to us via the Hel"]
+[0.000, "o", "p & Support link in the sidebar.\r\n"]
+[2.017, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"]
+[1.087, "o", "\u001b]2;tj.vanderpoel@MBP-H14VYY7M75:~/rubyists/linear-cli\u0007\u001b]1;..ts/linear-cli\u0007"]
+[0.003, "o", "\u001b]7;file://MBP-H14VYY7M75/Users/tj.vanderpoel/rubyists/linear-cli\u001b\\"]
+[1.040, "o", "\u001b[0m\u001b[49m\u001b[39m\u001b[K\r\n"]
+[0.004, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[0m\u001b[49m\u001b[39m\r\n\u001b[A\u001b[0m\u001b[49m\u001b[38;5;236m\u001b[0m\u001b[38;5;236m\u001b[48;5;236m\u001b[38;5;255m \u001b[0m\u001b[38;5;255m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;255m \u001b[0m\u001b[38;5;255m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;244m\u001b[0m\u001b[38;5;244m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;31m \u001b]8;;file:///Users/tj.vanderpoel/rubyists/linear-cli\u0007\u001b[1m\u001b[38;5;31m\u001b[48;5;236m\u001b[38;5;39m~\u001b[0m\u001b[38;5;39m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;31m/rubyists/\u001b[1m\u001b[38;5;31m\u001b[48;5;236m\u001b[38;5;39mlinear-cli\u001b[0m\u001b[38;5;39m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;31m\u001b]8;;\u0007\u001b[0m\u001b[38;5;31m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;31m \u001b[0m\u001b[38;5;31m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;244m\u001b[0m\u001b[38;5;244m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;178m \u001b[0m\u001b[38;5;178m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;76m\u001b[0m\u001b[38;5;76m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;178m \u001b[38;5;76m bougyman/ext…gement-skill \u001b[38;5;178m+6 \u001b[38;5;39m?2\u001b[0m\u001b[38;5;39m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;178m \u001b[0m\u001b[38;5;178m\u001b[48;5;236m\u001b[49m\u001b[38;5;236m\u001b[0m\u001b[38;5;236m\u001b[49m\u001b[39m \u001b[0m\u001b[49m\u001b[39m \u001b[0m\u001b[49m\u001b[38;5;236m\u001b[0m\u001b[38;5;23"]
+[0.000, "o", "6"]
+[0.000, "o", "m\u001b[48;5;236m\u001b[38;5;70m \u001b[0m\u001b[38;5;70m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;70m✔\u001b[0m\u001b[38;5;70m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;70m \u001b[0m\u001b[38;5;70m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;70m\u001b[0m\u001b[38;5;70m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;248m\u001b[38;5;244m\u001b[0m\u001b[38;5;244m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;248m 46s\u001b[0m\u001b[38;5;248m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;248m \u001b[0m\u001b[38;5;248m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;248m \u001b[0m\u001b[38;5;248m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;248m\u001b[0m\u001b[38;5;248m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;141m\u001b[38;5;244m\u001b[0m\u001b[38;5;244m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;141m 1.20.3-otp-29\u001b[0m\u001b[38;5;141m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;141m \u001b[0m\u001b[38;5;141m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;141m \u001b[0m\u001b[38;5;141m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;141m\u001b[0m\u001b[38;5;141m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;160m\u001b[38;5;244m\u001b[0m\u001b[38;5;244m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;160m 29.0.3\u001b[0m\u001b[38;5;160m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;160m \u001b[0m\u001b[38;5;160m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;160m \u001b[0m\u001b[38;5;160m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;160m\u001b[0m\u001b[38;5;160m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;39m\u001b[38;5;244m"]
+[0.000, "o", "\u001b[0m\u001b[38;5;244m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;39m 3.14.1\u001b[0m\u001b[38;5;39m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;39m \u001b[0m\u001b[38;5;39m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;39m \u001b[0m\u001b[38;5;39m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;39m\u001b[0m\u001b[38;5;39m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;66m\u001b[38;5;244m\u001b[0m\u001b[38;5;244m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;66m 16.1G\u001b[0m\u001b[38;5;66m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;66m \u001b[0m\u001b[38;5;66m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;66m \u001b[0m\u001b[38;5;66m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;66m\u001b[0m\u001b[38;5;66m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;66m\u001b[38;5;244m\u001b[0m\u001b[38;5;244m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;66m 17:28:34\u001b[0m\u001b[38;5;66m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;66m \u001b[0m\u001b[38;5;66m\u001b[48;5;236m\u001b[48;5;236m\u001b[38;5;66m \u001b[49m\u001b[38;5;236m\u001b[0m\u001b[38;5;236m\u001b[48;5;236m\u001b[38;5;66m\u001b[0m\u001b[38;5;66m\u001b[48;5;236m\u001b[49m\u001b[39m\r\n\u001b[0m\u001b[49m\u001b[0m\u001b[49m\u001b[39m"]
+[0.000, "o", "\u001b[K"]
+[0.000, "o", "\u001b[?1h\u001b="]
+[0.001, "o", "\u001b[?2004h\u001b[K"]
+[5.589, "o", "e"]
+[0.005, "o", "\r\u001b[32me\u001b[39m"]
+[0.001, "o", "\r\u001b[32me\u001b[39m\u001b[90mxport LINEAR_API_KEY=$(pass rubyists/linear-api)\u001b[39m\u001b[48D"]
+[0.241, "o", "\r\u001b[32me\u001b[32mx\u001b[39m"]
+[0.179, "o", "\r\u001b[32me\u001b[32mx\u001b[32mi\u001b[39m\u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[46D"]
+[0.006, "o", "\r\u001b[1m\u001b[31me\u001b[1m\u001b[31mx\u001b[1m\u001b[31mi\u001b[0m\u001b[39m"]
+[0.001, "o", "\u001b[90mt\u001b[39m\b"]
+[0.126, "o", "\b\u001b[1m\u001b[31mi\u001b[1m\u001b[31mt\u001b[0m\u001b[39m"]
+[0.004, "o", "\r\u001b[0m\u001b[32me\u001b[0m\u001b[32mx\u001b[0m\u001b[32mi\u001b[0m\u001b[32mt\u001b[39m"]
+[0.447, "o", "\u001b[?1l\u001b>"]
+[0.000, "o", "\u001b[?2004l"]
+[0.001, "o", "\r\r\n"]
+[0.000, "o", "\u001b]2;exit\u0007\u001b]1;exit\u0007"]
+[0.003, "x", "0"]
diff --git a/cinemas/elixir-lcls-demo.gif b/cinemas/elixir-lcls-demo.gif
new file mode 100644
index 0000000..b88c491
Binary files /dev/null and b/cinemas/elixir-lcls-demo.gif differ
diff --git a/documents/quality-gates-decision.adoc b/documents/quality-gates-decision.adoc
index 7ff8d07..af44d93 100644
--- a/documents/quality-gates-decision.adoc
+++ b/documents/quality-gates-decision.adoc
@@ -86,10 +86,10 @@ dependencies.
`mix ci` is an independent orchestrator. It installs dependencies, runs fast
metadata guards (PR-title and commit-range validation), invokes `mix precommit`
in full, and then performs the CI-only checks (Hex and dependency security
-audits and usage-rule drift), followed by the unfiltered app test suite. That
-final test run includes both ordinary tests and any future tests tagged
-`ci_only`; the tag is excluded only by `mix precommit`, never globally. GitHub
-Actions calls `mix ci`, not `mix precommit`.
+audits and usage-rule drift), followed by unfiltered root and app test suites.
+Those final test runs include both ordinary tests and tests tagged `ci_only`;
+the tag is excluded only by `mix precommit`, never globally. GitHub Actions
+calls `mix ci`, not `mix precommit`.
The repository's `pre-commit` hook refuses commits directly on `main` and then
invokes `mix precommit`. The subsequent `commit-msg` hook validates the commit
diff --git a/lib/mix/tasks/ci.ex b/lib/mix/tasks/ci.ex
index 9cd2518..75f0cf8 100644
--- a/lib/mix/tasks/ci.ex
+++ b/lib/mix/tasks/ci.ex
@@ -22,9 +22,11 @@ defmodule Mix.Tasks.Ci do
5. `mix hex.audit` — reject retired or vulnerable Hex packages
6. `mix deps.audit` — scan dependencies for known security advisories
7. `mix usage_rules.sync --check` — catch usage-rule drift after dep bumps
- 8. `mix test` — run the complete app test suite, including CI-classified tests
+ 8. `mix test` — run the complete root test suite, including CI-classified tests
+ 9. `mix test` — run the complete app test suite, including CI-classified tests
- Step 1 and steps 5-8 run inside `app/`. Steps 2-3 run from the repo root.
+ Step 1 and steps 5-7 and 9 run inside `app/`. Steps 2-3 and 8 run from the
+ repo root.
Step 4 expands to all of `mix precommit`'s steps in place.
Every check in `mix precommit` also runs through `mix ci`. The relationship
@@ -59,6 +61,7 @@ defmodule Mix.Tasks.Ci do
shell.("mix", ["hex.audit"], cd: "app")
shell.("mix", ["deps.audit"], cd: "app")
shell.("mix", ["usage_rules.sync", "--check"], cd: "app")
+ shell.("mix", ["test"], [])
shell.("mix", ["test"], cd: "app")
:ok
end
diff --git a/lib/mix/tasks/container.build.ex b/lib/mix/tasks/container.build.ex
index 8fa05c9..d376b82 100644
--- a/lib/mix/tasks/container.build.ex
+++ b/lib/mix/tasks/container.build.ex
@@ -13,9 +13,9 @@ defmodule Mix.Tasks.Container.Build do
container image itself via `ci/build_image.sh`. With `--push` (`-p`),
chains straight into `mix container.publish TAG` afterward.
- This is the exact flow `.github/workflows/main.yaml`'s `container` job
- runs - CI and a local `mix container.build v1.2.3 --push` do the
- identical thing, so there's one place to fix if either ever breaks.
+ This builds a local x86_64 image. Release CI reuses each native Linux
+ Burrito artifact to build x86_64 and ARM64 images, then publishes one
+ multi-architecture manifest under the release tag.
"""
use Mix.Task
diff --git a/lib/mix/tasks/precommit.ex b/lib/mix/tasks/precommit.ex
index 3dea858..d9756f2 100644
--- a/lib/mix/tasks/precommit.ex
+++ b/lib/mix/tasks/precommit.ex
@@ -18,14 +18,14 @@ defmodule Mix.Tasks.Precommit do
Steps:
1. `mix format --check-formatted` — check root project formatting
- 2. `mix test` — run the root project test suite (validator and task tests)
+ 2. `mix test --exclude ci_only` — run the root project test suite
3. `mix format --check-formatted` — check app formatting
4. `mix credo --strict` — run static analysis on the app
5. `mix test --exclude ci_only` — run the app unit test suite
Steps 1-2 run from the repo root; steps 3-5 run inside `app/`.
- App tests tagged `@moduletag :ci_only` are excluded only from this local
- gate. `mix ci` runs the complete app suite without a test-selection flag.
+ Tests tagged `@moduletag :ci_only` are excluded only from this local gate.
+ `mix ci` runs both complete test suites without a test-selection flag.
For the complete integration gate — dependency bootstrap and audits,
PR-title and commit-range validation, and CI-classified tests — use `mix ci`.
@@ -43,7 +43,7 @@ defmodule Mix.Tasks.Precommit do
@doc false
def run([], shell) do
shell.("mix", ["format", "--check-formatted"], [])
- shell.("mix", ["test"], [])
+ shell.("mix", ["test", "--exclude", "ci_only"], [])
shell.("mix", ["format", "--check-formatted"], cd: "app")
shell.("mix", ["credo", "--strict"], cd: "app")
shell.("mix", ["test", "--exclude", "ci_only"], cd: "app")
diff --git a/mise.toml b/mise.toml
index 5e5a88a..b29c54e 100644
--- a/mise.toml
+++ b/mise.toml
@@ -17,3 +17,11 @@ python = "3.14.1"
#CLAUDE_CODE_SUBAGENT_MODEL = "user.Qwen3.6-35B-A3B-ThinkingCoder"
#CLAUDE_CODE_ATTRIBUTION_HEADER = 0
#CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = 1
+
+[hooks]
+# Keep a freshly cloned checkout protected without rerunning setup after it is installed.
+enter = '''
+if [ "$(git config --get core.hooksPath 2>/dev/null)" != "git-hooks" ]; then
+ mix setup
+fi
+'''
diff --git a/oci/Containerfile b/oci/Containerfile
index e2b6ba5..63874ea 100644
--- a/oci/Containerfile
+++ b/oci/Containerfile
@@ -1,15 +1,16 @@
-# Wraps the already-built Linux x86_64 Burrito binary (see
+# Wraps the already-built Linux Burrito binary (see
# documents/phase-8-plan.adoc) rather than a separate Elixir build stage -
# one build artifact, reused for both the CLI distribution and this image.
# Build with ci/build_image.sh (which finds this via its
# Dockerfile/Containerfile/oci/Containerfile search order - build context
# is still the repo root, so the COPY paths below are unaffected by this
-# file's own location) after app/burrito_out/lc_linux_x86_64
-# already exists (`MIX_ENV=prod BURRITO_TARGET=linux_x86_64 mix release
+# file's own location) after the matching `app/burrito_out/lc_linux_*`
+# binary already exists (`MIX_ENV=prod BURRITO_TARGET=linux_* mix release
# lc`).
FROM alpine:3.22
ARG APP_VERSION
+ARG BURRITO_TARGET=linux_x86_64
LABEL org.opencontainers.image.version="${APP_VERSION}"
# ca-certificates: Linear's API is HTTPS-only, and Alpine's base image
@@ -24,7 +25,7 @@ RUN apk add --no-cache ca-certificates bash
# `exec lc ...`, resolving it via PATH - both go in /usr/local/bin/ together
# so that resolution actually works, rather than the bare `/lc` this used
# before (which isn't on PATH at all).
-COPY app/burrito_out/lc_linux_x86_64 /usr/local/bin/lc
+COPY app/burrito_out/lc_${BURRITO_TARGET} /usr/local/bin/lc
COPY bin/lcreate bin/lcls bin/lclose bin/lcomment bin/lproj /usr/local/bin/
RUN chmod +x /usr/local/bin/lc /usr/local/bin/lcreate /usr/local/bin/lcls \
/usr/local/bin/lclose /usr/local/bin/lcomment /usr/local/bin/lproj
diff --git a/test/git_hooks_test.exs b/test/git_hooks_test.exs
index b5e2afc..3df90ec 100644
--- a/test/git_hooks_test.exs
+++ b/test/git_hooks_test.exs
@@ -1,6 +1,8 @@
defmodule GitHooksTest do
use ExUnit.Case, async: true
+ @moduletag :ci_only
+
@subject_guard Path.expand("../ci/validate_conventional_subject.sh", __DIR__)
@branch_guard Path.expand("../ci/validate_commit_branch.sh", __DIR__)
@title_guard Path.expand("../ci/validate_pull_request_title.sh", __DIR__)
diff --git a/test/mix/tasks/ci_test.exs b/test/mix/tasks/ci_test.exs
index b75f0ec..44d73a5 100644
--- a/test/mix/tasks/ci_test.exs
+++ b/test/mix/tasks/ci_test.exs
@@ -20,7 +20,7 @@ defmodule Mix.Tasks.CiTest do
assert_received {:run, "./ci/validate_commit_range.sh", [], []}
# All precommit local checks are present (mix precommit ⊂ mix ci)
assert_received {:run, "mix", ["format", "--check-formatted"], []}
- assert_received {:run, "mix", ["test"], []}
+ assert_received {:run, "mix", ["test", "--exclude", "ci_only"], []}
assert_received {:run, "mix", ["format", "--check-formatted"], [cd: "app"]}
assert_received {:run, "mix", ["credo", "--strict"], [cd: "app"]}
assert_received {:run, "mix", ["test", "--exclude", "ci_only"], [cd: "app"]}
@@ -28,6 +28,7 @@ defmodule Mix.Tasks.CiTest do
assert_received {:run, "mix", ["hex.audit"], [cd: "app"]}
assert_received {:run, "mix", ["deps.audit"], [cd: "app"]}
assert_received {:run, "mix", ["usage_rules.sync", "--check"], [cd: "app"]}
+ assert_received {:run, "mix", ["test"], []}
assert_received {:run, "mix", ["test"], [cd: "app"]}
refute_received {:run, "mix", ["test", "--only", "ci_only"], _}
end
diff --git a/test/mix/tasks/precommit_test.exs b/test/mix/tasks/precommit_test.exs
index 2b06c05..b92c361 100644
--- a/test/mix/tasks/precommit_test.exs
+++ b/test/mix/tasks/precommit_test.exs
@@ -14,7 +14,7 @@ defmodule Mix.Tasks.PrecommitTest do
assert :ok = Precommit.run([], shell)
assert_received {:run, "mix", ["format", "--check-formatted"], []}
- assert_received {:run, "mix", ["test"], []}
+ assert_received {:run, "mix", ["test", "--exclude", "ci_only"], []}
assert_received {:run, "mix", ["format", "--check-formatted"], [cd: "app"]}
assert_received {:run, "mix", ["credo", "--strict"], [cd: "app"]}
assert_received {:run, "mix", ["test", "--exclude", "ci_only"], [cd: "app"]}
@@ -43,6 +43,9 @@ defmodule Mix.Tasks.PrecommitTest do
helper = Path.expand("../../../app/test/test_helper.exs", __DIR__)
refute File.read!(helper) =~ "ExUnit.start(exclude: [:ci_only])"
+
+ root_ci_only_test = Path.expand("../../git_hooks_test.exs", __DIR__)
+ assert File.read!(root_ci_only_test) =~ "@moduletag :ci_only"
end
test "rejects arguments" do