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
94 changes: 94 additions & 0 deletions .github/workflows/burrito-release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
---
name: Build and publish release artifacts

on: # yamllint disable-line rule:truthy
workflow_dispatch:
inputs:
tag_name:
description: Release tag to attach binaries to
required: true
type: string
workflow_call:
inputs:
tag_name:
description: Release tag to attach binaries to
required: true
type: string

permissions:
contents: write
packages: write

jobs:
burrito:
name: Build Burrito binaries
runs-on: ubuntu-latest
defaults:
run:
working-directory: app
steps:
-
uses: actions/checkout@v7
-
uses: erlef/setup-beam@v1
with:
otp-version: "29.0.3"
elixir-version: "1.20.3"
-
# Version pinned to what Burrito 1.6.0 actually requires - its own
# README says 0.15.2, but the version check enforces 0.16.0. See
# documents/phase-8-plan.adoc.
uses: mlugg/setup-zig@v2.2.1
with:
version: "0.16.0"
-
name: Install p7zip (Burrito needs 7z for Windows targets)
run: sudo apt-get update && sudo apt-get install -y p7zip-full
-
run: mix deps.get
-
name: Build all Burrito targets
run: MIX_ENV=prod mix release linear_cli
-
name: Upload binaries to the GitHub release
env:
GH_TOKEN: ${{ github.token }}
run: gh release upload "${{ inputs.tag_name }}" app/burrito_out/* --clobber
working-directory: .

container:
name: Build and publish container image
runs-on: ubuntu-latest
steps:
-
uses: actions/checkout@v7
-
uses: erlef/setup-beam@v1
with:
otp-version: "29.0.3"
elixir-version: "1.20.3"
-
# Same Burrito build as the `burrito` 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` succeeds.
uses: mlugg/setup-zig@v2.2.1
with:
version: "0.16.0"
-
run: mix deps.get
working-directory: app
-
name: Build the linux_x86_64 target (container's payload)
run: MIX_ENV=prod BURRITO_TARGET=linux_x86_64 mix release linear_cli
working-directory: app
-
name: Build the image
env:
APP_VERSION: ${{ inputs.tag_name }}
run: ./ci/build_image.sh "${{ inputs.tag_name }}"
-
name: Publish the image
env:
GITHUB_TOKEN: ${{ github.token }}
GITHUB_ACTOR: ${{ github.actor }}
run: ./ci/publish.sh "${{ inputs.tag_name }}"
56 changes: 56 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
name: CI

on: # yamllint disable-line rule:truthy
workflow_dispatch:
workflow_call:
pull_request:

jobs:
test:
name: Test
runs-on: ubuntu-latest
defaults:
run:
working-directory: app
steps:
-
uses: actions/checkout@v7
-
uses: erlef/setup-beam@v1
with:
otp-version: "29.0.3"
elixir-version: "1.20.3"
-
name: Cache deps/build
uses: actions/cache@v6
with:
path: |
app/deps
app/_build
key: ${{ runner.os }}-mix-${{ hashFiles('app/mix.lock') }}
-
run: mix deps.get
-
run: mix format --check-formatted
-
run: mix test

conventional_commits:
name: Validate Commit Subjects
runs-on: ubuntu-latest
steps:
-
uses: actions/checkout@v7
with:
fetch-depth: 0
# pull_request's default ref is the synthetic refs/pull/N/merge test-merge
# commit (subject "Merge <sha> into <sha>"), not the PR branch tip - check
# out the real head SHA instead so that commit is never in the validated
# range. Falls back to github.sha for non-PR triggers (workflow_dispatch/
# workflow_call), where github.event.pull_request is unset.
ref: ${{ github.event.pull_request.head.sha || github.sha }}
-
env:
FETCH_BASE_REF: "true"
run: ./ci/conventional_commits.sh
42 changes: 42 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
name: Release

on: # yamllint disable-line rule:truthy
push:
branches:
- main
workflow_dispatch:

jobs:
validate:
name: Validations
uses: ./.github/workflows/ci.yaml

release:
needs: [validate]
name: Create a release
runs-on: ubuntu-latest
outputs:
release_created: ${{ steps.release.outputs.release_created }}
tag_name: ${{ steps.release.outputs.tag_name }}
steps:
-
uses: actions/checkout@v7
with:
fetch-tags: true
-
uses: googleapis/release-please-action@v5
id: release
with:
config-file: .release-please-config.json
manifest-file: .release-please-manifest.json
token: ${{ secrets.RELEASE_PLEASE_TOKEN }}

publish:
if: needs.release.outputs.release_created == 'true'
needs: release
name: Build and publish release artifacts
uses: ./.github/workflows/burrito-release.yaml
with:
tag_name: ${{ needs.release.outputs.tag_name }}
secrets: inherit
24 changes: 24 additions & 0 deletions .release-please-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"packages": {
".": {
"changelog-path": "CHANGELOG.md",
"release-type": "simple",
"bump-minor-pre-major": true,
"bump-patch-for-minor-pre-major": true,
"draft": false,
"prerelease": false,
"version-file": ".version.txt",
"extra-files": [
{
"type": "generic",
"path": "app/mix.exs"
}
],
"exclude-paths": [
".release-please-manifest.json",
".version.txt"
]
}
},
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json"
}
3 changes: 3 additions & 0 deletions .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
".": "0.1.0"
}
1 change: 1 addition & 0 deletions .version.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.1.0
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and best practices for agents to follow.
own documents instead)
- Phase 6 Plan: documents/phase-6-plan.adoc
- Phase 7 Plan: documents/phase-7-plan.adoc
- Phase 8 Plan: documents/phase-8-plan.adoc

## Standards

Expand Down
3 changes: 3 additions & 0 deletions app/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ app-*.tar
# the real distribution target is the Burrito release, see Phase 8).
/linear_cli

# Burrito's per-target output binaries (mix release, see documents/phase-8-plan.adoc).
/burrito_out/

# Local dev SQLite db for Oban (daemon run mode only).
/oban_dev.db*

27 changes: 26 additions & 1 deletion app/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,44 @@ defmodule LinearCli.MixProject do
def project do
[
app: :linear_cli,
# x-release-please-start-version
version: "0.1.0",
# x-release-please-end
elixir: "~> 1.20",
start_permanent: Mix.env() == :prod,
deps: deps(),
consolidate_protocols: Mix.env() != :dev,
usage_rules: usage_rules(),
escript: escript()
escript: escript(),
releases: releases()
]
end

defp escript do
[main_module: LinearCli.CLI]
end

# Burrito-wrapped release, both the interactive CLI and (with
# LINEAR_CLI_DAEMON=true) the daemon - one binary, not two build
# artifacts. Targets and their host-compatibility verified against
# Burrito's own docs/source, and exqlite's NIF cross-compilation verified
# empirically (built+ran the linux_x86_64 target under podman/QEMU) - see
# documents/phase-8-plan.adoc. macOS Intel intentionally not targeted.
defp releases do
[
linear_cli: [
steps: [:assemble, &Burrito.wrap/1],
burrito: [
targets: [
macos_aarch64: [os: :darwin, cpu: :aarch64],
linux_x86_64: [os: :linux, cpu: :x86_64],
windows_x86_64: [os: :windows, cpu: :x86_64]
]
]
]
]
end

defp usage_rules do
[
file: "../AGENTS.md",
Expand Down Expand Up @@ -46,6 +70,7 @@ defmodule LinearCli.MixProject do
{:oban, "~> 2.23"},
{:ecto_sqlite3, "~> 0.9"},
{:postgrex, "~> 0.22"},
{:burrito, "~> 1.6"},
{:sourceror, "~> 1.8", only: [:dev, :test]},
{:ash, "~> 3.0"},
{:igniter, "~> 0.6", only: [:dev, :test]}
Expand Down
2 changes: 2 additions & 0 deletions app/mix.lock
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
%{
"ash": {:hex, :ash, "3.31.0", "9ccd7d5ff00a5329a33786063e4a3040681b61d736730cd7aa3f24e5843c1d4f", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.14", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 1.0", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.6.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.3", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7518ecf694e1b423478ddaa5baf713e9d38448df6873f3be6316f758bc6ab68d"},
"burrito": {:hex, :burrito, "1.6.0", "7af0a75f11680e8a6e9c01370c9af51cb9d0e15b3226eddf4f438dbc68570520", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:req, ">= 0.5.0", [hex: :req, repo: "hexpm", optional: false]}, {:typed_struct, "~> 0.2.0 or ~> 0.3.0", [hex: :typed_struct, repo: "hexpm", optional: false]}], "hexpm", "e636a00b032c45a69ff755d9fc53fa5fdc9e1d21bdbd229075fe4a15b05355fe"},
"cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
"crux": {:hex, :crux, "0.1.4", "1fa21f5ca886d3498f83871a3ef19379b80abf8cfcf2ed32007e80279e714f1e", [:mix], [{:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: true]}], "hexpm", "ff7d880cf732d82360aa81e439b8d4831cd30768061c9233f90c97e10162b9c0"},
"db_connection": {:hex, :db_connection, "2.10.2", "ae391e803a5adff104da913c2fc1c0c14a37f8b10001dcef568796e1fb7bf95c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "510b14482330f1af6490a2fa0efd8d4f1435d1529b165647df22ac0f2df0fa93"},
Expand Down Expand Up @@ -43,6 +44,7 @@
"stream_data": {:hex, :stream_data, "1.4.0", "026f929db613aabea6208012ae9b8970d3fd5f88b3bdf26831bc536f98c42036", [:mix], [], "hexpm", "2b0ee3a340dcce1c8cf6302a763ee757d1e01c54d6e16d9069062509d68b1dc9"},
"telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"},
"text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"},
"typed_struct": {:hex, :typed_struct, "0.3.0", "939789e3c1dca39d7170c87f729127469d1315dcf99fee8e152bb774b17e7ff7", [:mix], [], "hexpm", "c50bd5c3a61fe4e198a8504f939be3d3c85903b382bde4865579bc23111d1b6d"},
"usage_rules": {:hex, :usage_rules, "1.2.7", "aaacfc9eda3b33d37703ed2321f32ad89845282513168f8592a1826eeabf50a7", [:mix], [{:igniter, ">= 0.6.6 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "8601999d754974f361f1fd816f0747dd7ea39aa04b1bf6244d158a9616be204e"},
"yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"},
"yaml_elixir": {:hex, :yaml_elixir, "2.12.2", "9dd1330fb4cd9a36a7b0f502e5b12486eff632792ee4a5f0eba52a4d4ec32c9c", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "e7c1b10122f973e6558462d51c39026ba0e14afbc6745318e990ea82cfe9e159"},
Expand Down
11 changes: 11 additions & 0 deletions app/test/linear_cli/git_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@ defmodule LinearCli.GitTest do
File.mkdir_p!(origin_path)
{_output, 0} = System.cmd("git", ["init", "--bare", "-q"], cd: origin_path)

# `git init --bare`'s HEAD symref follows the runner's ambient
# init.defaultBranch config (falling back to git's legacy "master" if
# unset) - since this repo only ever gets a "main" branch pushed to it,
# that can leave HEAD dangling at a "master" that never exists, and
# `git ls-remote --symref` then reports nothing for it. Set it explicitly
# (the same thing a real git host does when you configure a repo's
# default branch) so this doesn't depend on the runner's global config -
# this is what made `default_branch/0`'s test pass locally but fail in CI.
{_output, 0} =
System.cmd("git", ["symbolic-ref", "HEAD", "refs/heads/main"], cd: origin_path)

repo_path = tmp_path("repo")
File.mkdir_p!(repo_path)
init_repo!(repo_path)
Expand Down
9 changes: 6 additions & 3 deletions ci/build_image.sh
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ else
fi

service=$(basename "$owner_and_repo" .git)
# Rebuilt from owner_and_repo (path only), never $repo_url directly - a
# credential-bearing remote (https://TOKEN@github.com/...) would otherwise
# get baked verbatim into this label, and from there into any pushed image.
image_url="https://github.com/${owner_and_repo%.git}"
created=$(date --utc --iso-8601=seconds 2>/dev/null || gdate --utc --iso-8601=seconds) ||
die 8 "Could not determine image creation timestamp"
full_tag=$IMAGE_NAME:$tag
Expand All @@ -168,10 +172,9 @@ debug "Building $full_tag with $runtime from $CONTAINERFILE"
--label org.opencontainers.image.description="Image for $service" \
--label org.opencontainers.image.licenses="$LICENSE" \
--label org.opencontainers.image.revision="$revision" \
--label org.opencontainers.image.url="$repo_url" \
--label org.opencontainers.image.url="$image_url" \
--label org.opencontainers.image.title="$IMAGE_NAME" \
--label org.opencontainers.image.source="Generated by wallet ci/build_image.sh (${USER:-unknown_user}@${HOSTNAME:-unknown_host})" \
--label org.opencontainers.image.version="$tag" \
--label org.opencontainers.image.source="$image_url/tree/$revision" \
--label shortref="$shortref" \
--build-arg APP_VERSION="$APP_VERSION" \
-f "$CONTAINERFILE" \
Expand Down
Loading