Skip to content

Commit 37754a1

Browse files
committed
feat: secrets input accepts a map; log per-call API timing
Three changes from the latest deploy run feedback. 1. Map-shaped `secrets` input Mirror the `tags` refactor: accept a YAML block mapping (the canonical workflow form) or a JSON object string, instead of multi-line `KEY=VALUE` lines. Same key validation as before — must start with a letter or underscore, alphanumeric + underscores only — and secret values can still contain any characters. Each value is added to the runner's secret-mask list before any API call. 2. Per-call API response time Every Spice Cloud Management API call now logs `<METHOD> <path> → <status> <statusText> (<durationMs>ms)` so the user can see each request's latency inline. Network failures log `<METHOD> <path> → network error in <durationMs>ms: <message>`. Removed the redundant pre-call path logs in deploy.ts that the timing line now covers. 3. Branch missing from step summary The list-deployments endpoint sometimes returns Deployment objects without `branch` even when the create request set it. The summary now falls back to the `branch` and `commit-sha` inputs we sent so the right values surface even when the API echoes them inconsistently. Tests: 107 passing (up from 101). New cases cover the YAML and JSON secret forms (incl. quote-stripping, special characters in values, and JSON validation) and the post-rebuild `dist/` round-trip.
1 parent 074f5c2 commit 37754a1

12 files changed

Lines changed: 239 additions & 108 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
88

