mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-11-24 08:02:18 +02:00
Replay pipeline using cli exec
by downloading metadata (#4103)
Co-authored-by: Anbraten <6918444+anbraten@users.noreply.github.com>
This commit is contained in:
parent
1a6c8dfec6
commit
fcc57dfc38
@ -37,6 +37,7 @@ import (
|
||||
"go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/kubernetes"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/local"
|
||||
backend_types "go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/types"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/metadata"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/compiler"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/linter"
|
||||
@ -76,6 +77,7 @@ func execDir(ctx context.Context, c *cli.Command, dir string) error {
|
||||
if runtime.GOOS == "windows" {
|
||||
repoPath = convertPathForWindows(repoPath)
|
||||
}
|
||||
// TODO: respect depends_on and do parallel runs with output to multiple _windows_ e.g. tmux like
|
||||
return filepath.Walk(dir, func(path string, info os.FileInfo, e error) error {
|
||||
if e != nil {
|
||||
return e
|
||||
@ -84,7 +86,7 @@ func execDir(ctx context.Context, c *cli.Command, dir string) error {
|
||||
// check if it is a regular file (not dir)
|
||||
if info.Mode().IsRegular() && (strings.HasSuffix(info.Name(), ".yaml") || strings.HasSuffix(info.Name(), ".yml")) {
|
||||
fmt.Println("#", info.Name())
|
||||
_ = runExec(ctx, c, path, repoPath) // TODO: should we drop errors or store them and report back?
|
||||
_ = runExec(ctx, c, path, repoPath, false) // TODO: should we drop errors or store them and report back?
|
||||
fmt.Println("")
|
||||
return nil
|
||||
}
|
||||
@ -103,10 +105,10 @@ func execFile(ctx context.Context, c *cli.Command, file string) error {
|
||||
if runtime.GOOS == "windows" {
|
||||
repoPath = convertPathForWindows(repoPath)
|
||||
}
|
||||
return runExec(ctx, c, file, repoPath)
|
||||
return runExec(ctx, c, file, repoPath, true)
|
||||
}
|
||||
|
||||
func runExec(ctx context.Context, c *cli.Command, file, repoPath string) error {
|
||||
func runExec(ctx context.Context, c *cli.Command, file, repoPath string, singleExec bool) error {
|
||||
dat, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -121,7 +123,7 @@ func runExec(ctx context.Context, c *cli.Command, file, repoPath string) error {
|
||||
axes = append(axes, matrix.Axis{})
|
||||
}
|
||||
for _, axis := range axes {
|
||||
err := execWithAxis(ctx, c, file, repoPath, axis)
|
||||
err := execWithAxis(ctx, c, file, repoPath, axis, singleExec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -129,11 +131,20 @@ func runExec(ctx context.Context, c *cli.Command, file, repoPath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func execWithAxis(ctx context.Context, c *cli.Command, file, repoPath string, axis matrix.Axis) error {
|
||||
metadata, err := metadataFromContext(ctx, c, axis)
|
||||
func execWithAxis(ctx context.Context, c *cli.Command, file, repoPath string, axis matrix.Axis, singleExec bool) error {
|
||||
var metadataWorkflow *metadata.Workflow
|
||||
if !singleExec {
|
||||
// TODO: proper try to use the engine to generate the same metadata for workflows
|
||||
// https://github.com/woodpecker-ci/woodpecker/pull/3967
|
||||
metadataWorkflow.Name = strings.TrimSuffix(strings.TrimSuffix(file, ".yaml"), ".yml")
|
||||
}
|
||||
metadata, err := metadataFromContext(ctx, c, axis, metadataWorkflow)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create metadata: %w", err)
|
||||
} else if metadata == nil {
|
||||
return fmt.Errorf("metadata is nil")
|
||||
}
|
||||
|
||||
environ := metadata.Environ()
|
||||
var secrets []compiler.Secret
|
||||
for key, val := range metadata.Workflow.Matrix {
|
||||
@ -239,7 +250,7 @@ func execWithAxis(ctx context.Context, c *cli.Command, file, repoPath string, ax
|
||||
c.String("netrc-password"),
|
||||
c.String("netrc-machine"),
|
||||
),
|
||||
compiler.WithMetadata(metadata),
|
||||
compiler.WithMetadata(*metadata),
|
||||
compiler.WithSecret(secrets...),
|
||||
compiler.WithEnviron(pipelineEnv),
|
||||
).Compile(conf)
|
||||
|
@ -32,6 +32,11 @@ var flags = []cli.Flag{
|
||||
Name: "repo-path",
|
||||
Usage: "path to local repository",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Sources: cli.EnvVars("WOODPECKER_METADATA_FILE"),
|
||||
Name: "metadata-file",
|
||||
Usage: "path to pipeline metadata file (normally downloaded from UI). Parameters can be adjusted by applying additional cli flags",
|
||||
},
|
||||
&cli.DurationFlag{
|
||||
Sources: cli.EnvVars("WOODPECKER_TIMEOUT"),
|
||||
Name: "timeout",
|
||||
|
@ -18,6 +18,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
@ -29,108 +30,131 @@ import (
|
||||
)
|
||||
|
||||
// return the metadata from the cli context.
|
||||
func metadataFromContext(_ context.Context, c *cli.Command, axis matrix.Axis) (metadata.Metadata, error) {
|
||||
func metadataFromContext(_ context.Context, c *cli.Command, axis matrix.Axis, w *metadata.Workflow) (*metadata.Metadata, error) {
|
||||
m := &metadata.Metadata{}
|
||||
|
||||
if c.IsSet("metadata-file") {
|
||||
metadataFile, err := os.Open(c.String("metadata-file"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer metadataFile.Close()
|
||||
|
||||
if err := json.NewDecoder(metadataFile).Decode(m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
platform := c.String("system-platform")
|
||||
if platform == "" {
|
||||
platform = runtime.GOOS + "/" + runtime.GOARCH
|
||||
}
|
||||
|
||||
fullRepoName := c.String("repo-name")
|
||||
repoOwner := ""
|
||||
repoName := ""
|
||||
if idx := strings.LastIndex(fullRepoName, "/"); idx != -1 {
|
||||
repoOwner = fullRepoName[:idx]
|
||||
repoName = fullRepoName[idx+1:]
|
||||
metadataFileAndOverrideOrDefault(c, "repo-name", func(fullRepoName string) {
|
||||
if idx := strings.LastIndex(fullRepoName, "/"); idx != -1 {
|
||||
m.Repo.Owner = fullRepoName[:idx]
|
||||
m.Repo.Name = fullRepoName[idx+1:]
|
||||
}
|
||||
}, c.String)
|
||||
|
||||
var err error
|
||||
metadataFileAndOverrideOrDefault(c, "pipeline-changed-files", func(changedFilesRaw string) {
|
||||
var changedFiles []string
|
||||
if len(changedFilesRaw) != 0 && changedFilesRaw[0] == '[' {
|
||||
if jsonErr := json.Unmarshal([]byte(changedFilesRaw), &changedFiles); jsonErr != nil {
|
||||
err = fmt.Errorf("pipeline-changed-files detected json but could not parse it: %w", jsonErr)
|
||||
}
|
||||
} else {
|
||||
for _, file := range strings.Split(changedFilesRaw, ",") {
|
||||
changedFiles = append(changedFiles, strings.TrimSpace(file))
|
||||
}
|
||||
}
|
||||
m.Curr.Commit.ChangedFiles = changedFiles
|
||||
}, c.String)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var changedFiles []string
|
||||
changedFilesRaw := c.String("pipeline-changed-files")
|
||||
if len(changedFilesRaw) != 0 && changedFilesRaw[0] == '[' {
|
||||
if err := json.Unmarshal([]byte(changedFilesRaw), &changedFiles); err != nil {
|
||||
return metadata.Metadata{}, fmt.Errorf("pipeline-changed-files detected json but could not parse it: %w", err)
|
||||
}
|
||||
} else {
|
||||
for _, file := range strings.Split(changedFilesRaw, ",") {
|
||||
changedFiles = append(changedFiles, strings.TrimSpace(file))
|
||||
}
|
||||
// Repo
|
||||
metadataFileAndOverrideOrDefault(c, "repo-remote-id", func(s string) { m.Repo.RemoteID = s }, c.String)
|
||||
metadataFileAndOverrideOrDefault(c, "repo-url", func(s string) { m.Repo.ForgeURL = s }, c.String)
|
||||
metadataFileAndOverrideOrDefault(c, "repo-scm", func(s string) { m.Repo.SCM = s }, c.String)
|
||||
metadataFileAndOverrideOrDefault(c, "repo-default-branch", func(s string) { m.Repo.Branch = s }, c.String)
|
||||
metadataFileAndOverrideOrDefault(c, "repo-clone-url", func(s string) { m.Repo.CloneURL = s }, c.String)
|
||||
metadataFileAndOverrideOrDefault(c, "repo-clone-ssh-url", func(s string) { m.Repo.CloneSSHURL = s }, c.String)
|
||||
metadataFileAndOverrideOrDefault(c, "repo-private", func(b bool) { m.Repo.Private = b }, c.Bool)
|
||||
metadataFileAndOverrideOrDefault(c, "repo-trusted", func(b bool) { m.Repo.Trusted = b }, c.Bool)
|
||||
|
||||
// Current Pipeline
|
||||
metadataFileAndOverrideOrDefault(c, "pipeline-number", func(i int64) { m.Curr.Number = i }, c.Int)
|
||||
metadataFileAndOverrideOrDefault(c, "pipeline-parent", func(i int64) { m.Curr.Parent = i }, c.Int)
|
||||
metadataFileAndOverrideOrDefault(c, "pipeline-created", func(i int64) { m.Curr.Created = i }, c.Int)
|
||||
metadataFileAndOverrideOrDefault(c, "pipeline-started", func(i int64) { m.Curr.Started = i }, c.Int)
|
||||
metadataFileAndOverrideOrDefault(c, "pipeline-finished", func(i int64) { m.Curr.Finished = i }, c.Int)
|
||||
metadataFileAndOverrideOrDefault(c, "pipeline-status", func(s string) { m.Curr.Status = s }, c.String)
|
||||
metadataFileAndOverrideOrDefault(c, "pipeline-event", func(s string) { m.Curr.Event = s }, c.String)
|
||||
metadataFileAndOverrideOrDefault(c, "pipeline-url", func(s string) { m.Curr.ForgeURL = s }, c.String)
|
||||
metadataFileAndOverrideOrDefault(c, "pipeline-deploy-to", func(s string) { m.Curr.DeployTo = s }, c.String)
|
||||
metadataFileAndOverrideOrDefault(c, "pipeline-deploy-task", func(s string) { m.Curr.DeployTask = s }, c.String)
|
||||
|
||||
// Current Pipeline Commit
|
||||
metadataFileAndOverrideOrDefault(c, "commit-sha", func(s string) { m.Curr.Commit.Sha = s }, c.String)
|
||||
metadataFileAndOverrideOrDefault(c, "commit-ref", func(s string) { m.Curr.Commit.Ref = s }, c.String)
|
||||
metadataFileAndOverrideOrDefault(c, "commit-refspec", func(s string) { m.Curr.Commit.Refspec = s }, c.String)
|
||||
metadataFileAndOverrideOrDefault(c, "commit-branch", func(s string) { m.Curr.Commit.Branch = s }, c.String)
|
||||
metadataFileAndOverrideOrDefault(c, "commit-message", func(s string) { m.Curr.Commit.Message = s }, c.String)
|
||||
metadataFileAndOverrideOrDefault(c, "commit-author-name", func(s string) { m.Curr.Commit.Author.Name = s }, c.String)
|
||||
metadataFileAndOverrideOrDefault(c, "commit-author-email", func(s string) { m.Curr.Commit.Author.Email = s }, c.String)
|
||||
metadataFileAndOverrideOrDefault(c, "commit-author-avatar", func(s string) { m.Curr.Commit.Author.Avatar = s }, c.String)
|
||||
|
||||
metadataFileAndOverrideOrDefault(c, "commit-pull-labels", func(sl []string) { m.Curr.Commit.PullRequestLabels = sl }, c.StringSlice)
|
||||
metadataFileAndOverrideOrDefault(c, "commit-release-is-pre", func(b bool) { m.Curr.Commit.IsPrerelease = b }, c.Bool)
|
||||
|
||||
// Previous Pipeline
|
||||
metadataFileAndOverrideOrDefault(c, "prev-pipeline-number", func(i int64) { m.Prev.Number = i }, c.Int)
|
||||
metadataFileAndOverrideOrDefault(c, "prev-pipeline-created", func(i int64) { m.Prev.Created = i }, c.Int)
|
||||
metadataFileAndOverrideOrDefault(c, "prev-pipeline-started", func(i int64) { m.Prev.Started = i }, c.Int)
|
||||
metadataFileAndOverrideOrDefault(c, "prev-pipeline-finished", func(i int64) { m.Prev.Finished = i }, c.Int)
|
||||
metadataFileAndOverrideOrDefault(c, "prev-pipeline-status", func(s string) { m.Prev.Status = s }, c.String)
|
||||
metadataFileAndOverrideOrDefault(c, "prev-pipeline-event", func(s string) { m.Prev.Event = s }, c.String)
|
||||
metadataFileAndOverrideOrDefault(c, "prev-pipeline-url", func(s string) { m.Prev.ForgeURL = s }, c.String)
|
||||
|
||||
// Previous Pipeline Commit
|
||||
metadataFileAndOverrideOrDefault(c, "prev-commit-sha", func(s string) { m.Prev.Commit.Sha = s }, c.String)
|
||||
metadataFileAndOverrideOrDefault(c, "prev-commit-ref", func(s string) { m.Prev.Commit.Ref = s }, c.String)
|
||||
metadataFileAndOverrideOrDefault(c, "prev-commit-refspec", func(s string) { m.Prev.Commit.Refspec = s }, c.String)
|
||||
metadataFileAndOverrideOrDefault(c, "prev-commit-branch", func(s string) { m.Prev.Commit.Branch = s }, c.String)
|
||||
metadataFileAndOverrideOrDefault(c, "prev-commit-message", func(s string) { m.Prev.Commit.Message = s }, c.String)
|
||||
metadataFileAndOverrideOrDefault(c, "prev-commit-author-name", func(s string) { m.Prev.Commit.Author.Name = s }, c.String)
|
||||
metadataFileAndOverrideOrDefault(c, "prev-commit-author-email", func(s string) { m.Prev.Commit.Author.Email = s }, c.String)
|
||||
metadataFileAndOverrideOrDefault(c, "prev-commit-author-avatar", func(s string) { m.Prev.Commit.Author.Avatar = s }, c.String)
|
||||
|
||||
// Workflow
|
||||
metadataFileAndOverrideOrDefault(c, "workflow-name", func(s string) { m.Workflow.Name = s }, c.String)
|
||||
metadataFileAndOverrideOrDefault(c, "workflow-number", func(i int64) { m.Workflow.Number = int(i) }, c.Int)
|
||||
m.Workflow.Matrix = axis
|
||||
|
||||
// System
|
||||
metadataFileAndOverrideOrDefault(c, "system-name", func(s string) { m.Sys.Name = s }, c.String)
|
||||
metadataFileAndOverrideOrDefault(c, "system-url", func(s string) { m.Sys.URL = s }, c.String)
|
||||
metadataFileAndOverrideOrDefault(c, "system-host", func(s string) { m.Sys.Host = s }, c.String)
|
||||
m.Sys.Platform = platform
|
||||
m.Sys.Version = version.Version
|
||||
|
||||
// Forge
|
||||
metadataFileAndOverrideOrDefault(c, "forge-type", func(s string) { m.Forge.Type = s }, c.String)
|
||||
metadataFileAndOverrideOrDefault(c, "forge-url", func(s string) { m.Forge.URL = s }, c.String)
|
||||
|
||||
if w != nil {
|
||||
m.Workflow = *w
|
||||
}
|
||||
|
||||
return metadata.Metadata{
|
||||
Repo: metadata.Repo{
|
||||
Name: repoName,
|
||||
Owner: repoOwner,
|
||||
RemoteID: c.String("repo-remote-id"),
|
||||
ForgeURL: c.String("repo-url"),
|
||||
SCM: c.String("repo-scm"),
|
||||
Branch: c.String("repo-default-branch"),
|
||||
CloneURL: c.String("repo-clone-url"),
|
||||
CloneSSHURL: c.String("repo-clone-ssh-url"),
|
||||
Private: c.Bool("repo-private"),
|
||||
Trusted: c.Bool("repo-trusted"),
|
||||
},
|
||||
Curr: metadata.Pipeline{
|
||||
Number: c.Int("pipeline-number"),
|
||||
Parent: c.Int("pipeline-parent"),
|
||||
Created: c.Int("pipeline-created"),
|
||||
Started: c.Int("pipeline-started"),
|
||||
Finished: c.Int("pipeline-finished"),
|
||||
Status: c.String("pipeline-status"),
|
||||
Event: c.String("pipeline-event"),
|
||||
ForgeURL: c.String("pipeline-url"),
|
||||
DeployTo: c.String("pipeline-deploy-to"),
|
||||
DeployTask: c.String("pipeline-deploy-task"),
|
||||
Commit: metadata.Commit{
|
||||
Sha: c.String("commit-sha"),
|
||||
Ref: c.String("commit-ref"),
|
||||
Refspec: c.String("commit-refspec"),
|
||||
Branch: c.String("commit-branch"),
|
||||
Message: c.String("commit-message"),
|
||||
Author: metadata.Author{
|
||||
Name: c.String("commit-author-name"),
|
||||
Email: c.String("commit-author-email"),
|
||||
Avatar: c.String("commit-author-avatar"),
|
||||
},
|
||||
PullRequestLabels: c.StringSlice("commit-pull-labels"),
|
||||
IsPrerelease: c.Bool("commit-release-is-pre"),
|
||||
ChangedFiles: changedFiles,
|
||||
},
|
||||
},
|
||||
Prev: metadata.Pipeline{
|
||||
Number: c.Int("prev-pipeline-number"),
|
||||
Created: c.Int("prev-pipeline-created"),
|
||||
Started: c.Int("prev-pipeline-started"),
|
||||
Finished: c.Int("prev-pipeline-finished"),
|
||||
Status: c.String("prev-pipeline-status"),
|
||||
Event: c.String("prev-pipeline-event"),
|
||||
ForgeURL: c.String("prev-pipeline-url"),
|
||||
Commit: metadata.Commit{
|
||||
Sha: c.String("prev-commit-sha"),
|
||||
Ref: c.String("prev-commit-ref"),
|
||||
Refspec: c.String("prev-commit-refspec"),
|
||||
Branch: c.String("prev-commit-branch"),
|
||||
Message: c.String("prev-commit-message"),
|
||||
Author: metadata.Author{
|
||||
Name: c.String("prev-commit-author-name"),
|
||||
Email: c.String("prev-commit-author-email"),
|
||||
Avatar: c.String("prev-commit-author-avatar"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Workflow: metadata.Workflow{
|
||||
Name: c.String("workflow-name"),
|
||||
Number: int(c.Int("workflow-number")),
|
||||
Matrix: axis,
|
||||
},
|
||||
Sys: metadata.System{
|
||||
Name: c.String("system-name"),
|
||||
URL: c.String("system-url"),
|
||||
Host: c.String("system-host"),
|
||||
Platform: platform,
|
||||
Version: version.Version,
|
||||
},
|
||||
Forge: metadata.Forge{
|
||||
Type: c.String("forge-type"),
|
||||
URL: c.String("forge-url"),
|
||||
},
|
||||
}, nil
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// metadataFileAndOverrideOrDefault will either use the flag default or if metadata file is set only overload if explicit set.
|
||||
func metadataFileAndOverrideOrDefault[T any](c *cli.Command, flag string, setter func(T), getter func(string) T) {
|
||||
if !c.IsSet("metadata-file") || c.IsSet(flag) {
|
||||
setter(getter(flag))
|
||||
}
|
||||
}
|
||||
|
142
cli/exec/metadata_test.go
Normal file
142
cli/exec/metadata_test.go
Normal file
@ -0,0 +1,142 @@
|
||||
// Copyright 2024 Woodpecker Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package exec
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/urfave/cli/v3"
|
||||
|
||||
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/metadata"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/matrix"
|
||||
)
|
||||
|
||||
func TestMetadataFromContext(t *testing.T) {
|
||||
sampleMetadata := &metadata.Metadata{
|
||||
Repo: metadata.Repo{Owner: "test-user", Name: "test-repo"},
|
||||
Curr: metadata.Pipeline{Number: 5},
|
||||
}
|
||||
|
||||
runCommand := func(flags []cli.Flag, fn func(c *cli.Command)) {
|
||||
c := &cli.Command{
|
||||
Flags: flags,
|
||||
Action: func(_ context.Context, c *cli.Command) error {
|
||||
fn(c)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
assert.NoError(t, c.Run(context.Background(), []string{"woodpecker-cli"}))
|
||||
}
|
||||
|
||||
t.Run("LoadFromFile", func(t *testing.T) {
|
||||
tempFileName := createTempFile(t, sampleMetadata)
|
||||
|
||||
flags := []cli.Flag{
|
||||
&cli.StringFlag{Name: "metadata-file"},
|
||||
}
|
||||
|
||||
runCommand(flags, func(c *cli.Command) {
|
||||
_ = c.Set("metadata-file", tempFileName)
|
||||
|
||||
m, err := metadataFromContext(context.Background(), c, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "test-repo", m.Repo.Name)
|
||||
assert.Equal(t, int64(5), m.Curr.Number)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("OverrideFromFlags", func(t *testing.T) {
|
||||
tempFileName := createTempFile(t, sampleMetadata)
|
||||
|
||||
flags := []cli.Flag{
|
||||
&cli.StringFlag{Name: "metadata-file"},
|
||||
&cli.StringFlag{Name: "repo-name"},
|
||||
&cli.IntFlag{Name: "pipeline-number"},
|
||||
}
|
||||
|
||||
runCommand(flags, func(c *cli.Command) {
|
||||
_ = c.Set("metadata-file", tempFileName)
|
||||
_ = c.Set("repo-name", "aUser/override-repo")
|
||||
_ = c.Set("pipeline-number", "10")
|
||||
|
||||
m, err := metadataFromContext(context.Background(), c, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "override-repo", m.Repo.Name)
|
||||
assert.Equal(t, int64(10), m.Curr.Number)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("InvalidFile", func(t *testing.T) {
|
||||
tempFile, err := os.CreateTemp("", "invalid.json")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { os.Remove(tempFile.Name()) })
|
||||
|
||||
_, err = tempFile.Write([]byte("invalid json"))
|
||||
require.NoError(t, err)
|
||||
|
||||
flags := []cli.Flag{
|
||||
&cli.StringFlag{Name: "metadata-file"},
|
||||
}
|
||||
|
||||
runCommand(flags, func(c *cli.Command) {
|
||||
_ = c.Set("metadata-file", tempFile.Name())
|
||||
|
||||
_, err = metadataFromContext(context.Background(), c, nil, nil)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("DefaultValues", func(t *testing.T) {
|
||||
flags := []cli.Flag{
|
||||
&cli.StringFlag{Name: "repo-name", Value: "test/default-repo"},
|
||||
&cli.IntFlag{Name: "pipeline-number", Value: 1},
|
||||
}
|
||||
|
||||
runCommand(flags, func(c *cli.Command) {
|
||||
m, err := metadataFromContext(context.Background(), c, nil, nil)
|
||||
require.NoError(t, err)
|
||||
if assert.NotNil(t, m) {
|
||||
assert.Equal(t, "test", m.Repo.Owner)
|
||||
assert.Equal(t, "default-repo", m.Repo.Name)
|
||||
assert.Equal(t, int64(1), m.Curr.Number)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("MatrixAxis", func(t *testing.T) {
|
||||
runCommand([]cli.Flag{}, func(c *cli.Command) {
|
||||
axis := matrix.Axis{"go": "1.16", "os": "linux"}
|
||||
m, err := metadataFromContext(context.Background(), c, axis, nil)
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, map[string]string{"go": "1.16", "os": "linux"}, m.Workflow.Matrix)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func createTempFile(t *testing.T, content any) string {
|
||||
t.Helper()
|
||||
tempFile, err := os.CreateTemp("", "metadata.json")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { os.Remove(tempFile.Name()) })
|
||||
|
||||
err = json.NewEncoder(tempFile).Encode(content)
|
||||
require.NoError(t, err)
|
||||
return tempFile.Name()
|
||||
}
|
@ -3144,6 +3144,49 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/repos/{repo_id}/pipelines/{number}/metadata": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Pipelines"
|
||||
],
|
||||
"summary": "Get metadata for a pipeline or a specific workflow, including previous pipeline info",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"default": "Bearer \u003cpersonal access token\u003e",
|
||||
"description": "Insert your personal access token",
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "the repository id",
|
||||
"name": "repo_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "the number of the pipeline",
|
||||
"name": "number",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/metadata.Metadata"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/repos/{repo_id}/pull_requests": {
|
||||
"get": {
|
||||
"produces": [
|
||||
@ -5148,6 +5191,225 @@ const docTemplate = `{
|
||||
"EventManual"
|
||||
]
|
||||
},
|
||||
"metadata.Author": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"avatar": {
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"metadata.Commit": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"author": {
|
||||
"$ref": "#/definitions/metadata.Author"
|
||||
},
|
||||
"branch": {
|
||||
"type": "string"
|
||||
},
|
||||
"changed_files": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"is_prerelease": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"labels": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"ref": {
|
||||
"type": "string"
|
||||
},
|
||||
"refspec": {
|
||||
"type": "string"
|
||||
},
|
||||
"sha": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"metadata.Forge": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"metadata.Metadata": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"curr": {
|
||||
"$ref": "#/definitions/metadata.Pipeline"
|
||||
},
|
||||
"forge": {
|
||||
"$ref": "#/definitions/metadata.Forge"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"prev": {
|
||||
"$ref": "#/definitions/metadata.Pipeline"
|
||||
},
|
||||
"repo": {
|
||||
"$ref": "#/definitions/metadata.Repo"
|
||||
},
|
||||
"step": {
|
||||
"$ref": "#/definitions/metadata.Step"
|
||||
},
|
||||
"sys": {
|
||||
"$ref": "#/definitions/metadata.System"
|
||||
},
|
||||
"workflow": {
|
||||
"$ref": "#/definitions/metadata.Workflow"
|
||||
}
|
||||
}
|
||||
},
|
||||
"metadata.Pipeline": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"commit": {
|
||||
"$ref": "#/definitions/metadata.Commit"
|
||||
},
|
||||
"created": {
|
||||
"type": "integer"
|
||||
},
|
||||
"cron": {
|
||||
"type": "string"
|
||||
},
|
||||
"event": {
|
||||
"type": "string"
|
||||
},
|
||||
"finished": {
|
||||
"type": "integer"
|
||||
},
|
||||
"forge_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"number": {
|
||||
"type": "integer"
|
||||
},
|
||||
"parent": {
|
||||
"type": "integer"
|
||||
},
|
||||
"started": {
|
||||
"type": "integer"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"target": {
|
||||
"type": "string"
|
||||
},
|
||||
"task": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"metadata.Repo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"clone_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"clone_url_ssh": {
|
||||
"type": "string"
|
||||
},
|
||||
"default_branch": {
|
||||
"type": "string"
|
||||
},
|
||||
"forge_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"private": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"remote_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"scm": {
|
||||
"type": "string"
|
||||
},
|
||||
"trusted": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"metadata.Step": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"number": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"metadata.System": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"arch": {
|
||||
"type": "string"
|
||||
},
|
||||
"host": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"metadata.Workflow": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"matrix": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"number": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"model.ForgeType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
@ -30,8 +30,10 @@ import (
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/model"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/pipeline"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/pipeline/stepbuilder"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/router/middleware/session"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/store"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/store/types"
|
||||
)
|
||||
|
||||
// CreatePipeline
|
||||
@ -392,6 +394,47 @@ func GetPipelineConfig(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, configs)
|
||||
}
|
||||
|
||||
// GetPipelineMetadata
|
||||
//
|
||||
// @Summary Get metadata for a pipeline or a specific workflow, including previous pipeline info
|
||||
// @Router /repos/{repo_id}/pipelines/{number}/metadata [get]
|
||||
// @Produce json
|
||||
// @Success 200 {object} metadata.Metadata
|
||||
// @Tags Pipelines
|
||||
// @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>)
|
||||
// @Param repo_id path int true "the repository id"
|
||||
// @Param number path int true "the number of the pipeline"
|
||||
func GetPipelineMetadata(c *gin.Context) {
|
||||
repo := session.Repo(c)
|
||||
num, err := strconv.ParseInt(c.Param("number"), 10, 64)
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
_store := store.FromContext(c)
|
||||
currentPipeline, err := _store.GetPipelineNumber(repo, num)
|
||||
if err != nil {
|
||||
handleDBError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
forge, err := server.Config.Services.Manager.ForgeFromRepo(repo)
|
||||
if err != nil {
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
prevPipeline, err := _store.GetPipelineLastBefore(repo, currentPipeline.Branch, currentPipeline.ID)
|
||||
if err != nil && !errors.Is(err, types.RecordNotExist) {
|
||||
handleDBError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
metadata := stepbuilder.MetadataFromStruct(forge, repo, currentPipeline, prevPipeline, nil, server.Config.Server.Host)
|
||||
c.JSON(http.StatusOK, metadata)
|
||||
}
|
||||
|
||||
// CancelPipeline
|
||||
//
|
||||
// @Summary Cancel a pipeline
|
||||
|
@ -1,129 +1,217 @@
|
||||
// Copyright 2024 Woodpecker Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/franela/goblin"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/metadata"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server"
|
||||
forge_mocks "go.woodpecker-ci.org/woodpecker/v2/server/forge/mocks"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/model"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/store/mocks"
|
||||
mocks_manager "go.woodpecker-ci.org/woodpecker/v2/server/services/mocks"
|
||||
store_mocks "go.woodpecker-ci.org/woodpecker/v2/server/store/mocks"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/store/types"
|
||||
)
|
||||
|
||||
var fakePipeline = &model.Pipeline{
|
||||
ID: 2,
|
||||
Number: 2,
|
||||
Status: model.StatusSuccess,
|
||||
}
|
||||
|
||||
func TestGetPipelines(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
g := goblin.Goblin(t)
|
||||
g.Describe("Pipeline", func() {
|
||||
g.It("should get pipelines", func() {
|
||||
pipelines := []*model.Pipeline{fakePipeline}
|
||||
t.Run("should get pipelines", func(t *testing.T) {
|
||||
pipelines := []*model.Pipeline{fakePipeline}
|
||||
|
||||
mockStore := mocks.NewStore(t)
|
||||
mockStore.On("GetPipelineList", mock.Anything, mock.Anything, mock.Anything).Return(pipelines, nil)
|
||||
mockStore := store_mocks.NewStore(t)
|
||||
mockStore.On("GetPipelineList", mock.Anything, mock.Anything, mock.Anything).Return(pipelines, nil)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Set("store", mockStore)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Set("store", mockStore)
|
||||
|
||||
GetPipelines(c)
|
||||
GetPipelines(c)
|
||||
|
||||
mockStore.AssertCalled(t, "GetPipelineList", mock.Anything, mock.Anything, mock.Anything)
|
||||
assert.Equal(t, http.StatusOK, c.Writer.Status())
|
||||
})
|
||||
mockStore.AssertCalled(t, "GetPipelineList", mock.Anything, mock.Anything, mock.Anything)
|
||||
assert.Equal(t, http.StatusOK, c.Writer.Status())
|
||||
})
|
||||
|
||||
g.It("should not parse pipeline filter", func() {
|
||||
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
c.Request, _ = http.NewRequest(http.MethodDelete, "/?before=2023-01-16&after=2023-01-15", nil)
|
||||
t.Run("should not parse pipeline filter", func(t *testing.T) {
|
||||
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
c.Request, _ = http.NewRequest(http.MethodDelete, "/?before=2023-01-16&after=2023-01-15", nil)
|
||||
|
||||
GetPipelines(c)
|
||||
GetPipelines(c)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, c.Writer.Status())
|
||||
})
|
||||
assert.Equal(t, http.StatusBadRequest, c.Writer.Status())
|
||||
})
|
||||
|
||||
g.It("should parse pipeline filter", func() {
|
||||
pipelines := []*model.Pipeline{fakePipeline}
|
||||
t.Run("should parse pipeline filter", func(t *testing.T) {
|
||||
pipelines := []*model.Pipeline{fakePipeline}
|
||||
|
||||
mockStore := mocks.NewStore(t)
|
||||
mockStore.On("GetPipelineList", mock.Anything, mock.Anything, mock.Anything).Return(pipelines, nil)
|
||||
mockStore := store_mocks.NewStore(t)
|
||||
mockStore.On("GetPipelineList", mock.Anything, mock.Anything, mock.Anything).Return(pipelines, nil)
|
||||
|
||||
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
c.Set("store", mockStore)
|
||||
c.Request, _ = http.NewRequest(http.MethodDelete, "/?2023-01-16T15:00:00Z&after=2023-01-15T15:00:00Z", nil)
|
||||
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
c.Set("store", mockStore)
|
||||
c.Request, _ = http.NewRequest(http.MethodDelete, "/?2023-01-16T15:00:00Z&after=2023-01-15T15:00:00Z", nil)
|
||||
|
||||
GetPipelines(c)
|
||||
GetPipelines(c)
|
||||
|
||||
assert.Equal(t, http.StatusOK, c.Writer.Status())
|
||||
})
|
||||
assert.Equal(t, http.StatusOK, c.Writer.Status())
|
||||
})
|
||||
|
||||
g.It("should parse pipeline filter with tz offset", func() {
|
||||
pipelines := []*model.Pipeline{fakePipeline}
|
||||
t.Run("should parse pipeline filter with tz offset", func(t *testing.T) {
|
||||
pipelines := []*model.Pipeline{fakePipeline}
|
||||
|
||||
mockStore := mocks.NewStore(t)
|
||||
mockStore.On("GetPipelineList", mock.Anything, mock.Anything, mock.Anything).Return(pipelines, nil)
|
||||
mockStore := store_mocks.NewStore(t)
|
||||
mockStore.On("GetPipelineList", mock.Anything, mock.Anything, mock.Anything).Return(pipelines, nil)
|
||||
|
||||
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
c.Set("store", mockStore)
|
||||
c.Request, _ = http.NewRequest(http.MethodDelete, "/?before=2023-01-16T15:00:00%2B01:00&after=2023-01-15T15:00:00%2B01:00", nil)
|
||||
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
c.Set("store", mockStore)
|
||||
c.Request, _ = http.NewRequest(http.MethodDelete, "/?before=2023-01-16T15:00:00%2B01:00&after=2023-01-15T15:00:00%2B01:00", nil)
|
||||
|
||||
GetPipelines(c)
|
||||
GetPipelines(c)
|
||||
|
||||
assert.Equal(t, http.StatusOK, c.Writer.Status())
|
||||
})
|
||||
assert.Equal(t, http.StatusOK, c.Writer.Status())
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeletePipeline(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
g := goblin.Goblin(t)
|
||||
g.Describe("Pipeline", func() {
|
||||
g.It("should delete pipeline", func() {
|
||||
mockStore := mocks.NewStore(t)
|
||||
mockStore.On("GetPipelineNumber", mock.Anything, mock.Anything).Return(fakePipeline, nil)
|
||||
mockStore.On("DeletePipeline", mock.Anything).Return(nil)
|
||||
t.Run("should delete pipeline", func(t *testing.T) {
|
||||
mockStore := store_mocks.NewStore(t)
|
||||
mockStore.On("GetPipelineNumber", mock.Anything, mock.Anything).Return(fakePipeline, nil)
|
||||
mockStore.On("DeletePipeline", mock.Anything).Return(nil)
|
||||
|
||||
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
c.Set("store", mockStore)
|
||||
c.Params = gin.Params{{Key: "number", Value: "2"}}
|
||||
|
||||
DeletePipeline(c)
|
||||
|
||||
mockStore.AssertCalled(t, "GetPipelineNumber", mock.Anything, mock.Anything)
|
||||
mockStore.AssertCalled(t, "DeletePipeline", mock.Anything)
|
||||
assert.Equal(t, http.StatusNoContent, c.Writer.Status())
|
||||
})
|
||||
|
||||
t.Run("should not delete without pipeline number", func(t *testing.T) {
|
||||
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
|
||||
DeletePipeline(c)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, c.Writer.Status())
|
||||
})
|
||||
|
||||
t.Run("should not delete pending", func(t *testing.T) {
|
||||
fakePipeline := *fakePipeline
|
||||
fakePipeline.Status = model.StatusPending
|
||||
|
||||
mockStore := store_mocks.NewStore(t)
|
||||
mockStore.On("GetPipelineNumber", mock.Anything, mock.Anything).Return(&fakePipeline, nil)
|
||||
|
||||
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
c.Set("store", mockStore)
|
||||
c.Params = gin.Params{{Key: "number", Value: "2"}}
|
||||
|
||||
DeletePipeline(c)
|
||||
|
||||
mockStore.AssertCalled(t, "GetPipelineNumber", mock.Anything, mock.Anything)
|
||||
mockStore.AssertNotCalled(t, "DeletePipeline", mock.Anything)
|
||||
assert.Equal(t, http.StatusUnprocessableEntity, c.Writer.Status())
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetPipelineMetadata(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
prevPipeline := &model.Pipeline{
|
||||
ID: 1,
|
||||
Number: 1,
|
||||
Status: model.StatusFailure,
|
||||
}
|
||||
|
||||
fakeRepo := &model.Repo{ID: 1}
|
||||
|
||||
mockForge := forge_mocks.NewForge(t)
|
||||
mockForge.On("Name").Return("mock")
|
||||
mockForge.On("URL").Return("https://codeberg.org")
|
||||
|
||||
mockManager := mocks_manager.NewManager(t)
|
||||
mockManager.On("ForgeFromRepo", fakeRepo).Return(mockForge, nil)
|
||||
server.Config.Services.Manager = mockManager
|
||||
|
||||
mockStore := store_mocks.NewStore(t)
|
||||
mockStore.On("GetPipelineNumber", mock.Anything, int64(2)).Return(fakePipeline, nil)
|
||||
mockStore.On("GetPipelineLastBefore", mock.Anything, mock.Anything, int64(2)).Return(prevPipeline, nil)
|
||||
|
||||
t.Run("PipelineMetadata", func(t *testing.T) {
|
||||
t.Run("should get pipeline metadata", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "number", Value: "2"}}
|
||||
c.Set("store", mockStore)
|
||||
c.Params = gin.Params{{Key: "number", Value: "1"}}
|
||||
c.Set("forge", mockForge)
|
||||
c.Set("repo", fakeRepo)
|
||||
|
||||
DeletePipeline(c)
|
||||
GetPipelineMetadata(c)
|
||||
|
||||
mockStore.AssertCalled(t, "GetPipelineNumber", mock.Anything, mock.Anything)
|
||||
mockStore.AssertCalled(t, "DeletePipeline", mock.Anything)
|
||||
assert.Equal(t, http.StatusNoContent, c.Writer.Status())
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var response metadata.Metadata
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, int64(1), response.Repo.ID)
|
||||
assert.Equal(t, int64(2), response.Curr.Number)
|
||||
assert.Equal(t, int64(1), response.Prev.Number)
|
||||
})
|
||||
|
||||
g.It("should not delete without pipeline number", func() {
|
||||
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
t.Run("should return bad request for invalid pipeline number", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "number", Value: "invalid"}}
|
||||
|
||||
DeletePipeline(c)
|
||||
GetPipelineMetadata(c)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, c.Writer.Status())
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
})
|
||||
|
||||
g.It("should not delete pending", func() {
|
||||
fakePipeline.Status = model.StatusPending
|
||||
t.Run("should return not found for non-existent pipeline", func(t *testing.T) {
|
||||
mockStore := store_mocks.NewStore(t)
|
||||
mockStore.On("GetPipelineNumber", mock.Anything, int64(3)).Return((*model.Pipeline)(nil), types.RecordNotExist)
|
||||
|
||||
mockStore := mocks.NewStore(t)
|
||||
mockStore.On("GetPipelineNumber", mock.Anything, mock.Anything).Return(fakePipeline, nil)
|
||||
|
||||
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "number", Value: "3"}}
|
||||
c.Set("store", mockStore)
|
||||
c.Params = gin.Params{{Key: "number", Value: "1"}}
|
||||
c.Set("repo", fakeRepo)
|
||||
|
||||
DeletePipeline(c)
|
||||
GetPipelineMetadata(c)
|
||||
|
||||
mockStore.AssertCalled(t, "GetPipelineNumber", mock.Anything, mock.Anything)
|
||||
mockStore.AssertNotCalled(t, "DeletePipeline", mock.Anything)
|
||||
assert.Equal(t, http.StatusUnprocessableEntity, c.Writer.Status())
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ func parsePipeline(forge forge.Forge, store store.Store, currentPipeline *model.
|
||||
}
|
||||
|
||||
// get the previous pipeline so that we can send status change notifications
|
||||
last, err := store.GetPipelineLastBefore(repo, currentPipeline.Branch, currentPipeline.ID)
|
||||
prev, err := store.GetPipelineLastBefore(repo, currentPipeline.Branch, currentPipeline.ID)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
log.Error().Err(err).Str("repo", repo.FullName).Msgf("error getting last pipeline before pipeline number '%d'", currentPipeline.Number)
|
||||
}
|
||||
@ -74,7 +74,7 @@ func parsePipeline(forge forge.Forge, store store.Store, currentPipeline *model.
|
||||
b := stepbuilder.StepBuilder{
|
||||
Repo: repo,
|
||||
Curr: currentPipeline,
|
||||
Last: last,
|
||||
Prev: prev,
|
||||
Netrc: netrc,
|
||||
Secs: secs,
|
||||
Regs: regs,
|
||||
|
@ -25,7 +25,7 @@ import (
|
||||
)
|
||||
|
||||
// MetadataFromStruct return the metadata from a pipeline will run with.
|
||||
func MetadataFromStruct(forge metadata.ServerForge, repo *model.Repo, pipeline, last *model.Pipeline, workflow *model.Workflow, sysURL string) metadata.Metadata {
|
||||
func MetadataFromStruct(forge metadata.ServerForge, repo *model.Repo, pipeline, prev *model.Pipeline, workflow *model.Workflow, sysURL string) metadata.Metadata {
|
||||
host := sysURL
|
||||
uri, err := url.Parse(sysURL)
|
||||
if err == nil {
|
||||
@ -78,7 +78,7 @@ func MetadataFromStruct(forge metadata.ServerForge, repo *model.Repo, pipeline,
|
||||
return metadata.Metadata{
|
||||
Repo: fRepo,
|
||||
Curr: metadataPipelineFromModelPipeline(pipeline, true),
|
||||
Prev: metadataPipelineFromModelPipeline(last, false),
|
||||
Prev: metadataPipelineFromModelPipeline(prev, false),
|
||||
Workflow: fWorkflow,
|
||||
Step: metadata.Step{},
|
||||
Sys: metadata.System{
|
||||
|
@ -33,7 +33,7 @@ func TestMetadataFromStruct(t *testing.T) {
|
||||
name string
|
||||
forge metadata.ServerForge
|
||||
repo *model.Repo
|
||||
pipeline, last *model.Pipeline
|
||||
pipeline, prev *model.Pipeline
|
||||
workflow *model.Workflow
|
||||
sysURL string
|
||||
expectedMetadata metadata.Metadata
|
||||
@ -63,7 +63,7 @@ func TestMetadataFromStruct(t *testing.T) {
|
||||
forge: forge,
|
||||
repo: &model.Repo{FullName: "testUser/testRepo", ForgeURL: "https://gitea.com/testUser/testRepo", Clone: "https://gitea.com/testUser/testRepo.git", CloneSSH: "git@gitea.com:testUser/testRepo.git", Branch: "main", IsSCMPrivate: true, SCMKind: "git"},
|
||||
pipeline: &model.Pipeline{Number: 3, ChangedFiles: []string{"test.go", "markdown file.md"}},
|
||||
last: &model.Pipeline{Number: 2},
|
||||
prev: &model.Pipeline{Number: 2},
|
||||
workflow: &model.Workflow{Name: "hello"},
|
||||
sysURL: "https://example.com",
|
||||
expectedMetadata: metadata.Metadata{
|
||||
@ -98,7 +98,7 @@ func TestMetadataFromStruct(t *testing.T) {
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
result := MetadataFromStruct(testCase.forge, testCase.repo, testCase.pipeline, testCase.last, testCase.workflow, testCase.sysURL)
|
||||
result := MetadataFromStruct(testCase.forge, testCase.repo, testCase.pipeline, testCase.prev, testCase.workflow, testCase.sysURL)
|
||||
assert.EqualValues(t, testCase.expectedMetadata, result)
|
||||
assert.EqualValues(t, testCase.expectedEnviron, result.Environ())
|
||||
})
|
||||
|
@ -42,7 +42,7 @@ import (
|
||||
type StepBuilder struct {
|
||||
Repo *model.Repo
|
||||
Curr *model.Pipeline
|
||||
Last *model.Pipeline
|
||||
Prev *model.Pipeline
|
||||
Netrc *model.Netrc
|
||||
Secs []*model.Secret
|
||||
Regs []*model.Registry
|
||||
@ -115,7 +115,7 @@ func (b *StepBuilder) Build() (items []*Item, errorsAndWarnings error) {
|
||||
}
|
||||
|
||||
func (b *StepBuilder) genItemForWorkflow(workflow *model.Workflow, axis matrix.Axis, data string) (item *Item, errorsAndWarnings error) {
|
||||
workflowMetadata := MetadataFromStruct(b.Forge, b.Repo, b.Curr, b.Last, workflow, b.Host)
|
||||
workflowMetadata := MetadataFromStruct(b.Forge, b.Repo, b.Curr, b.Prev, workflow, b.Host)
|
||||
environ := b.environmentVariables(workflowMetadata, axis)
|
||||
|
||||
// add global environment variables for substituting
|
||||
|
@ -42,7 +42,7 @@ func TestGlobalEnvsubst(t *testing.T) {
|
||||
Message: "aaa",
|
||||
Event: model.EventPush,
|
||||
},
|
||||
Last: &model.Pipeline{},
|
||||
Prev: &model.Pipeline{},
|
||||
Netrc: &model.Netrc{},
|
||||
Secs: []*model.Secret{},
|
||||
Regs: []*model.Registry{},
|
||||
@ -81,7 +81,7 @@ func TestMissingGlobalEnvsubst(t *testing.T) {
|
||||
Message: "aaa",
|
||||
Event: model.EventPush,
|
||||
},
|
||||
Last: &model.Pipeline{},
|
||||
Prev: &model.Pipeline{},
|
||||
Netrc: &model.Netrc{},
|
||||
Secs: []*model.Secret{},
|
||||
Regs: []*model.Registry{},
|
||||
@ -116,7 +116,7 @@ func TestMultilineEnvsubst(t *testing.T) {
|
||||
Message: `aaa
|
||||
bbb`,
|
||||
},
|
||||
Last: &model.Pipeline{},
|
||||
Prev: &model.Pipeline{},
|
||||
Netrc: &model.Netrc{},
|
||||
Secs: []*model.Secret{},
|
||||
Regs: []*model.Registry{},
|
||||
@ -159,7 +159,7 @@ func TestMultiPipeline(t *testing.T) {
|
||||
Curr: &model.Pipeline{
|
||||
Event: model.EventPush,
|
||||
},
|
||||
Last: &model.Pipeline{},
|
||||
Prev: &model.Pipeline{},
|
||||
Netrc: &model.Netrc{},
|
||||
Secs: []*model.Secret{},
|
||||
Regs: []*model.Registry{},
|
||||
@ -200,7 +200,7 @@ func TestDependsOn(t *testing.T) {
|
||||
Curr: &model.Pipeline{
|
||||
Event: model.EventPush,
|
||||
},
|
||||
Last: &model.Pipeline{},
|
||||
Prev: &model.Pipeline{},
|
||||
Netrc: &model.Netrc{},
|
||||
Secs: []*model.Secret{},
|
||||
Regs: []*model.Registry{},
|
||||
@ -255,7 +255,7 @@ func TestRunsOn(t *testing.T) {
|
||||
Curr: &model.Pipeline{
|
||||
Event: model.EventPush,
|
||||
},
|
||||
Last: &model.Pipeline{},
|
||||
Prev: &model.Pipeline{},
|
||||
Netrc: &model.Netrc{},
|
||||
Secs: []*model.Secret{},
|
||||
Regs: []*model.Registry{},
|
||||
@ -296,7 +296,7 @@ func TestPipelineName(t *testing.T) {
|
||||
Curr: &model.Pipeline{
|
||||
Event: model.EventPush,
|
||||
},
|
||||
Last: &model.Pipeline{},
|
||||
Prev: &model.Pipeline{},
|
||||
Netrc: &model.Netrc{},
|
||||
Secs: []*model.Secret{},
|
||||
Regs: []*model.Registry{},
|
||||
@ -339,7 +339,7 @@ func TestBranchFilter(t *testing.T) {
|
||||
Branch: "dev",
|
||||
Event: model.EventPush,
|
||||
},
|
||||
Last: &model.Pipeline{},
|
||||
Prev: &model.Pipeline{},
|
||||
Netrc: &model.Netrc{},
|
||||
Secs: []*model.Secret{},
|
||||
Regs: []*model.Registry{},
|
||||
@ -382,7 +382,7 @@ func TestRootWhenFilter(t *testing.T) {
|
||||
Forge: getMockForge(t),
|
||||
Repo: &model.Repo{},
|
||||
Curr: &model.Pipeline{Event: "tag"},
|
||||
Last: &model.Pipeline{},
|
||||
Prev: &model.Pipeline{},
|
||||
Netrc: &model.Netrc{},
|
||||
Secs: []*model.Secret{},
|
||||
Regs: []*model.Registry{},
|
||||
@ -432,7 +432,7 @@ func TestZeroSteps(t *testing.T) {
|
||||
Forge: getMockForge(t),
|
||||
Repo: &model.Repo{},
|
||||
Curr: pipeline,
|
||||
Last: &model.Pipeline{},
|
||||
Prev: &model.Pipeline{},
|
||||
Netrc: &model.Netrc{},
|
||||
Secs: []*model.Secret{},
|
||||
Regs: []*model.Registry{},
|
||||
@ -472,7 +472,7 @@ func TestZeroStepsAsMultiPipelineDeps(t *testing.T) {
|
||||
Forge: getMockForge(t),
|
||||
Repo: &model.Repo{},
|
||||
Curr: pipeline,
|
||||
Last: &model.Pipeline{},
|
||||
Prev: &model.Pipeline{},
|
||||
Netrc: &model.Netrc{},
|
||||
Secs: []*model.Secret{},
|
||||
Regs: []*model.Registry{},
|
||||
@ -530,7 +530,7 @@ func TestZeroStepsAsMultiPipelineTransitiveDeps(t *testing.T) {
|
||||
Forge: getMockForge(t),
|
||||
Repo: &model.Repo{},
|
||||
Curr: pipeline,
|
||||
Last: &model.Pipeline{},
|
||||
Prev: &model.Pipeline{},
|
||||
Netrc: &model.Netrc{},
|
||||
Secs: []*model.Secret{},
|
||||
Regs: []*model.Registry{},
|
||||
|
@ -102,6 +102,7 @@ func apiRoutes(e *gin.RouterGroup) {
|
||||
repo.DELETE("/pipelines/:number", session.MustRepoAdmin(), api.DeletePipeline)
|
||||
repo.GET("/pipelines/:number", api.GetPipeline)
|
||||
repo.GET("/pipelines/:number/config", api.GetPipelineConfig)
|
||||
repo.GET("/pipelines/:number/metadata", session.MustPush, api.GetPipelineMetadata)
|
||||
|
||||
// requires push permissions
|
||||
repo.POST("/pipelines/:number", session.MustPush, api.PostPipeline)
|
||||
|
@ -246,7 +246,16 @@
|
||||
"show_errors": "Show errors",
|
||||
"we_got_some_errors": "Oh no, we got some errors!",
|
||||
"duration": "Pipeline duration",
|
||||
"created": "Created: {created}"
|
||||
"created": "Created: {created}",
|
||||
"debug": {
|
||||
"title": "Debug",
|
||||
"download_metadata": "Download metadata",
|
||||
"metadata_download_error": "Error downloading metadata",
|
||||
"metadata_download_successful": "Metadata downloaded successfully",
|
||||
"no_permission": "You don't have permission to access debug information.",
|
||||
"metadata_exec_title": "Rerun pipeline locally",
|
||||
"metadata_exec_desc": "Download this pipeline's metadata to run it locally. This allows you to troubleshoot issues and test changes before committing them."
|
||||
}
|
||||
}
|
||||
},
|
||||
"org": {
|
||||
|
@ -119,6 +119,10 @@ export default class WoodpeckerClient extends ApiClient {
|
||||
return this._get(`/api/repos/${repoId}/pipelines/${pipelineNumber}/config`) as Promise<PipelineConfig[]>;
|
||||
}
|
||||
|
||||
async getPipelineMetadata(repoId: number, pipelineNumber: number): Promise<any> {
|
||||
return this._get(`/api/repos/${repoId}/pipelines/${pipelineNumber}/metadata`) as Promise<any>;
|
||||
}
|
||||
|
||||
async getPipelineFeed(): Promise<PipelineFeed[]> {
|
||||
return this._get(`/api/user/feed`) as Promise<PipelineFeed[]>;
|
||||
}
|
||||
|
@ -94,6 +94,11 @@ const routes: RouteRecordRaw[] = [
|
||||
component: (): Component => import('~/views/repo/pipeline/PipelineErrors.vue'),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: 'debug',
|
||||
name: 'repo-pipeline-debug',
|
||||
component: (): Component => import('~/views/repo/pipeline/PipelineDebug.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
77
web/src/views/repo/pipeline/PipelineDebug.vue
Normal file
77
web/src/views/repo/pipeline/PipelineDebug.vue
Normal file
@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<template v-if="repoPermissions && repoPermissions.push">
|
||||
<Panel>
|
||||
<InputField :label="$t('repo.pipeline.debug.metadata_exec_title')">
|
||||
<p class="text-sm text-wp-text-alt-100 mb-2">{{ $t('repo.pipeline.debug.metadata_exec_desc') }}</p>
|
||||
<pre class="code-box">{{ cliExecWithMetadata }}</pre>
|
||||
</InputField>
|
||||
<div class="flex items-center space-x-4">
|
||||
<Button :is-loading="isLoading" :text="$t('repo.pipeline.debug.download_metadata')" @click="downloadMetadata" />
|
||||
</div>
|
||||
</Panel>
|
||||
</template>
|
||||
<div v-else class="flex items-center justify-center h-full">
|
||||
<div class="text-center p-8 bg-wp-control-error-100 rounded-lg shadow-lg">
|
||||
<p class="text-2xl font-bold text-white">{{ $t('repo.pipeline.debug.no_permission') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, ref, type Ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import Button from '~/components/atomic/Button.vue';
|
||||
import InputField from '~/components/form/InputField.vue';
|
||||
import Panel from '~/components/layout/Panel.vue';
|
||||
import useApiClient from '~/compositions/useApiClient';
|
||||
import useNotifications from '~/compositions/useNotifications';
|
||||
import type { Pipeline, Repo, RepoPermissions } from '~/lib/api/types';
|
||||
|
||||
const { t } = useI18n();
|
||||
const apiClient = useApiClient();
|
||||
const notifications = useNotifications();
|
||||
|
||||
const repo = inject<Ref<Repo>>('repo');
|
||||
const pipeline = inject<Ref<Pipeline>>('pipeline');
|
||||
const repoPermissions = inject<Ref<RepoPermissions>>('repo-permissions');
|
||||
|
||||
const isLoading = ref(false);
|
||||
|
||||
const metadataFileName = computed(
|
||||
() => `${repo?.value.full_name.replaceAll('/', '-')}-pipeline-${pipeline?.value.number}-metadata.json`,
|
||||
);
|
||||
const cliExecWithMetadata = computed(() => `# woodpecker exec --metadata-file ${metadataFileName.value}`);
|
||||
|
||||
async function downloadMetadata() {
|
||||
if (!repo?.value || !pipeline?.value || !repoPermissions?.value?.push) {
|
||||
notifications.notify({ type: 'error', title: t('repo.pipeline.debug.metadata_download_error') });
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const metadata = await apiClient.getPipelineMetadata(repo.value.id, pipeline.value.number);
|
||||
|
||||
// Create a Blob with the JSON data
|
||||
const blob = new Blob([JSON.stringify(metadata, null, 2)], { type: 'application/json' });
|
||||
|
||||
// Create a download link and trigger the download
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = metadataFileName.value;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
notifications.notify({ type: 'success', title: t('repo.pipeline.debug.metadata_download_successful') });
|
||||
} catch (error) {
|
||||
console.error('Error fetching metadata:', error);
|
||||
notifications.notify({ type: 'error', title: t('repo.pipeline.debug.metadata_download_error') });
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
@ -93,6 +93,7 @@
|
||||
id="changed-files"
|
||||
:title="$t('repo.pipeline.files', { files: pipeline.changed_files?.length })"
|
||||
/>
|
||||
<Tab v-if="repoPermissions && repoPermissions.push" id="debug" :title="$t('repo.pipeline.debug.title')" />
|
||||
|
||||
<router-view />
|
||||
</Scaffold>
|
||||
@ -219,6 +220,10 @@ const activeTab = computed({
|
||||
return 'errors';
|
||||
}
|
||||
|
||||
if (route.name === 'repo-pipeline-debug' && repoPermissions.value?.push) {
|
||||
return 'debug';
|
||||
}
|
||||
|
||||
return 'tasks';
|
||||
},
|
||||
set(tab: string) {
|
||||
@ -237,6 +242,10 @@ const activeTab = computed({
|
||||
if (tab === 'errors') {
|
||||
router.replace({ name: 'repo-pipeline-errors' });
|
||||
}
|
||||
|
||||
if (tab === 'debug' && repoPermissions.value?.push) {
|
||||
router.replace({ name: 'repo-pipeline-debug' });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -110,6 +110,9 @@ type Client interface {
|
||||
// PipelineKill force kills the running pipeline.
|
||||
PipelineKill(repoID, pipeline int64) error
|
||||
|
||||
// PipelineMetadata returns metadata for a pipeline.
|
||||
PipelineMetadata(repoID int64, pipelineNumber int) ([]byte, error)
|
||||
|
||||
// StepLogEntries returns the LogEntries for the given pipeline step
|
||||
StepLogEntries(repoID, pipeline, stepID int64) ([]*LogEntry, error)
|
||||
|
||||
|
@ -1211,6 +1211,36 @@ func (_m *Client) PipelineList(repoID int64) ([]*woodpecker.Pipeline, error) {
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// PipelineMetadata provides a mock function with given fields: repoID, pipelineNumber
|
||||
func (_m *Client) PipelineMetadata(repoID int64, pipelineNumber int) ([]byte, error) {
|
||||
ret := _m.Called(repoID, pipelineNumber)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for PipelineMetadata")
|
||||
}
|
||||
|
||||
var r0 []byte
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(int64, int) ([]byte, error)); ok {
|
||||
return rf(repoID, pipelineNumber)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(int64, int) []byte); ok {
|
||||
r0 = rf(repoID, pipelineNumber)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]byte)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(int64, int) error); ok {
|
||||
r1 = rf(repoID, pipelineNumber)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// PipelineQueue provides a mock function with given fields:
|
||||
func (_m *Client) PipelineQueue() ([]*woodpecker.Feed, error) {
|
||||
ret := _m.Called()
|
||||
|
@ -1,8 +1,15 @@
|
||||
package woodpecker
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const pathPipelineQueue = "%s/api/pipelines"
|
||||
const (
|
||||
pathPipelineQueue = "%s/api/pipelines"
|
||||
pathPipelineMetadata = "%s/api/repos/%d/pipelines/%d/metadata"
|
||||
)
|
||||
|
||||
// PipelineQueue returns a list of enqueued pipelines.
|
||||
func (c *client) PipelineQueue() ([]*Feed, error) {
|
||||
@ -11,3 +18,16 @@ func (c *client) PipelineQueue() ([]*Feed, error) {
|
||||
err := c.get(uri, &out)
|
||||
return out, err
|
||||
}
|
||||
|
||||
// PipelineMetadata returns metadata for a pipeline, workflow name is optional.
|
||||
func (c *client) PipelineMetadata(repoID int64, pipelineNumber int) ([]byte, error) {
|
||||
uri := fmt.Sprintf(pathPipelineMetadata, c.addr, repoID, pipelineNumber)
|
||||
|
||||
body, err := c.open(uri, http.MethodGet, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer body.Close()
|
||||
|
||||
return io.ReadAll(body)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user