Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
8 changes: 8 additions & 0 deletions .changeset/safe-public-attribution.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@roomote/cloud-agents': patch
'@roomote/db': patch
'@roomote/sdk': patch
'@roomote/types': patch
---

Use linked source-control usernames instead of account names when attributing Roomote changes in public repositories.
17 changes: 17 additions & 0 deletions apps/docs/source-control.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,23 @@ After setup, verify that Roomote can:
- clone the repository inside a task sandbox
- push a branch or open a reviewable change when the task finishes

## Attribution on pull requests and commits

Roomote keeps human-readable attribution inside private repositories. For
public repositories, it uses the task participant's linked source-control
username when one is available. If Roomote cannot resolve a linked username,
the pull request or merge request says only that it was created by Roomote.

Roomote never derives public attribution from an account email address. Commit
emails use the source-control provider's `noreply` identity when available, or
the Roomote identity otherwise. A workspace containing any public or
unresolved repository uses the public-safe identity for all new commits because
Git author configuration applies across the workspace.

Changing a repository from private to public does not rewrite existing Git
history. Roomote sanitizes a legacy named attribution line the next time it
updates an open public pull request.

## Pull request review comments

When **Review Code** finds an issue on a changed line, Roomote posts the finding
Expand Down

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

25 changes: 20 additions & 5 deletions packages/cloud-agents/src/server/commit-author.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {
type CommitAuthorKind,
type TaskInitiator,
getUserDisplayName,
PRODUCT_NAME,
} from '@roomote/types';
import {
Expand Down Expand Up @@ -69,6 +68,8 @@ export type ResolvedTaskCommitAuthor = {
kind: CommitAuthorKind;
/** Human-readable display name; PRODUCT_NAME for roomote authorship. */
displayName: string;
/** Source-control handle safe to publish, including its leading `@`. */
publicDisplayName: string | null;
githubLogin: string | null;
prAssigneeLogin: string | null;
gitAuthor: ResolvedGitAuthor;
Expand All @@ -77,6 +78,7 @@ export type ResolvedTaskCommitAuthor = {
export const DEFAULT_ROOMOTE_COMMIT_AUTHOR: ResolvedTaskCommitAuthor = {
kind: 'roomote',
displayName: PRODUCT_NAME,
publicDisplayName: null,
githubLogin: null,
prAssigneeLogin: null,
gitAuthor: ROOMOTE_GIT_AUTHOR,
Expand Down Expand Up @@ -212,7 +214,6 @@ export async function resolveTaskCommitAuthor(
columns: {
id: true,
name: true,
email: true,
},
});

Expand All @@ -224,14 +225,14 @@ export async function resolveTaskCommitAuthor(
githubIdentity.githubLogin ??
normalizeNullableString(task.commitAuthorLogin);
const displayName =
normalizeNullableString(getUserDisplayName(user)) ??
githubLogin ??
PRODUCT_NAME;
normalizeNullableString(user?.name) ?? githubLogin ?? PRODUCT_NAME;
const publicDisplayName = githubLogin ? `@${githubLogin}` : null;

if (!githubIdentity.githubLogin || !githubIdentity.githubUserId) {
return {
kind: 'user',
displayName,
publicDisplayName,
githubLogin,
prAssigneeLogin: null,
gitAuthor: ROOMOTE_GIT_AUTHOR,
Expand All @@ -241,6 +242,7 @@ export async function resolveTaskCommitAuthor(
return {
kind: 'user',
displayName,
publicDisplayName,
githubLogin,
prAssigneeLogin: githubIdentity.githubLogin,
gitAuthor: {
Expand All @@ -257,11 +259,13 @@ export async function resolveTaskCommitAuthor(
normalizeNullableString(task.actorDisplayName) ??
githubLogin ??
PRODUCT_NAME;
const publicDisplayName = githubLogin ? `@${githubLogin}` : null;

if (!githubLogin || !externalId) {
return {
kind: 'external',
displayName,
publicDisplayName,
githubLogin,
prAssigneeLogin,
gitAuthor: ROOMOTE_GIT_AUTHOR,
Expand All @@ -271,6 +275,7 @@ export async function resolveTaskCommitAuthor(
return {
kind: 'external',
displayName,
publicDisplayName,
githubLogin,
prAssigneeLogin,
gitAuthor: {
Expand All @@ -286,6 +291,16 @@ export async function resolveTaskCommitAuthor(
};
}

/** Use the provider noreply identity only when its public handle is available. */
export function resolvePublicGitAuthor(
attribution: ResolvedTaskCommitAuthor,
): ResolvedGitAuthor {
return attribution.publicDisplayName &&
attribution.gitAuthor.email !== ROOMOTE_GIT_AUTHOR.email
? { ...attribution.gitAuthor, name: attribution.publicDisplayName }
: ROOMOTE_GIT_AUTHOR;
}

/**
* Resolves attribution for a live run. A linked participant owns their turns;
* all ownerless or unlinked runs use the Roomote app identity.
Expand Down

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

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

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

Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { buildGitHubMessageInstructions } from '../github-message-instructions';
const DEFAULT_ATTRIBUTION: ResolvedTaskCommitAuthor = {
kind: 'roomote',
displayName: PRODUCT_NAME,
publicDisplayName: null,
githubLogin: null,
prAssigneeLogin: null,
gitAuthor: {
Expand Down
8 changes: 6 additions & 2 deletions packages/cloud-agents/src/server/workflows/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
buildTelegramMessagePermalink,
buildDiscordMessagePermalink,
getGitHubFollowUpMention,
formatPrBodyAttribution,
resolveTaskWorkspace,
} from '@roomote/types';
import {
Expand Down Expand Up @@ -273,12 +274,15 @@ function buildPrBodyAttributionLine({
: defaultFollowUpInstruction;

if (attribution.kind === 'roomote') {
return `> Created by Roomote. ${instruction}`;
return formatPrBodyAttribution('Created by Roomote.', instruction);
}

const safeUserName = escapeValue(attribution.displayName || PRODUCT_NAME);

return `> Opened on behalf of ${safeUserName}. ${instruction}`;
return formatPrBodyAttribution(
`Opened on behalf of ${safeUserName}.`,
instruction,
);
}
export function getWorkspaceInstructions(
repoFullNames?: string[],
Expand Down
Loading
Loading