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
135 changes: 135 additions & 0 deletions .github/workflows/sync-openapi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
name: Sync OpenAPI client

# Daily: pull the latest OpenAPI spec, regenerate the Go REST client with
# oapi-codegen, and open a brand-new PR only when the regenerated output differs
# from what's committed. Mirrors the automation in massive-com/client-jvm and
# massive-com/client-js (adapted to the Go toolchain).

on:
schedule:
- cron: '0 15 * * *' # 07:00 America/Vancouver
workflow_dispatch:

permissions:
contents: write
pull-requests: write

concurrency:
group: sync-openapi
cancel-in-progress: false

jobs:
sync:
runs-on: ubuntu-latest
steps:
# Mint a short-lived token for the org GitHub App. The default GITHUB_TOKEN
# can't open PRs (org policy: "Allow GitHub Actions to create and approve
# pull requests" is off). The App token isn't subject to that restriction,
# triggers required checks, and authors the PR as the app bot so a human
# can still review.
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ vars.MASSIVE_CLIENT_LIBRARY_AUTOMATION_APP_ID }}
private-key: ${{ secrets.MASSIVE_CLIENT_LIBRARY_AUTOMATION_APP_PRIVATE_KEY }}

- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
persist-credentials: true

# The generator is oapi-codegen, a Go tool invoked via `go run <pkg>@<ver>`
# from scripts/generate.sh. setup-go caches the module + build cache, so the
# pinned generator is only downloaded/compiled once across runs.
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24.x'

# Node runs the spec pull (rest/scripts/pull_spec.js) and the generated-code
# post-processing (rest/scripts/fix-go-clashes.js).
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: '22'

# jq is preinstalled on ubuntu-latest runners (used to pre-process the spec).
- name: Regenerate client
run: bash scripts/generate.sh

- name: Detect changes
id: diff
run: |
git add -A
if git diff --cached --quiet; then
echo "changed=false" >> "$GITHUB_OUTPUT"
echo "No changes — spec + generated output match what's committed."
else
echo "changed=true" >> "$GITHUB_OUTPUT"
echo "Changes detected:"
git diff --cached --stat | tail -25
fi

- name: Import GPG signing key
if: steps.diff.outputs.changed == 'true'
uses: crazy-max/ghaction-import-gpg@v6
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
git_user_signingkey: true
git_commit_gpgsign: true

# A unique branch per run (date + run id) so every sync opens a brand-new
# PR and never reuses/updates a previous one.
- name: Commit and push
if: steps.diff.outputs.changed == 'true'
run: |
git config user.name "justinpolygon"
git config user.email "123573436+justinpolygon@users.noreply.github.com"
BRANCH="bot/openapi-sync-$(date -u +'%Y-%m-%d')-${GITHUB_RUN_ID}"
echo "BRANCH=$BRANCH" >> "$GITHUB_ENV"
git checkout -B "$BRANCH"
git commit -S -m "Sync client-go with OpenAPI spec"
git push origin "$BRANCH"

