Skip to content
Draft
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
47 changes: 46 additions & 1 deletion apps/api/src/handlers/github/__tests__/isMention.test.ts

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

15 changes: 11 additions & 4 deletions apps/api/src/handlers/github/isMention.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import {
getEffectiveGitHubAppSlug,
isCanonicalGitHubMentionEnabled,
Schemas as GitHubSchemas,
} from '@roomote/github';
import { ROOMOTE_CANONICAL_GITHUB_MENTION } from '@roomote/types';

const escapeRegExp = (value: string) =>
value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
Expand All @@ -14,11 +16,16 @@ export const isMention = (comment: {
return false;
}

// GitHub only treats `@name` as a mention when it stands alone; a bare
// substring check would also fire on longer logins (`@<slug>-fan`) and on
// email addresses (`grace@<slug>.example.com`).
// Deployments answer both their configured app slug and the canonical
// `@roomote` alias that product copy advertises. GitHub only treats `@name`
// as a mention when it stands alone; a bare substring check would also fire
// on longer logins (`@<slug>-fan`) and emails (`grace@<slug>.example.com`).
const handles = new Set([getEffectiveGitHubAppSlug().toLowerCase()]);
if (isCanonicalGitHubMentionEnabled()) {
handles.add(ROOMOTE_CANONICAL_GITHUB_MENTION.slice(1));
}
const mentionPattern = new RegExp(
`(^|[^\\w.-])@${escapeRegExp(getEffectiveGitHubAppSlug())}(?![\\w-])`,
`(^|[^\\w.-])@(?:${[...handles].map(escapeRegExp).join('|')})(?![\\w-])`,
'i',
);

Expand Down
7 changes: 7 additions & 0 deletions apps/docs/providers/source-control/github.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,13 @@ R_GITHUB_APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRI

`R_GITHUB_APP_SLUG` is required when your GitHub App slug is not
`roomote`; Roomote uses it for GitHub mentions and bot-authored PR detection.
Comments trigger Roomote when they mention either your app's own slug or the
canonical `@roomote` alias, and Roomote's PR footers always advertise
`@roomote`, so the copy your users see works on every deployment. If several
Roomote deployments are installed on the same repositories, set
`R_GITHUB_DISABLE_CANONICAL_MENTION=true` on all but one so a bare `@roomote`
does not make every deployment respond; opted-out deployments answer and
advertise only their own app slug.

Generate a private key from the app's **Private keys** section. GitHub
downloads a `.pem` file. Convert it to an escaped single-line value before
Expand Down
2 changes: 1 addition & 1 deletion apps/worker/src/mcp/roomote-mcp-server/about-me.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ If your org has Code Reviewer enabled (it starts disabled by default) and the PR

Linear: Start a task by creating an Agent Session or mentioning me in an issue comment. Same routing and confirmation flow as Slack. Follow-up comments continue the conversation.

GitHub: If your org has the GitHub integration and Code Reviewer enabled (it starts disabled by default), you can @mention me in PR comments to ask for follow-up work on that PR or for another review pass. Those mentions are explicit requests, separate from the automatic review gate that decides which PRs get proactively reviewed. For follow-ups, I push commits directly to the PR branch. Opening a PR or pushing new commits can also trigger an automated code review that posts inline comments and a summary when the PR matches the current review gate.
GitHub: If your org has the GitHub integration and Code Reviewer enabled (it starts disabled by default), you can @mention me (@roomote) in PR comments to ask for follow-up work on that PR or for another review pass. Those mentions are explicit requests, separate from the automatic review gate that decides which PRs get proactively reviewed. For follow-ups, I push commits directly to the PR branch. Opening a PR or pushing new commits can also trigger an automated code review that posts inline comments and a summary when the PR matches the current review gate.

Web Dashboard: You can launch tasks from the dashboard by typing a prompt and selecting a workspace. This is also where you configure environments, integrations, and monitor running tasks.

Expand Down

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

20 changes: 9 additions & 11 deletions packages/cloud-agents/src/server/workflows/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
buildSlackThreadPermalink,
buildTeamsMessagePermalink,
buildTelegramMessagePermalink,
ROOMOTE_CANONICAL_GITHUB_MENTION,
getGitHubAppMention,
resolveTaskWorkspace,
} from '@roomote/types';
Expand All @@ -22,12 +23,14 @@ import {
desc,
asc,
} from '@roomote/db/server';
import { Schemas, getEffectiveGitHubAppSlug } from '@roomote/github';
import {
Schemas,
getEffectiveGitHubAppSlug,
isCanonicalGitHubMentionEnabled,
} from '@roomote/github';
import { buildSlackThreadPromptBlocks } from '../../utils';
import type { ResolvedTaskCommitAuthor } from '../commit-author';

const DEFAULT_R_GITHUB_APP_SLUG = 'roomote';