9+
### Changed
10+
- `secrets` input now accepts the same YAML block mapping or JSON object form as `tags`, instead of multi-line `KEY=VALUE` lines. Existing key validation is unchanged (must start with a letter or underscore, alphanumeric + underscores only); values can contain any characters and are added to the runner's secret-mask list. Update your workflows to swap `=` for `:` between key and value.
11+
- Every Spice Cloud Management API call now logs `<METHOD> <path> → <status> <statusText> (<durationMs>ms)` so you can see latency for each request inline in the action logs. Network failures log `<METHOD> <path> → network error in <durationMs>ms: <message>`. Removed the redundant pre-call path logs in `deploy.ts` since the timing line covers them.
12+
13+
### Fixed
14+
- The step-summary "Branch" cell was empty when the management API's list-deployments response omitted the `branch` field even though the create request set it. The summary now falls back to the `branch` and `commit-sha` inputs we sent so the right values surface even when the API echoes them inconsistently.
15+
916
### Added
1017
- New `org` input. The action now constructs the `app-url` output as `https://spice.ai/<org>/<app-name>` (the canonical Spice Cloud portal URL pattern). When `org` is unset, it falls back to the owner part of `GITHUB_REPOSITORY`, which matches the Spice org slug for personal orgs and orgs created from a connected GitHub organization.
1118
- New `flight-url` input. The action now passes a regional Apache Arrow Flight gRPC endpoint to the `@spiceai/spice` SDK so the SQL probe uses gRPC instead of falling through to localhost. When `flight-url` is unset, it's derived from the resolved app's region as `<region>-prod-aws-flight.spiceai.io:443` (mirrors the data hostname with `-data` swapped for `-flight`). A `grpc+tls://` / `grpc://` scheme prefix on the input is stripped automatically.

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ Grant exactly the scopes for the features you use. The "All-in" row at the botto
9292
| `commit-sha` | no | `${{ github.sha }}` | Commit SHA attributed to the deployment. |
9393
| `commit-message` | no | head-commit message | Commit message attributed to the deployment. |
9494
| `debug` | no | `false` | Enable runtime debug mode for this deployment. |
95-
| `secrets` | no | — | Multi-line `KEY=VALUE` app secrets to upsert before deploy. Values are masked. |
95+
| `secrets` | no | — | YAML or JSON map of app secrets to upsert before deploy. Values are masked in logs. |
9696
| `wait-for-completion` | no | `true` | Poll the deployment until it finishes. |
9797
| `timeout-seconds` | no | `600` | Max wait when `wait-for-completion` is true. |
9898
| `poll-interval-seconds` | no | `10` | Seconds between status polls. |
@@ -159,8 +159,8 @@ Grant exactly the scopes for the features you use. The "All-in" row at the botto
159159
client-secret: ${{ secrets.SPICE_CLIENT_SECRET }}
160160
app-name: analytics
161161
secrets: |
162-
OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}
163-
PG_PASSWORD=${{ secrets.PG_PASSWORD }}
162+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
163+
PG_PASSWORD: ${{ secrets.PG_PASSWORD }}
164164
test-sql: SELECT count(*) FROM taxi_trips
165165
```
166166

__tests__/deploy.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ describe("runDeploy", () => {
128128

129129
await runDeploy(api, {
130130
...baseInputs,
131-
secretsRaw: "OPENAI=sk-1\nPG_PASS=hunter2",
131+
secretsRaw: "OPENAI: sk-1\nPG_PASS: hunter2",
132132
});
133133

134134
expect(upsertSecret).toHaveBeenNthCalledWith(1, 42, "OPENAI", "sk-1");

__tests__/secrets.test.ts

Lines changed: 66 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,44 +11,80 @@ describe("parseSecrets", () => {
1111
expect(parseSecrets(undefined)).toEqual([]);
1212
expect(parseSecrets("")).toEqual([]);
1313
expect(parseSecrets("\n \n")).toEqual([]);
14+
expect(parseSecrets("{}")).toEqual([]);
1415
});
1516

16-
it("parses KEY=VALUE pairs across lines", () => {
17-
const result = parseSecrets("FOO=bar\nBAZ=qux quux\n");
18-
expect(result).toEqual([
19-
{ name: "FOO", value: "bar" },
20-
{ name: "BAZ", value: "qux quux" },
21-
]);
22-
});
17+
describe("YAML block-map form", () => {
18+
it("parses KEY: VALUE lines", () => {
19+
expect(parseSecrets("FOO: bar\nBAZ: qux\n")).toEqual([
20+
{ name: "FOO", value: "bar" },
21+
{ name: "BAZ", value: "qux" },
22+
]);
23+
});
2324

24-
it("preserves '=' inside the value", () => {
25-
expect(parseSecrets("URL=https://x.com?a=1&b=2")).toEqual([
26-
{ name: "URL", value: "https://x.com?a=1&b=2" },
27-
]);
28-
});
25+
it("preserves any character (incl. ':' '=' '/') inside the value", () => {
26+
// Splits on the first ':' only. Values are otherwise unrestricted.
27+
expect(parseSecrets("URL: https://x.com:8080?a=1&b=2!@#$%^&*()")).toEqual([
28+
{ name: "URL", value: "https://x.com:8080?a=1&b=2!@#$%^&*()" },
29+
]);
30+
});
2931

30-
it("ignores blank lines and comments", () => {
31-
const result = parseSecrets("\n# a comment\nFOO=bar\n # indented comment\nBAZ=qux\n");
32-
expect(result).toEqual([
33-
{ name: "FOO", value: "bar" },
34-
{ name: "BAZ", value: "qux" },
35-
]);
36-
});
32+
it("strips matching single or double quotes around values", () => {
33+
expect(parseSecrets("A: \"with spaces and : colons\"\nB: 'special !@#$ chars'")).toEqual([
34+
{ name: "A", value: "with spaces and : colons" },
35+
{ name: "B", value: "special !@#$ chars" },
36+
]);
37+
});
3738

38-
it("rejects lines without '='", () => {
39-
expect(() => parseSecrets("FOO\nBAR=baz")).toThrow(/missing "="/);
40-
});
39+
it("ignores blank lines and #-comment lines", () => {
40+
expect(parseSecrets("\n# header\nFOO: bar\n # indented\nBAZ: qux")).toEqual([
41+
{ name: "FOO", value: "bar" },
42+
{ name: "BAZ", value: "qux" },
43+
]);
44+
});
4145

42-
it("rejects names that don't match the API pattern", () => {
43-
expect(() => parseSecrets("1FOO=x")).toThrow(/must start with a letter or underscore/);
44-
expect(() => parseSecrets("FOO-BAR=x")).toThrow(/letters, numbers, and underscores/);
45-
});
46+
it("rejects lines without ':'", () => {
47+
expect(() => parseSecrets("FOO\nBAR: baz")).toThrow(/expected "KEY: VALUE"/);
48+
});
49+
50+
it("rejects names that don't match the API pattern", () => {
51+
expect(() => parseSecrets("1FOO: x")).toThrow(/start with a letter or underscore/);
52+
expect(() => parseSecrets("FOO-BAR: x")).toThrow(/letters, numbers, and underscores/);
53+
});
4654

47-
it("rejects duplicate names", () => {
48-
expect(() => parseSecrets("FOO=a\nFOO=b")).toThrow(/duplicate secret "FOO"/);
55+
it("rejects duplicate names", () => {
56+
expect(() => parseSecrets("FOO: a\nFOO: b")).toThrow(/duplicate secret "FOO"/);
57+
});
58+
59+
it("allows empty values without crashing", () => {
60+
expect(parseSecrets("EMPTY: ")).toEqual([{ name: "EMPTY", value: "" }]);
61+
});
4962
});
5063

51-
it("allows empty values without crashing", () => {
52-
expect(parseSecrets("EMPTY=")).toEqual([{ name: "EMPTY", value: "" }]);
64+
describe("JSON object form", () => {
65+
it("parses a JSON object", () => {
66+
expect(parseSecrets('{"FOO":"bar","BAZ":"qux"}')).toEqual([
67+
{ name: "FOO", value: "bar" },
68+
{ name: "BAZ", value: "qux" },
69+
]);
70+
});
71+
72+
it("preserves any string value", () => {
73+
expect(parseSecrets('{"URL":"https://x.com:8080?a=1&b=2!@#"}')).toEqual([
74+
{ name: "URL", value: "https://x.com:8080?a=1&b=2!@#" },
75+
]);
76+
});
77+
78+
it("rejects malformed JSON that begins with {", () => {
79+
expect(() => parseSecrets("{ not json")).toThrow(/not valid JSON/);
80+
});
81+
82+
it("rejects non-string JSON values", () => {
83+
expect(() => parseSecrets('{"REPLICAS":3}')).toThrow(/must be a string/);
84+
});
85+
86+
it("rejects JSON keys that don't match the API pattern", () => {
87+
expect(() => parseSecrets('{"1FOO":"x"}')).toThrow(/start with a letter or underscore/);
88+
});
5389
});
5490
});

action.yml

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,20 @@ inputs:
9797
default: "false"
9898
secrets:
9999
description: |
100-
Newline-separated `KEY=VALUE` pairs to upsert as app secrets before deploy.
101-
Lines starting with `#` are comments. Values are masked in logs.
100+
App secrets to upsert before deploy, as a YAML or JSON map. Values are
101+
added to the runner's secret-mask list so they don't appear in logs.
102102
103-
Example:
103+
YAML form (recommended):
104104
secrets: |
105-
OPENAI_API_KEY=<openai-api-key>
106-
PG_PASSWORD=<pg-password>
105+
OPENAI_API_KEY: <openai-api-key>
106+
PG_PASSWORD: <pg-password>
107+
108+
JSON form:
109+
secrets: '{"OPENAI_API_KEY":"<openai-api-key>","PG_PASSWORD":"<pg-password>"}'
110+
111+
Lines beginning with `#` are treated as comments. Secret values can
112+
contain any characters; only the secret name is constrained (must
113+
start with a letter or underscore, alphanumeric + underscores only).
107114
required: false
108115
wait-for-completion:
109116
description: Poll the deployment until it succeeds or fails.

