diff --git a/.github/workflows/burrito-release.yaml b/.github/workflows/burrito-release.yaml new file mode 100644 index 0000000..c03c41a --- /dev/null +++ b/.github/workflows/burrito-release.yaml @@ -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 }}" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..5be5481 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -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 into "), 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 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..77912a6 --- /dev/null +++ b/.github/workflows/release.yaml @@ -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 diff --git a/.release-please-config.json b/.release-please-config.json new file mode 100644 index 0000000..5b6fc0d --- /dev/null +++ b/.release-please-config.json @@ -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" +} diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..466df71 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.1.0" +} diff --git a/.version.txt b/.version.txt new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/.version.txt @@ -0,0 +1 @@ +0.1.0 diff --git a/AGENTS.md b/AGENTS.md index 8f07aa9..64a778c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/app/.gitignore b/app/.gitignore index d29b4a7..2019493 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -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* diff --git a/app/mix.exs b/app/mix.exs index ce085c4..ac7b7cf 100644 --- a/app/mix.exs +++ b/app/mix.exs @@ -4,13 +4,16 @@ 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 @@ -18,6 +21,27 @@ defmodule LinearCli.MixProject 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", @@ -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]} diff --git a/app/mix.lock b/app/mix.lock index a2a7021..033a179 100644 --- a/app/mix.lock +++ b/app/mix.lock @@ -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"}, @@ -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"}, diff --git a/app/test/linear_cli/git_test.exs b/app/test/linear_cli/git_test.exs index b7c467a..4cec519 100644 --- a/app/test/linear_cli/git_test.exs +++ b/app/test/linear_cli/git_test.exs @@ -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) diff --git a/ci/build_image.sh b/ci/build_image.sh index 285df3c..a49cc40 100755 --- a/ci/build_image.sh +++ b/ci/build_image.sh @@ -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 @@ -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" \ diff --git a/documents/phase-8-plan.adoc b/documents/phase-8-plan.adoc new file mode 100644 index 0000000..4cc361b --- /dev/null +++ b/documents/phase-8-plan.adoc @@ -0,0 +1,133 @@ += {my-title} +Tj Vanderpoel (bougyman) +:revdate: Aug 07, 2026 +:my-title: Phase 8 plan: packaging, releasing, and CI +:icons: font +:env-github: +ifdef::env-github[] +:tip-caption: :bulb: +:note-caption: :information_source: +:important-caption: :heavy_exclamation_mark: +:caution-caption: :fire: +:warning-caption: :warning: +endif::[] +:toc: + +== Goal + +Make this thing actually releasable: cross-platform standalone binaries via +`Burrito`, a container image for the daemon, and `release-please`-driven +versioning - matching the conventions already used by `linear-cli` (the +Ruby sibling) and the user's other FOSS projects. + +== Decisions (reached in full agreement before implementation) + +1. *Burrito wraps the whole app* - one binary is both the interactive CLI + and (with `LINEAR_CLI_DAEMON=true`) the daemon. Not two separate build + artifacts. +2. *Targets*: macOS Apple Silicon (`aarch64-darwin`), Linux x86_64, Windows + x86_64. No macOS Intel. +3. *CI build shape*: a single `ubuntu-latest` job cross-compiles all three + targets via Zig - not a matrix of per-OS runners. Burrito's own docs + confirm a Linux host can target all three; a matrix would just add CI + minutes with no benefit here. +4. *Versioning*: `release-please`, mirroring `linear-cli`'s own setup - + `release-type: simple`, `.version.txt` as the tracked version file, + `app/mix.exs`'s `version:` field updated via a `generic` extra-file with + `x-release-please-start-version`/`x-release-please-end` markers, + `CHANGELOG.md` auto-maintained (fresh - no history to backfill). +5. *Container image*: also in scope. The daemon's container just wraps the + already-built Linux x86_64 Burrito binary (`FROM alpine`, `COPY` the + binary, `ENV LINEAR_CLI_DAEMON=true`, `ENTRYPOINT`) rather than a + separate Elixir build stage - one build artifact, reused. Published to + `ghcr.io` via the existing (currently unwired) `ci/build_image.sh` / + `ci/publish.sh` scripts from the initial scaffold. +6. *CI gate before release*: add `.github/workflows/ci.yaml` now (`mix + test`, `mix format --check-formatted`, `ci/conventional_commits.sh`) - + not deferred to Phase 9. A release pipeline with no test gate in front + of it is a real gap. +7. *Workflow shape* (mirrors `linear-cli`): `ci.yaml` (validate) -> + `release.yaml` (on push to main, runs `release-please-action`; if + `release_created`, calls `burrito-release.yaml`) -> `burrito-release.yaml` + (build all 3 targets + the container image, attach binaries to the GH + release via `gh release upload`, push the container to `ghcr.io`). +8. *`RELEASE_PLEASE_TOKEN` secret*: must exist in the repo's GitHub + settings (a PAT, not the default `GITHUB_TOKEN` - which can't trigger + other workflows from its own commits/PRs). This is a one-time manual + setup step on GitHub.com the user needs to do themselves; not something + scriptable from here. + +== Verified: Burrito + `exqlite`'s NIF survives cross-compilation + +This was the one real open question, and it was checked empirically, not +assumed: + +* Burrito's own docs only describe automatic per-target NIF recompilation + for `elixir_make`-based NIFs. `exqlite` (via `ecto_sqlite3`) was a + question mark - would its NIF get built for the *build host* instead of + the *target*, crashing the app at boot on other platforms? +* Built a real Burrito release targeting `linux_x86_64` from this macOS + arm64 host. The build log showed Burrito explicitly detecting and cross- + compiling it: `--> Going to recompile NIF for cross-build: exqlite -> + x86_64-linux` ... `--> Successfully re-built exqlite for x86_64-linux!` + (a real `CC`/`LD` invocation, not a precompiled-binary fetch). +* Ran the produced binary inside a `podman` container (`--platform + linux/amd64`, QEMU-emulated) with `LINEAR_CLI_DAEMON=true`. It booted, + stayed alive, and - crucially - actually created + `~/.linear_cli/oban.db` on disk, proving the NIF loaded and SQLite + connections genuinely worked on the cross-compiled target. (It later hit + Oban's well-known "database is locked" contention from booting a 10- + connection pool against an unmigrated file - the same benign transient + seen during Phase 7's native testing, unrelated to Burrito.) +* Along the way: Burrito 1.6.0 actually requires Zig `0.16.0`, not the + `0.15.2` its own README states (verified by hitting the version-check + error directly) - `mlugg/setup-zig@v2.2.1` with `version: '0.16.0'` in CI. +* Also: `custom_erts:` (used briefly to route around a not-yet-catalogued + local OTP patch version) silently skips Burrito's musl-runtime-embedding + step for Linux targets (verified by reading + `deps/burrito/lib/steps/fetch/fetch_musl.ex` - it pattern-matches on + `erts_source: {:precompiled, _}` specifically). Not a concern for the + real pipeline, which pins a specific, catalogued OTP version and never + needs the override - but worth remembering if it ever comes up again. + +== New building blocks needed + +* `app/mix.exs`: add `{:burrito, "~> 1.6"}`, a real `releases/0` (not the + throwaway exploratory one used for verification) with the 3 real + targets, wrap the `version:` field in generic extra-file markers. Keep + `escript()` too, as the fast local-dev build path - Burrito is for + distribution, escript is for iterating. +* `.version.txt`, `.release-please-manifest.json`, + `.release-please-config.json` at repo root, matching `linear-cli`'s + shape. (`ci/build_image.sh` already expects `.version.txt` to exist at + this exact path - confirmed by reading its `APP_VERSION` default.) +* `Containerfile` at repo root (`ci/build_image.sh` searches + `Dockerfile`/`Containerfile`/`oci/Containerfile`, build context = repo + root) - wraps the Linux Burrito binary, `alpine` base for CA certs + (Linear's API is HTTPS) rather than `scratch`. +* `.github/workflows/ci.yaml`: `mix test`, `mix format + --check-formatted`, `ci/conventional_commits.sh` (already written, + unwired). +* `.github/workflows/release.yaml`: `release-please-action@v5`, gated on + `ci.yaml` passing, calls `burrito-release.yaml` on `release_created`. +* `.github/workflows/burrito-release.yaml`: `erlef/setup-beam@v1` (OTP + 29.0.3, Elixir 1.20.3 - the exact combo verified above; Burrito's + precompiled-ERTS catalog lags bleeding-edge patches, so pin to one it + actually has), `mlugg/setup-zig@v2.2.1`, build all 3 targets, `gh + release upload` the binaries, build+push the container via the existing + `ci/build_image.sh` / `ci/publish.sh`. + +== Sequencing + +1. `.version.txt` + release-please config/manifest + `mix.exs` version + markers - no behavior change, just wiring, verify `mix compile` still + works. +2. Real Burrito `releases/0` in `mix.exs` (the 3 real targets) - verify + locally by rebuilding at least the Linux target again. +3. `Containerfile` - verify locally with `ci/build_image.sh` (podman). +4. `.github/workflows/ci.yaml` - can dry-run the shell scripts locally + even without a real GH Actions run. +5. `.github/workflows/release.yaml` + `burrito-release.yaml` - reviewed + for correctness against verified action versions/inputs, but the actual + release-please PR / GH release flow can only be truly verified by an + real push to `main` on GitHub, after `RELEASE_PLEASE_TOKEN` is set up. diff --git a/oci/Containerfile b/oci/Containerfile new file mode 100644 index 0000000..a64bb35 --- /dev/null +++ b/oci/Containerfile @@ -0,0 +1,23 @@ +# Wraps the already-built Linux x86_64 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 path below is unaffected by this +# file's own location) after app/burrito_out/linear_cli_linux_x86_64 +# already exists (`MIX_ENV=prod BURRITO_TARGET=linux_x86_64 mix release +# linear_cli`). +FROM alpine:3.22 + +ARG APP_VERSION +LABEL org.opencontainers.image.version="${APP_VERSION}" + +# ca-certificates: Linear's API is HTTPS-only, and Alpine's base image +# doesn't bundle a CA trust store. +RUN apk add --no-cache ca-certificates + +COPY app/burrito_out/linear_cli_linux_x86_64 /linear_cli +RUN chmod +x /linear_cli + +ENV LINEAR_CLI_DAEMON=true +ENTRYPOINT ["/linear_cli"]