Skip to content

SRVOCF-1038: Show build status and pipeline failures in the functions list - #177

Draft
matejvasek wants to merge 18 commits into
masterfrom
SRVOCF-1038-build-status
Draft

SRVOCF-1038: Show build status and pipeline failures in the functions list#177
matejvasek wants to merge 18 commits into
masterfrom
SRVOCF-1038-build-status

Conversation

@matejvasek

@matejvasek matejvasek commented Sep 2, 2026

Copy link
Copy Markdown

Summary

  • 🎁 Surface GitHub Actions build status (Building / BuildFailed, with failure reason and a link to the run) in the functions list
  • 🎁 Live updates over SSE (/api/v1/func/build/watch) plus a snapshot endpoint (/api/v1/func/build/status), user-scoped like /list
  • 🔧 scm.Client.LatestWorkflowRun via GitHub Actions, scoped to func-deploy.yaml, ETag-cached to keep 304s free against the rate limit
  • 🧪 Ginkgo tests (handler, scm/github, fakegithub); Vitest tests (useBuildStatus, list merge); Playwright e2e against the real backend and fakegithub
  • 📚 Design spec in docs/design/2026-08-26-SRVOCF-1038-build-status-design.md

Fixes SRVOCF-1038

Checklist

  • Updated docs/ARCHITECTURE.md (if there are relevant changes to our layered architecture)

Additional Info

  • Build status is non-destructive over an available function: Running/ScaledToZero keep their cluster badge with a secondary build indicator, so availability is never misrepresented.
  • Deferred: non-destructive treatment for a cluster Error (needs gating on cluster presence, not the status string, since Error is overloaded with a repo/list-level error). Noted in the design doc.
  • Draft while the xhigh self-review findings are triaged (no high-severity; remainder are low / prototype-scope).

🤖 Generated with Claude Code

matejvasek and others added 18 commits September 1, 2026 22:32
Design for surfacing GitHub Actions build status and pipeline
failures in the functions list, via an SSE stream from the backend
(polling GH Actions) read with consoleFetch. Covers the status merge
with the existing cluster watch, new Building/BuildFailed statuses,
parameterless user-scoped endpoints, fakegithub Actions API with
/_admin control, and the test strategy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add admin endpoints so tests and dev can script a repo's GitHub Actions
workflow run status, conclusion, and jobs, making build status
deterministic to exercise end to end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fetch the latest workflow run for a repo's default branch through the
GitHub Actions REST API and, on failure, derive a "<job> / <step>"
reason from the failed job. Includes fakegithub coverage and
failureReason fallbacks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a snapshot endpoint and an SSE watch endpoint that streams per-repo
build status, polling GitHub on an interval with heartbeats and periodic
repo rediscovery. Per-repo errors are surfaced in the snapshot, and each
snapshot is marshalled once with the bytes reused as the change key so
unchanged polls are skipped. Wire the routes into main.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stream build status over SSE via consoleFetch, sending the PAT in the
X-SCM-Token header, with reconnect/backoff and stop-on-auth-error.
Includes a consoleFetch stream test stub.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Merge streamed build status into the functions list: render Building and
BuildFailed (with a link to the run and a failure-reason tooltip), while
letting a Running cluster status win over a stale Failed build. Also
repairs the setup-guide test orphaned by a master helper rename.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pass the auth connectionId into useBuildStatus so the stream tears down
and reconnects with the current PAT on in-place login and account
switch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…limit usage

The build-status poll loop hit GitHub every 3s per repo, exhausting the
5,000/hr core rate limit. Wire a per-client in-memory httpcache transport
so unchanged responses come back as 304 Not Modified, which do not count
against the primary rate limit.

GitHub sends Cache-Control: max-age=60 on these responses, which would let
the cache serve a stale build status for up to ~60s. A forceRevalidate
transport sets Cache-Control: max-age=0 on every request so the cache
always revalidates with a conditional request: unchanged status stays a
free 304, but a real change is seen on the next poll.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A 2xx response with no body previously fell through the `if (!res.body)
return;` guard and permanently stopped the SSE stream, so the build-status
badges would silently freeze until the next connectionId change. Treat a
body-less response like any other stream end: fall through to the
backoff-and-reconnect path instead of giving up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
LatestWorkflowRun took the newest run across *all* workflows in a repo
(ListRepositoryWorkflowRuns), so an unrelated workflow (lint, CodeQL, a cron)
could mask or misrepresent the func build: a passing lint run could hide a
failed build, or a failing unrelated workflow could paint a function red.

