Commit

Deployments work end to end: fixes from the first live run

Verified on syntaqx/automation-lab: the Deployments plan bought with Stripe's test card; production built (28 s) and served its assets, wrangler vars, config for all environments and the Production row of a secret; pull request #6 got a preview with the Preview row, noindex, and the check 'g1t / deploy: Preview is live'; closing it took the preview down; every build was charged at cost plus 20%. - API: a build's reports came back as 500 ('Can't modify immutable headers'), so the sandbox gave up after 'started'. The pass-through now answers with a fresh response. - API: request bodies are turned to snake_case, so never reached the secrets store. It reads (camelCase still works) and the docs say so. - is named as callers send it. - The deployments service logs each report a build sends, and the runner a sandbox's exit code, so a failed build says why in the logs.

syntaqxcommitted Parent3d9f788Browse files
8 files+48−120/8 viewed
+12−2
361361 init.with_method(Method::Post)
362362 .with_headers(headers)
363363 .with_body(Some(worker::js_sys::Uint8Array::from(body.as_slice()).into()));
364− return env
364+ let mut answer = env
365365 .service("DEPLOYMENTS")?
366366 .fetch_request(Request::new_with_init(&target, &init)?)
367− .await;
367+ .await?;
368+ // A fresh response: a fetched one's headers cannot be changed, and
369+ // every response gets the API's own on the way out.
370+ let status = answer.status_code();
371+ return Ok(Response::from_bytes(answer.bytes().await?)?
372+ .with_status(status)
373+ .with_headers({
374+ let headers = worker::Headers::new();
375+ headers.set("content-type", "application/json")?;
376+ headers
377+ }));
368378 }
369379
370380 // A sandbox's artifacts and cache, with its job's token, which is not a
+6−4
579579 "The secrets of a repository (with the workspace's rows that reach it) or of a workspace: each row's key, the environments it applies to, and whether workflows (`secrets.NAME`), deployments, or both read it. Values are never returned. Members only."
580580 }
581581 Op::SetActionsSecret => {
582− "Add or change a secret's row. Without `id` or `environments`, the key's row for every environment, as GitHub's API addresses a secret. `availableTo` is workflows and/or deployments (both, for a new row); `environments` limits it to some, such as production or preview, so a key can hold a value per environment. A variable's row can become a secret this way; a secret never becomes a variable. A repository's need a member; a workspace's an owner. Workspace tokens, G1T_TOKEN included, cannot change them."
582+ "Add or change a secret's row. Without `id` or `environments`, the key's row for every environment, as GitHub's API addresses a secret. `available_to` is workflows and/or deployments (both, for a new row); `environments` limits it to some, such as production or preview, so a key can hold a value per environment. A variable's row can become a secret this way; a secret never becomes a variable. A repository's need a member; a workspace's an owner. Workspace tokens, G1T_TOKEN included, cannot change them."
583583 }
584584 Op::DeleteActionsSecret => "Remove a secret: one row by `id`, or every row of the key.",
585585 Op::ListActionsVariables => {
989989 "setting": { "type": "string", "description": "The key, such as NPM_TOKEN." },
990990 "value": { "type": "string", "description": "Needed for a new row; left out, the row keeps its value." },
991991 "id": { "type": "string", "description": "The row to change, from a list. Left out: the key's row for every environment." },
992− "availableTo": {
992+ "available_to": {
993993 "type": "array",
994994 "items": { "type": "string", "enum": ["workflows", "deployments"] },
995995 "description": "Who reads it. Both for a new row."
17341734 if let Some(value) = input["value"].as_str() {
17351735 args["value"] = json!(value);
17361736 }
1737− for key in ["availableTo", "environments", "repositories"] {
1737+ // Request bodies arrive in snake_case; the actions service
1738+ // takes `availableTo`.
1739+ for (key, to) in [("available_to", "availableTo"), ("environments", "environments"), ("repositories", "repositories")] {
17381740 if let Some(list) = strings(input, key) {
1739− args[key] = json!(list);
1741+ args[to] = json!(list);
17401742 }
17411743 }
17421744 for key in ["id", "note"] {
+2−2
119119 | Field | |
120120 | --- | --- |
121121 | `id` | The row to change or remove, from a list. |
122−| `availableTo` | `["workflows"]`, `["deployments"]` or both. |
122+| `available_to` | `["workflows"]`, `["deployments"]` or both. `availableTo` works too. |
123123 | `environments` | `["production"]`, `["preview", "staging"]`; `[]` for all. |
124124 | `repositories` | A workspace's row: repository names; `[]` for every one. |
125125 | `note` | Where to rotate it, or who to ask. |
127127 ```sh
128128 curl -X PUT https://api.g1t.sh/repos/acme/web/actions/secrets/STRIPE_KEY \
129129 -H "Authorization: Bearer $YOUR_TOKEN" \
130− -d '{"value":"sk_live_…","environments":["production"],"availableTo":["deployments"]}'
130+ -d '{"value":"sk_live_…","environments":["production"],"available_to":["deployments"]}'
131131 ```
132132
133133 Unlike GitHub's, a secret is sent as plain `value` over HTTPS, not
+1−1
147147 | `rerun_workflow_run` | `repo`, `id` | Run it again; `failed_only` for the jobs that did not succeed. Members only. | `POST /repos/{owner}/{name}/actions/runs/{id}/rerun` |
148148 | `update_workflow` | `repo`, `workflow`, `enabled` | Turn a workflow on or off. Members only. | `PATCH /repos/{owner}/{name}/actions/workflows/{workflow}` |
149149 | `list_actions_secrets` | `repo` or `workspace` | Secrets' rows: key, environments, who reads them. Never values. | `GET /repos/{owner}/{name}/actions/secrets`, `GET /workspaces/{workspace}/actions/secrets` |
150−| `set_actions_secret` | `setting` | Add or change a secret's row: `value`, and optionally `id`, `environments`, `availableTo`, `repositories`, `note`. | `PUT …/actions/secrets/{name}` |
150+| `set_actions_secret` | `setting` | Add or change a secret's row: `value`, and optionally `id`, `environments`, `available_to`, `repositories`, `note`. | `PUT …/actions/secrets/{name}` |
151151 | `delete_actions_secret` | `setting` | Remove one row (`id`) or every row of the key. | `DELETE …/actions/secrets/{name}` |
152152 | `list_actions_variables` | `repo` or `workspace` | Config rows with their values. | `GET …/actions/variables` |
153153 | `set_actions_variable` | `setting` | Add or change a config row, as for secrets. | `POST …/actions/variables`, `PATCH …/variables/{name}` |
+1−1
247247 production/preview, or a job's `environment:`), and whether workflows,
248248 deployments or both read it. API: `{repo}/actions/secrets` and
249249 `{repo}/actions/variables` (GitHub's routes) with extra `environments`,
250−`availableTo`, `repositories`, `note`, `id`. Trusted jobs get
250+`available_to`, `repositories`, `note`, `id`. Trusted jobs get
251251 `secrets.G1T_TOKEN` (the workspace's token; `GITHUB_TOKEN` is its alias),
252252 which cannot change secrets. Guide:
253253 https://docs.g1t.sh/guides/secrets-and-variables/
+22−1
318318 pub value: Option<String>,
319319 /// `workflows` and/or `deployments`; left out, unchanged (both, for a
320320 /// new row).
321− #[serde(default, alias = "availableTo")]
321+ // Named as callers send it: an `alias` is not honoured beside the
322+ // flattened owner in the Worker's build.
323+ #[serde(default, rename = "availableTo")]
322324 pub available_to: Option<Vec<String>>,
323325 /// The environments it applies to; empty is every one. Left out,
324326 /// unchanged.
391393 /// Minutes before the job is stopped.
392394 pub timeout_minutes: u32,
393395 }
396+
397+#[cfg(test)]
398+mod setting_args_tests {
399+ use super::*;
400+
401+ #[test]
402+ fn who_reads_a_row_is_read_as_the_site_and_api_send_it() {
403+ let args: SetSettingArgs = serde_json::from_value(serde_json::json!({
404+ "actor": { "id": "usr_1", "username": "a" },
405+ "repo": { "namespace": "acme", "name": "web" },
406+ "kind": "secret",
407+ "name": "STRIPE_KEY",
408+ "availableTo": ["deployments"],
409+ "environments": ["production"],
410+ }))
411+ .unwrap();
412+ assert_eq!(args.available_to, Some(vec!["deployments".to_owned()]));
413+ }
414+}
+2−0
558558 }
559559
560560 async job(id: string, step: string, body: Record<string, unknown>): Promise<Response> {
561+ // Each report, for the logs: a build's own failure says why.
562+ console.log("build", id, step, typeof body.message === "string" ? body.message.slice(0, 500) : "");
561563 const row = await this.building(id, body.token);
562564 if (!row) return Response.json(fail("not_found", "No such build, or it has finished."), { status: 404 });
563565 const cloudflare = this.cloudflare;
+2−1
150150 await this.start({ envVars, enableInternet: true });
151151 }
152152
153− override async onStop({ exitCode }: StopParams): Promise<void> {
153+ override async onStop({ exitCode, reason }: StopParams): Promise<void> {
154154 if (exitCode === 0) return;
155155 const run = await this.ctx.storage.get<Run>("run");
156+ console.log("sandbox stopped", run?.kind, "exit", exitCode, reason);
156157 if (!run) return;
157158 if (run.kind === "actions") {
158159 // Refused harmlessly if the job reported its end before it stopped.