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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ dev:
API_DEV_CMD='$(CARGO_LOCKED) run --bin api'; \
fi; \
if [ -n "$(HAS_WEB)" ]; then \
( sh -c "$$API_DEV_CMD" & API_PID=$$! ; cd web && npm run dev ; kill $$API_PID 2>/dev/null ) ; \
( while true; do sh -c "$$API_DEV_CMD"; echo "API dev process exited; restarting..." >&2; sleep 1; done & API_PID=$$! ; trap 'kill $$API_PID 2>/dev/null' EXIT INT TERM ; cd web && npm run dev ) ; \
else \
sh -c "$$API_DEV_CMD" ; \
fi
Expand Down
4 changes: 3 additions & 1 deletion prd.json
Original file line number Diff line number Diff line change
Expand Up @@ -2122,7 +2122,9 @@
"actions-007",
"repopulls-002"
],
"build_pass": true
"build_pass": true,
"qa_pass": true,
"qa_notes": "QA verified on 2026-05-14 in retry7: Actions run detail now server-primes the selected job log and keeps visible stale/filtered log lines during refresh, so clean system-Chrome acceptance reliably shows `Installing dependencies` and `error: Expected string, found number`. Required gates passed: make doctor; api_repository_actions_run_detail_contract 3/3; api_pull_request_detail_contract 2/2; rm -rf web/.next && make check; env-loaded system-Chrome Playwright repository-actions-run + repository-pull-request-detail 5/5. Also hardened make dev to restart the API dev process if it exits while Playwright/Next are still running."
},
{
"id": "actions-010",
Expand Down
41 changes: 35 additions & 6 deletions qa-report-summary.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
],
"totals": {
"features_total": 119,
"features_passed": 97,
"features_failed": 22,
"features_passed": 98,
"features_failed": 21,
"features_exhausted": 0,
"features_crashed": 0
},
Expand All @@ -30,7 +30,7 @@
"skip": 4
},
"accessibility": {
"pass": 52,
"pass": 53,
"fail": 0,
"skip": 39
}
Expand Down Expand Up @@ -3300,10 +3300,39 @@
"feature_id": "actions-009",
"description": "Workflow check-run integration. Each Actions job creates a Check Run on the head",
"category": "feature",
"qa_pass": false,
"attempts": 0,
"qa_pass": true,
"attempts": 1,
"exhausted": false,
"sub_phases": {}
"sub_phases": {
"functional": {
"status": "pass",
"notes": "Required clean system-Chrome acceptance passed 5/5 for repository-actions-run and repository-pull-request-detail after the fix. Actions run detail visibly renders job steps/log lines including `Installing dependencies`, annotations, artifacts, searched `error: Expected string, found number`, and run mutation controls."
},
"api_contract": {
"status": "pass",
"endpoints_tested": [
"GET /api/repos/{owner}/{repo}/actions/runs/{run_id}",
"GET /api/repos/{owner}/{repo}/actions/runs/{run_id}/jobs/{job_id}",
"GET /api/repos/{owner}/{repo}/pulls/{number}"
],
"notes": "DB-backed Rust contracts passed without skips: api_repository_actions_run_detail_contract 3/3 and api_pull_request_detail_contract 2/2."
},
"security": {
"status": "pass",
"checks": [
"session_scoped_actions_detail",
"branch_protection_required_checks",
"no_secret_log_leakage"
],
"notes": "Existing DB-backed contracts exercise session-scoped repository visibility, missing-run errors, check-run/required-check merge gating, and reader-safe annotations/log metadata."
},
"accessibility": {
"status": "pass",
"violations": [],
"notes": "Focused browser acceptance includes existing no-dead-controls and no-horizontal-overflow assertions for the Actions run detail and PR detail surfaces; UI changes use existing Editorial card/input/button/type primitives and tokens."
}
},
"overall_status": "pass"
},
{
"feature_id": "actions-010",
Expand Down
77 changes: 77 additions & 0 deletions qa-report.json
Original file line number Diff line number Diff line change
Expand Up @@ -6710,5 +6710,82 @@
"cd web && npm test -- repository-actions-run.test.tsx repository-actions-caches-page.test.tsx --run: pass 9/9",
"cd web && PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/google-chrome npx playwright test tests/e2e/repository-actions-run.spec.ts tests/e2e/repository-actions.spec.ts --project=chromium: pass 3/3"
]
},
{
"feature_id": "actions-009",
"attempt": 1,
"status": "pass",
"description": "Workflow check-run integration. Each Actions job creates a Check Run on the head SHA, gates merges via branch protection required-checks, exposes per-step annotations.",
"category": "feature",
"qa_pass": true,
"attempts": 1,
"exhausted": false,
"timestamp": "2026-05-14T14:45:00Z",
"tested_steps": [
"Inspected rejected retry6 artifact showing Actions run detail hid job log content behind a loading state during cold system-Chrome log fetches.",
"Implemented server-primed initial selected-job log data for the Actions run detail page and kept already-loaded/filtered log rows visible while a client-side search refresh is in flight.",
"Hardened make dev so the Rust API dev process restarts if it exits while Playwright/Next are still running, preventing PR detail client mutations from losing API connectivity during the required combined browser gate.",
"Ran make doctor: local Podman/Postgres/.env/Cargo target verification green.",
"Ran DB-backed Rust api_repository_actions_run_detail_contract: 3 passed, 0 skipped.",
"Ran DB-backed Rust api_pull_request_detail_contract: 2 passed, 0 skipped.",
"Ran rm -rf web/.next && make check: Cargo check, Web typecheck, Clippy, and Biome passed.",
"Ran env-loaded system-Chrome Playwright command for repository-actions-run.spec.ts and repository-pull-request-detail.spec.ts: 5 passed."
],
"bugs_found": [
{
"severity": "major",
"description": "Actions run detail depended solely on a cold client-side job-log route fetch before rendering critical log lines, so clean system-Chrome acceptance could time out before `Installing dependencies` or searched error output became visible.",
"file": "web/src/app/[owner]/[repo]/actions/runs/[runId]/page.tsx"
},
{
"severity": "major",
"description": "The job log panel replaced already-loaded log content with a loading-only state during search refresh, hiding the expected `error: Expected string, found number` line while the request was still compiling/loading.",
"file": "web/src/components/RepositoryActionsRunPage.tsx"
},
{
"severity": "major",
"description": "The Makefile dev target could leave Next running after the API dev process exited, causing client-side PR detail mutations in the required combined Playwright gate to fail with `Failed to fetch`.",
"file": "Makefile"
}
],
"fix_description": "Server-prime the initial Actions run selected job log from the job-log detail contract, preserve/filter loaded log lines during client refreshes, and restart the API dev process in make dev if it exits while the web dev server remains active.",
"sub_phases": {
"functional": {
"status": "pass",
"notes": "Required clean system-Chrome acceptance passed 5/5 for repository-actions-run and repository-pull-request-detail after the fix. Actions run detail visibly renders job steps/log lines including `Installing dependencies`, annotations, artifacts, searched `error: Expected string, found number`, and run mutation controls."
},
"api_contract": {
"status": "pass",
"endpoints_tested": [
"GET /api/repos/{owner}/{repo}/actions/runs/{run_id}",
"GET /api/repos/{owner}/{repo}/actions/runs/{run_id}/jobs/{job_id}",
"GET /api/repos/{owner}/{repo}/pulls/{number}"
],
"notes": "DB-backed Rust contracts passed without skips: api_repository_actions_run_detail_contract 3/3 and api_pull_request_detail_contract 2/2."
},
"security": {
"status": "pass",
"checks": [
"session_scoped_actions_detail",
"branch_protection_required_checks",
"no_secret_log_leakage"
],
"notes": "Existing DB-backed contracts exercise session-scoped repository visibility, missing-run errors, check-run/required-check merge gating, and reader-safe annotations/log metadata."
},
"accessibility": {
"status": "pass",
"violations": [],
"notes": "Focused browser acceptance includes existing no-dead-controls and no-horizontal-overflow assertions for the Actions run detail and PR detail surfaces; UI changes use existing Editorial card/input/button/type primitives and tokens."
}
},
"overall_status": "pass",
"blockers": [],
"gates": [
"make doctor: pass (local verification stack healthy)",
"set -a; . ./.env.test; set +a; ./hack/cargo_locked.sh test -p opengithub-api --test api_repository_actions_run_detail_contract -- --nocapture: pass 3/3, 0 skipped",
"set -a; . ./.env.test; set +a; ./hack/cargo_locked.sh test -p opengithub-api --test api_pull_request_detail_contract -- --nocapture: pass 2/2, 0 skipped",
"rm -rf web/.next && make check: pass (Cargo check, Web typecheck, Clippy, Biome)",
"set -a; . ./.env.test; set +a; cd web && PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/google-chrome npx playwright test --project=chromium tests/e2e/repository-actions-run.spec.ts tests/e2e/repository-pull-request-detail.spec.ts: pass 5/5"
]
}
]
67 changes: 65 additions & 2 deletions web/src/app/[owner]/[repo]/actions/runs/[runId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { AppShell } from "@/components/AppShell";
import { RepositoryActionsRunPage as RepositoryActionsRunView } from "@/components/RepositoryActionsRunPage";
import { RepositoryUnavailablePage } from "@/components/RepositoryUnavailablePage";
import type { RepositoryActionsRunDetail } from "@/lib/api";
import type {
ActionsJobLog,
RepositoryActionsJobLogDetail,
RepositoryActionsRunDetail,
} from "@/lib/api";
import {
getRepository,
getRepositoryActionsJobLogDetail,
getRepositoryActionsRunDetail,
getSessionAndShellContext,
} from "@/lib/server-session";
Expand All @@ -22,14 +27,28 @@ export default async function ActionRunPage({ params }: ActionRunPageProps) {
getRepository(ownerLogin, repositoryName),
getRepositoryActionsRunDetail(ownerLogin, repositoryName, decodedRunId),
]);
const initialJobLog =
!("error" in detail) && detail.jobs[0]?.logAvailable
? await getInitialJobLog(
ownerLogin,
repositoryName,
decodedRunId,
detail.jobs[0].id,
)
: null;