Filter to the func build workflow by file name via ListWorkflowRunsByFileName.
The identifier is func's own DefaultGitHubWorkflowFilename ("func-deploy.yaml"),
re-exported from the scaffold package as scaffold.WorkflowFilename so it stays in
sync with what we actually scaffold. The scm layer stays func-agnostic: the
workflow file name is passed in as a parameter, supplied by the func-aware
handler. A repo without that workflow file returns 404, which we map to a nil
run (no build signal) so non-func repos and not-yet-pushed workflows fall back
to the cluster-derived status instead of erroring.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tream

consoleFetch applies a default ~60s request timeout that aborts the
request when it fires. On the long-lived build-status SSE stream that
tore the connection down every minute regardless of the backend's 15s
heartbeats, forcing a reconnect and a full initial snapshot re-fetch
from GitHub each time.

Pass timeout 0 to disable it so the stream is ended only by the hook's
own AbortController (on unmount or connectionId change).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e functions

A function that is deployed and available (serving `Running` or idle
`ScaledToZero`) now keeps its cluster status while a rebuild runs or fails,
instead of being overwritten by `Building`/`BuildFailed`. The build activity is
surfaced only as a small secondary indicator next to the status: a spinner
(tooltip "Build in progress") while building, or a red danger-colored warning
icon (tooltip "Latest build failed: <reason>", link to the run) when the latest
build failed. This stops an available function from flip-flopping to a
build-centric status on every redeploy and keeps availability accurate.

Deferred: giving a cluster `Error` (broken deployed revision) the same
non-destructive treatment. `Error` is overloaded (it also covers a repo/list
error with no cluster resource), so doing it right means gating on cluster
presence rather than the status string. Noted in the design doc.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… workflow

The fake's by-file-name runs endpoint
(/repos/{owner}/{repo}/actions/workflows/{workflow}/runs) shared a handler with
the repo-wide endpoint and ignored the {workflow} path segment, so both returned
every scripted run. That gave the fakegithub and e2e suites no fidelity for
workflow-file scoping: a regression where build status stopped querying only
func-deploy.yaml would go uncaught.

Give each scripted run a workflow-file identity (defaulting to
functions.WorkflowFilename so it stays in sync with what the client requests, and
overridable via the admin /_admin/actions/runs "workflow" field) and filter by
the {workflow} path segment on the by-file-name route. The repo-wide route still
returns all runs. Add a test asserting a run under a different workflow is not
returned when querying the func workflow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…id-stream

Once the SSE stream is established, a per-repo LatestWorkflowRun error (including
ErrUnauthorized) is logged and the last-known status is carried forward, and the
30s rediscover ListRepos error was only logged. So if the caller's PAT was
revoked after connecting, every poll failed, the change-detection key never
moved, no new frame was sent, and the client showed stale build status
indefinitely without ever seeing an auth error to trigger re-auth.

ListRepos is a single global call, so its ErrUnauthorized unambiguously means the
token is no longer valid. End the stream in that case; the client's reconnect
then hits the initial ListRepos, gets a 401 before the SSE upgrade, and its
existing isAuthError path stops the loop / prompts re-auth. Detection latency is
bounded by the rediscover interval. Non-auth rediscover errors still just log.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The per-repo build-status item carried an "Err" field populated with the
raw error string from a failed workflow-run fetch. That string was never
consumed by the frontend but was serialized onto the wire, exposing
internal error detail to the browser. Drop the field: a failed fetch with
no prior state now reports a plain "None" item and the cause is logged
server-side instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@openshift-merge-bot

Copy link
Copy Markdown

Pipeline controller notification
This repo is configured to use the pipeline controller. Second-stage tests will be triggered either automatically or after lgtm label is added, depending on the repository configuration. The pipeline controller will automatically detect which contexts are required and will utilize /test Prow commands to trigger the second stage.

For optional jobs, comment /test ? to see a list of all defined jobs. To trigger manually all jobs from second stage use /pipeline required command.

This repository is configured in: LGTM mode

@openshift-ci openshift-ci Bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Sep 2, 2026
@openshift-ci

openshift-ci Bot commented Sep 2, 2026

Copy link
Copy Markdown

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

@matejvasek

Copy link
Copy Markdown
Author

/test all

@matejvasek

Copy link
Copy Markdown
Author

/test e2e-aws

@matejvasek matejvasek changed the title feat(SRVOCF-1038): show build status and pipeline failures in the functions list SRVOCF-1038: Show build status and pipeline failures in the functions list Sep 2, 2026
@openshift-ci-robot openshift-ci-robot added the jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. label Sep 2, 2026
@openshift-ci-robot

openshift-ci-robot commented Sep 2, 2026

Copy link
Copy Markdown

