feat(caretaker): egress cloud run service skeleton (#28167)

This commit is contained in:
Chad
2026-07-01 17:44:38 -07:00
committed by GitHub
parent ff00dacd9f
commit f7af4e5180
8 changed files with 315 additions and 0 deletions
@@ -0,0 +1,5 @@
node_modules
dist
.env
*.log
.git
@@ -0,0 +1,8 @@
FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
EXPOSE 8080
CMD ["node", "dist/server.js"]
@@ -0,0 +1,27 @@
{
"name": "egress-service",
"version": "1.0.0",
"description": "GitHub Egress Pub/Sub Cloud Run worker service",
"main": "dist/server.js",
"scripts": {
"dev": "tsx watch src/server.ts",
"build": "tsc",
"start": "node dist/server.js",
"test": "vitest run"
},
"dependencies": {
"@octokit/auth-app": "^8.2.0",
"@octokit/rest": "^20.1.1",
"dotenv": "^16.4.5",
"express": "^4.19.2"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^20.12.12",
"@types/supertest": "^6.0.3",
"supertest": "^7.1.4",
"tsx": "^4.9.3",
"typescript": "^5.4.5",
"vitest": "^1.6.0"
}
}
@@ -0,0 +1,84 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import request from 'supertest';
import { app } from './app.js';
/**
* Helper function simulating GCP Cloud Pub/Sub HTTP Push message wrapper.
* Encodes the payload object into Base64 format inside message.data.
*/
function createPubSubPushEnvelope(payload: unknown): {
message: { data: string };
} {
const jsonString =
typeof payload === 'string' ? payload : JSON.stringify(payload);
const base64Data = Buffer.from(jsonString).toString('base64');
return { message: { data: base64Data } };
}
describe('Egress Service App Router', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('GET / should return 200 OK with structured health debug info', async () => {
const res = await request(app).get('/');
expect(res.status).toBe(200);
expect(res.body).toEqual({
status: 'healthy',
service: 'caretaker-egress-service',
revision: 'local',
});
});
it('POST / should return 400 if Pub/Sub envelope is invalid', async () => {
const res = await request(app).post('/').send('not a json object');
expect(res.status).toBe(400);
});
it('POST / should return 400 if message.data is missing', async () => {
const res = await request(app).post('/').send({ message: {} });
expect(res.status).toBe(400);
expect(res.text).toBe('Missing message.data');
});
it('POST / should return 400 if message.data is invalid JSON', async () => {
const invalidEnvelope = createPubSubPushEnvelope('invalid-raw-json-string');
const res = await request(app).post('/').send(invalidEnvelope);
expect(res.status).toBe(400);
expect(res.text).toBe('Malformed payload: invalid JSON');
});
it('POST / should return 400 if egress payload is missing required fields', async () => {
const incompleteEvent = { action: 'COMMENT', payload: { owner: 'google' } };
const res = await request(app)
.post('/')
.send(createPubSubPushEnvelope(incompleteEvent));
expect(res.status).toBe(400);
expect(res.text).toContain('Malformed payload');
});
it('POST / should trigger handleEgressEvent stub and return 200 for valid payloads', async () => {
const validEvent = {
action: 'COMMENT',
payload: {
owner: 'google-gemini',
repo: 'gemini-cli',
issueNumber: 100,
commentBody: 'Test comment',
},
};
const res = await request(app)
.post('/')
.send(createPubSubPushEnvelope(validEvent));
expect(res.status).toBe(200);
expect(res.text).toBe('OK');
});
});
@@ -0,0 +1,78 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import express from 'express';
import dotenv from 'dotenv';
import {
isPubSubMessageEnvelope,
isEgressEvent,
type EgressEvent,
} from './types.js';
dotenv.config();
/**
* Top-down stub handler for Egress events.
* Octokit GitHub REST API integration will be added in a follow-up PR.
*
* @param event - The validated EgressEvent object decoded from Pub/Sub push envelope.
*/
export async function handleEgressEvent(event: EgressEvent): Promise<void> {
console.log(
`[EGRESS_STUB] Received ${event.action} event for ${event.payload.owner}/${event.payload.repo}#${event.payload.issueNumber}`,
);
}
export const app = express();
app.use(express.json());
// Health check endpoint for Cloud Run liveness/readiness probes
app.get('/', (_req, res) => {
res.json({
status: 'healthy',
service: process.env.K_SERVICE || 'caretaker-egress-service',
revision: process.env.K_REVISION || 'local',
});
});
// Pub/Sub push subscription endpoint
app.post('/', async (req, res) => {
if (!isPubSubMessageEnvelope(req.body)) {
return res.status(400).send('Invalid Pub/Sub message envelope');
}
const data = req.body.message?.data;
if (!data) {
return res.status(400).send('Missing message.data');
}
let event: unknown;
try {
const jsonStr = Buffer.from(data, 'base64').toString('utf-8');
event = JSON.parse(jsonStr);
} catch {
return res.status(400).send('Malformed payload: invalid JSON');
}
if (!isEgressEvent(event)) {
return res
.status(400)
.send('Malformed payload: missing or invalid required egress fields');
}
try {
await handleEgressEvent(event);
console.log(
`[EGRESS] Successfully executed ${event.action} for ${event.payload.owner}/${event.payload.repo}#${event.payload.issueNumber}`,
);
return res.status(200).send('OK');
} catch (err) {
console.error('[EGRESS_ERROR] Error handling egress event execution:', err);
return res
.status(500)
.send(err instanceof Error ? err.message : 'Internal Server Error');
}
});
@@ -0,0 +1,13 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { app } from './app.js';
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log(`Egress service listening on port ${port}`);
});
@@ -0,0 +1,84 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
export type EgressAction = 'COMMENT' | 'LABEL' | 'PATCH';
export interface EgressEventPayload {
owner: string;
repo: string;
issueNumber: number;
commentBody?: string;
labels?: string[];
patchContent?: string;
branchName?: string;
}
export interface EgressEvent {
action: EgressAction;
payload: EgressEventPayload;
}
export interface PubSubMessage {
data?: string;
messageId?: string;
publishTime?: string;
attributes?: Record<string, string>;
}
/**
* Standard GCP Cloud Pub/Sub HTTP Push message wrapper envelope.
*
* @see https://cloud.google.com/pubsub/docs/push#delivery_format
*/
export interface PubSubMessageEnvelope {
message?: PubSubMessage;
subscription?: string;
}
function isObject(obj: unknown): obj is Record<string, unknown> {
return typeof obj === 'object' && obj !== null;
}
/**
* Type guard for PubSubMessageEnvelope to eliminate unsafe 'as' casts.
*/
export function isPubSubMessageEnvelope(
obj: unknown,
): obj is PubSubMessageEnvelope {
if (!isObject(obj)) {
return false;
}
if ('message' in obj) {
if (obj.message !== undefined && !isObject(obj.message)) {
return false;
}
}
return true;
}
/**
* Type guard for EgressEvent.
*/
export function isEgressEvent(obj: unknown): obj is EgressEvent {
if (!isObject(obj)) {
return false;
}
if (
typeof obj.action !== 'string' ||
!['COMMENT', 'LABEL', 'PATCH'].includes(obj.action)
) {
return false;
}
if (!isObject(obj.payload)) {
return false;
}
const payload = obj.payload;
return (
typeof payload.owner === 'string' &&
typeof payload.repo === 'string' &&
typeof payload.issueNumber === 'number'
);
}
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}