Skip to content
Open
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
6 changes: 3 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

- name: Set up go
uses: actions/setup-go@v4
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
with:
go-version: ">=1.24.3"
go-version-file: go.mod
cache: false

- name: Build
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/codesee-arch-diagram.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
continue-on-error: true
name: Analyze the repo with CodeSee
steps:
- uses: Codesee-io/codesee-action@v2
- uses: Codesee-io/codesee-action@db076ce4b205a08da4d95bbefb3e278a958a4799 # v2
with:
codesee-token: ${{ secrets.CODESEE_ARCH_DIAG_API_TOKEN }}
codesee-url: https://app.codesee.io
10 changes: 5 additions & 5 deletions .github/workflows/linter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
with:
go-version: ">=1.24.3"
go-version-file: go.mod
cache: false
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
continue-on-error: false
with:
version: v1.64.3
version: v2.9.0
2 changes: 1 addition & 1 deletion .github/workflows/semgrep.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:

steps:
# Fetch project source with GitHub Actions Checkout.
- uses: actions/checkout@v3
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
# Run the "semgrep ci" command on the command line of the docker image.
- run: semgrep ci
env:
Expand Down
116 changes: 73 additions & 43 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
run:
timeout: 10m
version: "2"
output:
formats:
- format: colored-line-number
text:
path: stdout
linters:
enable-all: true
default: all
disable:
# Disabled because we use inits all over the place
- gochecknoinits
Expand Down Expand Up @@ -37,45 +36,76 @@ linters:
- maintidx
# We have legitimate use cases for returning an interface
- ireturn
# deprecated
- tenv
# Sometimes it's useful to return nil, nil
- nilnil
linters-settings:
# See https://golangci-lint.run/usage/linters#revive
revive:
# Deprecated alias of wsl_v5; keep it off so only wsl_v5 runs (avoids a deprecation warning).
- wsl
settings:
ireturn:
allow:
- error
- generic
- stdlib
- empty
misspell:
mode: restricted
locale: US
# See https://golangci-lint.run/usage/linters#revive
revive:
rules:
# Use var-naming, but add a special rule to accept "Id" rather than forcing "ID"
- name: var-naming
arguments:
- - ID
- API
disabled: false
# v2 merged stylecheck into staticcheck; keep "all" checks but drop ST1003 (var-naming), matching the prior config.
staticcheck:
checks:
- all
- -ST1003
varnamelen:
# The longest distance, in source lines, that is being considered a "small scope" where it is ok to use short variable names.
# The default is 5, which is quite strict.
max-distance: 15
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
# Use var-naming, but add a special rule to accept "Id" rather than forcing "ID"
- name: var-naming
disabled: false
arguments:
- ["ID", "API"]
# See https://staticcheck.io/docs/configuration/options/#checks
staticcheck:
checks: ["all"]
stylecheck:
checks: ["all", "-ST1003"]
varnamelen:
# The longest distance, in source lines, that is being considered a "small scope" where it is ok to use short variable names.
# The default is 5, which is quite strict.
max-distance: 15
ireturn:
allow:
- error
- generic
- stdlib
- empty
misspell:
locale: US
mode: restricted
issues:
exclude-rules:
- text: "don't use ALL_CAPS in Go names"
linters:
- revive
exclude-dirs:
- openapi
- scripts
- tmp
- playground
- testrun
- linters:
- revive
text: don't use ALL_CAPS in Go names
# `utils` is an established package name here; v2's revive flags it as "meaningless".
- linters:
- revive
text: avoid meaningless package names
paths:
- openapi
- scripts
- tmp
- playground
- testrun
- third_party$
- builtin$
- examples$
formatters:
enable:
- gci
- gofmt
- gofumpt
- goimports
exclusions:
generated: lax
paths:
- openapi
- scripts
- tmp
- playground
- testrun
- third_party$
- builtin$
- examples$
109 changes: 109 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this is

`amp` — the Ampersand CLI (Go, Cobra-based). It talks to the Ampersand API to manage
integrations, installations, connections, destinations, and projects, and provides local
webhook development tooling. User-facing docs: https://docs.withampersand.com/cli/overview

## Build / lint / test

