Skip to content

Commit 8102fe5

Browse files
pablo-albaladejoPablo Albaladejoclaude
authored
feat(garmin-connect): add Garmin Connect API client package (#131)
* feat(garmin-connect): add Garmin Connect API client package Add @kaiord/garmin-connect package that provides HTTP transport for communicating with Garmin Connect. Implements SSO authentication (CSRF + credentials + OAuth1/OAuth2 exchange), Bearer token management with auto-refresh, and full workout CRUD (push/pull/list/delete). Also adds generic ports to @kaiord/core (WorkoutService, AuthProvider, TokenStore) designed to be reusable for other platforms (Strava, etc). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: apply lint fixes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(garmin-connect): address CodeRabbit review comments - Fix broken export_tokens/restore_tokens round-trip (track oauth1 token) - Fix refresh token failure hanging subscribers (try/catch/finally + reject) - Add clearTokens() to HTTP client, use in logout instead of zombie tokens - Fix checkAccountLocked to only throw on ACCOUNT_LOCKED status - Fix checkPageTitle to use captured group substring match - Add HTTP response status checks in SSO functions - Refactor workout service into standalone helper functions (<40 lines each) - Wire Zod schemas for runtime validation of push/list responses - Consistent error wrapping with ServiceApiError across all CRUD operations - Move types before import in package.json exports - Add token refresh tests (proactive, reactive, coalescing, failure) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(garmin-connect): address second round of review comments - Add res.ok check in fetchConsumer before parsing JSON - Use block bodies in forEach callbacks to avoid implicit returns - Set file permissions to 0o600 for token store (owner-only) - Document cookie-aware fetch requirement on garminSso Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(garmin-connect): add cookie-aware fetch, simplify API, integration tests - Add fetch-cookie for SSO cookie persistence across requests - Simplify API surface to only login, list, and push operations - Remove pull and remove operations - Split HTTP modules for readability (<100 lines per file) - Extract shared types, token refresh, and OAuth consumer modules - Add integration tests with real Garmin Connect login - Add CI integration-garmin job with GitHub secrets - Add JSDoc to Zod response schemas Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore(garmin-connect): apply prettier formatting Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(garmin-connect): address CodeRabbit review comments and fix integration tests - Fix vitest integration config: use dedicated config file instead of --testPathPattern - Remove unsafe default fetchFn in garminSso (force cookie-aware fetch) - Add res.ok check on 401-retry path in authFetch - Handle 204 empty responses in del method - Add HTTP status check on CSRF page fetch - Fix CI always() condition for integration-garmin job - Document third-party S3 dependency for OAuth consumer Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: remove claude-code-review workflow The Anthropic API account has no credits, causing the workflow to fail on every PR. Remove until billing is resolved. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(garmin-connect): accept string dates in workout summary schema Garmin API returns createdDate/updatedDate as strings, not numbers. Update Zod schema and mapper types to accept both. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor(garmin-connect): move integration test next to entry point Move from src/adapters/integration/ to src/index.integration.test.ts to follow convention of keeping tests alongside their source files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(garmin-connect): add README and npm publish/release configuration Add garmin-connect to changeset linked packages, release workflow version tracking, and GitHub releases script. Add README matching monorepo style. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore(garmin-connect): apply prettier formatting to README Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Pablo Albaladejo <pablo.albaladejo@aircall.io> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4b45e02 commit 8102fe5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+2384
-152
lines changed

.changeset/config.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"@kaiord/tcx",
1111
"@kaiord/zwo",
1212
"@kaiord/garmin",
13+
"@kaiord/garmin-connect",
1314
"@kaiord/cli",
1415
"@kaiord/mcp"
1516
]

