mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
ci(proxy): harden OpenAI proxy and assert failures across workflows (issue-code, issue-comment, issue-triage, release); add chat-completions fallback provider; and disable upstream-merge concurrency cancellation
This commit is contained in:
@@ -163,7 +163,7 @@ jobs:
|
||||
run: |
|
||||
git remote set-url origin "https://x-access-token:${{ github.token }}@github.com/${{ github.repository }}.git"
|
||||
|
||||
- name: Start local OpenAI proxy (no key to agent)
|
||||
- name: Start local OpenAI proxy (no key to agent; hardened)
|
||||
id: openai_proxy
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
@@ -177,34 +177,79 @@ jobs:
|
||||
cat > .github/auto/openai-proxy.js <<'JS'
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const crypto = require('crypto');
|
||||
const { URL } = require('url');
|
||||
const PORT = process.env.PORT || 5057;
|
||||
const API_KEY = process.env.OPENAI_API_KEY || '';
|
||||
const UPSTREAM = new URL(process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1');
|
||||
const ALLOWED = ['/v1/chat/completions','/v1/responses'];
|
||||
if (!API_KEY) { console.error('OPENAI_API_KEY missing'); process.exit(1); }
|
||||
if (!API_KEY) { console.error('[fatal] OPENAI_API_KEY missing'); process.exit(1); }
|
||||
|
||||
function redactHeaders(h) {
|
||||
const out = {}; for (const [k,v] of Object.entries(h||{})) { const key=k.toLowerCase();
|
||||
if (['authorization','proxy-authorization','cookie','set-cookie'].includes(key)) out[key]='REDACTED'; else out[key]=String(Array.isArray(v)?v[0]:v);
|
||||
} return out;
|
||||
}
|
||||
function log(ev) { try { console.log(JSON.stringify({ ts: new Date().toISOString(), ...ev })); } catch {}
|
||||
}
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
if (req.method !== 'POST' || !ALLOWED.some(p => req.url.startsWith(p))) {
|
||||
res.writeHead(403, { 'content-type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'blocked' }));
|
||||
return;
|
||||
const rid = crypto.randomUUID();
|
||||
if (!ALLOWED.some(p => req.url.startsWith(p))) {
|
||||
res.writeHead(403, { 'content-type': 'application/json' }); res.end(JSON.stringify({ error: 'blocked', path: req.url })); return;
|
||||
}
|
||||
const chunks = [];
|
||||
req.on('data', c => { chunks.push(c); if (Buffer.concat(chunks).length > 1024*1024) req.destroy(); });
|
||||
const chunks=[]; let size=0; req.on('data', c=>{chunks.push(c); size+=c.length; if (size>2*1024*1024) req.destroy(new Error('body too large'))});
|
||||
req.on('error', err=>{ log({ level:'error', rid, msg:'client request error', err:String(err) }); if (!res.headersSent) res.writeHead(400,{'content-type':'application/json'}); res.end(JSON.stringify({error:'bad_request'})); });
|
||||
req.on('end', () => {
|
||||
const up = new URL('https://api.openai.com' + req.url);
|
||||
const fw = https.request({
|
||||
method: 'POST',
|
||||
hostname: up.hostname,
|
||||
path: up.pathname + up.search,
|
||||
headers: { 'content-type': 'application/json', 'authorization': `Bearer ${API_KEY}` }
|
||||
}, r => { res.writeHead(r.statusCode || 500, r.headers); r.pipe(res); });
|
||||
fw.on('error', e => { res.writeHead(502, {'content-type':'application/json'}); res.end(JSON.stringify({ error:'upstream', message: e.message })); });
|
||||
fw.end(Buffer.concat(chunks));
|
||||
const body = Buffer.concat(chunks);
|
||||
const up = new URL(req.url, UPSTREAM);
|
||||
const incoming = { ...req.headers };
|
||||
if (!incoming['content-type']) incoming['content-type']='application/json';
|
||||
// Note: exec uses streaming; set accept to SSE for responses path
|
||||
if (!incoming['accept']) incoming['accept']='text/event-stream';
|
||||
incoming['authorization'] = `Bearer ${API_KEY}`;
|
||||
if (up.pathname.startsWith('/v1/responses')) { if (!incoming['openai-beta']) incoming['openai-beta']='responses=experimental'; }
|
||||
incoming['connection'] = 'keep-alive';
|
||||
delete incoming['host']; incoming['host'] = up.host;
|
||||
if (incoming['content-length']) incoming['content-length'] = String(body.length);
|
||||
delete incoming['proxy-connection']; delete incoming['proxy-authorization'];
|
||||
|
||||
log({ level:'info', rid, phase:'request', method:req.method, url:up.toString(), headers:redactHeaders(incoming), body_bytes: body.length });
|
||||
|
||||
const opts = { protocol: up.protocol, hostname: up.hostname, port: up.port || (up.protocol==='https:'?443:80), path: up.pathname+up.search, method: req.method, headers: incoming,
|
||||
servername: up.hostname,
|
||||
agent: (up.protocol==='https:' ? new https.Agent({ keepAlive:true, maxSockets:64, servername: up.hostname }) : new http.Agent({ keepAlive:true, maxSockets:64 })) };
|
||||
|
||||
const upstream = (up.protocol==='https:'?https:http).request(opts, (upr) => {
|
||||
const resHeaders = { ...upr.headers }; res.writeHead(upr.statusCode||500, resHeaders);
|
||||
log({ level:'info', rid, phase:'response_head', status: upr.statusCode, headers: redactHeaders(resHeaders) });
|
||||
let total=0; upr.on('data', ch=>{ total+=ch.length; });
|
||||
upr.on('end', ()=>{ log({ level:'info', rid, phase:'response_end', status: upr.statusCode, bytes: total, request_id: resHeaders['x-request-id']||null }); });
|
||||
upr.on('error', e=>{ log({ level:'error', rid, phase:'response_error', err:String(e) }); });
|
||||
upr.pipe(res);
|
||||
});
|
||||
upstream.setTimeout(600000, ()=>{ upstream.destroy(new Error('upstream timeout')); });
|
||||
upstream.on('error', e=>{ log({ level:'error', rid, phase:'upstream_error', err:String(e) }); if (!res.headersSent) res.writeHead(502,{ 'content-type':'application/json'}); res.end(JSON.stringify({error:'upstream_error'})); });
|
||||
upstream.end(body);
|
||||
});
|
||||
});
|
||||
server.listen(PORT, '127.0.0.1', () => { console.log('proxy listening on 127.0.0.1:'+PORT); });
|
||||
server.headersTimeout = 650000; server.keepAliveTimeout = 650000;
|
||||
server.listen(PORT, '127.0.0.1', ()=>{ log({ level:'info', msg:'proxy listening', addr:'127.0.0.1', port: PORT }); });
|
||||
JS
|
||||
node .github/auto/openai-proxy.js > .github/auto/openai-proxy.log 2>&1 &
|
||||
# Wait briefly for readiness
|
||||
for i in {1..30}; do if nc -z 127.0.0.1 5057; then break; else sleep 0.2; fi; done || true
|
||||
|
||||
- name: Configure Code to use Chat Completions fallback
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$HOME/.codex"
|
||||
cat > "$HOME/.codex/config.toml" <<'TOML'
|
||||
[model_providers.openai_chat]
|
||||
name = "OpenAI (Chat)"
|
||||
base_url = "http://127.0.0.1:5057/v1"
|
||||
wire_api = "chat"
|
||||
TOML
|
||||
|
||||
- name: Run Code agent (workspace-write)
|
||||
id: run_agent
|
||||
@@ -244,7 +289,7 @@ jobs:
|
||||
GIT_TERMINAL_PROMPT="0" \
|
||||
GIT_ASKPASS="/bin/false" \
|
||||
npx -y @just-every/code@latest \
|
||||
exec \
|
||||
exec -c model_provider=openai_chat \
|
||||
-s workspace-write \
|
||||
-c sandbox_workspace_write.network_access=true \
|
||||
--cd "$GITHUB_WORKSPACE" \
|
||||
@@ -252,6 +297,27 @@ jobs:
|
||||
-; } 2>&1 | tee .github/auto/AGENT_STDOUT.txt
|
||||
true; set -e; set -o pipefail
|
||||
|
||||
- name: Assert agent success (fail on streaming/server errors)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if rg -n "^\\[.*\\] ERROR: (stream error|server error|exceeded retry limit)" .github/auto/AGENT_STDOUT.txt >/dev/null 2>&1; then
|
||||
echo "Agent reported a fatal error (stream/server). Failing job." >&2
|
||||
rg -n "^\\[.*\\] ERROR: (stream error|server error|exceeded retry limit)" .github/auto/AGENT_STDOUT.txt || true
|
||||
exit 1
|
||||
fi
|
||||
if [ -s .github/auto/openai-proxy.log ]; then
|
||||
if rg -n '"phase":"response_head".*"status":5\\d\\d' .github/auto/openai-proxy.log >/dev/null 2>&1; then
|
||||
echo "Proxy observed 5xx from upstream during agent run. Failing job." >&2
|
||||
rg -n '"phase":"response_head".*"status":5\\d\\d' .github/auto/openai-proxy.log | tail -n 10 || true
|
||||
exit 1
|
||||
fi
|
||||
if rg -n '"phase":"upstream_error"' .github/auto/openai-proxy.log >/dev/null 2>&1; then
|
||||
echo "Proxy upstream_error entries found. Failing job." >&2
|
||||
rg -n '"phase":"upstream_error"' .github/auto/openai-proxy.log | tail -n 10 || true
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Detect changes or new commits
|
||||
id: changes
|
||||
run: |
|
||||
|
||||
@@ -227,7 +227,7 @@ jobs:
|
||||
fs.mkdirSync('.github/auto', { recursive: true });
|
||||
fs.writeFileSync('.github/auto/ISSUE_PREVIEW_CONTEXT.md', lines.join('\n'));
|
||||
|
||||
- name: Start local OpenAI proxy (optional)
|
||||
- name: Start local OpenAI proxy (optional; hardened)
|
||||
if: env.OPENAI_API_KEY != ''
|
||||
id: proxy
|
||||
env:
|
||||
@@ -237,29 +237,51 @@ jobs:
|
||||
cat > .github/auto/openai-proxy.js <<'JS'
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const crypto = require('crypto');
|
||||
const { URL } = require('url');
|
||||
const PORT = process.env.PORT || 5056;
|
||||
const API_KEY = process.env.OPENAI_API_KEY || '';
|
||||
const UPSTREAM = new URL(process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1');
|
||||
const ALLOWED = ['/v1/chat/completions','/v1/responses'];
|
||||
if (!API_KEY) { process.exit(0); }
|
||||
function log(ev){ try{ console.log(JSON.stringify({ ts:new Date().toISOString(), ...ev })); }catch{} }
|
||||
const server = http.createServer((req, res) => {
|
||||
if (req.method !== 'POST' || !ALLOWED.some(p => req.url.startsWith(p))) {
|
||||
res.writeHead(403, { 'content-type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'blocked' }));
|
||||
return;
|
||||
}
|
||||
const chunks = [];
|
||||
req.on('data', c => { chunks.push(c); if (Buffer.concat(chunks).length > 1024*1024) req.destroy(); });
|
||||
const rid = crypto.randomUUID();
|
||||
if (!ALLOWED.some(p => req.url.startsWith(p))) { res.writeHead(403,{'content-type':'application/json'}); res.end(JSON.stringify({error:'blocked', path:req.url})); return; }
|
||||
const chunks=[]; let size=0; req.on('data', c=>{ chunks.push(c); size+=c.length; if (size>2*1024*1024) req.destroy(); });
|
||||
req.on('end', () => {
|
||||
const up = new URL('https://api.openai.com' + req.url);
|
||||
const fw = https.request({ method: 'POST', hostname: up.hostname, path: up.pathname + up.search, headers: { 'content-type': 'application/json', 'authorization': `Bearer ${API_KEY}` } }, r => { res.writeHead(r.statusCode || 500, r.headers); r.pipe(res); });
|
||||
fw.on('error', e => { res.writeHead(502, {'content-type':'application/json'}); res.end(JSON.stringify({ error:'upstream', message: e.message })); });
|
||||
fw.end(Buffer.concat(chunks));
|
||||
const body = Buffer.concat(chunks);
|
||||
const up = new URL(req.url, UPSTREAM);
|
||||
const headers = { ...req.headers };
|
||||
if (!headers['content-type']) headers['content-type']='application/json';
|
||||
headers['authorization'] = `Bearer ${API_KEY}`;
|
||||
if (up.pathname.startsWith('/v1/responses') && !headers['openai-beta']) headers['openai-beta']='responses=experimental';
|
||||
delete headers['host']; headers['host']=up.host;
|
||||
if (headers['content-length']) headers['content-length']=String(body.length);
|
||||
const opts = { protocol: up.protocol, hostname: up.hostname, port: up.port || (up.protocol==='https:'?443:80), path: up.pathname+up.search, method: req.method, headers,
|
||||
servername: up.hostname, agent:(up.protocol==='https:'? new https.Agent({keepAlive:true, maxSockets:64, servername: up.hostname}) : new http.Agent({keepAlive:true, maxSockets:64})) };
|
||||
const fw = (up.protocol==='https:'?https:http).request(opts, r => { res.writeHead(r.statusCode||500, r.headers); r.pipe(res); });
|
||||
fw.on('error', e => { log({ level:'error', rid, phase:'upstream_error', err:String(e) }); res.writeHead(502,{'content-type':'application/json'}); res.end(JSON.stringify({ error:'upstream', message: String(e.message||e) })); });
|
||||
fw.end(body);
|
||||
});
|
||||
});
|
||||
server.listen(PORT, '127.0.0.1', () => { console.log('proxy listening on 127.0.0.1:'+PORT); });
|
||||
server.headersTimeout=650000; server.keepAliveTimeout=650000;
|
||||
server.listen(PORT,'127.0.0.1',()=>{ log({ level:'info', msg:'proxy listening', addr:'127.0.0.1', port: PORT }); });
|
||||
JS
|
||||
node .github/auto/openai-proxy.js > .github/auto/openai-proxy.log 2>&1 &
|
||||
for i in {1..30}; do if nc -z 127.0.0.1 5056; then break; else sleep 0.2; fi; done || true
|
||||
|
||||
- name: Configure Code to use Chat Completions fallback
|
||||
if: env.OPENAI_API_KEY != ''
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$HOME/.codex"
|
||||
cat > "$HOME/.codex/config.toml" <<'TOML'
|
||||
[model_providers.openai_chat]
|
||||
name = "OpenAI (Chat)"
|
||||
base_url = "http://127.0.0.1:5056/v1"
|
||||
wire_api = "chat"
|
||||
TOML
|
||||
|
||||
- name: Generate comment with Code (read-only)
|
||||
id: gen
|
||||
@@ -307,10 +329,31 @@ jobs:
|
||||
set +e
|
||||
{ printf '%s' "$PROMPT" | env -i PATH="$SAFE_PATH" HOME="$SAFE_HOME" \
|
||||
OPENAI_API_KEY="x" OPENAI_BASE_URL="http://127.0.0.1:5056/v1" \
|
||||
npx -y @just-every/code@latest exec -s read-only --cd "$GITHUB_WORKSPACE" --skip-git-repo-check -; } \
|
||||
npx -y @just-every/code@latest exec -c model_provider=openai_chat -s read-only --cd "$GITHUB_WORKSPACE" --skip-git-repo-check -; } \
|
||||
2>&1 | tee .github/auto/AGENT_OUT.txt
|
||||
true
|
||||
set -e
|
||||
- name: Assert agent success (fail on streaming/server errors)
|
||||
if: env.OPENAI_API_KEY != ''
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if rg -n "^\\[.*\\] ERROR: (stream error|server error|exceeded retry limit)" .github/auto/AGENT_OUT.txt >/dev/null 2>&1; then
|
||||
echo "Agent reported a fatal error (stream/server). Failing job." >&2
|
||||
rg -n "^\\[.*\\] ERROR: (stream error|server error|exceeded retry limit)" .github/auto/AGENT_OUT.txt || true
|
||||
exit 1
|
||||
fi
|
||||
if [ -s .github/auto/openai-proxy.log ]; then
|
||||
if rg -n '"phase":"response_head".*"status":5\\d\\d' .github/auto/openai-proxy.log >/dev/null 2>&1; then
|
||||
echo "Proxy observed 5xx from upstream during agent run. Failing job." >&2
|
||||
rg -n '"phase":"response_head".*"status":5\\d\\d' .github/auto/openai-proxy.log | tail -n 10 || true
|
||||
exit 1
|
||||
fi
|
||||
if rg -n '"phase":"upstream_error"' .github/auto/openai-proxy.log >/dev/null 2>&1; then
|
||||
echo "Proxy upstream_error entries found. Failing job." >&2
|
||||
rg -n '"phase":"upstream_error"' .github/auto/openai-proxy.log | tail -n 10 || true
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
node - <<'JS'
|
||||
const fs = require('fs');
|
||||
const t = fs.readFileSync('.github/auto/AGENT_OUT.txt','utf8');
|
||||
|
||||
@@ -314,6 +314,7 @@ jobs:
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_BASE_URL: http://127.0.0.1:5054/v1
|
||||
with:
|
||||
script: |
|
||||
const apiKey = process.env.OPENAI_API_KEY;
|
||||
@@ -322,6 +323,7 @@ jobs:
|
||||
core.setOutput('reason', 'Missing OPENAI_API_KEY for safety screening.');
|
||||
core.notice('Safety: block (no OPENAI_API_KEY)');
|
||||
} else {
|
||||
const base = (process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1').replace(/\/$/,'');
|
||||
async function callResponses(model) {
|
||||
const input = [
|
||||
{
|
||||
@@ -381,9 +383,9 @@ jobs:
|
||||
}
|
||||
}
|
||||
};
|
||||
return fetch('https://api.openai.com/v1/responses', {
|
||||
return fetch(base + '/responses', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`, 'OpenAI-Beta': 'responses=experimental' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}
|
||||
@@ -426,6 +428,50 @@ jobs:
|
||||
}
|
||||
}
|
||||
|
||||
- name: Start local OpenAI proxy for triage (hardened)
|
||||
if: env.OPENAI_API_KEY != ''
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p .github/auto
|
||||
cat > .github/auto/openai-proxy.js <<'JS'
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const crypto = require('crypto');
|
||||
const { URL } = require('url');
|
||||
const PORT = process.env.PORT || 5054;
|
||||
const API_KEY = process.env.OPENAI_API_KEY || '';
|
||||
const UPSTREAM = new URL(process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1');
|
||||
const ALLOWED = ['/v1/chat/completions','/v1/responses'];
|
||||
if (!API_KEY) { process.exit(0); }
|
||||
function log(ev){ try{ console.log(JSON.stringify({ ts:new Date().toISOString(), ...ev })); }catch{} }
|
||||
const server = http.createServer((req, res) => {
|
||||
const rid = crypto.randomUUID();
|
||||
if (!ALLOWED.some(p => req.url.startsWith(p))) { res.writeHead(403,{'content-type':'application/json'}); res.end(JSON.stringify({error:'blocked', path:req.url})); return; }
|
||||
const chunks=[]; req.on('data', c=>{ chunks.push(c); if (Buffer.concat(chunks).length>2*1024*1024) req.destroy(); });
|
||||
req.on('end', ()=>{
|
||||
const body = Buffer.concat(chunks);
|
||||
const up = new URL(req.url, UPSTREAM);
|
||||
const headers = { ...req.headers };
|
||||
if (!headers['content-type']) headers['content-type']='application/json';
|
||||
headers['authorization'] = `Bearer ${API_KEY}`;
|
||||
if (up.pathname.startsWith('/v1/responses') && !headers['openai-beta']) headers['openai-beta']='responses=experimental';
|
||||
delete headers['host']; headers['host']=up.host;
|
||||
if (headers['content-length']) headers['content-length']=String(body.length);
|
||||
const opts = { protocol: up.protocol, hostname: up.hostname, port: up.port || (up.protocol==='https:'?443:80), path: up.pathname+up.search, method: req.method, headers,
|
||||
servername: up.hostname, agent:(up.protocol==='https:'? new https.Agent({keepAlive:true, maxSockets:64, servername: up.hostname}) : new http.Agent({keepAlive:true, maxSockets:64})) };
|
||||
const fw = (up.protocol==='https:'?https:http).request(opts, r=>{ res.writeHead(r.statusCode||500, r.headers); r.pipe(res); });
|
||||
fw.on('error', e=>{ log({ level:'error', rid, phase:'upstream_error', err:String(e) }); res.writeHead(502,{'content-type':'application/json'}); res.end(JSON.stringify({ error:'upstream', message: String(e.message||e) })); });
|
||||
fw.end(body);
|
||||
});
|
||||
});
|
||||
server.headersTimeout=650000; server.keepAliveTimeout=650000;
|
||||
server.listen(PORT,'127.0.0.1',()=>{ log({ level:'info', msg:'proxy listening', addr:'127.0.0.1', port: PORT }); });
|
||||
JS
|
||||
node .github/auto/openai-proxy.js > .github/auto/openai-proxy.log 2>&1 &
|
||||
for i in {1..30}; do if nc -z 127.0.0.1 5054; then break; else sleep 0.2; fi; done || true
|
||||
|
||||
- name: Gate decision (proceed vs. comment-only)
|
||||
timeout-minutes: 5
|
||||
id: gate
|
||||
|
||||
@@ -382,6 +382,62 @@ jobs:
|
||||
node-version: '20'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Start local OpenAI proxy for release (hardened)
|
||||
if: env.OPENAI_API_KEY != ''
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cat > .github/auto/openai-proxy.js <<'JS'
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const crypto = require('crypto');
|
||||
const { URL } = require('url');
|
||||
const PORT = process.env.PORT || 5058;
|
||||
const API_KEY = process.env.OPENAI_API_KEY || '';
|
||||
const UPSTREAM = new URL(process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1');
|
||||
const ALLOWED = ['/v1/chat/completions','/v1/responses'];
|
||||
if (!API_KEY) { process.exit(0); }
|
||||
function log(ev){ try{ console.log(JSON.stringify({ ts:new Date().toISOString(), ...ev })); }catch{} }
|
||||
const server = http.createServer((req, res) => {
|
||||
const rid = crypto.randomUUID();
|
||||
if (!ALLOWED.some(p => req.url.startsWith(p))) { res.writeHead(403,{'content-type':'application/json'}); res.end(JSON.stringify({error:'blocked', path:req.url})); return; }
|
||||
const chunks=[]; let size=0; req.on('data', c=>{chunks.push(c); size+=c.length; if (size>2*1024*1024) req.destroy();});
|
||||
req.on('end', ()=>{
|
||||
const body = Buffer.concat(chunks);
|
||||
const up = new URL(req.url, UPSTREAM);
|
||||
const headers = { ...req.headers };
|
||||
if (!headers['content-type']) headers['content-type']='application/json';
|
||||
if (!headers['accept']) headers['accept']='text/event-stream';
|
||||
headers['authorization'] = `Bearer ${API_KEY}`;
|
||||
if (up.pathname.startsWith('/v1/responses') && !headers['openai-beta']) headers['openai-beta']='responses=experimental';
|
||||
delete headers['host']; headers['host']=up.host;
|
||||
if (headers['content-length']) headers['content-length']=String(body.length);
|
||||
const opts = { protocol: up.protocol, hostname: up.hostname, port: up.port || (up.protocol==='https:'?443:80), path: up.pathname+up.search, method: req.method, headers,
|
||||
servername: up.hostname, agent:(up.protocol==='https:'? new https.Agent({keepAlive:true, maxSockets:64, servername: up.hostname}) : new http.Agent({keepAlive:true, maxSockets:64})) };
|
||||
const fw = (up.protocol==='https:'?https:http).request(opts, r=>{ res.writeHead(r.statusCode||500, r.headers); r.pipe(res); });
|
||||
fw.on('error', e=>{ log({ level:'error', rid, phase:'upstream_error', err:String(e) }); res.writeHead(502,{'content-type':'application/json'}); res.end(JSON.stringify({ error:'upstream', message: String(e.message||e) })); });
|
||||
fw.end(body);
|
||||
});
|
||||
});
|
||||
server.headersTimeout=650000; server.keepAliveTimeout=650000;
|
||||
server.listen(PORT,'127.0.0.1',()=>{ log({ level:'info', msg:'proxy listening', addr:'127.0.0.1', port: PORT }); });
|
||||
JS
|
||||
node .github/auto/openai-proxy.js > .github/auto/openai-proxy.log 2>&1 &
|
||||
for i in {1..30}; do if nc -z 127.0.0.1 5058; then break; else sleep 0.2; fi; done || true
|
||||
|
||||
- name: Configure Code to use Chat Completions fallback
|
||||
if: env.OPENAI_API_KEY != ''
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$HOME/.codex"
|
||||
cat > "$HOME/.codex/config.toml" <<'TOML'
|
||||
[model_providers.openai_chat]
|
||||
name = "OpenAI (Chat)"
|
||||
base_url = "http://127.0.0.1:5058/v1"
|
||||
wire_api = "chat"
|
||||
TOML
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
@@ -657,10 +713,20 @@ jobs:
|
||||
|
||||
# Run Code in fully automated exec mode against the repo root.
|
||||
# Note: the working-directory flag is `--cd` on the `exec` subcommand.
|
||||
./.code-bin/code exec --cd "$GITHUB_WORKSPACE" --full-auto --skip-git-repo-check < release-notes/prompt.expanded.txt || {
|
||||
OPENAI_BASE_URL="http://127.0.0.1:5058/v1" \
|
||||
./.code-bin/code exec -c model_provider=openai_chat --cd "$GITHUB_WORKSPACE" --full-auto --skip-git-repo-check < release-notes/prompt.expanded.txt | tee .github/auto/RELEASE_AGENT_OUT.txt || {
|
||||
echo "Code exec returned non-zero; continuing to check for outputs..." >&2
|
||||
}
|
||||
|
||||
# Assert no fatal streaming/server errors if proxy in use
|
||||
if [ -s .github/auto/RELEASE_AGENT_OUT.txt ]; then
|
||||
if rg -n "^\\[.*\\] ERROR: (stream error|server error|exceeded retry limit)" .github/auto/RELEASE_AGENT_OUT.txt >/dev/null 2>&1; then
|
||||
echo "Agent reported a fatal error (stream/server). Failing job." >&2
|
||||
rg -n "^\\[.*\\] ERROR: (stream error|server error|exceeded retry limit)" .github/auto/RELEASE_AGENT_OUT.txt || true
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Post-process: scrub upstream contributors from the Thanks section (e.g., usernames ending with -oai)
|
||||
if [ -s release-notes/RELEASE_NOTES.md ]; then
|
||||
node - <<'JS'
|
||||
|
||||
@@ -19,7 +19,9 @@ on:
|
||||
|
||||
concurrency:
|
||||
group: upstream-merge
|
||||
cancel-in-progress: true
|
||||
# Do not cancel in‑flight runs; upstream merges can be long‑running. Starting
|
||||
# a new run should not kill the previous one.
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
Reference in New Issue
Block a user