Events: review requests, assignments, stops and deployments are published
Work publishes pull.assigned, pull.review_requested and pull.review_request_removed from update_pull, issue.assigned now says who was newly added, pull.stalled the first time g1t stops on a pull request (with why, and who stopped it when a person did), and pull.resumed when it picks back up. Deployments publishes deployment.succeeded and deployment.failed for every finished build, saying whether a success follows a failure; it gets a service binding to g1t-events for that. Webhooks can be sent all seven, and workflows see the pull_request ones as assigned, review_requested and review_request_removed.
15 files+254−70/15 viewed
| 75 | 75 | | `repo.collaborator_added`, `repo.collaborator_role_changed`, `repo.collaborator_removed` | Someone was given a role on it, had their role changed, or lost it. `data.username`, `data.role`, `data.previous_role`. See [access and roles](/guides/access-and-roles/). | | |
| 76 | 76 | | `repo.archived`, `repo.unarchived` | It was made read-only, or writable again. | | |
| 77 | 77 | | `repo.deleted`, `repo.restored`, `repo.purged` | It was deleted, restored within its 30 days, or removed for good. | | |
| 78 | − | | `issue.opened`, `issue.updated`, `issue.assigned`, `issue.closed`, `issue.reopened` | An issue changed. `data.number` and `data.author` (`id` and `username`); on close, `data.reason` and `data.resolved_by`. For an issue g1t's agent filed while at work, `data.author` is g1t and `data.requested_by` is the person it was working for. | | |
| 78 | + | | `issue.opened`, `issue.updated`, `issue.assigned`, `issue.closed`, `issue.reopened` | An issue changed. `data.number` and `data.author` (`id` and `username`); on close, `data.reason` and `data.resolved_by`; on assignment, `data.assignees` and the newly assigned `data.added`. For an issue g1t's agent filed while at work, `data.author` is g1t and `data.requested_by` is the person it was working for. | | |
| 79 | 79 | | `comment.created` | A comment or review on an issue or pull request. | | |
| 80 | 80 | | `pull.opened`, `pull.ready`, `pull.updated`, `pull.merge_requested`, `pull.merged`, `pull.closed` | A pull request changed. `data.number`, `data.issue` and `data.author` (`id` and `username`); on merge, `data.commit`. For a change g1t made, `data.author` is g1t and `data.requested_by` is the person who asked for it; `actor` is still whoever caused the event. On a change by g1t, once g1t has worked it out, `data.confidence`: `level` (`high`, `medium` or `low`), `reasons`, `self_reported`, `uncertain_about`, `run_id` and `assessed_at`. See [how sure the agent is](/guides/working-with-g1t/#how-sure-the-agent-is). | | |
| 81 | + | | `pull.assigned` | People were assigned to a pull request. `data.assignees` is everyone assigned now, `data.added` those newly assigned. | | |
| 82 | + | | `pull.review_requested`, `pull.review_request_removed` | Reviewers were asked for a pull request, or no longer are. `data.reviewers` names them. | | |
| 83 | + | | `pull.stalled`, `pull.resumed` | g1t stopped seeing a pull request through until a person steps in, with why in `data.detail`; or it picked back up. | | |
| 81 | 84 | | `checks.completed` | A pull request's checks finished: every status on its head has reported and none is still pending, or the merge queue took it out. `data.number`, `data.commit`, and `data.status`, `passed` or `failed`. | | |
| 82 | 85 | | `review.completed` | g1t reviewed a pull request. `data.verdict`. | | |
| 83 | 86 | | `workflow.completed` | A [workflow](/guides/actions/) run finished. `data.workflow`, `data.conclusion`, `data.run_id`, `data.sha`, `data.pull`. | | |
| 87 | + | | `deployment.succeeded`, `deployment.failed` | A build of a [project](/guides/deployments/) finished, for production or a pull request's preview. `data.deployment_id`, `data.project`, `data.kind` (`production` or `preview`), `data.number` for a preview, `data.commit`, `data.path`, `data.error` on failure, and `data.recovered` when a success follows a failure. | | |
| 84 | 88 | | `queue.changed` | The merge queue gained, lost or settled an entry. | | |
| 85 | 89 | | `session.appended` | An agent's session grew. Busy: choose it only if you need it. | | |
| 86 | 90 | | `agent.asked` | An agent asked the agent on another pull request a question, or handed it work, while that one was not at work; g1t wakes it to answer. | |
| 15 | 15 | "pull.updated" => pull("synchronize"), | |
| 16 | 16 | "pull.ready" => pull("ready_for_review"), | |
| 17 | 17 | "pull.closed" | "pull.merged" => pull("closed"), | |
| 18 | + | "pull.assigned" => pull("assigned"), | |
| 19 | + | "pull.review_requested" => pull("review_requested"), | |
| 20 | + | "pull.review_request_removed" => pull("review_request_removed"), | |
| 18 | 21 | "issue.opened" => vec![("issues", Some("opened"))], | |
| 19 | 22 | "issue.updated" => vec![("issues", Some("edited"))], | |
| 20 | 23 | "issue.closed" => vec![("issues", Some("closed"))], |
| 96 | 96 | /// On `issue.assigned`: the people it is now assigned to. | |
| 97 | 97 | #[serde(skip_serializing_if = "Option::is_none")] | |
| 98 | 98 | pub assignees: Option<Vec<String>>, | |
| 99 | + | /// On `issue.assigned`: those of them who were not before. | |
| 100 | + | #[serde(skip_serializing_if = "Option::is_none")] | |
| 101 | + | pub added: Option<Vec<String>>, | |
| 99 | 102 | } | |
| 100 | 103 | ||
| 101 | 104 | /// The payload of `pull.opened`, `pull.ready`, `pull.updated` (its head | |
| 102 | − | /// moved), `pull.closed` and `pull.merged`; each uses the fields that apply to it. | |
| 105 | + | /// moved), `pull.closed`, `pull.merged`, `pull.assigned`, | |
| 106 | + | /// `pull.review_requested` and `pull.review_request_removed` (reviewers | |
| 107 | + | /// asked, or no longer), `pull.stalled` (g1t stopped seeing it through | |
| 108 | + | /// until a person steps in) and `pull.resumed` (it picked back up); each | |
| 109 | + | /// uses the fields that apply to it. | |
| 103 | 110 | #[derive(Debug, Default, Serialize)] | |
| 104 | 111 | #[serde(rename_all = "camelCase")] | |
| 105 | 112 | pub struct PullEvent { | |
| ⋯ | |||
| 127 | 134 | /// How sure g1t is of a g1t agent's change, once it has worked that out. | |
| 128 | 135 | #[serde(skip_serializing_if = "Option::is_none")] | |
| 129 | 136 | pub confidence: Option<crate::work::Confidence>, | |
| 137 | + | /// On `pull.assigned`: the people it is now assigned to. | |
| 138 | + | #[serde(skip_serializing_if = "Option::is_none")] | |
| 139 | + | pub assignees: Option<Vec<String>>, | |
| 140 | + | /// On `pull.assigned`: those newly assigned. | |
| 141 | + | #[serde(skip_serializing_if = "Option::is_none")] | |
| 142 | + | pub added: Option<Vec<String>>, | |
| 143 | + | /// On `pull.review_requested`: the reviewers newly asked; on | |
| 144 | + | /// `pull.review_request_removed`, those no longer asked. | |
| 145 | + | #[serde(skip_serializing_if = "Option::is_none")] | |
| 146 | + | pub reviewers: Option<Vec<String>>, | |
| 147 | + | /// On `pull.stalled`: why g1t stopped, and what would start it again. | |
| 148 | + | #[serde(skip_serializing_if = "Option::is_none")] | |
| 149 | + | pub detail: Option<String>, | |
| 150 | + | } | |
| 151 | + | ||
| 152 | + | /// `deployment.succeeded` and `deployment.failed`: a build of a project | |
| 153 | + | /// finished, for production or for one pull request's preview. | |
| 154 | + | #[derive(Debug, Serialize)] | |
| 155 | + | #[serde(rename_all = "camelCase")] | |
| 156 | + | pub struct DeploymentEvent { | |
| 157 | + | pub deployment_id: String, | |
| 158 | + | pub project_id: String, | |
| 159 | + | pub repo_id: String, | |
| 160 | + | pub workspace: String, | |
| 161 | + | /// The project's slug. | |
| 162 | + | pub project: String, | |
| 163 | + | /// `production` or `preview`. | |
| 164 | + | pub kind: String, | |
| 165 | + | pub branch: Option<String>, | |
| 166 | + | /// For a preview: its pull request. | |
| 167 | + | pub number: Option<u32>, | |
| 168 | + | pub commit: String, | |
| 169 | + | /// Where the deployment is on the site, such as | |
| 170 | + | /// `/acme/rocket/deployments/dpl_1`. | |
| 171 | + | pub path: String, | |
| 172 | + | /// For a failure: what went wrong. | |
| 173 | + | pub error: Option<String>, | |
| 174 | + | /// For a success: whether the deployment before it, of the same app, failed. | |
| 175 | + | pub recovered: bool, | |
| 176 | + | /// Who started it, by username, or `g1t`. | |
| 177 | + | pub triggered_by: String, | |
| 130 | 178 | } | |
| 131 | 179 | ||
| 132 | 180 | /// `checks.completed`: a run of an issue's acceptance checks against a pull | |
| 15 | 15 | use crate::{User, Viewer}; | |
| 16 | 16 | ||
| 17 | 17 | /// Every event a webhook can be sent, in the order people are shown them. | |
| 18 | − | pub const EVENT_TYPES: [&str; 39] = [ | |
| 18 | + | pub const EVENT_TYPES: [&str; 46] = [ | |
| 19 | 19 | "git.push", | |
| 20 | 20 | "branch.renamed", | |
| 21 | 21 | "repo.created", | |
| ⋯ | |||
| 45 | 45 | "pull.merge_requested", | |
| 46 | 46 | "pull.merged", | |
| 47 | 47 | "pull.closed", | |
| 48 | + | "pull.assigned", | |
| 49 | + | "pull.review_requested", | |
| 50 | + | "pull.review_request_removed", | |
| 51 | + | "pull.stalled", | |
| 52 | + | "pull.resumed", | |
| 48 | 53 | "agent.asked", | |
| 49 | 54 | "checks.completed", | |
| 50 | 55 | "review.completed", | |
| 51 | 56 | "workflow.completed", | |
| 57 | + | "deployment.succeeded", | |
| 58 | + | "deployment.failed", | |
| 52 | 59 | "queue.changed", | |
| 53 | 60 | "session.appended", | |
| 54 | 61 | "package.published", | |
| 1136 | 1136 | pub struct StallArgs { | |
| 1137 | 1137 | pub pull_id: String, | |
| 1138 | 1138 | pub reason: String, | |
| 1139 | + | /// The person who stopped it, by id, when someone did: they are not | |
| 1140 | + | /// told it needs them. | |
| 1141 | + | #[serde(default)] | |
| 1142 | + | pub by: Option<String>, | |
| 1139 | 1143 | } | |
| 1140 | 1144 | ||
| 1141 | 1145 | /// `managed_pulls`: ids of the open pull requests g1t is seeing through, |
| 19 | 19 | repoId: string | null; | |
| 20 | 20 | }; | |
| 21 | 21 | ||
| 22 | + | /** What `deployment.succeeded` and `deployment.failed` carry. */ | |
| 23 | + | export type DeploymentEventData = { | |
| 24 | + | deploymentId: string; | |
| 25 | + | projectId: string; | |
| 26 | + | repoId: string; | |
| 27 | + | workspace: string; | |
| 28 | + | project: string; | |
| 29 | + | kind: "production" | "preview"; | |
| 30 | + | branch: string | null; | |
| 31 | + | number: number | null; | |
| 32 | + | commit: string; | |
| 33 | + | path: string; | |
| 34 | + | error: string | null; | |
| 35 | + | recovered: boolean; | |
| 36 | + | /** A username, or `g1t`. */ | |
| 37 | + | triggeredBy: string; | |
| 38 | + | }; | |
| 39 | + | ||
| 22 | 40 | /** The payload of the `repo.collaborator_*` events. */ | |
| 23 | 41 | export type RepoCollaboratorData = { | |
| 24 | 42 | repoId: string; | |
| ⋯ | |||
| 121 | 139 | requestedBy?: { id: string; username: string }; | |
| 122 | 140 | }; | |
| 123 | 141 | "issue.updated": { issueId: string; repoId: string; number: number }; | |
| 124 | − | /** The people an issue is assigned to changed; `assignees` is the new set. */ | |
| 125 | − | "issue.assigned": { issueId: string; repoId: string; number: number; assignees: string[] }; | |
| 142 | + | /** The people an issue is assigned to changed; `assignees` is the new set, `added` those newly assigned. */ | |
| 143 | + | "issue.assigned": { issueId: string; repoId: string; number: number; assignees: string[]; added?: string[] }; | |
| 126 | 144 | /** `resolvedBy` is the number of the pull request whose merge closed it. */ | |
| 127 | 145 | "issue.closed": { | |
| 128 | 146 | issueId: string; | |
| ⋯ | |||
| 153 | 171 | /** A merge was asked for while the pull request was behind; it has to catch up first. */ | |
| 154 | 172 | "pull.merge_requested": { pullId: string; repoId: string; number: number; issue?: number; confidence?: Confidence }; | |
| 155 | 173 | "pull.closed": { pullId: string; repoId: string; number: number; issue?: number; confidence?: Confidence }; | |
| 174 | + | /** People were assigned to a pull request: `assignees` is the new set, `added` those newly assigned. */ | |
| 175 | + | "pull.assigned": { pullId: string; repoId: string; number: number; issue?: number; assignees: string[]; added: string[] }; | |
| 176 | + | /** Reviewers were asked for a pull request (`reviewers`), or no longer are. */ | |
| 177 | + | "pull.review_requested": { pullId: string; repoId: string; number: number; issue?: number; reviewers: string[] }; | |
| 178 | + | "pull.review_request_removed": { pullId: string; repoId: string; number: number; issue?: number; reviewers: string[] }; | |
| 179 | + | /** g1t stopped seeing a pull request through until a person steps in; `detail` says why. */ | |
| 180 | + | "pull.stalled": { pullId: string; repoId: string; number: number; issue?: number; detail: string }; | |
| 181 | + | /** A pull request g1t had stopped on is going again. */ | |
| 182 | + | "pull.resumed": { pullId: string; repoId: string; number: number; issue?: number }; | |
| 156 | 183 | /** | |
| 157 | 184 | * The pull request's head or its target moved and the files both changed | |
| 158 | 185 | * overlap: a sandbox should find out whether it still merges cleanly. | |
| ⋯ | |||
| 197 | 224 | }; | |
| 198 | 225 | "session.appended": { pullId: string; repoId: string; number: number; count: number }; | |
| 199 | 226 | /** | |
| 227 | + | * A build of a project finished, for production or one pull request's | |
| 228 | + | * preview (`number`). `path` is the deployment's page on the site; | |
| 229 | + | * `recovered`, on a success, says the build before it failed. | |
| 230 | + | */ | |
| 231 | + | "deployment.succeeded": DeploymentEventData; | |
| 232 | + | "deployment.failed": DeploymentEventData; | |
| 233 | + | /** | |
| 200 | 234 | * A package version was published, such as an image pushed by | |
| 201 | 235 | * `docker push`. `tags` are the tags that now point to it; `repoId` (and | |
| 202 | 236 | * the event's) is the repository the package is linked to, if any. | |
| 38 | 38 | "pull.merge_requested", | |
| 39 | 39 | "pull.merged", | |
| 40 | 40 | "pull.closed", | |
| 41 | + | "pull.assigned", | |
| 42 | + | "pull.review_requested", | |
| 43 | + | "pull.review_request_removed", | |
| 44 | + | "pull.stalled", | |
| 45 | + | "pull.resumed", | |
| 41 | 46 | "agent.asked", | |
| 42 | 47 | "checks.completed", | |
| 43 | 48 | "review.completed", | |
| 44 | 49 | "workflow.completed", | |
| 50 | + | "deployment.succeeded", | |
| 51 | + | "deployment.failed", | |
| 45 | 52 | "queue.changed", | |
| 46 | 53 | "session.appended", | |
| 47 | 54 | "package.published", |
| 40 | 40 | sandboxEstimateMicros, | |
| 41 | 41 | currentMovedPath, | |
| 42 | 42 | currentWorkspaceSlug, | |
| 43 | + | eventsClient, | |
| 43 | 44 | fail, | |
| 44 | 45 | identityClient, | |
| 45 | 46 | isProtectedWorkspace, | |
| ⋯ | |||
| 92 | 93 | PROJECTS: ServiceBinding; | |
| 93 | 94 | /** Secrets and variables: the actions service holds the one store. */ | |
| 94 | 95 | ACTIONS: ServiceBinding; | |
| 96 | + | /** The bus: each build that finishes is published, for the inbox, webhooks and workflows. */ | |
| 97 | + | EVENTS?: ServiceBinding; | |
| 95 | 98 | /** Secret: scoped to Workers scripts and analytics on g1t's account. */ | |
| 96 | 99 | CLOUDFLARE_API_TOKEN?: string; | |
| 97 | 100 | CLOUDFLARE_ACCOUNT_ID: string; | |
| ⋯ | |||
| 1057 | 1060 | await this.chargeBuild(row, seconds); | |
| 1058 | 1061 | await this.notePeak(row.workspace); | |
| 1059 | 1062 | await this.statusFor(row, "success", row.kind === "preview" ? "Preview is live" : "Production is live", appUrl(row.script)); | |
| 1063 | + | await this.announce(row, null); | |
| 1060 | 1064 | // A screenshot of production as it now is, for the project's overview. | |
| 1061 | 1065 | if (row.kind === "production") { | |
| 1062 | 1066 | await this.env.SCREENSHOTS?.capture({ host: appHost(row.script), commit: row.commit_sha }).catch((error) => | |
| ⋯ | |||
| 1137 | 1141 | // A failed build still used its sandbox. | |
| 1138 | 1142 | if (seconds) await this.chargeBuild(row, seconds); | |
| 1139 | 1143 | await this.statusFor(row, "failure", "Deployment failed", `${this.env.SITE}/${row.workspace}/${row.slug}/deployments/${id}`); | |
| 1144 | + | await this.announce(row, message.slice(0, 300)); | |
| 1145 | + | } | |
| 1146 | + | ||
| 1147 | + | /** | |
| 1148 | + | * Publishes a finished build: `deployment.failed` with what went wrong, | |
| 1149 | + | * or `deployment.succeeded`, saying whether the build of the same app | |
| 1150 | + | * before it failed. Whoever started it is the actor when it was a | |
| 1151 | + | * person known by id; otherwise `triggeredBy` names them, or g1t. | |
| 1152 | + | */ | |
| 1153 | + | private async announce(row: DeploymentRow, error: string | null): Promise<void> { | |
| 1154 | + | if (!this.env.EVENTS) return; | |
| 1155 | + | const previous = error | |
| 1156 | + | ? null | |
| 1157 | + | : await this.db | |
| 1158 | + | .prepare( | |
| 1159 | + | `SELECT status FROM deployments | |
| 1160 | + | WHERE script = ? AND id != ? AND created_at < ? AND status IN ('ready', 'replaced', 'down', 'failed') | |
| 1161 | + | ORDER BY created_at DESC LIMIT 1`, | |
| 1162 | + | ) | |
| 1163 | + | .bind(row.script, row.id, row.created_at) | |
| 1164 | + | .first<{ status: string }>(); | |
| 1165 | + | const byId = row.created_by.startsWith("usr_"); | |
| 1166 | + | const event = { | |
| 1167 | + | source: "deployments", | |
| 1168 | + | repoId: row.repo_id, | |
| 1169 | + | actor: byId ? row.created_by : null, | |
| 1170 | + | data: { | |
| 1171 | + | deploymentId: row.id, | |
| 1172 | + | projectId: row.project_id, | |
| 1173 | + | repoId: row.repo_id, | |
| 1174 | + | workspace: row.workspace, | |
| 1175 | + | project: row.slug, | |
| 1176 | + | kind: row.kind, | |
| 1177 | + | branch: row.branch, | |
| 1178 | + | number: row.kind === "preview" ? row.number : null, | |
| 1179 | + | commit: row.commit_sha, | |
| 1180 | + | path: `/${row.workspace}/${row.slug}/deployments/${row.id}`, | |
| 1181 | + | error, | |
| 1182 | + | recovered: previous?.status === "failed", | |
| 1183 | + | triggeredBy: byId ? "" : row.created_by, | |
| 1184 | + | }, | |
| 1185 | + | }; | |
| 1186 | + | await eventsClient(this.env.EVENTS) | |
| 1187 | + | .publish([error == null ? { type: "deployment.succeeded", ...event } : { type: "deployment.failed", ...event }]) | |
| 1188 | + | .catch((reason: unknown) => console.error("deployment not published", row.id, String(reason))); | |
| 1140 | 1189 | } | |
| 1141 | 1190 | ||
| 1142 | 1191 | /** Each build is charged by the second, from the first, at the container price plus the margin. */ | |
| 26 | 26 | // Secrets and variables, with who may read each. | |
| 27 | 27 | { "binding": "ACTIONS", "service": "g1t-actions" }, | |
| 28 | 28 | // Production's screenshot, asked for once per deploy (services/og). | |
| 29 | − | { "binding": "SCREENSHOTS", "service": "g1t-og", "entrypoint": "Screenshots" } | |
| 29 | + | { "binding": "SCREENSHOTS", "service": "g1t-og", "entrypoint": "Screenshots" }, | |
| 30 | + | // Each finished build is published: deployment.succeeded or | |
| 31 | + | // deployment.failed, for the inbox, webhooks and workflows. | |
| 32 | + | { "binding": "EVENTS", "service": "g1t-events" } | |
| 30 | 33 | ], | |
| 31 | 34 | // Pull requests opened, pushed to, closed and merged; pushes to the | |
| 32 | 35 | // default branch. |
| 313 | 313 | self.stall(StallArgs { | |
| 314 | 314 | pull_id: pull_id.to_owned(), | |
| 315 | 315 | reason, | |
| 316 | + | by: None, | |
| 316 | 317 | }) | |
| 317 | 318 | .await?; | |
| 318 | 319 | self.note(repo_id, number, (AGENT_ID, AGENT_NAME), ¬ed).await?; |
| 648 | 648 | ("assigned", "unassigned"), | |
| 649 | 649 | ) | |
| 650 | 650 | .await?; | |
| 651 | + | let added: Vec<String> = assignees.iter().filter(|name| !before.contains(name)).cloned().collect(); | |
| 651 | 652 | self.publish( | |
| 652 | 653 | "issue.assigned", | |
| 653 | 654 | &issue.repo_id, | |
| 654 | 655 | &a.actor, | |
| 655 | 656 | IssueEvent { | |
| 656 | 657 | assignees: Some(assignees), | |
| 658 | + | added: Some(added), | |
| 657 | 659 | ..Self::issue_event(&issue) | |
| 658 | 660 | }, | |
| 659 | 661 | ) | |
| ⋯ | |||
| 1472 | 1474 | ) | |
| 1473 | 1475 | .await?; | |
| 1474 | 1476 | } | |
| 1477 | + | // Who was newly assigned or asked to review, and whose request was | |
| 1478 | + | // withdrawn: the inbox tells them, and webhooks say so. | |
| 1479 | + | let newly = |after: &[String], before: &[String]| -> Vec<String> { | |
| 1480 | + | after.iter().filter(|name| !before.contains(name)).cloned().collect() | |
| 1481 | + | }; | |
| 1482 | + | if let Some(assignees) = &assignees { | |
| 1483 | + | let added = newly(assignees, &pull.assignees); | |
| 1484 | + | if !added.is_empty() { | |
| 1485 | + | self.publish( | |
| 1486 | + | "pull.assigned", | |
| 1487 | + | &pull.repo_id, | |
| 1488 | + | &a.actor, | |
| 1489 | + | PullEvent { | |
| 1490 | + | assignees: Some(assignees.clone()), | |
| 1491 | + | added: Some(added), | |
| 1492 | + | ..Self::pull_event(&pull) | |
| 1493 | + | }, | |
| 1494 | + | ) | |
| 1495 | + | .await?; | |
| 1496 | + | } | |
| 1497 | + | } | |
| 1498 | + | if let Some(reviewers) = &reviewers { | |
| 1499 | + | for (kind, names) in [ | |
| 1500 | + | ("pull.review_requested", newly(reviewers, &pull.reviewers)), | |
| 1501 | + | ("pull.review_request_removed", newly(&pull.reviewers, reviewers)), | |
| 1502 | + | ] { | |
| 1503 | + | if !names.is_empty() { | |
| 1504 | + | self.publish( | |
| 1505 | + | kind, | |
| 1506 | + | &pull.repo_id, | |
| 1507 | + | &a.actor, | |
| 1508 | + | PullEvent { | |
| 1509 | + | reviewers: Some(names), | |
| 1510 | + | ..Self::pull_event(&pull) | |
| 1511 | + | }, | |
| 1512 | + | ) | |
| 1513 | + | .await?; | |
| 1514 | + | } | |
| 1515 | + | } | |
| 1516 | + | } | |
| 1475 | 1517 | Ok(match self.pull(&pull.repo_id, pull.number).await? { | |
| 1476 | 1518 | Some(pull) => Outcome::Ok(pull), | |
| 1477 | 1519 | None => no_pull(), | |
| 1140 | 1140 | self.stall(StallArgs { | |
| 1141 | 1141 | pull_id: pull.id.clone(), | |
| 1142 | 1142 | reason: format!("g1t could not merge this: {}", failure.message), | |
| 1143 | + | by: None, | |
| 1143 | 1144 | }) | |
| 1144 | 1145 | .await?; | |
| 1145 | 1146 | Ok(()) | |
| ⋯ | |||
| 1275 | 1276 | "It was brought up to date but could not be merged: {}", | |
| 1276 | 1277 | failure.message | |
| 1277 | 1278 | ), | |
| 1279 | + | by: None, | |
| 1278 | 1280 | }) | |
| 1279 | 1281 | .await?; | |
| 1280 | 1282 | } | |
| ⋯ | |||
| 1341 | 1343 | }) | |
| 1342 | 1344 | } | |
| 1343 | 1345 | ||
| 1346 | + | /// Stops seeing a pull request through until a person steps in. The | |
| 1347 | + | /// first stop is published (`pull.stalled`), which tells its people | |
| 1348 | + | /// that it needs them; a stop on one already stopped only says why. | |
| 1344 | 1349 | pub(crate) async fn stall(&self, a: StallArgs) -> Result<bool> { | |
| 1350 | + | let before = self.pull_by_id(&a.pull_id).await?; | |
| 1351 | + | let was_stalled = self.is_stalled(&a.pull_id).await?; | |
| 1345 | 1352 | self.db | |
| 1346 | 1353 | .prepare( | |
| 1347 | 1354 | "UPDATE pulls | |
| ⋯ | |||
| 1354 | 1361 | .await?; | |
| 1355 | 1362 | self.db | |
| 1356 | 1363 | .prepare("UPDATE pulls SET stage = 'needs_you', stage_detail = ? WHERE id = ?") | |
| 1357 | − | .bind(&[a.reason.trim().into(), a.pull_id.into()])? | |
| 1364 | + | .bind(&[a.reason.trim().into(), a.pull_id.as_str().into()])? | |
| 1358 | 1365 | .run() | |
| 1359 | 1366 | .await?; | |
| 1367 | + | if let Some(pull) = before.filter(|pull| !was_stalled && pull.status == PullStatus::Open) { | |
| 1368 | + | self.publish_as( | |
| 1369 | + | "pull.stalled", | |
| 1370 | + | &pull.repo_id, | |
| 1371 | + | a.by.clone(), | |
| 1372 | + | g1t_contracts::events::PullEvent { | |
| 1373 | + | detail: Some(a.reason.trim().to_owned()), | |
| 1374 | + | ..Self::pull_event(&pull) | |
| 1375 | + | }, | |
| 1376 | + | ) | |
| 1377 | + | .await?; | |
| 1378 | + | } | |
| 1360 | 1379 | Ok(true) | |
| 1361 | 1380 | } | |
| 1362 | 1381 | ||
| 1382 | + | /// Whether g1t has stopped seeing the pull request through. | |
| 1383 | + | pub(crate) async fn is_stalled(&self, pull_id: &str) -> Result<bool> { | |
| 1384 | + | Ok(self | |
| 1385 | + | .db | |
| 1386 | + | .prepare("SELECT 1 AS value FROM pulls WHERE id = ? AND stalled IS NOT NULL") | |
| 1387 | + | .bind(&[pull_id.into()])? | |
| 1388 | + | .first::<u32>(Some("value")) | |
| 1389 | + | .await? | |
| 1390 | + | .is_some()) | |
| 1391 | + | } | |
| 1392 | + | ||
| 1393 | + | /// Says that a pull request g1t had stopped on is going again, which | |
| 1394 | + | /// closes what it was waiting on a person for. | |
| 1395 | + | pub(crate) async fn announce_resumed(&self, pull_id: &str, actor: Option<String>) -> Result<()> { | |
| 1396 | + | if let Some(pull) = self.pull_by_id(pull_id).await? { | |
| 1397 | + | self.publish_as("pull.resumed", &pull.repo_id, actor, Self::pull_event(&pull)).await?; | |
| 1398 | + | } | |
| 1399 | + | Ok(()) | |
| 1400 | + | } | |
| 1401 | + | ||
| 1363 | 1402 | pub(crate) async fn managed_pulls(&self, a: ManagedPullsArgs) -> Result<Vec<String>> { | |
| 1364 | 1403 | let rows = self | |
| 1365 | 1404 | .db | |
| 567 | 567 | return Ok(Outcome::fail(FailureCode::NotFound, "Pull request not found.")); | |
| 568 | 568 | }; | |
| 569 | 569 | // A person asking outranks a stop and the limit on revisions. | |
| 570 | + | let was_stalled = self.is_stalled(&pull.id).await?; | |
| 570 | 571 | self.db | |
| 571 | 572 | .prepare("UPDATE pulls SET stalled = NULL WHERE id = ?") | |
| 572 | 573 | .bind(&[pull.id.as_str().into()])? | |
| 573 | 574 | .run() | |
| 574 | 575 | .await?; | |
| 576 | + | if was_stalled { | |
| 577 | + | self.announce_resumed(&pull.id, Some(actor.id.clone())).await?; | |
| 578 | + | } | |
| 575 | 579 | if !self.claim(&pull.id, "revision", REVISION_MINUTES, true).await? { | |
| 576 | 580 | return Ok(Outcome::fail( | |
| 577 | 581 | FailureCode::Conflict, |
| 425 | 425 | "{} stopped the agent's {} run. Ask for a review, a revision or a catch-up to start again.", | |
| 426 | 426 | a.actor.username, run.kind | |
| 427 | 427 | ), | |
| 428 | + | by: Some(a.actor.id.clone()), | |
| 428 | 429 | }) | |
| 429 | 430 | .await?; | |
| 430 | 431 | self.note( |
| 261 | 261 | .first::<crate::rows::ValueRow>(None) | |
| 262 | 262 | .await?; | |
| 263 | 263 | if resumed.is_some() { | |
| 264 | + | self.announce_resumed(&head.id, None).await?; | |
| 264 | 265 | self.note( | |
| 265 | 266 | &a.repo_id, | |
| 266 | 267 | head.number, |