Skip to content

Commit a8366ed

Browse files
authored
fix: finish staging ingest callbacks (#568)
Allow the protected staging deployment to invalidate its cache using the Vercel automation bypass, stop retrying non-transient authorization failures, suppress staging Slack alerts, and use the broader callback PAT. Add regression coverage for the branch-scoped invalidation exception.\n\n中文:修复暂存结果摄取的收尾流程。受 Vercel 保护的 staging 部署改用自动化绕过凭据刷新缓存,不再重复重试非瞬态鉴权失败;同时关闭 staging 的 Slack 告警,并使用权限更完整的 PAT 发送回调。新增分支范围鉴权例外的回归测试。
1 parent d18bf92 commit a8366ed

4 files changed

Lines changed: 69 additions & 16 deletions

File tree

.github/workflows/ingest-agentic-results.yml

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,6 @@ on:
5353
VERCEL_INVALIDATE_SECRET:
5454
description: Production and development cache invalidation bearer token
5555
required: false
56-
VERCEL_STAGING_INVALIDATE_SECRET:
57-
description: Staging cache invalidation bearer token
58-
required: true
5956
VERCEL_STAGING_BYPASS_SECRET:
6057
description: Vercel protection automation bypass token
6158
required: true
@@ -128,7 +125,6 @@ jobs:
128125
DATABASE_WRITE_URL_DEV: ${{ secrets.DATABASE_DEV_WRITE_URL }}
129126
DATABASE_WRITE_URL_STAGING: ${{ secrets.DATABASE_STAGING_WRITE_URL }}
130127
INVALIDATE_SECRET_DEFAULT: ${{ secrets.VERCEL_INVALIDATE_SECRET }}
131-
INVALIDATE_SECRET_STAGING: ${{ secrets.VERCEL_STAGING_INVALIDATE_SECRET }}
132128
PROTECTION_BYPASS_SECRET_STAGING: ${{ secrets.VERCEL_STAGING_BYPASS_SECRET }}
133129
STAGING_SITE_URL: ${{ vars.STAGING_SITE_URL || 'https://inferencemax-app-git-staging-semianalysisai.vercel.app' }}
134130
run: |
@@ -146,7 +142,6 @@ jobs:
146142
staging)
147143
database_write_url="$DATABASE_WRITE_URL_STAGING"
148144
cache_invalidate_url="${STAGING_SITE_URL%/}/api/v1/invalidate"
149-
cache_invalidate_secret="$INVALIDATE_SECRET_STAGING"
150145
protection_bypass_secret="$PROTECTION_BYPASS_SECRET_STAGING"
151146
;;
152147
*)
@@ -159,7 +154,7 @@ jobs:
159154
echo "::error::Database secret is empty for target: $REQUESTED_DATABASE_TARGET"
160155
exit 1
161156
fi
162-
if [ -z "$cache_invalidate_secret" ]; then
157+
if [ "$REQUESTED_DATABASE_TARGET" != "staging" ] && [ -z "$cache_invalidate_secret" ]; then
163158
echo "::error::Cache invalidation secret is empty for target: $REQUESTED_DATABASE_TARGET"
164159
exit 1
165160
fi
@@ -169,7 +164,9 @@ jobs:
169164
fi
170165
171166
echo "::add-mask::$database_write_url"
172-
echo "::add-mask::$cache_invalidate_secret"
167+
if [ -n "$cache_invalidate_secret" ]; then
168+
echo "::add-mask::$cache_invalidate_secret"
169+
fi
173170
if [ -n "$protection_bypass_secret" ]; then
174171
echo "::add-mask::$protection_bypass_secret"
175172
fi
@@ -259,8 +256,7 @@ jobs:
259256
- name: Invalidate Vercel cache
260257
run: |
261258
if [ "$INGEST_DATABASE_TARGET" = "staging" ]; then
262-
curl --retry 12 --retry-delay 10 --retry-all-errors -sSf -X POST "$CACHE_INVALIDATE_URL" \
263-
-H "Authorization: Bearer $CACHE_INVALIDATE_SECRET" \
259+
curl --retry 3 --retry-delay 2 --retry-connrefused -sSf -X POST "$CACHE_INVALIDATE_URL" \
264260
-H "x-vercel-protection-bypass: $CACHE_PROTECTION_BYPASS_SECRET"
265261
else
266262
curl -sSf -X POST "$CACHE_INVALIDATE_URL" \
@@ -293,7 +289,7 @@ jobs:
293289
fi
294290
295291
- name: Notify Slack on unmapped entities
296-
if: steps.unmapped.outputs.found == 'true'
292+
if: steps.unmapped.outputs.found == 'true' && env.INGEST_DATABASE_TARGET != 'staging'
297293
uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
298294
with:
299295
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
@@ -304,7 +300,7 @@ jobs:
304300
}
305301
306302
- name: Notify Slack on failure
307-
if: failure()
303+
if: failure() && env.INGEST_DATABASE_TARGET != 'staging'
308304
uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
309305
with:
310306
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}

