diff --git a/models/actions/run.go b/models/actions/run.go
index 4656aa22a2..e84552682b 100644
--- a/models/actions/run.go
+++ b/models/actions/run.go
@@ -308,6 +308,17 @@ func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWork
 	return commiter.Commit()
 }
 
+func GetLatestRun(ctx context.Context, repoID int64) (*ActionRun, error) {
+	var run ActionRun
+	has, err := db.GetEngine(ctx).Where("repo_id=?", repoID).OrderBy("id DESC").Limit(1).Get(&run)
+	if err != nil {
+		return nil, err
+	} else if !has {
+		return nil, fmt.Errorf("latest run: %w", util.ErrNotExist)
+	}
+	return &run, nil
+}
+
 func GetRunByID(ctx context.Context, id int64) (*ActionRun, error) {
 	var run ActionRun
 	has, err := db.GetEngine(ctx).Where("id=?", id).Get(&run)
diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go
index 1cdae32a32..b6984567e5 100644
--- a/routers/web/repo/actions/view.go
+++ b/routers/web/repo/actions/view.go
@@ -46,6 +46,20 @@ func View(ctx *context_module.Context) {
 	ctx.HTML(http.StatusOK, tplViewActions)
 }
 
+func ViewLatest(ctx *context_module.Context) {
+	run, err := actions_model.GetLatestRun(ctx, ctx.Repo.Repository.ID)
+	if err != nil {
+		ctx.NotFound("GetLatestRun", err)
+		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 5172d33d03..69540d1d64 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -1345,22 +1345,25 @@ func registerRoutes(m *web.Route) {
 			m.Post("/disable", reqRepoAdmin, actions.DisableWorkflowFile)
 			m.Post("/enable", reqRepoAdmin, actions.EnableWorkflowFile)
 
-			m.Group("/runs/{run}", func() {
-				m.Combo("").
-					Get(actions.View).
-					Post(web.Bind(actions.ViewRequest{}), actions.ViewPost)
-				m.Group("/jobs/{job}", func() {
+			m.Group("/runs", func() {
+				m.Get("/latest", actions.ViewLatest)
+				m.Group("/{run}", func() {
 					m.Combo("").
 						Get(actions.View).
 						Post(web.Bind(actions.ViewRequest{}), actions.ViewPost)
+					m.Group("/jobs/{job}", func() {
+						m.Combo("").
+							Get(actions.View).
+							Post(web.Bind(actions.ViewRequest{}), actions.ViewPost)
+						m.Post("/rerun", reqRepoActionsWriter, actions.Rerun)
+						m.Get("/logs", actions.Logs)
+					})
+					m.Post("/cancel", reqRepoActionsWriter, actions.Cancel)
+					m.Post("/approve", reqRepoActionsWriter, actions.Approve)
+					m.Post("/artifacts", actions.ArtifactsView)
+					m.Get("/artifacts/{artifact_name}", actions.ArtifactsDownloadView)
 					m.Post("/rerun", reqRepoActionsWriter, actions.Rerun)
-					m.Get("/logs", actions.Logs)
 				})
-				m.Post("/cancel", reqRepoActionsWriter, actions.Cancel)
-				m.Post("/approve", reqRepoActionsWriter, actions.Approve)
-				m.Post("/artifacts", actions.ArtifactsView)
-				m.Get("/artifacts/{artifact_name}", actions.ArtifactsDownloadView)
-				m.Post("/rerun", reqRepoActionsWriter, actions.Rerun)
 			})
 		}, reqRepoActionsReader, actions.MustEnableActions)
 
diff --git a/tests/integration/actions_route_test.go b/tests/integration/actions_route_test.go
new file mode 100644
index 0000000000..b6ebacda8b
--- /dev/null
+++ b/tests/integration/actions_route_test.go
@@ -0,0 +1,91 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+	"fmt"
+	"net/http"
+	"net/url"
+	"strings"
+	"testing"
+	"time"
+
+	actions_model "code.gitea.io/gitea/models/actions"
+	"code.gitea.io/gitea/models/db"
+	repo_model "code.gitea.io/gitea/models/repo"
+	unit_model "code.gitea.io/gitea/models/unit"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/git"
+	repo_service "code.gitea.io/gitea/services/repository"
+	files_service "code.gitea.io/gitea/services/repository/files"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestActionsWebRouteLatestRun(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, err := repo_service.CreateRepository(db.DefaultContext, user2, user2, repo_service.CreateRepoOptions{
+			Name:          "actions-latest",
+			Description:   "test /actions/runs/latest",
+			AutoInit:      true,
+			Gitignores:    "Go",
+			License:       "MIT",
+			Readme:        "Default",
+			DefaultBranch: "main",
+			IsPrivate:     false,
+		})
+		assert.NoError(t, err)
+		assert.NotEmpty(t, repo)
+
+		// enable actions
+		err = repo_model.UpdateRepositoryUnits(db.DefaultContext, repo, []repo_model.RepoUnit{{
+			RepoID: repo.ID,
+			Type:   unit_model.TypeActions,
+		}}, nil)
+		assert.NoError(t, err)
+
+		// add workflow file to the repo
+		addWorkflowToBaseResp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
+			Files: []*files_service.ChangeRepoFile{
+				{
+					Operation:     "create",
+					TreePath:      ".gitea/workflows/pr.yml",
+					ContentReader: strings.NewReader("name: test\non:\n  push:\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - run: echo helloworld\n"),
+				},
+			},
+			Message:   "add workflow",
+			OldBranch: "main",
+			NewBranch: "main",
+			Author: &files_service.IdentityOptions{
+				Name:  user2.Name,
+				Email: user2.Email,
+			},
+			Committer: &files_service.IdentityOptions{
+				Name:  user2.Name,
+				Email: user2.Email,
+			},
+			Dates: &files_service.CommitDateOptions{
+				Author:    time.Now(),
+				Committer: time.Now(),
+			},
+		})
+		assert.NoError(t, err)
+		assert.NotEmpty(t, addWorkflowToBaseResp)
+
+		// a run has been created
+		assert.Equal(t, 1, unittest.GetCount(t, &actions_model.ActionRun{RepoID: repo.ID}))
+
+		// Hit the `/actions/runs/latest` route
+		req := NewRequest(t, "GET", fmt.Sprintf("%s/actions/runs/latest", repo.HTMLURL()))
+		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"))
+	})
+}