Building requires [Task](https://taskfile.dev). The build bakes stage-specific config
(API URL, Clerk URL, login URL, version metadata) into the binary via `-ldflags -X`, so
you build *per stage* rather than with a plain `go build`.

```sh
task build # alias for build-dev → produces bin/amp
task build-local # points API_URL at http://127.0.0.1:8080
task build-staging
task build-prod
```

For local dev the README suggests symlinking `bin/amp` to `lamp` ("local amp") so it
coexists with an installed production `amp`.

```sh
task lint # golangci-lint (config .golangci.yml)
task fix # wsl + gci + golangci-lint --fix (needs gci, wsl installed)
make fix-files FILES="cmd/a.go cmd/b.go" # format/vet specific files only

go test ./... # run tests (only files/ has tests currently)
go test ./files -run TestGetRemovedReadObjects # run a single test
```

There is no `task test`; use `go test` directly. Linting is strict (`enable-all` minus a
curated disable list in `.golangci.yml`); the `openapi/` directory is excluded from lint.
Note revive is configured to accept `Id`/`Api` (not just `ID`/`API`).

## Code generation

`openapi/*.gen.go` are generated types (via `oapi-codegen`) pulled from remote specs in the
`amp-labs/openapi` repo — the manifest, problem, and catalog schemas. Regenerate with:

```sh
cd openapi && make gen
```

Do not hand-edit the `.gen.go` files. The `Manifest`, `Integration`, `CatalogType`, etc.
types used throughout the code live in this generated `openapi` package.

## Architecture

**Command wiring (Cobra + Viper).** `main.go``cmd.Execute()``rootCmd` (in
`cmd/root.go`). Each command lives in its own file under `cmd/` and self-registers via an
`init()` that calls `rootCmd.AddCommand(...)`. Global persistent flags (`--debug/-d`,
`--project/-p`, `--key/-k`) are set up in `flags/config.go` (`flags.Init`) and bound to
Viper; `--key` also reads the `AMP_API_KEY` env var. Use the `flags` package helpers
(`GetProjectOrFail`, `GetAPIKey`, `GetOutputFormat`, `GetDebugMode`) rather than reading
Viper directly.

**Stage configuration is build-time.** The `vars` package holds `ApiURL`, `ClerkRootURL`,
`LoginURL`, `Stage`, `Version`, etc., all defaulting to `"unset"` and overwritten at build
time by ldflags (see `Taskfile.yaml`). At runtime these can be overridden by env vars for
testing: `AMP_API_URL`, `AMP_CLERK_URL_OVERRIDE`, `AMP_STAGE_OVERRIDE`, `AMP_API_KEY`.

**Authentication (two modes).** `request.APIClient.getAuthHeader` decides:
1. If an API key is present (`--key` / `AMP_API_KEY`) → sends `X-Api-Key`.
2. Otherwise → uses a Clerk JWT browser-login session. `amp login` performs the Clerk OAuth
flow and writes session data to the XDG config dir (`amp/jwt.json`, or
`amp/jwt-<stage>.json` for non-prod). The `clerk` package exchanges that stored session
for a fresh JWT on each request (`FetchJwt`).

**API layer (`request/`).** `request.go` is the low-level HTTP client (`Client`) that adds
`X-Amp-Client*` headers, marshals/unmarshals JSON, and parses error responses. Non-2xx
responses with `application/problem+json` are decoded into `ProblemError` (RFC 7807).
`api.go` defines `APIClient` — one method per API endpoint (`ListIntegrations`,
`BatchUpsertIntegrations`, `GetPreSignedUploadURL`, etc.), each building a URL under
`{ApiURL}/v1/projects/{projectId}/...` and attaching the auth header. Response DTOs are in
`request/types.go`.

**Deploy flow (`amp deploy`).** The most involved path, worth understanding end to end:
`files.Zip` locates and validates an `amp.yaml`/`amp.yml` manifest (parsed into the
generated `openapi.Manifest`, validated by `files/manifest.go` against `specVersion 1.0.0`),
zips it in memory → compute MD5 → `GetPreSignedUploadURL``storage.Upload` PUTs the zip to
GCS → `BatchUpsertIntegrations` is called with the resulting `gs://` URL. Before deploying,
`confirmReadObjectRemoval` compares the new manifest against existing integrations and, if
read objects were removed *and* there are live installations, interactively prompts (via
`promptui`) whether to pause reads (the `destructive` flag).

**Local webhook development.** `amp listen` (hidden command) runs a local HTTP server that
pretty-prints and forwards incoming webhooks to your app, writing its chosen port to the OS
cache dir (`ampersand/webhook-port`). `amp trigger` sends fixture events (see
`internal/fixtures/`, e.g. hubspot/stripe payloads) to that listener; `sync-ngrok` bridges
to a public ngrok URL. Webhook helpers live in `internal/webhook/`.

**Logging.** Use the `logger` package (`Info`, `Infof`, `Debugf`, `Fatal`, `FatalErr`).
`Debugf` output is gated on the `--debug` flag. Note: `logger` imports `flags`, so `flags`
must not import `logger` (there is an explicit circular-dependency workaround in
`flags/config.go`'s `GetProjectOrFail`).

## Conventions

- Command failures typically call `logger.Fatal`/`logger.FatalErr` (which `os.Exit(1)`),
rather than returning errors up the stack — follow the pattern of neighboring commands.
- Naming (`amp <verb>:<noun>`): list commands use `list:integrations`,
`list:installations`, etc.; deletes use `delete:integration`. Keep this colon style.
- Output format for list-style commands is controlled by a `--format/-f` flag (json|yaml);
wire it with `flags.InitAndBindFormatFlag` and render with `utils.WriteStruct`.
15 changes: 10 additions & 5 deletions appdata/appdata.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import (

const fileName = "Ampersand/config.json"

// IMPORTANT:
// Do not modify the JSON labels in this struct without ensuring backwards compatibility,
// since those strings are written to the user's config file on their computer.
// Config is the user's persisted CLI configuration.
//
// IMPORTANT: Do not modify the JSON labels in this struct without ensuring backwards
// compatibility, since those strings are written to the user's config file on their computer.
type Config struct {
Token Token `json:"token"`
}
Expand All @@ -40,7 +41,9 @@ func Get() (Config, error) {
}

var c Config
if err := json.Unmarshal(data, &c); err != nil {

err = json.Unmarshal(data, &c)
if err != nil {
return Config{}, fmt.Errorf("can't parse config: %w", err)
}

Expand All @@ -56,7 +59,9 @@ func Set(config Config) error {
}

merged := config
if err := mergo.Merge(&merged, existing); err != nil {

err = mergo.Merge(&merged, existing)
if err != nil {
return fmt.Errorf("can't merge new config with existing config: %w", err)
}

Expand Down
14 changes: 10 additions & 4 deletions clerk/clerk.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,9 @@ func FetchJwt(ctx context.Context) (string, error) { //nolint:funlen,cyclop
}

data := &LoginData{}
if err := json.Unmarshal(contents, data); err != nil {

err = json.Unmarshal(contents, data)
if err != nil {
return "", fmt.Errorf("error unmarshalling jwt file: %w", err)
}

Expand Down Expand Up @@ -224,11 +226,13 @@ func FetchJwt(ctx context.Context) (string, error) { //nolint:funlen,cyclop
}

if rsp.StatusCode != http.StatusOK {
return "", fmt.Errorf("http %d (%s)", rsp.StatusCode, string(bb)) //nolint:goerr113
return "", fmt.Errorf("http %d (%s)", rsp.StatusCode, string(bb)) //nolint:err113
}

cr := &clientResponse{}
if err := json.Unmarshal(bb, cr); err != nil {

err = json.Unmarshal(bb, cr)
if err != nil {
return "", fmt.Errorf("error unmarshalling response body: %w", err)
}

Expand Down Expand Up @@ -274,7 +278,9 @@ func DecodeJWT(jwt string) (string, string, error) {
func getHTML(emailStr string) (string, error) {
// Render the HTML
tmpl := mustache.New()
if err := tmpl.ParseString(HTML); err != nil {

err := tmpl.ParseString(HTML)
if err != nil {
return "", err
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/delete_installation.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ var deleteInstallationCmd = &cobra.Command{ //nolint:gochecknoglobals
Use: "delete:installation <integrationId> <installationId>",
Short: "Delete installation",
Long: "Delete installation",
Args: cobra.ExactArgs(2), //nolint:gomnd,mnd
Args: cobra.ExactArgs(2), //nolint:mnd
Run: func(cmd *cobra.Command, args []string) {
logger.Debug("Deleting installation")

Expand Down
Loading
Loading