Skip to content

Commit 173fada

Browse files
authored
feat(devbox): adding gateway config to devbox (#701)
1 parent 56a26f9 commit 173fada

7 files changed

Lines changed: 1609 additions & 94 deletions

File tree

.github/workflows/sdk-coverage.yml

Lines changed: 110 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -71,115 +71,132 @@ jobs:
7171
script: |
7272
const fs = require('fs');
7373
const testsPassed = '${{ steps.tests.outcome }}' === 'success';
74-
75-
// Build workflow run URL
7674
const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
77-
78-
let comment = '';
79-
75+
const COMMENT_MARKER = '<!-- sdk-coverage-report -->';
76+
77+
let lines = [COMMENT_MARKER];
78+
8079
if (!testsPassed) {
81-
// Try to parse failed tests from the log
82-
let failedTests = '';
80+
let failedTests = [];
8381
try {
8482
const testOutput = fs.readFileSync('test-output.log', 'utf8');
85-
86-
// Extract failed test names using regex
8783
const failedMatches = testOutput.match(/● (.+?)(?:\n|$)/g);
88-
if (failedMatches && failedMatches.length > 0) {
89-
const failedTestList = failedMatches
90-
.map(match => match.replace('● ', '').trim())
91-
.filter(test => test.length > 0)
92-
.slice(0, 10) // Limit to first 10 failures
93-
.map(test => `- ${test}`)
94-
.join('\n');
95-
96-
if (failedTestList) {
97-
failedTests = `\n\n**Failed Tests:**\n${failedTestList}`;
98-
if (failedMatches.length > 10) {
99-
failedTests += `\n- ... and ${failedMatches.length - 10} more`;
100-
}
101-
}
84+
if (failedMatches) {
85+
failedTests = failedMatches
86+
.map(m => m.replace('● ', '').trim())
87+
.filter(t => t.length > 0)
88+
.slice(0, 10);
10289
}
103-
} catch (error) {
104-
console.log('Could not read test output:', error.message);
105-
}
106-
107-
comment = `## ❌ Object Smoke Tests Failed
108-
109-
### Test Results
110-
❌ Some smoke tests failed
111-
${failedTests}
90+
} catch (e) {}
11291
113-
**Please fix the failing tests before checking coverage.**
114-
115-
[📋 View full test logs](${runUrl})`;
92+
lines.push('## ❌ Object Smoke Tests Failed');
93+
lines.push('');
94+
lines.push('### Test Results');
95+
lines.push('❌ Some smoke tests failed');
96+
if (failedTests.length > 0) {
97+
lines.push('');
98+
lines.push('**Failed Tests:**');
99+
failedTests.forEach(t => lines.push(`- ${t}`));
100+
}
101+
lines.push('');
102+
lines.push('**Please fix the failing tests before checking coverage.**');
103+
lines.push('');
104+
lines.push(`[📋 View full test logs](${runUrl})`);
116105
} else {
117106
let coverageSummary = null;
118107
try {
119108
coverageSummary = JSON.parse(fs.readFileSync('coverage-objects/coverage-summary.json', 'utf8'));
120-
} catch (error) {
121-
console.log('Coverage summary not found:', error.message);
122-
comment = `## ⚠️ Coverage Data Missing
123-
124-
### Test Results
125-
✅ All smoke tests passed
109+
} catch (e) {}
110+
111+
if (!coverageSummary) {
112+
lines.push('## ⚠️ Coverage Data Missing');
113+
lines.push('');
114+
lines.push('### Test Results');
115+
lines.push('✅ All smoke tests passed');
116+
lines.push('');
117+
lines.push('### Coverage Results');
118+
lines.push('⚠️ Coverage data could not be read');
119+
lines.push('');
120+
lines.push(`[📋 View workflow logs](${runUrl})`);
121+
} else {
122+
const total = coverageSummary.total;
123+
const funcs = total.functions.pct;
124+
const lns = total.lines.pct;
125+
const branches = total.branches.pct;
126+
const stmts = total.statements.pct;
127+
const coveragePassed = funcs === 100;
128+
const statusEmoji = coveragePassed ? '✅' : '⚠️';
126129
127-
### Coverage Results
128-
⚠️ Coverage data could not be read
130+
lines.push(`## ${statusEmoji} Object Smoke Tests & Coverage Report`);
131+
lines.push('');
132+
lines.push('### Test Results');
133+
lines.push('✅ All smoke tests passed');
134+
lines.push('');
135+
lines.push('### Coverage Results');
136+
lines.push('| Metric | Coverage | Required | Status |');
137+
lines.push('|--------|----------|----------|--------|');
138+
lines.push(`| **Functions** | **${funcs}%** | **100%** | ${coveragePassed ? '✅' : '❌'} |`);
139+
lines.push(`| Lines | ${lns}% | - | ℹ️ |`);
140+
lines.push(`| Branches | ${branches}% | - | ℹ️ |`);
141+
lines.push(`| Statements | ${stmts}% | - | ℹ️ |`);
142+
lines.push('');
143+
lines.push('**Coverage Requirement:** 100% function coverage (all public methods must be called in smoke tests)');
144+
lines.push('');
145+
lines.push(coveragePassed
146+
? '✅ All tests passed and all object methods are covered!'
147+
: '⚠️ Some object methods are not covered in smoke tests. Please add tests that call all public methods.');
148+
lines.push('');
149+
lines.push('<details>');
150+
lines.push('<summary>View detailed coverage report</summary>');
151+
lines.push('');
152+
lines.push('| File | Functions | Lines | Branches |');
153+
lines.push('|------|-----------|-------|----------|');
129154
130-
[📋 View workflow logs](${runUrl})`;
155+
for (const [filePath, data] of Object.entries(coverageSummary)) {
156+
if (filePath === 'total') continue;
157+
const shortPath = filePath.replace(/^.*?src\//, 'src/');
158+
const funcPct = data.functions.pct;
159+
const linesPct = data.lines.pct;
160+
const branchesPct = data.branches.pct;
161+
const funcStatus = funcPct === 100 ? '✅' : '❌';
162+
lines.push(`| ${shortPath} | ${funcStatus} ${funcPct}% | ${linesPct}% | ${branchesPct}% |`);
163+
}
131164
132-
github.rest.issues.createComment({
133-
issue_number: context.issue.number,
134-
owner: context.repo.owner,
135-
repo: context.repo.repo,
136-
body: comment
137-
});
138-
return;
165+
lines.push('');
166+
lines.push('</details>');
167+
lines.push('');
168+
lines.push(`[📋 View workflow run](${runUrl})`);
139169
}
140-
141-
const total = coverageSummary.total;
142-
const functions = total.functions.pct;
143-
const lines = total.lines.pct;
144-
const branches = total.branches.pct;
145-
const statements = total.statements.pct;
146-
147-
const coveragePassed = functions === 100;
148-
const allPassed = testsPassed && coveragePassed;
149-
const statusEmoji = allPassed ? '✅' : '⚠️';
150-
151-
comment = `## ${statusEmoji} Object Smoke Tests & Coverage Report
152-
153-
### Test Results
154-
✅ All smoke tests passed
155-
156-
### Coverage Results
157-
| Metric | Coverage | Required | Status |
158-
|--------|----------|----------|--------|
159-
| **Functions** | **${functions}%** | **100%** | ${coveragePassed ? '✅' : '❌'} |
160-
| Lines | ${lines}% | - | ℹ️ |
161-
| Branches | ${branches}% | - | ℹ️ |
162-
| Statements | ${statements}% | - | ℹ️ |
163-
164-
**Coverage Requirement:** 100% function coverage (all public methods must be called in smoke tests)
165-
166-
${coveragePassed
167-
? '✅ All tests passed and all object methods are covered!'
168-
: '⚠️ Some object methods are not covered in smoke tests. Please add tests that call all public methods.'}
169-
170-
<details>
171-
<summary>View detailed coverage report</summary>
172-
173-
Coverage reports are available in the workflow artifacts. Lines/branches/statements coverage is tracked but not required to be 100%.
174-
175-
</details>
176-
177-
[📋 View workflow run](${runUrl})`;
178170
}
179-
180-
github.rest.issues.createComment({
181-
issue_number: context.issue.number,
171+
172+
const comment = lines.join('\n');
173+
174+
const { data: comments } = await github.rest.issues.listComments({
182175
owner: context.repo.owner,
183176
repo: context.repo.repo,
184-
body: comment
177+
issue_number: context.issue.number
185178
});
179+
180+
const existingComment = comments.find(c => c.body.includes(COMMENT_MARKER));
181+
182+
if (existingComment) {
183+
await github.rest.issues.updateComment({
184+
owner: context.repo.owner,
185+
repo: context.repo.repo,
186+
comment_id: existingComment.id,
187+
body: comment
188+
});
189+
} else {
190+
await github.rest.issues.createComment({
191+
issue_number: context.issue.number,
192+
owner: context.repo.owner,
193+
repo: context.repo.repo,
194+
body: comment
195+
});
196+
}
197+
198+
- name: Fail if tests failed
199+
if: steps.tests.outcome != 'success'
200+
run: |
201+
echo "Smoke tests failed. See PR comment for details."
202+
exit 1

src/sdk.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { StorageObject } from './sdk/storage-object';
88
import { Agent } from './sdk/agent';
99
import { Scorer } from './sdk/scorer';
1010
import { NetworkPolicy } from './sdk/network-policy';
11+
import { GatewayConfig } from './sdk/gateway-config';
1112
import { Scenario } from './sdk/scenario';
1213

1314
// Import types used in this file
@@ -22,6 +23,7 @@ import type { ObjectCreateParams, ObjectListParams } from './resources/objects';
2223
import type { AgentCreateParams, AgentListParams } from './resources/agents';
2324
import type { ScorerCreateParams, ScorerListParams } from './resources/scenarios/scorers';
2425
import type { NetworkPolicyCreateParams, NetworkPolicyListParams } from './resources/network-policies';
26+
import type { GatewayConfigCreateParams, GatewayConfigListParams } from './resources/gateway-configs';
2527
import type { ScenarioListParams } from './resources/scenarios/scenarios';
2628
import { PollingOptions } from './lib/polling';
2729
import * as Shared from './resources/shared';
@@ -284,6 +286,15 @@ export class RunloopSDK {
284286
*/
285287
public readonly networkPolicy: NetworkPolicyOps;
286288

289+
/**
290+
* **Gateway Config Operations** - {@link GatewayConfigOps} for creating and accessing {@link GatewayConfig} class instances.
291+
*
292+
* Gateway configs define how to proxy API requests through the credential gateway. They specify
293+
* the target endpoint and how credentials should be applied. Use with devboxes to securely
294+
* proxy requests to external APIs without exposing API keys.
295+
*/
296+
public readonly gatewayConfig: GatewayConfigOps;
297+
287298
/**
288299
* **Scenario Operations** - {@link ScenarioOps} for accessing {@link Scenario} class instances.
289300
*
@@ -305,6 +316,7 @@ export class RunloopSDK {
305316
this.agent = new AgentOps(this.api);
306317
this.scorer = new ScorerOps(this.api);
307318
this.networkPolicy = new NetworkPolicyOps(this.api);
319+
this.gatewayConfig = new GatewayConfigOps(this.api);
308320
this.scenario = new ScenarioOps(this.api);
309321
}
310322
}
@@ -1508,6 +1520,117 @@ export class NetworkPolicyOps {
15081520
}
15091521
}
15101522

1523+
/**
1524+
* Gateway Config SDK interface for managing gateway configurations.
1525+
*
1526+
* @category Gateway Config
1527+
*
1528+
* @remarks
1529+
* ## Overview
1530+
*
1531+
* The `GatewayConfigOps` class provides a high-level abstraction for managing gateway configurations,
1532+
* which define how to proxy API requests through the credential gateway. Gateway configs specify
1533+
* the target endpoint and how credentials should be applied, enabling secure API proxying without
1534+
* exposing API keys.
1535+
*
1536+
* ## Usage
1537+
*
1538+
* This interface is accessed via {@link RunloopSDK.gatewayConfig}. You should construct
1539+
* a {@link RunloopSDK} instance and use it from there:
1540+
*
1541+
* @example
1542+
* ```typescript
1543+
* const runloop = new RunloopSDK();
1544+
* const gatewayConfig = await runloop.gatewayConfig.create({
1545+
* name: 'my-api-gateway',
1546+
* endpoint: 'https://api.example.com',
1547+
* auth_mechanism: { type: 'bearer' },
1548+
* });
1549+
*
1550+
* // Use with a devbox
1551+
* const devbox = await runloop.devbox.create({
1552+
* name: 'my-devbox',
1553+
* gateways: {
1554+
* 'MY_API': {
1555+
* gateway: gatewayConfig.id,
1556+
* secret: 'my-api-key-secret',
1557+
* },
1558+
* },
1559+
* });
1560+
* ```
1561+
*/
1562+
export class GatewayConfigOps {
1563+
/**
1564+
* @private
1565+
*/
1566+
constructor(private client: RunloopAPI) {}
1567+
1568+
/**
1569+
* Create a new gateway config.
1570+
*
1571+
* @example
1572+
* ```typescript
1573+
* const runloop = new RunloopSDK();
1574+
* const gatewayConfig = await runloop.gatewayConfig.create({
1575+
* name: 'my-gateway',
1576+
* endpoint: 'https://api.example.com',
1577+
* auth_mechanism: { type: 'header', key: 'x-api-key' },
1578+
* description: 'Gateway for My API',
1579+
* });
1580+
* ```
1581+
*
1582+
* @param {GatewayConfigCreateParams} params - Parameters for creating the gateway config.
1583+
* @param {Core.RequestOptions} [options] - Request options.
1584+
* @returns {Promise<GatewayConfig>} A {@link GatewayConfig} instance.
1585+
*/
1586+
async create(params: GatewayConfigCreateParams, options?: Core.RequestOptions): Promise<GatewayConfig> {
1587+
return GatewayConfig.create(this.client, params, options);
1588+
}
1589+
1590+
/**
1591+
* Get a gateway config object by its ID.
1592+
*
1593+
* @example
1594+
* ```typescript
1595+
* const runloop = new RunloopSDK();
1596+
* const gatewayConfig = runloop.gatewayConfig.fromId('gwc_1234567890');
1597+
* const info = await gatewayConfig.getInfo();
1598+
* console.log(`Gateway Config name: ${info.name}`);
1599+
* ```
1600+
*
1601+
* @param {string} id - The ID of the gateway config.
1602+
* @returns {GatewayConfig} A {@link GatewayConfig} instance.
1603+
*/
1604+
fromId(id: string): GatewayConfig {
1605+
return GatewayConfig.fromId(this.client, id);
1606+
}
1607+
1608+
/**
1609+
* List gateway configs with optional filters (paginated).
1610+
*
1611+
* @example
1612+
* ```typescript
1613+
* const runloop = new RunloopSDK();
1614+
* const configs = await runloop.gatewayConfig.list({ limit: 10 });
1615+
* console.log(configs.map((c) => c.id));
1616+
* ```
1617+
*
1618+
* @param {GatewayConfigListParams} [params] - Optional filter parameters.
1619+
* @param {Core.RequestOptions} [options] - Request options.
1620+
* @returns {Promise<GatewayConfig[]>} An array of {@link GatewayConfig} instances.
1621+
*/
1622+
async list(params?: GatewayConfigListParams, options?: Core.RequestOptions): Promise<GatewayConfig[]> {
1623+
const result = await this.client.gatewayConfigs.list(params, options);
1624+
const configs: GatewayConfig[] = [];
1625+
1626+
for await (const config of result) {
1627+
configs.push(GatewayConfig.fromId(this.client, config.id));
1628+
}
1629+
1630+
return configs;
1631+
}
1632+
}
1633+
15111634
/**
15121635
* Scenario SDK interface for managing scenarios.
15131636
*
@@ -1630,6 +1753,7 @@ export declare namespace RunloopSDK {
16301753
AgentOps as AgentOps,
16311754
ScorerOps as ScorerOps,
16321755
NetworkPolicyOps as NetworkPolicyOps,
1756+
GatewayConfigOps as GatewayConfigOps,
16331757
ScenarioOps as ScenarioOps,
16341758
Devbox as Devbox,
16351759
Blueprint as Blueprint,
@@ -1638,6 +1762,7 @@ export declare namespace RunloopSDK {
16381762
Agent as Agent,
16391763
Scorer as Scorer,
16401764
NetworkPolicy as NetworkPolicy,
1765+
GatewayConfig as GatewayConfig,
16411766
Scenario as Scenario,
16421767
};
16431768
}

0 commit comments

Comments
 (0)