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
4 changes: 3 additions & 1 deletion .ai/prompts/implement.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ necessary for release-please to pick up our squash merge commits to main.
git checkout -b {{ issue.identifier | lower }}-<short-description>
```
4. Implement the changes with clean, logical commits.
5. Run the full quality suite:
5. Run the local quality gate (fast, no network required after deps are installed):
- mix precommit
For full CI-equivalent assurance before opening a PR (runs audits and
validation that require network access), use `mix ci` instead.
6. Fix any failures before proceeding.
7. Push the branch and create a PR:
```
Expand Down
18 changes: 9 additions & 9 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
-
uses: actions/checkout@v7
with:
# mix precommit calls ci/validate_commit_range.sh, which validates
# mix ci calls ci/validate_commit_range.sh, which validates
# every commit since the merge base. A full-history checkout lets the
# script resolve that base locally without a second network fetch.
fetch-depth: 0
Expand Down Expand Up @@ -80,12 +80,13 @@ jobs:
app/_build
key: ${{ runner.os }}-mix-${{ hashFiles('app/mix.lock') }}
-
# mix precommit runs the full 11-step gate:
# 1. ci/validate_pull_request_title.sh — PR title (when required)
# 2. ci/validate_commit_range.sh — all commit subjects in the range
# 3-4. Root format + test (the repo-management tooling itself)
# 5-11. App deps.get, hex.audit, deps.audit, format, credo,
# usage_rules.sync, and test.
# mix ci is the complete integration gate:
# 1. App deps.get (bootstrap)
# 2. ci/validate_pull_request_title.sh — PR title (when required)
# 3. ci/validate_commit_range.sh — all commit subjects in the range
# 4. mix precommit — root format + test, app format + credo + test --exclude ci_only
# 5-7. App hex.audit, deps.audit, usage_rules.sync
# 8. App mix test (the unfiltered suite, including ci_only tests)
# BASE_REF carries the exact comparison base SHA so the commit-range
# validator never has to guess. For pull_request events it is the exact
# base SHA; for push events it is github.event.before; for
Expand All @@ -105,7 +106,7 @@ jobs:
PULL_REQUEST_TITLE: >-
${{ inputs.pull_request_title != '' && inputs.pull_request_title
|| github.event.pull_request.title }}
run: mix precommit
run: mix ci
working-directory: .

burrito_changes:
Expand Down Expand Up @@ -197,4 +198,3 @@ jobs:
name: Test the packaged binary across users
timeout-minutes: 2
run: ../ci/test_burrito_shared_loader.sh ./burrito_out/lc_linux_x86_64

10 changes: 10 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,19 @@ not need a structural-doc update.
change** whenever an Ash resource, action, code interface, association
attribute, or shared helper is added, removed, renamed, or materially
changed.
- GitHub Update-branch validation decision:
documents/github-update-branch-validation-decision.adoc — canonical
security rationale and required regression behavior for the narrowly scoped
exception that permits GitHub's trusted **Update branch** merge commits.
**Must be read and preserved** whenever changing commit-subject validation,
`ci/validate_commit_range.sh`, Git hooks, or CI quality-gate orchestration.

## Standards

- Bash error handling: never use `set -e`, `set -u`, or `set -o pipefail`
(including combined forms such as `set -euo pipefail`). Handle every command
that can fail with an explicit status check, diagnostic, and exit path. See
`documents/style/bash.adoc` for the required Bash style.
- Conventional Commits: app/usage-rules.md — enforced by the `commit-msg`
and `pre-push` hooks at `git-hooks/` (run `mix setup` once per clone to
activate them).
Expand Down
16 changes: 14 additions & 2 deletions Readme.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,8 @@ $ lproj list --mine
== Development

First, activate the repo's git hooks (enforces conventional-commit subjects
on every commit, and again on every commit about to be pushed):
on every commit and push, and runs a Hex dependency security audit before a
push):

[source,sh]
----
Expand All @@ -479,13 +480,24 @@ $ mix lc whoami
$ mix lc issue list --output json
----

The project uses ExUnit and `mix format`. Run the full quality gate with:
The project uses ExUnit and `mix format`. During development, run the fast
local gate repeatedly (format, static analysis, unit tests — no network
required after a one-time `mix deps.get` inside `app/`):

[source,sh]
----
$ mix precommit
----

Before opening a pull request, or to reproduce CI locally, run the complete
integration gate (includes dependency audits, PR-title and commit-range
validation, and any CI-classified tests):

[source,sh]
----
$ mix ci
----

To run only the app test suite or format check directly:

