Skip to content

Commit 99999a6

Browse files
committed
feat: update existing PR comment on re-push instead of creating new ones
On subsequent pushes to the same PR, the existing deployment comment is now updated with the new commit hash instead of creating a new comment each time. Changes: - Add pr_comments table to track comment IDs by org/repo/pr_number - Add database functions: get_pr_comment, upsert_pr_comment, delete_pr_comment - Update webhook handler to check for existing comment before creating new one - Clean up PR comment tracking when PR is closed
1 parent 60ca721 commit 99999a6

3 files changed

Lines changed: 142 additions & 10 deletions

File tree

migrations/003_pr_comments.sql

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
-- PR comments tracking table
2+
-- Tracks the GitHub comment ID for each PR deployment so we can update
3+
-- the same comment on subsequent pushes instead of creating new ones.
4+
5+
CREATE TABLE IF NOT EXISTS pr_comments (
6+
id SERIAL PRIMARY KEY,
7+
github_org VARCHAR(255) NOT NULL,
8+
github_repo VARCHAR(255) NOT NULL,
9+
pr_number INTEGER NOT NULL,
10+
comment_id BIGINT NOT NULL,
11+
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
12+
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
13+
14+
-- Unique constraint on org/repo/pr_number
15+
CONSTRAINT pr_comments_unique UNIQUE (github_org, github_repo, pr_number)
16+
);
17+
18+
-- Index for lookups
19+
CREATE INDEX IF NOT EXISTS idx_pr_comments_lookup
20+
ON pr_comments(LOWER(github_org), LOWER(github_repo), pr_number);

src/central/db/queries.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,3 +222,76 @@ pub async fn delete_authorized_org(pool: &PgPool, github_org: &str) -> Result<bo
222222

223223
Ok(result.rows_affected() > 0)
224224
}
225+
226+
// ==================== PR Comments ====================
227+
228+
/// Get the existing comment ID for a PR deployment
229+
pub async fn get_pr_comment(
230+
pool: &PgPool,
231+
org: &str,
232+
repo: &str,
233+
pr_number: u32,
234+
) -> Result<Option<i64>> {
235+
let result = sqlx::query_scalar::<_, i64>(
236+
r#"
237+
SELECT comment_id
238+
FROM pr_comments
239+
WHERE LOWER(github_org) = LOWER($1)
240+
AND LOWER(github_repo) = LOWER($2)
241+
AND pr_number = $3
242+
"#,
243+
)
244+
.bind(org)
245+
.bind(repo)
246+
.bind(pr_number as i32)
247+
.fetch_optional(pool)
248+
.await?;
249+
250+
Ok(result)
251+
}
252+
253+
/// Store or update the comment ID for a PR deployment
254+
pub async fn upsert_pr_comment(
255+
pool: &PgPool,
256+
org: &str,
257+
repo: &str,
258+
pr_number: u32,
259+
comment_id: i64,
260+
) -> Result<()> {
261+
sqlx::query(
262+
r#"
263+
INSERT INTO pr_comments (github_org, github_repo, pr_number, comment_id)
264+
VALUES ($1, $2, $3, $4)
265+
ON CONFLICT (github_org, github_repo, pr_number) DO UPDATE SET
266+
comment_id = EXCLUDED.comment_id,
267+
updated_at = NOW()
268+
"#,
269+
)
270+
.bind(org)
271+
.bind(repo)
272+
.bind(pr_number as i32)
273+
.bind(comment_id)
274+
.execute(pool)
275+
.await?;
276+
277+
Ok(())
278+
}
279+
280+
/// Delete PR comment tracking when PR is closed
281+
pub async fn delete_pr_comment(pool: &PgPool, org: &str, repo: &str, pr_number: u32) -> Result<bool> {
282+
let result = sqlx::query(
283+
r#"
284+
DELETE FROM pr_comments
285+
WHERE LOWER(github_org) = LOWER($1)
286+
AND LOWER(github_repo) = LOWER($2)
287+
AND pr_number = $3
288+
"#,
289+
)
290+
.bind(org)
291+
.bind(repo)
292+
.bind(pr_number as i32)
293+
.execute(pool)
294+
.await?;
295+
296+
Ok(result.rows_affected() > 0)
297+
}

src/central/handlers/webhook.rs

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -155,16 +155,45 @@ async fn process_webhook_event(state: &AppState, event: WebhookEvent) -> anyhow:
155155
// Generate job_id
156156
let job_id = Uuid::new_v4();
157157

158-
// Post "Building..." comment
158+
// Create or update the PR comment
159159
let github_client = GitHubClient::new(token.token.clone());
160-
let comment = github_client
161-
.create_pr_comment(
162-
org,
163-
repo,
164-
pr_event.number,
165-
&GitHubClient::building_comment(&pr_event.pull_request.head.sha),
166-
)
167-
.await?;
160+
let comment_id = match db::get_pr_comment(&state.db, org, repo, pr_event.number)
161+
.await?
162+
{
163+
Some(existing_comment_id) => {
164+
// Update existing comment
165+
tracing::debug!(
166+
pr = pr_event.number,
167+
comment_id = existing_comment_id,
168+
"Updating existing PR comment"
169+
);
170+
github_client
171+
.update_comment(
172+
org,
173+
repo,
174+
existing_comment_id,
175+
&GitHubClient::building_comment(&pr_event.pull_request.head.sha),
176+
)
177+
.await?;
178+
existing_comment_id
179+
}
180+
None => {
181+
// Create new comment
182+
tracing::debug!(pr = pr_event.number, "Creating new PR comment");
183+
let comment = github_client
184+
.create_pr_comment(
185+
org,
186+
repo,
187+
pr_event.number,
188+
&GitHubClient::building_comment(&pr_event.pull_request.head.sha),
189+
)
190+
.await?;
191+
// Store the comment ID for future updates
192+
db::upsert_pr_comment(&state.db, org, repo, pr_event.number, comment.id)
193+
.await?;
194+
comment.id
195+
}
196+
};
168197

169198
// Dispatch build job
170199
let job = BuildJob {
@@ -206,7 +235,7 @@ async fn process_webhook_event(state: &AppState, event: WebhookEvent) -> anyhow:
206235
installation_id,
207236
org,
208237
repo,
209-
Some(comment.id),
238+
Some(comment_id),
210239
&pr_event.pull_request.head.sha,
211240
)
212241
.await?;
@@ -231,6 +260,16 @@ async fn process_webhook_event(state: &AppState, event: WebhookEvent) -> anyhow:
231260
)
232261
.await?;
233262

263+
// Clean up the PR comment tracking
264+
if let Err(e) = db::delete_pr_comment(&state.db, org, repo, pr_event.number).await
265+
{
266+
tracing::warn!(
267+
error = %e,
268+
pr = pr_event.number,
269+
"Failed to delete PR comment tracking"
270+
);
271+
}
272+
234273
tracing::info!(
235274
job_id = %job.job_id,
236275
pr = pr_event.number,

0 commit comments

Comments
 (0)