Skip to content

Commit 1e25e7a

Browse files
committed
feat(ci,mcp): add Open VSX publish verification + manual login fallback
CI: - Add 3-minute polling loop after ovsx publish to verify version indexing - Curl Open VSX API every 10s (18 attempts) to confirm published version is queryable - Fail release if version not indexed within timeout window MCP login: - When email/password not provided, launch manual login flow instead of exiting - Open headed Chrome browser to airtable.com/login - Poll /v0.3/getUserProperties every 2s (max 5 min) to detect
1 parent e3ed3bf commit 1e25e7a

2 files changed

Lines changed: 88 additions & 5 deletions

File tree

.github/workflows/release.yml

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,29 @@ jobs:
239239
if: |
240240
!inputs.dry_run &&
241241
(inputs.target == 'extension' || inputs.target == 'both')
242-
run: npx ovsx publish "${{ steps.vsix.outputs.file }}" --pat $OVSX_PAT
242+
run: |
243+
npx ovsx publish "${{ steps.vsix.outputs.file }}" --pat $OVSX_PAT
244+
245+
# Verify the version actually landed (Open VSX indexes asynchronously)
246+
VERSION="${{ steps.ext_version.outputs.next }}"
247+
echo "Verifying Open VSX indexed v${VERSION}..."
248+
for i in $(seq 1 18); do
249+
FOUND=$(curl -s "https://open-vsx.org/api/Nskha/airtable-formula/${VERSION}" \
250+
| node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{
251+
try{const p=JSON.parse(d);console.log(p.version||'not-found')}
252+
catch{console.log('error')}
253+
})")
254+
if [[ "$FOUND" == "$VERSION" ]]; then
255+
echo "✓ Open VSX confirmed: v${VERSION}"
256+
break
257+
fi
258+
echo " not indexed yet (attempt $i/18), waiting 10s..."
259+
sleep 10
260+
done
261+
if [[ "$FOUND" != "$VERSION" ]]; then
262+
echo "::error::Open VSX did not index v${VERSION} within 3 minutes — check token and registry status"
263+
exit 1
264+
fi
243265
env:
244266
OVSX_PAT: ${{ secrets.OVSX_PAT }}
245267

packages/mcp-server/src/login.js

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,75 @@ async function generateTOTP(secretBase32) {
7777
return totp.generate();
7878
}
7979

80+
async function mainManual(profileDir) {
81+
console.log('Opening browser for manual login...');
82+
console.log(`Profile: ${profileDir}`);
83+
console.log('Please log in to Airtable in the browser window that opens.\n');
84+
85+
const chromium = await getChromium();
86+
const context = await chromium.launchPersistentContext(profileDir, {
87+
headless: false,
88+
channel: 'chrome',
89+
viewport: null,
90+
});
91+
92+
let outerError;
93+
try {
94+
const page = context.pages()[0] || await context.newPage();
95+
await page.goto('https://airtable.com/login', { waitUntil: 'domcontentloaded' });
96+
97+
let loggedIn = false;
98+
let attempts = 0;
99+
while (!loggedIn && attempts < 150) {
100+
attempts++;
101+
await page.waitForTimeout(2000);
102+
try {
103+
const result = await page.evaluate(async () => {
104+
try {
105+
const res = await fetch('/v0.3/getUserProperties', {
106+
headers: {
107+
'x-airtable-inter-service-client': 'webClient',
108+
'x-requested-with': 'XMLHttpRequest',
109+
},
110+
});
111+
if (res.ok) {
112+
const data = await res.json();
113+
return { ok: true, userId: data?.data?.userId };
114+
}
115+
return { ok: false, status: res.status };
116+
} catch (e) {
117+
return { ok: false, error: e.message };
118+
}
119+
});
120+
if (result.ok) {
121+
loggedIn = true;
122+
console.log('✅ Login verified! User:', result.userId);
123+
}
124+
} catch {
125+
// Page navigating, keep waiting
126+
}
127+
}
128+
129+
if (!loggedIn) throw new Error('Login not detected after 5 minutes');
130+
131+
console.log('\nSession stored in Chrome profile.');
132+
console.log('MCP server will use this session headlessly.');
133+
console.log('\nClosing browser...');
134+
console.log('Done!');
135+
} catch (err) {
136+
outerError = err;
137+
} finally {
138+
try { await context.close(); } catch { /* best-effort */ }
139+
}
140+
if (outerError) throw outerError;
141+
}
142+
80143
async function main() {
81144
const { email, password, otpSecret, profileDir } = parseArgs();
82145

83146
if (!email || !password) {
84-
console.error('ERROR: Provide credentials via env vars or arguments:');
85-
console.error(' node src/login.js --email <email> --password <pass> [--otp-secret <secret>]');
86-
console.error(' Or set AIRTABLE_EMAIL, AIRTABLE_PASSWORD, AIRTABLE_OTP_SECRET in .env');
87-
process.exit(1);
147+
await mainManual(profileDir);
148+
return;
88149
}
89150

90151
console.log('Opening Chrome with Patchright (undetected)...');

0 commit comments

Comments
 (0)