mirror of
https://code.forgejo.org/forgejo/runner.git
synced 2025-08-06 17:40:58 +00:00
chore: use the same .golangci.yml as the runner & gofumpt over gofmt (#206)
To prepare for a smooth merge in the runner codebase. - run with --fix for gofumpt and golangci - manual edits for - disabling useless package naming warning - rename variables that had underscore in their name - remove trailing else at the end of a few functions Reviewed-on: https://code.forgejo.org/forgejo/act/pulls/206 Reviewed-by: Michael Kriese <michael.kriese@gmx.de> Co-authored-by: Earl Warren <contact@earl-warren.org> Co-committed-by: Earl Warren <contact@earl-warren.org>
This commit is contained in:
parent
ed98625ae9
commit
c377159121
37 changed files with 138 additions and 140 deletions
|
@ -18,35 +18,39 @@ import (
|
||||||
"go.etcd.io/bbolt"
|
"go.etcd.io/bbolt"
|
||||||
)
|
)
|
||||||
|
|
||||||
const cache_repo = "testuser/repo"
|
const (
|
||||||
const cache_runnum = "1"
|
cacheRepo = "testuser/repo"
|
||||||
const cache_timestamp = "0"
|
cacheRunnum = "1"
|
||||||
const cache_mac = "c13854dd1ac599d1d61680cd93c26b77ba0ee10f374a3408bcaea82f38ca1865"
|
cacheTimestamp = "0"
|
||||||
|
cacheMac = "c13854dd1ac599d1d61680cd93c26b77ba0ee10f374a3408bcaea82f38ca1865"
|
||||||
|
)
|
||||||
|
|
||||||
var handlerExternalUrl string
|
var handlerExternalURL string
|
||||||
|
|
||||||
type AuthHeaderTransport struct {
|
type AuthHeaderTransport struct {
|
||||||
T http.RoundTripper
|
T http.RoundTripper
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *AuthHeaderTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
func (t *AuthHeaderTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
req.Header.Set("Forgejo-Cache-Repo", cache_repo)
|
req.Header.Set("Forgejo-Cache-Repo", cacheRepo)
|
||||||
req.Header.Set("Forgejo-Cache-RunNumber", cache_runnum)
|
req.Header.Set("Forgejo-Cache-RunNumber", cacheRunnum)
|
||||||
req.Header.Set("Forgejo-Cache-Timestamp", cache_timestamp)
|
req.Header.Set("Forgejo-Cache-Timestamp", cacheTimestamp)
|
||||||
req.Header.Set("Forgejo-Cache-MAC", cache_mac)
|
req.Header.Set("Forgejo-Cache-MAC", cacheMac)
|
||||||
req.Header.Set("Forgejo-Cache-Host", handlerExternalUrl)
|
req.Header.Set("Forgejo-Cache-Host", handlerExternalURL)
|
||||||
return t.T.RoundTrip(req)
|
return t.T.RoundTrip(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
var httpClientTransport = AuthHeaderTransport{http.DefaultTransport}
|
var (
|
||||||
var httpClient = http.Client{Transport: &httpClientTransport}
|
httpClientTransport = AuthHeaderTransport{http.DefaultTransport}
|
||||||
|
httpClient = http.Client{Transport: &httpClientTransport}
|
||||||
|
)
|
||||||
|
|
||||||
func TestHandler(t *testing.T) {
|
func TestHandler(t *testing.T) {
|
||||||
dir := filepath.Join(t.TempDir(), "artifactcache")
|
dir := filepath.Join(t.TempDir(), "artifactcache")
|
||||||
handler, err := StartHandler(dir, "", 0, "secret", nil)
|
handler, err := StartHandler(dir, "", 0, "secret", nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
handlerExternalUrl = handler.ExternalURL()
|
handlerExternalURL = handler.ExternalURL()
|
||||||
base := fmt.Sprintf("%s%s", handler.ExternalURL(), urlBase)
|
base := fmt.Sprintf("%s%s", handler.ExternalURL(), urlBase)
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
|
@ -386,11 +390,11 @@ func TestHandler(t *testing.T) {
|
||||||
// Perform the same request with incorrect MAC data
|
// Perform the same request with incorrect MAC data
|
||||||
req, err := http.NewRequest("GET", fmt.Sprintf("%s/cache?keys=%s&version=%s", base, key, version), nil)
|
req, err := http.NewRequest("GET", fmt.Sprintf("%s/cache?keys=%s&version=%s", base, key, version), nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
req.Header.Set("Forgejo-Cache-Repo", cache_repo)
|
req.Header.Set("Forgejo-Cache-Repo", cacheRepo)
|
||||||
req.Header.Set("Forgejo-Cache-RunNumber", cache_runnum)
|
req.Header.Set("Forgejo-Cache-RunNumber", cacheRunnum)
|
||||||
req.Header.Set("Forgejo-Cache-Timestamp", cache_timestamp)
|
req.Header.Set("Forgejo-Cache-Timestamp", cacheTimestamp)
|
||||||
req.Header.Set("Forgejo-Cache-MAC", "33f0e850ba0bdfd2f3e66ff79c1f8004b8226114e3b2e65c229222bb59df0f9d") // ! This is not the correct MAC
|
req.Header.Set("Forgejo-Cache-MAC", "33f0e850ba0bdfd2f3e66ff79c1f8004b8226114e3b2e65c229222bb59df0f9d") // ! This is not the correct MAC
|
||||||
req.Header.Set("Forgejo-Cache-Host", handlerExternalUrl)
|
req.Header.Set("Forgejo-Cache-Host", handlerExternalURL)
|
||||||
resp, err = http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, key, version))
|
resp, err = http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, key, version))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, 403, resp.StatusCode)
|
require.Equal(t, 403, resp.StatusCode)
|
||||||
|
|
|
@ -14,9 +14,7 @@ import (
|
||||||
"github.com/nektos/act/pkg/cacheproxy"
|
"github.com/nektos/act/pkg/cacheproxy"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var ErrValidation = errors.New("validation error")
|
||||||
ErrValidation = errors.New("validation error")
|
|
||||||
)
|
|
||||||
|
|
||||||
func (h *Handler) validateMac(rundata cacheproxy.RunData) (string, error) {
|
func (h *Handler) validateMac(rundata cacheproxy.RunData) (string, error) {
|
||||||
// TODO: allow configurable max age
|
// TODO: allow configurable max age
|
||||||
|
|
|
@ -31,7 +31,7 @@ func (s *Storage) Exist(id uint64) (bool, error) {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Storage) Write(id uint64, offset uint64, reader io.Reader) error {
|
func (s *Storage) Write(id, offset uint64, reader io.Reader) error {
|
||||||
name := s.tempName(id, offset)
|
name := s.tempName(id, offset)
|
||||||
if err := os.MkdirAll(filepath.Dir(name), 0o755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(name), 0o755); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -110,7 +110,7 @@ func (s *Storage) tempDir(id uint64) string {
|
||||||
return filepath.Join(s.rootDir, "tmp", fmt.Sprint(id))
|
return filepath.Join(s.rootDir, "tmp", fmt.Sprint(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Storage) tempName(id uint64, offset uint64) string {
|
func (s *Storage) tempName(id, offset uint64) string {
|
||||||
return filepath.Join(s.tempDir(id), fmt.Sprintf("%016x", offset))
|
return filepath.Join(s.tempDir(id), fmt.Sprintf("%016x", offset))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,9 +29,7 @@ const (
|
||||||
urlBase = "/_apis/artifactcache"
|
urlBase = "/_apis/artifactcache"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var urlRegex = regexp.MustCompile(`/(\w+)(/_apis/artifactcache/.+)`)
|
||||||
urlRegex = regexp.MustCompile(`/(\w+)(/_apis/artifactcache/.+)`)
|
|
||||||
)
|
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
router *httprouter.Router
|
router *httprouter.Router
|
||||||
|
@ -55,7 +53,7 @@ type RunData struct {
|
||||||
RepositoryMAC string
|
RepositoryMAC string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) CreateRunData(fullName string, runNumber string, timestamp string) RunData {
|
func (h *Handler) CreateRunData(fullName, runNumber, timestamp string) RunData {
|
||||||
mac := computeMac(h.cacheSecret, fullName, runNumber, timestamp)
|
mac := computeMac(h.cacheSecret, fullName, runNumber, timestamp)
|
||||||
return RunData{
|
return RunData{
|
||||||
RepositoryFullName: fullName,
|
RepositoryFullName: fullName,
|
||||||
|
@ -65,7 +63,7 @@ func (h *Handler) CreateRunData(fullName string, runNumber string, timestamp str
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func StartHandler(targetHost string, outboundIP string, port uint16, cacheSecret string, logger logrus.FieldLogger) (*Handler, error) {
|
func StartHandler(targetHost, outboundIP string, port uint16, cacheSecret string, logger logrus.FieldLogger) (*Handler, error) {
|
||||||
h := &Handler{}
|
h := &Handler{}
|
||||||
|
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
|
@ -142,7 +140,7 @@ func (h *Handler) newReverseProxy(targetHost string) (*httputil.ReverseProxy, er
|
||||||
h.logger.Warn(fmt.Sprintf("Tried starting a cache proxy with id %s, which does not exist.", id))
|
h.logger.Warn(fmt.Sprintf("Tried starting a cache proxy with id %s, which does not exist.", id))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var runData = data.(RunData)
|
runData := data.(RunData)
|
||||||
uri := matches[2]
|
uri := matches[2]
|
||||||
|
|
||||||
r.SetURL(targetURL)
|
r.SetURL(targetURL)
|
||||||
|
|
|
@ -72,6 +72,7 @@ func (p *Pen) drawTopBars(buf io.Writer, labels ...string) {
|
||||||
}
|
}
|
||||||
fmt.Fprintf(buf, "\n")
|
fmt.Fprintf(buf, "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Pen) drawBottomBars(buf io.Writer, labels ...string) {
|
func (p *Pen) drawBottomBars(buf io.Writer, labels ...string) {
|
||||||
style := styleDefs[p.style]
|
style := styleDefs[p.style]
|
||||||
for _, label := range labels {
|
for _, label := range labels {
|
||||||
|
@ -83,6 +84,7 @@ func (p *Pen) drawBottomBars(buf io.Writer, labels ...string) {
|
||||||
}
|
}
|
||||||
fmt.Fprintf(buf, "\n")
|
fmt.Fprintf(buf, "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Pen) drawLabels(buf io.Writer, labels ...string) {
|
func (p *Pen) drawLabels(buf io.Writer, labels ...string) {
|
||||||
style := styleDefs[p.style]
|
style := styleDefs[p.style]
|
||||||
for _, label := range labels {
|
for _, label := range labels {
|
||||||
|
|
|
@ -68,7 +68,7 @@ func NewPipelineExecutor(executors ...Executor) Executor {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewConditionalExecutor creates a new executor based on conditions
|
// NewConditionalExecutor creates a new executor based on conditions
|
||||||
func NewConditionalExecutor(conditional Conditional, trueExecutor Executor, falseExecutor Executor) Executor {
|
func NewConditionalExecutor(conditional Conditional, trueExecutor, falseExecutor Executor) Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
if conditional(ctx) {
|
if conditional(ctx) {
|
||||||
if trueExecutor != nil {
|
if trueExecutor != nil {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// CopyFile copy file
|
// CopyFile copy file
|
||||||
func CopyFile(source string, dest string) (err error) {
|
func CopyFile(source, dest string) (err error) {
|
||||||
sourcefile, err := os.Open(source)
|
sourcefile, err := os.Open(source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -30,11 +30,11 @@ func CopyFile(source string, dest string) (err error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// CopyDir recursive copy of directory
|
// CopyDir recursive copy of directory
|
||||||
func CopyDir(source string, dest string) (err error) {
|
func CopyDir(source, dest string) (err error) {
|
||||||
// get properties of source dir
|
// get properties of source dir
|
||||||
sourceinfo, err := os.Stat(source)
|
sourceinfo, err := os.Stat(source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -52,7 +52,7 @@ func (e *Error) Commit() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindGitRevision get the current git revision
|
// FindGitRevision get the current git revision
|
||||||
func FindGitRevision(ctx context.Context, file string) (shortSha string, sha string, err error) {
|
func FindGitRevision(ctx context.Context, file string) (shortSha, sha string, err error) {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
|
|
||||||
gitDir, err := git.PlainOpenWithOptions(
|
gitDir, err := git.PlainOpenWithOptions(
|
||||||
|
@ -62,7 +62,6 @@ func FindGitRevision(ctx context.Context, file string) (shortSha string, sha str
|
||||||
EnableDotGitCommonDir: true,
|
EnableDotGitCommonDir: true,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.WithError(err).Error("path", file, "not located inside a git repository")
|
logger.WithError(err).Error("path", file, "not located inside a git repository")
|
||||||
return "", "", err
|
return "", "", err
|
||||||
|
@ -96,8 +95,8 @@ func FindGitRef(ctx context.Context, file string) (string, error) {
|
||||||
logger.Debugf("HEAD points to '%s'", ref)
|
logger.Debugf("HEAD points to '%s'", ref)
|
||||||
|
|
||||||
// Prefer the git library to iterate over the references and find a matching tag or branch.
|
// Prefer the git library to iterate over the references and find a matching tag or branch.
|
||||||
var refTag = ""
|
refTag := ""
|
||||||
var refBranch = ""
|
refBranch := ""
|
||||||
repo, err := git.PlainOpenWithOptions(
|
repo, err := git.PlainOpenWithOptions(
|
||||||
file,
|
file,
|
||||||
&git.PlainOpenOptions{
|
&git.PlainOpenOptions{
|
||||||
|
@ -105,7 +104,6 @@ func FindGitRef(ctx context.Context, file string) (string, error) {
|
||||||
EnableDotGitCommonDir: true,
|
EnableDotGitCommonDir: true,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -144,7 +142,6 @@ func FindGitRef(ctx context.Context, file string) (string, error) {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -198,7 +195,7 @@ func findGitRemoteURL(_ context.Context, file, remoteName string) (string, error
|
||||||
return remote.Config().URLs[0], nil
|
return remote.Config().URLs[0], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func findGitSlug(url string, githubInstance string) (string, string, error) {
|
func findGitSlug(url, githubInstance string) (string, string, error) {
|
||||||
if matches := codeCommitHTTPRegex.FindStringSubmatch(url); matches != nil {
|
if matches := codeCommitHTTPRegex.FindStringSubmatch(url); matches != nil {
|
||||||
return "CodeCommit", matches[2], nil
|
return "CodeCommit", matches[2], nil
|
||||||
} else if matches := codeCommitSSHRegex.FindStringSubmatch(url); matches != nil {
|
} else if matches := codeCommitSSHRegex.FindStringSubmatch(url); matches != nil {
|
||||||
|
|
|
@ -19,7 +19,7 @@ import (
|
||||||
func TestFindGitSlug(t *testing.T) {
|
func TestFindGitSlug(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
var slugTests = []struct {
|
slugTests := []struct {
|
||||||
url string // input
|
url string // input
|
||||||
provider string // expected result
|
provider string // expected result
|
||||||
slug string // expected result
|
slug string // expected result
|
||||||
|
|
|
@ -49,11 +49,11 @@ type FileEntry struct {
|
||||||
|
|
||||||
// Container for managing docker run containers
|
// Container for managing docker run containers
|
||||||
type Container interface {
|
type Container interface {
|
||||||
Create(capAdd []string, capDrop []string) common.Executor
|
Create(capAdd, capDrop []string) common.Executor
|
||||||
ConnectToNetwork(name string) common.Executor
|
ConnectToNetwork(name string) common.Executor
|
||||||
Copy(destPath string, files ...*FileEntry) common.Executor
|
Copy(destPath string, files ...*FileEntry) common.Executor
|
||||||
CopyTarStream(ctx context.Context, destPath string, tarStream io.Reader) error
|
CopyTarStream(ctx context.Context, destPath string, tarStream io.Reader) error
|
||||||
CopyDir(destPath string, srcPath string, useGitIgnore bool) common.Executor
|
CopyDir(destPath, srcPath string, useGitIgnore bool) common.Executor
|
||||||
GetContainerArchive(ctx context.Context, srcPath string) (io.ReadCloser, error)
|
GetContainerArchive(ctx context.Context, srcPath string) (io.ReadCloser, error)
|
||||||
Pull(forcePull bool) common.Executor
|
Pull(forcePull bool) common.Executor
|
||||||
Start(attach bool) common.Executor
|
Start(attach bool) common.Executor
|
||||||
|
|
|
@ -68,7 +68,8 @@ func NewDockerBuildExecutor(input NewDockerBuildExecutorInput) common.Executor {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func createBuildContext(ctx context.Context, contextDir string, relDockerfile string) (io.ReadCloser, error) {
|
|
||||||
|
func createBuildContext(ctx context.Context, contextDir, relDockerfile string) (io.ReadCloser, error) {
|
||||||
common.Logger(ctx).Debugf("Creating archive for build context dir '%s' with relative dockerfile '%s'", contextDir, relDockerfile)
|
common.Logger(ctx).Debugf("Creating archive for build context dir '%s' with relative dockerfile '%s'", contextDir, relDockerfile)
|
||||||
|
|
||||||
// And canonicalize dockerfile name to a platform-independent one
|
// And canonicalize dockerfile name to a platform-independent one
|
||||||
|
@ -95,7 +96,7 @@ func createBuildContext(ctx context.Context, contextDir string, relDockerfile st
|
||||||
// removed. The daemon will remove them for us, if needed, after it
|
// removed. The daemon will remove them for us, if needed, after it
|
||||||
// parses the Dockerfile. Ignore errors here, as they will have been
|
// parses the Dockerfile. Ignore errors here, as they will have been
|
||||||
// caught by validateContextDirectory above.
|
// caught by validateContextDirectory above.
|
||||||
var includes = []string{"."}
|
includes := []string{"."}
|
||||||
keepThem1, _ := patternmatcher.Matches(".dockerignore", excludes)
|
keepThem1, _ := patternmatcher.Matches(".dockerignore", excludes)
|
||||||
keepThem2, _ := patternmatcher.Matches(relDockerfile, excludes)
|
keepThem2, _ := patternmatcher.Matches(relDockerfile, excludes)
|
||||||
if keepThem1 || keepThem2 {
|
if keepThem1 || keepThem2 {
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
// See DOCKER_LICENSE for the full license text.
|
// See DOCKER_LICENSE for the full license text.
|
||||||
//
|
//
|
||||||
|
|
||||||
//nolint:unparam,errcheck,depguard,unused
|
//nolint:unparam,errcheck,unused
|
||||||
package container
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
// See DOCKER_LICENSE for the full license text.
|
// See DOCKER_LICENSE for the full license text.
|
||||||
//
|
//
|
||||||
|
|
||||||
//nolint:unparam,whitespace,depguard,dupl,gocritic
|
//nolint:unparam,gocritic
|
||||||
package container
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -192,7 +192,6 @@ func TestParseRunWithInvalidArgs(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseWithVolumes(t *testing.T) {
|
func TestParseWithVolumes(t *testing.T) {
|
||||||
|
|
||||||
// A single volume
|
// A single volume
|
||||||
arr, tryit := setupPlatformVolume([]string{`/tmp`}, []string{`c:\tmp`})
|
arr, tryit := setupPlatformVolume([]string{`/tmp`}, []string{`c:\tmp`})
|
||||||
if config, hostConfig := mustParse(t, tryit); hostConfig.Binds != nil {
|
if config, hostConfig := mustParse(t, tryit); hostConfig.Binds != nil {
|
||||||
|
@ -260,14 +259,13 @@ func TestParseWithVolumes(t *testing.T) {
|
||||||
t.Fatalf("Error parsing %s. Should have a single bind mount and no volumes", arr[0])
|
t.Fatalf("Error parsing %s. Should have a single bind mount and no volumes", arr[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// setupPlatformVolume takes two arrays of volume specs - a Unix style
|
// setupPlatformVolume takes two arrays of volume specs - a Unix style
|
||||||
// spec and a Windows style spec. Depending on the platform being unit tested,
|
// spec and a Windows style spec. Depending on the platform being unit tested,
|
||||||
// it returns one of them, along with a volume string that would be passed
|
// it returns one of them, along with a volume string that would be passed
|
||||||
// on the docker CLI (e.g. -v /bar -v /foo).
|
// on the docker CLI (e.g. -v /bar -v /foo).
|
||||||
func setupPlatformVolume(u []string, w []string) ([]string, string) {
|
func setupPlatformVolume(u, w []string) ([]string, string) {
|
||||||
var a []string
|
var a []string
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
a = w
|
a = w
|
||||||
|
@ -301,9 +299,9 @@ func TestParseWithMacAddress(t *testing.T) {
|
||||||
t.Fatalf("Expected an error with %v mac-address, got %v", invalidMacAddress, err)
|
t.Fatalf("Expected an error with %v mac-address, got %v", invalidMacAddress, err)
|
||||||
}
|
}
|
||||||
config, _ := mustParse(t, validMacAddress)
|
config, _ := mustParse(t, validMacAddress)
|
||||||
if config.MacAddress != "92:d0:c6:0a:29:33" { //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.44.
|
if config.MacAddress != "92:d0:c6:0a:29:33" {
|
||||||
t.Fatalf("Expected the config to have '92:d0:c6:0a:29:33' as container-wide MacAddress, got '%v'",
|
t.Fatalf("Expected the config to have '92:d0:c6:0a:29:33' as container-wide MacAddress, got '%v'",
|
||||||
config.MacAddress) //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.44.
|
config.MacAddress)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -459,7 +457,6 @@ func TestParseDevice(t *testing.T) {
|
||||||
t.Fatalf("Expected %v, got %v", deviceMapping, hostconfig.Devices)
|
t.Fatalf("Expected %v, got %v", deviceMapping, hostconfig.Devices)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseNetworkConfig(t *testing.T) {
|
func TestParseNetworkConfig(t *testing.T) {
|
||||||
|
@ -964,7 +961,6 @@ func TestConvertToStandardNotation(t *testing.T) {
|
||||||
|
|
||||||
for key, ports := range valid {
|
for key, ports := range valid {
|
||||||
convertedPorts, err := convertToStandardNotation(ports)
|
convertedPorts, err := convertToStandardNotation(ports)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import (
|
||||||
|
|
||||||
// ImageExistsLocally returns a boolean indicating if an image with the
|
// ImageExistsLocally returns a boolean indicating if an image with the
|
||||||
// requested name, tag and architecture exists in the local docker image store
|
// requested name, tag and architecture exists in the local docker image store
|
||||||
func ImageExistsLocally(ctx context.Context, imageName string, platform string) (bool, error) {
|
func ImageExistsLocally(ctx context.Context, imageName, platform string) (bool, error) {
|
||||||
cli, err := GetDockerClient(ctx)
|
cli, err := GetDockerClient(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
|
@ -35,7 +35,7 @@ func ImageExistsLocally(ctx context.Context, imageName string, platform string)
|
||||||
|
|
||||||
// RemoveImage removes image from local store, the function is used to run different
|
// RemoveImage removes image from local store, the function is used to run different
|
||||||
// container image architectures
|
// container image architectures
|
||||||
func RemoveImage(ctx context.Context, imageName string, force bool, pruneChildren bool) (bool, error) {
|
func RemoveImage(ctx context.Context, imageName string, force, pruneChildren bool) (bool, error) {
|
||||||
cli, err := GetDockerClient(ctx)
|
cli, err := GetDockerClient(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
|
|
|
@ -86,7 +86,7 @@ func supportsContainerImagePlatform(ctx context.Context, cli client.APIClient) b
|
||||||
return constraint.Check(sv)
|
return constraint.Check(sv)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cr *containerReference) Create(capAdd []string, capDrop []string) common.Executor {
|
func (cr *containerReference) Create(capAdd, capDrop []string) common.Executor {
|
||||||
return common.
|
return common.
|
||||||
NewInfoExecutor("%sdocker create image=%s platform=%s entrypoint=%+q cmd=%+q network=%+q", logPrefix, cr.input.Image, cr.input.Platform, cr.input.Entrypoint, cr.input.Cmd, cr.input.NetworkMode).
|
NewInfoExecutor("%sdocker create image=%s platform=%s entrypoint=%+q cmd=%+q network=%+q", logPrefix, cr.input.Image, cr.input.Platform, cr.input.Entrypoint, cr.input.Cmd, cr.input.NetworkMode).
|
||||||
Then(
|
Then(
|
||||||
|
@ -143,7 +143,7 @@ func (cr *containerReference) Copy(destPath string, files ...*FileEntry) common.
|
||||||
).IfNot(common.Dryrun)
|
).IfNot(common.Dryrun)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cr *containerReference) CopyDir(destPath string, srcPath string, useGitIgnore bool) common.Executor {
|
func (cr *containerReference) CopyDir(destPath, srcPath string, useGitIgnore bool) common.Executor {
|
||||||
return common.NewPipelineExecutor(
|
return common.NewPipelineExecutor(
|
||||||
common.NewInfoExecutor("%sdocker cp src=%s dst=%s", logPrefix, srcPath, destPath),
|
common.NewInfoExecutor("%sdocker cp src=%s dst=%s", logPrefix, srcPath, destPath),
|
||||||
cr.copyDir(destPath, srcPath, useGitIgnore),
|
cr.copyDir(destPath, srcPath, useGitIgnore),
|
||||||
|
@ -191,7 +191,7 @@ func (cr *containerReference) Remove() common.Executor {
|
||||||
).IfNot(common.Dryrun)
|
).IfNot(common.Dryrun)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cr *containerReference) ReplaceLogWriter(stdout io.Writer, stderr io.Writer) (io.Writer, io.Writer) {
|
func (cr *containerReference) ReplaceLogWriter(stdout, stderr io.Writer) (io.Writer, io.Writer) {
|
||||||
out := cr.input.Stdout
|
out := cr.input.Stdout
|
||||||
err := cr.input.Stderr
|
err := cr.input.Stderr
|
||||||
|
|
||||||
|
@ -472,7 +472,7 @@ func parseOptions(ctx context.Context, options string) (*containerConfig, error)
|
||||||
return containerConfig, nil
|
return containerConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cr *containerReference) create(capAdd []string, capDrop []string) common.Executor {
|
func (cr *containerReference) create(capAdd, capDrop []string) common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
if cr.id != "" {
|
if cr.id != "" {
|
||||||
return nil
|
return nil
|
||||||
|
@ -726,7 +726,7 @@ func (cr *containerReference) tryReadGID() common.Executor {
|
||||||
return cr.tryReadID("-g", func(id int) { cr.GID = id })
|
return cr.tryReadID("-g", func(id int) { cr.GID = id })
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cr *containerReference) waitForCommand(ctx context.Context, isTerminal bool, resp types.HijackedResponse, _ container.ExecCreateResponse, _ string, _ string) error {
|
func (cr *containerReference) waitForCommand(ctx context.Context, isTerminal bool, resp types.HijackedResponse, _ container.ExecCreateResponse, _, _ string) error {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
|
|
||||||
cmdResponse := make(chan error)
|
cmdResponse := make(chan error)
|
||||||
|
@ -797,7 +797,7 @@ func (cr *containerReference) CopyTarStream(ctx context.Context, destPath string
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cr *containerReference) copyDir(dstPath string, srcPath string, useGitIgnore bool) common.Executor {
|
func (cr *containerReference) copyDir(dstPath, srcPath string, useGitIgnore bool) common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
tarFile, err := os.CreateTemp("", "act")
|
tarFile, err := os.CreateTemp("", "act")
|
||||||
|
|
|
@ -85,7 +85,7 @@ func (m *mockDockerClient) ContainerExecInspect(ctx context.Context, execID stri
|
||||||
return args.Get(0).(container.ExecInspect), args.Error(1)
|
return args.Get(0).(container.ExecInspect), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockDockerClient) CopyToContainer(ctx context.Context, id string, path string, content io.Reader, options container.CopyToContainerOptions) error {
|
func (m *mockDockerClient) CopyToContainer(ctx context.Context, id, path string, content io.Reader, options container.CopyToContainerOptions) error {
|
||||||
args := m.Called(ctx, id, path, content, options)
|
args := m.Called(ctx, id, path, content, options)
|
||||||
return args.Error(0)
|
return args.Error(0)
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,10 +28,10 @@ func TestCopyDir(t *testing.T) {
|
||||||
StdOut: os.Stdout,
|
StdOut: os.Stdout,
|
||||||
Workdir: path.Join("testdata", "scratch"),
|
Workdir: path.Join("testdata", "scratch"),
|
||||||
}
|
}
|
||||||
_ = os.MkdirAll(e.Path, 0700)
|
_ = os.MkdirAll(e.Path, 0o700)
|
||||||
_ = os.MkdirAll(e.TmpDir, 0700)
|
_ = os.MkdirAll(e.TmpDir, 0o700)
|
||||||
_ = os.MkdirAll(e.ToolCache, 0700)
|
_ = os.MkdirAll(e.ToolCache, 0o700)
|
||||||
_ = os.MkdirAll(e.ActPath, 0700)
|
_ = os.MkdirAll(e.ActPath, 0o700)
|
||||||
err = e.CopyDir(e.Workdir, e.Path, true)(ctx)
|
err = e.CopyDir(e.Workdir, e.Path, true)(ctx)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
@ -49,12 +49,12 @@ func TestGetContainerArchive(t *testing.T) {
|
||||||
StdOut: os.Stdout,
|
StdOut: os.Stdout,
|
||||||
Workdir: path.Join("testdata", "scratch"),
|
Workdir: path.Join("testdata", "scratch"),
|
||||||
}
|
}
|
||||||
_ = os.MkdirAll(e.Path, 0700)
|
_ = os.MkdirAll(e.Path, 0o700)
|
||||||
_ = os.MkdirAll(e.TmpDir, 0700)
|
_ = os.MkdirAll(e.TmpDir, 0o700)
|
||||||
_ = os.MkdirAll(e.ToolCache, 0700)
|
_ = os.MkdirAll(e.ToolCache, 0o700)
|
||||||
_ = os.MkdirAll(e.ActPath, 0700)
|
_ = os.MkdirAll(e.ActPath, 0o700)
|
||||||
expectedContent := []byte("sdde/7sh")
|
expectedContent := []byte("sdde/7sh")
|
||||||
err = os.WriteFile(filepath.Join(e.Path, "action.yml"), expectedContent, 0600)
|
err = os.WriteFile(filepath.Join(e.Path, "action.yml"), expectedContent, 0o600)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
archive, err := e.GetContainerArchive(ctx, e.Path)
|
archive, err := e.GetContainerArchive(ctx, e.Path)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
|
@ -137,7 +137,7 @@ func (impl *interperterImpl) format(str reflect.Value, replaceValue ...reflect.V
|
||||||
return output, nil
|
return output, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (impl *interperterImpl) join(array reflect.Value, sep reflect.Value) (string, error) {
|
func (impl *interperterImpl) join(array, sep reflect.Value) (string, error) {
|
||||||
separator := impl.coerceToString(sep).String()
|
separator := impl.coerceToString(sep).String()
|
||||||
switch array.Kind() {
|
switch array.Kind() {
|
||||||
case reflect.Slice:
|
case reflect.Slice:
|
||||||
|
|
|
@ -350,7 +350,7 @@ func (impl *interperterImpl) evaluateCompare(compareNode *actionlint.CompareOpNo
|
||||||
return impl.compareValues(leftValue, rightValue, compareNode.Kind)
|
return impl.compareValues(leftValue, rightValue, compareNode.Kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (impl *interperterImpl) compareValues(leftValue reflect.Value, rightValue reflect.Value, kind actionlint.CompareOpNodeKind) (interface{}, error) {
|
func (impl *interperterImpl) compareValues(leftValue, rightValue reflect.Value, kind actionlint.CompareOpNodeKind) (interface{}, error) {
|
||||||
if leftValue.Kind() != rightValue.Kind() {
|
if leftValue.Kind() != rightValue.Kind() {
|
||||||
if !impl.isNumber(leftValue) {
|
if !impl.isNumber(leftValue) {
|
||||||
leftValue = impl.coerceToNumber(leftValue)
|
leftValue = impl.coerceToNumber(leftValue)
|
||||||
|
@ -462,7 +462,7 @@ func (impl *interperterImpl) coerceToString(value reflect.Value) reflect.Value {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
func (impl *interperterImpl) compareString(left string, right string, kind actionlint.CompareOpNodeKind) (bool, error) {
|
func (impl *interperterImpl) compareString(left, right string, kind actionlint.CompareOpNodeKind) (bool, error) {
|
||||||
switch kind {
|
switch kind {
|
||||||
case actionlint.CompareOpNodeKindLess:
|
case actionlint.CompareOpNodeKindLess:
|
||||||
return left < right, nil
|
return left < right, nil
|
||||||
|
@ -481,7 +481,7 @@ func (impl *interperterImpl) compareString(left string, right string, kind actio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (impl *interperterImpl) compareNumber(left float64, right float64, kind actionlint.CompareOpNodeKind) (bool, error) {
|
func (impl *interperterImpl) compareNumber(left, right float64, kind actionlint.CompareOpNodeKind) (bool, error) {
|
||||||
switch kind {
|
switch kind {
|
||||||
case actionlint.CompareOpNodeKindLess:
|
case actionlint.CompareOpNodeKindLess:
|
||||||
return left < right, nil
|
return left < right, nil
|
||||||
|
|
|
@ -97,8 +97,7 @@ type Fs interface {
|
||||||
Readlink(path string) (string, error)
|
Readlink(path string) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type DefaultFs struct {
|
type DefaultFs struct{}
|
||||||
}
|
|
||||||
|
|
||||||
func (*DefaultFs) Walk(root string, fn filepath.WalkFunc) error {
|
func (*DefaultFs) Walk(root string, fn filepath.WalkFunc) error {
|
||||||
return filepath.Walk(root, fn)
|
return filepath.Walk(root, fn)
|
||||||
|
|
|
@ -6,8 +6,7 @@ type Env interface {
|
||||||
Getenv(name string) string
|
Getenv(name string) string
|
||||||
}
|
}
|
||||||
|
|
||||||
type defaultEnv struct {
|
type defaultEnv struct{}
|
||||||
}
|
|
||||||
|
|
||||||
func (*defaultEnv) Getenv(name string) string {
|
func (*defaultEnv) Getenv(name string) string {
|
||||||
return os.Getenv(name)
|
return os.Getenv(name)
|
||||||
|
|
|
@ -20,7 +20,7 @@ func findExecutable(file string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if m := d.Mode(); !m.IsDir() && m&0111 != 0 {
|
if m := d.Mode(); !m.IsDir() && m&0o111 != 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return fs.ErrPermission
|
return fs.ErrPermission
|
||||||
|
|
|
@ -22,7 +22,7 @@ func findExecutable(file string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if m := d.Mode(); !m.IsDir() && m&0111 != 0 {
|
if m := d.Mode(); !m.IsDir() && m&0o111 != 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return fs.ErrPermission
|
return fs.ErrPermission
|
||||||
|
|
|
@ -62,9 +62,9 @@ func nestedMapLookup(m map[string]interface{}, ks ...string) (rval interface{})
|
||||||
return rval
|
return rval
|
||||||
} else if m, ok = rval.(map[string]interface{}); !ok {
|
} else if m, ok = rval.(map[string]interface{}); !ok {
|
||||||
return nil
|
return nil
|
||||||
} else { // 1+ more keys
|
|
||||||
return nestedMapLookup(m, ks[1:]...)
|
|
||||||
}
|
}
|
||||||
|
// 1+ more keys
|
||||||
|
return nestedMapLookup(m, ks[1:]...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func withDefaultBranch(ctx context.Context, b string, event map[string]interface{}) map[string]interface{} {
|
func withDefaultBranch(ctx context.Context, b string, event map[string]interface{}) map[string]interface{} {
|
||||||
|
@ -90,10 +90,12 @@ func withDefaultBranch(ctx context.Context, b string, event map[string]interface
|
||||||
return event
|
return event
|
||||||
}
|
}
|
||||||
|
|
||||||
var findGitRef = git.FindGitRef
|
var (
|
||||||
var findGitRevision = git.FindGitRevision
|
findGitRef = git.FindGitRef
|
||||||
|
findGitRevision = git.FindGitRevision
|
||||||
|
)
|
||||||
|
|
||||||
func (ghc *GithubContext) SetRef(ctx context.Context, defaultBranch string, repoPath string) {
|
func (ghc *GithubContext) SetRef(ctx context.Context, defaultBranch, repoPath string) {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
|
|
||||||
// https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows
|
// https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows
|
||||||
|
@ -164,7 +166,7 @@ func (ghc *GithubContext) SetSha(ctx context.Context, repoPath string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ghc *GithubContext) SetRepositoryAndOwner(ctx context.Context, githubInstance string, remoteName string, repoPath string) {
|
func (ghc *GithubContext) SetRepositoryAndOwner(ctx context.Context, githubInstance, remoteName, repoPath string) {
|
||||||
if ghc.Repository == "" {
|
if ghc.Repository == "" {
|
||||||
repo, err := git.FindGithubRepo(ctx, repoPath, githubInstance, remoteName)
|
repo, err := git.FindGithubRepo(ctx, repoPath, githubInstance, remoteName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -642,7 +642,7 @@ func (s *Step) GetEnv() map[string]string {
|
||||||
|
|
||||||
// ShellCommand returns the command for the shell
|
// ShellCommand returns the command for the shell
|
||||||
func (s *Step) ShellCommand() string {
|
func (s *Step) ShellCommand() string {
|
||||||
shellCommand := ""
|
var shellCommand string
|
||||||
|
|
||||||
// Reference: https://github.com/actions/runner/blob/8109c962f09d9acc473d92c595ff43afceddb347/src/Runner.Worker/Handlers/ScriptHandlerHelpers.cs#L9-L17
|
// Reference: https://github.com/actions/runner/blob/8109c962f09d9acc473d92c595ff43afceddb347/src/Runner.Worker/Handlers/ScriptHandlerHelpers.cs#L9-L17
|
||||||
switch s.Shell {
|
switch s.Shell {
|
||||||
|
|
|
@ -29,7 +29,7 @@ type actionStep interface {
|
||||||
getCompositeSteps() *compositeSteps
|
getCompositeSteps() *compositeSteps
|
||||||
}
|
}
|
||||||
|
|
||||||
type readAction func(ctx context.Context, step *model.Step, actionDir string, actionPath string, readFile actionYamlReader, writeFile fileWriter) (*model.Action, error)
|
type readAction func(ctx context.Context, step *model.Step, actionDir, actionPath string, readFile actionYamlReader, writeFile fileWriter) (*model.Action, error)
|
||||||
|
|
||||||
type actionYamlReader func(filename string) (io.Reader, io.Closer, error)
|
type actionYamlReader func(filename string) (io.Reader, io.Closer, error)
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ type runAction func(step actionStep, actionDir string, remoteAction *remoteActio
|
||||||
//go:embed res/trampoline.js
|
//go:embed res/trampoline.js
|
||||||
var trampoline embed.FS
|
var trampoline embed.FS
|
||||||
|
|
||||||
func readActionImpl(ctx context.Context, step *model.Step, actionDir string, actionPath string, readFile actionYamlReader, writeFile fileWriter) (*model.Action, error) {
|
func readActionImpl(ctx context.Context, step *model.Step, actionDir, actionPath string, readFile actionYamlReader, writeFile fileWriter) (*model.Action, error) {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
allErrors := []error{}
|
allErrors := []error{}
|
||||||
addError := func(fileName string, err error) {
|
addError := func(fileName string, err error) {
|
||||||
|
@ -116,7 +116,7 @@ func readActionImpl(ctx context.Context, step *model.Step, actionDir string, act
|
||||||
return action, err
|
return action, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func maybeCopyToActionDir(ctx context.Context, step actionStep, actionDir string, actionPath string, containerActionDir string) error {
|
func maybeCopyToActionDir(ctx context.Context, step actionStep, actionDir, actionPath, containerActionDir string) error {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
rc := step.getRunContext()
|
rc := step.getRunContext()
|
||||||
stepModel := step.getStepModel()
|
stepModel := step.getStepModel()
|
||||||
|
@ -274,7 +274,7 @@ func removeGitIgnore(ctx context.Context, directory string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: break out parts of function to reduce complexicity
|
// TODO: break out parts of function to reduce complexicity
|
||||||
func execAsDocker(ctx context.Context, step actionStep, actionName string, basedir string, localAction bool) error {
|
func execAsDocker(ctx context.Context, step actionStep, actionName, basedir string, localAction bool) error {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
rc := step.getRunContext()
|
rc := step.getRunContext()
|
||||||
action := step.getActionModel()
|
action := step.getActionModel()
|
||||||
|
@ -402,7 +402,7 @@ func evalDockerArgs(ctx context.Context, step step, action *model.Action, cmd *[
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newStepContainer(ctx context.Context, step step, image string, cmd []string, entrypoint []string) container.Container {
|
func newStepContainer(ctx context.Context, step step, image string, cmd, entrypoint []string) container.Container {
|
||||||
rc := step.getRunContext()
|
rc := step.getRunContext()
|
||||||
stepModel := step.getStepModel()
|
stepModel := step.getStepModel()
|
||||||
rawLogger := common.Logger(ctx).WithField("raw_output", true)
|
rawLogger := common.Logger(ctx).WithField("raw_output", true)
|
||||||
|
@ -557,7 +557,7 @@ func runPreStep(step actionStep) common.Executor {
|
||||||
actionPath = ""
|
actionPath = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
actionLocation := ""
|
var actionLocation string
|
||||||
if actionPath != "" {
|
if actionPath != "" {
|
||||||
actionLocation = path.Join(actionDir, actionPath)
|
actionLocation = path.Join(actionDir, actionPath)
|
||||||
} else {
|
} else {
|
||||||
|
@ -608,7 +608,7 @@ func runPreStep(step actionStep) common.Executor {
|
||||||
actionPath = ""
|
actionPath = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
actionLocation := ""
|
var actionLocation string
|
||||||
if actionPath != "" {
|
if actionPath != "" {
|
||||||
actionLocation = path.Join(actionDir, actionPath)
|
actionLocation = path.Join(actionDir, actionPath)
|
||||||
} else {
|
} else {
|
||||||
|
@ -695,7 +695,7 @@ func runPostStep(step actionStep) common.Executor {
|
||||||
actionPath = ""
|
actionPath = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
actionLocation := ""
|
var actionLocation string
|
||||||
if actionPath != "" {
|
if actionPath != "" {
|
||||||
actionLocation = path.Join(actionDir, actionPath)
|
actionLocation = path.Join(actionDir, actionPath)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -122,7 +122,7 @@ runs:
|
||||||
|
|
||||||
writeFile := func(filename string, data []byte, perm fs.FileMode) error {
|
writeFile := func(filename string, data []byte, perm fs.FileMode) error {
|
||||||
assert.Equal(t, "actionDir/actionPath/trampoline.js", filename)
|
assert.Equal(t, "actionDir/actionPath/trampoline.js", filename)
|
||||||
assert.Equal(t, fs.FileMode(0400), perm)
|
assert.Equal(t, fs.FileMode(0o400), perm)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,8 +8,10 @@ import (
|
||||||
"github.com/nektos/act/pkg/common"
|
"github.com/nektos/act/pkg/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
var commandPatternGA *regexp.Regexp
|
var (
|
||||||
var commandPatternADO *regexp.Regexp
|
commandPatternGA *regexp.Regexp
|
||||||
|
commandPatternADO *regexp.Regexp
|
||||||
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
commandPatternGA = regexp.MustCompile("^::([^ ]+)( (.+))?::([^\r\n]*)[\r\n]+$")
|
commandPatternGA = regexp.MustCompile("^::([^ ]+)( (.+))?::([^\r\n]*)[\r\n]+$")
|
||||||
|
@ -28,7 +30,7 @@ func tryParseRawActionCommand(line string) (command string, kvPairs map[string]s
|
||||||
arg = m[4]
|
arg = m[4]
|
||||||
ok = true
|
ok = true
|
||||||
}
|
}
|
||||||
return
|
return command, kvPairs, arg, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rc *RunContext) commandHandler(ctx context.Context) common.LineHandler {
|
func (rc *RunContext) commandHandler(ctx context.Context) common.LineHandler {
|
||||||
|
@ -101,6 +103,7 @@ func (rc *RunContext) setEnv(ctx context.Context, kvPairs map[string]string, arg
|
||||||
mergeIntoMap(rc.Env, newenv)
|
mergeIntoMap(rc.Env, newenv)
|
||||||
mergeIntoMap(rc.GlobalEnv, newenv)
|
mergeIntoMap(rc.GlobalEnv, newenv)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rc *RunContext) setOutput(ctx context.Context, kvPairs map[string]string, arg string) {
|
func (rc *RunContext) setOutput(ctx context.Context, kvPairs map[string]string, arg string) {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
stepID := rc.CurrentStep
|
stepID := rc.CurrentStep
|
||||||
|
@ -119,6 +122,7 @@ func (rc *RunContext) setOutput(ctx context.Context, kvPairs map[string]string,
|
||||||
logger.Infof(" \U00002699 ::set-output:: %s=%s", outputName, arg)
|
logger.Infof(" \U00002699 ::set-output:: %s=%s", outputName, arg)
|
||||||
result.Outputs[outputName] = arg
|
result.Outputs[outputName] = arg
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rc *RunContext) addPath(ctx context.Context, arg string) {
|
func (rc *RunContext) addPath(ctx context.Context, arg string) {
|
||||||
common.Logger(ctx).Infof(" \U00002699 ::add-path:: %s", arg)
|
common.Logger(ctx).Infof(" \U00002699 ::add-path:: %s", arg)
|
||||||
extraPath := []string{arg}
|
extraPath := []string{arg}
|
||||||
|
@ -130,7 +134,7 @@ func (rc *RunContext) addPath(ctx context.Context, arg string) {
|
||||||
rc.ExtraPath = extraPath
|
rc.ExtraPath = extraPath
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseKeyValuePairs(kvPairs string, separator string) map[string]string {
|
func parseKeyValuePairs(kvPairs, separator string) map[string]string {
|
||||||
rtn := make(map[string]string)
|
rtn := make(map[string]string)
|
||||||
kvPairList := strings.Split(kvPairs, separator)
|
kvPairList := strings.Split(kvPairs, separator)
|
||||||
for _, kvPair := range kvPairList {
|
for _, kvPair := range kvPairList {
|
||||||
|
@ -141,6 +145,7 @@ func parseKeyValuePairs(kvPairs string, separator string) map[string]string {
|
||||||
}
|
}
|
||||||
return rtn
|
return rtn
|
||||||
}
|
}
|
||||||
|
|
||||||
func unescapeCommandData(arg string) string {
|
func unescapeCommandData(arg string) string {
|
||||||
escapeMap := map[string]string{
|
escapeMap := map[string]string{
|
||||||
"%25": "%",
|
"%25": "%",
|
||||||
|
@ -152,6 +157,7 @@ func unescapeCommandData(arg string) string {
|
||||||
}
|
}
|
||||||
return arg
|
return arg
|
||||||
}
|
}
|
||||||
|
|
||||||
func unescapeCommandProperty(arg string) string {
|
func unescapeCommandProperty(arg string) string {
|
||||||
escapeMap := map[string]string{
|
escapeMap := map[string]string{
|
||||||
"%25": "%",
|
"%25": "%",
|
||||||
|
@ -165,6 +171,7 @@ func unescapeCommandProperty(arg string) string {
|
||||||
}
|
}
|
||||||
return arg
|
return arg
|
||||||
}
|
}
|
||||||
|
|
||||||
func unescapeKvPairs(kvPairs map[string]string) map[string]string {
|
func unescapeKvPairs(kvPairs map[string]string) map[string]string {
|
||||||
for k, v := range kvPairs {
|
for k, v := range kvPairs {
|
||||||
kvPairs[k] = unescapeCommandProperty(v)
|
kvPairs[k] = unescapeCommandProperty(v)
|
||||||
|
|
|
@ -15,7 +15,7 @@ type containerMock struct {
|
||||||
container.LinuxContainerEnvironmentExtensions
|
container.LinuxContainerEnvironmentExtensions
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cm *containerMock) Create(capAdd []string, capDrop []string) common.Executor {
|
func (cm *containerMock) Create(capAdd, capDrop []string) common.Executor {
|
||||||
args := cm.Called(capAdd, capDrop)
|
args := cm.Called(capAdd, capDrop)
|
||||||
return args.Get(0).(func(context.Context) error)
|
return args.Get(0).(func(context.Context) error)
|
||||||
}
|
}
|
||||||
|
@ -55,7 +55,7 @@ func (cm *containerMock) Copy(destPath string, files ...*container.FileEntry) co
|
||||||
return args.Get(0).(func(context.Context) error)
|
return args.Get(0).(func(context.Context) error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cm *containerMock) CopyDir(destPath string, srcPath string, useGitIgnore bool) common.Executor {
|
func (cm *containerMock) CopyDir(destPath, srcPath string, useGitIgnore bool) common.Executor {
|
||||||
args := cm.Called(destPath, srcPath, useGitIgnore)
|
args := cm.Called(destPath, srcPath, useGitIgnore)
|
||||||
return args.Get(0).(func(context.Context) error)
|
return args.Get(0).(func(context.Context) error)
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,9 +26,11 @@ const (
|
||||||
gray = 37
|
gray = 37
|
||||||
)
|
)
|
||||||
|
|
||||||
var colors []int
|
var (
|
||||||
var nextColor int
|
colors []int
|
||||||
var mux sync.Mutex
|
nextColor int
|
||||||
|
mux sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
nextColor = 0
|
nextColor = 0
|
||||||
|
@ -70,7 +72,7 @@ func WithJobLoggerFactory(ctx context.Context, factory JobLoggerFactory) context
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithJobLogger attaches a new logger to context that is aware of steps
|
// WithJobLogger attaches a new logger to context that is aware of steps
|
||||||
func WithJobLogger(ctx context.Context, jobID string, jobName string, config *Config, masks *[]string, matrix map[string]interface{}) context.Context {
|
func WithJobLogger(ctx context.Context, jobID, jobName string, config *Config, masks *[]string, matrix map[string]interface{}) context.Context {
|
||||||
ctx = WithMasks(ctx, masks)
|
ctx = WithMasks(ctx, masks)
|
||||||
|
|
||||||
var logger *logrus.Logger
|
var logger *logrus.Logger
|
||||||
|
|
|
@ -113,9 +113,7 @@ func newActionCacheReusableWorkflowExecutor(rc *RunContext, filename string, rem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var executorLock sync.Mutex
|
||||||
executorLock sync.Mutex
|
|
||||||
)
|
|
||||||
|
|
||||||
func newMutexExecutor(executor common.Executor) common.Executor {
|
func newMutexExecutor(executor common.Executor) common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
|
@ -150,7 +148,7 @@ func cloneIfRequired(rc *RunContext, remoteReusableWorkflow remoteReusableWorkfl
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newReusableWorkflowExecutor(rc *RunContext, directory string, workflow string) common.Executor {
|
func newReusableWorkflowExecutor(rc *RunContext, directory, workflow string) common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
planner, err := model.NewWorkflowPlanner(path.Join(directory, workflow), true, false)
|
planner, err := model.NewWorkflowPlanner(path.Join(directory, workflow), true, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -263,7 +263,7 @@ func (rc *RunContext) stopHostEnvironment(ctx context.Context) error {
|
||||||
return common.NewPipelineExecutor(
|
return common.NewPipelineExecutor(
|
||||||
rc.JobContainer.Copy(rc.JobContainer.GetActPath()+"/", &container.FileEntry{
|
rc.JobContainer.Copy(rc.JobContainer.GetActPath()+"/", &container.FileEntry{
|
||||||
Name: "workflow/stop-lxc.sh",
|
Name: "workflow/stop-lxc.sh",
|
||||||
Mode: 0755,
|
Mode: 0o755,
|
||||||
Body: stopScript.String(),
|
Body: stopScript.String(),
|
||||||
}),
|
}),
|
||||||
rc.JobContainer.Exec([]string{rc.JobContainer.GetActPath() + "/workflow/stop-lxc.sh"}, map[string]string{}, "root", "/tmp"),
|
rc.JobContainer.Exec([]string{rc.JobContainer.GetActPath() + "/workflow/stop-lxc.sh"}, map[string]string{}, "root", "/tmp"),
|
||||||
|
@ -360,17 +360,17 @@ func (rc *RunContext) startHostEnvironment() common.Executor {
|
||||||
executors = append(executors,
|
executors = append(executors,
|
||||||
rc.JobContainer.Copy(rc.JobContainer.GetActPath()+"/", &container.FileEntry{
|
rc.JobContainer.Copy(rc.JobContainer.GetActPath()+"/", &container.FileEntry{
|
||||||
Name: "workflow/lxc-helpers-lib.sh",
|
Name: "workflow/lxc-helpers-lib.sh",
|
||||||
Mode: 0755,
|
Mode: 0o755,
|
||||||
Body: lxcHelpersLib,
|
Body: lxcHelpersLib,
|
||||||
}),
|
}),
|
||||||
rc.JobContainer.Copy(rc.JobContainer.GetActPath()+"/", &container.FileEntry{
|
rc.JobContainer.Copy(rc.JobContainer.GetActPath()+"/", &container.FileEntry{
|
||||||
Name: "workflow/lxc-helpers.sh",
|
Name: "workflow/lxc-helpers.sh",
|
||||||
Mode: 0755,
|
Mode: 0o755,
|
||||||
Body: lxcHelpers,
|
Body: lxcHelpers,
|
||||||
}),
|
}),
|
||||||
rc.JobContainer.Copy(rc.JobContainer.GetActPath()+"/", &container.FileEntry{
|
rc.JobContainer.Copy(rc.JobContainer.GetActPath()+"/", &container.FileEntry{
|
||||||
Name: "workflow/start-lxc.sh",
|
Name: "workflow/start-lxc.sh",
|
||||||
Mode: 0755,
|
Mode: 0o755,
|
||||||
Body: startScript.String(),
|
Body: startScript.String(),
|
||||||
}),
|
}),
|
||||||
rc.JobContainer.Exec([]string{rc.JobContainer.GetActPath() + "/workflow/start-lxc.sh"}, map[string]string{}, "root", "/tmp"),
|
rc.JobContainer.Exec([]string{rc.JobContainer.GetActPath() + "/workflow/start-lxc.sh"}, map[string]string{}, "root", "/tmp"),
|
||||||
|
@ -819,7 +819,7 @@ func (rc *RunContext) IsLXCHostEnv(ctx context.Context) bool {
|
||||||
func (rc *RunContext) GetLXCInfo(ctx context.Context) (isLXC bool, template, release, config string) {
|
func (rc *RunContext) GetLXCInfo(ctx context.Context) (isLXC bool, template, release, config string) {
|
||||||
platform := rc.runsOnImage(ctx)
|
platform := rc.runsOnImage(ctx)
|
||||||
if !strings.HasPrefix(platform, lxcPrefix) {
|
if !strings.HasPrefix(platform, lxcPrefix) {
|
||||||
return
|
return isLXC, template, release, config
|
||||||
}
|
}
|
||||||
isLXC = true
|
isLXC = true
|
||||||
s := strings.SplitN(strings.TrimPrefix(platform, lxcPrefix), ":", 3)
|
s := strings.SplitN(strings.TrimPrefix(platform, lxcPrefix), ":", 3)
|
||||||
|
@ -830,7 +830,7 @@ func (rc *RunContext) GetLXCInfo(ctx context.Context) (isLXC bool, template, rel
|
||||||
if len(s) > 2 {
|
if len(s) > 2 {
|
||||||
config = s[2]
|
config = s[2]
|
||||||
}
|
}
|
||||||
return
|
return isLXC, template, release, config
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rc *RunContext) IsHostEnv(ctx context.Context) bool {
|
func (rc *RunContext) IsHostEnv(ctx context.Context) bool {
|
||||||
|
@ -1247,9 +1247,9 @@ func nestedMapLookup(m map[string]interface{}, ks ...string) (rval interface{})
|
||||||
return rval
|
return rval
|
||||||
} else if m, ok = rval.(map[string]interface{}); !ok {
|
} else if m, ok = rval.(map[string]interface{}); !ok {
|
||||||
return nil
|
return nil
|
||||||
} else { // 1+ more keys
|
|
||||||
return nestedMapLookup(m, ks[1:]...)
|
|
||||||
}
|
}
|
||||||
|
// 1+ more keys
|
||||||
|
return nestedMapLookup(m, ks[1:]...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rc *RunContext) withGithubEnv(ctx context.Context, github *model.GithubContext, env map[string]string) map[string]string {
|
func (rc *RunContext) withGithubEnv(ctx context.Context, github *model.GithubContext, env map[string]string) map[string]string {
|
||||||
|
@ -1365,25 +1365,25 @@ func (rc *RunContext) handleCredentials(ctx context.Context) (string, string, er
|
||||||
|
|
||||||
func (rc *RunContext) handleServiceCredentials(ctx context.Context, creds map[string]string) (username, password string, err error) {
|
func (rc *RunContext) handleServiceCredentials(ctx context.Context, creds map[string]string) (username, password string, err error) {
|
||||||
if creds == nil {
|
if creds == nil {
|
||||||
return
|
return username, password, err
|
||||||
}
|
}
|
||||||
if len(creds) != 2 {
|
if len(creds) != 2 {
|
||||||
err = fmt.Errorf("invalid property count for key 'credentials:'")
|
err = fmt.Errorf("invalid property count for key 'credentials:'")
|
||||||
return
|
return username, password, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ee := rc.NewExpressionEvaluator(ctx)
|
ee := rc.NewExpressionEvaluator(ctx)
|
||||||
if username = ee.Interpolate(ctx, creds["username"]); username == "" {
|
if username = ee.Interpolate(ctx, creds["username"]); username == "" {
|
||||||
err = fmt.Errorf("failed to interpolate credentials.username")
|
err = fmt.Errorf("failed to interpolate credentials.username")
|
||||||
return
|
return username, password, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if password = ee.Interpolate(ctx, creds["password"]); password == "" {
|
if password = ee.Interpolate(ctx, creds["password"]); password == "" {
|
||||||
err = fmt.Errorf("failed to interpolate credentials.password")
|
err = fmt.Errorf("failed to interpolate credentials.password")
|
||||||
return
|
return username, password, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return username, password, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetServiceBindsAndMounts returns the binds and mounts for the service container, resolving paths as appopriate
|
// GetServiceBindsAndMounts returns the binds and mounts for the service container, resolving paths as appopriate
|
||||||
|
|
|
@ -139,7 +139,7 @@ func runStepExecutor(step step, stage stepStage, executor common.Executor) commo
|
||||||
Mode: 0o666,
|
Mode: 0o666,
|
||||||
}, &container.FileEntry{
|
}, &container.FileEntry{
|
||||||
Name: envFileCommand,
|
Name: envFileCommand,
|
||||||
Mode: 0666,
|
Mode: 0o666,
|
||||||
}, &container.FileEntry{
|
}, &container.FileEntry{
|
||||||
Name: summaryFileCommand,
|
Name: summaryFileCommand,
|
||||||
Mode: 0o666,
|
Mode: 0o666,
|
||||||
|
|
|
@ -24,7 +24,7 @@ func (salm *stepActionLocalMocks) runAction(step actionStep, actionDir string, r
|
||||||
return args.Get(0).(func(context.Context) error)
|
return args.Get(0).(func(context.Context) error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (salm *stepActionLocalMocks) readAction(_ context.Context, step *model.Step, actionDir string, actionPath string, readFile actionYamlReader, writeFile fileWriter) (*model.Action, error) {
|
func (salm *stepActionLocalMocks) readAction(_ context.Context, step *model.Step, actionDir, actionPath string, readFile actionYamlReader, writeFile fileWriter) (*model.Action, error) {
|
||||||
args := salm.Called(step, actionDir, actionPath, readFile, writeFile)
|
args := salm.Called(step, actionDir, actionPath, readFile, writeFile)
|
||||||
return args.Get(0).(*model.Action), args.Error(1)
|
return args.Get(0).(*model.Action), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,11 +85,9 @@ func (sd *stepDocker) runUsesContainer() common.Executor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var ContainerNewContainer = container.NewContainer
|
||||||
ContainerNewContainer = container.NewContainer
|
|
||||||
)
|
|
||||||
|
|
||||||
func (sd *stepDocker) newStepContainer(ctx context.Context, image string, cmd []string, entrypoint []string) container.Container {
|
func (sd *stepDocker) newStepContainer(ctx context.Context, image string, cmd, entrypoint []string) container.Container {
|
||||||
rc := sd.RunContext
|
rc := sd.RunContext
|
||||||
step := sd.Step
|
step := sd.Step
|
||||||
|
|
||||||
|
|
|
@ -195,16 +195,16 @@ func (sr *stepRun) setupShell(ctx context.Context) {
|
||||||
step.Shell = shellWithFallback[1]
|
step.Shell = shellWithFallback[1]
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
shell_fallback := `
|
shellFallback := `
|
||||||
if command -v bash >/dev/null; then
|
if command -v bash >/dev/null; then
|
||||||
echo -n bash
|
echo -n bash
|
||||||
else
|
else
|
||||||
echo -n sh
|
echo -n sh
|
||||||
fi
|
fi
|
||||||
`
|
`
|
||||||
stdout, _, err := rc.sh(ctx, shell_fallback)
|
stdout, _, err := rc.sh(ctx, shellFallback)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.Logger(ctx).Error("fail to run %q: %v", shell_fallback, err)
|
common.Logger(ctx).Error("fail to run %q: %v", shellFallback, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
step.Shell = stdout
|
step.Shell = stdout
|
||||||
|
@ -215,7 +215,7 @@ fi
|
||||||
func (sr *stepRun) setupWorkingDirectory(ctx context.Context) {
|
func (sr *stepRun) setupWorkingDirectory(ctx context.Context) {
|
||||||
rc := sr.RunContext
|
rc := sr.RunContext
|
||||||
step := sr.Step
|
step := sr.Step
|
||||||
workingdirectory := ""
|
var workingdirectory string
|
||||||
|
|
||||||
if step.WorkingDirectory == "" {
|
if step.WorkingDirectory == "" {
|
||||||
workingdirectory = rc.Run.Job().Defaults.Run.WorkingDirectory
|
workingdirectory = rc.Run.Job().Defaults.Run.WorkingDirectory
|
||||||
|
|
|
@ -89,14 +89,11 @@ type StringDefinition struct {
|
||||||
IsExpression bool `json:"is-expression"`
|
IsExpression bool `json:"is-expression"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type NumberDefinition struct {
|
type NumberDefinition struct{}
|
||||||
}
|
|
||||||
|
|
||||||
type BooleanDefinition struct {
|
type BooleanDefinition struct{}
|
||||||
}
|
|
||||||
|
|
||||||
type NullDefinition struct {
|
type NullDefinition struct{}
|
||||||
}
|
|
||||||
|
|
||||||
func GetWorkflowSchema() *Schema {
|
func GetWorkflowSchema() *Schema {
|
||||||
sh := &Schema{}
|
sh := &Schema{}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue