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
18 changes: 18 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
FROM node:22-bookworm-slim AS build

WORKDIR /workspace
RUN corepack enable
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.base.json ./
COPY packages ./packages
RUN pnpm install --frozen-lockfile
RUN pnpm build:packages

FROM node:22-bookworm-slim

WORKDIR /workspace
RUN apt-get update && apt-get install --yes --no-install-recommends git && rm -rf /var/lib/apt/lists/*
COPY --from=build /workspace /workspace
ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000
CMD ["node", "packages/github-app/dist/server.js"]
19 changes: 19 additions & 0 deletions docs/github-app-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "CodeDecay",
"description": "Deterministic CodeDecay pull-request safety analysis.",
"url": "https://github.com/SubmuxHQ/CodeDecay",
"hook_attributes": {
"url": "https://codedecay-github-app-1093061541451.us-central1.run.app/github/webhooks",
"active": true
},
"redirect_url": "https://github.com/SubmuxHQ/CodeDecay",
"public": false,
"default_permissions": {
"metadata": "read",
"contents": "read",
"pull_requests": "read",
"issues": "write",
"checks": "write"
},
"default_events": ["pull_request"]
}
41 changes: 31 additions & 10 deletions docs/github-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@ For the first hosted version, the app only runs deterministic PR analysis. It
does not run project commands, deployment commands, LLM calls, model calls, or
CodeDecayCloud services.

## GitHub App settings
## Create the GitHub App

Create a GitHub App in the GitHub organization that will own the hosted app.

Set the webhook URL to:
Create the App from [`github-app-manifest.json`](./github-app-manifest.json) in
the GitHub App creation flow. Its webhook URL is the Cloud Run service URL:

```text
https://<render-service-host>/github/webhooks
https://codedecay-github-app-1093061541451.us-central1.run.app/github/webhooks
```

Subscribe to these events:
Expand All @@ -45,23 +46,43 @@ Use these repository permissions:

The Issues permission is required because GitHub PR comments use the Issues API.

## Render deployment
## Google Cloud Run deployment

Create a Render Web Service connected to this repository.
The service is designed for Cloud Run in project
`project-7279e7f9-46cf-49d4-bbd`, region `us-central1`. Store each required
value in Secret Manager, never in a repository, command history, or GitHub
Actions secret.

Build command:
Create the secret containers, then add values from local files. Keep the
private key and webhook secret out of shell history and source control:

```bash
pnpm install --frozen-lockfile && pnpm --filter @submuxhq/codedecay-github-app build
gcloud secrets create codedecay-github-app-id
gcloud secrets create codedecay-github-app-private-key
gcloud secrets create codedecay-github-app-webhook-secret
gcloud secrets versions add codedecay-github-app-id --data-file=app-id.txt
gcloud secrets versions add codedecay-github-app-private-key --data-file=private-key.pem
gcloud secrets versions add codedecay-github-app-webhook-secret --data-file=webhook-secret.txt
```

Start command:
Deploy the reviewed commit with Cloud Run. Keep a single instance during the
staging rollout so duplicate webhook handling cannot race across instances.

```bash
pnpm --filter @submuxhq/codedecay-github-app start
gcloud run deploy codedecay-github-app \
--project=project-7279e7f9-46cf-49d4-bbd \
--region=us-central1 \
--source=. \
--allow-unauthenticated \
--min-instances=1 \
--max-instances=1 \
--memory=1Gi \
--cpu=1 \
--set-env-vars=NODE_ENV=production,GITHUB_WEBHOOK_PATH=/github/webhooks \
--set-secrets=GITHUB_APP_ID=codedecay-github-app-id:latest,GITHUB_PRIVATE_KEY=codedecay-github-app-private-key:latest,GITHUB_WEBHOOK_SECRET=codedecay-github-app-webhook-secret:latest
```

Required environment variables:
Cloud Run receives these required environment variables from Secret Manager:

```text
GITHUB_APP_ID=<numeric app id>
Expand Down
22 changes: 15 additions & 7 deletions packages/github-app/src/github/comments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,20 @@ async function findExistingComment(
octokit: GitHubClient,
target: GitHubTarget
): Promise<GitHubComment | undefined> {
const response = await octokit.rest.issues.listComments({
owner: target.owner,
repo: target.repo,
issue_number: target.pullNumber,
per_page: 100
});
for (let page = 1; page <= 10; page += 1) {
const response = await octokit.rest.issues.listComments({
owner: target.owner,
repo: target.repo,
issue_number: target.pullNumber,
per_page: 100,
page
});

const existing = response.data.find((comment) => comment.body?.includes(COMMENT_MARKER));
if (existing || response.data.length < 100) {
return existing;
}
}

return response.data.find((comment) => comment.body?.includes(COMMENT_MARKER));
return undefined;
}
4 changes: 3 additions & 1 deletion packages/github-app/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ createServer((request, response) => {
}

void Promise.resolve(middleware(request, response)).catch((error: unknown) => {
const message = error instanceof Error ? error.message : String(error);
process.stderr.write(`CodeDecay GitHub App request failed: ${message}\n`);
response.writeHead(500, { "content-type": "application/json" });
response.end(JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error) }));
response.end(JSON.stringify({ ok: false, error: "Internal server error" }));
});
}).listen(port, () => {
process.stdout.write(`CodeDecay GitHub App listening on port ${port} at ${webhooksPath}\n`);
Expand Down
20 changes: 20 additions & 0 deletions packages/github-app/test/github-app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,26 @@ describe("CodeDecay GitHub App", () => {
expect(octokit.rest.issues.createComment).not.toHaveBeenCalled();
});

it("finds a marker comment beyond the first page", async () => {
const octokit = createOctokit();
const firstPage = Array.from({ length: 100 }, (_, index) => ({ id: index + 1, body: "other comment" }));
vi.mocked(octokit.rest.issues.listComments)
.mockResolvedValueOnce({ data: firstPage })
.mockResolvedValueOnce({ data: [{ id: 101, body: `${COMMENT_MARKER}\nOld report` }] });

await upsertPullRequestComment({
octokit,
target: { owner: "SubmuxHQ", repo: "CodeDecay", pullNumber: 12, headSha: "head-sha" },
markdown: "## New report"
});

expect(octokit.rest.issues.listComments).toHaveBeenNthCalledWith(
2,
expect.objectContaining({ page: 2 })
);
expect(octokit.rest.issues.updateComment).toHaveBeenCalledWith(expect.objectContaining({ comment_id: 101 }));
});

it("marks the check run as failed when analysis fails", async () => {
const context = createContext();
const cleanup = vi.fn();
Expand Down
Loading