Skip to content

Commit 23007d3

Browse files
author
Marvin
committed
chore: add llms.txt, improve CLAUDE.md, native ARM64 Docker builds
- Add llms.txt for LLM-friendly project documentation (closes #9) - Improve CLAUDE.md with data update workflow, provider interfaces, functional options pattern, testing approach, and common mistakes (closes #10) - Replace QEMU emulation with native ARM64 runners (ubuntu-24.04-arm) for faster multi-arch Docker builds via matrix strategy + manifest merge (closes #11) Co-authored-by: Marvin <marvin@openclaw.ai>
1 parent 30c134b commit 23007d3

File tree

3 files changed

+223
-19
lines changed

3 files changed

+223
-19
lines changed

.github/workflows/docker.yaml

Lines changed: 89 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,28 +18,100 @@ env:
1818
IMAGE_NAME: ${{ github.repository }}
1919

2020
jobs:
21-
docker:
22-
runs-on: ubuntu-latest
23-
timeout-minutes: 30
21+
build:
22+
name: Build (${{ matrix.platform }})
23+
runs-on: ${{ matrix.runner }}
24+
timeout-minutes: 20
2425
permissions:
2526
contents: read
2627
packages: write
2728
id-token: write
28-
29+
30+
strategy:
31+
fail-fast: false
32+
matrix:
33+
include:
34+
- platform: linux/amd64
35+
runner: ubuntu-24.04
36+
- platform: linux/arm64
37+
runner: ubuntu-24.04-arm
38+
2939
steps:
3040
- name: Checkout code
3141
uses: actions/checkout@v4
32-
42+
43+
- name: Set up Docker Buildx
44+
uses: docker/setup-buildx-action@v3
45+
46+
- name: Log in to Container Registry
47+
uses: docker/login-action@v3
48+
with:
49+
registry: ${{ env.REGISTRY }}
50+
username: ${{ github.actor }}
51+
password: ${{ secrets.GITHUB_TOKEN }}
52+
53+
- name: Extract metadata
54+
id: meta
55+
uses: docker/metadata-action@v5
56+
with:
57+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
58+
59+
- name: Build and push by digest
60+
id: build
61+
uses: docker/build-push-action@v5
62+
with:
63+
context: .
64+
platforms: ${{ matrix.platform }}
65+
push: ${{ github.event_name != 'pull_request' }}
66+
labels: ${{ steps.meta.outputs.labels }}
67+
outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }}
68+
cache-from: type=gha,scope=${{ matrix.platform }}
69+
cache-to: type=gha,mode=max,scope=${{ matrix.platform }}
70+
71+
- name: Export digest
72+
if: github.event_name != 'pull_request'
73+
run: |
74+
mkdir -p /tmp/digests
75+
digest="${{ steps.build.outputs.digest }}"
76+
touch "/tmp/digests/${digest#sha256:}"
77+
78+
- name: Upload digest
79+
if: github.event_name != 'pull_request'
80+
uses: actions/upload-artifact@v4
81+
with:
82+
name: digests-${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }}
83+
path: /tmp/digests/*
84+
if-no-files-found: error
85+
retention-days: 1
86+
87+
merge:
88+
name: Merge multi-arch manifest
89+
runs-on: ubuntu-latest
90+
if: github.event_name != 'pull_request'
91+
needs: build
92+
timeout-minutes: 10
93+
permissions:
94+
contents: read
95+
packages: write
96+
97+
steps:
98+
- name: Download digests
99+
uses: actions/download-artifact@v4
100+
with:
101+
path: /tmp/digests
102+
pattern: digests-*
103+
merge-multiple: true
104+
33105
- name: Set up Docker Buildx
34106
uses: docker/setup-buildx-action@v3
35-
107+
36108
- name: Log in to Container Registry
37109
uses: docker/login-action@v3
38110
with:
39111
registry: ${{ env.REGISTRY }}
40112
username: ${{ github.actor }}
41113
password: ${{ secrets.GITHUB_TOKEN }}
42-
114+
43115
- name: Extract metadata
44116
id: meta
45117
uses: docker/metadata-action@v5
@@ -52,14 +124,13 @@ jobs:
52124
type=semver,pattern={{major}}.{{minor}}
53125
type=semver,pattern={{major}}
54126
type=raw,value=latest,enable={{is_default_branch}}
55-
56-
- name: Build and push Docker image
57-
uses: docker/build-push-action@v5
58-
with:
59-
context: .
60-
platforms: linux/amd64,linux/arm64
61-
push: ${{ github.event_name != 'pull_request' }}
62-
tags: ${{ steps.meta.outputs.tags }}
63-
labels: ${{ steps.meta.outputs.labels }}
64-
cache-from: type=gha
65-
cache-to: type=gha,mode=max
127+
128+
- name: Create and push manifest
129+
working-directory: /tmp/digests
130+
run: |
131+
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
132+
$(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
133+
134+
- name: Inspect manifest
135+
run: |
136+
docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}

CLAUDE.md

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,86 @@ The CLI supports multiple output formats: number, text, json, table, csv
9696
- Run `make test-verbose` before committing changes
9797
- Update embedded data with `make update-data update-price` when needed
9898
- Follow Go 1.24 best practices and modern testing patterns
99-
- NEVER add Claude as co-author to git commit message
99+
- NEVER add Claude as co-author to git commit message
100+
## Data Update Workflow
101+
102+
The embedded data files are critical — they provide offline capability:
103+
- `internal/spot/data/spot-advisor-data.json` — Interruption rates, savings % (from AWS S3)
104+
- `internal/spot/data/spot.js` — Static spot pricing (from AWS S3, wrapped in JS callback)
105+
106+
**Update flow:**
107+
1. `make update-data` — fetches fresh `spot-advisor-data.json`
108+
2. `make update-price` — fetches fresh `spot.js`
109+
3. `go test ./...` — verify embedded data parses correctly
110+
4. Commit both files with the binary update
111+
112+
**When to update:** Before each release, and when new instance families appear showing $0 price.
113+
114+
## Provider Interfaces (Key Pattern)
115+
116+
All data sources use interfaces for testability. Never call AWS directly in tests.
117+
118+
```go
119+
// advisorProvider — embedded/remote advisor data
120+
type advisorProvider interface {
121+
getRegions() []string
122+
getRegionAdvice(region, os string) (map[string]spotAdvice, error)
123+
getInstanceType(instance string) (TypeInfo, error)
124+
getRange(index int) (Range, error)
125+
}
126+
127+
// pricingProvider — static pricing data
128+
type pricingProvider interface {
129+
getSpotPrice(instance, region, os string) (float64, error)
130+
}
131+
132+
// livePriceProvider — EC2 API fallback for zero-price instances
133+
type livePriceProvider interface {
134+
fetchLivePrices(ctx context.Context, region string, instanceTypes []string, os string) (map[string]float64, error)
135+
}
136+
137+
// scoreProvider — EC2 placement scores
138+
type scoreProvider interface {
139+
enrichWithScores(ctx context.Context, advices []Advice, singleAZ bool, timeout time.Duration) error
140+
}
141+
```
142+
143+
In tests: use `mocks_test.go` which implements all interfaces with controllable behavior.
144+
In production: use `NewWithOptions()` which wires up real AWS providers.
145+
For injection: use `NewWithProviders(advisor, pricing)` + `SetLivePriceProvider()`.
146+
147+
## Functional Options Pattern
148+
149+
`GetSpotSavings` uses functional options — add new filters without breaking existing callers:
150+
151+
```go
152+
// Adding a new filter option:
153+
func WithFoo(foo string) GetSpotSavingsOption {
154+
return func(cfg *getSpotSavingsConfig) {
155+
cfg.foo = foo
156+
}
157+
}
158+
// Add `foo string` field to getSpotSavingsConfig
159+
// Apply in GetSpotSavings() after the options loop
160+
```
161+
162+
## Testing Approach
163+
164+
- **Unit tests** use mock providers from `mocks_test.go` — no AWS credentials needed
165+
- **Integration tests** require real AWS credentials — skip with `-short` flag
166+
- **Pattern**: `if testing.Short() { t.Skip("requires AWS credentials") }`
167+
- **Parallel**: all unit tests use `t.Parallel()` — keep it that way
168+
- **Table-driven**: use `tc := tc` (loop variable capture) or Go 1.22+ range semantics
169+
170+
When adding a new feature:
171+
1. Add mock support in `mocks_test.go` if new interface method needed
172+
2. Write unit test with mock provider
173+
3. Optionally add integration test guarded by `testing.Short()`
174+
175+
## Common Mistakes to Avoid
176+
177+
- **Never** call `enrichMissingPrices` with a nil provider — it's a no-op but check the guard
178+
- **Never** forget `Advice.LivePrice = true` when price comes from EC2 API (not static feed)
179+
- **Never** bypass the `maxPrice` re-filter after live price enrichment
180+
- `allRegionsKeyword = "all"` is the special value for `--region all`, not an actual region
181+
- `defaultScoreTimeout` and `livePriceTimeout` are separate — don't confuse them

llms.txt

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# spotinfo
2+
3+
> CLI tool and MCP server for AWS EC2 Spot Instance pricing, savings, and interruption frequency data.
4+
5+
`spotinfo` provides comprehensive access to AWS Spot Instance data — real-time pricing, savings percentages, interruption rates, and placement scores. It embeds AWS data feeds for offline capability and supports multiple output formats (table, JSON, CSV, text). Also works as an MCP server for AI assistant integration.
6+
7+
## Docs
8+
9+
- [README](README.md)
10+
- [Usage Guide](docs/usage.md)
11+
- [MCP Server Integration](docs/mcp-server.md)
12+
- [Data Sources](docs/data-sources.md)
13+
- [CLAUDE.md](CLAUDE.md) - AI coding guidelines for this project
14+
15+
## Key Features
16+
17+
- **Spot pricing**: current spot prices per instance type, region, and OS
18+
- **Interruption rates**: AWS Spot Advisor interruption frequency ranges (0-5%, 5-10%, etc.)
19+
- **Savings**: percentage savings vs on-demand pricing
20+
- **Placement scores**: real-time AWS EC2 placement scores (1-10) for launch success probability
21+
- **Live price fallback**: EC2 DescribeSpotPriceHistory API for newer instances missing from static feed
22+
- **Embedded data**: works offline using embedded AWS data files
23+
- **MCP server**: expose spot data to Claude, Cursor, and other MCP-compatible AI tools
24+
- **Flexible filtering**: by region, instance type (regex), vCPU, memory, price, placement score
25+
- **Multi-region**: compare across regions with `--region all`
26+
- **Output formats**: table, JSON, CSV, plain text
27+
28+
## Architecture
29+
30+
- `cmd/spotinfo/` — CLI entry point and flag definitions
31+
- `internal/spot/` — core business logic
32+
- `client.go` — `Client` struct with functional options pattern
33+
- `data.go` — embedded data fetching and parsing
34+
- `liveprice.go` — `livePriceProvider` interface, EC2 API live price enrichment
35+
- `score.go` — placement score fetching and caching
36+
- `types.go` — shared types (`Advice`, `TypeInfo`, `Range`, etc.)
37+
- `internal/mcp/` — MCP server tools
38+
39+
## Data Sources
40+
41+
- Spot Advisor data: `https://spot-price.s3.amazonaws.com/spot-advisor-data.json`
42+
- Static pricing: `https://spot-price.s3.amazonaws.com/spot.js`
43+
- Live prices: EC2 `DescribeSpotPriceHistory` API (fallback for zero-price instances)
44+
45+
## Install
46+
47+
```bash
48+
brew install alexei-led/tap/spotinfo
49+
```
50+
51+
Or download from [GitHub Releases](https://github.com/alexei-led/spotinfo/releases).

0 commit comments

Comments
 (0)