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
5 changes: 5 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ jobs:
with:
go-version-file: go.mod

- name: Verify generated code
run: |
make generate
git diff --exit-code -- internal/apicommands/catalog.gen.go

- name: Test
run: go test -v -failfast -race -timeout 10m ./...

Expand Down
122 changes: 122 additions & 0 deletions .github/workflows/release-code-samples.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
name: Release Code Samples

on:
release:
types:
- published

concurrency:
group: release-code-samples-${{ github.event.release.tag_name }}
cancel-in-progress: true

permissions:
contents: read

jobs:
sync-cli-code-samples:
name: Sync CLI code samples
runs-on: ubuntu-latest
env:
TARGET_REPOSITORY: sumup/sumup-developer
TARGET_BRANCH: automation/cli-code-samples
TARGET_FILE: src/codesamples/cli.json
steps:
- name: Checkout source code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: refs/tags/${{ github.event.release.tag_name }}
persist-credentials: false

- name: Install Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod

- name: Create GitHub App token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ secrets.SUMUP_BOT_APP_ID }}
private-key: ${{ secrets.SUMUP_BOT_PRIVATE_KEY }}
owner: sumup
repositories: sumup-developer

- name: Checkout target repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: ${{ env.TARGET_REPOSITORY }}
ref: main
token: ${{ steps.app-token.outputs.token }}
path: sumup-developer
persist-credentials: true

- name: Get GitHub App User ID
id: get-user-id
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"

- name: Configure git
run: |
git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]'
git config --global user.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com'

- name: Prepare target branch
working-directory: sumup-developer
run: |
git fetch origin "${{ env.TARGET_BRANCH }}:refs/remotes/origin/${{ env.TARGET_BRANCH }}" || true
git checkout -B "${{ env.TARGET_BRANCH }}" origin/main

- name: Generate CLI code samples
run: |
mkdir -p "sumup-developer/$(dirname "${{ env.TARGET_FILE }}")"
go run ./internal/cmd/generate-samples \
--cli-version "${{ github.event.release.tag_name }}" \
--out "sumup-developer/${{ env.TARGET_FILE }}"

- name: Commit generated samples
id: commit
working-directory: sumup-developer
run: |
git add "${{ env.TARGET_FILE }}"
if git diff --cached --quiet; then
echo "changed=false" >> "$GITHUB_OUTPUT"
exit 0
fi

git commit -m "chore: update CLI code samples for ${{ github.event.release.tag_name }}"
echo "changed=true" >> "$GITHUB_OUTPUT"

- name: Push branch
if: steps.commit.outputs.changed == 'true'
working-directory: sumup-developer
run: git push --force-with-lease origin "${{ env.TARGET_BRANCH }}"

- name: Create or update pull request
if: steps.commit.outputs.changed == 'true'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
head_ref="sumup:${{ env.TARGET_BRANCH }}"
pr_url="$(gh pr list \
--repo "${{ env.TARGET_REPOSITORY }}" \
--head "$head_ref" \
--base main \
--state open \
--json url \
--jq '.[0].url')"

if [ -n "$pr_url" ]; then
gh pr edit "$pr_url" \
--repo "${{ env.TARGET_REPOSITORY }}" \
--title "chore: update CLI code samples" \
--body "Updates \`${{ env.TARGET_FILE }}\` from \`${{ github.repository }}\` release \`${{ github.event.release.tag_name }}\`."
exit 0
fi

gh pr create \
--repo "${{ env.TARGET_REPOSITORY }}" \
--base main \
--head "${{ env.TARGET_BRANCH }}" \
--title "chore: update CLI code samples" \
--body "Updates \`${{ env.TARGET_FILE }}\` from \`${{ github.repository }}\` release \`${{ github.event.release.tag_name }}\`."
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/target
.envrc
/code-samples.json
10 changes: 10 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ lint-fix: ## Lint go files and apply auto-fixes
test: ## Run tests
go test -v -failfast -race -timeout 10m ./...

.PHONY: generate
generate: ## Generate the OpenAPI operation catalog from the pinned SDK
go generate ./internal/apicommands

CODESAMPLES_OUT ?= code-samples.json

.PHONY: generate-codesamples
generate-codesamples: ## Generate CLI code samples for the developer portal
go run ./internal/cmd/generate-samples --cli-version "$(VERSION)" --out "$(CODESAMPLES_OUT)"

