mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-06-22 09:44:24 +02:00
Compare commits
1 Commits
v1.0.4
...
3815aad750
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3815aad750 |
@@ -71,11 +71,6 @@ jobs:
|
||||
- name: Echo the tag
|
||||
run: echo "${{ env.DOCKER_ORG }}/runner:nightly${{ matrix.variant.tag_suffix }}"
|
||||
|
||||
- name: Get Meta
|
||||
id: meta
|
||||
run: |
|
||||
echo REPO_VERSION=$(git describe --tags --always | sed 's/-/+/' | sed 's/^v//') >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
@@ -88,5 +83,3 @@ jobs:
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_ORG }}/runner:nightly${{ matrix.variant.tag_suffix }}
|
||||
build-args: |
|
||||
VERSION=${{ steps.meta.outputs.REPO_VERSION }}
|
||||
|
||||
@@ -96,5 +96,3 @@ jobs:
|
||||
linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||
build-args: |
|
||||
VERSION=${{ steps.docker_meta.outputs.version }}
|
||||
|
||||
16
Dockerfile
16
Dockerfile
@@ -19,11 +19,6 @@ RUN make clean && make build
|
||||
#
|
||||
FROM docker:29-dind AS dind
|
||||
|
||||
ARG VERSION=dev
|
||||
|
||||
LABEL org.opencontainers.image.source="https://gitea.com/gitea/runner"
|
||||
LABEL org.opencontainers.image.version="${VERSION}"
|
||||
|
||||
RUN apk add --no-cache s6 bash git tzdata
|
||||
|
||||
COPY --from=builder /opt/src/runner/gitea-runner /usr/local/bin/gitea-runner
|
||||
@@ -39,11 +34,6 @@ ENTRYPOINT ["s6-svscan","/etc/s6"]
|
||||
#
|
||||
FROM docker:29-dind-rootless AS dind-rootless
|
||||
|
||||
ARG VERSION=dev
|
||||
|
||||
LABEL org.opencontainers.image.source="https://gitea.com/gitea/runner"
|
||||
LABEL org.opencontainers.image.version="${VERSION}"
|
||||
|
||||
USER root
|
||||
RUN apk add --no-cache s6 bash git tzdata
|
||||
|
||||
@@ -64,12 +54,6 @@ ENTRYPOINT ["s6-svscan","/etc/s6"]
|
||||
#
|
||||
#
|
||||
FROM alpine AS basic
|
||||
|
||||
ARG VERSION=dev
|
||||
|
||||
LABEL org.opencontainers.image.source="https://gitea.com/gitea/runner"
|
||||
LABEL org.opencontainers.image.version="${VERSION}"
|
||||
|
||||
RUN apk add --no-cache tini bash git tzdata
|
||||
|
||||
COPY --from=builder /opt/src/runner/gitea-runner /usr/local/bin/gitea-runner
|
||||
|
||||
@@ -431,7 +431,6 @@ func (h *Handler) upload(w http.ResponseWriter, r *http.Request, params httprout
|
||||
}
|
||||
if err := h.storage.Write(cache.ID, start, r.Body); err != nil {
|
||||
h.responseJSON(w, r, 500, err)
|
||||
return
|
||||
}
|
||||
h.useCache(id)
|
||||
h.responseJSON(w, r, 200)
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -339,54 +338,6 @@ func TestHandler(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("upload write failure returns only error", func(t *testing.T) {
|
||||
key := strings.ToLower(t.Name())
|
||||
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
|
||||
var id uint64
|
||||
{
|
||||
body, err := json.Marshal(&Request{
|
||||
Key: key,
|
||||
Version: version,
|
||||
Size: 100,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
resp, err := testClient.Post(base+"/caches", "application/json", bytes.NewReader(body))
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
got := struct {
|
||||
CacheID uint64 `json:"cacheId"`
|
||||
}{}
|
||||
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
|
||||
id = got.CacheID
|
||||
}
|
||||
|
||||
storageFile := filepath.Join(dir, "not-a-directory")
|
||||
require.NoError(t, os.WriteFile(storageFile, []byte("blocked"), 0o600))
|
||||
originalStorage := handler.storage
|
||||
handler.storage = &Storage{rootDir: storageFile}
|
||||
defer func() {
|
||||
handler.storage = originalStorage
|
||||
}()
|
||||
|
||||
req, err := http.NewRequest(http.MethodPatch,
|
||||
fmt.Sprintf("%s/caches/%d", base, id), bytes.NewReader(make([]byte, 100)))
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
req.Header.Set("Content-Range", "bytes 0-99/*")
|
||||
resp, err := testClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, 500, resp.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
var got map[string]string
|
||||
require.NoError(t, json.Unmarshal(body, &got))
|
||||
assert.NotEmpty(t, got["error"])
|
||||
})
|
||||
|
||||
t.Run("commit early", func(t *testing.T) {
|
||||
key := strings.ToLower(t.Name())
|
||||
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
|
||||
|
||||
146
act/common/draw.go
Normal file
146
act/common/draw.go
Normal file
@@ -0,0 +1,146 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2020 The nektos/act Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Style is a specific style
|
||||
type Style int
|
||||
|
||||
// Styles
|
||||
const (
|
||||
StyleDoubleLine = iota
|
||||
StyleSingleLine
|
||||
StyleDashedLine
|
||||
StyleNoLine
|
||||
)
|
||||
|
||||
// NewPen creates a new pen
|
||||
func NewPen(style Style, color int) *Pen {
|
||||
bgcolor := 49
|
||||
if os.Getenv("CLICOLOR") == "0" {
|
||||
color = 0
|
||||
bgcolor = 0
|
||||
}
|
||||
return &Pen{
|
||||
style: style,
|
||||
color: color,
|
||||
bgcolor: bgcolor,
|
||||
}
|
||||
}
|
||||
|
||||
type styleDef struct {
|
||||
cornerTL string
|
||||
cornerTR string
|
||||
cornerBL string
|
||||
cornerBR string
|
||||
lineH string
|
||||
lineV string
|
||||
}
|
||||
|
||||
var styleDefs = []styleDef{
|
||||
{"\u2554", "\u2557", "\u255a", "\u255d", "\u2550", "\u2551"},
|
||||
{"\u256d", "\u256e", "\u2570", "\u256f", "\u2500", "\u2502"},
|
||||
{"\u250c", "\u2510", "\u2514", "\u2518", "\u254c", "\u254e"},
|
||||
{" ", " ", " ", " ", " ", " "},
|
||||
}
|
||||
|
||||
// Pen struct
|
||||
type Pen struct {
|
||||
style Style
|
||||
color int
|
||||
bgcolor int
|
||||
}
|
||||
|
||||
// Drawing struct
|
||||
type Drawing struct {
|
||||
buf *strings.Builder
|
||||
width int
|
||||
}
|
||||
|
||||
func (p *Pen) drawTopBars(buf io.Writer, labels ...string) {
|
||||
style := styleDefs[p.style]
|
||||
for _, label := range labels {
|
||||
bar := strings.Repeat(style.lineH, len(label)+2)
|
||||
fmt.Fprintf(buf, " ")
|
||||
fmt.Fprintf(buf, "\x1b[%d;%dm", p.color, p.bgcolor)
|
||||
fmt.Fprintf(buf, "%s%s%s", style.cornerTL, bar, style.cornerTR)
|
||||
fmt.Fprintf(buf, "\x1b[%dm", 0)
|
||||
}
|
||||
fmt.Fprintf(buf, "\n")
|
||||
}
|
||||
|
||||
func (p *Pen) drawBottomBars(buf io.Writer, labels ...string) {
|
||||
style := styleDefs[p.style]
|
||||
for _, label := range labels {
|
||||
bar := strings.Repeat(style.lineH, len(label)+2)
|
||||
fmt.Fprintf(buf, " ")
|
||||
fmt.Fprintf(buf, "\x1b[%d;%dm", p.color, p.bgcolor)
|
||||
fmt.Fprintf(buf, "%s%s%s", style.cornerBL, bar, style.cornerBR)
|
||||
fmt.Fprintf(buf, "\x1b[%dm", 0)
|
||||
}
|
||||
fmt.Fprintf(buf, "\n")
|
||||
}
|
||||
|
||||
func (p *Pen) drawLabels(buf io.Writer, labels ...string) {
|
||||
style := styleDefs[p.style]
|
||||
for _, label := range labels {
|
||||
fmt.Fprintf(buf, " ")
|
||||
fmt.Fprintf(buf, "\x1b[%d;%dm", p.color, p.bgcolor)
|
||||
fmt.Fprintf(buf, "%s %s %s", style.lineV, label, style.lineV)
|
||||
fmt.Fprintf(buf, "\x1b[%dm", 0)
|
||||
}
|
||||
fmt.Fprintf(buf, "\n")
|
||||
}
|
||||
|
||||
// DrawArrow between boxes
|
||||
func (p *Pen) DrawArrow() *Drawing {
|
||||
drawing := &Drawing{
|
||||
buf: new(strings.Builder),
|
||||
width: 1,
|
||||
}
|
||||
fmt.Fprintf(drawing.buf, "\x1b[%dm", p.color)
|
||||
fmt.Fprintf(drawing.buf, "\u2b07")
|
||||
fmt.Fprintf(drawing.buf, "\x1b[%dm", 0)
|
||||
return drawing
|
||||
}
|
||||
|
||||
// DrawBoxes to draw boxes
|
||||
func (p *Pen) DrawBoxes(labels ...string) *Drawing {
|
||||
width := 0
|
||||
for _, l := range labels {
|
||||
width += len(l) + 2 + 2 + 1
|
||||
}
|
||||
drawing := &Drawing{
|
||||
buf: new(strings.Builder),
|
||||
width: width,
|
||||
}
|
||||
p.drawTopBars(drawing.buf, labels...)
|
||||
p.drawLabels(drawing.buf, labels...)
|
||||
p.drawBottomBars(drawing.buf, labels...)
|
||||
|
||||
return drawing
|
||||
}
|
||||
|
||||
// Draw to writer
|
||||
func (d *Drawing) Draw(writer io.Writer, centerOnWidth int) {
|
||||
padSize := max((centerOnWidth-d.GetWidth())/2, 0)
|
||||
for l := range strings.SplitSeq(d.buf.String(), "\n") {
|
||||
if len(l) > 0 {
|
||||
padding := strings.Repeat(" ", padSize)
|
||||
fmt.Fprintf(writer, "%s%s\n", padding, l)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetWidth of drawing
|
||||
func (d *Drawing) GetWidth() int {
|
||||
return d.width
|
||||
}
|
||||
@@ -12,6 +12,24 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Warning that implements `error` but safe to ignore
|
||||
type Warning struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
// Error the contract for error
|
||||
func (w Warning) Error() string {
|
||||
return w.Message
|
||||
}
|
||||
|
||||
// Warningf create a warning
|
||||
func Warningf(format string, args ...any) Warning {
|
||||
w := Warning{
|
||||
Message: fmt.Sprintf(format, args...),
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
// Executor define contract for the steps of a workflow
|
||||
type Executor func(ctx context.Context) error
|
||||
|
||||
@@ -144,9 +162,15 @@ func NewParallelExecutor(parallel int, executors ...Executor) Executor {
|
||||
// Then runs another executor if this executor succeeds
|
||||
func (e Executor) Then(then Executor) Executor {
|
||||
return func(ctx context.Context) error {
|
||||
if err := e(ctx); err != nil {
|
||||
err := e(ctx)
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case Warning:
|
||||
Logger(ctx).Warning(err.Error())
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
77
act/common/file.go
Normal file
77
act/common/file.go
Normal file
@@ -0,0 +1,77 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2020 The nektos/act Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// CopyFile copy file
|
||||
func CopyFile(source, dest string) (err error) {
|
||||
sourcefile, err := os.Open(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer sourcefile.Close()
|
||||
|
||||
destfile, err := os.Create(dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer destfile.Close()
|
||||
|
||||
_, err = io.Copy(destfile, sourcefile)
|
||||
if err == nil {
|
||||
sourceinfo, err := os.Stat(source)
|
||||
if err != nil {
|
||||
_ = os.Chmod(dest, sourceinfo.Mode())
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// CopyDir recursive copy of directory
|
||||
func CopyDir(source, dest string) (err error) {
|
||||
// get properties of source dir
|
||||
sourceinfo, err := os.Stat(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create dest dir
|
||||
|
||||
err = os.MkdirAll(dest, sourceinfo.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
objects, err := os.ReadDir(source)
|
||||
|
||||
for _, obj := range objects {
|
||||
sourcefilepointer := source + "/" + obj.Name()
|
||||
|
||||
destinationfilepointer := dest + "/" + obj.Name()
|
||||
|
||||
if obj.IsDir() {
|
||||
// create sub-directories - recursively
|
||||
err = CopyDir(sourcefilepointer, destinationfilepointer)
|
||||
if err != nil {
|
||||
fmt.Println(err) //nolint:forbidigo // pre-existing issue from nektos/act
|
||||
}
|
||||
} else {
|
||||
// perform copy
|
||||
err = CopyFile(sourcefilepointer, destinationfilepointer)
|
||||
if err != nil {
|
||||
fmt.Println(err) //nolint:forbidigo // pre-existing issue from nektos/act
|
||||
}
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -8,12 +8,24 @@ package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"gitea.com/gitea/runner/act/common"
|
||||
|
||||
"github.com/moby/moby/client"
|
||||
)
|
||||
|
||||
var (
|
||||
dockerNetworkRemoveRetryInterval = 200 * time.Millisecond
|
||||
dockerNetworkRemoveTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
type dockerNetworkClient interface {
|
||||
NetworkList(ctx context.Context, options client.NetworkListOptions) (client.NetworkListResult, error)
|
||||
NetworkInspect(ctx context.Context, networkID string, options client.NetworkInspectOptions) (client.NetworkInspectResult, error)
|
||||
NetworkRemove(ctx context.Context, networkID string, options client.NetworkRemoveOptions) (client.NetworkRemoveResult, error)
|
||||
}
|
||||
|
||||
func NewDockerNetworkCreateExecutor(name string) common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
cli, err := GetDockerClient(ctx)
|
||||
@@ -56,31 +68,64 @@ func NewDockerNetworkRemoveExecutor(name string) common.Executor {
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
// Make sure that all network of the specified name are removed
|
||||
// cli.NetworkRemove refuses to remove a network if there are duplicates
|
||||
networks, err := cli.NetworkList(ctx, client.NetworkListOptions{})
|
||||
return removeDockerNetworks(ctx, cli, name)
|
||||
}
|
||||
}
|
||||
|
||||
func removeDockerNetworks(ctx context.Context, cli dockerNetworkClient, name string) error {
|
||||
cleanupCtx, cancel := context.WithTimeout(ctx, dockerNetworkRemoveTimeout)
|
||||
defer cancel()
|
||||
|
||||
for {
|
||||
pendingRemoval, err := removeDockerNetworksOnce(cleanupCtx, cli, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !pendingRemoval {
|
||||
return nil
|
||||
}
|
||||
|
||||
select {
|
||||
case <-cleanupCtx.Done():
|
||||
common.Logger(ctx).Warnf("Timed out waiting for Docker network %v endpoints to detach; leaving network behind", name)
|
||||
return nil
|
||||
case <-time.After(dockerNetworkRemoveRetryInterval):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeDockerNetworksOnce(ctx context.Context, cli dockerNetworkClient, name string) (bool, error) {
|
||||
// Make sure that all network of the specified name are removed.
|
||||
// cli.NetworkRemove refuses to remove a network if there are duplicates.
|
||||
networks, err := cli.NetworkList(ctx, client.NetworkListOptions{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
// For Gitea, reduce log noise
|
||||
// common.Logger(ctx).Debugf("%v", networks)
|
||||
|
||||
pendingRemoval := false
|
||||
for _, n := range networks.Items {
|
||||
if n.Name == name {
|
||||
if n.Name != name {
|
||||
continue
|
||||
}
|
||||
|
||||
result, err := cli.NetworkInspect(ctx, n.ID, client.NetworkInspectOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
|
||||
if len(result.Network.Containers) != 0 {
|
||||
pendingRemoval = true
|
||||
common.Logger(ctx).Debugf("Waiting to remove network %v because it still has active endpoints", name)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(result.Network.Containers) == 0 {
|
||||
if _, err = cli.NetworkRemove(ctx, n.ID, client.NetworkRemoveOptions{}); err != nil {
|
||||
common.Logger(ctx).Debugf("%v", err)
|
||||
}
|
||||
} else {
|
||||
common.Logger(ctx).Debugf("Refusing to remove network %v because it still has active endpoints", name)
|
||||
}
|
||||
pendingRemoval = true
|
||||
common.Logger(ctx).Debugf("Retrying Docker network removal for %v: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
return pendingRemoval, nil
|
||||
}
|
||||
|
||||
115
act/container/docker_network_test.go
Normal file
115
act/container/docker_network_test.go
Normal file
@@ -0,0 +1,115 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 The nektos/act Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows || netbsd))
|
||||
|
||||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
containernetwork "github.com/moby/moby/api/types/network"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type fakeDockerNetworkClient struct {
|
||||
listResult client.NetworkListResult
|
||||
inspectByID map[string][]client.NetworkInspectResult
|
||||
inspectCalls map[string]int
|
||||
removeCalls []string
|
||||
removeErrs map[string][]error
|
||||
removeIdx map[string]int
|
||||
}
|
||||
|
||||
func (f *fakeDockerNetworkClient) NetworkList(context.Context, client.NetworkListOptions) (client.NetworkListResult, error) {
|
||||
return f.listResult, nil
|
||||
}
|
||||
|
||||
func (f *fakeDockerNetworkClient) NetworkInspect(_ context.Context, networkID string, _ client.NetworkInspectOptions) (client.NetworkInspectResult, error) {
|
||||
idx := f.inspectCalls[networkID]
|
||||
f.inspectCalls[networkID] = idx + 1
|
||||
results := f.inspectByID[networkID]
|
||||
if len(results) == 0 {
|
||||
return client.NetworkInspectResult{}, nil
|
||||
}
|
||||
if idx >= len(results) {
|
||||
return results[len(results)-1], nil
|
||||
}
|
||||
return results[idx], nil
|
||||
}
|
||||
|
||||
func (f *fakeDockerNetworkClient) NetworkRemove(_ context.Context, networkID string, _ client.NetworkRemoveOptions) (client.NetworkRemoveResult, error) {
|
||||
f.removeCalls = append(f.removeCalls, networkID)
|
||||
idx := f.removeIdx[networkID]
|
||||
f.removeIdx[networkID] = idx + 1
|
||||
if errs := f.removeErrs[networkID]; idx < len(errs) {
|
||||
return client.NetworkRemoveResult{}, errs[idx]
|
||||
}
|
||||
return client.NetworkRemoveResult{}, nil
|
||||
}
|
||||
|
||||
func TestRemoveDockerNetworksRetriesUntilEndpointsDetach(t *testing.T) {
|
||||
originalInterval := dockerNetworkRemoveRetryInterval
|
||||
originalTimeout := dockerNetworkRemoveTimeout
|
||||
dockerNetworkRemoveRetryInterval = time.Millisecond
|
||||
dockerNetworkRemoveTimeout = 50 * time.Millisecond
|
||||
t.Cleanup(func() {
|
||||
dockerNetworkRemoveRetryInterval = originalInterval
|
||||
dockerNetworkRemoveTimeout = originalTimeout
|
||||
})
|
||||
|
||||
cli := &fakeDockerNetworkClient{
|
||||
listResult: client.NetworkListResult{
|
||||
Items: []containernetwork.Summary{{Network: containernetwork.Network{ID: "n1", Name: "test"}}},
|
||||
},
|
||||
inspectByID: map[string][]client.NetworkInspectResult{
|
||||
"n1": {
|
||||
{Network: containernetwork.Inspect{Containers: map[string]containernetwork.EndpointResource{"c1": {}}}},
|
||||
{Network: containernetwork.Inspect{Containers: map[string]containernetwork.EndpointResource{}}},
|
||||
},
|
||||
},
|
||||
inspectCalls: map[string]int{},
|
||||
removeErrs: map[string][]error{},
|
||||
removeIdx: map[string]int{},
|
||||
}
|
||||
|
||||
err := removeDockerNetworks(context.Background(), cli, "test")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{"n1"}, cli.removeCalls)
|
||||
assert.GreaterOrEqual(t, cli.inspectCalls["n1"], 2)
|
||||
}
|
||||
|
||||
func TestRemoveDockerNetworksStopsRetryingAfterTimeout(t *testing.T) {
|
||||
originalInterval := dockerNetworkRemoveRetryInterval
|
||||
originalTimeout := dockerNetworkRemoveTimeout
|
||||
dockerNetworkRemoveRetryInterval = time.Millisecond
|
||||
dockerNetworkRemoveTimeout = 5 * time.Millisecond
|
||||
t.Cleanup(func() {
|
||||
dockerNetworkRemoveRetryInterval = originalInterval
|
||||
dockerNetworkRemoveTimeout = originalTimeout
|
||||
})
|
||||
|
||||
cli := &fakeDockerNetworkClient{
|
||||
listResult: client.NetworkListResult{
|
||||
Items: []containernetwork.Summary{{Network: containernetwork.Network{ID: "n1", Name: "test"}}},
|
||||
},
|
||||
inspectByID: map[string][]client.NetworkInspectResult{
|
||||
"n1": {
|
||||
{Network: containernetwork.Inspect{Containers: map[string]containernetwork.EndpointResource{"c1": {}}}},
|
||||
},
|
||||
},
|
||||
inspectCalls: map[string]int{},
|
||||
removeErrs: map[string][]error{},
|
||||
removeIdx: map[string]int{},
|
||||
}
|
||||
|
||||
err := removeDockerNetworks(context.Background(), cli, "test")
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, cli.removeCalls)
|
||||
assert.Positive(t, cli.inspectCalls["n1"])
|
||||
}
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -969,7 +968,22 @@ func (cr *containerReference) sanitizeConfig(ctx context.Context, config *contai
|
||||
logger := common.Logger(ctx)
|
||||
|
||||
if len(cr.input.ValidVolumes) > 0 {
|
||||
matcher := newValidVolumeMatcher(ctx, cr.input.ValidVolumes)
|
||||
globs := make([]glob.Glob, 0, len(cr.input.ValidVolumes))
|
||||
for _, v := range cr.input.ValidVolumes {
|
||||
if g, err := glob.Compile(v); err != nil {
|
||||
logger.Errorf("create glob from %s error: %v", v, err)
|
||||
} else {
|
||||
globs = append(globs, g)
|
||||
}
|
||||
}
|
||||
isValid := func(v string) bool {
|
||||
for _, g := range globs {
|
||||
if g.Match(v) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
// sanitize binds
|
||||
sanitizedBinds := make([]string, 0, len(hostConfig.Binds))
|
||||
for _, bind := range hostConfig.Binds {
|
||||
@@ -983,7 +997,7 @@ func (cr *containerReference) sanitizeConfig(ctx context.Context, config *contai
|
||||
sanitizedBinds = append(sanitizedBinds, bind)
|
||||
continue
|
||||
}
|
||||
if matcher.isValid(parsed.Source, mount.Type(parsed.Type)) {
|
||||
if isValid(parsed.Source) {
|
||||
sanitizedBinds = append(sanitizedBinds, bind)
|
||||
} else {
|
||||
logger.Warnf("[%s] is not a valid volume, will be ignored", parsed.Source)
|
||||
@@ -993,7 +1007,7 @@ func (cr *containerReference) sanitizeConfig(ctx context.Context, config *contai
|
||||
// sanitize mounts
|
||||
sanitizedMounts := make([]mount.Mount, 0, len(hostConfig.Mounts))
|
||||
for _, mt := range hostConfig.Mounts {
|
||||
if matcher.isValid(mt.Source, mt.Type) {
|
||||
if isValid(mt.Source) {
|
||||
sanitizedMounts = append(sanitizedMounts, mt)
|
||||
} else {
|
||||
logger.Warnf("[%s] is not a valid volume, will be ignored", mt.Source)
|
||||
@@ -1007,129 +1021,3 @@ func (cr *containerReference) sanitizeConfig(ctx context.Context, config *contai
|
||||
|
||||
return config, hostConfig
|
||||
}
|
||||
|
||||
type validVolumeMatcher struct {
|
||||
allowAll bool
|
||||
named []glob.Glob
|
||||
host []glob.Glob
|
||||
}
|
||||
|
||||
func newValidVolumeMatcher(ctx context.Context, validVolumes []string) validVolumeMatcher {
|
||||
logger := common.Logger(ctx)
|
||||
ret := validVolumeMatcher{
|
||||
named: make([]glob.Glob, 0, len(validVolumes)),
|
||||
host: make([]glob.Glob, 0, len(validVolumes)),
|
||||
}
|
||||
|
||||
for _, v := range validVolumes {
|
||||
if v == "**" {
|
||||
ret.allowAll = true
|
||||
continue
|
||||
}
|
||||
if !isHostVolumePattern(v) {
|
||||
if g, err := glob.Compile(v); err != nil {
|
||||
logger.Errorf("create glob from %s error: %v", v, err)
|
||||
} else {
|
||||
ret.named = append(ret.named, g)
|
||||
}
|
||||
continue
|
||||
}
|
||||
normalized, err := normalizeHostVolumePath(v)
|
||||
if err != nil {
|
||||
logger.Errorf("normalize volume pattern %s error: %v", v, err)
|
||||
continue
|
||||
}
|
||||
if g, err := glob.Compile(normalized); err != nil {
|
||||
logger.Errorf("create glob from %s error: %v", normalized, err)
|
||||
} else {
|
||||
ret.host = append(ret.host, g)
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (m validVolumeMatcher) isValid(source string, sourceType mount.Type) bool {
|
||||
if m.allowAll {
|
||||
return true
|
||||
}
|
||||
if isHostVolumeSource(source, sourceType) {
|
||||
normalized, err := normalizeHostVolumePath(source)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, g := range m.host {
|
||||
if g.Match(normalized) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
for _, g := range m.named {
|
||||
if g.Match(source) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isHostVolumePattern(pattern string) bool {
|
||||
return filepath.IsAbs(pattern) ||
|
||||
strings.HasPrefix(pattern, "."+string(filepath.Separator)) ||
|
||||
strings.HasPrefix(pattern, ".."+string(filepath.Separator)) ||
|
||||
strings.Contains(pattern, "/") ||
|
||||
strings.Contains(pattern, `\`)
|
||||
}
|
||||
|
||||
func isHostVolumeSource(source string, sourceType mount.Type) bool {
|
||||
if sourceType == mount.TypeBind {
|
||||
return true
|
||||
}
|
||||
if sourceType == mount.TypeVolume {
|
||||
return false
|
||||
}
|
||||
return isHostVolumePattern(source)
|
||||
}
|
||||
|
||||
func normalizeHostVolumePath(path string) (string, error) {
|
||||
abs, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return evalSymlinksExistingPrefix(abs)
|
||||
}
|
||||
|
||||
func evalSymlinksExistingPrefix(path string) (string, error) {
|
||||
resolved, err := filepath.EvalSymlinks(path)
|
||||
if err == nil {
|
||||
return filepath.Clean(resolved), nil
|
||||
}
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
current := path
|
||||
var missing []string
|
||||
for {
|
||||
_, err := os.Lstat(current)
|
||||
if err == nil {
|
||||
resolved, err := filepath.EvalSymlinks(current)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, name := range slices.Backward(missing) {
|
||||
resolved = filepath.Join(resolved, name)
|
||||
}
|
||||
return filepath.Clean(resolved), nil
|
||||
}
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
return "", err
|
||||
}
|
||||
parent := filepath.Dir(current)
|
||||
if parent == current {
|
||||
return filepath.Clean(path), nil
|
||||
}
|
||||
missing = append(missing, filepath.Base(current))
|
||||
current = parent
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,6 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -377,40 +375,3 @@ func TestCheckVolumes(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckVolumesRejectsEscapingHostPaths(t *testing.T) {
|
||||
logger, _ := test.NewNullLogger()
|
||||
ctx := common.WithLogger(context.Background(), logger)
|
||||
|
||||
base := t.TempDir()
|
||||
allowed := filepath.Join(base, "allowed")
|
||||
denied := filepath.Join(base, "denied")
|
||||
require.NoError(t, os.MkdirAll(allowed, 0o700))
|
||||
require.NoError(t, os.MkdirAll(denied, 0o700))
|
||||
|
||||
cr := &containerReference{
|
||||
input: &NewContainerInput{
|
||||
ValidVolumes: []string{filepath.Join(allowed, "**")},
|
||||
},
|
||||
}
|
||||
|
||||
escapingPath := allowed + string(filepath.Separator) + ".." + string(filepath.Separator) + "denied"
|
||||
_, hostConf := cr.sanitizeConfig(ctx, &container.Config{}, &container.HostConfig{
|
||||
Binds: []string{escapingPath + ":/mnt"},
|
||||
})
|
||||
assert.Empty(t, hostConf.Binds)
|
||||
|
||||
linkPath := filepath.Join(allowed, "link")
|
||||
if err := os.Symlink(denied, linkPath); err != nil {
|
||||
t.Skipf("cannot create symlink: %v", err)
|
||||
}
|
||||
_, hostConf = cr.sanitizeConfig(ctx, &container.Config{}, &container.HostConfig{
|
||||
Binds: []string{linkPath + ":/mnt"},
|
||||
})
|
||||
assert.Empty(t, hostConf.Binds)
|
||||
|
||||
_, hostConf = cr.sanitizeConfig(ctx, &container.Config{}, &container.HostConfig{
|
||||
Binds: []string{filepath.Join(linkPath, "missing") + ":/mnt"},
|
||||
})
|
||||
assert.Empty(t, hostConf.Binds)
|
||||
}
|
||||
|
||||
@@ -37,9 +37,9 @@ type HostEnvironment struct {
|
||||
TmpDir string
|
||||
ToolCache string
|
||||
Workdir string
|
||||
// CleanWorkdir means teardown owns Workdir and may delete it. Leave false
|
||||
// when Workdir points at a caller-owned checkout (e.g. `act` local mode).
|
||||
CleanWorkdir bool
|
||||
// BindWorkdir is true when the app runner mounts the workspace on the host and
|
||||
// deletes the task directory after the job; host teardown must not remove Workdir.
|
||||
BindWorkdir bool
|
||||
ActPath string
|
||||
CleanUp func()
|
||||
StdOut io.Writer
|
||||
@@ -483,7 +483,7 @@ func (e *HostEnvironment) Remove() common.Executor {
|
||||
logger.Warnf("failed to remove host misc state %s: %v", e.Path, err)
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if e.CleanWorkdir {
|
||||
if !e.BindWorkdir && e.Workdir != "" {
|
||||
if err := removePathWithRetry(ctx, e.Workdir); err != nil {
|
||||
logger.Warnf("failed to remove host workspace %s: %v", e.Workdir, err)
|
||||
errs = append(errs, err)
|
||||
|
||||
@@ -141,7 +141,7 @@ func TestHostEnvironmentAllocatePTY(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostEnvironmentRemovePreservesWorkdirByDefault(t *testing.T) {
|
||||
func TestHostEnvironmentRemoveCleansWorkdir(t *testing.T) {
|
||||
logger := logrus.New()
|
||||
ctx := common.WithLogger(context.Background(), logrus.NewEntry(logger))
|
||||
base := t.TempDir()
|
||||
@@ -154,6 +154,7 @@ func TestHostEnvironmentRemovePreservesWorkdirByDefault(t *testing.T) {
|
||||
e := &HostEnvironment{
|
||||
Path: path,
|
||||
Workdir: workdir,
|
||||
BindWorkdir: false,
|
||||
CleanUp: func() {
|
||||
_ = os.RemoveAll(miscRoot)
|
||||
},
|
||||
@@ -161,10 +162,10 @@ func TestHostEnvironmentRemovePreservesWorkdirByDefault(t *testing.T) {
|
||||
}
|
||||
require.NoError(t, e.Remove()(ctx))
|
||||
_, err := os.Stat(workdir)
|
||||
require.NoError(t, err)
|
||||
assert.ErrorIs(t, err, os.ErrNotExist)
|
||||
}
|
||||
|
||||
func TestHostEnvironmentRemoveCleansWorkdirWhenOwned(t *testing.T) {
|
||||
func TestHostEnvironmentRemoveSkipsWorkdirWhenBindWorkdir(t *testing.T) {
|
||||
logger := logrus.New()
|
||||
ctx := common.WithLogger(context.Background(), logrus.NewEntry(logger))
|
||||
base := t.TempDir()
|
||||
@@ -177,7 +178,7 @@ func TestHostEnvironmentRemoveCleansWorkdirWhenOwned(t *testing.T) {
|
||||
e := &HostEnvironment{
|
||||
Path: path,
|
||||
Workdir: workdir,
|
||||
CleanWorkdir: true,
|
||||
BindWorkdir: true,
|
||||
CleanUp: func() {
|
||||
_ = os.RemoveAll(miscRoot)
|
||||
},
|
||||
@@ -185,5 +186,5 @@ func TestHostEnvironmentRemoveCleansWorkdirWhenOwned(t *testing.T) {
|
||||
}
|
||||
require.NoError(t, e.Remove()(ctx))
|
||||
_, err := os.Stat(workdir)
|
||||
assert.ErrorIs(t, err, os.ErrNotExist)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -29,8 +29,6 @@ func parseEnvFile(e Container, srcPath string, env *map[string]string) common.Ex
|
||||
return err
|
||||
}
|
||||
s := bufio.NewScanner(reader)
|
||||
// Default 64 KiB max token size is too small for realistic env-file lines; allow up to 16 MiB.
|
||||
s.Buffer(make([]byte, 0, 64*1024), 16*1024*1024)
|
||||
for s.Scan() {
|
||||
line := s.Text()
|
||||
singleLineEnv := strings.Index(line, "=")
|
||||
@@ -52,9 +50,6 @@ func parseEnvFile(e Container, srcPath string, env *map[string]string) common.Ex
|
||||
}
|
||||
multiLineEnvContent += content
|
||||
}
|
||||
if err := s.Err(); err != nil {
|
||||
return fmt.Errorf("reading env file: %w", err)
|
||||
}
|
||||
if !delimiterFound {
|
||||
return fmt.Errorf("invalid format delimiter '%v' not found before end of file", multiLineEnvDelimiter)
|
||||
}
|
||||
@@ -63,9 +58,6 @@ func parseEnvFile(e Container, srcPath string, env *map[string]string) common.Ex
|
||||
return fmt.Errorf("invalid format '%v', expected a line with '=' or '<<'", line)
|
||||
}
|
||||
}
|
||||
if err := s.Err(); err != nil {
|
||||
return fmt.Errorf("reading env file: %w", err)
|
||||
}
|
||||
env = &localEnv
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package container
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func newTestHostEnv(t *testing.T) (*HostEnvironment, string) {
|
||||
t.Helper()
|
||||
e := &HostEnvironment{Path: t.TempDir()}
|
||||
return e, filepath.Join(e.Path, "envfile")
|
||||
}
|
||||
|
||||
func TestParseEnvFileSingleLine(t *testing.T) {
|
||||
e, envPath := newTestHostEnv(t)
|
||||
require.NoError(t, os.WriteFile(envPath, []byte("FOO=bar\nBAZ=qux\n"), 0o600))
|
||||
|
||||
env := map[string]string{}
|
||||
require.NoError(t, parseEnvFile(e, envPath, &env)(context.Background()))
|
||||
assert.Equal(t, "bar", env["FOO"])
|
||||
assert.Equal(t, "qux", env["BAZ"])
|
||||
}
|
||||
|
||||
func TestParseEnvFileMultiLine(t *testing.T) {
|
||||
e, envPath := newTestHostEnv(t)
|
||||
content := "FOO<<EOF\nline1\nline2\nEOF\n"
|
||||
require.NoError(t, os.WriteFile(envPath, []byte(content), 0o600))
|
||||
|
||||
env := map[string]string{}
|
||||
require.NoError(t, parseEnvFile(e, envPath, &env)(context.Background()))
|
||||
assert.Equal(t, "line1\nline2", env["FOO"])
|
||||
}
|
||||
|
||||
func TestParseEnvFileLargeValueWithinLimit(t *testing.T) {
|
||||
e, envPath := newTestHostEnv(t)
|
||||
big := strings.Repeat("x", 2*1024*1024)
|
||||
content := "FOO<<EOF\n" + big + "\nEOF\n"
|
||||
require.NoError(t, os.WriteFile(envPath, []byte(content), 0o600))
|
||||
|
||||
env := map[string]string{}
|
||||
require.NoError(t, parseEnvFile(e, envPath, &env)(context.Background()))
|
||||
assert.Equal(t, big, env["FOO"])
|
||||
}
|
||||
|
||||
func TestParseEnvFileLineExceedsBufferReportsScannerError(t *testing.T) {
|
||||
e, envPath := newTestHostEnv(t)
|
||||
tooBig := strings.Repeat("x", 17*1024*1024) // over the 16 MiB cap
|
||||
content := "FOO<<EOF\n" + tooBig + "\nEOF\n"
|
||||
require.NoError(t, os.WriteFile(envPath, []byte(content), 0o600))
|
||||
|
||||
env := map[string]string{}
|
||||
err := parseEnvFile(e, envPath, &env)(context.Background())
|
||||
require.ErrorIs(t, err, bufio.ErrTooLong)
|
||||
assert.Contains(t, err.Error(), "reading env file")
|
||||
}
|
||||
|
||||
func TestParseEnvFileMissingDelimiter(t *testing.T) {
|
||||
e, envPath := newTestHostEnv(t)
|
||||
require.NoError(t, os.WriteFile(envPath, []byte("FOO<<EOF\nline1\nline2\n"), 0o600))
|
||||
|
||||
env := map[string]string{}
|
||||
err := parseEnvFile(e, envPath, &env)(context.Background())
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "delimiter")
|
||||
}
|
||||
@@ -4,6 +4,18 @@
|
||||
|
||||
package lookpath
|
||||
|
||||
import "os"
|
||||
|
||||
type Env interface {
|
||||
Getenv(name string) string
|
||||
}
|
||||
|
||||
type defaultEnv struct{}
|
||||
|
||||
func (*defaultEnv) Getenv(name string) string {
|
||||
return os.Getenv(name)
|
||||
}
|
||||
|
||||
func LookPath(file string) (string, error) {
|
||||
return LookPath2(file, &defaultEnv{})
|
||||
}
|
||||
|
||||
45
act/runner/action_cache_offline_mode.go
Normal file
45
act/runner/action_cache_offline_mode.go
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2024 The nektos/act Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"path"
|
||||
|
||||
git "github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
)
|
||||
|
||||
type GoGitActionCacheOfflineMode struct {
|
||||
Parent GoGitActionCache
|
||||
}
|
||||
|
||||
func (c GoGitActionCacheOfflineMode) Fetch(ctx context.Context, cacheDir, url, ref, token string) (string, error) {
|
||||
sha, fetchErr := c.Parent.Fetch(ctx, cacheDir, url, ref, token)
|
||||
gitPath := path.Join(c.Parent.Path, safeFilename(cacheDir)+".git")
|
||||
gogitrepo, err := git.PlainOpen(gitPath)
|
||||
if err != nil {
|
||||
return "", fetchErr
|
||||
}
|
||||
refName := plumbing.ReferenceName("refs/action-cache-offline/" + ref)
|
||||
r, err := gogitrepo.Reference(refName, true)
|
||||
if fetchErr == nil {
|
||||
if err != nil || sha != r.Hash().String() {
|
||||
if err == nil {
|
||||
refName = r.Name()
|
||||
}
|
||||
ref := plumbing.NewHashReference(refName, plumbing.NewHash(sha))
|
||||
_ = gogitrepo.Storer.SetReference(ref)
|
||||
}
|
||||
} else if err == nil {
|
||||
return r.Hash().String(), nil
|
||||
}
|
||||
return sha, fetchErr
|
||||
}
|
||||
|
||||
func (c GoGitActionCacheOfflineMode) GetTarArchive(ctx context.Context, cacheDir, sha, includePrefix string) (io.ReadCloser, error) {
|
||||
return c.Parent.GetTarArchive(ctx, cacheDir, sha, includePrefix)
|
||||
}
|
||||
@@ -35,6 +35,7 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
|
||||
steps := make([]common.Executor, 0)
|
||||
preSteps := make([]common.Executor, 0)
|
||||
var postExecutor common.Executor
|
||||
var startErr error
|
||||
|
||||
steps = append(steps, func(ctx context.Context) error {
|
||||
logger := common.Logger(ctx)
|
||||
@@ -165,7 +166,12 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
|
||||
pipeline = append(pipeline, preSteps...)
|
||||
pipeline = append(pipeline, steps...)
|
||||
|
||||
return common.NewPipelineExecutor(info.startContainer(), common.NewPipelineExecutor(pipeline...).
|
||||
startContainer := func(ctx context.Context) error {
|
||||
startErr = info.startContainer()(ctx)
|
||||
return startErr
|
||||
}
|
||||
|
||||
return common.NewPipelineExecutor(startContainer, common.NewPipelineExecutor(pipeline...).
|
||||
Finally(func(ctx context.Context) error {
|
||||
var cancel context.CancelFunc
|
||||
if ctx.Err() == context.Canceled {
|
||||
@@ -176,8 +182,23 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
|
||||
}
|
||||
return postExecutor(ctx)
|
||||
}).
|
||||
Finally(info.interpolateOutputs()).
|
||||
Finally(info.closeContainer()))
|
||||
Finally(info.interpolateOutputs())).
|
||||
Finally(func(ctx context.Context) error {
|
||||
if startErr == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
cleanupCtx, cancel := context.WithTimeout(common.WithLogger(context.Background(), common.Logger(ctx)), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
logger := common.Logger(cleanupCtx)
|
||||
logger.Infof("Cleaning up container for failed startup of job %s", rc.JobName)
|
||||
if err := info.stopContainer()(cleanupCtx); err != nil {
|
||||
logger.Errorf("Error while cleaning up failed job startup: %v", err)
|
||||
}
|
||||
return nil
|
||||
}).
|
||||
Finally(info.closeContainer())
|
||||
}
|
||||
|
||||
func setJobResult(ctx context.Context, info jobInfo, rc *RunContext, success bool) {
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestJobExecutor(t *testing.T) {
|
||||
@@ -341,3 +342,64 @@ func TestNewJobExecutor(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewJobExecutorCleansUpAfterStartContainerFailure(t *testing.T) {
|
||||
ctx := common.WithJobErrorContainer(context.Background())
|
||||
jim := &jobInfoMock{}
|
||||
sfm := &stepFactoryMock{}
|
||||
rc := &RunContext{
|
||||
JobName: "test",
|
||||
JobContainer: &jobContainerMock{},
|
||||
Run: &model.Run{
|
||||
JobID: "test",
|
||||
Workflow: &model.Workflow{
|
||||
Jobs: map[string]*model.Job{
|
||||
"test": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
Config: &Config{},
|
||||
}
|
||||
rc.ExprEval = rc.NewExpressionEvaluator(ctx)
|
||||
|
||||
executorOrder := make([]string, 0)
|
||||
startErr := errors.New("failed to start container")
|
||||
stepModel := &model.Step{ID: "1"}
|
||||
sm := &stepMock{}
|
||||
|
||||
jim.On("steps").Return([]*model.Step{stepModel})
|
||||
jim.On("startContainer").Return(func(ctx context.Context) error {
|
||||
executorOrder = append(executorOrder, "startContainer")
|
||||
return startErr
|
||||
})
|
||||
jim.On("stopContainer").Return(func(ctx context.Context) error {
|
||||
executorOrder = append(executorOrder, "stopContainer")
|
||||
return nil
|
||||
})
|
||||
jim.On("closeContainer").Return(func(ctx context.Context) error {
|
||||
executorOrder = append(executorOrder, "closeContainer")
|
||||
return nil
|
||||
})
|
||||
jim.On("interpolateOutputs").Return(func(ctx context.Context) error {
|
||||
return nil
|
||||
})
|
||||
sfm.On("newStep", stepModel, rc).Return(sm, nil)
|
||||
sm.On("pre").Return(func(ctx context.Context) error {
|
||||
return nil
|
||||
})
|
||||
sm.On("main").Return(func(ctx context.Context) error {
|
||||
return nil
|
||||
})
|
||||
sm.On("post").Return(func(ctx context.Context) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
executor := newJobExecutor(jim, sfm, rc)
|
||||
err := executor(ctx)
|
||||
require.ErrorIs(t, err, startErr)
|
||||
assert.Equal(t, []string{"startContainer", "stopContainer", "closeContainer"}, executorOrder)
|
||||
|
||||
jim.AssertExpectations(t)
|
||||
sfm.AssertExpectations(t)
|
||||
sm.AssertExpectations(t)
|
||||
}
|
||||
|
||||
@@ -308,11 +308,6 @@ func getGitCloneToken(conf *Config, cloneURL string) string {
|
||||
// 1. cloneURL is from the same Gitea instance that the runner is registered to
|
||||
// 2. the cloneURL does not have basic auth embedded
|
||||
func shouldCloneURLUseToken(instanceURL, cloneURL string) bool {
|
||||
if !strings.HasPrefix(instanceURL, "http://") &&
|
||||
!strings.HasPrefix(instanceURL, "https://") {
|
||||
instanceURL = "https://" + instanceURL
|
||||
}
|
||||
|
||||
u1, err1 := url.Parse(instanceURL)
|
||||
u2, err2 := url.Parse(cloneURL)
|
||||
if err1 != nil || err2 != nil {
|
||||
|
||||
@@ -123,65 +123,6 @@ func TestNewReusableWorkflowExecutorHoldsCloneLock(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetGitCloneTokenWithSchemalessGiteaInstance(t *testing.T) {
|
||||
conf := &Config{
|
||||
GitHubInstance: "gitea.example.net",
|
||||
Secrets: map[string]string{
|
||||
"GITEA_TOKEN": "token-value",
|
||||
},
|
||||
}
|
||||
|
||||
token := getGitCloneToken(conf, "https://gitea.example.net/actions/tools")
|
||||
|
||||
require.Equal(t, "token-value", token)
|
||||
}
|
||||
|
||||
func TestShouldCloneURLUseToken(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
instanceURL string
|
||||
cloneURL string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "same host with schemaless instance",
|
||||
instanceURL: "gitea.example.net",
|
||||
cloneURL: "https://gitea.example.net/actions/tools",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "same host with schemaless instance and port",
|
||||
instanceURL: "gitea.example.net:3000",
|
||||
cloneURL: "https://gitea.example.net:3000/actions/tools",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "different host",
|
||||
instanceURL: "gitea.example.net",
|
||||
cloneURL: "https://github.com/actions/tools",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "embedded basic auth",
|
||||
instanceURL: "gitea.example.net",
|
||||
cloneURL: "https://user:pass@gitea.example.net/actions/tools",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "invalid clone URL",
|
||||
instanceURL: "gitea.example.net",
|
||||
cloneURL: "://gitea.example.net/actions/tools",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
require.Equal(t, tt.want, shouldCloneURLUseToken(tt.instanceURL, tt.cloneURL))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func gitMust(t *testing.T, dir string, args ...string) {
|
||||
t.Helper()
|
||||
cmd := exec.Command("git", args...)
|
||||
|
||||
@@ -224,7 +224,7 @@ func (rc *RunContext) startHostEnvironment() common.Executor {
|
||||
TmpDir: runnerTmp,
|
||||
ToolCache: toolCache,
|
||||
Workdir: rc.Config.Workdir,
|
||||
CleanWorkdir: rc.Config.CleanWorkdir,
|
||||
BindWorkdir: rc.Config.BindWorkdir,
|
||||
ActPath: actPath,
|
||||
CleanUp: func() {
|
||||
os.RemoveAll(miscpath)
|
||||
|
||||
@@ -73,7 +73,6 @@ type Config struct {
|
||||
EventJSON string // the content of JSON file to use for event.json in containers, overrides EventPath
|
||||
ContainerNamePrefix string // the prefix of container name
|
||||
ContainerMaxLifetime time.Duration // the max lifetime of job containers
|
||||
CleanWorkdir bool // remove host executor workdir on teardown
|
||||
DefaultActionInstance string // the default actions web site
|
||||
PlatformPicker func(labels []string) string // platform picker, it will take precedence over Platforms if isn't nil
|
||||
JobLoggerLevel *log.Level // the level of job logger
|
||||
@@ -92,17 +91,6 @@ func (c Config) GetToken() string {
|
||||
return token
|
||||
}
|
||||
|
||||
// DefaultActionURL returns the host used for implicit remote actions.
|
||||
func (c Config) DefaultActionURL() string {
|
||||
if c.DefaultActionInstance != "" {
|
||||
return c.DefaultActionInstance
|
||||
}
|
||||
if c.GitHubInstance != "" {
|
||||
return c.GitHubInstance
|
||||
}
|
||||
return "github.com"
|
||||
}
|
||||
|
||||
type caller struct {
|
||||
runContext *RunContext
|
||||
|
||||
|
||||
@@ -113,10 +113,9 @@ func (sar *stepActionRemote) prepareActionExecutor() common.Executor {
|
||||
}
|
||||
|
||||
actionDir := fmt.Sprintf("%s/%s", sar.RunContext.ActionCacheDir(), sar.Step.UsesHash())
|
||||
defaultActionURL := sar.RunContext.Config.DefaultActionURL()
|
||||
token := getGitCloneToken(sar.getRunContext().Config, sar.remoteAction.CloneURL(defaultActionURL))
|
||||
token := getGitCloneToken(sar.getRunContext().Config, sar.remoteAction.CloneURL(sar.RunContext.Config.DefaultActionInstance))
|
||||
gitClone := stepActionRemoteNewCloneExecutor(git.NewGitCloneExecutorInput{
|
||||
URL: sar.remoteAction.CloneURL(defaultActionURL),
|
||||
URL: sar.remoteAction.CloneURL(sar.RunContext.Config.DefaultActionInstance),
|
||||
Ref: sar.remoteAction.Ref,
|
||||
Dir: actionDir,
|
||||
Token: token,
|
||||
@@ -275,7 +274,7 @@ func (sar *stepActionRemote) cloneSkipTLS() bool {
|
||||
if sar.remoteAction.URL == "" {
|
||||
// Empty URL means the default action instance should be used
|
||||
// Return true if the URL of the Gitea instance is the same as the URL of the default action instance
|
||||
return sar.RunContext.Config.DefaultActionURL() == sar.RunContext.Config.GitHubInstance
|
||||
return sar.RunContext.Config.DefaultActionInstance == sar.RunContext.Config.GitHubInstance
|
||||
}
|
||||
// Return true if the URL of the remote action is the same as the URL of the Gitea instance
|
||||
return sar.remoteAction.URL == sar.RunContext.Config.GitHubInstance
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
@@ -435,57 +434,6 @@ func TestStepActionRemotePreThroughActionToken(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepActionRemoteUsesGitHubInstanceWhenDefaultActionInstanceEmpty(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
var actualURL string
|
||||
sarm := &stepActionRemoteMocks{}
|
||||
|
||||
origStepAtionRemoteNewCloneExecutor := stepActionRemoteNewCloneExecutor
|
||||
stepActionRemoteNewCloneExecutor = func(input git.NewGitCloneExecutorInput) common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
actualURL = input.URL
|
||||
return nil
|
||||
}
|
||||
}
|
||||
defer func() {
|
||||
stepActionRemoteNewCloneExecutor = origStepAtionRemoteNewCloneExecutor
|
||||
}()
|
||||
|
||||
sar := &stepActionRemote{
|
||||
Step: &model.Step{
|
||||
Uses: "actions/setup-go@v4",
|
||||
},
|
||||
RunContext: &RunContext{
|
||||
Config: &Config{
|
||||
GitHubInstance: "gitea.example",
|
||||
DefaultActionInstance: "",
|
||||
ActionCacheDir: t.TempDir(),
|
||||
},
|
||||
Run: &model.Run{
|
||||
JobID: "1",
|
||||
Workflow: &model.Workflow{
|
||||
Jobs: map[string]*model.Job{
|
||||
"1": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
readAction: sarm.readAction,
|
||||
}
|
||||
|
||||
suffixMatcher := func(suffix string) any {
|
||||
return mock.MatchedBy(func(actionDir string) bool {
|
||||
return strings.HasSuffix(actionDir, suffix)
|
||||
})
|
||||
}
|
||||
sarm.On("readAction", sar.Step, suffixMatcher(sar.Step.UsesHash()), "", mock.Anything, mock.Anything).Return(&model.Action{}, nil)
|
||||
|
||||
require.NoError(t, sar.prepareActionExecutor()(ctx))
|
||||
assert.Equal(t, "https://gitea.example/actions/setup-go", actualURL)
|
||||
sarm.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestStepActionRemotePost(t *testing.T) {
|
||||
table := []struct {
|
||||
name string
|
||||
|
||||
22
act/workflowpattern/trace_writer.go
Normal file
22
act/workflowpattern/trace_writer.go
Normal file
@@ -0,0 +1,22 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2023 The nektos/act Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package workflowpattern
|
||||
|
||||
import "fmt"
|
||||
|
||||
type TraceWriter interface {
|
||||
Info(string, ...any)
|
||||
}
|
||||
|
||||
type EmptyTraceWriter struct{}
|
||||
|
||||
func (*EmptyTraceWriter) Info(string, ...any) {
|
||||
}
|
||||
|
||||
type StdOutTraceWriter struct{}
|
||||
|
||||
func (*StdOutTraceWriter) Info(format string, args ...any) {
|
||||
fmt.Printf(format+"\n", args...) //nolint:forbidigo // pre-existing issue from nektos/act
|
||||
}
|
||||
199
act/workflowpattern/workflow_pattern.go
Normal file
199
act/workflowpattern/workflow_pattern.go
Normal file
@@ -0,0 +1,199 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2023 The nektos/act Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package workflowpattern
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type WorkflowPattern struct {
|
||||
Pattern string
|
||||
Negative bool
|
||||
Regex *regexp.Regexp
|
||||
}
|
||||
|
||||
func CompilePattern(rawpattern string) (*WorkflowPattern, error) {
|
||||
negative := false
|
||||
pattern := rawpattern
|
||||
if strings.HasPrefix(rawpattern, "!") {
|
||||
negative = true
|
||||
pattern = rawpattern[1:]
|
||||
}
|
||||
rpattern, err := PatternToRegex(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
regex, err := regexp.Compile(rpattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &WorkflowPattern{
|
||||
Pattern: pattern,
|
||||
Negative: negative,
|
||||
Regex: regex,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func PatternToRegex(pattern string) (string, error) {
|
||||
var rpattern strings.Builder
|
||||
rpattern.WriteString("^")
|
||||
pos := 0
|
||||
errors := map[int]string{}
|
||||
for pos < len(pattern) {
|
||||
switch pattern[pos] {
|
||||
case '*':
|
||||
if pos+1 < len(pattern) && pattern[pos+1] == '*' {
|
||||
if pos+2 < len(pattern) && pattern[pos+2] == '/' {
|
||||
rpattern.WriteString("(.+/)?")
|
||||
pos += 3
|
||||
} else {
|
||||
rpattern.WriteString(".*")
|
||||
pos += 2
|
||||
}
|
||||
} else {
|
||||
rpattern.WriteString("[^/]*")
|
||||
pos++
|
||||
}
|
||||
case '+', '?':
|
||||
if pos > 0 {
|
||||
rpattern.WriteByte(pattern[pos])
|
||||
} else {
|
||||
rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos]})))
|
||||
}
|
||||
pos++
|
||||
case '[':
|
||||
rpattern.WriteByte(pattern[pos])
|
||||
pos++
|
||||
if pos < len(pattern) && pattern[pos] == ']' {
|
||||
errors[pos] = "Unexpected empty brackets '[]'"
|
||||
pos++
|
||||
break
|
||||
}
|
||||
validChar := func(a, b, test byte) bool {
|
||||
return test >= a && test <= b
|
||||
}
|
||||
startPos := pos
|
||||
for pos < len(pattern) && pattern[pos] != ']' {
|
||||
switch pattern[pos] {
|
||||
case '-':
|
||||
if pos <= startPos || pos+1 >= len(pattern) {
|
||||
errors[pos] = "Invalid range"
|
||||
pos++
|
||||
break
|
||||
}
|
||||
validRange := func(a, b byte) bool {
|
||||
return validChar(a, b, pattern[pos-1]) && validChar(a, b, pattern[pos+1]) && pattern[pos-1] <= pattern[pos+1]
|
||||
}
|
||||
if !validRange('A', 'z') && !validRange('0', '9') {
|
||||
errors[pos] = "Ranges can only include a-z, A-Z, A-z, and 0-9"
|
||||
pos++
|
||||
break
|
||||
}
|
||||
rpattern.WriteString(pattern[pos : pos+2])
|
||||
pos += 2
|
||||
default:
|
||||
if !validChar('A', 'z', pattern[pos]) && !validChar('0', '9', pattern[pos]) {
|
||||
errors[pos] = "Ranges can only include a-z, A-Z and 0-9"
|
||||
pos++
|
||||
break
|
||||
}
|
||||
rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos]})))
|
||||
pos++
|
||||
}
|
||||
}
|
||||
if pos >= len(pattern) || pattern[pos] != ']' {
|
||||
errors[pos] = "Missing closing bracket ']' after '['"
|
||||
pos++
|
||||
}
|
||||
rpattern.WriteString("]")
|
||||
pos++
|
||||
case '\\':
|
||||
if pos+1 >= len(pattern) {
|
||||
errors[pos] = "Missing symbol after \\"
|
||||
pos++
|
||||
break
|
||||
}
|
||||
rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos+1]})))
|
||||
pos += 2
|
||||
default:
|
||||
rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos]})))
|
||||
pos++
|
||||
}
|
||||
}
|
||||
if len(errors) > 0 {
|
||||
var errorMessage strings.Builder
|
||||
for position, err := range errors {
|
||||
if errorMessage.Len() > 0 {
|
||||
errorMessage.WriteString(", ")
|
||||
}
|
||||
fmt.Fprintf(&errorMessage, "Position: %d Error: %s", position, err)
|
||||
}
|
||||
return "", fmt.Errorf("invalid Pattern '%s': %s", pattern, errorMessage.String())
|
||||
}
|
||||
rpattern.WriteString("$")
|
||||
return rpattern.String(), nil
|
||||
}
|
||||
|
||||
func CompilePatterns(patterns ...string) ([]*WorkflowPattern, error) {
|
||||
ret := []*WorkflowPattern{}
|
||||
for _, pattern := range patterns {
|
||||
cp, err := CompilePattern(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret = append(ret, cp)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// returns true if the workflow should be skipped paths/branches
|
||||
func Skip(sequence []*WorkflowPattern, input []string, traceWriter TraceWriter) bool {
|
||||
if len(sequence) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, file := range input {
|
||||
matched := false
|
||||
for _, item := range sequence {
|
||||
if item.Regex.MatchString(file) {
|
||||
pattern := item.Pattern
|
||||
if item.Negative {
|
||||
matched = false
|
||||
traceWriter.Info("%s excluded by pattern %s", file, pattern)
|
||||
} else {
|
||||
matched = true
|
||||
traceWriter.Info("%s included by pattern %s", file, pattern)
|
||||
}
|
||||
}
|
||||
}
|
||||
if matched {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// returns true if the workflow should be skipped paths-ignore/branches-ignore
|
||||
func Filter(sequence []*WorkflowPattern, input []string, traceWriter TraceWriter) bool {
|
||||
if len(sequence) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, file := range input {
|
||||
matched := false
|
||||
for _, item := range sequence {
|
||||
if item.Regex.MatchString(file) == !item.Negative {
|
||||
pattern := item.Pattern
|
||||
traceWriter.Info("%s ignored by pattern %s", file, pattern)
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
418
act/workflowpattern/workflow_pattern_test.go
Normal file
418
act/workflowpattern/workflow_pattern_test.go
Normal file
@@ -0,0 +1,418 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2023 The nektos/act Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package workflowpattern
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMatchPattern(t *testing.T) {
|
||||
kases := []struct {
|
||||
inputs []string
|
||||
patterns []string
|
||||
skipResult bool
|
||||
filterResult bool
|
||||
}{
|
||||
{
|
||||
patterns: []string{"*"},
|
||||
inputs: []string{"path/with/slash"},
|
||||
skipResult: true,
|
||||
filterResult: false,
|
||||
},
|
||||
{
|
||||
patterns: []string{"path/a", "path/b", "path/c"},
|
||||
inputs: []string{"meta", "path/b", "otherfile"},
|
||||
skipResult: false,
|
||||
filterResult: false,
|
||||
},
|
||||
{
|
||||
patterns: []string{"path/a", "path/b", "path/c"},
|
||||
inputs: []string{"path/b"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"path/a", "path/b", "path/c"},
|
||||
inputs: []string{"path/c", "path/b"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"path/a", "path/b", "path/c"},
|
||||
inputs: []string{"path/c", "path/b", "path/a"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"path/a", "path/b", "path/c"},
|
||||
inputs: []string{"path/c", "path/b", "path/d", "path/a"},
|
||||
skipResult: false,
|
||||
filterResult: false,
|
||||
},
|
||||
{
|
||||
patterns: []string{},
|
||||
inputs: []string{},
|
||||
skipResult: false,
|
||||
filterResult: false,
|
||||
},
|
||||
{
|
||||
patterns: []string{"\\!file"},
|
||||
inputs: []string{"!file"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"escape\\\\backslash"},
|
||||
inputs: []string{"escape\\backslash"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{".yml"},
|
||||
inputs: []string{"fyml"},
|
||||
skipResult: true,
|
||||
filterResult: false,
|
||||
},
|
||||
// https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#patterns-to-match-branches-and-tags
|
||||
{
|
||||
patterns: []string{"feature/*"},
|
||||
inputs: []string{"feature/my-branch"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"feature/*"},
|
||||
inputs: []string{"feature/your-branch"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"feature/**"},
|
||||
inputs: []string{"feature/beta-a/my-branch"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"feature/**"},
|
||||
inputs: []string{"feature/beta-a/my-branch"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"feature/**"},
|
||||
inputs: []string{"feature/mona/the/octocat"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"main", "releases/mona-the-octocat"},
|
||||
inputs: []string{"main"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"main", "releases/mona-the-octocat"},
|
||||
inputs: []string{"releases/mona-the-octocat"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"*"},
|
||||
inputs: []string{"main"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"*"},
|
||||
inputs: []string{"releases"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"**"},
|
||||
inputs: []string{"all/the/branches"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"**"},
|
||||
inputs: []string{"every/tag"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"*feature"},
|
||||
inputs: []string{"mona-feature"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"*feature"},
|
||||
inputs: []string{"feature"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"*feature"},
|
||||
inputs: []string{"ver-10-feature"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"v2*"},
|
||||
inputs: []string{"v2"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"v2*"},
|
||||
inputs: []string{"v2.0"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"v2*"},
|
||||
inputs: []string{"v2.9"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"v[12].[0-9]+.[0-9]+"},
|
||||
inputs: []string{"v1.10.1"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"v[12].[0-9]+.[0-9]+"},
|
||||
inputs: []string{"v2.0.0"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
// https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#patterns-to-match-file-paths
|
||||
{
|
||||
patterns: []string{"*"},
|
||||
inputs: []string{"README.md"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"*"},
|
||||
inputs: []string{"server.rb"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"*.jsx?"},
|
||||
inputs: []string{"page.js"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"*.jsx?"},
|
||||
inputs: []string{"page.jsx"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"**"},
|
||||
inputs: []string{"all/the/files.md"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"*.js"},
|
||||
inputs: []string{"app.js"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"*.js"},
|
||||
inputs: []string{"index.js"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"**.js"},
|
||||
inputs: []string{"index.js"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"**.js"},
|
||||
inputs: []string{"js/index.js"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"**.js"},
|
||||
inputs: []string{"src/js/app.js"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"docs/*"},
|
||||
inputs: []string{"docs/README.md"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"docs/*"},
|
||||
inputs: []string{"docs/file.txt"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"docs/**"},
|
||||
inputs: []string{"docs/README.md"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"docs/**"},
|
||||
inputs: []string{"docs/mona/octocat.txt"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"docs/**/*.md"},
|
||||
inputs: []string{"docs/README.md"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"docs/**/*.md"},
|
||||
inputs: []string{"docs/mona/hello-world.md"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"docs/**/*.md"},
|
||||
inputs: []string{"docs/a/markdown/file.md"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"**/docs/**"},
|
||||
inputs: []string{"docs/hello.md"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"**/docs/**"},
|
||||
inputs: []string{"dir/docs/my-file.txt"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"**/docs/**"},
|
||||
inputs: []string{"space/docs/plan/space.doc"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"**/README.md"},
|
||||
inputs: []string{"README.md"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"**/README.md"},
|
||||
inputs: []string{"js/README.md"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"**/*src/**"},
|
||||
inputs: []string{"a/src/app.js"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"**/*src/**"},
|
||||
inputs: []string{"my-src/code/js/app.js"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"**/*-post.md"},
|
||||
inputs: []string{"my-post.md"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"**/*-post.md"},
|
||||
inputs: []string{"path/their-post.md"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"**/migrate-*.sql"},
|
||||
inputs: []string{"migrate-10909.sql"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"**/migrate-*.sql"},
|
||||
inputs: []string{"db/migrate-v1.0.sql"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"**/migrate-*.sql"},
|
||||
inputs: []string{"db/sept/migrate-v1.sql"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"*.md", "!README.md"},
|
||||
inputs: []string{"hello.md"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"*.md", "!README.md"},
|
||||
inputs: []string{"README.md"},
|
||||
skipResult: true,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"*.md", "!README.md"},
|
||||
inputs: []string{"docs/hello.md"},
|
||||
skipResult: true,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"*.md", "!README.md", "README*"},
|
||||
inputs: []string{"hello.md"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"*.md", "!README.md", "README*"},
|
||||
inputs: []string{"README.md"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
{
|
||||
patterns: []string{"*.md", "!README.md", "README*"},
|
||||
inputs: []string{"README.doc"},
|
||||
skipResult: false,
|
||||
filterResult: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, kase := range kases {
|
||||
t.Run(strings.Join(kase.patterns, ","), func(t *testing.T) {
|
||||
patterns, err := CompilePatterns(kase.patterns...)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
assert.EqualValues(t, kase.skipResult, Skip(patterns, kase.inputs, &StdOutTraceWriter{}), "skipResult") //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.EqualValues(t, kase.filterResult, Filter(patterns, kase.inputs, &StdOutTraceWriter{}), "filterResult") //nolint:testifylint // pre-existing issue from nektos/act
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -363,7 +363,6 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
||||
EventJSON: string(eventJSON),
|
||||
ContainerNamePrefix: fmt.Sprintf("GITEA-ACTIONS-TASK-%d", task.Id),
|
||||
ContainerMaxLifetime: maxLifetime,
|
||||
CleanWorkdir: true,
|
||||
ContainerNetworkMode: container.NetworkMode(r.cfg.Container.Network),
|
||||
ContainerOptions: r.cfg.Container.Options,
|
||||
ContainerDaemonSocket: r.cfg.Container.DockerHost,
|
||||
|
||||
Reference in New Issue
Block a user