Skip to content

Commit cf3e371

Browse files
committed
canary action
1 parent 21482a4 commit cf3e371

File tree

4 files changed

+185
-15
lines changed

4 files changed

+185
-15
lines changed
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
name: Canary Release
2+
3+
on:
4+
push:
5+
branches:
6+
- dev
7+
paths-ignore:
8+
- "**.md"
9+
- "docs/**"
10+
- "examples/**"
11+
- ".github/**"
12+
- "!.github/workflows/canary-release.yml"
13+
14+
permissions:
15+
contents: write # Required to create releases and tags
16+
17+
jobs:
18+
generate-version:
19+
runs-on: ubuntu-latest
20+
outputs:
21+
canary-version: ${{ steps.version.outputs.canary-version }}
22+
base-version: ${{ steps.version.outputs.base-version }}
23+
steps:
24+
- name: Checkout code
25+
uses: actions/checkout@v4
26+
27+
- name: Setup Node.js
28+
uses: actions/setup-node@v4
29+
with:
30+
node-version: "20"
31+
32+
- name: Get base version
33+
id: base
34+
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
35+
36+
- name: Query npm for existing canary versions
37+
id: npm-versions
38+
run: |
39+
BASE_VERSION="${{ steps.base.outputs.version }}"
40+
echo "Base version: $BASE_VERSION"
41+
42+
# Query npm for all versions and find max canary increment
43+
MAX_INCREMENT=$(node -e "
44+
const { execSync } = require('child_process');
45+
const baseVersion = process.argv[1];
46+
let versions = [];
47+
try {
48+
const output = execSync('npm view integrate-sdk versions --json', { encoding: 'utf-8' });
49+
versions = JSON.parse(output);
50+
} catch (e) {
51+
versions = [];
52+
}
53+
const pattern = new RegExp('^' + baseVersion.replace(/\./g, '\\.') + '-dev\\.(\\d+)$');
54+
const matches = versions
55+
.filter(v => pattern.test(v))
56+
.map(v => {
57+
const match = v.match(pattern);
58+
return match ? parseInt(match[1], 10) : 0;
59+
});
60+
const maxIncrement = matches.length > 0 ? Math.max(...matches) : -1;
61+
console.log(maxIncrement);
62+
" "$BASE_VERSION")
63+
64+
echo "max-increment=$MAX_INCREMENT" >> $GITHUB_OUTPUT
65+
66+
- name: Generate canary version
67+
id: version
68+
run: |
69+
BASE_VERSION="${{ steps.base.outputs.version }}"
70+
MAX_INCREMENT="${{ steps.npm-versions.outputs.max-increment }}"
71+
72+
# Increment the number
73+
NEXT_INCREMENT=$((MAX_INCREMENT + 1))
74+
CANARY_VERSION="${BASE_VERSION}-dev.${NEXT_INCREMENT}"
75+
76+
echo "base-version=$BASE_VERSION" >> $GITHUB_OUTPUT
77+
echo "canary-version=$CANARY_VERSION" >> $GITHUB_OUTPUT
78+
echo "Generated canary version: $CANARY_VERSION"
79+
80+
publish:
81+
needs: generate-version
82+
runs-on: ubuntu-latest
83+
steps:
84+
- name: Checkout code
85+
uses: actions/checkout@v4
86+
87+
- name: Setup Bun
88+
uses: oven-sh/setup-bun@v1
89+
with:
90+
bun-version: latest
91+
92+
- name: Install dependencies
93+
run: bun install
94+
95+
- name: Run tests
96+
run: bun test
97+
98+
- name: Type check
99+
run: bun run type-check
100+
101+
- name: Build
102+
run: bun run build
103+
104+
- name: Update package.json version
105+
run: |
106+
CANARY_VERSION="${{ needs.generate-version.outputs.canary-version }}"
107+
# Use node to update package.json version
108+
node -e "
109+
const fs = require('fs');
110+
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
111+
pkg.version = '$CANARY_VERSION';
112+
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
113+
"
114+
echo "Updated package.json version to $CANARY_VERSION"
115+
116+
- name: Setup Node.js (for npm publish)
117+
uses: actions/setup-node@v4
118+
with:
119+
node-version: "20"
120+
registry-url: "https://registry.npmjs.org"
121+
122+
- name: Publish to npm
123+
run: npm publish
124+
env:
125+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
126+
127+
- name: Create Git Tag
128+
run: |
129+
CANARY_VERSION="${{ needs.generate-version.outputs.canary-version }}"
130+
git config user.name "github-actions[bot]"
131+
git config user.email "github-actions[bot]@users.noreply.github.com"
132+
git tag -a v$CANARY_VERSION -m "Canary release v$CANARY_VERSION"
133+
git push origin v$CANARY_VERSION
134+
135+
- name: Create GitHub Release
136+
uses: softprops/action-gh-release@v1
137+
with:
138+
tag_name: v${{ needs.generate-version.outputs.canary-version }}
139+
name: Canary Release v${{ needs.generate-version.outputs.canary-version }}
140+
body: |
141+
## Canary Release v${{ needs.generate-version.outputs.canary-version }}
142+
143+
This is a canary release from the `dev` branch.
144+
145+
### Installation
146+
```bash
147+
bun add integrate-sdk@${{ needs.generate-version.outputs.canary-version }}
148+
```
149+
draft: false
150+
prerelease: true
151+
152+
- name: Github Releases To Discord
153+
if: ${{ secrets.DISCORD_WEBHOOK_URL != '' }}
154+
uses: SethCohen/github-releases-to-discord@v1.13.1
155+
with:
156+
webhook_url: ${{ secrets.DISCORD_WEBHOOK_URL }}
157+
color: "1316451"
158+
content: "Canary release published:"
159+
footer_title: "integrate.dev"
160+
footer_timestamp: true
161+

src/adapters/base-handler.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -60,41 +60,49 @@ export interface OAuthHandlerConfig {
6060
* Called automatically after successful OAuth callback
6161
*
6262
* @param provider - Provider name (e.g., 'github')
63-
* @param tokenData - OAuth tokens (accessToken, refreshToken, etc.)
63+
* @param tokenData - OAuth tokens (accessToken, refreshToken, etc.), or null to delete
64+
* @param email - Optional email to store specific account token
6465
* @param context - User context (userId, organizationId, etc.)
6566
*
6667
* @example
6768
* ```typescript
68-
* setProviderToken: async (provider, tokens, context) => {
69+
* setProviderToken: async (provider, tokens, email, context) => {
6970
* await db.tokens.upsert({
70-
* where: { provider_userId: { provider, userId: context.userId } },
71-
* create: { provider, userId: context.userId, ...tokens },
71+
* where: { provider_email_userId: { provider, email, userId: context.userId } },
72+
* create: { provider, email, userId: context.userId, ...tokens },
7273
* update: tokens,
7374
* });
7475
* }
7576
* ```
7677
*/
77-
setProviderToken?: (provider: string, tokenData: ProviderTokenData, context?: MCPContext) => Promise<void> | void;
78+
setProviderToken?: (provider: string, tokenData: ProviderTokenData | null, email?: string, context?: MCPContext) => Promise<void> | void;
7879
/**
7980
* Optional callback to delete provider tokens from database
80-
* Called automatically when disconnecting providers
81+
* Called automatically when disconnecting providers or accounts
8182
*
8283
* @param provider - Provider name (e.g., 'github')
84+
* @param email - Optional email to delete specific account token. If not provided, deletes all tokens for the provider
8385
* @param context - User context (userId, organizationId, etc.)
8486
*
8587
* @example
8688
* ```typescript
87-
* removeProviderToken: async (provider, context) => {
89+
* removeProviderToken: async (provider, email, context) => {
8890
* const userId = context?.userId;
8991
* if (!userId) return;
9092
*
91-
* await db.tokens.delete({
92-
* where: { provider_userId: { provider, userId } }
93-
* });
93+
* if (email) {
94+
* await db.tokens.deleteMany({
95+
* where: { provider, email, userId }
96+
* });
97+
* } else {
98+
* await db.tokens.deleteMany({
99+
* where: { provider, userId }
100+
* });
101+
* }
94102
* }
95103
* ```
96104
*/
97-
removeProviderToken?: (provider: string, context?: MCPContext) => Promise<void> | void;
105+
removeProviderToken?: (provider: string, email?: string, context?: MCPContext) => Promise<void> | void;
98106
}
99107

100108
/**
@@ -565,9 +573,10 @@ export class OAuthHandler {
565573
}
566574

567575
// Call removeProviderToken callback with context
576+
// Note: Email is not available in disconnect requests - pass undefined to delete all tokens for provider
568577
if (context) {
569578
try {
570-
await this.config.removeProviderToken(request.provider, context);
579+
await this.config.removeProviderToken(request.provider, undefined, context);
571580
} catch (error) {
572581
// Log error but don't fail the request - MCP server revocation will still happen
573582
console.error(`Failed to delete token for ${request.provider} from database via removeProviderToken:`, error);

src/oauth/email-fetcher.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ async function fetchGitHubEmail(accessToken: string): Promise<string | undefined
8585
}
8686

8787
// Fallback to first email
88-
if (emails.length > 0) {
88+
if (emails.length > 0 && emails[0]?.email) {
8989
return emails[0].email;
9090
}
9191

src/server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -671,8 +671,8 @@ function createOAuthRouteHandlers(config: {
671671
serverUrl?: string;
672672
apiKey?: string;
673673
getSessionContext?: (request: Request) => Promise<import('./config/types.js').MCPContext | undefined> | import('./config/types.js').MCPContext | undefined;
674-
setProviderToken?: (provider: string, tokenData: import('./oauth/types.js').ProviderTokenData, context?: import('./config/types.js').MCPContext) => Promise<void> | void;
675-
removeProviderToken?: (provider: string, context?: import('./config/types.js').MCPContext) => Promise<void> | void;
674+
setProviderToken?: (provider: string, tokenData: import('./oauth/types.js').ProviderTokenData | null, email?: string, context?: import('./config/types.js').MCPContext) => Promise<void> | void;
675+
removeProviderToken?: (provider: string, email?: string, context?: import('./config/types.js').MCPContext) => Promise<void> | void;
676676
}) {
677677
const handler = createNextOAuthHandler(config);
678678
return handler.createRoutes();

0 commit comments

Comments
 (0)