1
0
mirror of https://github.com/woodpecker-ci/woodpecker.git synced 2025-11-23 21:44:44 +02:00

Resolve built-in variables for global when filter (#1790)

addresses
bd461477bd

close  #1244, close #1580

---------

Co-authored-by: Anbraten <anton@ju60.de>
This commit is contained in:
6543
2023-06-05 00:15:07 +02:00
committed by GitHub
parent c919f32e0b
commit ea895baf83
25 changed files with 990 additions and 581 deletions

View File

@@ -1,4 +1,4 @@
// Copyright 2022 Woodpecker Authors
// Copyright 2023 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -15,274 +15,131 @@
package frontend
import (
"regexp"
"strconv"
"fmt"
"net/url"
"strings"
"github.com/drone/envsubst"
"github.com/woodpecker-ci/woodpecker/pipeline/frontend/metadata"
"github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/version"
)
// Event types corresponding to scm hooks.
const (
EventPush = "push"
EventPull = "pull_request"
EventTag = "tag"
EventDeploy = "deployment"
EventCron = "cron"
EventManual = "manual"
)
// Different ways to handle failure states
const (
FailureIgnore = "ignore"
FailureFail = "fail"
// FailureCancel = "cancel" // Not implemented yet
)
type (
// Metadata defines runtime m.
Metadata struct {
ID string `json:"id,omitempty"`
Repo Repo `json:"repo,omitempty"`
Curr Pipeline `json:"curr,omitempty"`
Prev Pipeline `json:"prev,omitempty"`
Workflow Workflow `json:"workflow,omitempty"`
Step Step `json:"step,omitempty"`
Sys System `json:"sys,omitempty"`
Forge Forge `json:"forge,omitempty"`
}
// Repo defines runtime metadata for a repository.
Repo struct {
Name string `json:"name,omitempty"`
Link string `json:"link,omitempty"`
CloneURL string `json:"clone_url,omitempty"`
Private bool `json:"private,omitempty"`
Secrets []Secret `json:"secrets,omitempty"`
Branch string `json:"default_branch,omitempty"`
}
// Pipeline defines runtime metadata for a pipeline.
Pipeline struct {
Number int64 `json:"number,omitempty"`
Created int64 `json:"created,omitempty"`
Started int64 `json:"started,omitempty"`
Finished int64 `json:"finished,omitempty"`
Timeout int64 `json:"timeout,omitempty"`
Status string `json:"status,omitempty"`
Event string `json:"event,omitempty"`
Link string `json:"link,omitempty"`
Target string `json:"target,omitempty"`
Trusted bool `json:"trusted,omitempty"`
Commit Commit `json:"commit,omitempty"`
Parent int64 `json:"parent,omitempty"`
Cron string `json:"cron,omitempty"`
}
// Commit defines runtime metadata for a commit.
Commit struct {
Sha string `json:"sha,omitempty"`
Ref string `json:"ref,omitempty"`
Refspec string `json:"refspec,omitempty"`
Branch string `json:"branch,omitempty"`
Message string `json:"message,omitempty"`
Author Author `json:"author,omitempty"`
ChangedFiles []string `json:"changed_files,omitempty"`
PullRequestLabels []string `json:"labels,omitempty"`
}
// Author defines runtime metadata for a commit author.
Author struct {
Name string `json:"name,omitempty"`
Email string `json:"email,omitempty"`
Avatar string `json:"avatar,omitempty"`
}
// Workflow defines runtime metadata for a workflow.
Workflow struct {
Name string `json:"name,omitempty"`
Number int `json:"number,omitempty"`
Matrix map[string]string `json:"matrix,omitempty"`
}
// Step defines runtime metadata for a step.
Step struct {
Name string `json:"name,omitempty"`
Number int `json:"number,omitempty"`
}
// Secret defines a runtime secret
Secret struct {
Name string `json:"name,omitempty"`
Value string `json:"value,omitempty"`
Mount string `json:"mount,omitempty"`
Mask bool `json:"mask,omitempty"`
}
// System defines runtime metadata for a ci/cd system.
System struct {
Name string `json:"name,omitempty"`
Host string `json:"host,omitempty"`
Link string `json:"link,omitempty"`
Platform string `json:"arch,omitempty"`
Version string `json:"version,omitempty"`
}
// Forge defines runtime metadata about the forge that host the repo
Forge struct {
Type string `json:"type,omitempty"`
URL string `json:"url,omitempty"`
}
)
// Environ returns the metadata as a map of environment variables.
func (m *Metadata) Environ() map[string]string {
var (
repoOwner string
repoName string
sourceBranch string
targetBranch string
)
repoParts := strings.Split(m.Repo.Name, "/")
if len(repoParts) == 2 {
repoOwner = repoParts[0]
repoName = repoParts[1]
} else {
repoName = m.Repo.Name
}
branchParts := strings.Split(m.Curr.Commit.Refspec, ":")
if len(branchParts) == 2 {
sourceBranch = branchParts[0]
targetBranch = branchParts[1]
}
params := map[string]string{
"CI": m.Sys.Name,
"CI_REPO": m.Repo.Name,
"CI_REPO_OWNER": repoOwner,
"CI_REPO_NAME": repoName,
"CI_REPO_SCM": "git",
"CI_REPO_URL": m.Repo.Link,
"CI_REPO_CLONE_URL": m.Repo.CloneURL,
"CI_REPO_DEFAULT_BRANCH": m.Repo.Branch,
"CI_REPO_PRIVATE": strconv.FormatBool(m.Repo.Private),
"CI_REPO_TRUSTED": "false", // TODO should this be added?
"CI_COMMIT_SHA": m.Curr.Commit.Sha,
"CI_COMMIT_REF": m.Curr.Commit.Ref,
"CI_COMMIT_REFSPEC": m.Curr.Commit.Refspec,
"CI_COMMIT_BRANCH": m.Curr.Commit.Branch,
"CI_COMMIT_SOURCE_BRANCH": sourceBranch,
"CI_COMMIT_TARGET_BRANCH": targetBranch,
"CI_COMMIT_URL": m.Curr.Link,
"CI_COMMIT_MESSAGE": m.Curr.Commit.Message,
"CI_COMMIT_AUTHOR": m.Curr.Commit.Author.Name,
"CI_COMMIT_AUTHOR_EMAIL": m.Curr.Commit.Author.Email,
"CI_COMMIT_AUTHOR_AVATAR": m.Curr.Commit.Author.Avatar,
"CI_COMMIT_TAG": "", // will be set if event is tag
"CI_COMMIT_PULL_REQUEST": "", // will be set if event is pr
"CI_COMMIT_PULL_REQUEST_LABELS": "", // will be set if event is pr
"CI_PIPELINE_NUMBER": strconv.FormatInt(m.Curr.Number, 10),
"CI_PIPELINE_PARENT": strconv.FormatInt(m.Curr.Parent, 10),
"CI_PIPELINE_EVENT": m.Curr.Event,
"CI_PIPELINE_URL": m.Curr.Link,
"CI_PIPELINE_DEPLOY_TARGET": m.Curr.Target,
"CI_PIPELINE_STATUS": m.Curr.Status,
"CI_PIPELINE_CREATED": strconv.FormatInt(m.Curr.Created, 10),
"CI_PIPELINE_STARTED": strconv.FormatInt(m.Curr.Started, 10),
"CI_PIPELINE_FINISHED": strconv.FormatInt(m.Curr.Finished, 10),
"CI_WORKFLOW_NAME": m.Workflow.Name,
"CI_WORKFLOW_NUMBER": strconv.Itoa(m.Workflow.Number),
"CI_STEP_NAME": m.Step.Name,
"CI_STEP_NUMBER": strconv.Itoa(m.Step.Number),
"CI_STEP_STATUS": "", // will be set by agent
"CI_STEP_STARTED": "", // will be set by agent
"CI_STEP_FINISHED": "", // will be set by agent
"CI_PREV_COMMIT_SHA": m.Prev.Commit.Sha,
"CI_PREV_COMMIT_REF": m.Prev.Commit.Ref,
"CI_PREV_COMMIT_REFSPEC": m.Prev.Commit.Refspec,
"CI_PREV_COMMIT_BRANCH": m.Prev.Commit.Branch,
"CI_PREV_COMMIT_URL": m.Prev.Link,
"CI_PREV_COMMIT_MESSAGE": m.Prev.Commit.Message,
"CI_PREV_COMMIT_AUTHOR": m.Prev.Commit.Author.Name,
"CI_PREV_COMMIT_AUTHOR_EMAIL": m.Prev.Commit.Author.Email,
"CI_PREV_COMMIT_AUTHOR_AVATAR": m.Prev.Commit.Author.Avatar,
"CI_PREV_PIPELINE_NUMBER": strconv.FormatInt(m.Prev.Number, 10),
"CI_PREV_PIPELINE_PARENT": strconv.FormatInt(m.Prev.Parent, 10),
"CI_PREV_PIPELINE_EVENT": m.Prev.Event,
"CI_PREV_PIPELINE_URL": m.Prev.Link,
"CI_PREV_PIPELINE_DEPLOY_TARGET": m.Prev.Target,
"CI_PREV_PIPELINE_STATUS": m.Prev.Status,
"CI_PREV_PIPELINE_CREATED": strconv.FormatInt(m.Prev.Created, 10),
"CI_PREV_PIPELINE_STARTED": strconv.FormatInt(m.Prev.Started, 10),
"CI_PREV_PIPELINE_FINISHED": strconv.FormatInt(m.Prev.Finished, 10),
"CI_SYSTEM_NAME": m.Sys.Name,
"CI_SYSTEM_URL": m.Sys.Link,
"CI_SYSTEM_HOST": m.Sys.Host,
"CI_SYSTEM_PLATFORM": m.Sys.Platform, // will be set by pipeline platform option or by agent
"CI_SYSTEM_VERSION": version.Version,
"CI_FORGE_TYPE": m.Forge.Type,
"CI_FORGE_URL": m.Forge.URL,
// DEPRECATED
"CI_SYSTEM_ARCH": m.Sys.Platform, // TODO: remove after v1.0.x version
// use CI_PIPELINE_*
"CI_BUILD_NUMBER": strconv.FormatInt(m.Curr.Number, 10),
"CI_BUILD_PARENT": strconv.FormatInt(m.Curr.Parent, 10),
"CI_BUILD_EVENT": m.Curr.Event,
"CI_BUILD_LINK": m.Curr.Link,
"CI_BUILD_DEPLOY_TARGET": m.Curr.Target,
"CI_BUILD_STATUS": m.Curr.Status,
"CI_BUILD_CREATED": strconv.FormatInt(m.Curr.Created, 10),
"CI_BUILD_STARTED": strconv.FormatInt(m.Curr.Started, 10),
"CI_BUILD_FINISHED": strconv.FormatInt(m.Curr.Finished, 10),
// use CI_PREV_PIPELINE_*
"CI_PREV_BUILD_NUMBER": strconv.FormatInt(m.Prev.Number, 10),
"CI_PREV_BUILD_PARENT": strconv.FormatInt(m.Prev.Parent, 10),
"CI_PREV_BUILD_EVENT": m.Prev.Event,
"CI_PREV_BUILD_LINK": m.Prev.Link,
"CI_PREV_BUILD_DEPLOY_TARGET": m.Prev.Target,
"CI_PREV_BUILD_STATUS": m.Prev.Status,
"CI_PREV_BUILD_CREATED": strconv.FormatInt(m.Prev.Created, 10),
"CI_PREV_BUILD_STARTED": strconv.FormatInt(m.Prev.Started, 10),
"CI_PREV_BUILD_FINISHED": strconv.FormatInt(m.Prev.Finished, 10),
// use CI_STEP_*
"CI_JOB_NUMBER": strconv.Itoa(m.Step.Number),
"CI_JOB_STATUS": "", // will be set by agent
"CI_JOB_STARTED": "", // will be set by agent
"CI_JOB_FINISHED": "", // will be set by agent
// CI_REPO_CLONE_URL
"CI_REPO_REMOTE": m.Repo.CloneURL,
// use *_URL
"CI_REPO_LINK": m.Repo.Link,
"CI_COMMIT_LINK": m.Curr.Link,
"CI_PIPELINE_LINK": m.Curr.Link,
"CI_PREV_COMMIT_LINK": m.Prev.Link,
"CI_PREV_PIPELINE_LINK": m.Prev.Link,
"CI_SYSTEM_LINK": m.Sys.Link,
}
if m.Curr.Event == EventTag {
params["CI_COMMIT_TAG"] = strings.TrimPrefix(m.Curr.Commit.Ref, "refs/tags/")
}
if m.Curr.Event == EventPull {
params["CI_COMMIT_PULL_REQUEST"] = pullRegexp.FindString(m.Curr.Commit.Ref)
params["CI_COMMIT_PULL_REQUEST_LABELS"] = strings.Join(m.Curr.Commit.PullRequestLabels, ",")
}
return params
func EnvVarSubst(yaml string, environ map[string]string) (string, error) {
return envsubst.Eval(yaml, func(name string) string {
env := environ[name]
if strings.Contains(env, "\n") {
env = fmt.Sprintf("%q", env)
}
return env
})
}
var pullRegexp = regexp.MustCompile(`\d+`)
// MetadataFromStruct return the metadata from a pipeline will run with.
func MetadataFromStruct(forge metadata.ServerForge, repo *model.Repo, pipeline, last *model.Pipeline, workflow *model.Step, link string) metadata.Metadata {
host := link
uri, err := url.Parse(link)
if err == nil {
host = uri.Host
}
func (m *Metadata) SetPlatform(platform string) {
m.Sys.Platform = platform
fForge := metadata.Forge{}
if forge != nil {
fForge = metadata.Forge{
Type: forge.Name(),
URL: forge.URL(),
}
}
fRepo := metadata.Repo{}
if repo != nil {
fRepo = metadata.Repo{
Name: repo.Name,
Owner: repo.Owner,
RemoteID: fmt.Sprint(repo.ForgeRemoteID),
Link: repo.Link,
CloneURL: repo.Clone,
Private: repo.IsSCMPrivate,
Branch: repo.Branch,
Trusted: repo.IsTrusted,
}
if idx := strings.LastIndex(repo.FullName, "/"); idx != -1 {
if fRepo.Name == "" && repo.FullName != "" {
fRepo.Name = repo.FullName[idx+1:]
}
if fRepo.Owner == "" && repo.FullName != "" {
fRepo.Owner = repo.FullName[:idx]
}
}
}
fWorkflow := metadata.Workflow{}
if workflow != nil {
fWorkflow = metadata.Workflow{
Name: workflow.Name,
Number: workflow.PID,
Matrix: workflow.Environ,
}
}
return metadata.Metadata{
Repo: fRepo,
Curr: metadataPipelineFromModelPipeline(pipeline, true),
Prev: metadataPipelineFromModelPipeline(last, false),
Workflow: fWorkflow,
Step: metadata.Step{},
Sys: metadata.System{
Name: "woodpecker",
Link: link,
Host: host,
Platform: "", // will be set by pipeline platform option or by agent
Version: version.Version,
},
Forge: fForge,
}
}
func metadataPipelineFromModelPipeline(pipeline *model.Pipeline, includeParent bool) metadata.Pipeline {
if pipeline == nil {
return metadata.Pipeline{}
}
cron := ""
if pipeline.Event == model.EventCron {
cron = pipeline.Sender
}
parent := int64(0)
if includeParent {
parent = pipeline.Parent
}
return metadata.Pipeline{
Number: pipeline.Number,
Parent: parent,
Created: pipeline.Created,
Started: pipeline.Started,
Finished: pipeline.Finished,
Status: string(pipeline.Status),
Event: string(pipeline.Event),
Link: pipeline.Link,
Target: pipeline.Deploy,
Commit: metadata.Commit{
Sha: pipeline.Commit,
Ref: pipeline.Ref,
Refspec: pipeline.Refspec,
Branch: pipeline.Branch,
Message: pipeline.Message,
Author: metadata.Author{
Name: pipeline.Author,
Email: pipeline.Email,
Avatar: pipeline.Avatar,
},
ChangedFiles: pipeline.ChangedFiles,
PullRequestLabels: pipeline.PullRequestLabels,
},
Cron: cron,
}
}

View File

@@ -0,0 +1,32 @@
// Copyright 2022 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 metadata
// Event types corresponding to scm hooks.
const (
EventPush = "push"
EventPull = "pull_request"
EventTag = "tag"
EventDeploy = "deployment"
EventCron = "cron"
EventManual = "manual"
)
// Different ways to handle failure states
const (
FailureIgnore = "ignore"
FailureFail = "fail"
// FailureCancel = "cancel" // Not implemented yet
)

View File

@@ -0,0 +1,71 @@
// Copyright 2022 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 metadata
// SetDroneEnviron set dedicated to DroneCI environment vars as compatibility
// layer. Main purpose is to be compatible with drone plugins.
func SetDroneEnviron(env map[string]string) {
// webhook
copyEnv("CI_COMMIT_BRANCH", "DRONE_BRANCH", env)
copyEnv("CI_COMMIT_PULL_REQUEST", "DRONE_PULL_REQUEST", env)
copyEnv("CI_COMMIT_TAG", "DRONE_TAG", env)
copyEnv("CI_COMMIT_SOURCE_BRANCH", "DRONE_SOURCE_BRANCH", env)
copyEnv("CI_COMMIT_TARGET_BRANCH", "DRONE_TARGET_BRANCH", env)
// pipeline
copyEnv("CI_PIPELINE_NUMBER", "DRONE_BUILD_NUMBER", env)
copyEnv("CI_PIPELINE_PARENT", "DRONE_BUILD_PARENT", env)
copyEnv("CI_PIPELINE_EVENT", "DRONE_BUILD_EVENT", env)
copyEnv("CI_PIPELINE_STATUS", "DRONE_BUILD_STATUS", env)
copyEnv("CI_PIPELINE_URL", "DRONE_BUILD_LINK", env)
copyEnv("CI_PIPELINE_CREATED", "DRONE_BUILD_CREATED", env)
copyEnv("CI_PIPELINE_STARTED", "DRONE_BUILD_STARTED", env)
copyEnv("CI_PIPELINE_FINISHED", "DRONE_BUILD_FINISHED", env)
// commit
copyEnv("CI_COMMIT_SHA", "DRONE_COMMIT", env)
copyEnv("CI_COMMIT_SHA", "DRONE_COMMIT_SHA", env)
copyEnv("CI_PREV_COMMIT_SHA", "DRONE_COMMIT_BEFORE", env)
copyEnv("CI_COMMIT_REF", "DRONE_COMMIT_REF", env)
copyEnv("CI_COMMIT_BRANCH", "DRONE_COMMIT_BRANCH", env)
copyEnv("CI_COMMIT_URL", "DRONE_COMMIT_LINK", env)
copyEnv("CI_COMMIT_MESSAGE", "DRONE_COMMIT_MESSAGE", env)
copyEnv("CI_COMMIT_AUTHOR", "DRONE_COMMIT_AUTHOR", env)
copyEnv("CI_COMMIT_AUTHOR", "DRONE_COMMIT_AUTHOR_NAME", env)
copyEnv("CI_COMMIT_AUTHOR_EMAIL", "DRONE_COMMIT_AUTHOR_EMAIL", env)
copyEnv("CI_COMMIT_AUTHOR_AVATAR", "DRONE_COMMIT_AUTHOR_AVATAR", env)
// repo
copyEnv("CI_REPO", "DRONE_REPO", env)
copyEnv("CI_REPO_SCM", "DRONE_REPO_SCM", env)
copyEnv("CI_REPO_OWNER", "DRONE_REPO_OWNER", env)
copyEnv("CI_REPO_NAME", "DRONE_REPO_NAME", env)
copyEnv("CI_REPO_URL", "DRONE_REPO_LINK", env)
copyEnv("CI_REPO_DEFAULT_BRANCH", "DRONE_REPO_BRANCH", env)
copyEnv("CI_REPO_PRIVATE", "DRONE_REPO_PRIVATE", env)
// clone
copyEnv("CI_REPO_CLONE_URL", "DRONE_REMOTE_URL", env)
copyEnv("CI_REPO_CLONE_URL", "DRONE_GIT_HTTP_URL", env)
// misc
copyEnv("CI_SYSTEM_HOST", "DRONE_SYSTEM_HOST", env)
copyEnv("CI_STEP_NUMBER", "DRONE_STEP_NUMBER", env)
}
func copyEnv(woodpecker, drone string, env map[string]string) {
var present bool
var value string
value, present = env[woodpecker]
if present {
env[drone] = value
}
}

View File

@@ -0,0 +1,132 @@
package metadata_test
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/woodpecker-ci/woodpecker/pipeline/frontend/metadata"
)
func TestSetDroneEnviron(t *testing.T) {
woodpeckerVars := `CI=woodpecker
CI_BUILD_CREATED=1685749339
CI_BUILD_EVENT=pull_request
CI_BUILD_FINISHED=1685749350
CI_BUILD_LINK=https://codeberg.org/Epsilon_02/todo-checker/pulls/9
CI_BUILD_NUMBER=41
CI_BUILD_STARTED=1685749339
CI_BUILD_STATUS=success
CI_COMMIT_AUTHOR=6543
CI_COMMIT_AUTHOR_AVATAR=https://codeberg.org/avatars/09a234c768cb9bca78f6b2f82d6af173
CI_COMMIT_BRANCH=main
CI_COMMIT_LINK=https://codeberg.org/Epsilon_02/todo-checker/pulls/9
CI_COMMIT_MESSAGE=fix testscript
CI_COMMIT_PULL_REQUEST=9
CI_COMMIT_REF=refs/pull/9/head
CI_COMMIT_REFSPEC=fix_fail-on-err:main
CI_COMMIT_SHA=a778b069d9f5992786d2db9be493b43868cfce76
CI_COMMIT_SOURCE_BRANCH=fix_fail-on-err
CI_COMMIT_TARGET_BRANCH=main
CI_JOB_FINISHED=1685749350
CI_JOB_STARTED=1685749339
CI_JOB_STATUS=success
CI_MACHINE=7939910e431b
CI_PIPELINE_CREATED=1685749339
CI_PIPELINE_EVENT=pull_request
CI_PIPELINE_FINISHED=1685749350
CI_PIPELINE_LINK=https://codeberg.org/Epsilon_02/todo-checker/pulls/9
CI_PIPELINE_NUMBER=41
CI_PIPELINE_STARTED=1685749339
CI_PIPELINE_STATUS=success
CI_PREV_BUILD_CREATED=1685748680
CI_PREV_BUILD_EVENT=pull_request
CI_PREV_BUILD_FINISHED=1685748704
CI_PREV_BUILD_LINK=https://codeberg.org/Epsilon_02/todo-checker/pulls/13
CI_PREV_BUILD_NUMBER=40
CI_PREV_BUILD_STARTED=1685748680
CI_PREV_BUILD_STATUS=success
CI_PREV_COMMIT_AUTHOR=6543
CI_PREV_COMMIT_AUTHOR_AVATAR=https://codeberg.org/avatars/09a234c768cb9bca78f6b2f82d6af173
CI_PREV_COMMIT_BRANCH=main
CI_PREV_COMMIT_LINK=https://codeberg.org/Epsilon_02/todo-checker/pulls/13
CI_PREV_COMMIT_MESSAGE=Print filename and linenuber on fail
CI_PREV_COMMIT_REF=refs/pull/13/head
CI_PREV_COMMIT_REFSPEC=print_file_and_line:main
CI_PREV_COMMIT_SHA=e246aff5a9466df2e522efc9007823a7496d9d41
CI_PREV_PIPELINE_CREATED=1685748680
CI_PREV_PIPELINE_EVENT=pull_request
CI_PREV_PIPELINE_FINISHED=1685748704
CI_PREV_PIPELINE_LINK=https://codeberg.org/Epsilon_02/todo-checker/pulls/13
CI_PREV_PIPELINE_NUMBER=40
CI_PREV_PIPELINE_STARTED=1685748680
CI_PREV_PIPELINE_STATUS=success
CI_REPO=Epsilon_02/todo-checker
CI_REPO_CLONE_URL=https://codeberg.org/Epsilon_02/todo-checker.git
CI_REPO_DEFAULT_BRANCH=main
CI_REPO_LINK=https://codeberg.org/Epsilon_02/todo-checker
CI_REPO_NAME=todo-checker
CI_REPO_OWNER=Epsilon_02
CI_REPO_REMOTE=https://codeberg.org/Epsilon_02/todo-checker.git
CI_REPO_SCM=git
CI_STEP_FINISHED=1685749350
CI_STEP_NAME=wp_01h1z7v5d1tskaqjexw0ng6w7d_0_step_3
CI_STEP_STARTED=1685749339
CI_STEP_STATUS=success
CI_SYSTEM_ARCH=linux/amd64
CI_SYSTEM_HOST=ci.codeberg.org
CI_SYSTEM_LINK=https://ci.codeberg.org
CI_SYSTEM_NAME=woodpecker
CI_SYSTEM_VERSION=next-dd644da3
CI_WORKFLOW_NAME=woodpecker
CI_WORKFLOW_NUMBER=1
CI_WORKSPACE=/woodpecker/src/codeberg.org/Epsilon_02/todo-checker`
droneVars := `DRONE_BRANCH=main
DRONE_BUILD_CREATED=1685749339
DRONE_BUILD_EVENT=pull_request
DRONE_BUILD_FINISHED=1685749350
DRONE_BUILD_NUMBER=41
DRONE_BUILD_STARTED=1685749339
DRONE_BUILD_STATUS=success
DRONE_COMMIT=a778b069d9f5992786d2db9be493b43868cfce76
DRONE_COMMIT_AUTHOR=6543
DRONE_COMMIT_AUTHOR_AVATAR=https://codeberg.org/avatars/09a234c768cb9bca78f6b2f82d6af173
DRONE_COMMIT_AUTHOR_NAME=6543
DRONE_COMMIT_BEFORE=e246aff5a9466df2e522efc9007823a7496d9d41
DRONE_COMMIT_BRANCH=main
DRONE_COMMIT_MESSAGE=fix testscript
DRONE_COMMIT_REF=refs/pull/9/head
DRONE_COMMIT_SHA=a778b069d9f5992786d2db9be493b43868cfce76
DRONE_GIT_HTTP_URL=https://codeberg.org/Epsilon_02/todo-checker.git
DRONE_PULL_REQUEST=9
DRONE_REMOTE_URL=https://codeberg.org/Epsilon_02/todo-checker.git
DRONE_REPO=Epsilon_02/todo-checker
DRONE_REPO_BRANCH=main
DRONE_REPO_NAME=todo-checker
DRONE_REPO_OWNER=Epsilon_02
DRONE_REPO_SCM=git
DRONE_SOURCE_BRANCH=fix_fail-on-err
DRONE_SYSTEM_HOST=ci.codeberg.org
DRONE_TARGET_BRANCH=main`
env := convertListToEnvMap(t, woodpeckerVars)
metadata.SetDroneEnviron(env)
// filter only new added env vars
for k := range convertListToEnvMap(t, woodpeckerVars) {
delete(env, k)
}
assert.EqualValues(t, convertListToEnvMap(t, droneVars), env)
}
func convertListToEnvMap(t *testing.T, list string) map[string]string {
result := make(map[string]string)
for _, s := range strings.Split(list, "\n") {
ss := strings.SplitN(strings.TrimSpace(s), "=", 2)
if len(ss) != 2 {
t.Fatal("helper function got invalid test data")
}
result[ss[0]] = ss[1]
}
return result
}

View File

@@ -0,0 +1,161 @@
// Copyright 2023 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 metadata
import (
"path"
"regexp"
"strconv"
"strings"
)
var pullRegexp = regexp.MustCompile(`\d+`)
// Environ returns the metadata as a map of environment variables.
func (m *Metadata) Environ() map[string]string {
var (
sourceBranch string
targetBranch string
)
branchParts := strings.Split(m.Curr.Commit.Refspec, ":")
if len(branchParts) == 2 {
sourceBranch = branchParts[0]
targetBranch = branchParts[1]
}
params := map[string]string{
"CI": m.Sys.Name,
"CI_REPO": path.Join(m.Repo.Owner, m.Repo.Name),
"CI_REPO_NAME": m.Repo.Name,
"CI_REPO_OWNER": m.Repo.Owner,
"CI_REPO_REMOTE_ID": m.Repo.RemoteID,
"CI_REPO_SCM": "git",
"CI_REPO_URL": m.Repo.Link,
"CI_REPO_CLONE_URL": m.Repo.CloneURL,
"CI_REPO_DEFAULT_BRANCH": m.Repo.Branch,
"CI_REPO_PRIVATE": strconv.FormatBool(m.Repo.Private),
"CI_REPO_TRUSTED": strconv.FormatBool(m.Repo.Trusted),
"CI_COMMIT_SHA": m.Curr.Commit.Sha,
"CI_COMMIT_REF": m.Curr.Commit.Ref,
"CI_COMMIT_REFSPEC": m.Curr.Commit.Refspec,
"CI_COMMIT_BRANCH": m.Curr.Commit.Branch,
"CI_COMMIT_SOURCE_BRANCH": sourceBranch,
"CI_COMMIT_TARGET_BRANCH": targetBranch,
"CI_COMMIT_URL": m.Curr.Link,
"CI_COMMIT_MESSAGE": m.Curr.Commit.Message,
"CI_COMMIT_AUTHOR": m.Curr.Commit.Author.Name,
"CI_COMMIT_AUTHOR_EMAIL": m.Curr.Commit.Author.Email,
"CI_COMMIT_AUTHOR_AVATAR": m.Curr.Commit.Author.Avatar,
"CI_COMMIT_TAG": "", // will be set if event is tag
"CI_COMMIT_PULL_REQUEST": "", // will be set if event is pr
"CI_COMMIT_PULL_REQUEST_LABELS": "", // will be set if event is pr
"CI_PIPELINE_NUMBER": strconv.FormatInt(m.Curr.Number, 10),
"CI_PIPELINE_PARENT": strconv.FormatInt(m.Curr.Parent, 10),
"CI_PIPELINE_EVENT": m.Curr.Event,
"CI_PIPELINE_URL": m.Curr.Link,
"CI_PIPELINE_DEPLOY_TARGET": m.Curr.Target,
"CI_PIPELINE_STATUS": m.Curr.Status,
"CI_PIPELINE_CREATED": strconv.FormatInt(m.Curr.Created, 10),
"CI_PIPELINE_STARTED": strconv.FormatInt(m.Curr.Started, 10),
"CI_PIPELINE_FINISHED": strconv.FormatInt(m.Curr.Finished, 10),
"CI_WORKFLOW_NAME": m.Workflow.Name,
"CI_WORKFLOW_NUMBER": strconv.Itoa(m.Workflow.Number),
"CI_STEP_NAME": m.Step.Name,
"CI_STEP_NUMBER": strconv.Itoa(m.Step.Number),
"CI_STEP_STATUS": "", // will be set by agent
"CI_STEP_STARTED": "", // will be set by agent
"CI_STEP_FINISHED": "", // will be set by agent
"CI_PREV_COMMIT_SHA": m.Prev.Commit.Sha,
"CI_PREV_COMMIT_REF": m.Prev.Commit.Ref,
"CI_PREV_COMMIT_REFSPEC": m.Prev.Commit.Refspec,
"CI_PREV_COMMIT_BRANCH": m.Prev.Commit.Branch,
"CI_PREV_COMMIT_URL": m.Prev.Link,
"CI_PREV_COMMIT_MESSAGE": m.Prev.Commit.Message,
"CI_PREV_COMMIT_AUTHOR": m.Prev.Commit.Author.Name,
"CI_PREV_COMMIT_AUTHOR_EMAIL": m.Prev.Commit.Author.Email,
"CI_PREV_COMMIT_AUTHOR_AVATAR": m.Prev.Commit.Author.Avatar,
"CI_PREV_PIPELINE_NUMBER": strconv.FormatInt(m.Prev.Number, 10),
"CI_PREV_PIPELINE_PARENT": strconv.FormatInt(m.Prev.Parent, 10),
"CI_PREV_PIPELINE_EVENT": m.Prev.Event,
"CI_PREV_PIPELINE_URL": m.Prev.Link,
"CI_PREV_PIPELINE_DEPLOY_TARGET": m.Prev.Target,
"CI_PREV_PIPELINE_STATUS": m.Prev.Status,
"CI_PREV_PIPELINE_CREATED": strconv.FormatInt(m.Prev.Created, 10),
"CI_PREV_PIPELINE_STARTED": strconv.FormatInt(m.Prev.Started, 10),
"CI_PREV_PIPELINE_FINISHED": strconv.FormatInt(m.Prev.Finished, 10),
"CI_SYSTEM_NAME": m.Sys.Name,
"CI_SYSTEM_URL": m.Sys.Link,
"CI_SYSTEM_HOST": m.Sys.Host,
"CI_SYSTEM_PLATFORM": m.Sys.Platform, // will be set by pipeline platform option or by agent
"CI_SYSTEM_VERSION": m.Sys.Version,
"CI_FORGE_TYPE": m.Forge.Type,
"CI_FORGE_URL": m.Forge.URL,
// DEPRECATED
"CI_SYSTEM_ARCH": m.Sys.Platform, // TODO: remove after v1.0.x version
// use CI_PIPELINE_*
"CI_BUILD_NUMBER": strconv.FormatInt(m.Curr.Number, 10),
"CI_BUILD_PARENT": strconv.FormatInt(m.Curr.Parent, 10),
"CI_BUILD_EVENT": m.Curr.Event,
"CI_BUILD_LINK": m.Curr.Link,
"CI_BUILD_DEPLOY_TARGET": m.Curr.Target,
"CI_BUILD_STATUS": m.Curr.Status,
"CI_BUILD_CREATED": strconv.FormatInt(m.Curr.Created, 10),
"CI_BUILD_STARTED": strconv.FormatInt(m.Curr.Started, 10),
"CI_BUILD_FINISHED": strconv.FormatInt(m.Curr.Finished, 10),
// use CI_PREV_PIPELINE_*
"CI_PREV_BUILD_NUMBER": strconv.FormatInt(m.Prev.Number, 10),
"CI_PREV_BUILD_PARENT": strconv.FormatInt(m.Prev.Parent, 10),
"CI_PREV_BUILD_EVENT": m.Prev.Event,
"CI_PREV_BUILD_LINK": m.Prev.Link,
"CI_PREV_BUILD_DEPLOY_TARGET": m.Prev.Target,
"CI_PREV_BUILD_STATUS": m.Prev.Status,
"CI_PREV_BUILD_CREATED": strconv.FormatInt(m.Prev.Created, 10),
"CI_PREV_BUILD_STARTED": strconv.FormatInt(m.Prev.Started, 10),
"CI_PREV_BUILD_FINISHED": strconv.FormatInt(m.Prev.Finished, 10),
// use CI_STEP_*
"CI_JOB_NUMBER": strconv.Itoa(m.Step.Number),
"CI_JOB_STATUS": "", // will be set by agent
"CI_JOB_STARTED": "", // will be set by agent
"CI_JOB_FINISHED": "", // will be set by agent
// CI_REPO_CLONE_URL
"CI_REPO_REMOTE": m.Repo.CloneURL,
// use *_URL
"CI_REPO_LINK": m.Repo.Link,
"CI_COMMIT_LINK": m.Curr.Link,
"CI_PIPELINE_LINK": m.Curr.Link,
"CI_PREV_COMMIT_LINK": m.Prev.Link,
"CI_PREV_PIPELINE_LINK": m.Prev.Link,
"CI_SYSTEM_LINK": m.Sys.Link,
}
if m.Curr.Event == EventTag {
params["CI_COMMIT_TAG"] = strings.TrimPrefix(m.Curr.Commit.Ref, "refs/tags/")
}
if m.Curr.Event == EventPull {
params["CI_COMMIT_PULL_REQUEST"] = pullRegexp.FindString(m.Curr.Commit.Ref)
params["CI_COMMIT_PULL_REQUEST_LABELS"] = strings.Join(m.Curr.Commit.PullRequestLabels, ",")
}
return params
}

View File

@@ -0,0 +1,122 @@
// Copyright 2023 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 metadata
type (
// Metadata defines runtime m.
Metadata struct {
ID string `json:"id,omitempty"`
Repo Repo `json:"repo,omitempty"`
Curr Pipeline `json:"curr,omitempty"`
Prev Pipeline `json:"prev,omitempty"`
Workflow Workflow `json:"workflow,omitempty"`
Step Step `json:"step,omitempty"`
Sys System `json:"sys,omitempty"`
Forge Forge `json:"forge,omitempty"`
}
// Repo defines runtime metadata for a repository.
Repo struct {
Name string `json:"name,omitempty"`
Owner string `json:"owner,omitempty"`
RemoteID string `json:"remote_id,omitempty"`
Link string `json:"link,omitempty"`
CloneURL string `json:"clone_url,omitempty"`
Private bool `json:"private,omitempty"`
Secrets []Secret `json:"secrets,omitempty"`
Branch string `json:"default_branch,omitempty"`
Trusted bool `json:"trusted,omitempty"`
}
// Pipeline defines runtime metadata for a pipeline.
Pipeline struct {
Number int64 `json:"number,omitempty"`
Created int64 `json:"created,omitempty"`
Started int64 `json:"started,omitempty"`
Finished int64 `json:"finished,omitempty"`
Timeout int64 `json:"timeout,omitempty"`
Status string `json:"status,omitempty"`
Event string `json:"event,omitempty"`
Link string `json:"link,omitempty"`
Target string `json:"target,omitempty"`
Trusted bool `json:"trusted,omitempty"`
Commit Commit `json:"commit,omitempty"`
Parent int64 `json:"parent,omitempty"`
Cron string `json:"cron,omitempty"`
}
// Commit defines runtime metadata for a commit.
Commit struct {
Sha string `json:"sha,omitempty"`
Ref string `json:"ref,omitempty"`
Refspec string `json:"refspec,omitempty"`
Branch string `json:"branch,omitempty"`
Message string `json:"message,omitempty"`
Author Author `json:"author,omitempty"`
ChangedFiles []string `json:"changed_files,omitempty"`
PullRequestLabels []string `json:"labels,omitempty"`
}
// Author defines runtime metadata for a commit author.
Author struct {
Name string `json:"name,omitempty"`
Email string `json:"email,omitempty"`
Avatar string `json:"avatar,omitempty"`
}
// Workflow defines runtime metadata for a workflow.
Workflow struct {
Name string `json:"name,omitempty"`
Number int `json:"number,omitempty"`
Matrix map[string]string `json:"matrix,omitempty"`
}
// Step defines runtime metadata for a step.
Step struct {
Name string `json:"name,omitempty"`
Number int `json:"number,omitempty"`
}
// Secret defines a runtime secret
Secret struct {
Name string `json:"name,omitempty"`
Value string `json:"value,omitempty"`
Mount string `json:"mount,omitempty"`
Mask bool `json:"mask,omitempty"`
}
// System defines runtime metadata for a ci/cd system.
System struct {
Name string `json:"name,omitempty"`
Host string `json:"host,omitempty"`
Link string `json:"link,omitempty"`
Platform string `json:"arch,omitempty"`
Version string `json:"version,omitempty"`
}
// Forge defines runtime metadata about the forge that host the repo
Forge struct {
Type string `json:"type,omitempty"`
URL string `json:"url,omitempty"`
}
// ServerForge represent the needed func of a server forge to get its metadata
ServerForge interface {
// Name returns the string name of this driver
Name() string
// URL returns the root url of a configured forge
URL() string
}
)

View File

@@ -0,0 +1,132 @@
// Copyright 2023 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 frontend_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/woodpecker-ci/woodpecker/pipeline/frontend"
"github.com/woodpecker-ci/woodpecker/pipeline/frontend/metadata"
"github.com/woodpecker-ci/woodpecker/server/forge/mocks"
"github.com/woodpecker-ci/woodpecker/server/model"
)
func TestEnvVarSubst(t *testing.T) {
testCases := []struct {
name string
yaml string
environ map[string]string
want string
}{{
name: "simple substitution",
yaml: `pipeline:
step1:
image: ${HELLO_IMAGE}`,
environ: map[string]string{"HELLO_IMAGE": "hello-world"},
want: `pipeline:
step1:
image: hello-world`,
}}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
result, err := frontend.EnvVarSubst(testCase.yaml, testCase.environ)
assert.NoError(t, err)
assert.EqualValues(t, testCase.want, result)
})
}
}
func TestMetadataFromStruct(t *testing.T) {
forge := mocks.NewForge(t)
forge.On("Name").Return("gitea")
forge.On("URL").Return("https://gitea.com")
testCases := []struct {
name string
forge metadata.ServerForge
repo *model.Repo
pipeline, last *model.Pipeline
workflow *model.Step
link string
expectedMetadata metadata.Metadata
expectedEnviron map[string]string
}{
{
name: "Test with empty info",
expectedMetadata: metadata.Metadata{Sys: metadata.System{Name: "woodpecker"}},
expectedEnviron: map[string]string{
"CI": "woodpecker", "CI_BUILD_CREATED": "0", "CI_BUILD_DEPLOY_TARGET": "", "CI_BUILD_EVENT": "", "CI_BUILD_FINISHED": "0", "CI_BUILD_LINK": "", "CI_BUILD_NUMBER": "0", "CI_BUILD_PARENT": "0",
"CI_BUILD_STARTED": "0", "CI_BUILD_STATUS": "", "CI_COMMIT_AUTHOR": "", "CI_COMMIT_AUTHOR_AVATAR": "", "CI_COMMIT_AUTHOR_EMAIL": "", "CI_COMMIT_BRANCH": "", "CI_COMMIT_LINK": "",
"CI_COMMIT_MESSAGE": "", "CI_COMMIT_PULL_REQUEST": "", "CI_COMMIT_PULL_REQUEST_LABELS": "", "CI_COMMIT_REF": "", "CI_COMMIT_REFSPEC": "", "CI_COMMIT_SHA": "", "CI_COMMIT_SOURCE_BRANCH": "",
"CI_COMMIT_TAG": "", "CI_COMMIT_TARGET_BRANCH": "", "CI_COMMIT_URL": "", "CI_FORGE_TYPE": "", "CI_FORGE_URL": "", "CI_JOB_FINISHED": "", "CI_JOB_NUMBER": "0", "CI_JOB_STARTED": "",
"CI_JOB_STATUS": "", "CI_PIPELINE_CREATED": "0", "CI_PIPELINE_DEPLOY_TARGET": "", "CI_PIPELINE_EVENT": "", "CI_PIPELINE_FINISHED": "0", "CI_PIPELINE_LINK": "", "CI_PIPELINE_NUMBER": "0",
"CI_PIPELINE_PARENT": "0", "CI_PIPELINE_STARTED": "0", "CI_PIPELINE_STATUS": "", "CI_PIPELINE_URL": "", "CI_PREV_BUILD_CREATED": "0", "CI_PREV_BUILD_DEPLOY_TARGET": "",
"CI_PREV_BUILD_EVENT": "", "CI_PREV_BUILD_FINISHED": "0", "CI_PREV_BUILD_LINK": "", "CI_PREV_BUILD_NUMBER": "0", "CI_PREV_BUILD_PARENT": "0", "CI_PREV_BUILD_STARTED": "0",
"CI_PREV_BUILD_STATUS": "", "CI_PREV_COMMIT_AUTHOR": "", "CI_PREV_COMMIT_AUTHOR_AVATAR": "", "CI_PREV_COMMIT_AUTHOR_EMAIL": "", "CI_PREV_COMMIT_BRANCH": "", "CI_PREV_COMMIT_LINK": "",
"CI_PREV_COMMIT_MESSAGE": "", "CI_PREV_COMMIT_REF": "", "CI_PREV_COMMIT_REFSPEC": "", "CI_PREV_COMMIT_SHA": "", "CI_PREV_COMMIT_URL": "", "CI_PREV_PIPELINE_CREATED": "0",
"CI_PREV_PIPELINE_DEPLOY_TARGET": "", "CI_PREV_PIPELINE_EVENT": "", "CI_PREV_PIPELINE_FINISHED": "0", "CI_PREV_PIPELINE_LINK": "", "CI_PREV_PIPELINE_NUMBER": "0", "CI_PREV_PIPELINE_PARENT": "0",
"CI_PREV_PIPELINE_STARTED": "0", "CI_PREV_PIPELINE_STATUS": "", "CI_PREV_PIPELINE_URL": "", "CI_REPO": "", "CI_REPO_CLONE_URL": "", "CI_REPO_DEFAULT_BRANCH": "", "CI_REPO_LINK": "", "CI_REPO_REMOTE_ID": "",
"CI_REPO_NAME": "", "CI_REPO_OWNER": "", "CI_REPO_PRIVATE": "false", "CI_REPO_REMOTE": "", "CI_REPO_SCM": "git", "CI_REPO_TRUSTED": "false", "CI_REPO_URL": "", "CI_STEP_FINISHED": "",
"CI_STEP_NAME": "", "CI_STEP_NUMBER": "0", "CI_STEP_STARTED": "", "CI_STEP_STATUS": "", "CI_SYSTEM_ARCH": "", "CI_SYSTEM_HOST": "", "CI_SYSTEM_LINK": "", "CI_SYSTEM_NAME": "woodpecker",
"CI_SYSTEM_PLATFORM": "", "CI_SYSTEM_URL": "", "CI_SYSTEM_VERSION": "", "CI_WORKFLOW_NAME": "", "CI_WORKFLOW_NUMBER": "0",
},
},
{
name: "Test with forge",
forge: forge,
repo: &model.Repo{FullName: "testUser/testRepo", Link: "https://gitea.com/testUser/testRepo", Clone: "https://gitea.com/testUser/testRepo.git", Branch: "main", IsSCMPrivate: true},
pipeline: &model.Pipeline{Number: 3},
last: &model.Pipeline{Number: 2},
workflow: &model.Step{Name: "hello"},
link: "https://example.com",
expectedMetadata: metadata.Metadata{
Forge: metadata.Forge{Type: "gitea", URL: "https://gitea.com"},
Sys: metadata.System{Name: "woodpecker", Host: "example.com", Link: "https://example.com"},
Repo: metadata.Repo{Owner: "testUser", Name: "testRepo", Link: "https://gitea.com/testUser/testRepo", CloneURL: "https://gitea.com/testUser/testRepo.git", Branch: "main", Private: true},
Curr: metadata.Pipeline{Number: 3},
Prev: metadata.Pipeline{Number: 2},
Workflow: metadata.Workflow{Name: "hello"},
},
expectedEnviron: map[string]string{
"CI": "woodpecker", "CI_BUILD_CREATED": "0", "CI_BUILD_DEPLOY_TARGET": "", "CI_BUILD_EVENT": "", "CI_BUILD_FINISHED": "0", "CI_BUILD_LINK": "", "CI_BUILD_NUMBER": "3", "CI_BUILD_PARENT": "0",
"CI_BUILD_STARTED": "0", "CI_BUILD_STATUS": "", "CI_COMMIT_AUTHOR": "", "CI_COMMIT_AUTHOR_AVATAR": "", "CI_COMMIT_AUTHOR_EMAIL": "", "CI_COMMIT_BRANCH": "", "CI_COMMIT_LINK": "",
"CI_COMMIT_MESSAGE": "", "CI_COMMIT_PULL_REQUEST": "", "CI_COMMIT_PULL_REQUEST_LABELS": "", "CI_COMMIT_REF": "", "CI_COMMIT_REFSPEC": "", "CI_COMMIT_SHA": "", "CI_COMMIT_SOURCE_BRANCH": "",
"CI_COMMIT_TAG": "", "CI_COMMIT_TARGET_BRANCH": "", "CI_COMMIT_URL": "", "CI_FORGE_TYPE": "gitea", "CI_FORGE_URL": "https://gitea.com", "CI_JOB_FINISHED": "", "CI_JOB_NUMBER": "0",
"CI_JOB_STARTED": "", "CI_JOB_STATUS": "", "CI_PIPELINE_CREATED": "0", "CI_PIPELINE_DEPLOY_TARGET": "", "CI_PIPELINE_EVENT": "", "CI_PIPELINE_FINISHED": "0", "CI_PIPELINE_LINK": "",
"CI_PIPELINE_NUMBER": "3", "CI_PIPELINE_PARENT": "0", "CI_PIPELINE_STARTED": "0", "CI_PIPELINE_STATUS": "", "CI_PIPELINE_URL": "", "CI_PREV_BUILD_CREATED": "0", "CI_PREV_BUILD_DEPLOY_TARGET": "",
"CI_PREV_BUILD_EVENT": "", "CI_PREV_BUILD_FINISHED": "0", "CI_PREV_BUILD_LINK": "", "CI_PREV_BUILD_NUMBER": "2", "CI_PREV_BUILD_PARENT": "0", "CI_PREV_BUILD_STARTED": "0",
"CI_PREV_BUILD_STATUS": "", "CI_PREV_COMMIT_AUTHOR": "", "CI_PREV_COMMIT_AUTHOR_AVATAR": "", "CI_PREV_COMMIT_AUTHOR_EMAIL": "", "CI_PREV_COMMIT_BRANCH": "", "CI_PREV_COMMIT_LINK": "",
"CI_PREV_COMMIT_MESSAGE": "", "CI_PREV_COMMIT_REF": "", "CI_PREV_COMMIT_REFSPEC": "", "CI_PREV_COMMIT_SHA": "", "CI_PREV_COMMIT_URL": "", "CI_PREV_PIPELINE_CREATED": "0",
"CI_PREV_PIPELINE_DEPLOY_TARGET": "", "CI_PREV_PIPELINE_EVENT": "", "CI_PREV_PIPELINE_FINISHED": "0", "CI_PREV_PIPELINE_LINK": "", "CI_PREV_PIPELINE_NUMBER": "2", "CI_PREV_PIPELINE_PARENT": "0",
"CI_PREV_PIPELINE_STARTED": "0", "CI_PREV_PIPELINE_STATUS": "", "CI_PREV_PIPELINE_URL": "", "CI_REPO": "testUser/testRepo", "CI_REPO_CLONE_URL": "https://gitea.com/testUser/testRepo.git",
"CI_REPO_DEFAULT_BRANCH": "main", "CI_REPO_LINK": "https://gitea.com/testUser/testRepo", "CI_REPO_NAME": "testRepo", "CI_REPO_OWNER": "testUser", "CI_REPO_PRIVATE": "true", "CI_REPO_REMOTE_ID": "",
"CI_REPO_REMOTE": "https://gitea.com/testUser/testRepo.git", "CI_REPO_SCM": "git", "CI_REPO_TRUSTED": "false", "CI_REPO_URL": "https://gitea.com/testUser/testRepo", "CI_STEP_FINISHED": "",
"CI_STEP_NAME": "", "CI_STEP_NUMBER": "0", "CI_STEP_STARTED": "", "CI_STEP_STATUS": "", "CI_SYSTEM_ARCH": "", "CI_SYSTEM_HOST": "example.com", "CI_SYSTEM_LINK": "https://example.com",
"CI_SYSTEM_NAME": "woodpecker", "CI_SYSTEM_PLATFORM": "", "CI_SYSTEM_URL": "https://example.com", "CI_SYSTEM_VERSION": "", "CI_WORKFLOW_NAME": "hello", "CI_WORKFLOW_NUMBER": "0",
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
result := frontend.MetadataFromStruct(testCase.forge, testCase.repo, testCase.pipeline, testCase.last, testCase.workflow, testCase.link)
assert.EqualValues(t, testCase.expectedMetadata, result)
assert.EqualValues(t, testCase.expectedEnviron, result.Environ())
})
}
}