.github/workflows/ci.yml

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ jobs:
3232
outputs:
3333
core-changed: ${{ steps.changes.outputs.core-changed }}
3434
garmin-changed: ${{ steps.changes.outputs.garmin-changed }}
35+
garmin-connect-changed: ${{ steps.changes.outputs.garmin-connect-changed }}
3536
frontend-changed: ${{ steps.changes.outputs.frontend-changed }}
3637
should-test: ${{ steps.changes.outputs.should-test }}
3738
steps:
@@ -49,6 +50,8 @@ jobs:
4950
- 'packages/core/**'
5051
garmin:
5152
- 'packages/garmin/**'
53+
garmin_connect:
54+
- 'packages/garmin-connect/**'
5255
frontend:
5356
- 'packages/workout-spa-editor/**'
5457
root_deps:
@@ -81,6 +84,14 @@ jobs:
8184
echo "garmin-changed=false" >> $GITHUB_OUTPUT
8285
fi
8386
87+
# Garmin Connect package changes
88+
if [[ "${{ steps.changed-files.outputs.garmin_connect_any_changed }}" == "true" ]] || \
89+
[[ "${{ steps.changed-files.outputs.root_deps_any_changed }}" == "true" ]]; then
90+
echo "garmin-connect-changed=true" >> $GITHUB_OUTPUT
91+
else
92+
echo "garmin-connect-changed=false" >> $GITHUB_OUTPUT
93+
fi
94+
8495
# Frontend package changes
8596
if [[ "${{ steps.changed-files.outputs.frontend_any_changed }}" == "true" ]] || \
8697
[[ "${{ steps.changed-files.outputs.root_deps_any_changed }}" == "true" ]]; then
@@ -94,6 +105,7 @@ jobs:
94105
if [[ "${{ steps.changed-files.outputs.docs_any_changed }}" == "true" ]] && \
95106
[[ "${{ steps.changed-files.outputs.core_any_changed }}" == "false" ]] && \
96107
[[ "${{ steps.changed-files.outputs.garmin_any_changed }}" == "false" ]] && \
108+
[[ "${{ steps.changed-files.outputs.garmin_connect_any_changed }}" == "false" ]] && \
97109
[[ "${{ steps.changed-files.outputs.frontend_any_changed }}" == "false" ]] && \
98110
[[ "${{ steps.changed-files.outputs.root_deps_any_changed }}" == "false" ]]; then
99111
echo "should-test=false" >> $GITHUB_OUTPUT
@@ -105,6 +117,7 @@ jobs:
105117
run: |
106118
echo "Core changed: ${{ steps.changes.outputs.core-changed }}"
107119
echo "Garmin changed: ${{ steps.changes.outputs.garmin-changed }}"
120+
echo "Garmin Connect changed: ${{ steps.changes.outputs.garmin-connect-changed }}"
108121
echo "Frontend changed: ${{ steps.changes.outputs.frontend-changed }}"
109122
echo "Should test: ${{ steps.changes.outputs.should-test }}"
110123
@@ -156,7 +169,7 @@ jobs:
156169
fail-fast: false
157170
matrix:
158171
node-version: ["20.x", "22.x", "24.x"]
159-
package: ["core", "garmin"]
172+
package: ["core", "garmin", "garmin-connect"]
160173
steps:
161174
- name: Checkout code
162175
uses: actions/checkout@v6
@@ -211,7 +224,7 @@ jobs:
211224

212225
- name: Verify build outputs
213226
run: |
214-
for pkg in core garmin; do
227+
for pkg in core garmin garmin-connect; do
215228
if [ ! -d "packages/$pkg/dist" ]; then
216229
echo "::error::@kaiord/$pkg build output not found"
217230
exit 1
@@ -328,6 +341,31 @@ jobs:
328341
path: round-trip-output.txt
329342
retention-days: 30
330343

344+
integration-garmin:
345+
runs-on: ubuntu-latest
346+
needs: [detect-changes, build]
347+
if: |
348+
needs.build.result == 'success' &&
349+
needs.detect-changes.outputs.should-test == 'true' &&
350+
(needs.detect-changes.outputs.garmin-connect-changed == 'true' || github.ref == 'refs/heads/main')
351+
steps:
352+
- name: Checkout code
353+
uses: actions/checkout@v6
354+
355+
- name: Setup pnpm with caching
356+
uses: ./.github/actions/setup-pnpm
357+
with:
358+
node-version: "20.x"
359+
360+
- name: Build dependencies
361+
run: pnpm -r build
362+
363+
- name: Run Garmin Connect integration tests
364+
env:
365+
GARMIN_TEST_EMAIL: ${{ secrets.GARMIN_TEST_EMAIL }}
366+
GARMIN_TEST_PASSWORD: ${{ secrets.GARMIN_TEST_PASSWORD }}
367+
run: pnpm --filter @kaiord/garmin-connect test:integration
368+
331369
# Summary jobs for branch protection
332370
# These jobs have fixed names that match branch protection requirements
333371
lint-summary:
@@ -390,7 +428,16 @@ jobs:
390428
name: Notify on Failure
391429
runs-on: ubuntu-latest
392430
needs:
393-
[detect-changes, lint, typecheck, test, test-frontend, build, round-trip]
431+
[
432+
detect-changes,
433+
lint,
434+
typecheck,
435+
test,
436+
test-frontend,
437+
build,
438+
round-trip,
439+
integration-garmin,
440+
]
394441
if: |
395442
always() &&
396443
github.ref == 'refs/heads/main' &&
@@ -399,7 +446,8 @@ jobs:
399446
needs.test.result == 'failure' ||
400447
needs.test-frontend.result == 'failure' ||
401448
needs.build.result == 'failure' ||
402-
needs.round-trip.result == 'failure')
449+
needs.round-trip.result == 'failure' ||
450+
needs.integration-garmin.result == 'failure')
403451
permissions:
404452
issues: write
405453
steps:
@@ -427,6 +475,9 @@ jobs:
427475
if ('${{ needs.round-trip.result }}' === 'failure') {
428476
failedJobs.push('Round-trip Tests');
429477
}
478+
if ('${{ needs.integration-garmin.result }}' === 'failure') {
479+
failedJobs.push('Integration (Garmin Connect)');
480+
}
430481
431482
const title = `🚨 CI Failure on main branch`;
432483