export function resolveConflictResolverLabel(
conflictResolverLabel?: string,
): string | undefined {
Expand All @@ -51,7 +54,6 @@ export function getPrBodyAttributionLine({
teamsMessageId,
teamsTenantId,
teamsBotAppId,
githubAppSlug = getEffectiveGitHubAppSlug(),
escapeDoubleQuotes = false,
}: {
attribution: ResolvedTaskCommitAuthor;
Expand Down Expand Up @@ -80,7 +82,6 @@ export function getPrBodyAttributionLine({
teamsMessageId?: string;
teamsTenantId?: string;
teamsBotAppId?: string;
githubAppSlug?: string | null;
escapeDoubleQuotes?: boolean;
}) {
if (
Expand Down Expand Up @@ -112,7 +113,6 @@ export function getPrBodyAttributionLine({
teamsMessageId,
teamsTenantId,
teamsBotAppId,
githubAppSlug,
escapeDoubleQuotes,
});
}
Expand All @@ -134,7 +134,6 @@ function buildPrBodyAttributionLine({
teamsMessageId,
teamsTenantId,
teamsBotAppId,
githubAppSlug,
escapeDoubleQuotes = false,
}: {
attribution: ResolvedTaskCommitAuthor;
Expand Down Expand Up @@ -163,7 +162,6 @@ function buildPrBodyAttributionLine({
teamsMessageId?: string;
teamsTenantId?: string;
teamsBotAppId?: string;
githubAppSlug?: string | null;
escapeDoubleQuotes?: boolean;
}) {
const escapeValue = (value: string) =>
Expand All @@ -180,9 +178,9 @@ function buildPrBodyAttributionLine({
const safeSlackConversationUrl = resolvedSlackConversationUrl
? escapeValue(resolvedSlackConversationUrl)
: undefined;
const appMention = getGitHubAppMention(
githubAppSlug?.trim() || DEFAULT_R_GITHUB_APP_SLUG,
);
const appMention = isCanonicalGitHubMentionEnabled()
? ROOMOTE_CANONICAL_GITHUB_MENTION
: getGitHubAppMention(getEffectiveGitHubAppSlug());
const isChatSurface =
taskSurface === 'slack' ||
taskSurface === 'teams' ||
Expand Down
4 changes: 4 additions & 0 deletions packages/env/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ const serverSchema = {
SANDBOX_OIDC_PUBLIC_KEY_SECONDARY: z.string().min(1).optional(),
R_GITHUB_APP_ID: emptyStringDefault(),
R_GITHUB_APP_SLUG: z.string().min(1).default('roomote'),
// Set to `true` to stop this deployment from answering the canonical
// `@roomote` GitHub mention alias (and advertising it in PR footers) —
// for fleets where several deployments share the same repositories.
R_GITHUB_DISABLE_CANONICAL_MENTION: z.string().optional(),
R_GITHUB_APP_PRIVATE_KEY: emptyStringDefault(),
R_GITHUB_CLIENT_ID: emptyStringDefault(),
R_GITHUB_CLIENT_SECRET: emptyStringDefault(),
Expand Down
43 changes: 0 additions & 43 deletions packages/github/src/__tests__/resolve-app-slug.test.ts

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

11 changes: 10 additions & 1 deletion packages/github/src/app-slug.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Env } from '@roomote/env';
import { Env, isEnvFlagEnabled } from '@roomote/env';

/**
* Cached result of resolving the deployment's configured GitHub App slug
Expand Down Expand Up @@ -35,3 +35,12 @@ export function setConfiguredGitHubAppSlugCache(
export function getEffectiveGitHubAppSlug(): string {
return configuredAppSlugCache?.value ?? Env.R_GITHUB_APP_SLUG;
}
/**
* Whether this deployment answers (and advertises) the canonical `@roomote`
* mention alias in addition to its own app slug. Disable with
* `R_GITHUB_DISABLE_CANONICAL_MENTION=true` when several deployments share
* the same repositories and a bare `@roomote` would make all of them respond.
*/
export function isCanonicalGitHubMentionEnabled(): boolean {
return !isEnvFlagEnabled(Env.R_GITHUB_DISABLE_CANONICAL_MENTION);
}
45 changes: 0 additions & 45 deletions packages/github/src/resolve-app-slug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,48 +46,3 @@ export async function resolveConfiguredGitHubAppSlug(): Promise<string> {

return getEffectiveGitHubAppSlug();
}

/**
* Like {@link resolveConfiguredGitHubAppSlug}, but returns only a slug that is
* actually configured in process env or the deployment env table. Returns
* `null` when nothing is configured or resolution fails cold, so callers do
* not confuse the schema default `'roomote'` with a real deployment setting.
*
* Prefer this when rewriting user-visible attribution text: a missing config
* should leave the agent-supplied body alone instead of downgrading a correct
* custom-slug mention to `@roomote`.
*/
export async function resolveConfiguredGitHubAppSlugIfConfigured(): Promise<
string | null
> {
const cache = getConfiguredGitHubAppSlugCache();

if (cache?.value && cache.expiresAt > Date.now()) {
return cache.value;
}

try {
const slug = await resolveDeploymentEnvVar('R_GITHUB_APP_SLUG');

setConfiguredGitHubAppSlugCache({
value: slug,
expiresAt: Date.now() + CONFIGURED_APP_SLUG_CACHE_TTL_MS,
});

return slug;
} catch (error) {
console.warn(
`[resolveConfiguredGitHubAppSlugIfConfigured] leaving body-owned mentions alone after slug resolution failed: ${
error instanceof Error ? error.message : String(error)
}`,
);

const lastKnown = getConfiguredGitHubAppSlugCache()?.value;
if (lastKnown) {
return lastKnown;
}

const processSlug = process.env.R_GITHUB_APP_SLUG?.trim();
return processSlug || null;
}
}
Loading
Loading