View File

@@ -2,10 +2,11 @@ package compiler
import (
"fmt"
"path"
"strings"
backend "github.com/woodpecker-ci/woodpecker/pipeline/backend/types"
"github.com/woodpecker-ci/woodpecker/pipeline/frontend"
"github.com/woodpecker-ci/woodpecker/pipeline/frontend/metadata"
"github.com/woodpecker-ci/woodpecker/pipeline/frontend/yaml"
"github.com/woodpecker-ci/woodpecker/shared/constant"
)
@@ -75,7 +76,7 @@ type Compiler struct {
cloneEnv map[string]string
base string
path string
metadata frontend.Metadata
metadata metadata.Metadata
registries []Registry
secrets secretMap
cacher Cacher
@@ -156,7 +157,7 @@ func (c *Compiler) Compile(conf *yaml.Config) (*backend.Config, error) {
// add default clone step
if !c.local && len(conf.Clone.Containers) == 0 && !conf.SkipClone {
cloneSettings := map[string]interface{}{"depth": "0"}
if c.metadata.Curr.Event == frontend.EventTag {
if c.metadata.Curr.Event == metadata.EventTag {
cloneSettings["tags"] = "true"
}
container := &yaml.Container{
@@ -263,7 +264,7 @@ func (c *Compiler) setupCache(conf *yaml.Config, ir *backend.Config) {
return
}
container := c.cacher.Restore(c.metadata.Repo.Name, c.metadata.Curr.Commit.Branch, conf.Cache)
container := c.cacher.Restore(path.Join(c.metadata.Repo.Owner, c.metadata.Repo.Name), c.metadata.Curr.Commit.Branch, conf.Cache)
name := fmt.Sprintf("%s_restore_cache", c.prefix)
step := c.createProcess(name, container, "cache")
@@ -276,10 +277,10 @@ func (c *Compiler) setupCache(conf *yaml.Config, ir *backend.Config) {
}
func (c *Compiler) setupCacheRebuild(conf *yaml.Config, ir *backend.Config) {
if c.local || len(conf.Cache) == 0 || c.metadata.Curr.Event != frontend.EventPush || c.cacher == nil {
if c.local || len(conf.Cache) == 0 || c.metadata.Curr.Event != metadata.EventPush || c.cacher == nil {
return
}
container := c.cacher.Rebuild(c.metadata.Repo.Name, c.metadata.Curr.Commit.Branch, conf.Cache)
container := c.cacher.Rebuild(path.Join(c.metadata.Repo.Owner, c.metadata.Repo.Name), c.metadata.Curr.Commit.Branch, conf.Cache)
name := fmt.Sprintf("%s_rebuild_cache", c.prefix)
step := c.createProcess(name, container, "cache")

View File

@@ -9,7 +9,7 @@ import (
"github.com/rs/zerolog/log"
backend "github.com/woodpecker-ci/woodpecker/pipeline/backend/types"
"github.com/woodpecker-ci/woodpecker/pipeline/frontend"
"github.com/woodpecker-ci/woodpecker/pipeline/frontend/metadata"
"github.com/woodpecker-ci/woodpecker/pipeline/frontend/yaml"
"github.com/woodpecker-ci/woodpecker/pipeline/frontend/yaml/compiler/settings"
)
@@ -152,7 +152,7 @@ func (c *Compiler) createProcess(name string, container *yaml.Container, section
failure := container.Failure
if container.Failure == "" {
failure = frontend.FailureFail
failure = metadata.FailureFail
}
return &backend.Step{

View File

@@ -20,7 +20,7 @@ import (
"path/filepath"
"strings"
"github.com/woodpecker-ci/woodpecker/pipeline/frontend"
"github.com/woodpecker-ci/woodpecker/pipeline/frontend/metadata"
)
// Option configures a compiler option.
@@ -67,7 +67,7 @@ func WithSecret(secrets ...Secret) Option {
// and system metadata. The metadata is used to remove steps from
// the compiled pipeline configuration that should be skipped. The
// metadata is also added to each container as environment variables.
func WithMetadata(metadata frontend.Metadata) Option {
func WithMetadata(metadata metadata.Metadata) Option {
return func(compiler *Compiler) {
compiler.metadata = metadata

View File

@@ -3,10 +3,9 @@ package compiler
import (
"os"
"reflect"
"strings"
"testing"
"github.com/woodpecker-ci/woodpecker/pipeline/frontend"
"github.com/woodpecker-ci/woodpecker/pipeline/frontend/metadata"
)
func TestWithWorkspace(t *testing.T) {
@@ -98,9 +97,10 @@ func TestWithPrefix(t *testing.T) {
}
func TestWithMetadata(t *testing.T) {
metadata := frontend.Metadata{
Repo: frontend.Repo{
Name: "octocat/hello-world",
metadata := metadata.Metadata{
Repo: metadata.Repo{
Owner: "octacat",
Name: "hello-world",
Private: true,
Link: "https://github.com/octocat/hello-world",
CloneURL: "https://github.com/octocat/hello-world.git",
@@ -113,7 +113,7 @@ func TestWithMetadata(t *testing.T) {
t.Errorf("WithMetadata must set compiler the metadata")
}
if compiler.env["CI_REPO_NAME"] != strings.Split(metadata.Repo.Name, "/")[1] {
if compiler.env["CI_REPO_NAME"] != metadata.Repo.Name {
t.Errorf("WithMetadata must set CI_REPO_NAME")
}
if compiler.env["CI_REPO_URL"] != metadata.Repo.Link {

View File

@@ -1,7 +1,10 @@
package yaml
import (
"fmt"
"codeberg.org/6543/xyaml"
"github.com/woodpecker-ci/woodpecker/pipeline/frontend/yaml/constraint"
"github.com/woodpecker-ci/woodpecker/pipeline/frontend/yaml/types"
)
@@ -23,7 +26,7 @@ type (
RunsOn []string `yaml:"runs_on,omitempty"`
SkipClone bool `yaml:"skip_clone"`
// Deprecated use When.Branch
Branches constraint.List
BranchesDontUseIt *constraint.List `yaml:"branches,omitempty"`
}
// Workspace defines a pipeline workspace.
@@ -41,6 +44,18 @@ func ParseBytes(b []byte) (*Config, error) {
return nil, err
}
// support deprecated branch filter
if out.BranchesDontUseIt != nil {
if out.When.Constraints == nil {
out.When.Constraints = []constraint.Constraint{{Branch: *out.BranchesDontUseIt}}
} else if len(out.When.Constraints) == 1 && out.When.Constraints[0].Branch.IsEmpty() {
out.When.Constraints[0].Branch = *out.BranchesDontUseIt
} else {
return nil, fmt.Errorf("could not apply deprecated branches filter into global when filter")
}
out.BranchesDontUseIt = nil
}
return out, nil
}

View File

@@ -5,7 +5,7 @@ import (
"github.com/franela/goblin"
"github.com/woodpecker-ci/woodpecker/pipeline/frontend"
"github.com/woodpecker-ci/woodpecker/pipeline/frontend/metadata"
"github.com/woodpecker-ci/woodpecker/pipeline/frontend/yaml/types"
)
@@ -79,8 +79,8 @@ func TestParse(t *testing.T) {
}
g.It("Should match event tester", func() {
match, err := matchConfig.When.Match(frontend.Metadata{
Curr: frontend.Pipeline{
match, err := matchConfig.When.Match(metadata.Metadata{
Curr: metadata.Pipeline{
Event: "tester",
},
}, false)
@@ -89,8 +89,8 @@ func TestParse(t *testing.T) {
})
g.It("Should match event tester2", func() {
match, err := matchConfig.When.Match(frontend.Metadata{
Curr: frontend.Pipeline{
match, err := matchConfig.When.Match(metadata.Metadata{
Curr: metadata.Pipeline{
Event: "tester2",
},
}, false)
@@ -99,9 +99,9 @@ func TestParse(t *testing.T) {
})
g.It("Should match branch tester", func() {
match, err := matchConfig.When.Match(frontend.Metadata{
Curr: frontend.Pipeline{
Commit: frontend.Commit{
match, err := matchConfig.When.Match(metadata.Metadata{
Curr: metadata.Pipeline{
Commit: metadata.Commit{
Branch: "tester",
},
},
@@ -111,8 +111,8 @@ func TestParse(t *testing.T) {
})
g.It("Should not match event push", func() {
match, err := matchConfig.When.Match(frontend.Metadata{
Curr: frontend.Pipeline{
match, err := matchConfig.When.Match(metadata.Metadata{
Curr: metadata.Pipeline{
Event: "push",
},
}, false)

View File

@@ -3,13 +3,14 @@ package constraint
import (
"errors"
"fmt"
"path"
"strings"
"github.com/antonmedv/expr"
"github.com/bmatcuk/doublestar/v4"
"gopkg.in/yaml.v3"
"github.com/woodpecker-ci/woodpecker/pipeline/frontend"
"github.com/woodpecker-ci/woodpecker/pipeline/frontend/metadata"
"github.com/woodpecker-ci/woodpecker/pipeline/frontend/yaml/types"
)
@@ -61,7 +62,7 @@ func (when *When) IsEmpty() bool {
}
// Returns true if at least one of the internal constraints is true.
func (when *When) Match(metadata frontend.Metadata, global bool) (bool, error) {
func (when *When) Match(metadata metadata.Metadata, global bool) (bool, error) {
for _, c := range when.Constraints {
match, err := c.Match(metadata, global)
if err != nil {
@@ -138,37 +139,37 @@ func (when *When) UnmarshalYAML(value *yaml.Node) error {
// Match returns true if all constraints match the given input. If a single
// constraint fails a false value is returned.
func (c *Constraint) Match(metadata frontend.Metadata, global bool) (bool, error) {
func (c *Constraint) Match(m metadata.Metadata, global bool) (bool, error) {
match := true
if !global {
c.SetDefaultEventFilter()
// apply step only filters
match = c.Matrix.Match(metadata.Workflow.Matrix)
match = c.Matrix.Match(m.Workflow.Matrix)
}
match = match && c.Platform.Match(metadata.Sys.Platform) &&
c.Environment.Match(metadata.Curr.Target) &&
c.Event.Match(metadata.Curr.Event) &&
c.Repo.Match(metadata.Repo.Name) &&
c.Ref.Match(metadata.Curr.Commit.Ref) &&
c.Instance.Match(metadata.Sys.Host)
match = match && c.Platform.Match(m.Sys.Platform) &&
c.Environment.Match(m.Curr.Target) &&
c.Event.Match(m.Curr.Event) &&
c.Repo.Match(path.Join(m.Repo.Owner, m.Repo.Name)) &&
c.Ref.Match(m.Curr.Commit.Ref) &&
c.Instance.Match(m.Sys.Host)
// changed files filter apply only for pull-request and push events
if metadata.Curr.Event == frontend.EventPull || metadata.Curr.Event == frontend.EventPush {
match = match && c.Path.Match(metadata.Curr.Commit.ChangedFiles, metadata.Curr.Commit.Message)
if m.Curr.Event == metadata.EventPull || m.Curr.Event == metadata.EventPush {
match = match && c.Path.Match(m.Curr.Commit.ChangedFiles, m.Curr.Commit.Message)
}
if metadata.Curr.Event != frontend.EventTag {
match = match && c.Branch.Match(metadata.Curr.Commit.Branch)
if m.Curr.Event != metadata.EventTag {
match = match && c.Branch.Match(m.Curr.Commit.Branch)
}
if metadata.Curr.Event == frontend.EventCron {
match = match && c.Cron.Match(metadata.Curr.Cron)
if m.Curr.Event == metadata.EventCron {
match = match && c.Cron.Match(m.Curr.Cron)
}
if c.Evaluate != "" {
env := metadata.Environ()
env := m.Environ()
out, err := expr.Compile(c.Evaluate, expr.Env(env), expr.AsBool())
if err != nil {
return false, err
@@ -187,11 +188,11 @@ func (c *Constraint) Match(metadata frontend.Metadata, global bool) (bool, error
func (c *Constraint) SetDefaultEventFilter() {
if c.Event.IsEmpty() {
c.Event.Include = []string{
frontend.EventPush,
frontend.EventPull,
frontend.EventTag,
frontend.EventDeploy,
frontend.EventManual,
metadata.EventPush,
metadata.EventPull,
metadata.EventTag,
metadata.EventDeploy,
metadata.EventManual,
}
}
}

View File

@@ -7,6 +7,7 @@ import (
"gopkg.in/yaml.v3"
"github.com/woodpecker-ci/woodpecker/pipeline/frontend"
"github.com/woodpecker-ci/woodpecker/pipeline/frontend/metadata"
)
func TestConstraint(t *testing.T) {
@@ -403,15 +404,15 @@ func TestConstraints(t *testing.T) {
testdata := []struct {
desc string
conf string
with frontend.Metadata
with metadata.Metadata
want bool
}{
{
desc: "no constraints, must match on default events",
conf: "",
with: frontend.Metadata{
Curr: frontend.Pipeline{
Event: frontend.EventPush,
with: metadata.Metadata{
Curr: metadata.Pipeline{
Event: metadata.EventPush,
},
},
want: true,
@@ -419,106 +420,117 @@ func TestConstraints(t *testing.T) {
{
desc: "global branch filter",
conf: "{ branch: develop }",
with: frontend.Metadata{Curr: frontend.Pipeline{Event: frontend.EventPush, Commit: frontend.Commit{Branch: "master"}}},
with: metadata.Metadata{Curr: metadata.Pipeline{Event: metadata.EventPush, Commit: metadata.Commit{Branch: "master"}}},
want: false,
},
{
desc: "global branch filter",
conf: "{ branch: master }",
with: frontend.Metadata{Curr: frontend.Pipeline{Event: frontend.EventPush, Commit: frontend.Commit{Branch: "master"}}},
with: metadata.Metadata{Curr: metadata.Pipeline{Event: metadata.EventPush, Commit: metadata.Commit{Branch: "master"}}},
want: true,
},
{
desc: "repo constraint",
conf: "{ repo: owner/* }",
with: frontend.Metadata{Curr: frontend.Pipeline{Event: frontend.EventPush}, Repo: frontend.Repo{Name: "owner/repo"}},
with: metadata.Metadata{Curr: metadata.Pipeline{Event: metadata.EventPush}, Repo: metadata.Repo{Owner: "owner", Name: "repo"}},
want: true,
},
{
desc: "repo constraint",
conf: "{ repo: octocat/* }",
with: frontend.Metadata{Curr: frontend.Pipeline{Event: frontend.EventPush}, Repo: frontend.Repo{Name: "owner/repo"}},
with: metadata.Metadata{Curr: metadata.Pipeline{Event: metadata.EventPush}, Repo: metadata.Repo{Owner: "owner", Name: "repo"}},
want: false,
},
{
desc: "ref constraint",
conf: "{ ref: refs/tags/* }",
with: frontend.Metadata{Curr: frontend.Pipeline{Commit: frontend.Commit{Ref: "refs/tags/v1.0.0"}, Event: frontend.EventPush}},
with: metadata.Metadata{Curr: metadata.Pipeline{Commit: metadata.Commit{Ref: "refs/tags/v1.0.0"}, Event: metadata.EventPush}},
want: true,
},
{
desc: "ref constraint",
conf: "{ ref: refs/tags/* }",
with: frontend.Metadata{Curr: frontend.Pipeline{Commit: frontend.Commit{Ref: "refs/heads/master"}, Event: frontend.EventPush}},
with: metadata.Metadata{Curr: metadata.Pipeline{Commit: metadata.Commit{Ref: "refs/heads/master"}, Event: metadata.EventPush}},
want: false,
},
{
desc: "platform constraint",
conf: "{ platform: linux/amd64 }",
with: frontend.Metadata{Curr: frontend.Pipeline{Event: frontend.EventPush}, Sys: frontend.System{Platform: "linux/amd64"}},
with: metadata.Metadata{Curr: metadata.Pipeline{Event: metadata.EventPush}, Sys: metadata.System{Platform: "linux/amd64"}},
want: true,
},
{
desc: "platform constraint",
conf: "{ repo: linux/amd64 }",
with: frontend.Metadata{Curr: frontend.Pipeline{Event: frontend.EventPush}, Sys: frontend.System{Platform: "windows/amd64"}},
with: metadata.Metadata{Curr: metadata.Pipeline{Event: metadata.EventPush}, Sys: metadata.System{Platform: "windows/amd64"}},
want: false,
},
{
desc: "instance constraint",
conf: "{ instance: agent.tld }",
with: frontend.Metadata{Curr: frontend.Pipeline{Event: frontend.EventPush}, Sys: frontend.System{Host: "agent.tld"}},
with: metadata.Metadata{Curr: metadata.Pipeline{Event: metadata.EventPush}, Sys: metadata.System{Host: "agent.tld"}},
want: true,
},
{
desc: "instance constraint",
conf: "{ instance: agent.tld }",
with: frontend.Metadata{Curr: frontend.Pipeline{Event: frontend.EventPush}, Sys: frontend.System{Host: "beta.agent.tld"}},
with: metadata.Metadata{Curr: metadata.Pipeline{Event: metadata.EventPush}, Sys: metadata.System{Host: "beta.agent.tld"}},
want: false,
},
{
desc: "filter cron by default constraint",
conf: "{}",
with: frontend.Metadata{Curr: frontend.Pipeline{Event: frontend.EventCron}},
with: metadata.Metadata{Curr: metadata.Pipeline{Event: metadata.EventCron}},
want: false,
},
{
desc: "filter cron by matching name",
conf: "{ event: cron, cron: job1 }",
with: frontend.Metadata{Curr: frontend.Pipeline{Event: frontend.EventCron, Cron: "job1"}},
with: metadata.Metadata{Curr: metadata.Pipeline{Event: metadata.EventCron, Cron: "job1"}},
want: true,
},
{
desc: "filter cron by name",
conf: "{ event: cron, cron: job2 }",
with: frontend.Metadata{Curr: frontend.Pipeline{Event: frontend.EventCron, Cron: "job1"}},
with: metadata.Metadata{Curr: metadata.Pipeline{Event: metadata.EventCron, Cron: "job1"}},
want: false,
},
{
desc: "no constraints, event gets filtered by default event filter",
conf: "",
with: frontend.Metadata{
Curr: frontend.Pipeline{Event: "non-default"},
with: metadata.Metadata{
Curr: metadata.Pipeline{Event: "non-default"},
},
want: false,
},
{
desc: "filter with build-in env passes",
conf: "{ branch: ${CI_REPO_DEFAULT_BRANCH} }",
with: metadata.Metadata{
Curr: metadata.Pipeline{Event: metadata.EventPush, Commit: metadata.Commit{Branch: "stable"}},
Repo: metadata.Repo{Branch: "stable"},
},
want: true,
},
{
desc: "filter by eval based on event",
conf: `{ evaluate: 'CI_PIPELINE_EVENT == "push"' }`,
with: frontend.Metadata{Curr: frontend.Pipeline{Event: frontend.EventPush}},
with: metadata.Metadata{Curr: metadata.Pipeline{Event: metadata.EventPush}},
want: true,
},
{
desc: "filter by eval based on event and repo",
conf: `{ evaluate: 'CI_PIPELINE_EVENT == "push" && CI_REPO == "owner/repo"' }`,
with: frontend.Metadata{Curr: frontend.Pipeline{Event: frontend.EventPush}, Repo: frontend.Repo{Name: "owner/repo"}},
with: metadata.Metadata{Curr: metadata.Pipeline{Event: metadata.EventPush}, Repo: metadata.Repo{Owner: "owner", Name: "repo"}},
want: true,
},
}
for _, test := range testdata {
t.Run(test.desc, func(t *testing.T) {
c := parseConstraints(t, test.conf)
conf, err := frontend.EnvVarSubst(test.conf, test.with.Environ())
assert.NoError(t, err)
c := parseConstraints(t, conf)
got, err := c.Match(test.with, false)
if err != nil {
t.Errorf("Match returned error: %v", err)