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
1 change: 1 addition & 0 deletions .envrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
use flake
4 changes: 0 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,6 @@ jobs:

- run: go test ./...

- name: Check the installer
run: shellcheck install.sh

- name: Build the release the same way a tag would
uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3
with:
Expand All @@ -66,5 +63,4 @@ jobs:
args: release --snapshot --clean
env:
TAP_GITHUB_TOKEN: snapshots-publish-nothing
WINGET_GITHUB_TOKEN: snapshots-publish-nothing
AUR_KEY: snapshots-publish-nothing
7 changes: 0 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,19 +53,13 @@ jobs:
- name: Verify the publishing credentials are present
env:
TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
WINGET_GITHUB_TOKEN: ${{ secrets.WINGET_GITHUB_TOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }}
run: |
if [ -z "$TAP_GITHUB_TOKEN" ]; then
echo "TAP_GITHUB_TOKEN is unset, and GoReleaser would skip the tap and bucket uploads without failing"
exit 1
fi

if [ -z "$WINGET_GITHUB_TOKEN" ]; then
echo "WINGET_GITHUB_TOKEN is unset, and GoReleaser would skip the winget pull request without failing"
exit 1
fi

if [ -z "$AUR_KEY" ]; then
echo "AUR_KEY is unset, and GoReleaser would skip the AUR upload without failing"
exit 1
Expand All @@ -79,5 +73,4 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
WINGET_GITHUB_TOKEN: ${{ secrets.WINGET_GITHUB_TOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }}
30 changes: 0 additions & 30 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -118,33 +118,3 @@ aurs:
install -Dm755 "./superstack" "${pkgdir}/usr/bin/superstack"
install -Dm644 "./LICENSE" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
install -Dm644 "./README.md" "${pkgdir}/usr/share/doc/${pkgname}/README.md"

winget:
- name: superstack
ids:
- superstack
package_identifier: SiliconWitchery.Superstack
publisher: Silicon Witchery
publisher_url: https://siliconwitchery.com
publisher_support_url: https://github.com/siliconwitchery/superstack-cli/issues
homepage: https://github.com/siliconwitchery/superstack-cli
short_description: The Superstack command line interface
license: ISC
license_url: https://github.com/siliconwitchery/superstack-cli/blob/main/LICENSE
copyright: Silicon Witchery AB
skip_upload: auto
repository:
owner: siliconwitchery
name: winget-pkgs
branch: superstack-{{ .Version }}
token: "{{ .Env.WINGET_GITHUB_TOKEN }}"
pull_request:
enabled: true
draft: false
base:
owner: microsoft
name: winget-pkgs
branch: master
commit_author:
name: Raj Nakarja
email: raj@siliconwitchery.com
49 changes: 25 additions & 24 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,12 @@ the server, and the hidden `--server <url>` flag development uses to aim a
run at another server. The flag is deliberately absent from the help and wins
over `SUPERSTACK_API`.

Targets are flags rather than positions. Every command that touches devices
takes `--fleet` or `--device`, so an operation that can apply to one device or
to many stays one verb instead of splitting into a noun-verb pair per level.
Targets are positional. A fleet is named by the id `fleet list` shows, a
device by its IMEI, and a verb that can act on either takes one argument
accepting both. There is no default target and no bypass flag: a command
missing its target errors, and the destructive verbs ask for interactive
confirmation before acting. `internal/commands/fleets.go` holds the fleet
fetch the fleet-reading commands share.

## Releases

Expand All @@ -128,19 +131,21 @@ where it can read the history.
Pushing a `v*` tag is the whole release. GoReleaser builds the static binaries
for Linux, macOS, and Windows, publishes the GitHub release with checksums,
pushes the Homebrew cask to `siliconwitchery/homebrew-tap`, pushes the Scoop
manifest to `siliconwitchery/scoop-bucket`, pushes the `superstack-bin`
PKGBUILD to the AUR, and opens a winget pull request against
`microsoft/winget-pkgs` from the fork at `siliconwitchery/winget-pkgs`. The
`install.sh` at the repo root installs straight from GitHub Releases and needs
no per-release attention.

The flake is the fifth distribution path, and the only one not driven by a
tag. It builds from source at whatever commit the user points it at, so it
needs no release to work. Superstack is not in nixpkgs and will not be until
the project has traction, so the flake is how Nix users install until then.
`flake.nix` reads the version straight out of `main.go`, which keeps the
single source of truth intact, and renames the binary in `postInstall`
because Go names it after the module path rather than after the command.
manifest to `siliconwitchery/scoop-bucket`, and pushes the `superstack-bin`
PKGBUILD to the AUR. There is deliberately no winget package (its pull
requests review too slowly for the version gate's forced upgrades, and scoop
is the standard channel for developer tools), no `install.sh` (manual
installs are a download from the releases page, unpacked onto the PATH), and
no self-update command in the CLI (each channel updates itself; the server's
version gate is what prompts users to do so).

The flake is the one distribution path not driven by a tag. It builds from
source at whatever commit the user points it at, so it needs no release to
work. Superstack is not in nixpkgs and will not be until the project has
traction, so the flake is how Nix users install until then. `flake.nix`
reads the version straight out of `main.go`, which keeps the single source
of truth intact, and renames the binary in `postInstall` because Go names it
after the module path rather than after the command.

`.goreleaser.yaml` is the only description of the build matrix. Nothing else
may restate it, because a second copy drifts. CI proves the release path by
Expand All @@ -150,17 +155,13 @@ that config.

