Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions .ai/skills/linear/SKILL.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions .claude/skills
1 change: 1 addition & 0 deletions .codex/skills
108 changes: 54 additions & 54 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -350,71 +350,44 @@ 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.
# `lc version` boots the OTP app and performs hidden syntax-highlighted
# 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: |
Expand All @@ -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"
-
Expand All @@ -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
Expand All @@ -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
Expand Down
39 changes: 39 additions & 0 deletions Readme-docinfo-footer.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<style>
.cast-preview a { cursor: zoom-in; }
.cast-lightbox {
max-width: 96vw;
max-height: 96vh;
padding: 1rem;
background: #1d1f2b;
border: 0;
border-radius: .5rem;
}
.cast-lightbox::backdrop { background: rgb(0 0 0 / 80%); }
.cast-lightbox img {
display: block;
max-width: 92vw;
max-height: 82vh;
}
.cast-lightbox form { margin: 0 0 .75rem; text-align: right; }
.cast-lightbox button { cursor: pointer; }
</style>
<script>
(() => {
const previews = document.querySelectorAll('.cast-preview a');
if (!previews.length || !HTMLDialogElement.prototype.showModal) return;

const dialog = document.createElement('dialog');
dialog.className = 'cast-lightbox';
dialog.innerHTML = '<form method="dialog"><button>Close</button></form><img>';
const image = dialog.querySelector('img');
document.body.append(dialog);

previews.forEach((preview) => preview.addEventListener('click', (event) => {
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
event.preventDefault();
image.src = preview.href;
image.alt = preview.querySelector('img').alt;
dialog.showModal();
}));
})();
</script>
Loading
Loading