Skip to content

Commit 9869326

Browse files
committed
Implement direct JWT service account deployment
- Create deploy-chrome-simple.js with direct JWT token generation - Use Chrome Web Store API V1 with service account authentication - Bypass complex IAM permissions and workload identity federation - Simplify deployment with direct service account key usage - Add GOOGLE_SERVICE_ACCOUNT_KEY environment variable support This approach should work around CRX format requirements by using service account authentication with Chrome Web Store API V1.
1 parent cb4a855 commit 9869326

File tree

4 files changed

+173
-11
lines changed

4 files changed

+173
-11
lines changed

.github/workflows/ci-cd.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -278,15 +278,16 @@ jobs:
278278
- name: Deploy to Chrome Web Store (Service Account - Primary)
279279
id: chrome-deploy
280280
run: |
281-
echo "🚀 Deploying to Chrome Web Store using Service Account (API V2)"
281+
echo "🚀 Deploying to Chrome Web Store using Service Account (Direct JWT)"
282282
echo "📦 Extension ID: $CHROME_EXTENSION_ID"
283283
echo "📁 ZIP Path: $CHROME_ZIP_PATH"
284284
285285
# Install dependencies and deploy with service account
286-
npm run deploy:chrome-service-account
286+
npm run deploy:chrome-simple
287287
env:
288288
CHROME_EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }}
289289
CHROME_ZIP_PATH: ${{ env.CHROME_ZIP_PATH }}
290+
GOOGLE_SERVICE_ACCOUNT_KEY: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }}
290291
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
291292
continue-on-error: true
292293

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
"package:all-formats": "npm run package && npm run package:firefox",
2121
"package:crx": "node scripts/package-chrome.js",
2222
"deploy:chrome": "node scripts/deploy-chrome.js",
23-
"deploy:chrome-service-account": "node scripts/deploy-chrome-service-account.js"
23+
"deploy:chrome-service-account": "node scripts/deploy-chrome-service-account.js",
24+
"deploy:chrome-simple": "node scripts/deploy-chrome-simple.js"
2425
},
2526
"keywords": [
2627
"blog",

scripts/deploy-chrome-service-account.js

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,21 @@ try {
3030
throw new Error(`ZIP file not found: ${config.zipPath}`);
3131
}
3232

33-
// Initialize Google Auth with Application Default Credentials
34-
const auth = new GoogleAuth({
33+
// Use service account directly for Chrome Web Store API
34+
// Chrome Web Store API supports service account authentication
35+
console.log('🔐 Setting up service account authentication...');
36+
37+
// Get access token using service account key
38+
const { JWT } = await import('google-auth-library');
39+
const jwt = new JWT({
40+
email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
41+
key: process.env.GOOGLE_SERVICE_ACCOUNT_KEY || await fs.readFileSync(process.env.GOOGLE_APPLICATION_CREDENTIALS, 'utf8'),
3542
scopes: ['https://www.googleapis.com/auth/chromewebstore']
3643
});
37-
38-
console.log('🔐 Authenticating with Google Cloud...');
3944

40-
// Get access token
41-
const client = await auth.getClient();
42-
const accessToken = await client.getAccessToken();
45+
const accessToken = await jwt.getAccessToken();
4346

44-
console.log('✅ Authentication successful');
47+
console.log('✅ Service account authentication successful');
4548

4649
// Read ZIP file
4750
const zipData = fs.readFileSync(config.zipPath);

scripts/deploy-chrome-simple.js

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
#!/usr/bin/env node
2+
3+
import fs from 'fs';
4+
5+
// Configuration from environment variables
6+
const config = {
7+
extensionId: process.env.CHROME_EXTENSION_ID,
8+
zipPath: process.env.CHROME_ZIP_PATH,
9+
serviceAccountKey: process.env.GOOGLE_SERVICE_ACCOUNT_KEY
10+
};
11+
12+
// Validate required environment variables
13+
const requiredVars = ['extensionId', 'zipPath', 'serviceAccountKey'];
14+
const missingVars = requiredVars.filter(varName => !config[varName]);
15+
16+
if (missingVars.length > 0) {
17+
console.error('❌ Missing required environment variables:', missingVars.join(', '));
18+
process.exit(1);
19+
}
20+
21+
console.log('🚀 Starting Chrome Web Store deployment with Service Account...');
22+
console.log(`📦 Extension ID: ${config.extensionId}`);
23+
console.log(`📁 ZIP Path: ${config.zipPath}`);
24+
25+
try {
26+
// Check if ZIP file exists
27+
if (!fs.existsSync(config.zipPath)) {
28+
throw new Error(`ZIP file not found: ${config.zipPath}`);
29+
}
30+
31+
// Parse service account key
32+
let serviceAccountKey;
33+
try {
34+
serviceAccountKey = JSON.parse(config.serviceAccountKey);
35+
} catch (error) {
36+
throw new Error('Invalid service account key format');
37+
}
38+
39+
// Create JWT token for Chrome Web Store API
40+
const header = {
41+
alg: 'RS256',
42+
typ: 'JWT'
43+
};
44+
45+
const now = Math.floor(Date.now() / 1000);
46+
const payload = {
47+
iss: serviceAccountKey.client_email,
48+
scope: 'https://www.googleapis.com/auth/chromewebstore',
49+
aud: 'https://oauth2.googleapis.com/token',
50+
exp: now + 3600,
51+
iat: now
52+
};
53+
54+
// Import crypto for JWT signing
55+
const { createSign } = await import('crypto');
56+
57+
// Base64url encoding function
58+
const base64urlEncode = (str) => {
59+
return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
60+
};
61+
62+
// Create JWT
63+
const encodedHeader = base64urlEncode(Buffer.from(JSON.stringify(header)).toString('base64'));
64+
const encodedPayload = base64urlEncode(Buffer.from(JSON.stringify(payload)).toString('base64'));
65+
const jwtInput = `${encodedHeader}.${encodedPayload}`;
66+
67+
// Sign JWT
68+
const sign = createSign(serviceAccountKey.private_key);
69+
const signature = sign.update(jwtInput).sign('base64');
70+
const encodedSignature = base64urlEncode(signature);
71+
72+
const jwt = `${jwtInput}.${encodedSignature}`;
73+
74+
// Exchange JWT for access token
75+
console.log('🔐 Getting access token...');
76+
const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
77+
method: 'POST',
78+
headers: {
79+
'Content-Type': 'application/x-www-form-urlencoded'
80+
},
81+
body: new URLSearchParams({
82+
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
83+
assertion: jwt
84+
})
85+
});
86+
87+
if (!tokenResponse.ok) {
88+
const errorText = await tokenResponse.text();
89+
throw new Error(`Token exchange failed: ${tokenResponse.status} ${errorText}`);
90+
}
91+
92+
const tokenData = await tokenResponse.json();
93+
const accessToken = tokenData.access_token;
94+
95+
console.log('✅ Access token obtained successfully');
96+
97+
// Read ZIP file
98+
const zipData = fs.readFileSync(config.zipPath);
99+
100+
// Upload extension using Chrome Web Store API V1 (works with service accounts)
101+
console.log('📤 Uploading extension...');
102+
103+
const uploadResponse = await fetch(`https://www.googleapis.com/upload/chromewebstore/v1.1/items/${config.extensionId}`, {
104+
method: 'PUT',
105+
headers: {
106+
'Authorization': `Bearer ${accessToken}`,
107+
'Content-Type': 'application/zip',
108+
'x-goog-upload-protocol': 'raw'
109+
},
110+
body: zipData
111+
});
112+
113+
if (!uploadResponse.ok) {
114+
const errorText = await uploadResponse.text();
115+
throw new Error(`Upload failed: ${uploadResponse.status} ${uploadResponse.statusText} - ${errorText}`);
116+
}
117+
118+
const uploadResult = await uploadResponse.json();
119+
console.log('✅ Upload successful');
120+
console.log('📋 Upload response:', JSON.stringify(uploadResult, null, 2));
121+
122+
// Publish extension
123+
console.log('🚀 Publishing extension...');
124+
125+
const publishResponse = await fetch(`https://chromewebstore.googleapis.com/chromewebstore/v1.1/items/${config.extensionId}/publish`, {
126+
method: 'POST',
127+
headers: {
128+
'Authorization': `Bearer ${accessToken}`,
129+
'Content-Type': 'application/json',
130+
'x-goog-api-key': process.env.GOOGLE_API_KEY || ''
131+
},
132+
body: JSON.stringify({
133+
target: 'default'
134+
})
135+
});
136+
137+
if (!publishResponse.ok) {
138+
const errorText = await publishResponse.text();
139+
throw new Error(`Publish failed: ${publishResponse.status} ${publishResponse.statusText} - ${errorText}`);
140+
}
141+
142+
const publishResult = await publishResponse.json();
143+
console.log('✅ Published successfully');
144+
console.log('📋 Publish response:', JSON.stringify(publishResult, null, 2));
145+
146+
console.log('🎉 Chrome Web Store deployment completed successfully!');
147+
148+
} catch (error) {
149+
console.error('❌ Deployment failed:', error.message);
150+
151+
// Provide more detailed error information
152+
if (error.response) {
153+
console.error('📋 Error response:', JSON.stringify(error.response.data, null, 2));
154+
}
155+
156+
process.exit(1);
157+
}

0 commit comments

Comments
 (0)