@matejvasek: This pull request references SRVOCF-1038 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the story to target the "5.1.0" version, but no target version was set.

Details

In response to this:

Summary

  • 🎁 Surface GitHub Actions build status (Building / BuildFailed, with failure reason and a link to the run) in the functions list
  • 🎁 Live updates over SSE (/api/v1/func/build/watch) plus a snapshot endpoint (/api/v1/func/build/status), user-scoped like /list
  • 🔧 scm.Client.LatestWorkflowRun via GitHub Actions, scoped to func-deploy.yaml, ETag-cached to keep 304s free against the rate limit
  • 🧪 Ginkgo tests (handler, scm/github, fakegithub); Vitest tests (useBuildStatus, list merge); Playwright e2e against the real backend and fakegithub
  • 📚 Design spec in docs/design/2026-08-26-SRVOCF-1038-build-status-design.md

Fixes SRVOCF-1038

Checklist

  • Updated docs/ARCHITECTURE.md (if there are relevant changes to our layered architecture)

Additional Info

  • Build status is non-destructive over an available function: Running/ScaledToZero keep their cluster badge with a secondary build indicator, so availability is never misrepresented.
  • Deferred: non-destructive treatment for a cluster Error (needs gating on cluster presence, not the status string, since Error is overloaded with a repo/list-level error). Noted in the design doc.
  • Draft while the xhigh self-review findings are triaged (no high-severity; remainder are low / prototype-scope).

🤖 Generated with Claude Code

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@openshift-ci

openshift-ci Bot commented Sep 2, 2026

Copy link
Copy Markdown

@matejvasek: all tests passed!

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

@pmeida

pmeida commented Sep 4, 2026

Copy link
Copy Markdown
image

Side note: Can we have a more accurate message here?
the "Save & Deploy" btn can maybe stay as it is but the message should indicate that clicking it starts a build first, specially now that we have the status

@twoGiants twoGiants left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nice! Good work :) 👍

First review batch.

// to 'get resources from all namespaces'
isAllNamespacesKey(namespace) ? undefined : namespace,
);
const buildStatuses = useBuildStatus(connectionId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The status should be constructed in one place, so we need to gather all we need for it in one place too.

This belongs into useCluster where we get the other data for the status from the cluster and create it via deriveKnativeStatus.

At some point when we migrate the resource watching (useCluster logic) to the backend the full status will be created in the backend and the frontend will only receive the value.

@matejvasek matejvasek Sep 4, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

For simplicity I could create composite hook, e.g. useFunctions that would call useBuildStatus and useCluster and merge their result.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

At some point when we migrate the resource watching (useCluster logic) to the backend the full status will be created in the backend and the frontend will only receive the value.

Yeah, exactly. But that's for another time. The PR is already like huge.

Comment thread src/common/types.ts
Comment on lines 77 to 79
| 'CreatingRepo'
| 'Pushing'
| 'PushedToGitHub'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I think you can drop those. We don't use them anyway.

Comment thread src/common/types.ts
readonly mainResource: K8sResourceCommon;
}

