diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 63e3a352a..757ff07c0 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -22,6 +22,7 @@ import ( "code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/base" context_module "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -60,6 +61,34 @@ func ViewLatest(ctx *context_module.Context) { ctx.Redirect(run.HTMLURL(), http.StatusTemporaryRedirect) } +func ViewLatestWorkflowRun(ctx *context_module.Context) { + branch := ctx.FormString("branch") + if branch == "" { + branch = ctx.Repo.Repository.DefaultBranch + } + branch = fmt.Sprintf("refs/heads/%s", branch) + event := ctx.FormString("event") + + workflowFile := ctx.Params("workflow_name") + run, err := actions_model.GetLatestRunForBranchAndWorkflow(ctx, ctx.Repo.Repository.ID, branch, workflowFile, event) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.NotFound("GetLatestRunForBranchAndWorkflow", err) + } else { + log.Error("GetLatestRunForBranchAndWorkflow: %v", err) + ctx.Error(http.StatusInternalServerError, "Unable to get latest run for workflow on branch") + } + return + } + + err = run.LoadAttributes(ctx) + if err != nil { + ctx.ServerError("LoadAttributes", err) + return + } + ctx.Redirect(run.HTMLURL(), http.StatusTemporaryRedirect) +} + type ViewRequest struct { LogCursors []struct { Step int `json:"step"` diff --git a/routers/web/web.go b/routers/web/web.go index 1744ddb83..e9f3f76c5 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1403,7 +1403,10 @@ func registerRoutes(m *web.Route) { }) }) - m.Get("/workflows/{workflow_name}/badge.svg", badges.GetWorkflowBadge) + m.Group("/workflows/{workflow_name}", func() { + m.Get("/badge.svg", badges.GetWorkflowBadge) + m.Get("/runs/latest", actions.ViewLatestWorkflowRun) + }) }, reqRepoActionsReader, actions.MustEnableActions) m.Group("/wiki", func() { diff --git a/tests/integration/actions_route_test.go b/tests/integration/actions_route_test.go index df22fc864..c941fca2e 100644 --- a/tests/integration/actions_route_test.go +++ b/tests/integration/actions_route_test.go @@ -1,9 +1,11 @@ // Copyright 2023 The Gitea Authors. All rights reserved. +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. // SPDX-License-Identifier: MIT package integration import ( + "context" "fmt" "net/http" "net/url" @@ -15,10 +17,82 @@ import ( "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" files_service "code.gitea.io/gitea/services/repository/files" + "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" ) +func TestActionsWebRouteLatestWorkflowRun(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + // create the repo + repo, _, f := CreateDeclarativeRepo(t, user2, "", + []unit_model.Type{unit_model.TypeActions}, nil, + []*files_service.ChangeRepoFile{ + { + Operation: "create", + TreePath: ".gitea/workflows/workflow-1.yml", + ContentReader: strings.NewReader("name: workflow-1\non:\n push:\njobs:\n job-1:\n runs-on: ubuntu-latest\n steps:\n - run: echo helloworld\n"), + }, + { + Operation: "create", + TreePath: ".gitea/workflows/workflow-2.yml", + ContentReader: strings.NewReader("name: workflow-2\non:\n push:\njobs:\n job-2:\n runs-on: ubuntu-latest\n steps:\n - run: echo helloworld\n"), + }, + }, + ) + defer f() + + t.Run("valid workflows", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // helpers + getWorkflowRunRedirectURI := func(workflow string) string { + req := NewRequest(t, "GET", fmt.Sprintf("%s/actions/workflows/%s/runs/latest", repo.HTMLURL(), workflow)) + resp := MakeRequest(t, req, http.StatusTemporaryRedirect) + + return resp.Header().Get("Location") + } + + // two runs have been created + assert.Equal(t, 2, unittest.GetCount(t, &actions_model.ActionRun{RepoID: repo.ID})) + + // Get the redirect URIs for both workflows + workflowOneURI := getWorkflowRunRedirectURI("workflow-1.yml") + workflowTwoURI := getWorkflowRunRedirectURI("workflow-2.yml") + + // Verify that the two are different. + assert.NotEqual(t, workflowOneURI, workflowTwoURI) + + // Verify that each points to the correct workflow. + workflowOne := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: repo.ID, Index: 1}) + err := workflowOne.LoadAttributes(context.Background()) + assert.NoError(t, err) + assert.Equal(t, workflowOneURI, workflowOne.HTMLURL()) + + workflowTwo := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: repo.ID, Index: 2}) + err = workflowTwo.LoadAttributes(context.Background()) + assert.NoError(t, err) + assert.Equal(t, workflowTwoURI, workflowTwo.HTMLURL()) + }) + + t.Run("existing workflow, non-existent branch", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("%s/actions/workflows/workflow-1.yml/runs/latest?branch=foobar", repo.HTMLURL())) + MakeRequest(t, req, http.StatusNotFound) + }) + + t.Run("non-existing workflow", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("%s/actions/workflows/workflow-3.yml/runs/latest", repo.HTMLURL())) + MakeRequest(t, req, http.StatusNotFound) + }) + }) +} + func TestActionsWebRouteLatestRun(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) @@ -44,7 +118,10 @@ func TestActionsWebRouteLatestRun(t *testing.T) { resp := MakeRequest(t, req, http.StatusTemporaryRedirect) // Verify that it redirects to the run we just created - expectedURI := fmt.Sprintf("%s/actions/runs/1", repo.HTMLURL()) - assert.Equal(t, expectedURI, resp.Header().Get("Location")) + workflow := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: repo.ID}) + err := workflow.LoadAttributes(context.Background()) + assert.NoError(t, err) + + assert.Equal(t, workflow.HTMLURL(), resp.Header().Get("Location")) }) }