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