Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.DS_Store
node_modules/
*/.workforce/build/
.claude/
33 changes: 5 additions & 28 deletions granola/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,8 @@
* → ask the model "is this a prospect call, and what did they ask for?"
* → if yes: file a Linear issue, then have the coding agent open a PR for it
*/
import {
defineAgent,
draftFile,
encodeSegment,
resolveMountRoot,
writeJsonFile,
type IntegrationClientOptions,
type WorkforceCtx
} from '@agentworkforce/runtime';

function vfsClient(): IntegrationClientOptions {
return { relayfileMountRoot: resolveMountRoot({}) };
}
import { defineAgent, type WorkforceCtx } from '@agentworkforce/runtime';
import { relayClient } from '@relayfile/relay-helpers';

interface Ask {
isProspect: boolean;
Expand All @@ -44,14 +33,8 @@ export default defineAgent({
}

const teamId = await resolveTeamId(ctx);
const client = vfsClient();
const created = await writeJsonFile(
client,
'linear',
'createIssue',
`/linear/issues/${draftFile('create issue')}`,
{ teamId, title: ask.title, description: ask.summary }
);
const linear = relayClient('linear');
const created = await linear.write('issues', {}, { teamId, title: ask.title, description: ask.summary });
// The writeback worker returns a receipt carrying the real issue URL/id once
// the Linear create lands. Without a receipt we can't link the issue or
// address a follow-up comment, so log and continue with the implementation.
Expand All @@ -72,13 +55,7 @@ export default defineAgent({

const prUrl = run.output.match(/https?:\/\/\S*\/pull\/\d+/g)?.pop();
if (prUrl && issueId) {
await writeJsonFile(
client,
'linear',
'comment',
`/linear/issues/${encodeSegment(issueId)}/comments/${draftFile('comment')}`,
{ body: `:rocket: Implementation PR: ${prUrl}` }
);
await linear.write('comments', { issueId }, { body: `:rocket: Implementation PR: ${prUrl}` });
} else if (prUrl) {
ctx.log('warn', 'granola-prospect.comment-skipped.no-issue-id', { prUrl, draftPath: created.path });
}
Expand Down
23 changes: 3 additions & 20 deletions hn-monitor/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,8 @@
* → summarize with ctx.llm
* → post to Slack
*/
import {
defineAgent,
draftFile,
encodeSegment,
resolveMountRoot,
writeJsonFile,
type IntegrationClientOptions,
type WorkforceCtx
} from '@agentworkforce/runtime';

function vfsClient(): IntegrationClientOptions {
return { relayfileMountRoot: resolveMountRoot({}) };
}
import { defineAgent, type WorkforceCtx } from '@agentworkforce/runtime';
import { slackClient } from '@relayfile/relay-helpers';

interface Story {
id: number;
Expand Down Expand Up @@ -48,13 +37,7 @@ export default defineAgent({
return;
}

await writeJsonFile(
vfsClient(),
'slack',
'post',
`/slack/channels/${encodeSegment(channel)}/messages/${draftFile('message')}`,
{ text: await summarize(ctx, fresh) }
);
await slackClient().post(channel, await summarize(ctx, fresh));
await saveSeen(ctx, [...seen, ...fresh.map((s) => s.id)].slice(-200));
}
});
Expand Down
40 changes: 9 additions & 31 deletions linear/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,8 @@
* The repo is already in the sandbox: the cloud materializes the github
* integration's repo into ctx.sandbox.cwd via relayfile, so there's no clone.
*/
import {
defineAgent,
draftFile,
encodeSegment,
readJsonFile,
resolveMountRoot,
writeJsonFile,
type IntegrationClientOptions,
type WorkforceCtx
} from '@agentworkforce/runtime';
import { defineAgent, type WorkforceCtx } from '@agentworkforce/runtime';
import { linearClient } from '@relayfile/relay-helpers';

interface LinearIssue {
id?: string;
Expand All @@ -29,10 +21,6 @@ interface LinearIssue {
[key: string]: unknown;
}

function vfsClient(): IntegrationClientOptions {
return { relayfileMountRoot: resolveMountRoot({}) };
}

export default defineAgent({
// Two Linear triggers — `on` autocompletes Linear's catalog events.
triggers: {
Expand All @@ -44,7 +32,7 @@ export default defineAgent({
handler: async (ctx, event) => {
if (event.source !== 'linear') return;

const client = vfsClient();
const linear = linearClient();

// The comment path only fires when someone @-mentions the agent (configurable
// via MENTION, e.g. "@agentrelay") — and never on the agent's own reply.
Expand All @@ -54,12 +42,7 @@ export default defineAgent({

const issueId = readIssueId(event.payload);
if (!issueId) return;
const issue = await readJsonFile<LinearIssue>(
client,
'linear',
'getIssue',
`/linear/issues/${encodeSegment(issueId)}.json`
);
const issue = await linear.getIssue<LinearIssue>(issueId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To prevent potential runtime crashes, we should defensively check if the issue was successfully retrieved before passing it to parseRepo. If the issue is not found or the API call returns undefined/null, calling parseRepo(issue) will throw a TypeError when attempting to access issue.title.

Suggested change
const issue = await linear.getIssue<LinearIssue>(issueId);
const issue = await linear.getIssue<LinearIssue>(issueId);
if (!issue) {
ctx.log('warn', 'linear-implementer.issue-not-found', { issueId });
return;
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declining: linearClient().getIssue<LinearIssue>(issueId) returns a non-nullable LinearIssue — on a missing/unreadable file it throws (readJsonFileRelayfileWritebackError), it never resolves to undefined/null. So if (!issue) is dead code, and the behavior is unchanged from the pre-migration readJsonFile<LinearIssue>(...) which threw the same way.


// The issue may name its own target repo (a github URL); if so, tell the agent
// to work there — otherwise it uses the materialized repo.
Expand All @@ -78,16 +61,11 @@ export default defineAgent({
});

const prUrl = findPrUrl(run.output);
await writeJsonFile(
client,
'linear',
'comment',
`/linear/issues/${encodeSegment(issueId)}/comments/${draftFile('comment')}`,
{
body: prUrl
? `:rocket: Opened a PR: ${prUrl}`
: "I worked on this but couldn't open a PR — check the run logs."
}
await linear.comment(
issueId,
prUrl
? `:rocket: Opened a PR: ${prUrl}`
: "I worked on this but couldn't open a PR — check the run logs."
);
}
});
Expand Down
19 changes: 15 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
},
"dependencies": {
"@agentworkforce/persona-kit": "^3.0.39",
"@agentworkforce/runtime": "^3.0.39"
"@agentworkforce/runtime": "^3.0.39",
"@relayfile/relay-helpers": "^0.3.29"
},
"devDependencies": {
"@types/node": "^22",
Expand Down
17 changes: 3 additions & 14 deletions repo-hygiene/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
type WorkforceCtx,
type WorkforceProviderEvent
} from '@agentworkforce/runtime';
import { githubClient, slackClient } from '@relayfile/relay-helpers';

function vfsClient(): IntegrationClientOptions {
return { relayfileMountRoot: resolveMountRoot({}) };
Expand Down Expand Up @@ -84,13 +85,7 @@ export default defineAgent({
const report = await diagnose(ctx, pr, details.diff ?? '');
const body = renderPrComment(pr, report);

await writeJsonFile(
client,
'github',
'comment',
`/github/repos/${encodeSegment(pr.owner)}/${encodeSegment(pr.repo)}/issues/${pr.number}/comments/${draftFile('comment')}`,
{ body }
);
await githubClient().comment({ owner: pr.owner, repo: pr.repo, number: pr.number }, body);
let notionUrl: string | undefined;
try {
const notionPage = await writeNotionJournal(ctx, client, pr, event, report, body);
Expand All @@ -102,13 +97,7 @@ export default defineAgent({

const channel = input(ctx, 'SLACK_CHANNEL');
if (channel) {
await writeJsonFile(
client,
'slack',
'post',
`/slack/channels/${encodeSegment(channel)}/messages/${draftFile('message')}`,
{ text: renderSlackSummary(pr, report, notionUrl) }
);
await slackClient().post(channel, renderSlackSummary(pr, report, notionUrl));
}
}
});
Expand Down
59 changes: 19 additions & 40 deletions review/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,9 @@
*/
import {
defineAgent,
draftFile,
encodeSegment,
resolveMountRoot,
writeJsonFile,
type IntegrationClientOptions,
type WorkforceCtx
} from '@agentworkforce/runtime';
import { githubClient, slackClient } from '@relayfile/relay-helpers';

interface Pr {
owner: string;
Expand All @@ -29,20 +25,6 @@ interface Pr {
headSha?: string;
}

function vfsClient(): IntegrationClientOptions {
return { relayfileMountRoot: resolveMountRoot({}) };
}

/** Draft path for a PR-level comment (PRs are issues on the github side). */
function prCommentPath(pr: Pr): string {
return `/github/repos/${encodeSegment(pr.owner)}/${encodeSegment(pr.repo)}/issues/${pr.number}/comments/${draftFile('comment')}`;
}

/** Slack channel message draft path. */
function slackPostPath(channel: string): string {
return `/slack/channels/${encodeSegment(channel)}/messages/${draftFile('message')}`;
}

export default defineAgent({
// Re-review on every PR change (open, new commits, review comments, finished
// CI), and merge when you approve. Every `on` value autocompletes from
Expand Down Expand Up @@ -112,19 +94,19 @@ async function reviewAndFix(ctx: WorkforceCtx, pr: Pr): Promise<void> {
if (!body) {
await failReviewRun(ctx, pr, 'The review harness produced no review output.');
}
const client = vfsClient();
if (body) {
await writeJsonFile(client, 'github', 'comment', prCommentPath(pr), { body });
await githubClient().comment({ owner: pr.owner, repo: pr.repo, number: pr.number }, body);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Guard this outbound comment write with idempotency/deduplication. On handler redelivery, this path can post duplicate PR comments.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At review/agent.ts, line 98:

<comment>Guard this outbound comment write with idempotency/deduplication. On handler redelivery, this path can post duplicate PR comments.</comment>

<file context>
@@ -112,19 +94,19 @@ async function reviewAndFix(ctx: WorkforceCtx, pr: Pr): Promise<void> {
-  const client = vfsClient();
   if (body) {
-    await writeJsonFile(client, 'github', 'comment', prCommentPath(pr), { body });
+    await githubClient().comment({ owner: pr.owner, repo: pr.repo, number: pr.number }, body);
   }
 
</file context>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged, but deferring out of this PR: this is a mechanical migration (same writes, just via relay-helpers) — the duplicate-on-redelivery behavior is pre-existing, not introduced here. Also the runtime dedupes redeliveries on event.id (WorkforceEventBase.id — "the runtime dedupes on this"), so the handler shouldn't re-run for the same event. Full write-idempotency (stable keys / querying existing comments) is a heavy lift and relay-helpers/the writeback worker don't expose idempotency headers today — better tracked as its own issue than bundled into the migration.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback.

}
Comment on lines +98 to 99

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

Does @relayfile/relay-helpers githubClient/slackClient provide idempotency keys or dedupe for comment/post calls?

💡 Result:

No evidence found that @relayfile/relay-helpers’ githubClient/slackClient supports idempotency keys or request deduplication for comment/post (write) calls. In the searches performed, nothing in the relay-helpers GitHub code references idempotency-key headers, idempotency-key parameters, or any dedupe mechanism for comment/post operations; results only surfaced unrelated “relay” projects and general idempotency-key articles, not the @relayfile/relay-helpers implementation itself [1][2][3]. Therefore, based on the sources retrieved, the correct conclusion is that idempotency/dedupe is not provided (or at least not discoverable/documented) in githubClient/slackClient for comment/post calls. If you share the relay-helpers repo link (or the specific file paths for githubClient/slackClient), I can re-run a more targeted source-code search for “idempotency”, “dedupe”, “Idempotency-Key”, or “X-Request-ID” within those exact implementations.

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Repo file tree (focus) =="
ls -R

echo "== Find references to relay-helpers / githubClient / slackClient =="
rg -n "relay-helpers|githubClient\(|slackClient\(|failReviewRun|post\(|comment\(" -S .

echo "== Show review/agent.ts around referenced lines =="
sed -n '70,180p' review/agent.ts

echo "== Locate package.json/lock to identify where `@relayfile/relay-helpers` comes from =="
ls
fd -a "package.json" .
fd -a "package-lock.json" .
fd -a "pnpm-lock.yaml" .
fd -a "yarn.lock" .

Repository: AgentWorkforce/agents

Length of output: 8940


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check if node_modules exists (so we can inspect actual relay-helpers code)
if [ -d "node_modules/@relayfile/relay-helpers" ]; then
  echo "Found local node_modules/@relayfile/relay-helpers"
  ls -R node_modules/@relayfile/relay-helpers | head -n 200
  echo "== Search for idempotency/dedupe markers in relay-helpers =="
  rg -n "Idempotency|idempotency|dedupe|dedup|X-Request-ID|requestId|retry|deduplication" node_modules/@relayfile/relay-helpers -S || true
  echo "== Search for GitHub comment/post call wrappers =="
  rg -n "comment\\(|slackClient\\(\\)|post\\(|mergePullRequest\\(" node_modules/@relayfile/relay-helpers -S || true
else
  echo "node_modules/@relayfile/relay-helpers not present in this checkout."
fi

echo "== Search for retry/at-least-once hints in review harness entrypoints =="
rg -n "retry|retries|redeliver|at-?least-?once|idempotent|dedup" review/agent.ts . -S || true

echo "== Show any local docs about idempotency for relay-helpers in repo =="
rg -n "`@relayfile/relay-helpers`|relay-helpers" -S README.md review/README.md . || true

Repository: AgentWorkforce/agents

Length of output: 1197


🌐 Web query:

@relayfile/relay-helpers githubClient slackClient source githubClient().comment slackClient().post idempotency dedupe

💡 Result:

I couldn’t find any public, authoritative evidence for the specific internal chain you referenced: githubClient().comment slackClient().post idempotency dedupe in @relayfile/relay-helpers. What I did find from the public Relayfile ecosystem sources is: 1) Relayfile has a JavaScript SDK and a client (RelayFileClient) and the SDK “adds X-Correlation-Id automatically for API calls” (useful for tracing/replay debugging, but this is not the same as idempotent write deduplication). [1] 2) Relayfile adapters exist for GitHub and Slack (so there are likely code paths where GitHub comments and Slack posts happen), but the sources surfaced in search results didn’t include the @relayfile/relay-helpers implementation details about how dedupe/idempotency is handled for GitHub comment creation or Slack message posting. [2] 3) Some other open-source codebases that integrate with Slack/GitHub implement message/event deduplication explicitly (TTL caches keyed by message/event id), showing the common approach when providers/clients can retry deliveries (e.g., a TTL-based deduplicator). This indicates the pattern you probably want, but it is not proof of Relayfile’s behavior. [3] If you can share the exact GitHub URL (or paste the relevant code) for @relayfile/relay-helpers and where githubClient() / slackClient() are defined, I can re-research specifically for how dedupe keys are generated and where they are applied (e.g., per-post hash, per-operation idempotency keys, or provider-side headers).

Citations:


🌐 Web query:

site:github.com "@relayfile/relay-helpers" githubClient comment implementation

💡 Result:

I couldn’t find any authoritative public source that shows the actual “@relayfile/relay-helpers githubClient comment” implementation (i.e., the code for how @relayfile/relay-helpers uses a GitHub client to create/update PR comments). What I was able to verify from public GitHub sources is that the relayfile ecosystem uses adapters/providers to write back to GitHub comments via an adapter path like /github/repos///pulls/<pull_number>/comments, and that the adapter consumes a provider for auth. Example usage pattern from the relayfile providers repo: - It constructs a RelayFileClient with a Relayfile token. - It uses a GitHubAdapter from @relayfile/adapter-github. - It calls adapter.writeback(...) with a path pointing to a PR comments endpoint and a payload like { body: "Looks good!" }. This demonstrates where a “comment implementation” would be invoked, but not the internal code inside @relayfile/relay-helpers itself. [1] If you can share the specific function/class name inside @relayfile/relay-helpers (or a link to the repository/package source), I can re-run targeted searches for that exact file and produce a fully code-grounded answer for the githubClient comment implementation.

Citations:


🌐 Web query:

site:npmjs.com "@relayfile/relay-helpers" idempotency slackClient

💡 Result:

I couldn’t find any authoritative or directly relevant information about the package name “@relayfile/relay-helpers” (including any mention of “idempotency” or “slackClient”). The web results I found around “relay-helpers” appear to be unrelated packages (e.g., Relay GraphQL helpers/network-layer, or other “relay” libraries), not “@relayfile/relay-helpers”. So, I can’t confidently answer how (or whether) “@relayfile/relay-helpers” implements idempotency for a “slackClient” from evidence. If you paste the repository link (GitHub URL) or the relevant file path(s) (e.g., where slackClient/idempotency are implemented), I can re-run the research specifically on that code/docs and give a grounded answer.

Citations:


Make GitHub/Slack write operations idempotent under handler retries.

review/agent.ts performs unconditional direct write calls; if the platform redelivers/retries the event, these calls will be re-issued, with no dedupe/idempotency guard at this layer.

  • Success path: githubClient().comment(...) (lines 98-99) and slackClient().post(...) (lines 104-109).
  • Failure path: failReviewRun() always re-posts both the GitHub comment and Slack warning (lines 125-132) before throwing.
  • Merge path: mergePr() always posts the Slack “Merged PR” message (line 151).

No accessible evidence shows @relayfile/relay-helpers githubClient() / slackClient() adding Idempotency-Key/request deduplication for comment/post operations.

await githubClient().comment({ owner: pr.owner, repo: pr.repo, number: pr.number }, body);

Require idempotent behavior (e.g., provider/client-side idempotency keys + retry-safe semantics, or local/per-PR-per-run dedupe) or ensure these handler paths cannot be retried/redelivered.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@review/agent.ts` around lines 98 - 99, Ensure GitHub and Slack writes are
idempotent by adding a dedupe guard before calling githubClient().comment(...)
and slackClient().post(...): generate a stable idempotency key per PR/run (e.g.,
using pr.owner, pr.repo, pr.number and the review run id) and either pass it to
the client if it supports idempotency headers or check-and-skip by querying
existing PR comments / Slack messages for that key; apply the same guard in
failReviewRun() and mergePr() so they first verify whether the comment/post with
that idempotency key already exists before posting to avoid duplicate writes on
retries.


const channel = input(ctx, 'SLACK_CHANNEL');
if (channel) {
const who = `<https://github.com/${pr.author}|@${pr.author}>`; // the PR opener
await writeJsonFile(client, 'slack', 'post', slackPostPath(channel), {
text: ready
await slackClient().post(
channel,
ready
? `:white_check_mark: ${who} — PR #${pr.number} in *${pr.owner}/${pr.repo}* is ready for your review: ${pr.url}`
: `:eyes: ${who} — reviewing PR #${pr.number} in *${pr.owner}/${pr.repo}*, still working on it: ${pr.url}`
});
);
}
}

Expand All @@ -140,36 +122,33 @@ async function failReviewRun(ctx: WorkforceCtx, pr: Pr, reason: string): Promise
number: pr.number,
reason,
});
const client = vfsClient();
await writeJsonFile(client, 'github', 'comment', prCommentPath(pr), { body: message });
await githubClient().comment({ owner: pr.owner, repo: pr.repo, number: pr.number }, message);
const channel = input(ctx, 'SLACK_CHANNEL');
if (channel) {
await writeJsonFile(client, 'slack', 'post', slackPostPath(channel), {
text: `:warning: pr-reviewer failed for PR #${pr.number} in *${pr.owner}/${pr.repo}*: ${reason}`
});
await slackClient().post(
channel,
`:warning: pr-reviewer failed for PR #${pr.number} in *${pr.owner}/${pr.repo}*: ${reason}`
);
}
throw new Error(message);
}

async function mergePr(ctx: WorkforceCtx, pr: Pr): Promise<void> {
const client = vfsClient();
const mergePath = `/github/repos/${encodeSegment(pr.owner)}/${encodeSegment(pr.repo)}/pulls/${pr.number}/merge.json`;
const result = await writeJsonFile(client, 'github', 'merge', mergePath, {
const result = await githubClient().mergePullRequest({
owner: pr.owner,
repo: pr.repo,
number: pr.number,
method: 'squash',
...(pr.headSha ? { sha: pr.headSha } : {})
});
// The writeback worker reports the merge outcome on the receipt. A missing
// receipt means fire-and-forget didn't confirm — surface that loudly rather
// than pretend the merge landed.
const merged = result.receipt?.merged;
if (merged !== true && merged !== 'true') {
// mergePullRequest surfaces the writeback worker's merge outcome as `merged`.
// A false/unconfirmed result means we shouldn't pretend the merge landed.
if (!result.merged) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Merge confirmation uses a truthiness check, which can falsely accept non-boolean values like 'false' as a successful merge.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At review/agent.ts, line 146:

<comment>Merge confirmation uses a truthiness check, which can falsely accept non-boolean values like `'false'` as a successful merge.</comment>

<file context>
@@ -140,36 +122,33 @@ async function failReviewRun(ctx: WorkforceCtx, pr: Pr, reason: string): Promise
-  if (merged !== true && merged !== 'true') {
+  // mergePullRequest surfaces the writeback worker's merge outcome as `merged`.
+  // A false/unconfirmed result means we shouldn't pretend the merge landed.
+  if (!result.merged) {
     throw new Error(`GitHub did not confirm PR #${pr.number} in ${pr.owner}/${pr.repo} was merged.`);
   }
</file context>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declining: result.merged here is the normalized boolean returned by githubClient().mergePullRequest(...) (Promise<{ merged: boolean; sha?: string }>), not the raw receipt. The client already collapses receipt.merged (which may be true/'true'/false/'false'/undefined-with-sha) into a real boolean'false'false — so if (!result.merged) correctly rejects a failed merge. The string case the old code guarded (merged !== true && merged !== 'true') is handled inside the client now.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback! I've saved this as a new learning to improve future reviews.

throw new Error(`GitHub did not confirm PR #${pr.number} in ${pr.owner}/${pr.repo} was merged.`);
}
const channel = input(ctx, 'SLACK_CHANNEL');
if (channel) {
await writeJsonFile(client, 'slack', 'post', slackPostPath(channel), {
text: `:tada: Merged PR #${pr.number} in ${pr.owner}/${pr.repo}.`
});
await slackClient().post(channel, `:tada: Merged PR #${pr.number} in ${pr.owner}/${pr.repo}.`);
}
}

Expand Down
Loading