[source,sh]
Expand Down
2 changes: 2 additions & 0 deletions app/test/test_helper.exs
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,6 @@ Application.put_env(:elixir, :ansi_enabled, true)
# creates a profile.
Application.fetch_env!(:linear_cli, :profiles_db_path) |> File.rm()

# Test selection belongs to the invoking quality gate. `mix precommit` excludes
# `:ci_only`; `mix ci` is unfiltered so it runs the complete suite.
ExUnit.start()
5 changes: 3 additions & 2 deletions app/usage-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@
- Enforced locally by the `commit-msg` hook at `git-hooks/commit-msg` (each
commit's own subject, via `ci/validate_conventional_subject.sh`) and the
`pre-push` hook at `git-hooks/pre-push` (every non-deletion ref update,
via `ci/validate_push_refs.sh`) — run `mix setup` once per clone to
activate both.
via `ci/validate_push_refs.sh`, followed by `ci/hex-audit.sh`) — run
`mix setup` once per clone to activate both. The Hex audit needs network
access and prevents a push when it finds a vulnerable or retired package.
- Enforced in CI across a whole PR's commit range by
`ci/validate_commit_range.sh` (skips GitHub's own auto-generated
update-branch merge commits).
Expand Down
31 changes: 31 additions & 0 deletions ci/hex-audit.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
# Runs Hex's dependency security audit from the app Mix project. This is used
# by the pre-push hook as well as the CI quality gate.

if ! repo_top=$(git rev-parse --show-toplevel 2>&1)
then
printf 'ERROR: unable to resolve repository root: %s\n' "$repo_top" >&2
exit 1
fi

app_dir="$repo_top/app"

if [ ! -d "$app_dir" ]
then
printf 'ERROR: app Mix project not found: %s\n' "$app_dir" >&2
exit 1
fi

if ! cd "$app_dir"
then
printf 'ERROR: unable to change to app Mix project: %s\n' "$app_dir" >&2
exit 1
fi

if ! command -v mix >/dev/null 2>&1
then
printf 'ERROR: mix is not available on PATH\n' >&2
exit 1
fi

exec mix hex.audit
74 changes: 74 additions & 0 deletions ci/patch_musl_nifs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/bin/sh
# Runs inside Alpine to patch the supplied musl NIFs and bundle their libgcc
# runtimes. Arguments are alternating NIF paths and replacement libgcc names.

die() {
printf 'ERROR: %s\n' "$*" >&2
exit 1
}

contains_line() {
value=$1
expected=$2

if printf '%s\n' "$value" | grep -Fxq "$expected"
then
return 0
fi

return 1
}

if [ "$#" -eq 0 ]
then
die "expected one or more NIF/libgcc-name pairs"
fi

apk add --no-cache libgcc patchelf || die "unable to install Alpine patching tools"

while [ "$#" -gt 0 ]
do
nif=$1
libgcc_name=$2
shift 2

bundled_libgcc="${nif%/*}/$libgcc_name"
install -m 0755 /usr/lib/libgcc_s.so.1 "$bundled_libgcc" || die "unable to install libgcc for $nif"

# libc.so is the musl dependency name; glibc NIFs require libc.so.6.
# Check this before patching so a host artifact cannot slip through.
needed=$(patchelf --print-needed "$nif") || die "unable to inspect dependencies for $nif"

if ! contains_line "$needed" libc.so
then
die "musl NIF does not depend on libc.so: $nif"
fi

# Set RUNPATH before growing DT_NEEDED. With patchelf 0.18, doing these
# two mutations in the opposite order can produce a loadable NIF that
# crashes on its first call.
patchelf --set-rpath '$ORIGIN' "$nif" || die "unable to set RUNPATH for $nif"
needed=$(patchelf --print-needed "$nif") || die "unable to inspect libgcc dependency for $nif"

if contains_line "$needed" libgcc_s.so.1
then
patchelf --replace-needed libgcc_s.so.1 "$libgcc_name" "$nif" || die "unable to replace libgcc dependency for $nif"
elif ! contains_line "$needed" "$libgcc_name"
then
die "musl NIF has no expected libgcc dependency: $nif"
fi

needed=$(patchelf --print-needed "$nif") || die "unable to verify libgcc dependency for $nif"

if ! contains_line "$needed" "$libgcc_name"
then
die "musl NIF did not retain renamed libgcc dependency: $nif"
fi

rpath=$(patchelf --print-rpath "$nif") || die "unable to inspect RUNPATH for $nif"

if [ "$rpath" != '$ORIGIN' ]
then
die "musl NIF RUNPATH is not \$ORIGIN: $nif"
fi
done
Loading
Loading