[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 <forgejo@gergo.csillger.hu>
This commit is contained in:
Gergely Nagy 2024-02-06 00:53:08 +01:00
parent cf1c57b681
commit 5f2d8be38e
No known key found for this signature in database
3 changed files with 112 additions and 3 deletions

View file

@ -22,6 +22,7 @@ import (
"code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/actions"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
context_module "code.gitea.io/gitea/modules/context" 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/storage"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
@ -60,6 +61,34 @@ func ViewLatest(ctx *context_module.Context) {
ctx.Redirect(run.HTMLURL(), http.StatusTemporaryRedirect) 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 { type ViewRequest struct {
LogCursors []struct { LogCursors []struct {
Step int `json:"step"` Step int `json:"step"`

View file

@ -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) }, reqRepoActionsReader, actions.MustEnableActions)
m.Group("/wiki", func() { m.Group("/wiki", func() {

View file

@ -1,9 +1,11 @@
// Copyright 2023 The Gitea Authors. All rights reserved. // 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 // SPDX-License-Identifier: MIT
package integration package integration
import ( import (
"context"
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
@ -15,10 +17,82 @@ import (
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
files_service "code.gitea.io/gitea/services/repository/files" files_service "code.gitea.io/gitea/services/repository/files"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert" "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) { func TestActionsWebRouteLatestRun(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) { onGiteaRun(t, func(t *testing.T, u *url.URL) {
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
@ -44,7 +118,10 @@ func TestActionsWebRouteLatestRun(t *testing.T) {
resp := MakeRequest(t, req, http.StatusTemporaryRedirect) resp := MakeRequest(t, req, http.StatusTemporaryRedirect)
// Verify that it redirects to the run we just created // Verify that it redirects to the run we just created
expectedURI := fmt.Sprintf("%s/actions/runs/1", repo.HTMLURL()) workflow := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: repo.ID})
assert.Equal(t, expectedURI, resp.Header().Get("Location")) err := workflow.LoadAttributes(context.Background())
assert.NoError(t, err)
assert.Equal(t, workflow.HTMLURL(), resp.Header().Get("Location"))
}) })
} }