.PHONY: vulncheck
vulncheck: ## Check for Vulnerabilities (make sure you have the tools install: `make install-tools`)
govulncheck ./...
Expand Down
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,38 @@ sumup readers status \
reader_42
```

## OpenAPI command coverage

The CLI keeps its user-facing command implementations handwritten, but derives
an operation catalog from the OpenAPI document shipped with the exact
`sumup-go` version pinned in `go.mod`. The catalog records operation IDs, SDK
clients and methods, HTTP paths, parameters, and request-body metadata.

Run the generator after updating `sumup-go`:

```bash
make generate
```

[`internal/commands/operations.go`](internal/commands/operations.go) maps CLI
command paths to generated OpenAPI operation IDs. Tests enforce parity between
the pinned SDK, the generated catalog, and the CLI command tree, so an SDK
upgrade fails CI until every new endpoint has a corresponding command.

### Developer portal code samples

Generate a deterministic, portal-compatible JSON catalog containing one CLI
code sample for every OpenAPI operation exposed by the CLI:

```bash
VERSION=v0.1.0 make generate-codesamples
```

The catalog is written to `code-samples.json` by default. Set
`CODESAMPLES_OUT` to use a different path. The generated file uses the same
versioned schema as the SDK sample catalogs and is not committed to this
repository. Published releases automatically open or update a pull request
that writes the catalog to `src/codesamples/cli.json` in
`sumup/sumup-developer`.

[docs-badge]: https://img.shields.io/badge/SumUp-documentation-white.svg?logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgY29sb3I9IndoaXRlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogICAgPHBhdGggZD0iTTIyLjI5IDBIMS43Qy43NyAwIDAgLjc3IDAgMS43MVYyMi4zYzAgLjkzLjc3IDEuNyAxLjcxIDEuN0gyMi4zYy45NCAwIDEuNzEtLjc3IDEuNzEtMS43MVYxLjdDMjQgLjc3IDIzLjIzIDAgMjIuMjkgMFptLTcuMjIgMTguMDdhNS42MiA1LjYyIDAgMCAxLTcuNjguMjQuMzYuMzYgMCAwIDEtLjAxLS40OWw3LjQ0LTcuNDRhLjM1LjM1IDAgMCAxIC40OSAwIDUuNiA1LjYgMCAwIDEtLjI0IDcuNjlabTEuNTUtMTEuOS03LjQ0IDcuNDVhLjM1LjM1IDAgMCAxLS41IDAgNS42MSA1LjYxIDAgMCAxIDcuOS03Ljk2bC4wMy4wM2MuMTMuMTMuMTQuMzUuMDEuNDlaIiBmaWxsPSJjdXJyZW50Q29sb3IiLz4KPC9zdmc+
77 changes: 77 additions & 0 deletions internal/apicommands/apicommands.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Package apicommands connects CLI commands to the OpenAPI operations exposed
// by the pinned SumUp Go SDK.
package apicommands

import (
"fmt"

"github.com/urfave/cli/v3"
)

//go:generate go run ../cmd/generate-operations -out catalog.gen.go

const operationIDMetadataKey = "sumup.openapi.operation-id"

// Parameter describes an OpenAPI operation parameter.
type Parameter struct {
Name string
Location string
Description string
Type string
Format string
Required bool
}

// RequestBody describes the JSON request body accepted by an operation.
type RequestBody struct {
Schema string
Required bool
}

// Operation describes one SDK method generated from an OpenAPI operation.
type Operation struct {
ID string
Client string
SDKMethod string
HTTPMethod string
Path string
Summary string
Description string
Parameters []Parameter
RequestBody *RequestBody
}

// Lookup returns the generated operation with the given OpenAPI operation ID.
func Lookup(operationID string) (Operation, bool) {
for _, operation := range Operations {
if operation.ID == operationID {
return operation, true
}
}

return Operation{}, false
}

// Bind records which OpenAPI operation a CLI command exposes.
func Bind(command *cli.Command, operationID string) {
if command == nil {
panic("cannot bind an OpenAPI operation to a nil command")
}
if _, ok := Lookup(operationID); !ok {
panic(fmt.Sprintf("unknown OpenAPI operation %q", operationID))
}
if command.Metadata == nil {
command.Metadata = make(map[string]any)
}
command.Metadata[operationIDMetadataKey] = operationID
}

// OperationID returns the OpenAPI operation ID bound to a CLI command.
func OperationID(command *cli.Command) (string, bool) {
if command == nil || command.Metadata == nil {
return "", false
}

operationID, ok := command.Metadata[operationIDMetadataKey].(string)
return operationID, ok && operationID != ""
}
Loading
Loading