Skip to content
Open
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

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
@@ -0,0 +1,53 @@
import { db, eq, githubInstallations } from '@roomote/db/server';
import * as GitHub from '@roomote/github';

import type { WebhookResponse } from '../../types';

interface InstallationRepositoriesChangePayload {
installation?: { id?: number } | null;
}

/**
* Resync an installation's repository list when its accessible repositories
* change on GitHub: `installation_repositories.added/removed` (selected-repos
* installs) and `repository.created/deleted/renamed` (all-repos installs emit
* these instead). A full resync is used rather than a narrow upsert because
* these payloads omit fields the `repositories` row needs (default branch,
* clone URL), and `syncRepositories` already handles upsert + deactivation.
*/
export async function handleInstallationRepositoriesChange(
payload: InstallationRepositoriesChangePayload,
): Promise<WebhookResponse> {
const installationId = payload.installation?.id;

if (typeof installationId !== 'number') {
return { status: 'ok', message: 'missing_installation' };
}

const installation = await db.query.githubInstallations.findFirst({
where: eq(githubInstallations.installationId, installationId),
columns: { installedByUserId: true },
});

if (!installation) {
return { status: 'ok', message: 'unknown_installation' };
}

const result = await GitHub.syncGitHubInstallation({
userId: installation.installedByUserId,
installationId,
});

if (!result.success) {
return {
status: 'error',
message: `Failed to resync installation ${installationId}: ${result.error}`,
};
}

return {
status: 'ok',
message: `Resynced installation ${installationId}`,
metadata: { repositoryCount: result.repositories.length },
};
}
21 changes: 21 additions & 0 deletions apps/api/src/handlers/github/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { handleWorkflowRunCompleted } from './handleWorkflowRunCompleted';

// Repository metadata sync:
import { handleRepositoryEdited } from './handleRepositoryEdited';
import { handleInstallationRepositoriesChange } from './handleInstallationRepositoriesChange';

// Utilities:
import { isFromKnownInstallation } from './isFromKnownInstallation';
Expand Down Expand Up @@ -462,6 +463,26 @@ github.post('/', async (c) => {
),
);

// Keep the stored repository list in sync as repos appear, disappear, or
// change access. Selected-repos installs emit `installation_repositories`;
// all-repos installs emit `repository.created/deleted` instead. Not gated
// on isRepoSkipped: the row must exist even for skipped repos.
webhooks.on(
['installation_repositories.added', 'installation_repositories.removed'],
({ id, name, payload }) =>
recordWebhook(id, `${name}.${payload.action}`, payload, () =>
handleInstallationRepositoriesChange(payload),
),
);

webhooks.on(
['repository.created', 'repository.deleted', 'repository.renamed'],
({ id, name, payload }) =>
recordWebhook(id, `${name}.${payload.action}`, payload, () =>
handleInstallationRepositoriesChange(payload),
),
);

webhooks.on('workflow_run.completed', ({ id, name, payload }) =>
recordWebhook(id, `${name}.${payload.action}`, payload, async () => {
if (isRepoSkipped(payload.repository.full_name)) {
Expand Down
15 changes: 15 additions & 0 deletions apps/docs/environments.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,21 @@ The setup task is meant to produce a working environment Roomote can reuse. If
it cannot finish, adjust the input and try again from
**Settings > Environments**.

## Start from a brand-new repository

You do not need an existing codebase to set up an environment. From
**Settings > Environments > New** (or the onboarding repo-selection step),
choose **Create a new repository** to open github.com with the right owner
pre-filled — either a brand-new repository or a fork of an existing one by
URL. Once you create it on GitHub, it appears in the repository list
automatically; if the GitHub App only has access to selected repositories,
grant it access to the new repository first.

An empty repository is fine. When you start setup against a repository with
no commits, Roomote pushes a minimal initial commit (a README and a
.gitignore) to the default branch and creates a basic environment. Building
the actual project is then just your first task in that environment.

## What to include

Add enough context for Roomote to start productively:
Expand Down
6 changes: 6 additions & 0 deletions apps/docs/providers/source-control/github.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,12 @@ Subscribe to these events:
GitHub Apps also receive `installation` and `installation_repositories`
events automatically; you do not need to subscribe to them in the app form.

Roomote uses the **Repository** and `installation_repositories` events to
pick up newly created or newly granted repositories without a manual refresh.
If your app was created before the **Repository** event was part of the
manifest, confirm it is enabled under the app's **Permissions & events** page
— GitHub has no API to update an app's event subscriptions.

## Save credentials

Copy these values into the `/setup` manual form, deployment env vars, or your
Expand Down

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

Loading
Loading