dist/index.js

Lines changed: 43 additions & 43 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/index.js.map

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/full.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ jobs:
3333
team: data-platform
3434
commit: ${{ github.sha }}
3535
secrets: |
36-
OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}
37-
PG_PASSWORD=${{ secrets.PG_PASSWORD }}
36+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
37+
PG_PASSWORD: ${{ secrets.PG_PASSWORD }}
3838
test-sql: SELECT count(*) AS n FROM taxi_trips
3939
test-nsql: Show me revenue by month for the last 6 months
4040
test-chat: "Summarize today's top 3 trips"

src/api.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ export class SpiceApiClient {
9797
};
9898
if (body !== undefined) headers["Content-Type"] = "application/json";
9999

100+
const startMs = Date.now();
100101
let res: Response;
101102
try {
102103
res = await this.fetchImpl(url, {
@@ -105,7 +106,9 @@ export class SpiceApiClient {
105106
body: body === undefined ? undefined : JSON.stringify(body),
106107
});
107108
} catch (err) {
109+
const durationMs = Date.now() - startMs;
108110
lastError = err as Error;
111+
core.info(`${method} ${path} → network error in ${durationMs}ms: ${lastError.message}`);
109112
if (attempt < this.maxAttempts) {
110113
await this.sleep(this.backoff(attempt));
111114
continue;
@@ -117,6 +120,9 @@ export class SpiceApiClient {
117120
);
118121
}
119122

123+
const durationMs = Date.now() - startMs;
124+
core.info(`${method} ${path}${res.status} ${res.statusText} (${durationMs}ms)`);
125+
120126
if (res.status === 204) {
121127
return undefined as T;
122128
}

src/deploy.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ export async function runDeploy(
5151

5252
const body = buildDeploymentBody(inputs);
5353
core.startGroup("Trigger deployment");
54-
core.info(`POST /v1/apps/${app.id}/deployments`);
5554
core.info(`Payload: ${JSON.stringify(body)}`);
5655
const deployment = await api.createDeployment(app.id, body);
5756
core.info(`Deployment created (id=${deployment.id}, status=${deployment.status}).`);
@@ -162,7 +161,7 @@ async function maybeUpdateAppMetadata(
162161
const merged = { ...(app.tags ?? {}), ...tags };
163162
const update: UpdateAppBody = { tags: merged };
164163
core.startGroup("Update app tags");
165-
core.info(`PUT /v1/apps/${app.id} tags=${JSON.stringify(merged)}`);
164+
core.info(`Tags: ${JSON.stringify(merged)}`);
166165
await api.updateApp(app.id, update);
167166
core.endGroup();
168167
}
@@ -192,7 +191,7 @@ async function maybeUpsertSecrets(
192191

193192
core.startGroup(`Upsert ${secrets.length} secret(s)`);
194193
for (const secret of secrets) {
195-
core.info(`POST /v1/apps/${app.id}/secrets — ${secret.name}`);
194+
core.info(`Secret: ${secret.name}`);
196195
await api.upsertSecret(app.id, secret.name, secret.value);
197196
}
198197
core.endGroup();

0 commit comments

Comments
 (0)