mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-06-22 17:54:22 +02:00
Compare commits
25 Commits
00b7fec80f
...
lunny/remo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5d0457615 | ||
|
|
273f6b4247 | ||
|
|
47ee45412a | ||
|
|
38b69bb214 | ||
|
|
1c62c0635f | ||
|
|
0e0c54b272 | ||
|
|
d6fbe75721 | ||
|
|
b30204aa94 | ||
|
|
7b5ebe9618 | ||
|
|
4317662a38 | ||
|
|
2208e7ec63 | ||
|
|
3815aad750 | ||
|
|
fab9714f9a | ||
|
|
10475db58a | ||
|
|
9e738c203c | ||
|
|
6023928876 | ||
|
|
014ce438c1 | ||
|
|
cf7e29c10d | ||
|
|
8a99506fed | ||
|
|
5873b8b054 | ||
|
|
5464d33eef | ||
|
|
3c5f03ff8f | ||
|
|
880e9755d9 | ||
|
|
8d7cf48a6f | ||
|
|
f23605c614 |
27
.gitea/workflows/pull-pr-title.yml
Normal file
27
.gitea/workflows/pull-pr-title.yml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
name: pr-title
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- edited
|
||||||
|
- reopened
|
||||||
|
- synchronize
|
||||||
|
- ready_for_review
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint-pr-title:
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 5
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
- run: make lint-pr-title
|
||||||
|
env:
|
||||||
|
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
- name: goreleaser
|
- name: goreleaser
|
||||||
uses: goreleaser/goreleaser-action@v6
|
uses: goreleaser/goreleaser-action@v7
|
||||||
with:
|
with:
|
||||||
distribution: goreleaser-pro
|
distribution: goreleaser-pro
|
||||||
args: release --nightly
|
args: release --nightly
|
||||||
@@ -57,13 +57,13 @@ jobs:
|
|||||||
fetch-depth: 0 # all history for all branches and tags
|
fetch-depth: 0 # all history for all branches and tags
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v4
|
||||||
|
|
||||||
- name: Set up Docker BuildX
|
- name: Set up Docker BuildX
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
@@ -71,8 +71,13 @@ jobs:
|
|||||||
- name: Echo the tag
|
- name: Echo the tag
|
||||||
run: echo "${{ env.DOCKER_ORG }}/runner:nightly${{ matrix.variant.tag_suffix }}"
|
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
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
@@ -83,3 +88,5 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
${{ env.DOCKER_ORG }}/runner:nightly${{ matrix.variant.tag_suffix }}
|
${{ env.DOCKER_ORG }}/runner:nightly${{ matrix.variant.tag_suffix }}
|
||||||
|
build-args: |
|
||||||
|
VERSION=${{ steps.meta.outputs.REPO_VERSION }}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
passphrase: ${{ secrets.PASSPHRASE }}
|
passphrase: ${{ secrets.PASSPHRASE }}
|
||||||
fingerprint: CC64B1DB67ABBEECAB24B6455FC346329753F4B0
|
fingerprint: CC64B1DB67ABBEECAB24B6455FC346329753F4B0
|
||||||
- name: goreleaser
|
- name: goreleaser
|
||||||
uses: goreleaser/goreleaser-action@v6
|
uses: goreleaser/goreleaser-action@v7
|
||||||
with:
|
with:
|
||||||
distribution: goreleaser-pro
|
distribution: goreleaser-pro
|
||||||
args: release
|
args: release
|
||||||
@@ -60,20 +60,20 @@ jobs:
|
|||||||
fetch-depth: 0 # all history for all branches and tags
|
fetch-depth: 0 # all history for all branches and tags
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v4
|
||||||
|
|
||||||
- name: Set up Docker BuildX
|
- name: Set up Docker BuildX
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
- name: "Docker meta"
|
- name: "Docker meta"
|
||||||
id: docker_meta
|
id: docker_meta
|
||||||
uses: https://github.com/docker/metadata-action@v5
|
uses: docker/metadata-action@v6
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ env.DOCKER_ORG }}/runner
|
${{ env.DOCKER_ORG }}/runner
|
||||||
@@ -86,7 +86,7 @@ jobs:
|
|||||||
suffix=${{ matrix.variant.tag_suffix }},onlatest=true
|
suffix=${{ matrix.variant.tag_suffix }},onlatest=true
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
@@ -96,3 +96,5 @@ jobs:
|
|||||||
linux/arm64
|
linux/arm64
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||||
|
build-args: |
|
||||||
|
VERSION=${{ steps.docker_meta.outputs.version }}
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,5 +1,6 @@
|
|||||||
/gitea-runner
|
/gitea-runner
|
||||||
.env
|
.env
|
||||||
|
!/act/runner/testdata/secrets/.env
|
||||||
.runner
|
.runner
|
||||||
coverage.txt
|
coverage.txt
|
||||||
/config.yaml
|
/config.yaml
|
||||||
@@ -10,4 +11,4 @@ coverage.txt
|
|||||||
.vscode
|
.vscode
|
||||||
__debug_bin
|
__debug_bin
|
||||||
# gorelease binary folder
|
# gorelease binary folder
|
||||||
dist
|
/dist
|
||||||
|
|||||||
24
Dockerfile
24
Dockerfile
@@ -1,7 +1,7 @@
|
|||||||
### BUILDER STAGE
|
### BUILDER STAGE
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
FROM golang:1.26-alpine AS builder
|
FROM golang:1.26-alpine3.23 AS builder
|
||||||
|
|
||||||
# Do not remove `git` here, it is required for getting runner version when executing `make build`
|
# Do not remove `git` here, it is required for getting runner version when executing `make build`
|
||||||
RUN apk add --no-cache make git
|
RUN apk add --no-cache make git
|
||||||
@@ -17,7 +17,12 @@ RUN make clean && make build
|
|||||||
### DIND VARIANT
|
### DIND VARIANT
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
FROM docker:29-dind AS dind
|
FROM docker:29.5.2-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
|
RUN apk add --no-cache s6 bash git tzdata
|
||||||
|
|
||||||
@@ -32,7 +37,12 @@ ENTRYPOINT ["s6-svscan","/etc/s6"]
|
|||||||
### DIND-ROOTLESS VARIANT
|
### DIND-ROOTLESS VARIANT
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
FROM docker:29-dind-rootless AS dind-rootless
|
FROM docker:29.5.2-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
|
USER root
|
||||||
RUN apk add --no-cache s6 bash git tzdata
|
RUN apk add --no-cache s6 bash git tzdata
|
||||||
@@ -53,7 +63,13 @@ ENTRYPOINT ["s6-svscan","/etc/s6"]
|
|||||||
### BASIC VARIANT
|
### BASIC VARIANT
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
FROM alpine AS basic
|
FROM alpine:3.23 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
|
RUN apk add --no-cache tini bash git tzdata
|
||||||
|
|
||||||
COPY --from=builder /opt/src/runner/gitea-runner /usr/local/bin/gitea-runner
|
COPY --from=builder /opt/src/runner/gitea-runner /usr/local/bin/gitea-runner
|
||||||
|
|||||||
4
Makefile
4
Makefile
@@ -118,6 +118,10 @@ lint-go: ## lint go files
|
|||||||
lint-go-fix: ## lint go files and fix issues
|
lint-go-fix: ## lint go files and fix issues
|
||||||
$(GO) run $(GOLANGCI_LINT_PACKAGE) run --fix
|
$(GO) run $(GOLANGCI_LINT_PACKAGE) run --fix
|
||||||
|
|
||||||
|
.PHONY: lint-pr-title
|
||||||
|
lint-pr-title: ## lint PR title against Conventional Commits (set PR_TITLE=...)
|
||||||
|
@node ./tools/lint-pr-title.ts
|
||||||
|
|
||||||
.PHONY: security-check
|
.PHONY: security-check
|
||||||
security-check: deps-tools
|
security-check: deps-tools
|
||||||
GOEXPERIMENT= $(GO) run $(GOVULNCHECK_PACKAGE) -show color ./... || true
|
GOEXPERIMENT= $(GO) run $(GOVULNCHECK_PACKAGE) -show color ./... || true
|
||||||
|
|||||||
@@ -431,6 +431,7 @@ func (h *Handler) upload(w http.ResponseWriter, r *http.Request, params httprout
|
|||||||
}
|
}
|
||||||
if err := h.storage.Write(cache.ID, start, r.Body); err != nil {
|
if err := h.storage.Write(cache.ID, start, r.Body); err != nil {
|
||||||
h.responseJSON(w, r, 500, err)
|
h.responseJSON(w, r, 500, err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
h.useCache(id)
|
h.useCache(id)
|
||||||
h.responseJSON(w, r, 200)
|
h.responseJSON(w, r, 200)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -338,6 +339,54 @@ 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) {
|
t.Run("commit early", func(t *testing.T) {
|
||||||
key := strings.ToLower(t.Name())
|
key := strings.ToLower(t.Name())
|
||||||
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
|
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
|
||||||
|
|||||||
@@ -1,146 +0,0 @@
|
|||||||
// 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,24 +12,6 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
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
|
// Executor define contract for the steps of a workflow
|
||||||
type Executor func(ctx context.Context) error
|
type Executor func(ctx context.Context) error
|
||||||
|
|
||||||
@@ -97,6 +79,12 @@ func NewErrorExecutor(err error) Executor {
|
|||||||
|
|
||||||
// NewParallelExecutor creates a new executor from a parallel of other executors
|
// NewParallelExecutor creates a new executor from a parallel of other executors
|
||||||
func NewParallelExecutor(parallel int, executors ...Executor) Executor {
|
func NewParallelExecutor(parallel int, executors ...Executor) Executor {
|
||||||
|
if len(executors) == 0 {
|
||||||
|
return func(ctx context.Context) error {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
work := make(chan Executor, len(executors))
|
work := make(chan Executor, len(executors))
|
||||||
errs := make(chan error, len(executors))
|
errs := make(chan error, len(executors))
|
||||||
@@ -156,14 +144,8 @@ func NewParallelExecutor(parallel int, executors ...Executor) Executor {
|
|||||||
// Then runs another executor if this executor succeeds
|
// Then runs another executor if this executor succeeds
|
||||||
func (e Executor) Then(then Executor) Executor {
|
func (e Executor) Then(then Executor) Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
err := e(ctx)
|
if err := e(ctx); err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
switch err.(type) {
|
|
||||||
case Warning:
|
|
||||||
Logger(ctx).Warning(err.Error())
|
|
||||||
default:
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewWorkflow(t *testing.T) {
|
func TestNewWorkflow(t *testing.T) {
|
||||||
@@ -119,6 +120,19 @@ func TestNewParallelExecutor(t *testing.T) {
|
|||||||
assert.NoError(errSingle)
|
assert.NoError(errSingle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNewParallelExecutorEmpty(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
require.NoError(t, NewParallelExecutor(2)(ctx))
|
||||||
|
|
||||||
|
canceledCtx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
err := NewParallelExecutor(2)(canceledCtx)
|
||||||
|
assert.ErrorIs(err, context.Canceled)
|
||||||
|
}
|
||||||
|
|
||||||
func TestNewParallelExecutorFailed(t *testing.T) {
|
func TestNewParallelExecutorFailed(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
@@ -243,47 +243,50 @@ type NewGitCloneExecutorInput struct {
|
|||||||
InsecureSkipTLS bool
|
InsecureSkipTLS bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// CloneIfRequired ...
|
// CloneIfRequired returns the repository and a boolean indicating whether an existing local clone was reused.
|
||||||
func CloneIfRequired(ctx context.Context, refName plumbing.ReferenceName, input NewGitCloneExecutorInput, logger log.FieldLogger) (*git.Repository, error) {
|
func CloneIfRequired(ctx context.Context, refName plumbing.ReferenceName, input NewGitCloneExecutorInput, logger log.FieldLogger) (*git.Repository, bool, error) {
|
||||||
r, err := git.PlainOpen(input.Dir)
|
r, err := git.PlainOpen(input.Dir)
|
||||||
if err != nil {
|
if err == nil {
|
||||||
var progressWriter io.Writer
|
// Reuse existing clone
|
||||||
if isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) {
|
return r, true, nil
|
||||||
if entry, ok := logger.(*log.Entry); ok {
|
}
|
||||||
progressWriter = entry.WriterLevel(log.DebugLevel)
|
|
||||||
} else if lgr, ok := logger.(*log.Logger); ok {
|
|
||||||
progressWriter = lgr.WriterLevel(log.DebugLevel)
|
|
||||||
} else {
|
|
||||||
log.Errorf("Unable to get writer from logger (type=%T)", logger)
|
|
||||||
progressWriter = os.Stdout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cloneOptions := git.CloneOptions{
|
var progressWriter io.Writer
|
||||||
URL: input.URL,
|
if isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) {
|
||||||
Progress: progressWriter,
|
if entry, ok := logger.(*log.Entry); ok {
|
||||||
|
progressWriter = entry.WriterLevel(log.DebugLevel)
|
||||||
InsecureSkipTLS: input.InsecureSkipTLS, // For Gitea
|
} else if lgr, ok := logger.(*log.Logger); ok {
|
||||||
}
|
progressWriter = lgr.WriterLevel(log.DebugLevel)
|
||||||
if input.Token != "" {
|
} else {
|
||||||
cloneOptions.Auth = &http.BasicAuth{
|
log.Errorf("Unable to get writer from logger (type=%T)", logger)
|
||||||
Username: "token",
|
progressWriter = os.Stdout
|
||||||
Password: input.Token,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
r, err = git.PlainCloneContext(ctx, input.Dir, false, &cloneOptions)
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorf("Unable to clone %v %s: %v", input.URL, refName, err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = os.Chmod(input.Dir, 0o755); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return r, nil
|
cloneOptions := git.CloneOptions{
|
||||||
|
URL: input.URL,
|
||||||
|
Progress: progressWriter,
|
||||||
|
|
||||||
|
InsecureSkipTLS: input.InsecureSkipTLS, // For Gitea
|
||||||
|
}
|
||||||
|
if input.Token != "" {
|
||||||
|
cloneOptions.Auth = &http.BasicAuth{
|
||||||
|
Username: "token",
|
||||||
|
Password: input.Token,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err = git.PlainCloneContext(ctx, input.Dir, false, &cloneOptions)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Unable to clone %v %s: %v", input.URL, refName, err)
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = os.Chmod(input.Dir, 0o755); err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return r, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func gitOptions(token string) (fetchOptions git.FetchOptions, pullOptions git.PullOptions) {
|
func gitOptions(token string) (fetchOptions git.FetchOptions, pullOptions git.PullOptions) {
|
||||||
@@ -313,7 +316,7 @@ func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor {
|
|||||||
defer AcquireCloneLock(input.Dir)()
|
defer AcquireCloneLock(input.Dir)()
|
||||||
|
|
||||||
refName := plumbing.ReferenceName("refs/heads/" + input.Ref)
|
refName := plumbing.ReferenceName("refs/heads/" + input.Ref)
|
||||||
r, err := CloneIfRequired(ctx, refName, input, logger)
|
r, reused, err := CloneIfRequired(ctx, refName, input, logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -338,10 +341,10 @@ func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor {
|
|||||||
var hash *plumbing.Hash
|
var hash *plumbing.Hash
|
||||||
rev := plumbing.Revision(input.Ref)
|
rev := plumbing.Revision(input.Ref)
|
||||||
if hash, err = r.ResolveRevision(rev); err != nil {
|
if hash, err = r.ResolveRevision(rev); err != nil {
|
||||||
|
// ResolveRevision returns a nil hash on error, and a branch ref legitimately fails
|
||||||
|
// here (no local refs/heads/<ref>); the duck-typing below resolves it.
|
||||||
logger.Errorf("Unable to resolve %s: %v", input.Ref, err)
|
logger.Errorf("Unable to resolve %s: %v", input.Ref, err)
|
||||||
}
|
} else if hash.String() != input.Ref && strings.HasPrefix(hash.String(), input.Ref) {
|
||||||
|
|
||||||
if hash.String() != input.Ref && strings.HasPrefix(hash.String(), input.Ref) {
|
|
||||||
return &Error{
|
return &Error{
|
||||||
err: ErrShortRef,
|
err: ErrShortRef,
|
||||||
commit: hash.String(),
|
commit: hash.String(),
|
||||||
@@ -392,12 +395,18 @@ func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reusedMsg := ""
|
||||||
|
|
||||||
if !isOfflineMode {
|
if !isOfflineMode {
|
||||||
if err = w.Pull(&pullOptions); err != nil && err != git.NoErrAlreadyUpToDate {
|
if err = w.Pull(&pullOptions); err != nil && err != git.NoErrAlreadyUpToDate {
|
||||||
logger.Debugf("Unable to pull %s: %v", refName, err)
|
logger.Debugf("Unable to pull %s: %v", refName, err)
|
||||||
}
|
}
|
||||||
|
} else if reused {
|
||||||
|
reusedMsg = " (reused in offline mode)"
|
||||||
}
|
}
|
||||||
logger.Debugf("Cloned %s to %s", input.URL, input.Dir)
|
|
||||||
|
logger.Debugf("Cloned %s to %s%s", input.URL, input.Dir, reusedMsg)
|
||||||
|
|
||||||
if hash.String() != input.Ref && refType == "branch" {
|
if hash.String() != input.Ref && refType == "branch" {
|
||||||
logger.Debugf("Provided ref is not a sha. Updating branch ref after pull")
|
logger.Debugf("Provided ref is not a sha. Updating branch ref after pull")
|
||||||
|
|||||||
@@ -279,6 +279,54 @@ func TestGitCloneExecutorNonFastForwardRef(t *testing.T) {
|
|||||||
assert.Equal(t, "second", strings.TrimSpace(string(out)), "working tree should be at the latest commit")
|
assert.Equal(t, "second", strings.TrimSpace(string(out)), "working tree should be at the latest commit")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGitCloneExecutorOfflineMode(t *testing.T) {
|
||||||
|
gitConfig()
|
||||||
|
|
||||||
|
// Build a local "remote" with a single commit on main.
|
||||||
|
remoteDir := t.TempDir()
|
||||||
|
require.NoError(t, gitCmd("init", "--bare", "--initial-branch=main", remoteDir))
|
||||||
|
workDir := t.TempDir()
|
||||||
|
require.NoError(t, gitCmd("clone", remoteDir, workDir))
|
||||||
|
require.NoError(t, gitCmd("-C", workDir, "checkout", "-b", "main"))
|
||||||
|
require.NoError(t, gitCmd("-C", workDir, "commit", "--allow-empty", "-m", "initial"))
|
||||||
|
require.NoError(t, gitCmd("-C", workDir, "push", "-u", "origin", "main"))
|
||||||
|
|
||||||
|
// Prime the cache with an online clone of main.
|
||||||
|
cacheDir := t.TempDir()
|
||||||
|
require.NoError(t, NewGitCloneExecutor(NewGitCloneExecutorInput{
|
||||||
|
URL: remoteDir,
|
||||||
|
Ref: "main",
|
||||||
|
Dir: cacheDir,
|
||||||
|
})(context.Background()))
|
||||||
|
|
||||||
|
t.Run("cached branch resolves without fetching", func(t *testing.T) {
|
||||||
|
// Offline reuse of a cached branch must succeed even though ResolveRevision(input.Ref)
|
||||||
|
// finds no local refs/heads/<ref>.
|
||||||
|
err := NewGitCloneExecutor(NewGitCloneExecutorInput{
|
||||||
|
URL: remoteDir,
|
||||||
|
Ref: "main",
|
||||||
|
Dir: cacheDir,
|
||||||
|
OfflineMode: true,
|
||||||
|
})(context.Background())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
out, err := exec.Command("git", "-C", cacheDir, "log", "--oneline", "-1", "--format=%s").Output()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "initial", strings.TrimSpace(string(out)))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("unresolvable cached ref returns error", func(t *testing.T) {
|
||||||
|
// The ref was never cached; offline mode cannot resolve it and must return an error.
|
||||||
|
err := NewGitCloneExecutor(NewGitCloneExecutorInput{
|
||||||
|
URL: remoteDir,
|
||||||
|
Ref: "never-fetched",
|
||||||
|
Dir: cacheDir,
|
||||||
|
OfflineMode: true,
|
||||||
|
})(context.Background())
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func gitConfig() {
|
func gitConfig() {
|
||||||
if os.Getenv("GITHUB_ACTIONS") == "true" {
|
if os.Getenv("GITHUB_ACTIONS") == "true" {
|
||||||
var err error
|
var err error
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ type NewContainerInput struct {
|
|||||||
// Gitea specific
|
// Gitea specific
|
||||||
AutoRemove bool
|
AutoRemove bool
|
||||||
ValidVolumes []string
|
ValidVolumes []string
|
||||||
|
AllocatePTY bool // allocate a pseudo-TTY for the container's exec processes
|
||||||
}
|
}
|
||||||
|
|
||||||
// FileEntry is a file to copy to a container
|
// FileEntry is a file to copy to a container
|
||||||
|
|||||||
@@ -8,12 +8,24 @@ package container
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
|
||||||
"github.com/moby/moby/client"
|
"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 {
|
func NewDockerNetworkCreateExecutor(name string) common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
cli, err := GetDockerClient(ctx)
|
cli, err := GetDockerClient(ctx)
|
||||||
@@ -56,31 +68,64 @@ func NewDockerNetworkRemoveExecutor(name string) common.Executor {
|
|||||||
}
|
}
|
||||||
defer cli.Close()
|
defer cli.Close()
|
||||||
|
|
||||||
// Make sure that all network of the specified name are removed
|
return removeDockerNetworks(ctx, cli, name)
|
||||||
// cli.NetworkRemove refuses to remove a network if there are duplicates
|
}
|
||||||
networks, err := cli.NetworkList(ctx, client.NetworkListOptions{})
|
}
|
||||||
|
|
||||||
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// For Gitea, reduce log noise
|
if !pendingRemoval {
|
||||||
// common.Logger(ctx).Debugf("%v", networks)
|
return nil
|
||||||
for _, n := range networks.Items {
|
|
||||||
if n.Name == name {
|
|
||||||
result, err := cli.NetworkInspect(ctx, n.ID, client.NetworkInspectOptions{})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
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 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := cli.NetworkInspect(ctx, n.ID, client.NetworkInspectOptions{})
|
||||||
|
if err != nil {
|
||||||
|
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 _, err = cli.NetworkRemove(ctx, n.ID, client.NetworkRemoveOptions{}); err != nil {
|
||||||
|
pendingRemoval = true
|
||||||
|
common.Logger(ctx).Debugf("Retrying Docker network removal for %v: %v", name, 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,6 +17,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -41,7 +42,6 @@ import (
|
|||||||
"github.com/moby/moby/client"
|
"github.com/moby/moby/client"
|
||||||
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
"golang.org/x/term"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewContainer creates a reference to a container
|
// NewContainer creates a reference to a container
|
||||||
@@ -450,7 +450,6 @@ func (cr *containerReference) create(capAdd, capDrop []string) common.Executor {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
isTerminal := term.IsTerminal(int(os.Stdout.Fd()))
|
|
||||||
input := cr.input
|
input := cr.input
|
||||||
exposedPorts, err := convertPortSet(input.ExposedPorts)
|
exposedPorts, err := convertPortSet(input.ExposedPorts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -466,7 +465,7 @@ func (cr *containerReference) create(capAdd, capDrop []string) common.Executor {
|
|||||||
WorkingDir: input.WorkingDir,
|
WorkingDir: input.WorkingDir,
|
||||||
Env: input.Env,
|
Env: input.Env,
|
||||||
ExposedPorts: exposedPorts,
|
ExposedPorts: exposedPorts,
|
||||||
Tty: isTerminal,
|
Tty: input.AllocatePTY,
|
||||||
}
|
}
|
||||||
// For Gitea, reduce log noise
|
// For Gitea, reduce log noise
|
||||||
// logger.Debugf("Common container.Config ==> %+v", config)
|
// logger.Debugf("Common container.Config ==> %+v", config)
|
||||||
@@ -604,7 +603,7 @@ func (cr *containerReference) exec(cmd []string, env map[string]string, user, wo
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.Debugf("Exec command '%s'", cmd)
|
logger.Debugf("Exec command '%s'", cmd)
|
||||||
isTerminal := term.IsTerminal(int(os.Stdout.Fd()))
|
isTerminal := cr.input.AllocatePTY
|
||||||
envList := make([]string, 0)
|
envList := make([]string, 0)
|
||||||
for k, v := range env {
|
for k, v := range env {
|
||||||
envList = append(envList, fmt.Sprintf("%s=%s", k, v))
|
envList = append(envList, fmt.Sprintf("%s=%s", k, v))
|
||||||
@@ -899,7 +898,7 @@ func (cr *containerReference) attach() common.Executor {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to attach to container: %w", err)
|
return fmt.Errorf("failed to attach to container: %w", err)
|
||||||
}
|
}
|
||||||
isTerminal := term.IsTerminal(int(os.Stdout.Fd()))
|
isTerminal := cr.input.AllocatePTY
|
||||||
|
|
||||||
var outWriter io.Writer
|
var outWriter io.Writer
|
||||||
outWriter = cr.input.Stdout
|
outWriter = cr.input.Stdout
|
||||||
@@ -970,22 +969,7 @@ func (cr *containerReference) sanitizeConfig(ctx context.Context, config *contai
|
|||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
|
|
||||||
if len(cr.input.ValidVolumes) > 0 {
|
if len(cr.input.ValidVolumes) > 0 {
|
||||||
globs := make([]glob.Glob, 0, len(cr.input.ValidVolumes))
|
matcher := newValidVolumeMatcher(ctx, 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
|
// sanitize binds
|
||||||
sanitizedBinds := make([]string, 0, len(hostConfig.Binds))
|
sanitizedBinds := make([]string, 0, len(hostConfig.Binds))
|
||||||
for _, bind := range hostConfig.Binds {
|
for _, bind := range hostConfig.Binds {
|
||||||
@@ -999,7 +983,7 @@ func (cr *containerReference) sanitizeConfig(ctx context.Context, config *contai
|
|||||||
sanitizedBinds = append(sanitizedBinds, bind)
|
sanitizedBinds = append(sanitizedBinds, bind)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if isValid(parsed.Source) {
|
if matcher.isValid(parsed.Source, mount.Type(parsed.Type)) {
|
||||||
sanitizedBinds = append(sanitizedBinds, bind)
|
sanitizedBinds = append(sanitizedBinds, bind)
|
||||||
} else {
|
} else {
|
||||||
logger.Warnf("[%s] is not a valid volume, will be ignored", parsed.Source)
|
logger.Warnf("[%s] is not a valid volume, will be ignored", parsed.Source)
|
||||||
@@ -1009,7 +993,7 @@ func (cr *containerReference) sanitizeConfig(ctx context.Context, config *contai
|
|||||||
// sanitize mounts
|
// sanitize mounts
|
||||||
sanitizedMounts := make([]mount.Mount, 0, len(hostConfig.Mounts))
|
sanitizedMounts := make([]mount.Mount, 0, len(hostConfig.Mounts))
|
||||||
for _, mt := range hostConfig.Mounts {
|
for _, mt := range hostConfig.Mounts {
|
||||||
if isValid(mt.Source) {
|
if matcher.isValid(mt.Source, mt.Type) {
|
||||||
sanitizedMounts = append(sanitizedMounts, mt)
|
sanitizedMounts = append(sanitizedMounts, mt)
|
||||||
} else {
|
} else {
|
||||||
logger.Warnf("[%s] is not a valid volume, will be ignored", mt.Source)
|
logger.Warnf("[%s] is not a valid volume, will be ignored", mt.Source)
|
||||||
@@ -1023,3 +1007,129 @@ func (cr *containerReference) sanitizeConfig(ctx context.Context, config *contai
|
|||||||
|
|
||||||
return config, hostConfig
|
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,6 +11,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -375,3 +377,40 @@ 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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
@@ -36,12 +37,13 @@ type HostEnvironment struct {
|
|||||||
TmpDir string
|
TmpDir string
|
||||||
ToolCache string
|
ToolCache string
|
||||||
Workdir string
|
Workdir string
|
||||||
// BindWorkdir is true when the app runner mounts the workspace on the host and
|
// CleanWorkdir means teardown owns Workdir and may delete it. Leave false
|
||||||
// deletes the task directory after the job; host teardown must not remove Workdir.
|
// when Workdir points at a caller-owned checkout (e.g. `act` local mode).
|
||||||
BindWorkdir bool
|
CleanWorkdir bool
|
||||||
ActPath string
|
ActPath string
|
||||||
CleanUp func()
|
CleanUp func()
|
||||||
StdOut io.Writer
|
StdOut io.Writer
|
||||||
|
AllocatePTY bool // allocate a pseudo-TTY for each step's process
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
runningPIDs map[int]struct{}
|
runningPIDs map[int]struct{}
|
||||||
@@ -200,12 +202,12 @@ func (e *HostEnvironment) Start(_ bool) common.Executor {
|
|||||||
|
|
||||||
type ptyWriter struct {
|
type ptyWriter struct {
|
||||||
Out io.Writer
|
Out io.Writer
|
||||||
AutoStop bool
|
AutoStop atomic.Bool
|
||||||
dirtyLine bool
|
dirtyLine bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *ptyWriter) Write(buf []byte) (int, error) {
|
func (w *ptyWriter) Write(buf []byte) (int, error) {
|
||||||
if w.AutoStop && len(buf) > 0 && buf[len(buf)-1] == 4 {
|
if w.AutoStop.Load() && len(buf) > 0 && buf[len(buf)-1] == 4 {
|
||||||
n, err := w.Out.Write(buf[:len(buf)-1])
|
n, err := w.Out.Write(buf[:len(buf)-1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return n, err
|
return n, err
|
||||||
@@ -335,21 +337,20 @@ func (e *HostEnvironment) exec(ctx context.Context, command []string, cmdline st
|
|||||||
tty.Close()
|
tty.Close()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
if true /* allocate Terminal */ {
|
if e.AllocatePTY {
|
||||||
var err error
|
var err error
|
||||||
ppty, tty, err = setupPty(cmd, cmdline)
|
ppty, tty, err = setupPty(cmd, cmdline)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.Logger(ctx).Debugf("Failed to setup Pty %v\n", err.Error())
|
common.Logger(ctx).Debugf("Failed to setup Pty %v\n", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
writer := &ptyWriter{Out: e.StdOut}
|
var writer *ptyWriter
|
||||||
logctx, finishLog := context.WithCancel(context.Background())
|
var logctx context.Context
|
||||||
if ppty != nil {
|
if ppty != nil {
|
||||||
|
writer = &ptyWriter{Out: e.StdOut}
|
||||||
|
var finishLog context.CancelFunc
|
||||||
|
logctx, finishLog = context.WithCancel(context.Background())
|
||||||
go copyPtyOutput(writer, ppty, finishLog)
|
go copyPtyOutput(writer, ppty, finishLog)
|
||||||
} else {
|
|
||||||
finishLog()
|
|
||||||
}
|
|
||||||
if ppty != nil {
|
|
||||||
go writeKeepAlive(ppty)
|
go writeKeepAlive(ppty)
|
||||||
}
|
}
|
||||||
// Split Start/Wait so the PID can be registered before the process can exit;
|
// Split Start/Wait so the PID can be registered before the process can exit;
|
||||||
@@ -379,14 +380,11 @@ func (e *HostEnvironment) exec(ctx context.Context, command []string, cmdline st
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if tty != nil {
|
if tty != nil {
|
||||||
writer.AutoStop = true
|
writer.AutoStop.Store(true)
|
||||||
if _, err := tty.WriteString("\x04"); err != nil {
|
if _, err := tty.WriteString("\x04"); err != nil {
|
||||||
common.Logger(ctx).Debug("Failed to write EOT")
|
common.Logger(ctx).Debug("Failed to write EOT")
|
||||||
}
|
}
|
||||||
}
|
<-logctx.Done()
|
||||||
<-logctx.Done()
|
|
||||||
|
|
||||||
if ppty != nil {
|
|
||||||
ppty.Close()
|
ppty.Close()
|
||||||
ppty = nil
|
ppty = nil
|
||||||
}
|
}
|
||||||
@@ -485,7 +483,7 @@ func (e *HostEnvironment) Remove() common.Executor {
|
|||||||
logger.Warnf("failed to remove host misc state %s: %v", e.Path, err)
|
logger.Warnf("failed to remove host misc state %s: %v", e.Path, err)
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
}
|
}
|
||||||
if !e.BindWorkdir && e.Workdir != "" {
|
if e.CleanWorkdir {
|
||||||
if err := removePathWithRetry(ctx, e.Workdir); err != nil {
|
if err := removePathWithRetry(ctx, e.Workdir); err != nil {
|
||||||
logger.Warnf("failed to remove host workspace %s: %v", e.Workdir, err)
|
logger.Warnf("failed to remove host workspace %s: %v", e.Workdir, err)
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ package container
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/tar"
|
"archive/tar"
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
@@ -100,7 +102,46 @@ func TestHostEnvironmentExecExitCode(t *testing.T) {
|
|||||||
assert.Equal(t, "Process completed with exit code 3.", err.Error())
|
assert.Equal(t, "Process completed with exit code 3.", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHostEnvironmentRemoveCleansWorkdir(t *testing.T) {
|
func TestHostEnvironmentAllocatePTY(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("uses POSIX shell")
|
||||||
|
}
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
allocPTY bool
|
||||||
|
expect string
|
||||||
|
}{
|
||||||
|
{name: "off", allocPTY: false, expect: "NOTTY"},
|
||||||
|
{name: "on", allocPTY: true, expect: "TTY"},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
e := &HostEnvironment{
|
||||||
|
Path: filepath.Join(dir, "path"),
|
||||||
|
TmpDir: filepath.Join(dir, "tmp"),
|
||||||
|
ToolCache: filepath.Join(dir, "tool_cache"),
|
||||||
|
ActPath: filepath.Join(dir, "act_path"),
|
||||||
|
StdOut: buf,
|
||||||
|
Workdir: filepath.Join(dir, "path"),
|
||||||
|
AllocatePTY: tc.allocPTY,
|
||||||
|
}
|
||||||
|
for _, p := range []string{e.Path, e.TmpDir, e.ToolCache, e.ActPath} {
|
||||||
|
require.NoError(t, os.MkdirAll(p, 0o700))
|
||||||
|
}
|
||||||
|
|
||||||
|
err := e.Exec(
|
||||||
|
[]string{"sh", "-c", "[ -t 1 ] && printf TTY || printf NOTTY"},
|
||||||
|
map[string]string{"PATH": os.Getenv("PATH")}, "", "",
|
||||||
|
)(context.Background())
|
||||||
|
require.NoError(t, err)
|
||||||
|
got := strings.TrimSpace(strings.ReplaceAll(buf.String(), "\r", ""))
|
||||||
|
assert.Equal(t, tc.expect, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHostEnvironmentRemovePreservesWorkdirByDefault(t *testing.T) {
|
||||||
logger := logrus.New()
|
logger := logrus.New()
|
||||||
ctx := common.WithLogger(context.Background(), logrus.NewEntry(logger))
|
ctx := common.WithLogger(context.Background(), logrus.NewEntry(logger))
|
||||||
base := t.TempDir()
|
base := t.TempDir()
|
||||||
@@ -111,9 +152,8 @@ func TestHostEnvironmentRemoveCleansWorkdir(t *testing.T) {
|
|||||||
require.NoError(t, os.MkdirAll(workdir, 0o700))
|
require.NoError(t, os.MkdirAll(workdir, 0o700))
|
||||||
|
|
||||||
e := &HostEnvironment{
|
e := &HostEnvironment{
|
||||||
Path: path,
|
Path: path,
|
||||||
Workdir: workdir,
|
Workdir: workdir,
|
||||||
BindWorkdir: false,
|
|
||||||
CleanUp: func() {
|
CleanUp: func() {
|
||||||
_ = os.RemoveAll(miscRoot)
|
_ = os.RemoveAll(miscRoot)
|
||||||
},
|
},
|
||||||
@@ -121,10 +161,10 @@ func TestHostEnvironmentRemoveCleansWorkdir(t *testing.T) {
|
|||||||
}
|
}
|
||||||
require.NoError(t, e.Remove()(ctx))
|
require.NoError(t, e.Remove()(ctx))
|
||||||
_, err := os.Stat(workdir)
|
_, err := os.Stat(workdir)
|
||||||
assert.ErrorIs(t, err, os.ErrNotExist)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHostEnvironmentRemoveSkipsWorkdirWhenBindWorkdir(t *testing.T) {
|
func TestHostEnvironmentRemoveCleansWorkdirWhenOwned(t *testing.T) {
|
||||||
logger := logrus.New()
|
logger := logrus.New()
|
||||||
ctx := common.WithLogger(context.Background(), logrus.NewEntry(logger))
|
ctx := common.WithLogger(context.Background(), logrus.NewEntry(logger))
|
||||||
base := t.TempDir()
|
base := t.TempDir()
|
||||||
@@ -135,9 +175,9 @@ func TestHostEnvironmentRemoveSkipsWorkdirWhenBindWorkdir(t *testing.T) {
|
|||||||
require.NoError(t, os.MkdirAll(workdir, 0o700))
|
require.NoError(t, os.MkdirAll(workdir, 0o700))
|
||||||
|
|
||||||
e := &HostEnvironment{
|
e := &HostEnvironment{
|
||||||
Path: path,
|
Path: path,
|
||||||
Workdir: workdir,
|
Workdir: workdir,
|
||||||
BindWorkdir: true,
|
CleanWorkdir: true,
|
||||||
CleanUp: func() {
|
CleanUp: func() {
|
||||||
_ = os.RemoveAll(miscRoot)
|
_ = os.RemoveAll(miscRoot)
|
||||||
},
|
},
|
||||||
@@ -145,5 +185,5 @@ func TestHostEnvironmentRemoveSkipsWorkdirWhenBindWorkdir(t *testing.T) {
|
|||||||
}
|
}
|
||||||
require.NoError(t, e.Remove()(ctx))
|
require.NoError(t, e.Remove()(ctx))
|
||||||
_, err := os.Stat(workdir)
|
_, err := os.Stat(workdir)
|
||||||
require.NoError(t, err)
|
assert.ErrorIs(t, err, os.ErrNotExist)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ func parseEnvFile(e Container, srcPath string, env *map[string]string) common.Ex
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s := bufio.NewScanner(reader)
|
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() {
|
for s.Scan() {
|
||||||
line := s.Text()
|
line := s.Text()
|
||||||
singleLineEnv := strings.Index(line, "=")
|
singleLineEnv := strings.Index(line, "=")
|
||||||
@@ -50,6 +52,9 @@ func parseEnvFile(e Container, srcPath string, env *map[string]string) common.Ex
|
|||||||
}
|
}
|
||||||
multiLineEnvContent += content
|
multiLineEnvContent += content
|
||||||
}
|
}
|
||||||
|
if err := s.Err(); err != nil {
|
||||||
|
return fmt.Errorf("reading env file: %w", err)
|
||||||
|
}
|
||||||
if !delimiterFound {
|
if !delimiterFound {
|
||||||
return fmt.Errorf("invalid format delimiter '%v' not found before end of file", multiLineEnvDelimiter)
|
return fmt.Errorf("invalid format delimiter '%v' not found before end of file", multiLineEnvDelimiter)
|
||||||
}
|
}
|
||||||
@@ -58,6 +63,9 @@ func parseEnvFile(e Container, srcPath string, env *map[string]string) common.Ex
|
|||||||
return fmt.Errorf("invalid format '%v', expected a line with '=' or '<<'", line)
|
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
|
env = &localEnv
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
75
act/container/parse_env_file_test.go
Normal file
75
act/container/parse_env_file_test.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
// 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,18 +4,6 @@
|
|||||||
|
|
||||||
package lookpath
|
package lookpath
|
||||||
|
|
||||||
import "os"
|
|
||||||
|
|
||||||
type Env interface {
|
type Env interface {
|
||||||
Getenv(name string) string
|
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{})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -456,6 +456,7 @@ func newStepContainer(ctx context.Context, step step, image string, cmd, entrypo
|
|||||||
Options: rc.Config.ContainerOptions,
|
Options: rc.Config.ContainerOptions,
|
||||||
AutoRemove: rc.Config.AutoRemove,
|
AutoRemove: rc.Config.AutoRemove,
|
||||||
ValidVolumes: rc.Config.ValidVolumes,
|
ValidVolumes: rc.Config.ValidVolumes,
|
||||||
|
AllocatePTY: rc.Config.AllocatePTY,
|
||||||
})
|
})
|
||||||
return stepContainer
|
return stepContainer
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
// 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)
|
steps := make([]common.Executor, 0)
|
||||||
preSteps := make([]common.Executor, 0)
|
preSteps := make([]common.Executor, 0)
|
||||||
var postExecutor common.Executor
|
var postExecutor common.Executor
|
||||||
|
var startErr error
|
||||||
|
|
||||||
steps = append(steps, func(ctx context.Context) error {
|
steps = append(steps, func(ctx context.Context) error {
|
||||||
logger := common.Logger(ctx)
|
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, preSteps...)
|
||||||
pipeline = append(pipeline, steps...)
|
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 {
|
Finally(func(ctx context.Context) error {
|
||||||
var cancel context.CancelFunc
|
var cancel context.CancelFunc
|
||||||
if ctx.Err() == context.Canceled {
|
if ctx.Err() == context.Canceled {
|
||||||
@@ -176,8 +182,23 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
|
|||||||
}
|
}
|
||||||
return postExecutor(ctx)
|
return postExecutor(ctx)
|
||||||
}).
|
}).
|
||||||
Finally(info.interpolateOutputs()).
|
Finally(info.interpolateOutputs())).
|
||||||
Finally(info.closeContainer()))
|
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) {
|
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/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestJobExecutor(t *testing.T) {
|
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,6 +308,11 @@ func getGitCloneToken(conf *Config, cloneURL string) string {
|
|||||||
// 1. cloneURL is from the same Gitea instance that the runner is registered to
|
// 1. cloneURL is from the same Gitea instance that the runner is registered to
|
||||||
// 2. the cloneURL does not have basic auth embedded
|
// 2. the cloneURL does not have basic auth embedded
|
||||||
func shouldCloneURLUseToken(instanceURL, cloneURL string) bool {
|
func shouldCloneURLUseToken(instanceURL, cloneURL string) bool {
|
||||||
|
if !strings.HasPrefix(instanceURL, "http://") &&
|
||||||
|
!strings.HasPrefix(instanceURL, "https://") {
|
||||||
|
instanceURL = "https://" + instanceURL
|
||||||
|
}
|
||||||
|
|
||||||
u1, err1 := url.Parse(instanceURL)
|
u1, err1 := url.Parse(instanceURL)
|
||||||
u2, err2 := url.Parse(cloneURL)
|
u2, err2 := url.Parse(cloneURL)
|
||||||
if err1 != nil || err2 != nil {
|
if err1 != nil || err2 != nil {
|
||||||
|
|||||||
@@ -123,6 +123,65 @@ 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) {
|
func gitMust(t *testing.T, dir string, args ...string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
cmd := exec.Command("git", args...)
|
cmd := exec.Command("git", args...)
|
||||||
|
|||||||
@@ -220,16 +220,17 @@ func (rc *RunContext) startHostEnvironment() common.Executor {
|
|||||||
}
|
}
|
||||||
toolCache := filepath.Join(cacheDir, "tool_cache")
|
toolCache := filepath.Join(cacheDir, "tool_cache")
|
||||||
rc.JobContainer = &container.HostEnvironment{
|
rc.JobContainer = &container.HostEnvironment{
|
||||||
Path: path,
|
Path: path,
|
||||||
TmpDir: runnerTmp,
|
TmpDir: runnerTmp,
|
||||||
ToolCache: toolCache,
|
ToolCache: toolCache,
|
||||||
Workdir: rc.Config.Workdir,
|
Workdir: rc.Config.Workdir,
|
||||||
BindWorkdir: rc.Config.BindWorkdir,
|
CleanWorkdir: rc.Config.CleanWorkdir,
|
||||||
ActPath: actPath,
|
ActPath: actPath,
|
||||||
CleanUp: func() {
|
CleanUp: func() {
|
||||||
os.RemoveAll(miscpath)
|
os.RemoveAll(miscpath)
|
||||||
},
|
},
|
||||||
StdOut: logWriter,
|
StdOut: logWriter,
|
||||||
|
AllocatePTY: rc.Config.AllocatePTY,
|
||||||
}
|
}
|
||||||
rc.cleanUpJobContainer = rc.JobContainer.Remove()
|
rc.cleanUpJobContainer = rc.JobContainer.Remove()
|
||||||
for k, v := range rc.JobContainer.GetRunnerContext(ctx) {
|
for k, v := range rc.JobContainer.GetRunnerContext(ctx) {
|
||||||
@@ -371,6 +372,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
|||||||
NetworkAliases: []string{serviceID},
|
NetworkAliases: []string{serviceID},
|
||||||
ExposedPorts: exposedPorts,
|
ExposedPorts: exposedPorts,
|
||||||
PortBindings: portBindings,
|
PortBindings: portBindings,
|
||||||
|
AllocatePTY: rc.Config.AllocatePTY,
|
||||||
})
|
})
|
||||||
rc.ServiceContainers = append(rc.ServiceContainers, c)
|
rc.ServiceContainers = append(rc.ServiceContainers, c)
|
||||||
}
|
}
|
||||||
@@ -431,6 +433,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
|||||||
Options: rc.options(ctx),
|
Options: rc.options(ctx),
|
||||||
AutoRemove: rc.Config.AutoRemove,
|
AutoRemove: rc.Config.AutoRemove,
|
||||||
ValidVolumes: rc.Config.ValidVolumes,
|
ValidVolumes: rc.Config.ValidVolumes,
|
||||||
|
AllocatePTY: rc.Config.AllocatePTY,
|
||||||
})
|
})
|
||||||
if rc.JobContainer == nil {
|
if rc.JobContainer == nil {
|
||||||
return errors.New("Failed to create job container")
|
return errors.New("Failed to create job container")
|
||||||
@@ -598,10 +601,34 @@ func (rc *RunContext) interpolateOutputs() common.Executor {
|
|||||||
|
|
||||||
func (rc *RunContext) startContainer() common.Executor {
|
func (rc *RunContext) startContainer() common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
|
var err error
|
||||||
if rc.IsHostEnv(ctx) {
|
if rc.IsHostEnv(ctx) {
|
||||||
return rc.startHostEnvironment()(ctx)
|
err = rc.startHostEnvironment()(ctx)
|
||||||
|
} else {
|
||||||
|
err = rc.startJobContainer()(ctx)
|
||||||
}
|
}
|
||||||
return rc.startJobContainer()(ctx)
|
if err != nil {
|
||||||
|
// The job executor's teardown only runs after a successful start, so a failed
|
||||||
|
// start would otherwise leak the per-job network and container.
|
||||||
|
rc.cleanupFailedStart(ctx)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *RunContext) cleanupFailedStart(ctx context.Context) {
|
||||||
|
if rc.cleanUpJobContainer == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cleanCtx := ctx
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
// the start likely failed because ctx was cancelled, detach so teardown still runs
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
cleanCtx, cancel = context.WithTimeout(common.WithLogger(context.Background(), common.Logger(ctx)), time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
}
|
||||||
|
if err := rc.cleanUpJobContainer(cleanCtx); err != nil {
|
||||||
|
common.Logger(ctx).Errorf("Error while cleaning up after failed container start for job %s: %v", rc.JobName, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
assert "github.com/stretchr/testify/assert"
|
assert "github.com/stretchr/testify/assert"
|
||||||
|
require "github.com/stretchr/testify/require"
|
||||||
yaml "go.yaml.in/yaml/v4"
|
yaml "go.yaml.in/yaml/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -659,3 +660,53 @@ func TestPrintStartJobContainerGroupGolden(t *testing.T) {
|
|||||||
}, "\n")
|
}, "\n")
|
||||||
assert.Equal(t, want, buf.String())
|
assert.Equal(t, want, buf.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunContext_cleanupFailedStart(t *testing.T) {
|
||||||
|
type ctxKey string
|
||||||
|
const sentinel = ctxKey("sentinel")
|
||||||
|
|
||||||
|
// the fresh context is cancelled via defer on return, so capture state inside the stub
|
||||||
|
type capture struct {
|
||||||
|
calls int
|
||||||
|
err error
|
||||||
|
sentinel any
|
||||||
|
}
|
||||||
|
newRC := func(c *capture) *RunContext {
|
||||||
|
return &RunContext{
|
||||||
|
JobName: "job",
|
||||||
|
cleanUpJobContainer: func(ctx context.Context) error {
|
||||||
|
c.calls++
|
||||||
|
c.err = ctx.Err()
|
||||||
|
c.sentinel = ctx.Value(sentinel)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("runs teardown on the live context", func(t *testing.T) {
|
||||||
|
var c capture
|
||||||
|
ctx := context.WithValue(context.Background(), sentinel, "v")
|
||||||
|
|
||||||
|
newRC(&c).cleanupFailedStart(ctx)
|
||||||
|
|
||||||
|
assert.Equal(t, 1, c.calls)
|
||||||
|
require.NoError(t, c.err)
|
||||||
|
assert.Equal(t, "v", c.sentinel)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("falls back to a fresh context when the input is done", func(t *testing.T) {
|
||||||
|
var c capture
|
||||||
|
ctx, cancel := context.WithCancel(context.WithValue(context.Background(), sentinel, "v"))
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
newRC(&c).cleanupFailedStart(ctx)
|
||||||
|
|
||||||
|
assert.Equal(t, 1, c.calls)
|
||||||
|
require.NoError(t, c.err)
|
||||||
|
assert.Nil(t, c.sentinel)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no-op when there is nothing to clean up", func(t *testing.T) {
|
||||||
|
assert.NotPanics(t, func() { (&RunContext{}).cleanupFailedStart(context.Background()) })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ type Config struct {
|
|||||||
Actor string // the user that triggered the event
|
Actor string // the user that triggered the event
|
||||||
Workdir string // path to working directory
|
Workdir string // path to working directory
|
||||||
ActionCacheDir string // path used for caching action contents
|
ActionCacheDir string // path used for caching action contents
|
||||||
ActionOfflineMode bool // when offline, use caching action contents
|
ActionOfflineMode bool // when offline, use cached action contents
|
||||||
BindWorkdir bool // bind the workdir to the job container
|
BindWorkdir bool // bind the workdir to the job container
|
||||||
EventName string // name of event to run
|
EventName string // name of event to run
|
||||||
EventPath string // path to JSON file to use for event.json in containers
|
EventPath string // path to JSON file to use for event.json in containers
|
||||||
@@ -73,12 +73,14 @@ type Config struct {
|
|||||||
EventJSON string // the content of JSON file to use for event.json in containers, overrides EventPath
|
EventJSON string // the content of JSON file to use for event.json in containers, overrides EventPath
|
||||||
ContainerNamePrefix string // the prefix of container name
|
ContainerNamePrefix string // the prefix of container name
|
||||||
ContainerMaxLifetime time.Duration // the max lifetime of job containers
|
ContainerMaxLifetime time.Duration // the max lifetime of job containers
|
||||||
|
CleanWorkdir bool // remove host executor workdir on teardown
|
||||||
DefaultActionInstance string // the default actions web site
|
DefaultActionInstance string // the default actions web site
|
||||||
PlatformPicker func(labels []string) string // platform picker, it will take precedence over Platforms if isn't nil
|
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
|
JobLoggerLevel *log.Level // the level of job logger
|
||||||
ValidVolumes []string // only volumes (and bind mounts) in this slice can be mounted on the job container or service containers
|
ValidVolumes []string // only volumes (and bind mounts) in this slice can be mounted on the job container or service containers
|
||||||
InsecureSkipTLS bool // whether to skip verifying TLS certificate of the Gitea instance
|
InsecureSkipTLS bool // whether to skip verifying TLS certificate of the Gitea instance
|
||||||
MaxParallel int // max parallel jobs to run across all workflows (0 = no limit, uses CPU count)
|
MaxParallel int // max parallel jobs to run across all workflows (0 = no limit, uses CPU count)
|
||||||
|
AllocatePTY bool // allocate a pseudo-TTY for each step's process
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetToken: Adapt to Gitea
|
// GetToken: Adapt to Gitea
|
||||||
@@ -90,6 +92,17 @@ func (c Config) GetToken() string {
|
|||||||
return token
|
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 {
|
type caller struct {
|
||||||
runContext *RunContext
|
runContext *RunContext
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
"gitea.com/gitea/runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
@@ -192,6 +193,7 @@ func (j *TestJobFileInfo) runTest(ctx context.Context, t *testing.T, cfg *Config
|
|||||||
Inputs: cfg.Inputs,
|
Inputs: cfg.Inputs,
|
||||||
GitHubInstance: "github.com",
|
GitHubInstance: "github.com",
|
||||||
ContainerArchitecture: cfg.ContainerArchitecture,
|
ContainerArchitecture: cfg.ContainerArchitecture,
|
||||||
|
ContainerMaxLifetime: time.Hour,
|
||||||
Matrix: cfg.Matrix,
|
Matrix: cfg.Matrix,
|
||||||
ActionCache: cfg.ActionCache,
|
ActionCache: cfg.ActionCache,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,9 +113,10 @@ func (sar *stepActionRemote) prepareActionExecutor() common.Executor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
actionDir := fmt.Sprintf("%s/%s", sar.RunContext.ActionCacheDir(), sar.Step.UsesHash())
|
actionDir := fmt.Sprintf("%s/%s", sar.RunContext.ActionCacheDir(), sar.Step.UsesHash())
|
||||||
token := getGitCloneToken(sar.getRunContext().Config, sar.remoteAction.CloneURL(sar.RunContext.Config.DefaultActionInstance))
|
defaultActionURL := sar.RunContext.Config.DefaultActionURL()
|
||||||
|
token := getGitCloneToken(sar.getRunContext().Config, sar.remoteAction.CloneURL(defaultActionURL))
|
||||||
gitClone := stepActionRemoteNewCloneExecutor(git.NewGitCloneExecutorInput{
|
gitClone := stepActionRemoteNewCloneExecutor(git.NewGitCloneExecutorInput{
|
||||||
URL: sar.remoteAction.CloneURL(sar.RunContext.Config.DefaultActionInstance),
|
URL: sar.remoteAction.CloneURL(defaultActionURL),
|
||||||
Ref: sar.remoteAction.Ref,
|
Ref: sar.remoteAction.Ref,
|
||||||
Dir: actionDir,
|
Dir: actionDir,
|
||||||
Token: token,
|
Token: token,
|
||||||
@@ -274,7 +275,7 @@ func (sar *stepActionRemote) cloneSkipTLS() bool {
|
|||||||
if sar.remoteAction.URL == "" {
|
if sar.remoteAction.URL == "" {
|
||||||
// Empty URL means the default action instance should be used
|
// 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 true if the URL of the Gitea instance is the same as the URL of the default action instance
|
||||||
return sar.RunContext.Config.DefaultActionInstance == sar.RunContext.Config.GitHubInstance
|
return sar.RunContext.Config.DefaultActionURL() == sar.RunContext.Config.GitHubInstance
|
||||||
}
|
}
|
||||||
// Return true if the URL of the remote action is the same as the URL of the Gitea instance
|
// 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
|
return sar.remoteAction.URL == sar.RunContext.Config.GitHubInstance
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"go.yaml.in/yaml/v4"
|
"go.yaml.in/yaml/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -434,6 +435,57 @@ 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) {
|
func TestStepActionRemotePost(t *testing.T) {
|
||||||
table := []struct {
|
table := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ func (sd *stepDocker) newStepContainer(ctx context.Context, image string, cmd, e
|
|||||||
Platform: rc.Config.ContainerArchitecture,
|
Platform: rc.Config.ContainerArchitecture,
|
||||||
AutoRemove: rc.Config.AutoRemove,
|
AutoRemove: rc.Config.AutoRemove,
|
||||||
ValidVolumes: rc.Config.ValidVolumes,
|
ValidVolumes: rc.Config.ValidVolumes,
|
||||||
|
AllocatePTY: rc.Config.AllocatePTY,
|
||||||
})
|
})
|
||||||
return stepContainer
|
return stepContainer
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,6 +109,55 @@ func TestStepDockerMain(t *testing.T) {
|
|||||||
cm.AssertExpectations(t)
|
cm.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStepDockerNewStepContainerAllocatePTY(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
allocPTY bool
|
||||||
|
}{
|
||||||
|
{name: "off", allocPTY: false},
|
||||||
|
{name: "on", allocPTY: true},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
cm := &containerMock{}
|
||||||
|
|
||||||
|
var captured *container.NewContainerInput
|
||||||
|
origContainerNewContainer := ContainerNewContainer
|
||||||
|
ContainerNewContainer = func(input *container.NewContainerInput) container.ExecutionsEnvironment {
|
||||||
|
captured = input
|
||||||
|
return cm
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
ContainerNewContainer = origContainerNewContainer
|
||||||
|
}()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
sd := &stepDocker{
|
||||||
|
RunContext: &RunContext{
|
||||||
|
StepResults: map[string]*model.StepResult{},
|
||||||
|
Config: &Config{
|
||||||
|
AllocatePTY: tc.allocPTY,
|
||||||
|
PlatformPicker: func(_ []string) string {
|
||||||
|
return "node:14"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Run: &model.Run{
|
||||||
|
JobID: "1",
|
||||||
|
Workflow: &model.Workflow{
|
||||||
|
Jobs: map[string]*model.Job{"1": {}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
JobContainer: cm,
|
||||||
|
},
|
||||||
|
Step: &model.Step{ID: "1", Uses: "docker://node:14"},
|
||||||
|
}
|
||||||
|
sd.RunContext.ExprEval = sd.RunContext.NewExpressionEvaluator(ctx)
|
||||||
|
|
||||||
|
_ = sd.newStepContainer(ctx, "node:14", []string{"echo", "hi"}, nil)
|
||||||
|
assert.Equal(t, tc.allocPTY, captured.AllocatePTY)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestStepDockerPrePost(t *testing.T) {
|
func TestStepDockerPrePost(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
sd := &stepDocker{}
|
sd := &stepDocker{}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM alpine:3
|
FROM alpine:3.23
|
||||||
|
|
||||||
COPY entrypoint.sh /entrypoint.sh
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
|
|
||||||
|
|||||||
@@ -10,4 +10,4 @@ outputs:
|
|||||||
description: 'The time we greeted you'
|
description: 'The time we greeted you'
|
||||||
runs:
|
runs:
|
||||||
using: 'node24'
|
using: 'node24'
|
||||||
main: 'dist/index.js'
|
main: 'index.js'
|
||||||
|
|||||||
21
act/runner/testdata/actions/node24/index.js
vendored
21
act/runner/testdata/actions/node24/index.js
vendored
@@ -1,11 +1,14 @@
|
|||||||
import {getInput, setOutput, setFailed} from '@actions/core';
|
import {appendFileSync, readFileSync} from 'node:fs';
|
||||||
import {context} from '@actions/github';
|
|
||||||
|
|
||||||
try {
|
const nameToGreet = process.env['INPUT_WHO-TO-GREET'] || 'World';
|
||||||
const nameToGreet = getInput('who-to-greet');
|
console.log(`Hello ${nameToGreet}!`);
|
||||||
console.log(`Hello ${nameToGreet}!`);
|
|
||||||
setOutput('time', (new Date()).toTimeString());
|
if (process.env.GITHUB_OUTPUT) {
|
||||||
console.log(`The event payload: ${JSON.stringify(context.payload, undefined, 2)}`);
|
appendFileSync(process.env.GITHUB_OUTPUT, `time=${new Date().toTimeString()}\n`);
|
||||||
} catch (error) {
|
|
||||||
setFailed(error.message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let payload = {};
|
||||||
|
if (process.env.GITHUB_EVENT_PATH) {
|
||||||
|
payload = JSON.parse(readFileSync(process.env.GITHUB_EVENT_PATH, 'utf8'));
|
||||||
|
}
|
||||||
|
console.log(`The event payload: ${JSON.stringify(payload, undefined, 2)}`);
|
||||||
|
|||||||
20
act/runner/testdata/actions/node24/package.json
vendored
20
act/runner/testdata/actions/node24/package.json
vendored
@@ -1,21 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "node24",
|
"name": "node24",
|
||||||
"version": "1.0.0",
|
"private": true,
|
||||||
"description": "",
|
"type": "module"
|
||||||
"main": "index.js",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"build": "ncc build index.js"
|
|
||||||
},
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"@actions/core": "^3.0.1",
|
|
||||||
"@actions/github": "^9.1.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@vercel/ncc": "^0.38.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=24"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
2
act/runner/testdata/secrets/.env
vendored
Normal file
2
act/runner/testdata/secrets/.env
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
HELLO=WORLD
|
||||||
|
MULTILINE_ENV="foo\nbar\nbaz"
|
||||||
@@ -9,9 +9,9 @@ inputs:
|
|||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '16'
|
node-version: '24'
|
||||||
- run: |
|
- run: |
|
||||||
console.log(process.version);
|
console.log(process.version);
|
||||||
console.log("Hi from node");
|
console.log("Hi from node");
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
@@ -1,199 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
@@ -1,418 +0,0 @@
|
|||||||
// 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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
8
go.mod
8
go.mod
@@ -4,17 +4,17 @@ go 1.26.0
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
code.gitea.io/actions-proto-go v0.4.1
|
code.gitea.io/actions-proto-go v0.4.1
|
||||||
connectrpc.com/connect v1.19.2
|
connectrpc.com/connect v1.20.0
|
||||||
dario.cat/mergo v1.0.2
|
dario.cat/mergo v1.0.2
|
||||||
github.com/Masterminds/semver v1.5.0
|
github.com/Masterminds/semver v1.5.0
|
||||||
github.com/avast/retry-go/v5 v5.0.0
|
github.com/avast/retry-go/v5 v5.0.0
|
||||||
github.com/containerd/errdefs v1.0.0
|
github.com/containerd/errdefs v1.0.0
|
||||||
github.com/creack/pty v1.1.24
|
github.com/creack/pty v1.1.24
|
||||||
github.com/distribution/reference v0.6.0
|
github.com/distribution/reference v0.6.0
|
||||||
github.com/docker/cli v29.4.3+incompatible
|
github.com/docker/cli v29.5.2+incompatible
|
||||||
github.com/docker/go-connections v0.7.0
|
github.com/docker/go-connections v0.7.0
|
||||||
github.com/go-git/go-billy/v5 v5.9.0
|
github.com/go-git/go-billy/v5 v5.9.0
|
||||||
github.com/go-git/go-git/v5 v5.19.0
|
github.com/go-git/go-git/v5 v5.19.1
|
||||||
github.com/gobwas/glob v0.2.3
|
github.com/gobwas/glob v0.2.3
|
||||||
github.com/google/go-cmp v0.7.0
|
github.com/google/go-cmp v0.7.0
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
@@ -26,7 +26,7 @@ require (
|
|||||||
github.com/moby/moby/client v0.4.1
|
github.com/moby/moby/client v0.4.1
|
||||||
github.com/moby/patternmatcher v0.6.1
|
github.com/moby/patternmatcher v0.6.1
|
||||||
github.com/opencontainers/image-spec v1.1.1
|
github.com/opencontainers/image-spec v1.1.1
|
||||||
github.com/opencontainers/selinux v1.14.1
|
github.com/opencontainers/selinux v1.15.0
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/prometheus/client_golang v1.23.2
|
github.com/prometheus/client_golang v1.23.2
|
||||||
github.com/rhysd/actionlint v1.7.12
|
github.com/rhysd/actionlint v1.7.12
|
||||||
|
|||||||
12
go.sum
12
go.sum
@@ -2,6 +2,8 @@ code.gitea.io/actions-proto-go v0.4.1 h1:l0EYhjsgpUe/1VABo2eK7zcoNX2W44WOnb0MSLr
|
|||||||
code.gitea.io/actions-proto-go v0.4.1/go.mod h1:mn7Wkqz6JbnTOHQpot3yDeHx+O5C9EGhMEE+htvHBas=
|
code.gitea.io/actions-proto-go v0.4.1/go.mod h1:mn7Wkqz6JbnTOHQpot3yDeHx+O5C9EGhMEE+htvHBas=
|
||||||
connectrpc.com/connect v1.19.2 h1:McQ83FGdzL+t60peksi0gXC7MQ/iLKgLduAnThbM0mo=
|
connectrpc.com/connect v1.19.2 h1:McQ83FGdzL+t60peksi0gXC7MQ/iLKgLduAnThbM0mo=
|
||||||
connectrpc.com/connect v1.19.2/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
|
connectrpc.com/connect v1.19.2/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
|
||||||
|
connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ=
|
||||||
|
connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4=
|
||||||
cyphar.com/go-pathrs v0.2.3 h1:0pH8gep37wB0BgaXrEaN1OtZhUMeS7VvaejSr6i822o=
|
cyphar.com/go-pathrs v0.2.3 h1:0pH8gep37wB0BgaXrEaN1OtZhUMeS7VvaejSr6i822o=
|
||||||
cyphar.com/go-pathrs v0.2.3/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc=
|
cyphar.com/go-pathrs v0.2.3/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc=
|
||||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||||
@@ -47,8 +49,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
github.com/docker/cli v29.4.3+incompatible h1:u+UliYm2J/rYrIh2FqHQg32neRG8GjbvNuwQRTzGspU=
|
github.com/docker/cli v29.5.2+incompatible h1:ubykJ1Y8LmNRGJ2BuMQ0kHOt/RO1YzGNswqWMJgivuQ=
|
||||||
github.com/docker/cli v29.4.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
github.com/docker/cli v29.5.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||||
github.com/docker/docker-credential-helpers v0.9.6 h1:cT2PbRPSlnMmNTfT2TDMXRyQ1KMWHG7xoTLBcn1ZNv0=
|
github.com/docker/docker-credential-helpers v0.9.6 h1:cT2PbRPSlnMmNTfT2TDMXRyQ1KMWHG7xoTLBcn1ZNv0=
|
||||||
github.com/docker/docker-credential-helpers v0.9.6/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
|
github.com/docker/docker-credential-helpers v0.9.6/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
|
||||||
github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=
|
github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=
|
||||||
@@ -71,8 +73,8 @@ github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmm
|
|||||||
github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
|
github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
|
||||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||||
github.com/go-git/go-git/v5 v5.19.0 h1:+WkVUQZSy/F1Gb13udrMKjIM2PrzsNfDKFSfo5tkMtc=
|
github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00=
|
||||||
github.com/go-git/go-git/v5 v5.19.0/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
|
github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
@@ -149,6 +151,8 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw
|
|||||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||||
github.com/opencontainers/selinux v1.14.1 h1:a7XlXV/nN/l5zFP1FWZYoExpClu1QOPMfWUV2CZ8kEQ=
|
github.com/opencontainers/selinux v1.14.1 h1:a7XlXV/nN/l5zFP1FWZYoExpClu1QOPMfWUV2CZ8kEQ=
|
||||||
github.com/opencontainers/selinux v1.14.1/go.mod h1:LenyElirjUHszfxrjuFqC85HIeXZKumHcKMQtnaDlQQ=
|
github.com/opencontainers/selinux v1.14.1/go.mod h1:LenyElirjUHszfxrjuFqC85HIeXZKumHcKMQtnaDlQQ=
|
||||||
|
github.com/opencontainers/selinux v1.15.0 h1:4Gs40e/R2FvM8PC1HPaPncLLaDor8Y2WDfk5gjU9o5M=
|
||||||
|
github.com/opencontainers/selinux v1.15.0/go.mod h1:LenyElirjUHszfxrjuFqC85HIeXZKumHcKMQtnaDlQQ=
|
||||||
github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU=
|
github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU=
|
||||||
github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
|||||||
@@ -132,7 +132,6 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
|
|||||||
cfg.Runner.Insecure,
|
cfg.Runner.Insecure,
|
||||||
reg.UUID,
|
reg.UUID,
|
||||||
reg.Token,
|
reg.Token,
|
||||||
ver.Version(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
runner := run.NewRunner(cfg, reg, cli)
|
runner := run.NewRunner(cfg, reg, cli)
|
||||||
|
|||||||
@@ -325,7 +325,6 @@ func doRegister(ctx context.Context, cfg *config.Config, inputs *registerInputs)
|
|||||||
cfg.Runner.Insecure,
|
cfg.Runner.Insecure,
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
ver.Version(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@@ -366,12 +365,11 @@ func doRegister(ctx context.Context, cfg *config.Config, inputs *registerInputs)
|
|||||||
}
|
}
|
||||||
// register new runner.
|
// register new runner.
|
||||||
resp, err := cli.Register(ctx, connect.NewRequest(&runnerv1.RegisterRequest{
|
resp, err := cli.Register(ctx, connect.NewRequest(&runnerv1.RegisterRequest{
|
||||||
Name: reg.Name,
|
Name: reg.Name,
|
||||||
Token: reg.Token,
|
Token: reg.Token,
|
||||||
Version: ver.Version(),
|
Version: ver.Version(),
|
||||||
AgentLabels: ls, // Could be removed after Gitea 1.20
|
Labels: ls,
|
||||||
Labels: ls,
|
Ephemeral: reg.Ephemeral,
|
||||||
Ephemeral: reg.Ephemeral,
|
|
||||||
}))
|
}))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Error("poller: cannot register new runner")
|
log.WithError(err).Error("poller: cannot register new runner")
|
||||||
|
|||||||
@@ -344,9 +344,11 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
|||||||
runnerConfig := &runner.Config{
|
runnerConfig := &runner.Config{
|
||||||
// On Linux, Workdir will be like "/<parent_directory>/<owner>/<repo>"
|
// On Linux, Workdir will be like "/<parent_directory>/<owner>/<repo>"
|
||||||
// On Windows, Workdir will be like "\<parent_directory>\<owner>\<repo>"
|
// On Windows, Workdir will be like "\<parent_directory>\<owner>\<repo>"
|
||||||
Workdir: workdir,
|
Workdir: workdir,
|
||||||
BindWorkdir: r.cfg.Container.BindWorkdir,
|
BindWorkdir: r.cfg.Container.BindWorkdir,
|
||||||
ActionCacheDir: filepath.FromSlash(r.cfg.Host.WorkdirParent),
|
ActionCacheDir: filepath.FromSlash(r.cfg.Host.WorkdirParent),
|
||||||
|
AllocatePTY: r.cfg.Runner.AllocatePTY,
|
||||||
|
ActionOfflineMode: r.cfg.Cache.OfflineMode,
|
||||||
|
|
||||||
ReuseContainers: false,
|
ReuseContainers: false,
|
||||||
ForcePull: r.cfg.Container.ForcePull,
|
ForcePull: r.cfg.Container.ForcePull,
|
||||||
@@ -362,6 +364,7 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
|||||||
EventJSON: string(eventJSON),
|
EventJSON: string(eventJSON),
|
||||||
ContainerNamePrefix: fmt.Sprintf("GITEA-ACTIONS-TASK-%d", task.Id),
|
ContainerNamePrefix: fmt.Sprintf("GITEA-ACTIONS-TASK-%d", task.Id),
|
||||||
ContainerMaxLifetime: maxLifetime,
|
ContainerMaxLifetime: maxLifetime,
|
||||||
|
CleanWorkdir: true,
|
||||||
ContainerNetworkMode: container.NetworkMode(r.cfg.Container.Network),
|
ContainerNetworkMode: container.NetworkMode(r.cfg.Container.Network),
|
||||||
ContainerOptions: r.cfg.Container.Options,
|
ContainerOptions: r.cfg.Container.Options,
|
||||||
ContainerDaemonSocket: r.cfg.Container.DockerHost,
|
ContainerDaemonSocket: r.cfg.Container.DockerHost,
|
||||||
|
|||||||
@@ -6,6 +6,4 @@ package client
|
|||||||
const (
|
const (
|
||||||
UUIDHeader = "x-runner-uuid"
|
UUIDHeader = "x-runner-uuid"
|
||||||
TokenHeader = "x-runner-token"
|
TokenHeader = "x-runner-token"
|
||||||
// Deprecated: could be removed after Gitea 1.20 released
|
|
||||||
VersionHeader = "x-runner-version"
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ func getHTTPClient(endpoint string, insecure bool) *http.Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new runner client.
|
// New returns a new runner client.
|
||||||
func New(endpoint string, insecure bool, uuid, token, version string, opts ...connect.ClientOption) *HTTPClient {
|
func New(endpoint string, insecure bool, uuid, token string, opts ...connect.ClientOption) *HTTPClient {
|
||||||
baseURL := strings.TrimRight(endpoint, "/") + "/api/actions"
|
baseURL := strings.TrimRight(endpoint, "/") + "/api/actions"
|
||||||
|
|
||||||
opts = append(opts, connect.WithInterceptors(connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc {
|
opts = append(opts, connect.WithInterceptors(connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc {
|
||||||
@@ -42,10 +42,6 @@ func New(endpoint string, insecure bool, uuid, token, version string, opts ...co
|
|||||||
if token != "" {
|
if token != "" {
|
||||||
req.Header().Set(TokenHeader, token)
|
req.Header().Set(TokenHeader, token)
|
||||||
}
|
}
|
||||||
// TODO: version will be removed from request header after Gitea 1.20 released.
|
|
||||||
if version != "" {
|
|
||||||
req.Header().Set(VersionHeader, version)
|
|
||||||
}
|
|
||||||
return next(ctx, req)
|
return next(ctx, req)
|
||||||
}
|
}
|
||||||
})))
|
})))
|
||||||
|
|||||||
@@ -74,6 +74,11 @@ runner:
|
|||||||
- "ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest"
|
- "ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest"
|
||||||
- "ubuntu-24.04:docker://docker.gitea.com/runner-images:ubuntu-24.04"
|
- "ubuntu-24.04:docker://docker.gitea.com/runner-images:ubuntu-24.04"
|
||||||
- "ubuntu-22.04:docker://docker.gitea.com/runner-images:ubuntu-22.04"
|
- "ubuntu-22.04:docker://docker.gitea.com/runner-images:ubuntu-22.04"
|
||||||
|
# Allocate a pseudo-TTY for each step's process. Applies to both host and docker backends.
|
||||||
|
# Default false matches GitHub actions/runner. Enable only for jobs that need an interactive
|
||||||
|
# terminal; tools like `docker build` emit redrawing progress frames into the captured log
|
||||||
|
# when a TTY is present.
|
||||||
|
allocate_pty: false
|
||||||
|
|
||||||
cache:
|
cache:
|
||||||
# Enable cache server to use actions/cache.
|
# Enable cache server to use actions/cache.
|
||||||
@@ -97,6 +102,9 @@ cache:
|
|||||||
# (or `gitea-runner cache-server`) is in use: the runner pre-registers each job's ACTIONS_RUNTIME_TOKEN with the
|
# (or `gitea-runner cache-server`) is in use: the runner pre-registers each job's ACTIONS_RUNTIME_TOKEN with the
|
||||||
# cache-server, and the cache-server enforces bearer auth + per-repo cache isolation.
|
# cache-server, and the cache-server enforces bearer auth + per-repo cache isolation.
|
||||||
external_secret: ""
|
external_secret: ""
|
||||||
|
# When true, reuse a cached action instead of fetching from the remote on every job. Note: a moved tag
|
||||||
|
# (e.g. a re-tagged "v6") or an updated branch stays at the cached commit until its cache entry is removed.
|
||||||
|
offline_mode: false
|
||||||
|
|
||||||
container:
|
container:
|
||||||
# Specifies the network to which the container will connect.
|
# Specifies the network to which the container will connect.
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ type Runner struct {
|
|||||||
StateReportInterval time.Duration `yaml:"state_report_interval"` // StateReportInterval specifies the interval for state reporting.
|
StateReportInterval time.Duration `yaml:"state_report_interval"` // StateReportInterval specifies the interval for state reporting.
|
||||||
Labels []string `yaml:"labels"` // Labels specify the labels of the runner. Labels are declared on each startup
|
Labels []string `yaml:"labels"` // Labels specify the labels of the runner. Labels are declared on each startup
|
||||||
GithubMirror string `yaml:"github_mirror"` // GithubMirror defines what mirrors should be used when using github
|
GithubMirror string `yaml:"github_mirror"` // GithubMirror defines what mirrors should be used when using github
|
||||||
|
AllocatePTY bool `yaml:"allocate_pty"` // AllocatePTY allocates a pseudo-TTY for each step's process. Default is false, matching GitHub's actions/runner. Enable only for jobs that need an interactive terminal; tools like docker build emit redrawing progress frames into the captured log when a TTY is present. Applies to both host and docker backends.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache represents the configuration for caching.
|
// Cache represents the configuration for caching.
|
||||||
@@ -51,6 +52,7 @@ type Cache struct {
|
|||||||
Port uint16 `yaml:"port"` // Port specifies the caching port.
|
Port uint16 `yaml:"port"` // Port specifies the caching port.
|
||||||
ExternalServer string `yaml:"external_server"` // ExternalServer specifies the URL of external cache server
|
ExternalServer string `yaml:"external_server"` // ExternalServer specifies the URL of external cache server
|
||||||
ExternalSecret string `yaml:"external_secret"` // ExternalSecret is a shared secret between this runner and an external gitea-runner cache-server, enabling per-job ACTIONS_RUNTIME_TOKEN authentication and repo scoping over the network. Leave empty to keep the legacy unauthenticated behavior.
|
ExternalSecret string `yaml:"external_secret"` // ExternalSecret is a shared secret between this runner and an external gitea-runner cache-server, enabling per-job ACTIONS_RUNTIME_TOKEN authentication and repo scoping over the network. Leave empty to keep the legacy unauthenticated behavior.
|
||||||
|
OfflineMode bool `yaml:"offline_mode"` // OfflineMode reuses a cached action without fetching from the remote; a moved tag or branch stays at the cached commit until the cache entry is removed.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Container represents the configuration for the container.
|
// Container represents the configuration for the container.
|
||||||
@@ -108,7 +110,6 @@ func LoadDefault(file string) (*Config, error) {
|
|||||||
return nil, fmt.Errorf("parse config file %q for defaults metadata: %w", file, err)
|
return nil, fmt.Errorf("parse config file %q for defaults metadata: %w", file, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
compatibleWithOldEnvs(file != "", cfg)
|
|
||||||
|
|
||||||
if cfg.Runner.EnvFile != "" {
|
if cfg.Runner.EnvFile != "" {
|
||||||
if stat, err := os.Stat(cfg.Runner.EnvFile); err == nil && !stat.IsDir() {
|
if stat, err := os.Stat(cfg.Runner.EnvFile); err == nil && !stat.IsDir() {
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Deprecated: could be removed in the future. TODO: remove it when Gitea 1.20.0 is released.
|
|
||||||
// Be compatible with old envs.
|
|
||||||
func compatibleWithOldEnvs(fileUsed bool, cfg *Config) {
|
|
||||||
handleEnv := func(key string) (string, bool) {
|
|
||||||
if v, ok := os.LookupEnv(key); ok {
|
|
||||||
if fileUsed {
|
|
||||||
log.Warnf("env %s has been ignored because config file is used", key)
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
log.Warnf("env %s will be deprecated, please use config file instead", key)
|
|
||||||
return v, true
|
|
||||||
}
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
if v, ok := handleEnv("GITEA_DEBUG"); ok {
|
|
||||||
if b, _ := strconv.ParseBool(v); b {
|
|
||||||
cfg.Log.Level = "debug"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := handleEnv("GITEA_TRACE"); ok {
|
|
||||||
if b, _ := strconv.ParseBool(v); b {
|
|
||||||
cfg.Log.Level = "trace"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := handleEnv("GITEA_RUNNER_CAPACITY"); ok {
|
|
||||||
if i, _ := strconv.Atoi(v); i > 0 {
|
|
||||||
cfg.Runner.Capacity = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := handleEnv("GITEA_RUNNER_FILE"); ok {
|
|
||||||
cfg.Runner.File = v
|
|
||||||
}
|
|
||||||
if v, ok := handleEnv("GITEA_RUNNER_ENVIRON"); ok {
|
|
||||||
splits := strings.Split(v, ",")
|
|
||||||
if cfg.Runner.Envs == nil {
|
|
||||||
cfg.Runner.Envs = map[string]string{}
|
|
||||||
}
|
|
||||||
for _, split := range splits {
|
|
||||||
kv := strings.SplitN(split, ":", 2)
|
|
||||||
if len(kv) == 2 && kv[0] != "" {
|
|
||||||
cfg.Runner.Envs[kv[0]] = kv[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := handleEnv("GITEA_RUNNER_ENV_FILE"); ok {
|
|
||||||
cfg.Runner.EnvFile = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -205,7 +205,7 @@ func (r *Reporter) Fire(entry *log.Entry) error {
|
|||||||
urgentState = true
|
urgentState = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !r.duringSteps() {
|
if r.shouldAppendLogRow(entry) {
|
||||||
r.logRows = appendIfNotNil(r.logRows, r.parseLogRow(entry))
|
r.logRows = appendIfNotNil(r.logRows, r.parseLogRow(entry))
|
||||||
}
|
}
|
||||||
r.unlockAndNotify(urgentState)
|
r.unlockAndNotify(urgentState)
|
||||||
@@ -219,7 +219,7 @@ func (r *Reporter) Fire(entry *log.Entry) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if step == nil {
|
if step == nil {
|
||||||
if !r.duringSteps() {
|
if r.shouldAppendLogRow(entry) {
|
||||||
r.logRows = appendIfNotNil(r.logRows, r.parseLogRow(entry))
|
r.logRows = appendIfNotNil(r.logRows, r.parseLogRow(entry))
|
||||||
}
|
}
|
||||||
r.unlockAndNotify(false)
|
r.unlockAndNotify(false)
|
||||||
@@ -246,7 +246,7 @@ func (r *Reporter) Fire(entry *log.Entry) error {
|
|||||||
r.logRows = append(r.logRows, row)
|
r.logRows = append(r.logRows, row)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if !r.duringSteps() {
|
} else if r.shouldAppendLogRow(entry) {
|
||||||
r.logRows = appendIfNotNil(r.logRows, r.parseLogRow(entry))
|
r.logRows = appendIfNotNil(r.logRows, r.parseLogRow(entry))
|
||||||
}
|
}
|
||||||
if v, ok := entry.Data["stepResult"]; ok && isJobStepEntry(entry) {
|
if v, ok := entry.Data["stepResult"]; ok && isJobStepEntry(entry) {
|
||||||
@@ -576,6 +576,13 @@ func (r *Reporter) duringSteps() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// shouldAppendLogRow reports whether a non-raw_output entry should be written
|
||||||
|
// to the job log: only when we are between steps and the entry's level is
|
||||||
|
// within the globally configured log level.
|
||||||
|
func (r *Reporter) shouldAppendLogRow(entry *log.Entry) bool {
|
||||||
|
return !r.duringSteps() && entry.Level <= log.GetLevel()
|
||||||
|
}
|
||||||
|
|
||||||
var stringToResult = map[string]runnerv1.Result{
|
var stringToResult = map[string]runnerv1.Result{
|
||||||
"success": runnerv1.Result_RESULT_SUCCESS,
|
"success": runnerv1.Result_RESULT_SUCCESS,
|
||||||
"failure": runnerv1.Result_RESULT_FAILURE,
|
"failure": runnerv1.Result_RESULT_FAILURE,
|
||||||
@@ -639,7 +646,7 @@ func (r *Reporter) handleCommand(originalContent, command, value string) *string
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Reporter) parseLogRow(entry *log.Entry) *runnerv1.LogRow {
|
func (r *Reporter) parseLogRow(entry *log.Entry) *runnerv1.LogRow {
|
||||||
content := strings.TrimRightFunc(entry.Message, func(r rune) bool { return r == '\r' || r == '\n' })
|
content := strings.TrimRight(entry.Message, "\r\n")
|
||||||
|
|
||||||
matches := cmdRegex.FindStringSubmatch(content)
|
matches := cmdRegex.FindStringSubmatch(content)
|
||||||
if matches != nil {
|
if matches != nil {
|
||||||
|
|||||||
@@ -219,6 +219,59 @@ func TestReporter_Fire(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestReporter_LogLevelFiltering(t *testing.T) {
|
||||||
|
// Set global level to Info so Debug entries should be filtered.
|
||||||
|
origLevel := log.GetLevel()
|
||||||
|
log.SetLevel(log.InfoLevel)
|
||||||
|
defer log.SetLevel(origLevel)
|
||||||
|
|
||||||
|
client := mocks.NewClient(t)
|
||||||
|
client.On("UpdateLog", mock.Anything, mock.Anything).Return(func(_ context.Context, req *connect_go.Request[runnerv1.UpdateLogRequest]) (*connect_go.Response[runnerv1.UpdateLogResponse], error) {
|
||||||
|
return connect_go.NewResponse(&runnerv1.UpdateLogResponse{
|
||||||
|
AckIndex: req.Msg.Index + int64(len(req.Msg.Rows)),
|
||||||
|
}), nil
|
||||||
|
})
|
||||||
|
client.On("UpdateTask", mock.Anything, mock.Anything).Return(func(_ context.Context, req *connect_go.Request[runnerv1.UpdateTaskRequest]) (*connect_go.Response[runnerv1.UpdateTaskResponse], error) {
|
||||||
|
return connect_go.NewResponse(&runnerv1.UpdateTaskResponse{}), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
taskCtx, err := structpb.NewStruct(map[string]any{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
cfg, _ := config.LoadDefault("")
|
||||||
|
reporter := NewReporter(ctx, cancel, client, &runnerv1.Task{Context: taskCtx}, cfg)
|
||||||
|
reporter.RunDaemon()
|
||||||
|
defer func() {
|
||||||
|
require.NoError(t, reporter.Close(""))
|
||||||
|
}()
|
||||||
|
reporter.ResetSteps(2)
|
||||||
|
|
||||||
|
dataStep0 := log.Fields{"stage": "Main", "stepNumber": 0, "raw_output": true}
|
||||||
|
dataStep0Internal := log.Fields{"stage": "Main", "stepNumber": 0}
|
||||||
|
|
||||||
|
// raw_output entries always appear in job log regardless of level.
|
||||||
|
require.NoError(t, reporter.Fire(&log.Entry{Message: "step output", Data: dataStep0, Level: log.InfoLevel}))
|
||||||
|
require.NoError(t, reporter.Fire(&log.Entry{Message: "step debug output", Data: dataStep0, Level: log.DebugLevel}))
|
||||||
|
assert.Equal(t, int64(2), reporter.state.Steps[0].LogLength, "raw_output entries must always be forwarded")
|
||||||
|
|
||||||
|
// Non-raw_output entries during steps are not added to logRows regardless of level.
|
||||||
|
require.NoError(t, reporter.Fire(&log.Entry{Message: "internal info", Data: dataStep0Internal, Level: log.InfoLevel}))
|
||||||
|
require.NoError(t, reporter.Fire(&log.Entry{Message: "internal debug", Data: dataStep0Internal, Level: log.DebugLevel}))
|
||||||
|
|
||||||
|
// stepResult at DebugLevel (skipped step) must still update state even when filtered from log.
|
||||||
|
require.NoError(t, reporter.Fire(&log.Entry{
|
||||||
|
Message: "Skipping step",
|
||||||
|
Data: log.Fields{
|
||||||
|
"stage": "Main",
|
||||||
|
"stepNumber": 1,
|
||||||
|
"stepResult": "skipped",
|
||||||
|
},
|
||||||
|
Level: log.DebugLevel,
|
||||||
|
}))
|
||||||
|
assert.Equal(t, runnerv1.Result_RESULT_SKIPPED, reporter.state.Steps[1].Result,
|
||||||
|
"stepResult at DebugLevel must update step state even when log entry is filtered from job log output")
|
||||||
|
}
|
||||||
|
|
||||||
// TestReporter_EphemeralRunnerDeletion reproduces the exact scenario from
|
// TestReporter_EphemeralRunnerDeletion reproduces the exact scenario from
|
||||||
// https://gitea.com/gitea/runner/issues/793:
|
// https://gitea.com/gitea/runner/issues/793:
|
||||||
//
|
//
|
||||||
|
|||||||
19
tools/lint-pr-title.ts
Normal file
19
tools/lint-pr-title.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import {env, exit} from 'node:process';
|
||||||
|
|
||||||
|
const allowedTypes = 'build, chore, ci, docs, enhance, feat, fix, perf, refactor, revert, style, test';
|
||||||
|
const title = env.PR_TITLE;
|
||||||
|
|
||||||
|
if (!title) {
|
||||||
|
console.error('Missing PR_TITLE');
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const validTitlePattern = new RegExp(`^(${allowedTypes.replaceAll(', ', '|')})(\\([\\w.-]+\\))?(!)?: .+$`);
|
||||||
|
|
||||||
|
if (!validTitlePattern.test(title)) {
|
||||||
|
console.error(`Invalid PR title: ${title}`);
|
||||||
|
console.error('Expected format: type(scope): subject');
|
||||||
|
console.error(`Allowed types: ${allowedTypes}`);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user