feat(prd-6): verify workflow triggers and add assignee_trigger (#13)

This commit is contained in:
Vlad Mocanu committed 2026-02-25 21:43:37 +00:00
1 parent 095c2c2429
commit a129a15f61
5 files changed
+71 -19

No files matched your search

+18 -1
View File
@@ -14,10 +14,26 @@ concurrency:
group: claude-${{ github.event.issue.number || github.event.pull_request.number }}-${{ github.run_id }} group: claude-${{ github.event.issue.number || github.event.pull_request.number }}-${{ github.run_id }}
jobs: jobs:
# Temporary debug job to inspect Forgejo event payloads (PRD #6 E2E testing)
debug-event:
runs-on: grunt
steps:
- name: Dump event context
run: |
echo "=== Event Context ==="
echo "github.event_name: ${{ github.event_name }}"
echo "github.event.action: ${{ github.event.action }}"
echo "github.event.comment.body: ${{ github.event.comment.body }}"
echo "github.event.review.body: ${{ github.event.review.body }}"
echo "GITHUB_EVENT_NAME env: $GITHUB_EVENT_NAME"
echo ""
echo "=== Event Payload (first 200 lines) ==="
cat "$GITHUB_EVENT_PATH" | head -200
claude: claude:
if: | if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || ((github.event_name == 'pull_request_review_comment' || github.event_name == 'pull_request_comment') && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && ( (github.event_name == 'issues' && (
github.event.action == 'labeled' || github.event.action == 'labeled' ||
@@ -42,6 +58,7 @@ jobs:
bot_name: "claude" bot_name: "claude"
bot_email: "claude@wxs.ro" bot_email: "claude@wxs.ro"
platform: "forgejo" platform: "forgejo"
assignee_trigger: "claude"
env: env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+1 -1
View File
@@ -24,7 +24,7 @@ Systematically exercise each untested scenario on the live Forgejo instance (git
## Milestones ## Milestones
- [ ] **M1: Ensure workflow covers all trigger types** - [x] **M1: Ensure workflow covers all trigger types**
- Review `.forgejo/workflows/claude.yml` to confirm all 5 trigger types are listed - Review `.forgejo/workflows/claude.yml` to confirm all 5 trigger types are listed
- Add any missing triggers to the workflow file - Add any missing triggers to the workflow file
+15 -1
View File
@@ -60,6 +60,18 @@ const ENTITY_EVENT_NAMES = [
"pull_request_review_comment", "pull_request_review_comment",
] as const; ] as const;
/**
* Forgejo uses different event names for some webhook events.
* Normalize to GitHub-canonical names so all downstream code works unchanged.
*/
const FORGEJO_EVENT_NAME_MAP: Record<string, string> = {
pull_request_comment: "pull_request_review_comment",
};
function normalizeEventName(eventName: string): string {
return FORGEJO_EVENT_NAME_MAP[eventName] ?? eventName;
}
const AUTOMATION_EVENT_NAMES = [ const AUTOMATION_EVENT_NAMES = [
"workflow_dispatch", "workflow_dispatch",
"repository_dispatch", "repository_dispatch",
@@ -132,6 +144,8 @@ export type GitHubContext = ParsedGitHubContext | AutomationContext;
export function parseGitHubContext(): GitHubContext { export function parseGitHubContext(): GitHubContext {
const context = github.context; const context = github.context;
// Normalize Forgejo event names to GitHub-canonical equivalents
const eventName = normalizeEventName(context.eventName);
const commonFields = { const commonFields = {
runId: process.env.GITHUB_RUN_ID!, runId: process.env.GITHUB_RUN_ID!,
@@ -165,7 +179,7 @@ export function parseGitHubContext(): GitHubContext {
}, },
}; };
switch (context.eventName) { switch (eventName) {
case "issues": { case "issues": {
const payload = context.payload as IssuesEvent; const payload = context.payload as IssuesEvent;
return { return {
+11 -2
View File
@@ -2,6 +2,15 @@ import fs from "fs/promises";
import path from "path"; import path from "path";
import type { Octokits } from "../api/client"; import type { Octokits } from "../api/client";
// Mutable fs reference for cross-platform test compatibility.
// On Linux, Bun gives test and source files different fs/promises objects,
// so spyOn(fs, "mkdir") in a test file won't intercept calls here.
// Tests override these methods directly instead.
export const _fsForTest = {
mkdir: fs.mkdir.bind(fs),
writeFile: fs.writeFile.bind(fs),
};
// Build regexes at call time, reading GITHUB_SERVER_URL from process.env // Build regexes at call time, reading GITHUB_SERVER_URL from process.env
// directly. The config.ts export freezes the value at module load time, // directly. The config.ts export freezes the value at module load time,
// which can be wrong when the Forgejo CI runner injects its own // which can be wrong when the Forgejo CI runner injects its own
@@ -67,7 +76,7 @@ export async function downloadCommentImages(
const urlToPathMap = new Map<string, string>(); const urlToPathMap = new Map<string, string>();
const downloadsDir = "/tmp/github-images"; const downloadsDir = "/tmp/github-images";
await fs.mkdir(downloadsDir, { recursive: true }); await _fsForTest.mkdir(downloadsDir, { recursive: true });
const { imageRegex, htmlImgRegex } = buildImageRegexes(); const { imageRegex, htmlImgRegex } = buildImageRegexes();
@@ -217,7 +226,7 @@ export async function downloadCommentImages(
const arrayBuffer = await imageResponse.arrayBuffer(); const arrayBuffer = await imageResponse.arrayBuffer();
const buffer = Buffer.from(arrayBuffer); const buffer = Buffer.from(arrayBuffer);
await fs.writeFile(localPath, buffer); await _fsForTest.writeFile(localPath, buffer);
console.log(`✓ Saved: ${localPath}`); console.log(`✓ Saved: ${localPath}`);
urlToPathMap.set(originalUrl, localPath); urlToPathMap.set(originalUrl, localPath);
+26 -14
View File
@@ -8,20 +8,28 @@ import {
jest, jest,
setSystemTime, setSystemTime,
} from "bun:test"; } from "bun:test";
import fs from "fs/promises"; import {
import { downloadCommentImages } from "../src/github/utils/image-downloader"; downloadCommentImages,
import type { CommentWithImages } from "../src/github/utils/image-downloader"; _fsForTest,
type CommentWithImages,
} from "../src/github/utils/image-downloader";
import type { Octokits } from "../src/github/api/client"; import type { Octokits } from "../src/github/api/client";
describe("downloadCommentImages", () => { describe("downloadCommentImages", () => {
let consoleLogSpy: any; let consoleLogSpy: any;
let consoleWarnSpy: any; let consoleWarnSpy: any;
let consoleErrorSpy: any; let consoleErrorSpy: any;
let fsMkdirSpy: any;
let fsWriteFileSpy: any;
let fetchSpy: any; let fetchSpy: any;
let savedServerUrl: string | undefined; let savedServerUrl: string | undefined;
// Store originals for restoration
const origMkdir = _fsForTest.mkdir;
const origWriteFile = _fsForTest.writeFile;
// Mock functions for fs operations
const mockMkdir = jest.fn().mockResolvedValue(undefined);
const mockWriteFile = jest.fn().mockResolvedValue(undefined);
beforeEach(() => { beforeEach(() => {
// Pin GITHUB_SERVER_URL so tests pass regardless of runner environment. // Pin GITHUB_SERVER_URL so tests pass regardless of runner environment.
// The image-downloader reads process.env directly at call time. // The image-downloader reads process.env directly at call time.
@@ -33,9 +41,11 @@ describe("downloadCommentImages", () => {
consoleWarnSpy = spyOn(console, "warn").mockImplementation(() => {}); consoleWarnSpy = spyOn(console, "warn").mockImplementation(() => {});
consoleErrorSpy = spyOn(console, "error").mockImplementation(() => {}); consoleErrorSpy = spyOn(console, "error").mockImplementation(() => {});
// Spy on fs methods // Override _fsForTest methods directly — works cross-platform
fsMkdirSpy = spyOn(fs, "mkdir").mockResolvedValue(undefined); mockMkdir.mockClear();
fsWriteFileSpy = spyOn(fs, "writeFile").mockResolvedValue(undefined); mockWriteFile.mockClear();
_fsForTest.mkdir = mockMkdir as any;
_fsForTest.writeFile = mockWriteFile as any;
// Set fake system time for consistent filenames // Set fake system time for consistent filenames
setSystemTime(new Date("2024-01-01T00:00:00.000Z")); // 1704067200000 setSystemTime(new Date("2024-01-01T00:00:00.000Z")); // 1704067200000
@@ -49,11 +59,13 @@ describe("downloadCommentImages", () => {
delete process.env.GITHUB_SERVER_URL; delete process.env.GITHUB_SERVER_URL;
} }
// Restore original fs methods
_fsForTest.mkdir = origMkdir;
_fsForTest.writeFile = origWriteFile;
consoleLogSpy.mockRestore(); consoleLogSpy.mockRestore();
consoleWarnSpy.mockRestore(); consoleWarnSpy.mockRestore();
consoleErrorSpy.mockRestore(); consoleErrorSpy.mockRestore();
fsMkdirSpy.mockRestore();
fsWriteFileSpy.mockRestore();
if (fetchSpy) fetchSpy.mockRestore(); if (fetchSpy) fetchSpy.mockRestore();
setSystemTime(); // Reset to real time setSystemTime(); // Reset to real time
}); });
@@ -80,7 +92,7 @@ describe("downloadCommentImages", () => {
await downloadCommentImages(mockOctokit, "owner", "repo", comments); await downloadCommentImages(mockOctokit, "owner", "repo", comments);
expect(fsMkdirSpy).toHaveBeenCalledWith("/tmp/github-images", { expect(mockMkdir).toHaveBeenCalledWith("/tmp/github-images", {
recursive: true, recursive: true,
}); });
}); });
@@ -153,7 +165,7 @@ describe("downloadCommentImages", () => {
}); });
expect(fetchSpy).toHaveBeenCalledWith(signedUrl); expect(fetchSpy).toHaveBeenCalledWith(signedUrl);
expect(fsWriteFileSpy).toHaveBeenCalledWith( expect(mockWriteFile).toHaveBeenCalledWith(
"/tmp/github-images/image-1704067200000-0.png", "/tmp/github-images/image-1704067200000-0.png",
Buffer.from(mockArrayBuffer), Buffer.from(mockArrayBuffer),
); );
@@ -628,7 +640,7 @@ describe("downloadCommentImages", () => {
); );
// Reset for next iteration // Reset for next iteration
fsWriteFileSpy.mockClear(); mockWriteFile.mockClear();
callIndex++; callIndex++;
} }
}); });
@@ -721,7 +733,7 @@ describe("downloadCommentImages", () => {
}); });
expect(fetchSpy).toHaveBeenCalledWith(signedUrl); expect(fetchSpy).toHaveBeenCalledWith(signedUrl);
expect(fsWriteFileSpy).toHaveBeenCalledWith( expect(mockWriteFile).toHaveBeenCalledWith(
"/tmp/github-images/image-1704067200000-0.png", "/tmp/github-images/image-1704067200000-0.png",
Buffer.from(mockArrayBuffer), Buffer.from(mockArrayBuffer),
); );