From d3218d8b637f38f4a5267fe2853d5935ac49353d Mon Sep 17 00:00:00 2001 From: Arnon Rotem-Gal-Oz Date: Tue, 8 Sep 2026 20:32:03 +0300 Subject: [PATCH 1/2] build: release with goreleaser and publish a Homebrew cask The v0.8.0 release never produced usable artifacts. Its matrix needed a macOS runner for the darwin targets, which queued indefinitely, and the checksums it did publish recorded `dist/`-prefixed paths that matched nothing where the assets are actually downloaded -- `sha256sum -c` printed three "No such file or directory" lines and still exited 0. Replace the hand-rolled matrix with goreleaser on a single Linux runner. Nothing in the build needs cgo: the darwin keychain backend shells out to /usr/bin/security via os/exec, verified with a set/get/delete round-trip from a CGO_ENABLED=0 binary whose `otool -L` lists only libSystem and libresolv. So every target cross-compiles from one runner, which also restores the Intel macOS build that da665a3 dropped on the mistaken grounds that darwin requires cgo. Publish a Homebrew cask to mondaycom/homebrew-tap. homebrew-core is not an option yet -- it requires notability the repo does not have -- and goreleaser deprecated `brews` in favour of `homebrew_casks`, since what it generated for pre-compiled binaries were "hackyish" formulas. The cask carries a postflight hook that strips com.apple.quarantine; without it macOS reports "mcli is damaged and cannot be opened", because these binaries are neither signed nor notarized. Tarballs are new; the bare `mcli--` binaries keep the names v0.8.0 used so existing download instructions still resolve. CI now runs `goreleaser check` and a full snapshot build on every PR, so a deprecated property fails a pull request instead of a tag push. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 33 +++++++++- .github/workflows/release.yml | 99 +++++++---------------------- .goreleaser.yaml | 113 ++++++++++++++++++++++++++++++++++ README.md | 13 ++++ 4 files changed, 179 insertions(+), 79 deletions(-) create mode 100644 .goreleaser.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e276c30..a091422 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,10 +13,10 @@ jobs: go-version: ["1.26"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v7 with: go-version: ${{ matrix.go-version }} @@ -38,3 +38,32 @@ jobs: - name: Test run: make test + + packaging: + name: Packaging config + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v7 + with: + go-version: "1.26" + + # Catches deprecated or invalid goreleaser properties on a PR, rather than + # when a tag push turns them into a failed release. + - name: Validate goreleaser config + uses: goreleaser/goreleaser-action@v7 + with: + version: "~> v2" + args: check + + # Proves all four release targets still cross-compile without cgo. + - name: Build all release targets + uses: goreleaser/goreleaser-action@v7 + with: + version: "~> v2" + args: build --snapshot --clean + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 294d186..aa4fd16 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,91 +5,36 @@ on: tags: - 'v*' -jobs: - build: - name: Build ${{ matrix.goos }}/${{ matrix.goarch }} - runs-on: ${{ matrix.os }} - strategy: - matrix: - include: - - os: ubuntu-latest - goos: linux - goarch: amd64 - cgo_enabled: "0" - - os: ubuntu-latest - goos: linux - goarch: arm64 - cgo_enabled: "0" - - os: macos-latest - goos: darwin - goarch: arm64 - cgo_enabled: "1" +permissions: + contents: write +jobs: + goreleaser: + name: Build and publish + # Every target cross-compiles from this one runner because the build needs no + # cgo. The previous matrix required a macOS runner for darwin, which is what + # made the Intel build queue indefinitely (no macos-13 runner) and left tags + # unpublished. + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: + # goreleaser derives the version and changelog from tags. fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v7 with: go-version: "1.26" - - name: Build - env: - GOOS: ${{ matrix.goos }} - GOARCH: ${{ matrix.goarch }} - CGO_ENABLED: ${{ matrix.cgo_enabled }} - run: | - VERSION=$(git describe --tags --always) - BINARY=mcli-${{ matrix.goos }}-${{ matrix.goarch }} - go build \ - -ldflags "-X github.com/mondaycom/mcli/internal/cli.version=${VERSION}" \ - -o "${BINARY}" \ - ./cmd/mcli - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: mcli-${{ matrix.goos }}-${{ matrix.goarch }} - path: mcli-${{ matrix.goos }}-${{ matrix.goarch }} - if-no-files-found: error - - release: - name: Create Release - needs: build - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v4 + - name: Release + uses: goreleaser/goreleaser-action@v7 with: - fetch-depth: 0 - - - name: Download artifacts - uses: actions/download-artifact@v4 - with: - path: dist/ - merge-multiple: true - - # Run inside dist/ so the recorded names are flat (mcli-linux-amd64, not - # dist/mcli-linux-amd64). Users download the assets side by side, where a - # dist/ prefix makes `sha256sum -c checksums.txt` fail to find every file — - # and it exits 0 when no file is found, so a scripted install that thinks it - # verifies checksums would silently verify nothing. - - name: Checksums - working-directory: dist - run: sha256sum mcli-* > checksums.txt - - - name: Create release + version: "~> v2" + args: release --clean env: - GH_TOKEN: ${{ github.token }} - TAG: ${{ github.ref_name }} - run: | - gh release create "$TAG" \ - --title "$TAG" \ - --generate-notes \ - dist/mcli-linux-amd64 \ - dist/mcli-linux-arm64 \ - dist/mcli-darwin-arm64 \ - dist/checksums.txt + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # PAT with contents:write on mondaycom/homebrew-tap. The per-run + # GITHUB_TOKEN is scoped to this repository and cannot push the cask + # to another one. + HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..eb96780 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,113 @@ +version: 2 + +project_name: mcli + +before: + hooks: + - go mod download + +builds: + - id: mcli + main: ./cmd/mcli + binary: mcli + # CGO is not needed on any target. The darwin keychain backend + # (zalando/go-keyring) shells out to /usr/bin/security via os/exec and + # contains no cgo at all, verified by a real set/get/delete round-trip from a + # CGO_ENABLED=0 binary. That is what lets every target cross-compile from one + # Linux runner, Intel macOS included. + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + ldflags: + - -s -w -X github.com/mondaycom/mcli/internal/cli.version={{ .Version }} + # Reproducible builds: stamp binaries with the commit time rather than the + # build time, so rebuilding a tag produces identical bytes. + mod_timestamp: "{{ .CommitTimestamp }}" + +archives: + # Tarballs are what the Homebrew formula consumes, and they carry the LICENSE + # alongside the binary. + - id: archive + ids: + - mcli + formats: + - tar.gz + name_template: "mcli_{{ .Version }}_{{ .Os }}_{{ .Arch }}" + files: + - LICENSE + - README.md + # Bare binaries under the same names v0.8.0 published, so existing + # `curl -LO .../mcli-darwin-arm64` instructions keep working. + - id: binary + ids: + - mcli + formats: + - binary + name_template: "mcli-{{ .Os }}-{{ .Arch }}" + +checksum: + # Flat names, so `sha256sum -c checksums.txt` works where the assets are + # actually downloaded. goreleaser does this correctly by default; the previous + # hand-rolled step recorded a dist/ prefix that matched nothing and still + # exited 0. + name_template: "checksums.txt" + +changelog: + use: github + sort: asc + filters: + exclude: + - "^test:" + - "^chore:" + - "^ci:" + - "Merge pull request" + +# Casks, not formulas: goreleaser deprecated `brews` in favour of +# `homebrew_casks`, since what it generated for pre-compiled binaries were +# "hackyish" formulas. The generated cask does declare on_linux URLs, but +# Homebrew only supports casks on macOS, so Linux users install via Nix, +# `go install`, or the tarballs above. +homebrew_casks: + - name: mcli + # Build the cask from the tarball, not the bare binary. + ids: + - archive + # `binaries` defaults to the cask name (mcli), so it is left unset. + repository: + owner: mondaycom + name: homebrew-tap + # Needs a PAT with `contents: write` on mondaycom/homebrew-tap: the + # per-run GITHUB_TOKEN is scoped to this repo only and cannot push there. + token: "{{ .Env.HOMEBREW_TAP_TOKEN }}" + homepage: "https://github.com/mondaycom/mcli" + description: "Command-line interface for monday.com's GraphQL API, built for LLM agents" + hooks: + post: + # These binaries are not signed or notarized, and Homebrew quarantines + # cask downloads, so without this macOS reports "mcli is damaged and + # cannot be opened" on first run. The proper fix is an Apple Developer + # signing identity plus notarization; until then, strip the attribute. + install: | + if OS.mac? + system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/mcli"] + end + +release: + prerelease: auto + footer: | + ## Install + + ```sh + brew install mondaycom/tap/mcli + ``` + + Verify a manual download: + + ```sh + sha256sum -c checksums.txt + ``` diff --git a/README.md b/README.md index 9b5524d..95eb094 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,12 @@ A command-line interface for monday.com's GraphQL API. Single static binary, str ## Install ```sh +# Homebrew (macOS) +brew install mondaycom/tap/mcli + +# Nix +nix profile install github:mondaycom/mcli + # From source go install github.com/mondaycom/mcli/cmd/mcli@latest @@ -12,6 +18,13 @@ go install github.com/mondaycom/mcli/cmd/mcli@latest make build # → bin/mcli ``` +Or download a binary from the [latest release](https://github.com/mondaycom/mcli/releases/latest) +and verify it against the published manifest: + +```sh +sha256sum -c checksums.txt +``` + ## Quick Start ```sh From daa12fadd28faf08402d3e2bfa3cc57362284352 Mon Sep 17 00:00:00 2001 From: Arnon Rotem-Gal-Oz Date: Tue, 8 Sep 2026 20:32:17 +0300 Subject: [PATCH 2/2] build: add a Nix flake Gives Nix and NixOS users a first-class install (`nix profile install github:mondaycom/mcli`) and contributors a pinned dev shell, without waiting on a nixpkgs submission -- `mcli` is unclaimed there, but the package would live outside this repo and land on nixpkgs' review cadence. `nix build` runs the full test suite in the Nix sandbox, which has no network. That passes today: no test reaches the API, and the one test that touches a real keychain is behind the `keychain_real` build tag. CI builds the flake on every PR to guard vendorHash. It goes stale whenever go.mod or go.sum changes, and nothing else would catch it -- the Go build is unaffected, so the flake would stay broken until a user ran `nix run`. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 17 +++++++++++ .gitignore | 3 ++ flake.lock | 27 +++++++++++++++++ flake.nix | 64 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 111 insertions(+) create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a091422..bfc110e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,3 +67,20 @@ jobs: version: "~> v2" args: build --snapshot --clean + nix: + name: Nix flake + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - uses: cachix/install-nix-action@v31 + + # Guards vendorHash, which goes stale whenever go.mod or go.sum changes. + # Nothing else would catch it: the Go build is unaffected, so the flake + # would stay broken until someone ran `nix run`. This also runs the test + # suite inside the Nix sandbox, which has no network. + - name: Build the flake + run: nix build .#mcli + + - name: Smoke-test the built binary + run: ./result/bin/mcli version diff --git a/.gitignore b/.gitignore index a686333..493d9b9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # Build artifacts /bin/ /dist/ +# `nix build` output symlinks +/result +/result-* /mcli /mcli.exe diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..9889bc3 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1788752844, + "narHash": "sha256-VaWGJ6+cIYN2erfSecbRV+4ljI185Ty2wUrXyvQbgOw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "dc5d91f840324650bac8c379428c7037a416959a", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..e87cb03 --- /dev/null +++ b/flake.nix @@ -0,0 +1,64 @@ +{ + description = "mcli — a command-line interface for monday.com's GraphQL API, built for LLM agents"; + + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + + outputs = + { self, nixpkgs }: + let + # Bumped at release time. Nix builds from a source tree with no .git, so + # `git describe` is unavailable here and the version has to be stated. + version = "0.8.1"; + + systems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system}); + in + { + packages = forAllSystems (pkgs: rec { + default = mcli; + + mcli = pkgs.buildGoModule { + pname = "mcli"; + inherit version; + src = ./.; + + # Update with `nix build` — it prints the expected hash on mismatch — + # whenever go.mod or go.sum changes. + vendorHash = "sha256-XBewtzyN7HaNA04X89p0KTBhlHE5GZ7cjMMcRhbJxtU="; + + # No cgo on any platform: the darwin keychain backend shells out to + # /usr/bin/security rather than linking Security.framework. + env.CGO_ENABLED = "0"; + + ldflags = [ + "-s" + "-w" + "-X github.com/mondaycom/mcli/internal/cli.version=${version}" + ]; + + meta = { + description = "Command-line interface for monday.com's GraphQL API, built for LLM agents"; + homepage = "https://github.com/mondaycom/mcli"; + license = nixpkgs.lib.licenses.mit; + mainProgram = "mcli"; + }; + }; + }); + + devShells = forAllSystems (pkgs: { + default = pkgs.mkShell { + packages = [ + pkgs.go + pkgs.golangci-lint + pkgs.gopls + pkgs.gh + ]; + }; + }); + }; +}