Skip to content

Commit 6056ee1

Browse files
committed
feat: Gmail token auto-renewal system - keepalive + auto-rotation
- New: gmail-token-keepalive.yml runs daily at 6AM to prevent 7-day expiry - New: gmail-token-keepalive.js lightweight token refresh script - Updated: fetch-gmail-diagnostics.js captures new refresh tokens + logs expiry info - Updated: gmail-diagnostics.yml auto-rotates GitHub secret when new token received - Protection: 3 layers - daily keepalive, 3x/day diagnostics, auto-rotation on renewal
1 parent 9a432bb commit 6056ee1

4 files changed

Lines changed: 70 additions & 1 deletion

File tree

.github/scripts/fetch-gmail-diagnostics.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@ async function getToken(){
3636
headers:{'Content-Type':'application/x-www-form-urlencoded'},
3737
body:'client_id='+id+'&client_secret='+s+'&refresh_token='+r+'&grant_type=refresh_token'});
3838
if(!res.ok){const e=await res.text();console.error('Token refresh failed:',res.status,e);if(e.includes('invalid_grant'))console.error('EXPIRED! Publish OAuth consent screen or re-generate refresh token');return null;}
39-
return(await res.json()).access_token;
39+
const j=await res.json();
40+
if(j.refresh_token_expires_in)console.log('Refresh token expires in '+Math.round(j.refresh_token_expires_in/3600)+'h (Testing mode)');
41+
if(j.refresh_token){console.log('New refresh token received - writing for auto-rotation');fs.writeFileSync(path.join(SD,'_new_refresh_token.txt'),j.refresh_token);}
42+
return j.access_token;
4043
}
4144

4245
async function gapi(tk,ep,retries=2){
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/usr/bin/env node
2+
'use strict';
3+
const fs=require('fs'),path=require('path');
4+
const SD=path.join(__dirname,'..','state');
5+
async function main(){
6+
const{GMAIL_CLIENT_ID:id,GMAIL_CLIENT_SECRET:s,GMAIL_REFRESH_TOKEN:r}=process.env;
7+
if(!id||!s||!r){console.log('No creds');process.exit(0);}
8+
const res=await fetch('https://oauth2.googleapis.com/token',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'client_id='+id+'&client_secret='+s+'&refresh_token='+r+'&grant_type=refresh_token'});
9+
if(!res.ok){console.error('FAIL:',res.status);process.exit(1);}
10+
const j=await res.json();
11+
console.log('OK:',j.expires_in+'s');
12+
if(j.refresh_token){fs.mkdirSync(SD,{recursive:true});fs.writeFileSync(path.join(SD,'_new_refresh_token.txt'),j.refresh_token);console.log('NEW token saved');}
13+
}
14+
main().catch(e=>{console.error(e);process.exit(1);});

.github/workflows/gmail-diagnostics.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,17 @@ jobs:
3838
COUNT=$(node -p "try{JSON.parse(require('fs').readFileSync('.github/state/diagnostics-report.json')).emails?.length||0}catch(e){0}")
3939
git diff --staged --quiet || git commit -m "Gmail diagnostics $(date -u +%Y-%m-%d): ${COUNT} emails analyzed [skip ci]"
4040
git push || true
41+
- name: Auto-rotate refresh token if renewed
42+
env:
43+
GH_TOKEN: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }}
44+
run: |
45+
TOKEN_FILE=".github/state/_new_refresh_token.txt"
46+
if [ -f "$TOKEN_FILE" ]; then
47+
echo "New refresh token detected - rotating GitHub secret..."
48+
cat "$TOKEN_FILE" | gh secret set GMAIL_REFRESH_TOKEN
49+
rm -f "$TOKEN_FILE"
50+
echo "::notice::Gmail refresh token auto-rotated"
51+
fi
4152
- name: Create issues for critical diagnostics
4253
if: always()
4354
run: |
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: Gmail Token Keepalive
2+
on:
3+
schedule:
4+
- cron: '0 6 * * *'
5+
workflow_dispatch:
6+
7+
permissions:
8+
contents: read
9+
10+
concurrency:
11+
group: gmail-keepalive
12+
cancel-in-progress: true
13+
14+
jobs:
15+
keepalive:
16+
runs-on: ubuntu-latest
17+
timeout-minutes: 5
18+
steps:
19+
- uses: actions/checkout@v4
20+
- uses: actions/setup-node@v4
21+
with:
22+
node-version: '22'
23+
- name: Refresh Gmail token
24+
env:
25+
GMAIL_CLIENT_ID: ${{ secrets.GMAIL_CLIENT_ID }}
26+
GMAIL_CLIENT_SECRET: ${{ secrets.GMAIL_CLIENT_SECRET }}
27+
GMAIL_REFRESH_TOKEN: ${{ secrets.GMAIL_REFRESH_TOKEN }}
28+
run: node .github/scripts/gmail-token-keepalive.js
29+
- name: Auto-rotate if new token
30+
env:
31+
GH_TOKEN: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }}
32+
run: |
33+
TOKEN_FILE=".github/state/_new_refresh_token.txt"
34+
if [ -f "$TOKEN_FILE" ]; then
35+
echo "Rotating refresh token..."
36+
cat "$TOKEN_FILE" | gh secret set GMAIL_REFRESH_TOKEN
37+
rm -f "$TOKEN_FILE"
38+
echo "::notice::Gmail refresh token auto-rotated"
39+
else
40+
echo "Token still valid - no rotation needed"
41+
fi

0 commit comments

Comments
 (0)