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
18 changes: 18 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,24 @@ jobs:
git config --global user.email "dev@37signals.com"
scripts/publish-aur.sh "$VERSION"

nix-verify:
name: Verify Nix flake
needs: [release]
if: startsWith(github.ref, 'refs/tags/v')
continue-on-error: true
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

- name: Install Nix
uses: cachix/install-nix-action@526118121621777ccd86f79b04685a9319637641 # v31

- name: Build and verify
run: |
STORE_PATH=$(nix build --no-link --print-out-paths)
$STORE_PATH/bin/basecamp --version

sync-skills:
name: Sync skills
needs: [release]
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,6 @@ benchmarks-current.txt

# PGO profile (generated, can optionally be committed for reproducible builds)
default.pgo

# Nix
result
51 changes: 51 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,34 @@ archives:
- README*
- completions/*

nfpms:
- id: packages
package_name: basecamp-cli
ids: [basecamp]
vendor: 37signals
homepage: "https://github.com/basecamp/basecamp-cli"
maintainer: "37signals <support@37signals.com>"
description: "Command-line interface for Basecamp"
license: MIT
formats:
- deb
- rpm
- apk
contents:
- src: completions/basecamp.bash
dst: /usr/share/bash-completion/completions/basecamp
- src: completions/_basecamp
dst: /usr/share/zsh/vendor-completions/_basecamp
packager: deb
- src: completions/_basecamp
dst: /usr/share/zsh/vendor-completions/_basecamp
packager: apk
- src: completions/_basecamp
dst: /usr/share/zsh/site-functions/_basecamp
packager: rpm
- src: completions/basecamp.fish
dst: /usr/share/fish/vendor_completions.d/basecamp.fish

checksum:
name_template: 'checksums.txt'
algorithm: sha256
Expand Down Expand Up @@ -109,12 +137,35 @@ release:
basecamp auth login
```

**Linux (deb/rpm/apk):**
Download the package for your architecture from the assets below, then:
```sh
sudo apt install ./basecamp-cli_{{.Version}}_linux_amd64.deb # Debian/Ubuntu
sudo dnf install ./basecamp-cli_{{.Version}}_linux_amd64.rpm # Fedora/RHEL
sudo apk add --allow-untrusted ./basecamp-cli_{{.Version}}_linux_amd64.apk # Alpine
basecamp auth login
```
Arm64 users: substitute `arm64` for `amd64` in the filename. Verify the SHA-256 checksum from `checksums.txt` before installing — especially Alpine packages installed with `--allow-untrusted`.

**Arch Linux / Omarchy:**
```sh
yay -S basecamp-cli
basecamp auth login
```

**Nix:**
```sh
nix profile install github:basecamp/basecamp-cli
basecamp auth login
```

**Windows:**
```sh
scoop bucket add basecamp https://github.com/basecamp/homebrew-tap
scoop install basecamp
basecamp auth login
```

**Other platforms:** download the matching archive from the assets below.

homebrew_casks:
Expand Down
7 changes: 7 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,12 @@ clean-pgo:
bump-sdk:
./scripts/bump-sdk.sh $(REF)

# Recompute Nix vendorHash via Docker and update nix/package.nix
.PHONY: update-nix-hash
update-nix-hash:
@VERSION=$$(sed -n 's/.*version = "\([^"]*\)".*/\1/p' nix/package.nix | head -1) && \
scripts/update-nix-flake.sh "$$VERSION" || true