Every publisher carries `skip_upload: auto`, so a tag with a prerelease suffix
publishes a GitHub release and touches no package manager. That is how the
pipeline gets exercised without shipping. It leaves the four publisher pushes
pipeline gets exercised without shipping. It leaves the three publisher pushes
themselves untested, which only a real tag proves.

Three credentials sit behind the release, and the workflow's guard checks only
Two credentials sit behind the release, and the workflow's guard checks only
that they are present, never that they work. `TAP_GITHUB_TOKEN` is
fine-grained and scoped to the tap and the bucket. `WINGET_GITHUB_TOKEN` has to
be a classic token, because a fine-grained one cannot reach a repository
outside its resource owner and so cannot open the pull request against
Microsoft. `AUR_KEY` is a passphraseless SSH key registered with an AUR
account. GoReleaser reports a failed winget pull request without failing the
run, so that step needs checking by hand after a release.
fine-grained and scoped to the tap and the bucket. `AUR_KEY` is a
passphraseless SSH key registered with an AUR account.

The generated changelog is deliberately disabled, so a fresh release starts
with an empty body. Write the notes into it afterwards; GoReleaser keeps an
Expand Down
33 changes: 8 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ single static binary talking to the Superstack server's JSON API. The server is
a separate project; this repo is the CLI only. It is laid out as follows:

```sh
├── .envrc # Loads the Nix dev shell via direnv
├── .github/dependabot.yml # Weekly action and module update PRs
├── .github/workflows # CI on pull requests, release on v* tags
├── .gitignore
Expand All @@ -14,7 +15,6 @@ a separate project; this repo is the CLI only. It is laid out as follows:
├── flake.lock # Pins nixpkgs
├── flake.nix # The superstack package, and the dev shell
├── go.mod
├── install.sh # curl-to-shell installer for Linux and macOS
├── internal
│ └── commands # One file per command, plus the shared server client
├── LICENSE
Expand All @@ -27,25 +27,12 @@ a separate project; this repo is the CLI only. It is laid out as follows:

Each option needs a published release.

- **Linux and macOS.** Pin a version by putting `VERSION=1.2.3` on the `sh`
side of the pipe.

```sh
curl -fsSL https://raw.githubusercontent.com/siliconwitchery/superstack-cli/main/install.sh | sh
```

- **Homebrew:**

```sh
brew install --cask siliconwitchery/tap/superstack
```

- **winget:**

```sh
winget install SiliconWitchery.Superstack
```

- **Scoop:**

```sh
Expand All @@ -71,42 +58,40 @@ Each option needs a published release.

## Local development

1. Install [Nix](https://nixos.org) with flakes enabled.
1. Install [Go](https://go.dev) 1.25 or newer.

1. Clone the repository and enter the dev shell:
1. Clone the repository:

```sh
git clone git@github.com:siliconwitchery/superstack-cli.git ~/projects/superstack-cli
cd ~/projects/superstack-cli
nix develop
```

1. Build and run:

```sh
go build -o superstack .
CGO_ENABLED=0 go build -o superstack .
./superstack
```

[Nix](https://nixos.org) users need no Go install: `nix develop` enters the
dev shell, and with [direnv](https://direnv.net) hooked into your shell,
`direnv allow` run once in the checkout loads it automatically from then on.

## Release setup

Do everything below once.

1. Create public repositories `siliconwitchery/homebrew-tap` and
`siliconwitchery/scoop-bucket`, each with a README.

1. Fork `microsoft/winget-pkgs` into `siliconwitchery`.

1. Add a fine-grained token (Settings > Developer settings > Personal access
tokens) as the Actions secret `TAP_GITHUB_TOKEN`:

- Resource owner: `siliconwitchery`
- Repository access: `homebrew-tap` and `scoop-bucket`
- Permissions: Contents, read and write

1. Add a classic token with the `public_repo` scope as the Actions secret
`WINGET_GITHUB_TOKEN`. A fine-grained token cannot open the pull request.

1. Register at [aur.archlinux.org](https://aur.archlinux.org/register), then:

```sh
Expand Down Expand Up @@ -143,7 +128,5 @@ Do everything below once.
1. Write the release notes into the empty release body on GitHub, following the
shape in CLAUDE.md.

1. Check that the winget pull request opened against `microsoft/winget-pkgs`.

A tag carrying a prerelease suffix, `v0.0.2-rc1`, publishes a GitHub prerelease
and skips every package manager. Tags cannot be moved or deleted.
70 changes: 0 additions & 70 deletions install.sh

This file was deleted.

35 changes: 35 additions & 0 deletions internal/commands/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"io"
"io/fs"
"net/http"
"os"
"path/filepath"
Expand Down Expand Up @@ -95,6 +96,40 @@ func apiRequest(method string, path string, body io.Reader) (*http.Request, erro
return request, nil
}

func authenticatedRequest(method string, path string, body io.Reader) (*http.Request, error) {
storedKeyPath, err := keyPath()

if err != nil {
return nil, err
}

keyBytes, err := os.ReadFile(storedKeyPath)

if errors.Is(err, fs.ErrNotExist) {
return nil, errors.New("you are not logged in, run login first")
}

if err != nil {
return nil, err
}

key := strings.TrimSpace(string(keyBytes))

if key == "" {
return nil, errors.New("you are not logged in, run login first")
}

request, err := apiRequest(method, path, body)

if err != nil {
return nil, err
}

request.Header.Set("Authorization", "Bearer "+key)

return request, nil
}

func keyPath() (string, error) {
// The key is state, not configuration: linux dotfile repos routinely
// publish all of ~/.config, so the key must never live there. The mac
Expand Down
Loading