export interface BuildStatus {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

In the current implementation the composite status of a function deployment is separated into two statuses: ClusterFunction.status and BuildStatus. They need to move closer together and become one composite status i.e. the status property needs an overhaul.

It could be done like this:

export interface ClusterFunction {
  readonly name: string;
  readonly namespace: string;
  readonly status: FunctionStatus;
  readonly url: string;
  readonly replicas: number;
  readonly mainResource: K8sResourceCommon;
}

interface FunctionStatus {
  deploymentStatus: DeploymentStatus;
  buildStatus: BuildStatus;
  conclusion?: string;
  runURL?: string;
  failureReason?: string;
}

type DeploymentStatus = 
  | 'Deploying'
  | 'Running'
  | 'ScaledToZero'
  | 'Error'
  | 'Unknown' // <- not sure about this
  | 'NotDeployed';

type BuildStatus = 
  | 'Building' 
  | 'Succeeded' 
  | 'Error'  // <- to be consistent with DeploymentStatus
  | 'None';

// OR THIS
interface FunctionStatus {
  deploymentStatus: DeploymentStatus;
  buildStatus: BuildStatusDetails;
}

type DeploymentStatus = 
  | 'Deploying'
  | 'Running'
  | 'ScaledToZero'
  | 'Error'
  | 'Unknown' // <- not sure about this
  | 'NotDeployed';

interface BuildStatusDetails {
  status: BuildStatus
  conclusion?: string;
  runURL?: string;
  failureReason?: string;

type BuildStatus = 
  | 'Building' 
  | 'Succeeded' 
  | 'Error'  // <- to be consistent with DeploymentStatus
  | 'None';

And the status should be created and updated in one place where the data for it is gathered -> useCluster.

return status === 'Running' || status === 'ScaledToZero';
}

function mergeBuild(item: FunctionTableItem, build: BuildStatus): FunctionTableItem {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This should move to useCluster where ClusterFunction is constructed and then 1 to 1 mapped to FunctionTableItem.

// actively serving (`Running`) or idle but ready to cold-start (`ScaledToZero`).
// Both have deployed successfully at least once, so a subsequent build is a
// rebuild whose status must not misrepresent the function's availability.
function isAvailable(status: FunctionTableItem['status']): boolean {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Move the func into mergeBuild and remove the comment, it's low value, tha name and the condition says it all.

// or a rebuild fails, so availability is never misrepresented. The build
// activity is surfaced only as a secondary indicator (see BuildActivityIndicator).
if (build.buildStatus === 'Building') {
// The in-progress indicator is a non-clickable spinner, so no run URL is

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remove comment.


function mergeBuild(item: FunctionTableItem, build: BuildStatus): FunctionTableItem {
if (isAvailable(item.status)) {
// Non-destructive over an available function: a function that is deployed

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I think a brief comment above the function is enough to explain what happens in this function.

Comment on lines +304 to +305
// Not currently deployed/available: the build status is the most useful thing
// to show, so it becomes the primary status.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I dislike the LLM style speak in the comments.

Suggested change
// Not currently deployed/available: the build status is the most useful thing
// to show, so it becomes the primary status.
// Not currently deployed/available: show only the build status.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Move this stub code to sdkTestDoubles.tsx module. I created it to contain all the test doubles for the OCP SDK.

export interface FunctionTableItem {
name: string;
repoName: string;
owner: string;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

You added the owner for using it as a part of the composite key for buildStatus or?

The repo name is unique and would suffice, or?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

As of now repoName would suffice because we are displaying repos only for currently logged user (currently set PAT owner).

The owner was added mostly to match backend strcutres, and backed was done to match GH API. So it's there more for consistency.

However I think we could display function from various repositories (owners) that given user has access to, so I would keep.

@pmeida pmeida left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nice job!

Also, can we cleanup the comments and only preserve the ones that explain the why? if they are absolutely needed?

if (buildActivity === 'Building') {
return (
<Tooltip content={t('Build in progress')}>
<Spinner size="sm" aria-label={t('Build in progress')} />

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Image

When I click it this kind of border appears. Can we disable this behaviour?

// so availability is never misrepresented.
case 'Running':
return <SuccessStatus title={status} />;
return withBuildActivity(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Image
Image
Image

In a normal non-failing build status 2 appears during a second or two.
This looks like a glitch.

This status appears for more time when its the first build, on function creation, but here it looks weird.

Can we get rid of this status at all? Or at least not allow it to happen in this case?

icon
);
const tooltip = failureReason
? t('Latest build failed: {{reason}}', { reason: failureReason })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

IMO we dont need the failureReason.

Saying something like "Latest build failed: deploy / Run tests" doesnt really give me much, I would still have to click on the link and investigate.

Also, this adds complexity when integrating with other building mechanisms other than github actions.

if (buildActivity === 'Building') {
return (
<Tooltip content={t('Build in progress')}>
<Spinner size="sm" aria-label={t('Build in progress')} />

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nothing to loose on making the in progress spinner also clickable. Users may want to check the in progress build details.

Comment thread backend/main.go
mux.HandleFunc("GET /api/v1/func/{owner}/{name}/files", h.HandleGetFiles)
mux.HandleFunc("PUT /api/v1/func/{owner}/{name}/files", h.HandlePutFiles)
mux.HandleFunc("POST /api/v1/func/create", h.HandleFuncCreate)
mux.HandleFunc("GET /api/v1/func/build/status", h.HandleBuildStatus)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This has currently no consumer. Did you kept it for debugging?

if (line.startsWith('event:')) event = line.slice('event:'.length).trim();
else if (line.startsWith('data:')) dataLines.push(line.slice('data:'.length).trim());
}
if (event && event !== 'build-status') return null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
if (event && event !== 'build-status') return null;
if (event !== 'build-status') return null;
let event = '';
if (event && event !== 'build-status'){ console.log(null)} else {console.log("continue")}
continue

This treats an empty-string event (SSE frame with no event: line) as a valid build-status snapshot.

@openshift-ci

openshift-ci Bot commented Sep 4, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please ask for approval from twogiants. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. jira/valid-reference Indicates that this PR references a valid Jira ticket of any type.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants