From 5f2d8be38e9354b0feafd0246ed1ab8b6f74ecf7 Mon Sep 17 00:00:00 2001 From: Gergely Nagy Date: Tue, 6 Feb 2024 00:53:08 +0100 Subject: [PATCH] [FEAT]: New route to view lates run of specific workflows This adds a new route at `/actions/workflows/{workflow}/runs/latest`, which will redirect to the latest run of the given workflow. It can be further restricted by specifying an optional `?branch={branch}` query parameter. If no branch is specified, the route defaults to using the repo's default branch. This route is meant to go hand in hand with the Badge route that returns the result of the same workflow as a badge. This route can be used to link to the run that produced that result. Fixes #2303. Signed-off-by: Gergely Nagy --- routers/web/repo/actions/view.go | 29 +++++++++ routers/web/web.go | 5 +- tests/integration/actions_route_test.go | 81 ++++++++++++++++++++++++- 3 files changed, 112 insertions(+), 3 deletions(-) diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 63e3a352a5..757ff07c0d 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 1744ddb83a..e9f3f76c54 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 df22fc8647..c941fca2e5 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")) }) }