- name: Open PR
if: steps.diff.outputs.changed == 'true'
id: pr
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
url=$(gh pr create \
--base master --head "$BRANCH" \
--title "[bot] Sync client-go with OpenAPI spec ($(date -u +'%Y-%m-%d'))" \
--body "Automated regeneration from \`https://api.massive.com/openapi\` via \`scripts/generate.sh\` (oapi-codegen v2.5.1).

- Regenerated REST client: \`rest/gen/client.gen.go\` + committed spec \`rest/scripts/openapi.json\`
- Hand-written WebSocket client (\`websocket/\`) and REST helpers (\`rest/client.go\`, \`rest/iterator.go\`) preserved
- Curated \`README.md\` / \`go.mod\` preserved

Please review the diff before merging.")
echo "url=$url" >> "$GITHUB_OUTPUT"
# Best-effort label — don't fail the run if the app lacks label perms.
gh label create automated --color ededed --description "Automated PR" --force >/dev/null 2>&1 || true
gh pr edit "$url" --add-label automated >/dev/null 2>&1 || true

- name: Notify Slack
if: steps.diff.outputs.changed == 'true'
env:
SLACK_CLIENT_LIBRARY_WEBHOOK: ${{ secrets.SLACK_CLIENT_LIBRARY_WEBHOOK }}
PR_URL: ${{ steps.pr.outputs.url }}
run: |
if [ -z "$SLACK_CLIENT_LIBRARY_WEBHOOK" ]; then
echo "No Slack webhook configured — skipping."
exit 0
fi
payload=$(jq -n --arg url "$PR_URL" --arg date "$(date -u +'%Y-%m-%d')" '{
blocks: [
{ type: "header", text: { type: "plain_text", text: ("client-go OpenAPI sync – " + $date), emoji: true } },
{ type: "section", text: { type: "mrkdwn", text: ("A new OpenAPI sync PR is ready for review:\n<" + $url + "|" + $url + ">") } }
]
}')
resp=$(curl -sS -X POST -H 'Content-type: application/json' --data "$payload" "$SLACK_CLIENT_LIBRARY_WEBHOOK")
[ "$resp" = "ok" ] && echo "✅ Slack posted" || echo "❌ Slack post failed: $resp"
40 changes: 39 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The official Go client library for the [Massive](https://massive.com/) REST and

For details, see our [rebrand announcement blog post](https://massive.com/blog/polygon-is-now-massive/) or open an issue / contact [support@massive.com](mailto:support@massive.com) if you have questions.

## How this SDK stays up to date

The REST client is generated from the Massive.com OpenAPI spec. A scheduled GitHub Actions workflow ([`.github/workflows/sync-openapi.yml`](./.github/workflows/sync-openapi.yml)) runs **daily**: it pulls the latest spec, regenerates the client with [oapi-codegen](https://github.com/oapi-codegen/oapi-codegen), and — only when the output differs from what's committed — opens a brand-new `[bot]`-prefixed pull request on a unique `bot/openapi-sync-<date>-<run-id>` branch for a human to review and merge. Each run opens its own PR (never reusing a previous one), so the author and reviewer are always different people. You can also trigger it on demand from the Actions tab ("Run workflow"). See [`scripts/readme.md`](./scripts/readme.md) for the maintainer details.

## Getting Started

This section guides you through setting up a simple project with massive.com/client-go.
Expand All @@ -20,7 +24,7 @@ Next, initialize a new module for dependency management. This creates a `go.mod`
go mod init example
```

Then, create a `main.go` file. For quick start, you can find over 100+ [example code snippets](https://github.com/massive-com/client-go/tree/master/rest/example) that demonstrate connecting to both the REST and WebSocket APIs.
Then, create a `main.go` file. For a quick start, use the REST example below, or see the runnable [WebSocket examples](./websocket/example) that demonstrate connecting to the streaming APIs.

Here's a working example that fetches daily aggregates for AAPL (with full pagination and trace support):

Expand Down Expand Up @@ -251,6 +255,40 @@ for {

See the [full example](./websocket/example/main.go) for more details on how to use this client effectively.

## Developing & regenerating the client

The repository is a mix of generated and hand-written code:

| Path | Category | Notes |
| --- | --- | --- |
| `rest/gen/client.gen.go` | **Generated** | REST client + models, produced by oapi-codegen. Overwritten on every regen — do not edit by hand. |
| `rest/scripts/openapi.json` | **Committed spec** | The filtered OpenAPI spec the client is generated from. Written by `pull_spec.js`; committed so spec changes are visible in PR diffs. |
| `rest/client.go`, `rest/iterator.go` | **Hand-written** | Client constructor, options, pagination iterator. |
| `websocket/` | **Hand-written** | The entire WebSocket client. Never touched by generation. |
| `README.md`, `go.mod`, `LICENSE` | **Curated** | Never touched by generation. |
| `scripts/generate.sh`, `rest/scripts/*` | **Tooling** | The generation pipeline (see [`scripts/readme.md`](./scripts/readme.md)). |

### Regenerate locally

Prerequisites: **Go 1.21+**, **Node.js 18+**, and **jq**.

```bash
bash scripts/generate.sh
go build ./... && go test ./...
```

`scripts/generate.sh` pulls the latest spec, pre-processes it, runs the pinned generator (`oapi-codegen v2.5.1`, invoked via `go run <pkg>@<version>` so the version is reproducible), and post-processes the output. It aborts if the spec pull returns nothing or if generation produces no client, so a bad run never clobbers the committed client.

If you don't have the toolchain locally, run it in a container that matches CI (Go 1.24 + Node 22 + jq):

```bash
docker run --rm -v "$PWD":/src -w /src golang:1.24-bookworm bash -c '
apt-get update && apt-get install -y jq curl >/dev/null &&
curl -fsSL https://deb.nodesource.com/setup_22.x | bash - >/dev/null &&
apt-get install -y nodejs >/dev/null &&
bash scripts/generate.sh'
```

## Contributing

If you found a bug or have an idea for a new feature, please first discuss it with us by [submitting a new issue](https://github.com/massive-com/client-go/issues/new/choose). We will respond to issues within at most 3 weeks. We're also open to volunteers if you want to submit a PR for any open issues but please discuss it with us beforehand. PRs that aren't linked to an existing issue or discussed with us ahead of time will generally be declined.
Expand Down
39 changes: 0 additions & 39 deletions rest/generation.sh

This file was deleted.

88 changes: 88 additions & 0 deletions scripts/generate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#!/usr/bin/env bash
#
# generate.sh — one-command regeneration of the Massive.com Go REST client.
#
# Reproduces the committed repo layout deterministically:
# 1. Pull the OpenAPI spec -> rest/scripts/openapi.json
# 2. Pre-process the spec (jq) (fix invalid numeric schemas)
# 3. Generate the Go REST client with oapi-codegen into the isolated
# package rest/gen/client.gen.go, REPLACING only that generated file.
# 4. Post-process (fix single-letter JSON field clashes + gofmt).
#
# oapi-codegen only understands the REST endpoints. Hand-written code
# (rest/client.go, rest/iterator.go and the entire websocket/ package) and
# curated files (README.md, go.mod, LICENSE) live OUTSIDE rest/gen/ and are
# never touched by this script.
#
# The generator version is PINNED below (OAPI_CODEGEN_VERSION) and invoked via
# `go run <module>@<version>` so a run is reproducible regardless of what
# `oapi-codegen` happens to be on PATH — diffs then reflect spec changes, not
# generator upgrades. (There is no openapitools.json here: that file configures
# the Java openapi-generator, which this Go SDK does not use.)
#
# Usage (from anywhere):
# bash scripts/generate.sh
#
# Requirements: bash, Go (1.21+), Node.js (18+), jq.
#
set -euo pipefail

# Pinned generator version — keep in lockstep with the cache comment in
# .github/workflows/sync-openapi.yml.
OAPI_CODEGEN_VERSION="v2.5.1"
OAPI_CODEGEN_PKG="github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen"

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$ROOT"

REST_DIR="$ROOT/rest"
SCRIPTS_DIR="$REST_DIR/scripts"
SPEC_FILE="$SCRIPTS_DIR/openapi.json"
GEN_FILE="$REST_DIR/gen/client.gen.go"
GEN_CONFIG="./scripts/oapi-codegen.yaml" # relative to REST_DIR

echo "==> [1/4] Pulling OpenAPI spec -> ${SPEC_FILE#$ROOT/}"
# pull_spec.js writes ./openapi.json relative to its cwd, so run it from there.
( cd "$SCRIPTS_DIR" && node pull_spec.js )

# Safety gate: never regenerate against a missing/empty spec (pull_spec.js only
# logs fetch failures, it does not exit non-zero — so a failed pull would
# otherwise leave a stale or empty spec in place).
if [ ! -s "$SPEC_FILE" ]; then
echo "ERROR: $SPEC_FILE is missing or empty after pull_spec.js; aborting." >&2
exit 1
fi

echo "==> [2/4] Pre-processing spec (fixing invalid 'number'+'int32' schemas)"
FIXED_SPEC="$(mktemp -t openapi-fixed.XXXXXX.json)"
trap 'rm -f "$FIXED_SPEC"' EXIT
jq '
walk(
if type == "object" and .type == "number" and .format == "int32" then
.type = "integer"
else
.
end
)
' "$SPEC_FILE" > "$FIXED_SPEC"

echo "==> [3/4] Generating Go client with oapi-codegen ${OAPI_CODEGEN_VERSION}"
rm -rf "$REST_DIR/gen"
# oapi-codegen resolves the config's `output:` relative to its cwd, so run it
# from REST_DIR to land the file at rest/gen/client.gen.go.
( cd "$REST_DIR" && go run "${OAPI_CODEGEN_PKG}@${OAPI_CODEGEN_VERSION}" \
-config "$GEN_CONFIG" \
"$FIXED_SPEC" )

# Safety gate: never leave a partial/empty generated client committed.
if [ ! -s "$GEN_FILE" ]; then
echo "ERROR: generation did not produce $GEN_FILE; aborting." >&2
exit 1
fi

echo "==> [4/4] Post-processing (fix JSON field clashes + gofmt)"
node "$SCRIPTS_DIR/fix-go-clashes.js"

echo "Done. Regenerated ${GEN_FILE#$ROOT/} from ${SPEC_FILE#$ROOT/}"
echo " (hand-written rest/client.go, rest/iterator.go and websocket/ left untouched)"
Loading
Loading