.github/workflows/stage-results.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,6 @@ jobs:
216216
database-target: staging
217217
secrets:
218218
DATABASE_STAGING_WRITE_URL: ${{ secrets.DATABASE_STAGING_WRITE_URL }}
219-
VERCEL_STAGING_INVALIDATE_SECRET: ${{ secrets.VERCEL_STAGING_INVALIDATE_SECRET }}
220219
VERCEL_STAGING_BYPASS_SECRET: ${{ secrets.VERCEL_STAGING_BYPASS_SECRET }}
221220
INFX_MAIN_PAT: ${{ secrets.INFX_MAIN_PAT }}
222221
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
@@ -238,7 +237,7 @@ jobs:
238237
COMMENT_ID: ${{ needs.validate.outputs.comment-id }}
239238
STAGE_SUCCEEDED: ${{ needs['sync-staging-branch'].result == 'success' && needs['refresh-staging-database'].result == 'success' && needs.ingest.result == 'success' }}
240239
with:
241-
github-token: ${{ secrets.INFX_MAIN_PAT }}
240+
github-token: ${{ secrets.PAT }}
242241
script: |
243242
await github.rest.repos.createDispatchEvent({
244243
owner: 'SemiAnalysisAI',

packages/app/src/app/api/v1/invalidate/route.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,17 @@ vi.mock('@/lib/api-cache', () => ({
1111
import { POST } from './route';
1212

1313
let origSecret: string | undefined;
14+
let origVercelEnv: string | undefined;
15+
let origVercelGitCommitRef: string | undefined;
1416

1517
beforeEach(() => {
1618
vi.clearAllMocks();
1719
origSecret = process.env.INVALIDATE_SECRET;
20+
origVercelEnv = process.env.VERCEL_ENV;
21+
origVercelGitCommitRef = process.env.VERCEL_GIT_COMMIT_REF;
1822
process.env.INVALIDATE_SECRET = 'test-secret-123';
23+
delete process.env.VERCEL_ENV;
24+
delete process.env.VERCEL_GIT_COMMIT_REF;
1925
});
2026

2127
afterEach(() => {
@@ -24,6 +30,16 @@ afterEach(() => {
2430
} else {
2531
process.env.INVALIDATE_SECRET = origSecret;
2632
}
33+
if (origVercelEnv === undefined) {
34+
delete process.env.VERCEL_ENV;
35+
} else {
36+
process.env.VERCEL_ENV = origVercelEnv;
37+
}
38+
if (origVercelGitCommitRef === undefined) {
39+
delete process.env.VERCEL_GIT_COMMIT_REF;
40+
} else {
41+
process.env.VERCEL_GIT_COMMIT_REF = origVercelGitCommitRef;
42+
}
2743
});
2844

2945
function postReq(headers?: Record<string, string>): Request {
@@ -58,6 +74,38 @@ describe('POST /api/v1/invalidate', () => {
5874
expect(res.status).toBe(401);
5975
});
6076

77+
it('allows the Vercel-protected staging branch with an automation bypass', async () => {
78+
process.env.VERCEL_ENV = 'preview';
79+
process.env.VERCEL_GIT_COMMIT_REF = 'staging';
80+
mockPurgeAll.mockResolvedValueOnce(4);
81+
82+
const res = await POST(postReq({ 'x-vercel-protection-bypass': 'opaque-bypass' }));
83+
84+
expect(res.status).toBe(200);
85+
expect(await res.json()).toEqual({ invalidated: true, blobsDeleted: 4 });
86+
expect(mockPurgeAll).toHaveBeenCalledOnce();
87+
});
88+
89+
it('still requires app auth for non-staging preview branches', async () => {
90+
process.env.VERCEL_ENV = 'preview';
91+
process.env.VERCEL_GIT_COMMIT_REF = 'feature-branch';
92+
93+
const res = await POST(postReq({ 'x-vercel-protection-bypass': 'opaque-bypass' }));
94+
95+
expect(res.status).toBe(401);
96+
expect(mockPurgeAll).not.toHaveBeenCalled();
97+
});
98+
99+
it('still requires app auth when staging has no automation bypass header', async () => {
100+
process.env.VERCEL_ENV = 'preview';
101+
process.env.VERCEL_GIT_COMMIT_REF = 'staging';
102+
103+
const res = await POST(postReq());
104+
105+
expect(res.status).toBe(401);
106+
expect(mockPurgeAll).not.toHaveBeenCalled();
107+
});
108+
61109
it('invalidates cache with correct auth', async () => {
62110
mockPurgeAll.mockResolvedValueOnce(15);
63111

packages/app/src/app/api/v1/invalidate/route.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,21 @@ export async function POST(request: Request) {
88
const secret = process.env.INVALIDATE_SECRET;
99
const authHeader = request.headers.get('Authorization') ?? '';
1010
const expected = `Bearer ${secret}`;
11+
// The shared staging deployment is already protected by Vercel. Its CI
12+
// caller must present the project-scoped automation bypass before Vercel
13+
// forwards this header to the route, so a second app secret is redundant.
14+
// Keep this exception branch-specific; production and every other preview
15+
// continue to require INVALIDATE_SECRET below.
16+
const isProtectedStagingRequest =
17+
process.env.VERCEL_ENV === 'preview' &&
18+
process.env.VERCEL_GIT_COMMIT_REF === 'staging' &&
19+
Boolean(request.headers.get('x-vercel-protection-bypass'));
1120

1221
if (
13-
!secret ||
14-
authHeader.length !== expected.length ||
15-
!timingSafeEqual(Buffer.from(authHeader), Buffer.from(expected))
22+
!isProtectedStagingRequest &&
23+
(!secret ||
24+
authHeader.length !== expected.length ||
25+
!timingSafeEqual(Buffer.from(authHeader), Buffer.from(expected)))
1626
) {
1727
return new NextResponse('Unauthorized', { status: 401 });
1828
}

0 commit comments

Comments
 (0)