# Verify sdk-provenance.json matches go.mod
# Skips when a replace directive is active (local dev with go.work or go.mod replace)
.PHONY: provenance-check
Expand Down Expand Up @@ -457,6 +463,7 @@ help:
@echo " check-surface-diff Compare CLI surface snapshots (fails on removals)"
@echo ""
@echo "Dependencies:"
@echo " update-nix-hash Recompute Nix vendorHash via Docker"
@echo " bump-sdk Bump SDK and update provenance (REF=<git-ref>)"
@echo " provenance-check Verify sdk-provenance.json matches go.mod"
@echo ""
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ That's it. You now have full access to Basecamp from your terminal.
yay -S basecamp-cli
```

**Linux (deb/rpm/apk):**
```bash
# Download from https://github.com/basecamp/basecamp-cli/releases/latest
sudo apt install ./basecamp-cli_*_linux_amd64.deb # Debian/Ubuntu
sudo dnf install ./basecamp-cli_*_linux_amd64.rpm # Fedora/RHEL
sudo apk add --allow-untrusted ./basecamp-cli_*_linux_amd64.apk # Alpine
```
Arm64: substitute `arm64` for `amd64` in the filename. Verify the SHA-256 checksum from `checksums.txt` before installing unsigned Alpine packages.

**Scoop (Windows):**
```bash
scoop bucket add basecamp https://github.com/basecamp/homebrew-tap
Expand All @@ -34,6 +43,11 @@ scoop install basecamp
curl -fsSL https://raw.githubusercontent.com/basecamp/basecamp-cli/main/scripts/install.sh | bash
```

**Nix:**
```bash
nix profile install github:basecamp/basecamp-cli
```

**Go install:**
```bash
go install github.com/basecamp/basecamp-cli/cmd/basecamp@latest
Expand Down
22 changes: 19 additions & 3 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,22 @@ make release VERSION=0.2.0 DRY_RUN=1

1. Validates semver format, main branch, clean tree, synced with remote
2. Checks for `replace` directives in go.mod
3. Runs `make release-check` (quality checks, vuln scan, replace-check, race-test, surface compat)
4. Creates annotated tag `v$VERSION` and pushes to origin
5. GitHub Actions [release workflow](.github/workflows/release.yml) runs:
3. Updates `nix/package.nix` version and recomputes `vendorHash` via Docker if `go.mod` changed
4. Runs `make release-check` (quality checks, vuln scan, replace-check, race-test, surface compat)
5. Creates annotated tag `v$VERSION` and pushes to origin
6. GitHub Actions [release workflow](.github/workflows/release.yml) runs:
- Security scan + full test suite + CLI surface compatibility check
- Collects PGO profile from benchmarks
- Generates AI changelog from commit history
- Builds binaries for all platforms (darwin, linux, windows, freebsd, openbsd × amd64/arm64)
- Builds `.deb`, `.rpm`, `.apk` Linux packages (amd64 + arm64)
- Signs and notarizes macOS binaries (Developer ID via GoReleaser/quill)
- Signs checksums with cosign (keyless via Sigstore OIDC)
- Generates SBOM for supply chain transparency
- Updates Homebrew cask in `basecamp/homebrew-tap`
- Updates Scoop manifest in `basecamp/homebrew-tap`
- Updates AUR `basecamp-cli` package (when `AUR_KEY` is configured)
- Verifies Nix flake builds successfully

## Versioning

Expand Down Expand Up @@ -69,6 +72,17 @@ Pre-1.0: minor bumps for features, patch bumps for fixes. Prerelease tags
4. Add the public key to your AUR profile
5. Add the private key as `AUR_KEY` in GitHub Actions secrets

## Nix flake maintenance

The `flake.nix` provides `nix profile install github:basecamp/basecamp-cli`. The release
script automatically updates `nix/package.nix` version and recomputes `vendorHash` when
`go.mod` changes (requires Docker).

To manually update the vendorHash (e.g. after an SDK bump):
```bash
make update-nix-hash
```

## Distribution channels

| Channel | Location | Updated by |
Expand All @@ -77,4 +91,6 @@ Pre-1.0: minor bumps for features, patch bumps for fixes. Prerelease tags
| Homebrew cask | `basecamp/homebrew-tap` Casks/ | GoReleaser |
| Scoop | `basecamp/homebrew-tap` root | GoReleaser |
| AUR | `basecamp-cli` | GoReleaser |
| deb/rpm/apk packages | GitHub Release assets | GoReleaser (nfpm) |
| Nix flake | `flake.nix` in repo | Self-serve (`nix profile install github:basecamp/basecamp-cli`) |
| go install | `go install github.com/basecamp/basecamp-cli/cmd/basecamp@latest` | Go module proxy |
27 changes: 27 additions & 0 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
description = "Command-line interface for Basecamp";

inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
};

