Skip to content

chore(main): release 1.8.0 (#118) #115

chore(main): release 1.8.0 (#118)

chore(main): release 1.8.0 (#118) #115

Workflow file for this run

---
name: main
on: # yamllint disable-line rule:truthy
push:
branches: [main]
pull_request:
types: [closed]
branches: [main]
workflow_dispatch:
permissions:
contents: write
packages: write
pull-requests: write
jobs:
validate:
name: Validations
uses: ./.github/workflows/ci.yaml
with:
skip_commit_validation: true
manage-release-pr:
needs: [validate]
# Every push to main (not a release-PR merge itself) - keeps the
# pending "chore(main): release X.Y.Z" PR current as commits land.
if: github.event_name == 'push'
name: Open/update the release PR
runs-on: ubuntu-latest
steps:
-
uses: actions/checkout@v7
with:
fetch-tags: true
-
# skip-github-release: this only ever manages the version-bump PR
# (manifest/CHANGELOG.md/mix.exs) - it never creates a tag or
# GitHub Release. The burrito-package job below does that itself,
# atomically, once this PR merges and binaries are already built -
# see its own comments for why.
uses: googleapis/release-please-action@v5
with:
config-file: .release-please-config.json
manifest-file: .release-please-manifest.json
token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
skip-github-release: true
burrito-build:
needs: [validate]
# Only when release-please's own release PR (always from this exact
# branch) actually merges - "approving the release PR" is the trigger
# for building, not every push to main.
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.head.ref == 'release-please--branches--main')
name: Build Burrito target (${{ matrix.target }})
runs-on: ${{ matrix.runner }}
strategy:
# Kept in sync with mix.exs's releases.lc.burrito.targets - Burrito
# has no introspection command to read this list back out, so it's
# duplicated here by hand (see #66). Default fail-fast: true - a
# failed required leg cancels the others and fails this job outright,
# which correctly skips burrito-package below rather than packaging
# a release with a target missing.
#
# Each target runs on its matching OS runner so rustler_precompiled
# (used by mdex_native) downloads the correct precompiled NIF for the
# target platform. Cross-compiling from Ubuntu caused the Linux NIF to
# be bundled into macOS/Windows releases, making MDEx unavailable at
# runtime (see CRY-40).
matrix:
include:
- target: macos_aarch64
runner: macos-latest
- target: linux_x86_64
runner: ubuntu-latest
- target: windows_x86_64
runner: windows-latest
defaults:
run:
working-directory: app
shell: bash
steps:
-
uses: actions/checkout@v7
-
uses: erlef/setup-beam@v1
with:
otp-version: "29.0.3"
elixir-version: "1.20.3"
# Without this, setup-beam's problem matchers promote every
# compiler warning from deps (e.g. postgrex/rewrite's deprecated
# `xref: [exclude: ...]`, yamerl's deprecated `catch ...` syntax -
# both already at their latest published Hex versions, so not
# fixable from here) into noisy GH Actions annotations. See #9.
disable_problem_matchers: true
-
# 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"
-
run: mix deps.get
-
name: Build the ${{ matrix.target }} target
run: MIX_ENV=prod BURRITO_TARGET=${{ matrix.target }} mix release lc
-
# `mix release lc`'s exit code alone doesn't guarantee this target
# actually produced a binary - Burrito builds each target
# independently, so a target failing partway through wouldn't
# necessarily fail the whole `mix release` invocation. Not everyone
# installing this uses Homebrew (no tap exists yet anyway - see
# #14/CRY-37), so linux_x86_64/macos_aarch64 are the primary
# install path for a lot of users; silently shipping a release
# missing one of them is worse than failing loudly here.
# windows_x86_64 stays best-effort, as it always has.
name: Verify this target actually built
run: |
artifact="burrito_out/lc_${{ matrix.target }}"
if [ "${{ matrix.target }}" = "windows_x86_64" ]; then
artifact="${artifact}.exe"
fi
if [ ! -f "$artifact" ]; then
if [ "${{ matrix.target }}" = "windows_x86_64" ]; then
printf '::warning::missing optional release artifact: %s\n' "$artifact"
else
printf '::error::missing required release artifact: %s\n' "$artifact"
exit 1
fi
fi
-
# Handed off to burrito-package below, which needs every target's
# binary gathered back into one place before it can build the
# per-platform tarballs/SBOM/release.
name: Upload the built binary
uses: actions/upload-artifact@v7
with:
name: burrito-${{ matrix.target }}
path: app/burrito_out/lc_${{ matrix.target }}*
burrito-package:
needs: [burrito-build]
name: Package Burrito binaries and create the release
runs-on: ubuntu-latest
outputs:
tag_name: ${{ steps.version.outputs.tag_name }}
defaults:
run:
working-directory: app
steps:
-
uses: actions/checkout@v7
-
# Needed for erlef/mix_sbom below (an accurate dependency graph
# needs `mix deps.get` to have run against a real OTP/Elixir
# install) - this job never builds a release itself, so no
# setup-zig/p7zip here.
uses: erlef/setup-beam@v1
with:
otp-version: "29.0.3"
elixir-version: "1.20.3"
disable_problem_matchers: true
-
run: mix deps.get
-
# Every burrito-build matrix leg uploaded exactly one binary under
# its own `burrito-<target>` artifact name - merge them all back
# into one directory here, same layout the pre-matrix single job
# produced.
name: Download every built target's binary
uses: actions/download-artifact@v7
with:
path: app/burrito_out
pattern: burrito-*
merge-multiple: true
-
# A bare `lc_macos_aarch64` binary download doesn't get you
# lcreate/lcls/lclose/lcomment/lproj - only install.sh and the
# container image ever pulled those, straight from the repo
# checkout, never from a release asset. Package each target
# together with the wrapper scripts so a release download is
# actually installable on its own, matching what install.sh gives
# you.
name: Package each target into a per-platform tarball with the wrapper scripts
run: |
for artifact in lc_*
do
case "$artifact" in
lc_windows_x86_64.exe)
target=lc_windows_x86_64
binary_name=lc.exe
;;
*)
target=$artifact
binary_name=lc
;;
esac
stage=$(mktemp -d)
cp "$artifact" "$stage/$binary_name"
cp ../../bin/lcreate ../../bin/lcls ../../bin/lclose ../../bin/lcomment ../../bin/lproj "$stage/"
chmod +x "$stage"/*
tar -czf "${target}.tar.gz" -C "$stage" .
rm -rf "$stage" "$artifact"
done
working-directory: app/burrito_out
-
name: Generate checksums for every built artifact
run: sha256sum -- * > SHA256SUMS
working-directory: app/burrito_out
-
# manage-release-pr already bumped .release-please-manifest.json
# on main as part of the PR this job's trigger just merged - read
# the version straight from it rather than asking release-please
# again.
name: Read the version release-please just bumped to
id: version
run: |
version=$(ruby -rjson -e "print JSON.parse(File.read('../.release-please-manifest.json'))['.']")
printf 'tag_name=v%s\n' "$version" >> "$GITHUB_OUTPUT"
-
# Covers the app + Hex deps + the actual Erlang/OTP and Elixir
# versions this release was built with (mix_sbom includes those by
# default - verified they show up as individual OTP application
# components like kernel/stdlib/crypto/ssl, not one umbrella
# "erlang" entry). Not the container image's own OS packages -
# that's a separate SBOM, generated in the container job below,
# since it's meaningless to anyone not using the container.
name: Generate the app SBOM
uses: erlef/mix_sbom@v0
id: sbom
with:
project-path: ${{ github.workspace }}/app
reuse-beam: true
schema: "1.6"
format: "json"
-
# mix_sbom has no output-filename input - it always writes to
# $RUNNER_TEMP/$RANDOM.cdx.json internally (verified directly in
# the action's own script) and exposes that random path via
# sbom-path. gh release create uses a file's own basename as the
# asset's actual download filename (the `#label` syntax only sets
# a cosmetic display label, not the filename) - without this
# rename, the Readme's documented .../download/sbom.cdx.json URL
# would 404.
name: Rename the SBOM to a stable filename
run: mv "${{ steps.sbom.outputs.sbom-path }}" sbom.cdx.json
-
# One atomic command creates the tag, the release, and uploads
# every asset together - no separate release object sits around
# empty/unlocked waiting for a later upload, so GitHub's Immutable
# Releases (GA since Oct 2025) never gets a chance to lock us out
# (that's exactly what broke v0.2.0 permanently - see #18).
name: Create the release with every asset attached
env:
GH_TOKEN: ${{ github.token }}
run: gh release create "${{ steps.version.outputs.tag_name }}" burrito_out/*.tar.gz burrito_out/SHA256SUMS sbom.cdx.json --generate-notes
working-directory: app
-
# release-please labels its own release PR "autorelease: pending"
# and only relabels it "autorelease: tagged" once it creates the
# tag itself. Since we tag it ourselves above instead, that
# transition never happens on its own. Without this,
# release-please's own merged-PR guard (in manage-release-pr)
# keeps finding this PR stuck "pending" forever and refuses to
# open any future release PR ("There are untagged, merged release
# PRs outstanding - aborting").
name: Mark the release PR as tagged
env:
GH_TOKEN: ${{ github.token }}
run: |
gh pr edit "${{ github.event.pull_request.number }}" \
--remove-label "autorelease: pending" \
--add-label "autorelease: tagged"
working-directory: .
container:
needs: [burrito-package]
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"
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
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 lc
working-directory: app
-
name: Build the image
env:
APP_VERSION: ${{ needs.burrito-package.outputs.tag_name }}
run: ./ci/build_image.sh "${{ needs.burrito-package.outputs.tag_name }}"
-
# ci/build_image.sh prefers Podman over Docker (both are present on
# GitHub-hosted runners), so the image above lives only in Podman's
# own local storage. Trivy's `image-ref` scanning mode expects a
# Docker-compatible daemon/socket and has no visibility into that
# storage, so it'd either find nothing or silently try to pull from
# a registry instead (see #67 - a runtime-preference reorder was
# tried and rejected, since it would've meant Podman-built images
# go untested here). Saving to a tarball and scanning that instead
# sidesteps the runtime question entirely: Trivy reads the tarball
# directly, no daemon of either kind involved.
name: Save the image to a tarball for Trivy to scan directly
id: save
run: |
tarball="$RUNNER_TEMP/linear-cli.tar"
./ci/save_image.sh "${{ needs.burrito-package.outputs.tag_name }}" "$tarball"
printf 'tarball_path=%s\n' "$tarball" >> "$GITHUB_OUTPUT"
-
# Covers the container's own OS packages (Alpine/apk - ca-certificates,
# bash) - meaningless to anyone not using the container, which is
# exactly why it's a separate file from the app SBOM in the
# burrito-package job above, not merged into it. Scans the tarball
# the previous step just saved, not a live image-ref - see that
# step's own comment for why - before publishing, though nothing
# here gates the publish step on it.
name: Generate the container SBOM
uses: aquasecurity/trivy-action@v0.36.0
with:
input: ${{ steps.save.outputs.tarball_path }}
scan-type: image
format: cyclonedx
output: container-sbom.cdx.json
-
name: Publish the image
env:
GITHUB_TOKEN: ${{ github.token }}
GITHUB_ACTOR: ${{ github.actor }}
run: ./ci/publish.sh "${{ needs.burrito-package.outputs.tag_name }}"
-
# Can't attach this to the GitHub release the burrito-package job
# already created - it's published (and thus immutable, see #18) by
# the time this job runs. A workflow artifact is the honest option
# here, not a release asset pretending to be one.
name: Upload the container SBOM as a workflow artifact
uses: actions/upload-artifact@v7
with:
name: container-sbom
path: container-sbom.cdx.json
retention-days: 90
homebrew-tap-bump:
needs: [burrito-package]
name: Bump the Homebrew tap formula
runs-on: ubuntu-latest
steps:
-
name: Checkout linear-cli (for ci/bump_homebrew_formula.rb)
uses: actions/checkout@v7
with:
path: linear-cli
-
# RELEASE_PLEASE_TOKEN (org secret) - the default GITHUB_TOKEN is
# scoped only to this repo, and can't push/open a PR against a
# different one.
name: Checkout rubyists/homebrew-tap
uses: actions/checkout@v7
with:
repository: rubyists/homebrew-tap
token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
path: homebrew-tap
-
name: Download this release's SHA256SUMS
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release download "${{ needs.burrito-package.outputs.tag_name }}" \
--repo rubyists/linear-cli --pattern SHA256SUMS --output linear-cli/SHA256SUMS
-
name: Bump the formula's url/sha256 for each platform
run: |
ruby linear-cli/ci/bump_homebrew_formula.rb \
homebrew-tap/Formula/linear-cli/linear-cli.rb \
"${{ needs.burrito-package.outputs.tag_name }}" \
linear-cli/SHA256SUMS
-
# No-op (no PR opened, nothing pushed) when there's nothing to
# commit - same action/behavior already relied on in
# usage-rules-sync.yaml.
name: Open a PR bumping the formula, if anything changed
uses: peter-evans/create-pull-request@v8
with:
token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
path: homebrew-tap
commit-message: "fix: bump linear-cli formula to ${{ needs.burrito-package.outputs.tag_name }}"
title: "fix: bump linear-cli formula to ${{ needs.burrito-package.outputs.tag_name }}"
body: >-
Auto-generated after rubyists/linear-cli's
${{ needs.burrito-package.outputs.tag_name }} release.
branch: "bump-formula-${{ needs.burrito-package.outputs.tag_name }}"
delete-branch: true