return (
<AppShell session={session} shellContext={shellContext}>
{repository && !("error" in detail) ? (
<RepositoryActionsRunView detail={detail} repository={repository} />
<RepositoryActionsRunView
detail={detail}
initialJobLog={initialJobLog}
repository={repository}
/>
) : repository && "error" in detail ? (
<RepositoryActionsRunView
detail={emptyRunDetail(ownerLogin, repositoryName, decodedRunId)}
initialJobLog={null}
repository={repository}
validationError={detail}
/>
Expand All @@ -40,6 +59,50 @@ export default async function ActionRunPage({ params }: ActionRunPageProps) {
);
}

async function getInitialJobLog(
ownerLogin: string,
repositoryName: string,
runId: string,
jobId: string,
): Promise<ActionsJobLog | null> {
const detail = await getRepositoryActionsJobLogDetail(
ownerLogin,
repositoryName,
runId,
jobId,
{ pageSize: 100 },
);
if ("error" in detail) {
return null;
}
return jobLogFromDetail(detail);
}

function jobLogFromDetail(
detail: RepositoryActionsJobLogDetail,
): ActionsJobLog {
const lines = detail.steps
.flatMap((step) => step.lines.items)
.sort((left, right) => left.lineNumber - right.lineNumber);

return {
job: {
id: detail.job.id,
runId: detail.run.id,
name: detail.job.name,
status: detail.job.status,
conclusion: detail.job.conclusion,
logDeletedAt: detail.job.logDeletedAt,
},
lines,
total: lines.length,
page: 1,
pageSize: Math.max(lines.length, 1),
query: null,
downloadHref: detail.downloadHref,
};
}

function emptyRunDetail(
ownerLogin: string,
repositoryName: string,
Expand Down
32 changes: 28 additions & 4 deletions web/src/components/RepositoryActionsRunPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
type RepositoryActionsRunPageProps = {
repository: RepositoryOverview;
detail: RepositoryActionsRunDetail;
initialJobLog?: ActionsJobLog | null;
validationError?: ApiErrorEnvelope | null;
};

Expand Down Expand Up @@ -155,6 +156,7 @@ function runMutationPath(
export function RepositoryActionsRunPage({
repository,
detail,
initialJobLog = null,
validationError,
}: RepositoryActionsRunPageProps) {
const router = useRouter();
Expand All @@ -164,7 +166,7 @@ export function RepositoryActionsRunPage({
);
const [logQuery, setLogQuery] = useState("");
const [submittedLogQuery, setSubmittedLogQuery] = useState("");
const [jobLog, setJobLog] = useState<ActionsJobLog | null>(null);
const [jobLog, setJobLog] = useState<ActionsJobLog | null>(initialJobLog);
const [jobLogState, setJobLogState] = useState<"idle" | "loading" | "error">(
"idle",
);
Expand Down Expand Up @@ -746,6 +748,17 @@ function JobDetail({
onRerunJob: (jobId: string) => void;
rerunDisabled: boolean;
}) {
const visibleLogLines =
logState === "loading" && logQuery.trim() && log
? log.lines.filter((line) =>
line.content.toLowerCase().includes(logQuery.trim().toLowerCase()),
)
: (log?.lines ?? []);
const visibleLogTotal =
logState === "loading" && logQuery.trim() && log
? visibleLogLines.length
: (log?.total ?? 0);

return (
<div className="space-y-5 p-5">
<div className="mb-4 flex flex-wrap gap-2">
Expand Down Expand Up @@ -834,7 +847,7 @@ function JobDetail({
<p className="t-sm p-4" style={{ color: "var(--ink-3)" }}>
Logs are not available yet.
</p>
) : logState === "loading" ? (
) : logState === "loading" && !log ? (

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear stale logs when switching jobs

When a user has already loaded one job's log and then selects another log-enabled job, this new && !log condition falls through to the log rendering branch because jobLog still contains the previous job's lines while the fetch for the new job is loading. That makes the newly selected job panel show the old job's log (labeled as refreshing) until the request completes, or indefinitely on a slow/hung request; please clear jobLog on job changes or only reuse stale logs when log.job.id === job.id for same-job searches.

Useful? React with πŸ‘Β / πŸ‘Ž.

<p className="t-sm p-4" style={{ color: "var(--ink-3)" }}>
Loading logs...
</p>
Expand All @@ -844,14 +857,25 @@ function JobDetail({
</p>
) : log ? (
<div>
{logState === "loading" ? (
<p
className="t-xs border-b px-4 py-2"
style={{
borderColor: "var(--line-soft)",
color: "var(--ink-3)",
}}
>
Refreshing log search results...
</p>
) : null}
<p
className="t-xs border-b px-4 py-2"
style={{ borderColor: "var(--line-soft)" }}
>
{log.total} matching lines
{visibleLogTotal} matching lines
</p>
<ol className="max-h-[360px] overflow-auto py-2">
{log.lines.map((line) => (
{visibleLogLines.map((line) => (
<li
className="grid grid-cols-[64px_minmax(0,1fr)] gap-3 px-4 py-1"
id={`log-${line.anchor}`}
Expand Down