.github/workflows/claude-code-review.yml

Lines changed: 0 additions & 143 deletions
This file was deleted.

.github/workflows/release.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ on:
1111
- "packages/tcx/**"
1212
- "packages/zwo/**"
1313
- "packages/garmin/**"
14+
- "packages/garmin-connect/**"
1415
- "packages/cli/**"
1516
- "packages/mcp/**"
1617
workflow_dispatch:
@@ -81,6 +82,7 @@ jobs:
8182
# Store current versions
8283
CORE_VERSION_BEFORE=$(node -p "require('./packages/core/package.json').version")
8384
GARMIN_VERSION_BEFORE=$(node -p "require('./packages/garmin/package.json').version")
85+
GARMIN_CONNECT_VERSION_BEFORE=$(node -p "require('./packages/garmin-connect/package.json').version")
8486
MCP_VERSION_BEFORE=$(node -p "require('./packages/mcp/package.json').version")
8587
CLI_VERSION_BEFORE=$(node -p "require('./packages/cli/package.json').version")
8688
@@ -96,12 +98,14 @@ jobs:
9698
pnpm --filter @kaiord/tcx build
9799
pnpm --filter @kaiord/zwo build
98100
pnpm --filter @kaiord/garmin build
101+
pnpm --filter @kaiord/garmin-connect build
99102
pnpm --filter @kaiord/mcp build
100103
pnpm --filter @kaiord/cli build
101104
102105
# Get new versions
103106
CORE_VERSION_AFTER=$(node -p "require('./packages/core/package.json').version")
104107
GARMIN_VERSION_AFTER=$(node -p "require('./packages/garmin/package.json').version")
108+
GARMIN_CONNECT_VERSION_AFTER=$(node -p "require('./packages/garmin-connect/package.json').version")
105109
MCP_VERSION_AFTER=$(node -p "require('./packages/mcp/package.json').version")
106110
CLI_VERSION_AFTER=$(node -p "require('./packages/cli/package.json').version")
107111
@@ -120,6 +124,13 @@ jobs:
120124
echo "garmin-changed=false" >> $GITHUB_OUTPUT
121125
fi
122126
127+
if [ "$GARMIN_CONNECT_VERSION_BEFORE" != "$GARMIN_CONNECT_VERSION_AFTER" ]; then
128+
echo "garmin-connect-changed=true" >> $GITHUB_OUTPUT
129+
echo "garmin-connect-version=$GARMIN_CONNECT_VERSION_AFTER" >> $GITHUB_OUTPUT
130+
else
131+
echo "garmin-connect-changed=false" >> $GITHUB_OUTPUT
132+
fi
133+
123134
if [ "$MCP_VERSION_BEFORE" != "$MCP_VERSION_AFTER" ]; then
124135
echo "mcp-changed=true" >> $GITHUB_OUTPUT
125136
echo "mcp-version=$MCP_VERSION_AFTER" >> $GITHUB_OUTPUT
@@ -174,6 +185,10 @@ jobs:
174185
echo "- @kaiord/garmin@${{ steps.version.outputs.garmin-version }}" >> $GITHUB_STEP_SUMMARY
175186
fi
176187
188+
if [ "${{ steps.version.outputs.garmin-connect-changed }}" = "true" ]; then
189+
echo "- @kaiord/garmin-connect@${{ steps.version.outputs.garmin-connect-version }}" >> $GITHUB_STEP_SUMMARY
190+
fi
191+
177192
if [ "${{ steps.version.outputs.mcp-changed }}" = "true" ]; then
178193
echo "- @kaiord/mcp@${{ steps.version.outputs.mcp-version }}" >> $GITHUB_STEP_SUMMARY
179194
fi

0 commit comments

Comments
 (0)