diff --git a/coderd/coderd.go b/coderd/coderd.go index 6e88759fa340a..2cd94d1eac646 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -270,7 +270,10 @@ func New(options *Options) *API { r.Get("/", api.organizationsByUser) r.Get("/{organizationname}", api.organizationByUserAndName) }) - r.Get("/workspace/{workspacename}", api.workspaceByOwnerAndName) + r.Route("/workspace/{workspacename}", func(r chi.Router) { + r.Get("/", api.workspaceByOwnerAndName) + r.Get("/builds/{buildnumber}", api.workspaceBuildByBuildNumber) + }) r.Get("/gitsshkey", api.gitSSHKey) r.Put("/gitsshkey", api.regenerateGitSSHKey) }) diff --git a/coderd/coderd_test.go b/coderd/coderd_test.go index 02fba90966992..7053b4ff56cb8 100644 --- a/coderd/coderd_test.go +++ b/coderd/coderd_test.go @@ -4,6 +4,7 @@ import ( "context" "io" "net/http" + "strconv" "strings" "testing" "time" @@ -163,6 +164,10 @@ func TestAuthorizeAllEndpoints(t *testing.T) { AssertObject: rbac.ResourceWorkspace, AssertAction: rbac.ActionRead, }, + "GET:/api/v2/users/me/workspace/{workspacename}/builds/{buildnumber}": { + AssertObject: rbac.ResourceWorkspace, + AssertAction: rbac.ActionRead, + }, "GET:/api/v2/workspaces/{workspace}/builds/{workspacebuildname}": { AssertAction: rbac.ActionRead, AssertObject: workspaceRBACObj, @@ -388,6 +393,7 @@ func TestAuthorizeAllEndpoints(t *testing.T) { route = strings.ReplaceAll(route, "{workspacename}", workspace.Name) route = strings.ReplaceAll(route, "{workspacebuildname}", workspace.LatestBuild.Name) route = strings.ReplaceAll(route, "{workspaceagent}", workspaceResources[0].Agents[0].ID.String()) + route = strings.ReplaceAll(route, "{buildnumber}", strconv.FormatInt(int64(workspace.LatestBuild.BuildNumber), 10)) route = strings.ReplaceAll(route, "{template}", template.ID.String()) route = strings.ReplaceAll(route, "{hash}", file.Hash) route = strings.ReplaceAll(route, "{workspaceresource}", workspaceResources[0].ID.String()) diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index 78f85e71c597c..3409d3041e739 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -616,6 +616,22 @@ func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceIDAndName(_ context.Context, a return database.WorkspaceBuild{}, sql.ErrNoRows } +func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(_ context.Context, arg database.GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (database.WorkspaceBuild, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + for _, workspaceBuild := range q.workspaceBuilds { + if workspaceBuild.WorkspaceID.String() != arg.WorkspaceID.String() { + continue + } + if workspaceBuild.BuildNumber != arg.BuildNumber { + continue + } + return workspaceBuild, nil + } + return database.WorkspaceBuild{}, sql.ErrNoRows +} + func (q *fakeQuerier) GetWorkspacesByOrganizationIDs(_ context.Context, req database.GetWorkspacesByOrganizationIDsParams) ([]database.Workspace, error) { q.mutex.RLock() defer q.mutex.RUnlock() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 59e8f4577ef8e..c3f57d3a9f795 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -70,6 +70,7 @@ type querier interface { GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (WorkspaceBuild, error) GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UUID) (WorkspaceBuild, error) GetWorkspaceBuildByWorkspaceID(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDParams) ([]WorkspaceBuild, error) + GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (WorkspaceBuild, error) GetWorkspaceBuildByWorkspaceIDAndName(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDAndNameParams) (WorkspaceBuild, error) GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Workspace, error) GetWorkspaceByOwnerIDAndName(ctx context.Context, arg GetWorkspaceByOwnerIDAndNameParams) (Workspace, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 06e5f61105269..e83051d65e667 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -3212,6 +3212,41 @@ func (q *sqlQuerier) GetWorkspaceBuildByWorkspaceID(ctx context.Context, arg Get return items, nil } +const getWorkspaceBuildByWorkspaceIDAndBuildNumber = `-- name: GetWorkspaceBuildByWorkspaceIDAndBuildNumber :one +SELECT + id, created_at, updated_at, workspace_id, template_version_id, name, build_number, transition, initiator_id, provisioner_state, job_id, deadline +FROM + workspace_builds +WHERE + workspace_id = $1 + AND build_number = $2 +` + +type GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams struct { + WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` + BuildNumber int32 `db:"build_number" json:"build_number"` +} + +func (q *sqlQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (WorkspaceBuild, error) { + row := q.db.QueryRowContext(ctx, getWorkspaceBuildByWorkspaceIDAndBuildNumber, arg.WorkspaceID, arg.BuildNumber) + var i WorkspaceBuild + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.WorkspaceID, + &i.TemplateVersionID, + &i.Name, + &i.BuildNumber, + &i.Transition, + &i.InitiatorID, + &i.ProvisionerState, + &i.JobID, + &i.Deadline, + ) + return i, err +} + const getWorkspaceBuildByWorkspaceIDAndName = `-- name: GetWorkspaceBuildByWorkspaceIDAndName :one SELECT id, created_at, updated_at, workspace_id, template_version_id, name, build_number, transition, initiator_id, provisioner_state, job_id, deadline diff --git a/coderd/database/queries/workspacebuilds.sql b/coderd/database/queries/workspacebuilds.sql index 5b53a874060e8..4bace271cb125 100644 --- a/coderd/database/queries/workspacebuilds.sql +++ b/coderd/database/queries/workspacebuilds.sql @@ -27,6 +27,15 @@ WHERE workspace_id = $1 AND "name" = $2; +-- name: GetWorkspaceBuildByWorkspaceIDAndBuildNumber :one +SELECT + * +FROM + workspace_builds +WHERE + workspace_id = $1 + AND build_number = $2; + -- name: GetWorkspaceBuildByWorkspaceID :many SELECT * diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index e6505a5395c83..cefe4ded6cdc6 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "net/http" + "strconv" "github.com/go-chi/chi/v5" "github.com/google/uuid" @@ -160,6 +161,82 @@ func (api *API) workspaceBuilds(rw http.ResponseWriter, r *http.Request) { httpapi.Write(rw, http.StatusOK, apiBuilds) } +func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Request) { + owner := httpmw.UserParam(r) + workspaceName := chi.URLParam(r, "workspacename") + buildNumber, err := strconv.ParseInt(chi.URLParam(r, "buildnumber"), 10, 32) + if err != nil { + httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ + Message: "Failed to parse build number as integer.", + Detail: err.Error(), + }) + return + } + + workspace, err := api.Database.GetWorkspaceByOwnerIDAndName(r.Context(), database.GetWorkspaceByOwnerIDAndNameParams{ + OwnerID: owner.ID, + Name: workspaceName, + }) + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ + Message: fmt.Sprintf("Workspace %q does not exist.", workspaceName), + }) + return + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: "Internal error fetching workspace by name.", + Detail: err.Error(), + }) + return + } + + if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceWorkspace. + InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) { + return + } + + workspaceBuild, err := api.Database.GetWorkspaceBuildByWorkspaceIDAndBuildNumber(r.Context(), database.GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams{ + WorkspaceID: workspace.ID, + BuildNumber: int32(buildNumber), + }) + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ + Message: fmt.Sprintf("Workspace %q Build %d does not exist.", workspaceName, buildNumber), + }) + return + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: "Internal error fetching workspace build.", + Detail: err.Error(), + }) + return + } + + job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: "Internal error fetching provisioner job.", + Detail: err.Error(), + }) + return + } + + users, err := api.Database.GetUsersByIDs(r.Context(), []uuid.UUID{workspace.OwnerID, workspaceBuild.InitiatorID}) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: "Internal error fetching user.", + Detail: err.Error(), + }) + return + } + + httpapi.Write(rw, http.StatusOK, + convertWorkspaceBuild(findUser(workspace.OwnerID, users), findUser(workspaceBuild.InitiatorID, users), + workspace, workspaceBuild, job)) +} + func (api *API) workspaceBuildByName(rw http.ResponseWriter, r *http.Request) { workspace := httpmw.WorkspaceParam(r) if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceWorkspace. diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index 1734f52b836f9..3833cee09171c 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -2,7 +2,9 @@ package coderd_test import ( "context" + "fmt" "net/http" + "strconv" "testing" "time" @@ -28,6 +30,94 @@ func TestWorkspaceBuild(t *testing.T) { require.NoError(t, err) } +func TestWorkspaceBuildByBuildNumber(t *testing.T) { + t.Parallel() + t.Run("Successful", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) + first := coderdtest.CreateFirstUser(t, client) + user, err := client.User(context.Background(), codersdk.Me) + require.NoError(t, err, "fetch me") + version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, first.OrganizationID, template.ID) + _, err = client.WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber( + context.Background(), + user.Username, + workspace.Name, + strconv.FormatInt(int64(workspace.LatestBuild.BuildNumber), 10), + ) + require.NoError(t, err) + }) + + t.Run("BuildNumberNotInt", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) + first := coderdtest.CreateFirstUser(t, client) + user, err := client.User(context.Background(), codersdk.Me) + require.NoError(t, err, "fetch me") + version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, first.OrganizationID, template.ID) + _, err = client.WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber( + context.Background(), + user.Username, + workspace.Name, + "buildNumber", + ) + var apiError *codersdk.Error + require.ErrorAs(t, err, &apiError) + require.Equal(t, http.StatusBadRequest, apiError.StatusCode()) + require.ErrorContains(t, apiError, "Failed to parse build number as integer.") + }) + + t.Run("WorkspaceNotFound", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) + first := coderdtest.CreateFirstUser(t, client) + user, err := client.User(context.Background(), codersdk.Me) + require.NoError(t, err, "fetch me") + version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, first.OrganizationID, template.ID) + _, err = client.WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber( + context.Background(), + user.Username, + "workspaceName", + strconv.FormatInt(int64(workspace.LatestBuild.BuildNumber), 10), + ) + var apiError *codersdk.Error + require.ErrorAs(t, err, &apiError) + require.Equal(t, http.StatusNotFound, apiError.StatusCode()) + require.ErrorContains(t, apiError, "Workspace \"workspaceName\" does not exist.") + }) + + t.Run("WorkspaceBuildNotFound", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) + first := coderdtest.CreateFirstUser(t, client) + user, err := client.User(context.Background(), codersdk.Me) + require.NoError(t, err, "fetch me") + version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, first.OrganizationID, template.ID) + _, err = client.WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber( + context.Background(), + user.Username, + workspace.Name, + "200", + ) + var apiError *codersdk.Error + require.ErrorAs(t, err, &apiError) + require.Equal(t, http.StatusNotFound, apiError.StatusCode()) + require.ErrorContains(t, apiError, fmt.Sprintf("Workspace %q Build 200 does not exist.", workspace.Name)) + }) +} + func TestWorkspaceBuilds(t *testing.T) { t.Parallel() t.Run("Single", func(t *testing.T) { diff --git a/codersdk/workspacebuilds.go b/codersdk/workspacebuilds.go index ec5dfcbc63ccd..79dae16d8f12c 100644 --- a/codersdk/workspacebuilds.go +++ b/codersdk/workspacebuilds.go @@ -103,3 +103,16 @@ func (c *Client) WorkspaceBuildState(ctx context.Context, build uuid.UUID) ([]by } return io.ReadAll(res.Body) } + +func (c *Client) WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber(ctx context.Context, username string, workspaceName string, buildNumber string) (WorkspaceBuild, error) { + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/workspace/%s/builds/%s", username, workspaceName, buildNumber), nil) + if err != nil { + return WorkspaceBuild{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return WorkspaceBuild{}, readBodyAsError(res) + } + var workspaceBuild WorkspaceBuild + return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild) +} diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index 8c8880c9de408..253a3c8e8c63f 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -113,15 +113,6 @@ export const AppRouter: FC = () => ( } /> - - - - } - /> - ( } /> + + + + + } + /> diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 39de3d7aff046..205f42fef8c23 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -268,8 +268,14 @@ export const getWorkspaceBuilds = async (workspaceId: string): Promise => { - const response = await axios.get(`/api/v2/workspacebuilds/${workspaceId}`) +export const getWorkspaceBuildByNumber = async ( + username = "me", + workspaceName: string, + buildNumber: string, +): Promise => { + const response = await axios.get( + `/api/v2/users/${username}/workspace/${workspaceName}/builds/${buildNumber}`, + ) return response.data } diff --git a/site/src/components/BuildsTable/BuildsTable.tsx b/site/src/components/BuildsTable/BuildsTable.tsx index 2e924c8157f4f..c9de9bd6d14ea 100644 --- a/site/src/components/BuildsTable/BuildsTable.tsx +++ b/site/src/components/BuildsTable/BuildsTable.tsx @@ -8,7 +8,7 @@ import TableRow from "@material-ui/core/TableRow" import KeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight" import useTheme from "@material-ui/styles/useTheme" import { FC } from "react" -import { useNavigate } from "react-router-dom" +import { useNavigate, useParams } from "react-router-dom" import * as TypesGen from "../../api/typesGenerated" import { displayWorkspaceBuildDuration, getDisplayWorkspaceBuildStatus } from "../../util/workspace" import { EmptyState } from "../EmptyState/EmptyState" @@ -29,6 +29,7 @@ export interface BuildsTableProps { } export const BuildsTable: FC = ({ builds, className }) => { + const { username, workspace: workspaceName } = useParams() const isLoading = !builds const theme: Theme = useTheme() const navigate = useNavigate() @@ -52,7 +53,7 @@ export const BuildsTable: FC = ({ builds, className }) => { const status = getDisplayWorkspaceBuildStatus(theme, build) const navigateToBuildPage = () => { - navigate(`/builds/${build.id}`) + navigate(`/@${username}/${workspaceName}/builds/${build.build_number}`) } return ( diff --git a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx index cebff84da793d..45d13caa88496 100644 --- a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx +++ b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx @@ -1,6 +1,11 @@ import { screen } from "@testing-library/react" import * as API from "../../api/api" -import { MockWorkspaceBuild, MockWorkspaceBuildLogs, renderWithAuth } from "../../testHelpers/renderHelpers" +import { + MockWorkspace, + MockWorkspaceBuild, + MockWorkspaceBuildLogs, + renderWithAuth, +} from "../../testHelpers/renderHelpers" import { WorkspaceBuildPage } from "./WorkspaceBuildPage" describe("WorkspaceBuildPage", () => { @@ -16,7 +21,10 @@ describe("WorkspaceBuildPage", () => { closed: Promise.resolve(undefined), cancel: jest.fn(), }) - renderWithAuth(, { route: `/builds/${MockWorkspaceBuild.id}`, path: "/builds/:buildId" }) + renderWithAuth(, { + route: `/@${MockWorkspace.owner_name}/${MockWorkspace.name}/builds/${MockWorkspace.latest_build.build_number}`, + path: "/@:username/:workspace/builds/:buildNumber", + }) await screen.findByText(MockWorkspaceBuild.workspace_name) await screen.findByText(MockWorkspaceBuildLogs[0].stage) diff --git a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx index 46550ac62225a..9d570db458dfe 100644 --- a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx +++ b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx @@ -6,19 +6,9 @@ import { pageTitle } from "../../util/page" import { workspaceBuildMachine } from "../../xServices/workspaceBuild/workspaceBuildXService" import { WorkspaceBuildPageView } from "./WorkspaceBuildPageView" -const useBuildId = () => { - const { buildId } = useParams() - - if (!buildId) { - throw new Error("buildId param is required.") - } - - return buildId -} - export const WorkspaceBuildPage: FC = () => { - const buildId = useBuildId() - const [buildState] = useMachine(workspaceBuildMachine, { context: { buildId } }) + const { username, workspace: workspaceName, buildNumber } = useParams() + const [buildState] = useMachine(workspaceBuildMachine, { context: { username, workspaceName, buildNumber } }) const { logs, build } = buildState.context const isWaitingForLogs = !buildState.matches("logs.loaded") diff --git a/site/src/testHelpers/handlers.ts b/site/src/testHelpers/handlers.ts index b77dd09c11aca..bfb8e9be0f4ff 100644 --- a/site/src/testHelpers/handlers.ts +++ b/site/src/testHelpers/handlers.ts @@ -115,7 +115,7 @@ export const handlers = [ rest.get("/api/v2/workspaces/:workspaceId/builds", async (req, res, ctx) => { return res(ctx.status(200), ctx.json(M.MockBuilds)) }), - rest.get("/api/v2/workspacebuilds/:workspaceBuildId", (req, res, ctx) => { + rest.get("/api/v2/users/:username/workspace/:workspaceName/builds/:buildNumber", (req, res, ctx) => { return res(ctx.status(200), ctx.json(M.MockWorkspaceBuild)) }), rest.get("/api/v2/workspacebuilds/:workspaceBuildId/resources", (req, res, ctx) => { diff --git a/site/src/xServices/workspaceBuild/workspaceBuildXService.ts b/site/src/xServices/workspaceBuild/workspaceBuildXService.ts index 88008892fe251..81883b3eaaa08 100644 --- a/site/src/xServices/workspaceBuild/workspaceBuildXService.ts +++ b/site/src/xServices/workspaceBuild/workspaceBuildXService.ts @@ -4,6 +4,9 @@ import { ProvisionerJobLog, WorkspaceBuild } from "../../api/typesGenerated" type LogsContext = { // Build + username: string + workspaceName: string + buildNumber: string buildId: string build?: WorkspaceBuild getBuildError?: Error | unknown @@ -36,28 +39,23 @@ export const workspaceBuildMachine = createMachine( }, }, tsTypes: {} as import("./workspaceBuildXService.typegen").Typegen0, - type: "parallel", + initial: "gettingBuild", states: { - build: { - initial: "gettingBuild", - states: { - gettingBuild: { - entry: "clearGetBuildError", - invoke: { - src: "getWorkspaceBuild", - onDone: { - target: "idle", - actions: "assignBuild", - }, - onError: { - target: "idle", - actions: "assignGetBuildError", - }, - }, + gettingBuild: { + entry: "clearGetBuildError", + invoke: { + src: "getWorkspaceBuild", + onDone: { + target: "logs", + actions: ["assignBuild", "assignBuildId"], + }, + onError: { + target: "idle", + actions: "assignGetBuildError", }, - idle: {}, }, }, + idle: {}, logs: { initial: "gettingExistentLogs", states: { @@ -95,6 +93,10 @@ export const workspaceBuildMachine = createMachine( }, { actions: { + // Build ID + assignBuildId: assign({ + buildId: (_, event) => event.data.id, + }), // Build assignBuild: assign({ build: (_, event) => event.data, @@ -117,7 +119,7 @@ export const workspaceBuildMachine = createMachine( }), }, services: { - getWorkspaceBuild: (ctx) => API.getWorkspaceBuild(ctx.buildId), + getWorkspaceBuild: (ctx) => API.getWorkspaceBuildByNumber(ctx.username, ctx.workspaceName, ctx.buildNumber), getLogs: async (ctx) => API.getWorkspaceBuildLogs(ctx.buildId), streamWorkspaceBuildLogs: (ctx) => async (callback) => { const reader = await API.streamWorkspaceBuildLogs(ctx.buildId)