outputs = { self, nixpkgs }:
let
supportedSystems = [
"x86_64-linux"
"aarch64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; });
in {
packages = forAllSystems (system:
let pkgs = nixpkgsFor.${system};
in {
basecamp = pkgs.callPackage ./nix/package.nix { };
default = self.packages.${system}.basecamp;
}
);
};
}
20 changes: 17 additions & 3 deletions install.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,33 @@ scoop bucket add basecamp https://github.com/basecamp/homebrew-tap
scoop install basecamp
```

### Option C: Shell script
### Option C: Linux package (Debian/Ubuntu, Fedora/RHEL, Alpine)
```bash
# Download the matching package from https://github.com/basecamp/basecamp-cli/releases/latest
sudo apt install ./basecamp-cli_*_linux_amd64.deb # Debian/Ubuntu
sudo dnf install ./basecamp-cli_*_linux_amd64.rpm # Fedora/RHEL
sudo apk add --allow-untrusted ./basecamp-cli_*_linux_amd64.apk # Alpine
```
Arm64: substitute `arm64` for `amd64` in the filename. Verify the SHA-256 checksum from `checksums.txt` before installing unsigned Alpine packages.

### Option D: Nix
```bash
nix profile install github:basecamp/basecamp-cli
```

### Option E: Shell script
```bash
curl -fsSL https://raw.githubusercontent.com/basecamp/basecamp-cli/main/scripts/install.sh | bash
```

The install script downloads the latest release, verifies the SHA-256 checksum, and verifies the cosign signature when cosign is available.

### Option D: Go install
### Option F: Go install
```bash
go install github.com/basecamp/basecamp-cli/cmd/basecamp@latest
```

### Option E: GitHub Release
### Option G: GitHub Release
Download the archive for your platform from [Releases](https://github.com/basecamp/basecamp-cli/releases), extract, and move `basecamp` to a directory on your PATH.

**Verify:**
Expand Down
37 changes: 37 additions & 0 deletions nix/package.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{ lib, buildGoModule, go_1_26, installShellFiles, stdenv }:

buildGoModule.override { go = go_1_26; } (finalAttrs: {
pname = "basecamp";
# Updated automatically by scripts/update-nix-flake.sh on each release.
version = "0.2.3";

src = lib.cleanSource ./..;

# To update: set to lib.fakeHash, run `nix build`, use the hash from the error.
vendorHash = "sha256-9YnQ8PaJxcoaYuWM8jB7KSB7MFI5K5Pm/d3wkwugtrA=";

subPackages = [ "cmd/basecamp" ];

ldflags = [
"-s" "-w"
"-X github.com/basecamp/basecamp-cli/internal/version.Version=${finalAttrs.version}"
];

nativeBuildInputs = [ installShellFiles ];

postInstall = lib.optionalString
(stdenv.buildPlatform.canExecute stdenv.hostPlatform) ''
installShellCompletion --cmd basecamp \
--bash <($out/bin/basecamp completion bash) \
--fish <($out/bin/basecamp completion fish) \
--zsh <($out/bin/basecamp completion zsh)
'';

meta = {
description = "Command-line interface for Basecamp";
homepage = "https://github.com/basecamp/basecamp-cli";
changelog = "https://github.com/basecamp/basecamp-cli/releases/tag/v${finalAttrs.version}";
license = lib.licenses.mit;
mainProgram = "basecamp";
};
})
12 changes: 12 additions & 0 deletions scripts/release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,18 @@ if grep -q '^[[:space:]]*replace[[:space:]]' go.mod; then
die "go.mod contains replace directives. Remove them before releasing."
fi

# --- Update Nix flake ---
info "Updating Nix flake"
if [[ "${DRY_RUN}" == "true" || "${DRY_RUN}" == "1" ]]; then
echo " (skipped — dry run)"
elif scripts/update-nix-flake.sh "${VERSION}"; then
git add nix/package.nix
git commit -m "Update nix flake for v${VERSION}"
git push origin main --quiet
LOCAL=$(git rev-parse HEAD)
info "Pushed nix flake update"
fi

# --- Run pre-flight checks ---
info "Running release checks"
make release-check
Expand Down
Loading
Loading