mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-05-08 08:13:25 +02:00
Compare commits
37 Commits
8af9a2b47a
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
594c9ade7c | ||
|
|
2a4d56c650 | ||
|
|
a22119cf88 | ||
|
|
b68ecf2580 | ||
|
|
d1434237c2 | ||
|
|
35c65e2b14 | ||
|
|
c45a4e6d32 | ||
|
|
68d9fc45c9 | ||
|
|
b1c873a66b | ||
|
|
1d6e7879c8 | ||
|
|
13dc9386fe | ||
|
|
8e6b3be96a | ||
|
|
e5e53c732e | ||
|
|
2516573592 | ||
|
|
35834bf817 | ||
|
|
11a5dc8936 | ||
|
|
f09fafcb0a | ||
|
|
801e5cf4d5 | ||
|
|
3f05040438 | ||
|
|
59d90bff26 | ||
|
|
5edc4ba550 | ||
|
|
547a0ff297 | ||
|
|
f2b4dbf05f | ||
|
|
bad4239d18 | ||
|
|
589db33e70 | ||
|
|
1032f857a1 | ||
|
|
e56b984c04 | ||
|
|
fa5334eb24 | ||
|
|
7c6f1261d4 | ||
|
|
fbd6316928 | ||
|
|
ade5b8202e | ||
|
|
a31f3962c0 | ||
|
|
04244fc3f7 | ||
|
|
cb58492678 | ||
|
|
9faadad0ce | ||
|
|
352096c5bf | ||
|
|
b5c50bb3ab |
52
.dockerignore
Normal file
52
.dockerignore
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Folders
|
||||||
|
_obj
|
||||||
|
_test
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
.idea
|
||||||
|
# Goland's output filename can not be set manually
|
||||||
|
/go_build_*
|
||||||
|
|
||||||
|
# MS VSCode
|
||||||
|
.vscode
|
||||||
|
__debug_bin*
|
||||||
|
|
||||||
|
# Architecture specific extensions/prefixes
|
||||||
|
*.[568vq]
|
||||||
|
[568vq].out
|
||||||
|
|
||||||
|
*.cgo1.go
|
||||||
|
*.cgo2.c
|
||||||
|
_cgo_defun.c
|
||||||
|
_cgo_gotypes.go
|
||||||
|
_cgo_export.*
|
||||||
|
|
||||||
|
_testmain.go
|
||||||
|
|
||||||
|
*.exe
|
||||||
|
*.test
|
||||||
|
*.prof
|
||||||
|
|
||||||
|
*coverage.out
|
||||||
|
coverage.all
|
||||||
|
coverage.txt
|
||||||
|
cpu.out
|
||||||
|
|
||||||
|
*.db
|
||||||
|
*.log
|
||||||
|
|
||||||
|
/gitea-runner
|
||||||
|
/debug
|
||||||
|
|
||||||
|
/bin
|
||||||
|
/dist
|
||||||
|
/.env
|
||||||
|
/.runner
|
||||||
|
/config.yaml
|
||||||
|
/Dockerfile
|
||||||
|
.DS_Store
|
||||||
@@ -12,5 +12,8 @@ insert_final_newline = true
|
|||||||
[*.{go}]
|
[*.{go}]
|
||||||
indent_style = tab
|
indent_style = tab
|
||||||
|
|
||||||
|
[go.*]
|
||||||
|
indent_style = tab
|
||||||
|
|
||||||
[Makefile]
|
[Makefile]
|
||||||
indent_style = tab
|
indent_style = tab
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ jobs:
|
|||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
- name: Echo the tag
|
- name: Echo the tag
|
||||||
run: echo "${{ env.DOCKER_ORG }}/act_runner:nightly${{ matrix.variant.tag_suffix }}"
|
run: echo "${{ env.DOCKER_ORG }}/runner:nightly${{ matrix.variant.tag_suffix }}"
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
@@ -82,4 +82,4 @@ jobs:
|
|||||||
linux/arm64
|
linux/arm64
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
${{ env.DOCKER_ORG }}/act_runner:nightly${{ matrix.variant.tag_suffix }}
|
${{ env.DOCKER_ORG }}/runner:nightly${{ matrix.variant.tag_suffix }}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
- name: Import GPG key
|
- name: Import GPG key
|
||||||
id: import_gpg
|
id: import_gpg
|
||||||
uses: crazy-max/ghaction-import-gpg@v6
|
uses: crazy-max/ghaction-import-gpg@v7
|
||||||
with:
|
with:
|
||||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
passphrase: ${{ secrets.PASSPHRASE }}
|
passphrase: ${{ secrets.PASSPHRASE }}
|
||||||
@@ -71,17 +71,12 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
- name: Repo Meta
|
|
||||||
id: repo_meta
|
|
||||||
run: |
|
|
||||||
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: "Docker meta"
|
- name: "Docker meta"
|
||||||
id: docker_meta
|
id: docker_meta
|
||||||
uses: https://github.com/docker/metadata-action@v5
|
uses: https://github.com/docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ env.DOCKER_ORG }}/${{ steps.repo_meta.outputs.REPO_NAME }}
|
${{ env.DOCKER_ORG }}/runner
|
||||||
tags: |
|
tags: |
|
||||||
type=semver,pattern={{major}}.{{minor}}.{{patch}}
|
type=semver,pattern={{major}}.{{minor}}.{{patch}}
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
name: checks
|
name: checks
|
||||||
on:
|
on:
|
||||||
- push
|
push:
|
||||||
- pull_request
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
@@ -17,4 +19,4 @@ jobs:
|
|||||||
- name: build
|
- name: build
|
||||||
run: make build
|
run: make build
|
||||||
- name: test
|
- name: test
|
||||||
run: make test
|
run: make test
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,4 +1,4 @@
|
|||||||
/act_runner
|
/gitea-runner
|
||||||
.env
|
.env
|
||||||
.runner
|
.runner
|
||||||
coverage.txt
|
coverage.txt
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ formatters:
|
|||||||
custom-order: true
|
custom-order: true
|
||||||
sections:
|
sections:
|
||||||
- standard
|
- standard
|
||||||
- prefix(gitea.com/gitea/act_runner)
|
- prefix(gitea.com/gitea/runner)
|
||||||
- blank
|
- blank
|
||||||
- default
|
- default
|
||||||
gofumpt:
|
gofumpt:
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
version: 2
|
version: 2
|
||||||
|
|
||||||
|
project_name: gitea-runner
|
||||||
|
|
||||||
before:
|
before:
|
||||||
hooks:
|
hooks:
|
||||||
- go mod tidy
|
- go mod tidy
|
||||||
@@ -63,7 +65,7 @@ builds:
|
|||||||
flags:
|
flags:
|
||||||
- -trimpath
|
- -trimpath
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w -X gitea.com/gitea/act_runner/internal/pkg/ver.version={{ .Summary }}
|
- -s -w -X gitea.com/gitea/runner/internal/pkg/ver.version={{ .Summary }}
|
||||||
binary: >-
|
binary: >-
|
||||||
{{ .ProjectName }}-
|
{{ .ProjectName }}-
|
||||||
{{- .Version }}-
|
{{- .Version }}-
|
||||||
@@ -86,7 +88,7 @@ blobs:
|
|||||||
provider: s3
|
provider: s3
|
||||||
bucket: "{{ .Env.S3_BUCKET }}"
|
bucket: "{{ .Env.S3_BUCKET }}"
|
||||||
region: "{{ .Env.S3_REGION }}"
|
region: "{{ .Env.S3_REGION }}"
|
||||||
directory: "act_runner/{{.Version}}"
|
directory: "gitea-runner/{{.Version}}"
|
||||||
extra_files:
|
extra_files:
|
||||||
- glob: ./**.xz
|
- glob: ./**.xz
|
||||||
- glob: ./**.sha256
|
- glob: ./**.sha256
|
||||||
|
|||||||
10
AGENTS.md
Normal file
10
AGENTS.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
- Use `make help` to find available development targets
|
||||||
|
- Run `make fmt` to format `.go` files, and run `make lint-go` to lint them
|
||||||
|
- Run `make tidy` after any `go.mod` changes
|
||||||
|
- Run single go unit tests with `go test -run '^TestName$' ./modulepath/`
|
||||||
|
- Add the current year into the copyright header of new `.go` files
|
||||||
|
- Ensure no trailing whitespace in edited files
|
||||||
|
- Never force-push, amend, or squash unless asked. Use new commits and normal push for pull request updates
|
||||||
|
- Preserve existing code comments, do not remove or rewrite comments that are still relevant
|
||||||
|
- Include authorship attribution in issue and pull request comments
|
||||||
|
- Add `Co-Authored-By` lines to all commits, indicating name and model used
|
||||||
10
Dockerfile
10
Dockerfile
@@ -9,8 +9,8 @@ RUN apk add --no-cache make git
|
|||||||
ARG GOPROXY
|
ARG GOPROXY
|
||||||
ENV GOPROXY=${GOPROXY:-}
|
ENV GOPROXY=${GOPROXY:-}
|
||||||
|
|
||||||
COPY . /opt/src/act_runner
|
COPY . /opt/src/runner
|
||||||
WORKDIR /opt/src/act_runner
|
WORKDIR /opt/src/runner
|
||||||
|
|
||||||
RUN make clean && make build
|
RUN make clean && make build
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ FROM docker:28-dind AS dind
|
|||||||
|
|
||||||
RUN apk add --no-cache s6 bash git tzdata
|
RUN apk add --no-cache s6 bash git tzdata
|
||||||
|
|
||||||
COPY --from=builder /opt/src/act_runner/act_runner /usr/local/bin/act_runner
|
COPY --from=builder /opt/src/runner/gitea-runner /usr/local/bin/gitea-runner
|
||||||
COPY scripts/run.sh /usr/local/bin/run.sh
|
COPY scripts/run.sh /usr/local/bin/run.sh
|
||||||
COPY scripts/s6 /etc/s6
|
COPY scripts/s6 /etc/s6
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ FROM docker:28-dind-rootless AS dind-rootless
|
|||||||
USER root
|
USER root
|
||||||
RUN apk add --no-cache s6 bash git tzdata
|
RUN apk add --no-cache s6 bash git tzdata
|
||||||
|
|
||||||
COPY --from=builder /opt/src/act_runner/act_runner /usr/local/bin/act_runner
|
COPY --from=builder /opt/src/runner/gitea-runner /usr/local/bin/gitea-runner
|
||||||
COPY scripts/run.sh /usr/local/bin/run.sh
|
COPY scripts/run.sh /usr/local/bin/run.sh
|
||||||
COPY scripts/s6 /etc/s6
|
COPY scripts/s6 /etc/s6
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ ENTRYPOINT ["s6-svscan","/etc/s6"]
|
|||||||
FROM alpine AS basic
|
FROM alpine AS basic
|
||||||
RUN apk add --no-cache tini bash git tzdata
|
RUN apk add --no-cache tini bash git tzdata
|
||||||
|
|
||||||
COPY --from=builder /opt/src/act_runner/act_runner /usr/local/bin/act_runner
|
COPY --from=builder /opt/src/runner/gitea-runner /usr/local/bin/gitea-runner
|
||||||
COPY scripts/run.sh /usr/local/bin/run.sh
|
COPY scripts/run.sh /usr/local/bin/run.sh
|
||||||
|
|
||||||
VOLUME /data
|
VOLUME /data
|
||||||
|
|||||||
74
Makefile
74
Makefile
@@ -1,19 +1,19 @@
|
|||||||
DIST := dist
|
DIST := dist
|
||||||
EXECUTABLE := act_runner
|
EXECUTABLE := gitea-runner
|
||||||
DIST_DIRS := $(DIST)/binaries $(DIST)/release
|
DIST_DIRS := $(DIST)/binaries $(DIST)/release
|
||||||
GO ?= go
|
GO ?= go
|
||||||
SHASUM ?= shasum -a 256
|
SHASUM ?= shasum -a 256
|
||||||
HAS_GO = $(shell hash $(GO) > /dev/null 2>&1 && echo "GO" || echo "NOGO" )
|
HAS_GO = $(shell hash $(GO) > /dev/null 2>&1 && echo "GO" || echo "NOGO" )
|
||||||
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
|
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
|
||||||
XGO_VERSION := go-1.26.x
|
XGO_VERSION := go-1.26.x
|
||||||
GXZ_PAGAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.10
|
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.10
|
||||||
|
|
||||||
LINUX_ARCHS ?= linux/amd64,linux/arm64
|
LINUX_ARCHS ?= linux/amd64,linux/arm64
|
||||||
DARWIN_ARCHS ?= darwin-12/amd64,darwin-12/arm64
|
DARWIN_ARCHS ?= darwin-12/amd64,darwin-12/arm64
|
||||||
WINDOWS_ARCHS ?= windows/amd64
|
WINDOWS_ARCHS ?= windows/amd64
|
||||||
GOFILES := $(shell find . -type f -name "*.go" -o -name "go.mod" ! -name "generated.*")
|
GOFILES := $(shell find . -type f -name "*.go" -o -name "go.mod" ! -name "generated.*")
|
||||||
|
|
||||||
DOCKER_IMAGE ?= gitea/act_runner
|
DOCKER_IMAGE ?= gitea/runner
|
||||||
DOCKER_TAG ?= nightly
|
DOCKER_TAG ?= nightly
|
||||||
DOCKER_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)
|
DOCKER_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)
|
||||||
DOCKER_ROOTLESS_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)-dind-rootless
|
DOCKER_ROOTLESS_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)-dind-rootless
|
||||||
@@ -21,10 +21,10 @@ DOCKER_ROOTLESS_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)-dind-rootless
|
|||||||
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.4
|
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.4
|
||||||
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
|
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
|
||||||
|
|
||||||
ifneq ($(shell uname), Darwin)
|
STATIC ?=
|
||||||
EXTLDFLAGS = -extldflags "-static" $(null)
|
EXTLDFLAGS ?=
|
||||||
else
|
ifneq ($(STATIC),)
|
||||||
EXTLDFLAGS =
|
EXTLDFLAGS = -extldflags "-static"
|
||||||
endif
|
endif
|
||||||
|
|
||||||
ifeq ($(HAS_GO), GO)
|
ifeq ($(HAS_GO), GO)
|
||||||
@@ -67,12 +67,17 @@ else
|
|||||||
endif
|
endif
|
||||||
|
|
||||||
TAGS ?=
|
TAGS ?=
|
||||||
LDFLAGS ?= -X "gitea.com/gitea/act_runner/internal/pkg/ver.version=v$(RELASE_VERSION)"
|
LDFLAGS ?= -X "gitea.com/gitea/runner/internal/pkg/ver.version=v$(RELASE_VERSION)"
|
||||||
|
|
||||||
|
.PHONY: all
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
|
.PHONY: help
|
||||||
|
help: Makefile ## print Makefile help information.
|
||||||
|
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m[TARGETS] default target: build\033[0m\n\n\033[35mTargets:\033[0m\n"} /^[0-9A-Za-z._-]+:.*?##/ { printf " \033[36m%-45s\033[0m %s\n", $$1, $$2 }' Makefile
|
||||||
|
|
||||||
.PHONY: fmt
|
.PHONY: fmt
|
||||||
fmt:
|
fmt: ## format the Go code
|
||||||
$(GO) run $(GOLANGCI_LINT_PACKAGE) fmt
|
$(GO) run $(GOLANGCI_LINT_PACKAGE) fmt
|
||||||
|
|
||||||
.PHONY: go-check
|
.PHONY: go-check
|
||||||
@@ -81,7 +86,7 @@ go-check:
|
|||||||
$(eval MIN_GO_VERSION := $(shell printf "%03d%03d" $(shell echo '$(MIN_GO_VERSION_STR)' | tr '.' ' ')))
|
$(eval MIN_GO_VERSION := $(shell printf "%03d%03d" $(shell echo '$(MIN_GO_VERSION_STR)' | tr '.' ' ')))
|
||||||
$(eval GO_VERSION := $(shell printf "%03d%03d" $(shell $(GO) version | grep -Eo '[0-9]+\.[0-9]+' | tr '.' ' ');))
|
$(eval GO_VERSION := $(shell printf "%03d%03d" $(shell $(GO) version | grep -Eo '[0-9]+\.[0-9]+' | tr '.' ' ');))
|
||||||
@if [ "$(GO_VERSION)" -lt "$(MIN_GO_VERSION)" ]; then \
|
@if [ "$(GO_VERSION)" -lt "$(MIN_GO_VERSION)" ]; then \
|
||||||
echo "Act Runner requires Go $(MIN_GO_VERSION_STR) or greater to build. You can get it at https://go.dev/dl/"; \
|
echo "Gitea Runner requires Go $(MIN_GO_VERSION_STR) or greater to build. You can get it at https://go.dev/dl/"; \
|
||||||
exit 1; \
|
exit 1; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -96,10 +101,14 @@ fmt-check: fmt
|
|||||||
|
|
||||||
.PHONY: deps-tools
|
.PHONY: deps-tools
|
||||||
deps-tools: ## install tool dependencies
|
deps-tools: ## install tool dependencies
|
||||||
$(GO) install $(GOVULNCHECK_PACKAGE)
|
$(GO) install $(GOLANGCI_LINT_PACKAGE) & \
|
||||||
|
$(GO) install $(GXZ_PACKAGE) & \
|
||||||
|
$(GO) install $(XGO_PACKAGE) & \
|
||||||
|
$(GO) install $(GOVULNCHECK_PACKAGE) & \
|
||||||
|
wait
|
||||||
|
|
||||||
.PHONY: lint
|
.PHONY: lint
|
||||||
lint: lint-go
|
lint: lint-go ## lint everything
|
||||||
|
|
||||||
.PHONY: lint-go
|
.PHONY: lint-go
|
||||||
lint-go: ## lint go files
|
lint-go: ## lint go files
|
||||||
@@ -114,58 +123,59 @@ security-check: deps-tools
|
|||||||
GOEXPERIMENT= $(GO) run $(GOVULNCHECK_PACKAGE) -show color ./... || true
|
GOEXPERIMENT= $(GO) run $(GOVULNCHECK_PACKAGE) -show color ./... || true
|
||||||
|
|
||||||
.PHONY: tidy
|
.PHONY: tidy
|
||||||
tidy:
|
tidy: ## run go mod tidy
|
||||||
$(GO) mod tidy
|
$(GO) mod tidy
|
||||||
|
|
||||||
.PHONY: tidy-check
|
.PHONY: tidy-check
|
||||||
tidy-check: tidy
|
tidy-check: tidy
|
||||||
@diff=$$(git diff -- go.mod go.sum); \
|
@diff=$$(git diff --color=always -- go.mod go.sum); \
|
||||||
if [ -n "$$diff" ]; then \
|
if [ -n "$$diff" ]; then \
|
||||||
echo "Please run 'make tidy' and commit the result:"; \
|
echo "Please run 'make tidy' and commit the result:"; \
|
||||||
echo "$${diff}"; \
|
printf "%s" "$${diff}"; \
|
||||||
exit 1; \
|
exit 1; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
test: fmt-check security-check
|
.PHONY: test
|
||||||
|
test: fmt-check security-check ## test everything
|
||||||
@$(GO) test -race -short -v -cover -coverprofile coverage.txt ./... && echo "\n==>\033[32m Ok\033[m\n" || exit 1
|
@$(GO) test -race -short -v -cover -coverprofile coverage.txt ./... && echo "\n==>\033[32m Ok\033[m\n" || exit 1
|
||||||
|
|
||||||
install: $(GOFILES)
|
.PHONY: install
|
||||||
$(GO) install -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)'
|
install: $(GOFILES) ## install the runner binary via `go install`
|
||||||
|
$(GO) install -v -tags '$(TAGS)' -ldflags '-s -w $(EXTLDFLAGS) $(LDFLAGS)'
|
||||||
|
|
||||||
build: go-check $(EXECUTABLE)
|
.PHONY: build
|
||||||
|
build: go-check $(EXECUTABLE) ## build the runner binary
|
||||||
|
|
||||||
$(EXECUTABLE): $(GOFILES)
|
$(EXECUTABLE): $(GOFILES)
|
||||||
$(GO) build -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o $@
|
$(GO) build -v -tags '$(TAGS)' -ldflags '-s -w $(EXTLDFLAGS) $(LDFLAGS)' -o $@
|
||||||
|
|
||||||
.PHONY: deps-backend
|
.PHONY: deps-backend
|
||||||
deps-backend:
|
deps-backend: ## install backend dependencies
|
||||||
$(GO) mod download
|
$(GO) mod download
|
||||||
$(GO) install $(GXZ_PAGAGE)
|
|
||||||
$(GO) install $(XGO_PACKAGE)
|
|
||||||
|
|
||||||
.PHONY: release
|
.PHONY: release
|
||||||
release: release-windows release-linux release-darwin release-copy release-compress release-check
|
release: release-windows release-linux release-darwin release-copy release-compress release-check ## build release artifacts
|
||||||
|
|
||||||
$(DIST_DIRS):
|
$(DIST_DIRS):
|
||||||
mkdir -p $(DIST_DIRS)
|
mkdir -p $(DIST_DIRS)
|
||||||
|
|
||||||
.PHONY: release-windows
|
.PHONY: release-windows
|
||||||
release-windows: | $(DIST_DIRS)
|
release-windows: | $(DIST_DIRS)
|
||||||
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets '$(WINDOWS_ARCHS)' -out $(EXECUTABLE)-$(VERSION) .
|
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w -linkmode external -extldflags "-static" $(LDFLAGS)' -targets '$(WINDOWS_ARCHS)' -out $(EXECUTABLE)-$(VERSION) .
|
||||||
ifeq ($(CI),true)
|
ifeq ($(CI),true)
|
||||||
cp -r /build/* $(DIST)/binaries/
|
cp -r /build/* $(DIST)/binaries/
|
||||||
endif
|
endif
|
||||||
|
|
||||||
.PHONY: release-linux
|
.PHONY: release-linux
|
||||||
release-linux: | $(DIST_DIRS)
|
release-linux: | $(DIST_DIRS)
|
||||||
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets '$(LINUX_ARCHS)' -out $(EXECUTABLE)-$(VERSION) .
|
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w -linkmode external -extldflags "-static" $(LDFLAGS)' -targets '$(LINUX_ARCHS)' -out $(EXECUTABLE)-$(VERSION) .
|
||||||
ifeq ($(CI),true)
|
ifeq ($(CI),true)
|
||||||
cp -r /build/* $(DIST)/binaries/
|
cp -r /build/* $(DIST)/binaries/
|
||||||
endif
|
endif
|
||||||
|
|
||||||
.PHONY: release-darwin
|
.PHONY: release-darwin
|
||||||
release-darwin: | $(DIST_DIRS)
|
release-darwin: | $(DIST_DIRS)
|
||||||
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '$(LDFLAGS)' -targets '$(DARWIN_ARCHS)' -out $(EXECUTABLE)-$(VERSION) .
|
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w $(LDFLAGS)' -targets '$(DARWIN_ARCHS)' -out $(EXECUTABLE)-$(VERSION) .
|
||||||
ifeq ($(CI),true)
|
ifeq ($(CI),true)
|
||||||
cp -r /build/* $(DIST)/binaries/
|
cp -r /build/* $(DIST)/binaries/
|
||||||
endif
|
endif
|
||||||
@@ -180,18 +190,20 @@ release-check: | $(DIST_DIRS)
|
|||||||
|
|
||||||
.PHONY: release-compress
|
.PHONY: release-compress
|
||||||
release-compress: | $(DIST_DIRS)
|
release-compress: | $(DIST_DIRS)
|
||||||
cd $(DIST)/release/; for file in `find . -type f -name "*"`; do echo "compressing $${file}" && $(GO) run $(GXZ_PAGAGE) -k -9 $${file}; done;
|
cd $(DIST)/release/; for file in `find . -type f -name "*"`; do echo "compressing $${file}" && $(GO) run $(GXZ_PACKAGE) -k -9 $${file}; done;
|
||||||
|
|
||||||
.PHONY: docker
|
.PHONY: docker
|
||||||
docker:
|
docker: ## build the docker image
|
||||||
if ! docker buildx version >/dev/null 2>&1; then \
|
if ! docker buildx version >/dev/null 2>&1; then \
|
||||||
ARG_DISABLE_CONTENT_TRUST=--disable-content-trust=false; \
|
ARG_DISABLE_CONTENT_TRUST=--disable-content-trust=false; \
|
||||||
fi; \
|
fi; \
|
||||||
docker build $${ARG_DISABLE_CONTENT_TRUST} -t $(DOCKER_REF) .
|
docker build $${ARG_DISABLE_CONTENT_TRUST} -t $(DOCKER_REF) .
|
||||||
|
|
||||||
clean:
|
.PHONY: clean
|
||||||
|
clean: ## delete binary and coverage files
|
||||||
$(GO) clean -x -i ./...
|
$(GO) clean -x -i ./...
|
||||||
rm -rf coverage.txt $(EXECUTABLE) $(DIST)
|
rm -rf coverage.txt $(EXECUTABLE) $(DIST)
|
||||||
|
|
||||||
version:
|
.PHONY: version
|
||||||
|
version: ## print the version
|
||||||
@echo $(VERSION)
|
@echo $(VERSION)
|
||||||
|
|||||||
69
README.md
69
README.md
@@ -1,6 +1,4 @@
|
|||||||
# act runner
|
# Gitea Runner
|
||||||
|
|
||||||
Act runner is a runner for Gitea.
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -10,7 +8,7 @@ Docker Engine Community version is required for docker mode. To install Docker C
|
|||||||
|
|
||||||
### Download pre-built binary
|
### Download pre-built binary
|
||||||
|
|
||||||
Visit [here](https://dl.gitea.com/act_runner/) and download the right version for your platform.
|
Visit [here](https://dl.gitea.com/gitea-runner/) and download the right version for your platform.
|
||||||
|
|
||||||
### Build from source
|
### Build from source
|
||||||
|
|
||||||
@@ -26,8 +24,8 @@ make docker
|
|||||||
|
|
||||||
## Quickstart
|
## Quickstart
|
||||||
|
|
||||||
Actions are disabled by default, so you need to add the following to the configuration file of your Gitea instance to enable it:
|
Actions are disabled by default, so you need to add the following to the configuration file of your Gitea instance to enable it:
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
[actions]
|
[actions]
|
||||||
ENABLED=true
|
ENABLED=true
|
||||||
@@ -36,7 +34,7 @@ ENABLED=true
|
|||||||
### Register
|
### Register
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./act_runner register
|
./gitea-runner register
|
||||||
```
|
```
|
||||||
|
|
||||||
And you will be asked to input:
|
And you will be asked to input:
|
||||||
@@ -68,7 +66,7 @@ INFO Runner registered successfully.
|
|||||||
You can also register with command line arguments.
|
You can also register with command line arguments.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./act_runner register --instance http://192.168.8.8:3000 --token <my_runner_token> --no-interactive
|
./gitea-runner register --instance http://192.168.8.8:3000 --token <my_runner_token> --no-interactive
|
||||||
```
|
```
|
||||||
|
|
||||||
If the registry succeed, it will run immediately. Next time, you could run the runner directly.
|
If the registry succeed, it will run immediately. Next time, you could run the runner directly.
|
||||||
@@ -76,32 +74,69 @@ If the registry succeed, it will run immediately. Next time, you could run the r
|
|||||||
### Run
|
### Run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./act_runner daemon
|
./gitea-runner daemon
|
||||||
```
|
```
|
||||||
|
|
||||||
### Run with docker
|
### Run with docker
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -e GITEA_INSTANCE_URL=https://your_gitea.com -e GITEA_RUNNER_REGISTRATION_TOKEN=<your_token> -v /var/run/docker.sock:/var/run/docker.sock --name my_runner gitea/act_runner:nightly
|
docker run -e GITEA_INSTANCE_URL=https://your_gitea.com -e GITEA_RUNNER_REGISTRATION_TOKEN=<your_token> -v /var/run/docker.sock:/var/run/docker.sock --name my_runner gitea/runner:nightly
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Mount a volume on `/data` if you want the registration file and optional config to survive container recreation (see [scripts/run.sh](scripts/run.sh)).
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
You can also configure the runner with a configuration file.
|
The runner is configured with a YAML file. Generate a starting point (this matches what ships in the tree):
|
||||||
The configuration file is a YAML file, you can generate a sample configuration file with `./act_runner generate-config`.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./act_runner generate-config > config.yaml
|
./gitea-runner generate-config > config.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
You can specify the configuration file path with `-c`/`--config` argument.
|
Pass it with `-c` / `--config` on any command that loads configuration (`register`, `daemon`, `cache-server`):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./act_runner -c config.yaml register # register with config file
|
./gitea-runner -c config.yaml register
|
||||||
./act_runner -c config.yaml daemon # run with config file
|
./gitea-runner -c config.yaml daemon
|
||||||
|
./gitea-runner -c config.yaml cache-server
|
||||||
```
|
```
|
||||||
|
|
||||||
You can read the latest version of the configuration file online at [config.example.yaml](internal/pkg/config/config.example.yaml).
|
Every option is described in [config.example.yaml](internal/pkg/config/config.example.yaml) (the same content `generate-config` prints).
|
||||||
|
|
||||||
|
#### Without a config file
|
||||||
|
|
||||||
|
If you omit `-c`, built-in defaults apply (same as an empty YAML document). A small set of **deprecated** environment variables can still override parts of that default config, but **only when no `-c` path was given**; they are ignored if you use a config file:
|
||||||
|
|
||||||
|
| Variable | Effect |
|
||||||
|
| --- | --- |
|
||||||
|
| `GITEA_DEBUG` | If true, sets log level to `debug` |
|
||||||
|
| `GITEA_TRACE` | If true, sets log level to `trace` |
|
||||||
|
| `GITEA_RUNNER_CAPACITY` | Concurrent jobs (integer) |
|
||||||
|
| `GITEA_RUNNER_FILE` | Registration state file path (default `.runner`) |
|
||||||
|
| `GITEA_RUNNER_ENVIRON` | Extra job env vars as comma-separated `KEY:VALUE` pairs |
|
||||||
|
| `GITEA_RUNNER_ENV_FILE` | Path to an env file merged into job env (same idea as `runner.env_file` in YAML) |
|
||||||
|
|
||||||
|
Prefer a YAML file for all settings.
|
||||||
|
|
||||||
|
#### Registration vs config labels
|
||||||
|
|
||||||
|
If `runner.labels` is set in the YAML file, those labels are used during `register` and the `--labels` CLI flag is ignored.
|
||||||
|
|
||||||
|
#### External cache (`actions/cache`)
|
||||||
|
|
||||||
|
If `cache.external_server` is set, you must set `cache.external_secret` to the same value on this runner and on the standalone cache server. Run the server with `gitea-runner cache-server` using a config that defines `cache.external_secret` (and matching `cache.dir` / host / port as needed). Flags `--dir`, `--host`, and `--port` on `cache-server` override the file.
|
||||||
|
|
||||||
|
#### Official Docker image
|
||||||
|
|
||||||
|
Besides `GITEA_INSTANCE_URL` and `GITEA_RUNNER_REGISTRATION_TOKEN`, the image entrypoint supports optional variables such as `CONFIG_FILE` (passed through as `-c`), `GITEA_RUNNER_LABELS`, `GITEA_RUNNER_EPHEMERAL`, `GITEA_RUNNER_ONCE`, `GITEA_RUNNER_NAME`, `GITEA_MAX_REG_ATTEMPTS`, `RUNNER_STATE_FILE`, and `GITEA_RUNNER_REGISTRATION_TOKEN_FILE`. See [scripts/run.sh](scripts/run.sh) for exact behavior.
|
||||||
|
|
||||||
|
For a fuller container-oriented walkthrough, see [examples/docker](examples/docker/README.md).
|
||||||
|
|
||||||
|
When `container.bind_workdir` is enabled, stale task workspace directories can be cleaned while the runner is idle:
|
||||||
|
- directories older than `runner.workdir_cleanup_age` are removed (default: `24h`; set `0` to disable)
|
||||||
|
- cleanup runs every `runner.idle_cleanup_interval` (default: `10m`; set `0` to disable)
|
||||||
|
- only purely numeric subdirectories under `container.workdir_parent` are treated as task workspaces and may be removed
|
||||||
|
- cleanup assumes `container.workdir_parent` is not shared across multiple runners
|
||||||
|
|
||||||
### Example Deployments
|
### Example Deployments
|
||||||
|
|
||||||
|
|||||||
@@ -5,21 +5,28 @@
|
|||||||
package artifactcache
|
package artifactcache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
|
||||||
"github.com/julienschmidt/httprouter"
|
"github.com/julienschmidt/httprouter"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@@ -28,9 +35,36 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
urlBase = "/_apis/artifactcache"
|
apiPath = "/_apis/artifactcache"
|
||||||
|
internalPath = "/_internal"
|
||||||
|
|
||||||
|
// artifactURLTTL bounds how long a signed artifactLocation URL stays valid.
|
||||||
|
// Short enough that a leaked URL is near-worthless; long enough to let the
|
||||||
|
// @actions/cache client download a big blob that was returned from /cache.
|
||||||
|
artifactURLTTL = 10 * time.Minute
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type credKey struct{}
|
||||||
|
|
||||||
|
// JobCredential ties a per-job bearer token (ACTIONS_RUNTIME_TOKEN) to the
|
||||||
|
// repository that owns it. Every cache entry is stamped with Repo on
|
||||||
|
// reserve/commit and checked on read/write so one repo can never observe or
|
||||||
|
// poison another repo's cache, even from inside a container that reaches the
|
||||||
|
// cache server over the docker bridge network.
|
||||||
|
type JobCredential struct {
|
||||||
|
Repo string
|
||||||
|
}
|
||||||
|
|
||||||
|
// credEntry holds a registered job's credential along with an active
|
||||||
|
// registration count. RegisterJob is reference-counted so that if two tasks
|
||||||
|
// briefly share an ACTIONS_RUNTIME_TOKEN — e.g. a runner that retries a task
|
||||||
|
// after a crash before the old registration is revoked — the first task's
|
||||||
|
// revoker does not cut the second task's auth out from under it.
|
||||||
|
type credEntry struct {
|
||||||
|
cred JobCredential
|
||||||
|
refs int
|
||||||
|
}
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
dir string
|
dir string
|
||||||
storage *Storage
|
storage *Storage
|
||||||
@@ -43,10 +77,36 @@ type Handler struct {
|
|||||||
gcAt time.Time
|
gcAt time.Time
|
||||||
|
|
||||||
outboundIP string
|
outboundIP string
|
||||||
|
|
||||||
|
// internalSecret guards /_internal/{register,revoke}. When set, a remote
|
||||||
|
// runner can use these endpoints to pre-register per-job
|
||||||
|
// ACTIONS_RUNTIME_TOKENs against this server, enabling the same
|
||||||
|
// per-job auth and repo scoping as the embedded handler over the
|
||||||
|
// network. Empty disables the control-plane entirely.
|
||||||
|
internalSecret string
|
||||||
|
|
||||||
|
// secret signs short-lived artifact download URLs. The @actions/cache
|
||||||
|
// toolkit does not send Authorization on the download request, so blob
|
||||||
|
// GETs authenticate via a per-URL HMAC signature with expiry rather than
|
||||||
|
// via the bearer token used for management endpoints.
|
||||||
|
secret []byte
|
||||||
|
|
||||||
|
credMu sync.RWMutex
|
||||||
|
creds map[string]*credEntry
|
||||||
}
|
}
|
||||||
|
|
||||||
func StartHandler(dir, outboundIP string, port uint16, logger logrus.FieldLogger) (*Handler, error) {
|
// StartHandler opens the on-disk cache store and starts the HTTP server.
|
||||||
h := &Handler{}
|
//
|
||||||
|
// internalSecret, when non-empty, enables a control-plane API at
|
||||||
|
// /_internal/{register,revoke} that lets a remote runner pre-register the
|
||||||
|
// per-job ACTIONS_RUNTIME_TOKENs it expects this server to honor. The
|
||||||
|
// embedded in-process handler leaves it empty and registers tokens via the
|
||||||
|
// in-process RegisterJob method directly.
|
||||||
|
func StartHandler(dir, outboundIP string, port uint16, internalSecret string, logger logrus.FieldLogger) (*Handler, error) {
|
||||||
|
h := &Handler{
|
||||||
|
creds: make(map[string]*credEntry),
|
||||||
|
internalSecret: internalSecret,
|
||||||
|
}
|
||||||
|
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
discard := logrus.New()
|
discard := logrus.New()
|
||||||
@@ -83,19 +143,37 @@ func StartHandler(dir, outboundIP string, port uint16, logger logrus.FieldLogger
|
|||||||
h.outboundIP = ip.String()
|
h.outboundIP = ip.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
secret, err := loadOrCreateSecret(dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
h.secret = secret
|
||||||
|
|
||||||
router := httprouter.New()
|
router := httprouter.New()
|
||||||
router.GET(urlBase+"/cache", h.middleware(h.find))
|
router.GET(apiPath+"/cache", h.bearerAuth(h.find))
|
||||||
router.POST(urlBase+"/caches", h.middleware(h.reserve))
|
router.POST(apiPath+"/caches", h.bearerAuth(h.reserve))
|
||||||
router.PATCH(urlBase+"/caches/:id", h.middleware(h.upload))
|
router.PATCH(apiPath+"/caches/:id", h.bearerAuth(h.upload))
|
||||||
router.POST(urlBase+"/caches/:id", h.middleware(h.commit))
|
router.POST(apiPath+"/caches/:id", h.bearerAuth(h.commit))
|
||||||
router.GET(urlBase+"/artifacts/:id", h.middleware(h.get))
|
router.POST(apiPath+"/clean", h.bearerAuth(h.clean))
|
||||||
router.POST(urlBase+"/clean", h.middleware(h.clean))
|
// Artifact GET is signed via query-string HMAC because @actions/cache
|
||||||
|
// does not attach Authorization when downloading archiveLocation.
|
||||||
|
router.GET(apiPath+"/artifacts/:id", h.signedURLAuth(h.get))
|
||||||
|
// Control-plane: a remote runner registers/revokes per-job tokens so the
|
||||||
|
// cache API can authenticate them. Always wired so the routes exist; the
|
||||||
|
// handlers themselves 401 when internalSecret is unset.
|
||||||
|
router.POST(internalPath+"/register", h.internalAuth(h.internalRegister))
|
||||||
|
router.POST(internalPath+"/revoke", h.internalAuth(h.internalRevoke))
|
||||||
|
|
||||||
h.router = router
|
h.router = router
|
||||||
|
|
||||||
h.gcCache()
|
h.gcCache()
|
||||||
|
|
||||||
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) // listen on all interfaces
|
// Listen on all interfaces. Binding to outboundIP only would give no real
|
||||||
|
// security benefit (it is the LAN/internet-facing address either way) and
|
||||||
|
// can break Docker Desktop variants where the host's outbound IP is not
|
||||||
|
// routable from inside the container network. Authentication is enforced
|
||||||
|
// by the bearer middleware and per-repo scoping, not by reachability.
|
||||||
|
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -121,6 +199,91 @@ func (h *Handler) ExternalURL() string {
|
|||||||
h.listener.Addr().(*net.TCPAddr).Port)
|
h.listener.Addr().(*net.TCPAddr).Port)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RegisterJob makes token a valid bearer credential for cache requests from
|
||||||
|
// the given repository and returns a function that removes it. The runner
|
||||||
|
// calls this at job start and defers the returned func so that the credential
|
||||||
|
// is only accepted while the job is running.
|
||||||
|
//
|
||||||
|
// Registrations are reference-counted: if a token is already registered, the
|
||||||
|
// existing repo is kept and the refcount is incremented. The entry is
|
||||||
|
// removed only when every revoker returned by RegisterJob has been called.
|
||||||
|
// This keeps a stray re-registration from silently revoking a live job.
|
||||||
|
func (h *Handler) RegisterJob(token, repo string) func() {
|
||||||
|
if h == nil || token == "" {
|
||||||
|
return func() {}
|
||||||
|
}
|
||||||
|
h.credMu.Lock()
|
||||||
|
if existing, ok := h.creds[token]; ok {
|
||||||
|
existing.refs++
|
||||||
|
} else {
|
||||||
|
h.creds[token] = &credEntry{
|
||||||
|
cred: JobCredential{Repo: repo},
|
||||||
|
refs: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h.credMu.Unlock()
|
||||||
|
return func() {
|
||||||
|
h.credMu.Lock()
|
||||||
|
if entry, ok := h.creds[token]; ok {
|
||||||
|
entry.refs--
|
||||||
|
if entry.refs <= 0 {
|
||||||
|
delete(h.creds, token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h.credMu.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RevokeJob explicitly revokes one registration of token, mirroring one call
|
||||||
|
// of the closure returned by RegisterJob. Used by the control-plane endpoint
|
||||||
|
// so a remote runner can revoke without holding the closure.
|
||||||
|
func (h *Handler) RevokeJob(token string) {
|
||||||
|
if h == nil || token == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.credMu.Lock()
|
||||||
|
if entry, ok := h.creds[token]; ok {
|
||||||
|
entry.refs--
|
||||||
|
if entry.refs <= 0 {
|
||||||
|
delete(h.creds, token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h.credMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) lookupCredential(token string) (JobCredential, bool) {
|
||||||
|
h.credMu.RLock()
|
||||||
|
entry, ok := h.creds[token]
|
||||||
|
h.credMu.RUnlock()
|
||||||
|
if !ok {
|
||||||
|
return JobCredential{}, false
|
||||||
|
}
|
||||||
|
return entry.cred, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadOrCreateSecret returns the 32-byte HMAC signing key for artifact URLs,
|
||||||
|
// persisted in dir/.secret so signed URLs handed out before a restart stay
|
||||||
|
// valid across the restart and so the standalone cache-server can be pointed
|
||||||
|
// at by config.Cache.ExternalServer without the URL rotating.
|
||||||
|
func loadOrCreateSecret(dir string) ([]byte, error) {
|
||||||
|
path := filepath.Join(dir, ".secret")
|
||||||
|
if data, err := os.ReadFile(path); err == nil {
|
||||||
|
if secret, err := hex.DecodeString(strings.TrimSpace(string(data))); err == nil && len(secret) >= 32 {
|
||||||
|
return secret, nil
|
||||||
|
}
|
||||||
|
} else if !os.IsNotExist(err) {
|
||||||
|
return nil, fmt.Errorf("read cache secret: %w", err)
|
||||||
|
}
|
||||||
|
secret := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(secret); err != nil {
|
||||||
|
return nil, fmt.Errorf("generate cache secret: %w", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(path, []byte(hex.EncodeToString(secret)), 0o600); err != nil {
|
||||||
|
return nil, fmt.Errorf("write cache secret: %w", err)
|
||||||
|
}
|
||||||
|
return secret, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) Close() error {
|
func (h *Handler) Close() error {
|
||||||
if h == nil {
|
if h == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -160,6 +323,7 @@ func (h *Handler) openDB() (*bolthold.Store, error) {
|
|||||||
|
|
||||||
// GET /_apis/artifactcache/cache
|
// GET /_apis/artifactcache/cache
|
||||||
func (h *Handler) find(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
func (h *Handler) find(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||||
|
cred := credFromContext(r.Context())
|
||||||
keys := strings.Split(r.URL.Query().Get("keys"), ",")
|
keys := strings.Split(r.URL.Query().Get("keys"), ",")
|
||||||
// cache keys are case insensitive
|
// cache keys are case insensitive
|
||||||
for i, key := range keys {
|
for i, key := range keys {
|
||||||
@@ -174,7 +338,7 @@ func (h *Handler) find(w http.ResponseWriter, r *http.Request, _ httprouter.Para
|
|||||||
}
|
}
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
cache, err := findCache(db, keys, version)
|
cache, err := findCache(db, cred.Repo, keys, version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.responseJSON(w, r, 500, err)
|
h.responseJSON(w, r, 500, err)
|
||||||
return
|
return
|
||||||
@@ -194,13 +358,14 @@ func (h *Handler) find(w http.ResponseWriter, r *http.Request, _ httprouter.Para
|
|||||||
}
|
}
|
||||||
h.responseJSON(w, r, 200, map[string]any{
|
h.responseJSON(w, r, 200, map[string]any{
|
||||||
"result": "hit",
|
"result": "hit",
|
||||||
"archiveLocation": fmt.Sprintf("%s%s/artifacts/%d", h.ExternalURL(), urlBase, cache.ID),
|
"archiveLocation": h.signedArtifactURL(cache.ID, time.Now().Add(artifactURLTTL)),
|
||||||
"cacheKey": cache.Key,
|
"cacheKey": cache.Key,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /_apis/artifactcache/caches
|
// POST /_apis/artifactcache/caches
|
||||||
func (h *Handler) reserve(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
func (h *Handler) reserve(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||||
|
cred := credFromContext(r.Context())
|
||||||
api := &Request{}
|
api := &Request{}
|
||||||
if err := json.NewDecoder(r.Body).Decode(api); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(api); err != nil {
|
||||||
h.responseJSON(w, r, 400, err)
|
h.responseJSON(w, r, 400, err)
|
||||||
@@ -210,6 +375,7 @@ func (h *Handler) reserve(w http.ResponseWriter, r *http.Request, _ httprouter.P
|
|||||||
api.Key = strings.ToLower(api.Key)
|
api.Key = strings.ToLower(api.Key)
|
||||||
|
|
||||||
cache := api.ToCache()
|
cache := api.ToCache()
|
||||||
|
cache.Repo = cred.Repo
|
||||||
db, err := h.openDB()
|
db, err := h.openDB()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.responseJSON(w, r, 500, err)
|
h.responseJSON(w, r, 500, err)
|
||||||
@@ -231,6 +397,7 @@ func (h *Handler) reserve(w http.ResponseWriter, r *http.Request, _ httprouter.P
|
|||||||
|
|
||||||
// PATCH /_apis/artifactcache/caches/:id
|
// PATCH /_apis/artifactcache/caches/:id
|
||||||
func (h *Handler) upload(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
func (h *Handler) upload(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
||||||
|
cred := credFromContext(r.Context())
|
||||||
id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
|
id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.responseJSON(w, r, 400, err)
|
h.responseJSON(w, r, 400, err)
|
||||||
@@ -253,6 +420,11 @@ func (h *Handler) upload(w http.ResponseWriter, r *http.Request, params httprout
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cache.Repo != cred.Repo {
|
||||||
|
h.responseJSON(w, r, 403, fmt.Errorf("cache %d: forbidden", id))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if cache.Complete {
|
if cache.Complete {
|
||||||
h.responseJSON(w, r, 400, fmt.Errorf("cache %v %q: already complete", cache.ID, cache.Key))
|
h.responseJSON(w, r, 400, fmt.Errorf("cache %v %q: already complete", cache.ID, cache.Key))
|
||||||
return
|
return
|
||||||
@@ -272,6 +444,7 @@ func (h *Handler) upload(w http.ResponseWriter, r *http.Request, params httprout
|
|||||||
|
|
||||||
// POST /_apis/artifactcache/caches/:id
|
// POST /_apis/artifactcache/caches/:id
|
||||||
func (h *Handler) commit(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
func (h *Handler) commit(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
||||||
|
cred := credFromContext(r.Context())
|
||||||
id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
|
id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.responseJSON(w, r, 400, err)
|
h.responseJSON(w, r, 400, err)
|
||||||
@@ -294,6 +467,11 @@ func (h *Handler) commit(w http.ResponseWriter, r *http.Request, params httprout
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cache.Repo != cred.Repo {
|
||||||
|
h.responseJSON(w, r, 403, fmt.Errorf("cache %d: forbidden", id))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if cache.Complete {
|
if cache.Complete {
|
||||||
h.responseJSON(w, r, 400, fmt.Errorf("cache %v %q: already complete", cache.ID, cache.Key))
|
h.responseJSON(w, r, 400, fmt.Errorf("cache %v %q: already complete", cache.ID, cache.Key))
|
||||||
return
|
return
|
||||||
@@ -326,6 +504,10 @@ func (h *Handler) commit(w http.ResponseWriter, r *http.Request, params httprout
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GET /_apis/artifactcache/artifacts/:id
|
// GET /_apis/artifactcache/artifacts/:id
|
||||||
|
// Authenticated via signed URL (see signedURLAuth), not bearer, because the
|
||||||
|
// @actions/cache toolkit downloads archiveLocation without Authorization.
|
||||||
|
// Repository scoping is already enforced at find() time; the signature binds
|
||||||
|
// the URL to the specific cache ID and an expiry.
|
||||||
func (h *Handler) get(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
func (h *Handler) get(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
||||||
id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
|
id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -344,21 +526,158 @@ func (h *Handler) clean(w http.ResponseWriter, r *http.Request, _ httprouter.Par
|
|||||||
h.responseJSON(w, r, 200)
|
h.responseJSON(w, r, 200)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) middleware(handler httprouter.Handle) httprouter.Handle {
|
// bearerAuth resolves ACTIONS_RUNTIME_TOKEN against the set of currently
|
||||||
|
// registered jobs. A match attaches the job's JobCredential to the request
|
||||||
|
// context; a miss returns 401 before the handler body runs.
|
||||||
|
func (h *Handler) bearerAuth(handler httprouter.Handle) httprouter.Handle {
|
||||||
return func(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
return func(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
||||||
h.logger.Debugf("%s %s", r.Method, r.RequestURI)
|
h.logger.Debugf("%s %s", r.Method, r.URL.Path)
|
||||||
|
token := bearerToken(r)
|
||||||
|
if token == "" {
|
||||||
|
h.responseJSON(w, r, http.StatusUnauthorized, errors.New("missing bearer token"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cred, ok := h.lookupCredential(token)
|
||||||
|
if !ok {
|
||||||
|
h.responseJSON(w, r, http.StatusUnauthorized, errors.New("unknown bearer token"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx := context.WithValue(r.Context(), credKey{}, cred)
|
||||||
|
handler(w, r.WithContext(ctx), params)
|
||||||
|
go h.gcCache()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) signedURLAuth(handler httprouter.Handle) httprouter.Handle {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
||||||
|
h.logger.Debugf("%s %s", r.Method, r.URL.Path)
|
||||||
|
id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
h.responseJSON(w, r, 400, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
expStr := r.URL.Query().Get("exp")
|
||||||
|
sig := r.URL.Query().Get("sig")
|
||||||
|
if expStr == "" || sig == "" {
|
||||||
|
h.responseJSON(w, r, http.StatusUnauthorized, errors.New("missing signature"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
exp, err := strconv.ParseInt(expStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
h.responseJSON(w, r, http.StatusUnauthorized, errors.New("invalid expiry"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if time.Now().Unix() > exp {
|
||||||
|
h.responseJSON(w, r, http.StatusUnauthorized, errors.New("signature expired"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
expected := h.computeSignature(id, exp)
|
||||||
|
if !hmac.Equal([]byte(sig), []byte(expected)) {
|
||||||
|
h.responseJSON(w, r, http.StatusUnauthorized, errors.New("bad signature"))
|
||||||
|
return
|
||||||
|
}
|
||||||
handler(w, r, params)
|
handler(w, r, params)
|
||||||
go h.gcCache()
|
go h.gcCache()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// internalAuth gates the control-plane endpoints. The bearer must
|
||||||
|
// constant-time-equal the configured internalSecret. If the secret is empty,
|
||||||
|
// the control-plane is disabled and every request gets 404 — which matches
|
||||||
|
// the upstream nektos/act behavior of "the route does not exist".
|
||||||
|
func (h *Handler) internalAuth(handler httprouter.Handle) httprouter.Handle {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
||||||
|
if h.internalSecret == "" {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
token := bearerToken(r)
|
||||||
|
if token == "" || !hmac.Equal([]byte(token), []byte(h.internalSecret)) {
|
||||||
|
h.responseJSON(w, r, http.StatusUnauthorized, errors.New("internal: bad secret"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
handler(w, r, params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type internalRegisterBody struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
Repo string `json:"repo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type internalRevokeBody struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /_internal/register
|
||||||
|
func (h *Handler) internalRegister(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||||
|
var body internalRegisterBody
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
h.responseJSON(w, r, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if body.Token == "" {
|
||||||
|
h.responseJSON(w, r, http.StatusBadRequest, errors.New("token is required"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.RegisterJob(body.Token, body.Repo)
|
||||||
|
h.responseJSON(w, r, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /_internal/revoke
|
||||||
|
func (h *Handler) internalRevoke(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||||
|
var body internalRevokeBody
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
h.responseJSON(w, r, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if body.Token == "" {
|
||||||
|
h.responseJSON(w, r, http.StatusBadRequest, errors.New("token is required"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.RevokeJob(body.Token)
|
||||||
|
h.responseJSON(w, r, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func bearerToken(r *http.Request) string {
|
||||||
|
auth := r.Header.Get("Authorization")
|
||||||
|
const prefix = "Bearer "
|
||||||
|
if len(auth) > len(prefix) && strings.EqualFold(auth[:len(prefix)], prefix) {
|
||||||
|
return auth[len(prefix):]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func credFromContext(ctx context.Context) JobCredential {
|
||||||
|
if cred, ok := ctx.Value(credKey{}).(JobCredential); ok {
|
||||||
|
return cred
|
||||||
|
}
|
||||||
|
return JobCredential{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) computeSignature(cacheID, exp int64) string {
|
||||||
|
mac := hmac.New(sha256.New, h.secret)
|
||||||
|
fmt.Fprintf(mac, "%d:%d", cacheID, exp)
|
||||||
|
return hex.EncodeToString(mac.Sum(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) signedArtifactURL(cacheID uint64, exp time.Time) string {
|
||||||
|
expUnix := exp.Unix()
|
||||||
|
sig := h.computeSignature(int64(cacheID), expUnix)
|
||||||
|
q := url.Values{}
|
||||||
|
q.Set("exp", strconv.FormatInt(expUnix, 10))
|
||||||
|
q.Set("sig", sig)
|
||||||
|
return fmt.Sprintf("%s%s/artifacts/%d?%s", h.ExternalURL(), apiPath, cacheID, q.Encode())
|
||||||
|
}
|
||||||
|
|
||||||
// if not found, return (nil, nil) instead of an error.
|
// if not found, return (nil, nil) instead of an error.
|
||||||
func findCache(db *bolthold.Store, keys []string, version string) (*Cache, error) {
|
func findCache(db *bolthold.Store, repo string, keys []string, version string) (*Cache, error) {
|
||||||
cache := &Cache{}
|
cache := &Cache{}
|
||||||
for _, prefix := range keys {
|
for _, prefix := range keys {
|
||||||
// if a key in the list matches exactly, don't return partial matches
|
// if a key in the list matches exactly, don't return partial matches
|
||||||
if err := db.FindOne(cache,
|
if err := db.FindOne(cache,
|
||||||
bolthold.Where("Key").Eq(prefix).
|
bolthold.Where("Repo").Eq(repo).
|
||||||
|
And("Key").Eq(prefix).
|
||||||
And("Version").Eq(version).
|
And("Version").Eq(version).
|
||||||
And("Complete").Eq(true).
|
And("Complete").Eq(true).
|
||||||
SortBy("CreatedAt").Reverse()); err == nil || !errors.Is(err, bolthold.ErrNotFound) {
|
SortBy("CreatedAt").Reverse()); err == nil || !errors.Is(err, bolthold.ErrNotFound) {
|
||||||
@@ -373,7 +692,8 @@ func findCache(db *bolthold.Store, keys []string, version string) (*Cache, error
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := db.FindOne(cache,
|
if err := db.FindOne(cache,
|
||||||
bolthold.Where("Key").RegExp(re).
|
bolthold.Where("Repo").Eq(repo).
|
||||||
|
And("Key").RegExp(re).
|
||||||
And("Version").Eq(version).
|
And("Version").Eq(version).
|
||||||
And("Complete").Eq(true).
|
And("Complete").Eq(true).
|
||||||
SortBy("CreatedAt").Reverse()); err != nil {
|
SortBy("CreatedAt").Reverse()); err != nil {
|
||||||
@@ -419,7 +739,6 @@ const (
|
|||||||
keepOld = 5 * time.Minute
|
keepOld = 5 * time.Minute
|
||||||
)
|
)
|
||||||
|
|
||||||
//nolint:gocyclo // function handles many cases
|
|
||||||
func (h *Handler) gcCache() {
|
func (h *Handler) gcCache() {
|
||||||
if h.gcing.Load() {
|
if h.gcing.Load() {
|
||||||
return
|
return
|
||||||
@@ -494,12 +813,16 @@ func (h *Handler) gcCache() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the old caches with the same key and version, keep the latest one.
|
// Remove the old caches with the same key and version within the same
|
||||||
|
// repository, keep the latest one. Aggregation must include Repo so two
|
||||||
|
// repos that happen to share a (key, version) do not evict each other —
|
||||||
|
// otherwise per-repo scoping holds for reads but one repo can age
|
||||||
|
// another out after keepOld.
|
||||||
// Also keep the olds which have been used recently for a while in case of the cache is still in use.
|
// Also keep the olds which have been used recently for a while in case of the cache is still in use.
|
||||||
if results, err := db.FindAggregate(
|
if results, err := db.FindAggregate(
|
||||||
&Cache{},
|
&Cache{},
|
||||||
bolthold.Where("Complete").Eq(true),
|
bolthold.Where("Complete").Eq(true),
|
||||||
"Key", "Version",
|
"Repo", "Key", "Version",
|
||||||
); err != nil {
|
); err != nil {
|
||||||
h.logger.Warnf("find aggregate caches: %v", err)
|
h.logger.Warnf("find aggregate caches: %v", err)
|
||||||
} else {
|
} else {
|
||||||
@@ -533,7 +856,7 @@ func (h *Handler) responseJSON(w http.ResponseWriter, r *http.Request, code int,
|
|||||||
if len(v) == 0 || v[0] == nil {
|
if len(v) == 0 || v[0] == nil {
|
||||||
data, _ = json.Marshal(struct{}{})
|
data, _ = json.Marshal(struct{}{})
|
||||||
} else if err, ok := v[0].(error); ok {
|
} else if err, ok := v[0].(error); ok {
|
||||||
h.logger.Errorf("%v %v: %v", r.Method, r.RequestURI, err)
|
h.logger.Errorf("%v %v: %v", r.Method, r.URL.Path, err)
|
||||||
data, _ = json.Marshal(map[string]any{
|
data, _ = json.Marshal(map[string]any{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -22,12 +22,38 @@ import (
|
|||||||
"go.etcd.io/bbolt"
|
"go.etcd.io/bbolt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// testToken is registered with the cache server in every test that needs to
|
||||||
|
// make authenticated requests; testClient then attaches it as the
|
||||||
|
// Authorization: Bearer header. testRepo is the repository scope used when
|
||||||
|
// registering it; cross-repo isolation is exercised in its own test.
|
||||||
|
const (
|
||||||
|
testToken = "test-runtime-token"
|
||||||
|
testRepo = "owner/repo"
|
||||||
|
)
|
||||||
|
|
||||||
|
type bearerTransport struct{ token string }
|
||||||
|
|
||||||
|
func (b *bearerTransport) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||||
|
r.Header.Set("Authorization", "Bearer "+b.token)
|
||||||
|
return http.DefaultTransport.RoundTrip(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
var testClient = &http.Client{Transport: &bearerTransport{token: testToken}}
|
||||||
|
|
||||||
|
// signArtifactURL builds a signed download URL the same way the server does;
|
||||||
|
// tests use it to reach the get handler directly without going through a
|
||||||
|
// find/cache-hit round trip.
|
||||||
|
func signArtifactURL(h *Handler, id int64) string {
|
||||||
|
return h.signedArtifactURL(uint64(id), time.Now().Add(artifactURLTTL))
|
||||||
|
}
|
||||||
|
|
||||||
func TestHandler(t *testing.T) {
|
func TestHandler(t *testing.T) {
|
||||||
dir := filepath.Join(t.TempDir(), "artifactcache")
|
dir := filepath.Join(t.TempDir(), "artifactcache")
|
||||||
handler, err := StartHandler(dir, "", 0, nil)
|
handler, err := StartHandler(dir, "", 0, "", nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
handler.RegisterJob(testToken, testRepo)
|
||||||
|
|
||||||
base := fmt.Sprintf("%s%s", handler.ExternalURL(), urlBase)
|
base := fmt.Sprintf("%s%s", handler.ExternalURL(), apiPath)
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
t.Run("inpect db", func(t *testing.T) {
|
t.Run("inpect db", func(t *testing.T) {
|
||||||
@@ -45,7 +71,10 @@ func TestHandler(t *testing.T) {
|
|||||||
require.NoError(t, handler.Close())
|
require.NoError(t, handler.Close())
|
||||||
assert.Nil(t, handler.server)
|
assert.Nil(t, handler.server)
|
||||||
assert.Nil(t, handler.listener)
|
assert.Nil(t, handler.listener)
|
||||||
_, err := http.Post(fmt.Sprintf("%s/caches/%d", base, 1), "", nil) //nolint:bodyclose // pre-existing issue from nektos/act
|
resp, err := testClient.Post(fmt.Sprintf("%s/caches/%d", base, 1), "", nil)
|
||||||
|
if err == nil {
|
||||||
|
resp.Body.Close()
|
||||||
|
}
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
})
|
})
|
||||||
}()
|
}()
|
||||||
@@ -53,8 +82,9 @@ func TestHandler(t *testing.T) {
|
|||||||
t.Run("get not exist", func(t *testing.T) {
|
t.Run("get not exist", func(t *testing.T) {
|
||||||
key := strings.ToLower(t.Name())
|
key := strings.ToLower(t.Name())
|
||||||
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
|
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
|
||||||
resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, key, version)) //nolint:bodyclose // pre-existing issue from nektos/act
|
resp, err := testClient.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, key, version))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
require.Equal(t, 204, resp.StatusCode)
|
require.Equal(t, 204, resp.StatusCode)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -68,16 +98,18 @@ func TestHandler(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("clean", func(t *testing.T) {
|
t.Run("clean", func(t *testing.T) {
|
||||||
resp, err := http.Post(base+"/clean", "", nil) //nolint:bodyclose // pre-existing issue from nektos/act
|
resp, err := testClient.Post(base+"/clean", "", nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
assert.Equal(t, 200, resp.StatusCode)
|
assert.Equal(t, 200, resp.StatusCode)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("reserve with bad request", func(t *testing.T) {
|
t.Run("reserve with bad request", func(t *testing.T) {
|
||||||
body := []byte(`invalid json`)
|
body := []byte(`invalid json`)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
resp, err := http.Post(base+"/caches", "application/json", bytes.NewReader(body)) //nolint:bodyclose // pre-existing issue from nektos/act
|
resp, err := testClient.Post(base+"/caches", "application/json", bytes.NewReader(body))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
assert.Equal(t, 400, resp.StatusCode)
|
assert.Equal(t, 400, resp.StatusCode)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -94,8 +126,9 @@ func TestHandler(t *testing.T) {
|
|||||||
Size: 100,
|
Size: 100,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
resp, err := http.Post(base+"/caches", "application/json", bytes.NewReader(body)) //nolint:bodyclose // pre-existing issue from nektos/act
|
resp, err := testClient.Post(base+"/caches", "application/json", bytes.NewReader(body))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
assert.Equal(t, 200, resp.StatusCode)
|
assert.Equal(t, 200, resp.StatusCode)
|
||||||
|
|
||||||
require.NoError(t, json.NewDecoder(resp.Body).Decode(&first))
|
require.NoError(t, json.NewDecoder(resp.Body).Decode(&first))
|
||||||
@@ -108,8 +141,9 @@ func TestHandler(t *testing.T) {
|
|||||||
Size: 100,
|
Size: 100,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
resp, err := http.Post(base+"/caches", "application/json", bytes.NewReader(body)) //nolint:bodyclose // pre-existing issue from nektos/act
|
resp, err := testClient.Post(base+"/caches", "application/json", bytes.NewReader(body))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
assert.Equal(t, 200, resp.StatusCode)
|
assert.Equal(t, 200, resp.StatusCode)
|
||||||
|
|
||||||
require.NoError(t, json.NewDecoder(resp.Body).Decode(&second))
|
require.NoError(t, json.NewDecoder(resp.Body).Decode(&second))
|
||||||
@@ -125,8 +159,9 @@ func TestHandler(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
req.Header.Set("Content-Type", "application/octet-stream")
|
req.Header.Set("Content-Type", "application/octet-stream")
|
||||||
req.Header.Set("Content-Range", "bytes 0-99/*")
|
req.Header.Set("Content-Range", "bytes 0-99/*")
|
||||||
resp, err := http.DefaultClient.Do(req) //nolint:bodyclose // pre-existing issue from nektos/act
|
resp, err := testClient.Do(req)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
assert.Equal(t, 400, resp.StatusCode)
|
assert.Equal(t, 400, resp.StatusCode)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -136,8 +171,9 @@ func TestHandler(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
req.Header.Set("Content-Type", "application/octet-stream")
|
req.Header.Set("Content-Type", "application/octet-stream")
|
||||||
req.Header.Set("Content-Range", "bytes 0-99/*")
|
req.Header.Set("Content-Range", "bytes 0-99/*")
|
||||||
resp, err := http.DefaultClient.Do(req) //nolint:bodyclose // pre-existing issue from nektos/act
|
resp, err := testClient.Do(req)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
assert.Equal(t, 400, resp.StatusCode)
|
assert.Equal(t, 400, resp.StatusCode)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -155,8 +191,9 @@ func TestHandler(t *testing.T) {
|
|||||||
Size: 100,
|
Size: 100,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
resp, err := http.Post(base+"/caches", "application/json", bytes.NewReader(body)) //nolint:bodyclose // pre-existing issue from nektos/act
|
resp, err := testClient.Post(base+"/caches", "application/json", bytes.NewReader(body))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
assert.Equal(t, 200, resp.StatusCode)
|
assert.Equal(t, 200, resp.StatusCode)
|
||||||
|
|
||||||
got := struct {
|
got := struct {
|
||||||
@@ -171,13 +208,15 @@ func TestHandler(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
req.Header.Set("Content-Type", "application/octet-stream")
|
req.Header.Set("Content-Type", "application/octet-stream")
|
||||||
req.Header.Set("Content-Range", "bytes 0-99/*")
|
req.Header.Set("Content-Range", "bytes 0-99/*")
|
||||||
resp, err := http.DefaultClient.Do(req) //nolint:bodyclose // pre-existing issue from nektos/act
|
resp, err := testClient.Do(req)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
assert.Equal(t, 200, resp.StatusCode)
|
assert.Equal(t, 200, resp.StatusCode)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil) //nolint:bodyclose // pre-existing issue from nektos/act
|
resp, err := testClient.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
assert.Equal(t, 200, resp.StatusCode)
|
assert.Equal(t, 200, resp.StatusCode)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
@@ -186,8 +225,9 @@ func TestHandler(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
req.Header.Set("Content-Type", "application/octet-stream")
|
req.Header.Set("Content-Type", "application/octet-stream")
|
||||||
req.Header.Set("Content-Range", "bytes 0-99/*")
|
req.Header.Set("Content-Range", "bytes 0-99/*")
|
||||||
resp, err := http.DefaultClient.Do(req) //nolint:bodyclose // pre-existing issue from nektos/act
|
resp, err := testClient.Do(req)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
assert.Equal(t, 400, resp.StatusCode)
|
assert.Equal(t, 400, resp.StatusCode)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -206,8 +246,9 @@ func TestHandler(t *testing.T) {
|
|||||||
Size: 100,
|
Size: 100,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
resp, err := http.Post(base+"/caches", "application/json", bytes.NewReader(body)) //nolint:bodyclose // pre-existing issue from nektos/act
|
resp, err := testClient.Post(base+"/caches", "application/json", bytes.NewReader(body))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
assert.Equal(t, 200, resp.StatusCode)
|
assert.Equal(t, 200, resp.StatusCode)
|
||||||
|
|
||||||
got := struct {
|
got := struct {
|
||||||
@@ -222,24 +263,27 @@ func TestHandler(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
req.Header.Set("Content-Type", "application/octet-stream")
|
req.Header.Set("Content-Type", "application/octet-stream")
|
||||||
req.Header.Set("Content-Range", "bytes xx-99/*")
|
req.Header.Set("Content-Range", "bytes xx-99/*")
|
||||||
resp, err := http.DefaultClient.Do(req) //nolint:bodyclose // pre-existing issue from nektos/act
|
resp, err := testClient.Do(req)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
assert.Equal(t, 400, resp.StatusCode)
|
assert.Equal(t, 400, resp.StatusCode)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("commit with bad id", func(t *testing.T) {
|
t.Run("commit with bad id", func(t *testing.T) {
|
||||||
{
|
{
|
||||||
resp, err := http.Post(base+"/caches/invalid_id", "", nil) //nolint:bodyclose // pre-existing issue from nektos/act
|
resp, err := testClient.Post(base+"/caches/invalid_id", "", nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
assert.Equal(t, 400, resp.StatusCode)
|
assert.Equal(t, 400, resp.StatusCode)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("commit with not exist id", func(t *testing.T) {
|
t.Run("commit with not exist id", func(t *testing.T) {
|
||||||
{
|
{
|
||||||
resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, 100), "", nil) //nolint:bodyclose // pre-existing issue from nektos/act
|
resp, err := testClient.Post(fmt.Sprintf("%s/caches/%d", base, 100), "", nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
assert.Equal(t, 400, resp.StatusCode)
|
assert.Equal(t, 400, resp.StatusCode)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -258,8 +302,9 @@ func TestHandler(t *testing.T) {
|
|||||||
Size: 100,
|
Size: 100,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
resp, err := http.Post(base+"/caches", "application/json", bytes.NewReader(body)) //nolint:bodyclose // pre-existing issue from nektos/act
|
resp, err := testClient.Post(base+"/caches", "application/json", bytes.NewReader(body))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
assert.Equal(t, 200, resp.StatusCode)
|
assert.Equal(t, 200, resp.StatusCode)
|
||||||
|
|
||||||
got := struct {
|
got := struct {
|
||||||
@@ -274,18 +319,21 @@ func TestHandler(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
req.Header.Set("Content-Type", "application/octet-stream")
|
req.Header.Set("Content-Type", "application/octet-stream")
|
||||||
req.Header.Set("Content-Range", "bytes 0-99/*")
|
req.Header.Set("Content-Range", "bytes 0-99/*")
|
||||||
resp, err := http.DefaultClient.Do(req) //nolint:bodyclose // pre-existing issue from nektos/act
|
resp, err := testClient.Do(req)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
assert.Equal(t, 200, resp.StatusCode)
|
assert.Equal(t, 200, resp.StatusCode)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil) //nolint:bodyclose // pre-existing issue from nektos/act
|
resp, err := testClient.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
assert.Equal(t, 200, resp.StatusCode)
|
assert.Equal(t, 200, resp.StatusCode)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil) //nolint:bodyclose // pre-existing issue from nektos/act
|
resp, err := testClient.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
assert.Equal(t, 400, resp.StatusCode)
|
assert.Equal(t, 400, resp.StatusCode)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -304,8 +352,9 @@ func TestHandler(t *testing.T) {
|
|||||||
Size: 100,
|
Size: 100,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
resp, err := http.Post(base+"/caches", "application/json", bytes.NewReader(body)) //nolint:bodyclose // pre-existing issue from nektos/act
|
resp, err := testClient.Post(base+"/caches", "application/json", bytes.NewReader(body))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
assert.Equal(t, 200, resp.StatusCode)
|
assert.Equal(t, 200, resp.StatusCode)
|
||||||
|
|
||||||
got := struct {
|
got := struct {
|
||||||
@@ -320,32 +369,37 @@ func TestHandler(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
req.Header.Set("Content-Type", "application/octet-stream")
|
req.Header.Set("Content-Type", "application/octet-stream")
|
||||||
req.Header.Set("Content-Range", "bytes 0-59/*")
|
req.Header.Set("Content-Range", "bytes 0-59/*")
|
||||||
resp, err := http.DefaultClient.Do(req) //nolint:bodyclose // pre-existing issue from nektos/act
|
resp, err := testClient.Do(req)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
assert.Equal(t, 200, resp.StatusCode)
|
assert.Equal(t, 200, resp.StatusCode)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil) //nolint:bodyclose // pre-existing issue from nektos/act
|
resp, err := testClient.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
assert.Equal(t, 500, resp.StatusCode)
|
assert.Equal(t, 500, resp.StatusCode)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("get with bad id", func(t *testing.T) {
|
t.Run("get with bad id", func(t *testing.T) {
|
||||||
resp, err := http.Get(base + "/artifacts/invalid_id") //nolint:bodyclose // pre-existing issue from nektos/act
|
resp, err := testClient.Get(base + "/artifacts/invalid_id")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
require.Equal(t, 400, resp.StatusCode)
|
require.Equal(t, 400, resp.StatusCode)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("get with not exist id", func(t *testing.T) {
|
t.Run("get with not exist id", func(t *testing.T) {
|
||||||
resp, err := http.Get(fmt.Sprintf("%s/artifacts/%d", base, 100)) //nolint:bodyclose // pre-existing issue from nektos/act
|
resp, err := testClient.Get(signArtifactURL(handler, 100))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
require.Equal(t, 404, resp.StatusCode)
|
require.Equal(t, 404, resp.StatusCode)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("get with not exist id", func(t *testing.T) {
|
t.Run("get with not exist id", func(t *testing.T) {
|
||||||
resp, err := http.Get(fmt.Sprintf("%s/artifacts/%d", base, 100)) //nolint:bodyclose // pre-existing issue from nektos/act
|
resp, err := testClient.Get(signArtifactURL(handler, 100))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
require.Equal(t, 404, resp.StatusCode)
|
require.Equal(t, 404, resp.StatusCode)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -375,8 +429,9 @@ func TestHandler(t *testing.T) {
|
|||||||
key + "_a",
|
key + "_a",
|
||||||
}, ",")
|
}, ",")
|
||||||
|
|
||||||
resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKeys, version)) //nolint:bodyclose // pre-existing issue from nektos/act
|
resp, err := testClient.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKeys, version))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
require.Equal(t, 200, resp.StatusCode)
|
require.Equal(t, 200, resp.StatusCode)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -395,8 +450,9 @@ func TestHandler(t *testing.T) {
|
|||||||
assert.Equal(t, "hit", got.Result)
|
assert.Equal(t, "hit", got.Result)
|
||||||
assert.Equal(t, keys[except], got.CacheKey)
|
assert.Equal(t, keys[except], got.CacheKey)
|
||||||
|
|
||||||
contentResp, err := http.Get(got.ArchiveLocation) //nolint:bodyclose // pre-existing issue from nektos/act
|
contentResp, err := testClient.Get(got.ArchiveLocation)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer contentResp.Body.Close()
|
||||||
require.Equal(t, 200, contentResp.StatusCode)
|
require.Equal(t, 200, contentResp.StatusCode)
|
||||||
content, err := io.ReadAll(contentResp.Body)
|
content, err := io.ReadAll(contentResp.Body)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -413,8 +469,9 @@ func TestHandler(t *testing.T) {
|
|||||||
|
|
||||||
{
|
{
|
||||||
reqKey := key + "_aBc"
|
reqKey := key + "_aBc"
|
||||||
resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKey, version)) //nolint:bodyclose // pre-existing issue from nektos/act
|
resp, err := testClient.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKey, version))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
require.Equal(t, 200, resp.StatusCode)
|
require.Equal(t, 200, resp.StatusCode)
|
||||||
got := struct {
|
got := struct {
|
||||||
Result string `json:"result"`
|
Result string `json:"result"`
|
||||||
@@ -452,8 +509,9 @@ func TestHandler(t *testing.T) {
|
|||||||
key + "_a_b",
|
key + "_a_b",
|
||||||
}, ",")
|
}, ",")
|
||||||
|
|
||||||
resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKeys, version)) //nolint:bodyclose // pre-existing issue from nektos/act
|
resp, err := testClient.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKeys, version))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
require.Equal(t, 200, resp.StatusCode)
|
require.Equal(t, 200, resp.StatusCode)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -470,8 +528,9 @@ func TestHandler(t *testing.T) {
|
|||||||
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
|
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
|
||||||
assert.Equal(t, keys[expect], got.CacheKey)
|
assert.Equal(t, keys[expect], got.CacheKey)
|
||||||
|
|
||||||
contentResp, err := http.Get(got.ArchiveLocation) //nolint:bodyclose // pre-existing issue from nektos/act
|
contentResp, err := testClient.Get(got.ArchiveLocation)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer contentResp.Body.Close()
|
||||||
require.Equal(t, 200, contentResp.StatusCode)
|
require.Equal(t, 200, contentResp.StatusCode)
|
||||||
content, err := io.ReadAll(contentResp.Body)
|
content, err := io.ReadAll(contentResp.Body)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -504,8 +563,9 @@ func TestHandler(t *testing.T) {
|
|||||||
key + "_a_b",
|
key + "_a_b",
|
||||||
}, ",")
|
}, ",")
|
||||||
|
|
||||||
resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKeys, version)) //nolint:bodyclose // pre-existing issue from nektos/act
|
resp, err := testClient.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKeys, version))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
require.Equal(t, 200, resp.StatusCode)
|
require.Equal(t, 200, resp.StatusCode)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -523,8 +583,9 @@ func TestHandler(t *testing.T) {
|
|||||||
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
|
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
|
||||||
assert.Equal(t, keys[expect], got.CacheKey)
|
assert.Equal(t, keys[expect], got.CacheKey)
|
||||||
|
|
||||||
contentResp, err := http.Get(got.ArchiveLocation) //nolint:bodyclose // pre-existing issue from nektos/act
|
contentResp, err := testClient.Get(got.ArchiveLocation)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer contentResp.Body.Close()
|
||||||
require.Equal(t, 200, contentResp.StatusCode)
|
require.Equal(t, 200, contentResp.StatusCode)
|
||||||
content, err := io.ReadAll(contentResp.Body)
|
content, err := io.ReadAll(contentResp.Body)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -541,8 +602,9 @@ func uploadCacheNormally(t *testing.T, base, key, version string, content []byte
|
|||||||
Size: int64(len(content)),
|
Size: int64(len(content)),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
resp, err := http.Post(base+"/caches", "application/json", bytes.NewReader(body)) //nolint:bodyclose // pre-existing issue from nektos/act
|
resp, err := testClient.Post(base+"/caches", "application/json", bytes.NewReader(body))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
assert.Equal(t, 200, resp.StatusCode)
|
assert.Equal(t, 200, resp.StatusCode)
|
||||||
|
|
||||||
got := struct {
|
got := struct {
|
||||||
@@ -557,19 +619,22 @@ func uploadCacheNormally(t *testing.T, base, key, version string, content []byte
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
req.Header.Set("Content-Type", "application/octet-stream")
|
req.Header.Set("Content-Type", "application/octet-stream")
|
||||||
req.Header.Set("Content-Range", "bytes 0-99/*")
|
req.Header.Set("Content-Range", "bytes 0-99/*")
|
||||||
resp, err := http.DefaultClient.Do(req) //nolint:bodyclose // pre-existing issue from nektos/act
|
resp, err := testClient.Do(req)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
assert.Equal(t, 200, resp.StatusCode)
|
assert.Equal(t, 200, resp.StatusCode)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil) //nolint:bodyclose // pre-existing issue from nektos/act
|
resp, err := testClient.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
assert.Equal(t, 200, resp.StatusCode)
|
assert.Equal(t, 200, resp.StatusCode)
|
||||||
}
|
}
|
||||||
var archiveLocation string
|
var archiveLocation string
|
||||||
{
|
{
|
||||||
resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, key, version)) //nolint:bodyclose // pre-existing issue from nektos/act
|
resp, err := testClient.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, key, version))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
require.Equal(t, 200, resp.StatusCode)
|
require.Equal(t, 200, resp.StatusCode)
|
||||||
got := struct {
|
got := struct {
|
||||||
Result string `json:"result"`
|
Result string `json:"result"`
|
||||||
@@ -582,8 +647,9 @@ func uploadCacheNormally(t *testing.T, base, key, version string, content []byte
|
|||||||
archiveLocation = got.ArchiveLocation
|
archiveLocation = got.ArchiveLocation
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
resp, err := http.Get(archiveLocation) //nolint:bodyclose // pre-existing issue from nektos/act
|
resp, err := testClient.Get(archiveLocation)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
require.Equal(t, 200, resp.StatusCode)
|
require.Equal(t, 200, resp.StatusCode)
|
||||||
got, err := io.ReadAll(resp.Body)
|
got, err := io.ReadAll(resp.Body)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -593,7 +659,7 @@ func uploadCacheNormally(t *testing.T, base, key, version string, content []byte
|
|||||||
|
|
||||||
func TestHandler_gcCache(t *testing.T) {
|
func TestHandler_gcCache(t *testing.T) {
|
||||||
dir := filepath.Join(t.TempDir(), "artifactcache")
|
dir := filepath.Join(t.TempDir(), "artifactcache")
|
||||||
handler, err := StartHandler(dir, "", 0, nil)
|
handler, err := StartHandler(dir, "", 0, "", nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -699,3 +765,421 @@ func TestHandler_gcCache(t *testing.T) {
|
|||||||
}
|
}
|
||||||
require.NoError(t, db.Close())
|
require.NoError(t, db.Close())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestHandler_RejectsMissingBearer covers the advisory's root cause:
|
||||||
|
// unauthenticated access to management endpoints is now refused with 401.
|
||||||
|
func TestHandler_RejectsMissingBearer(t *testing.T) {
|
||||||
|
dir := filepath.Join(t.TempDir(), "artifactcache")
|
||||||
|
handler, err := StartHandler(dir, "", 0, "", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer handler.Close()
|
||||||
|
|
||||||
|
base := handler.ExternalURL() + apiPath
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
method string
|
||||||
|
path string
|
||||||
|
body string
|
||||||
|
}{
|
||||||
|
{"find", http.MethodGet, "/cache?keys=x&version=y", ""},
|
||||||
|
{"reserve", http.MethodPost, "/caches", "{}"},
|
||||||
|
{"upload", http.MethodPatch, "/caches/1", ""},
|
||||||
|
{"commit", http.MethodPost, "/caches/1", ""},
|
||||||
|
{"clean", http.MethodPost, "/clean", ""},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
req, err := http.NewRequest(tc.method, base+tc.path, strings.NewReader(tc.body))
|
||||||
|
require.NoError(t, err)
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
resp.Body.Close()
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHandler_RejectsUnknownBearer verifies that a bearer token is only
|
||||||
|
// accepted after RegisterJob; stale/forged tokens cannot be replayed.
|
||||||
|
func TestHandler_RejectsUnknownBearer(t *testing.T) {
|
||||||
|
dir := filepath.Join(t.TempDir(), "artifactcache")
|
||||||
|
handler, err := StartHandler(dir, "", 0, "", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer handler.Close()
|
||||||
|
|
||||||
|
base := handler.ExternalURL() + apiPath
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, base+"/cache?keys=x&version=y", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
req.Header.Set("Authorization", "Bearer not-a-registered-token")
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
resp.Body.Close()
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHandler_UnregisterRevokes ensures that the function returned by
|
||||||
|
// RegisterJob invalidates the credential, so a token leaked at job time stops
|
||||||
|
// working the moment the job ends instead of living for the runner's lifetime.
|
||||||
|
func TestHandler_UnregisterRevokes(t *testing.T) {
|
||||||
|
dir := filepath.Join(t.TempDir(), "artifactcache")
|
||||||
|
handler, err := StartHandler(dir, "", 0, "", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer handler.Close()
|
||||||
|
|
||||||
|
unregister := handler.RegisterJob("tmp-token", testRepo)
|
||||||
|
|
||||||
|
base := handler.ExternalURL() + apiPath
|
||||||
|
req, err := http.NewRequest(http.MethodGet, base+"/cache?keys=x&version=y", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
req.Header.Set("Authorization", "Bearer tmp-token")
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
resp.Body.Close()
|
||||||
|
assert.NotEqual(t, http.StatusUnauthorized, resp.StatusCode)
|
||||||
|
|
||||||
|
unregister()
|
||||||
|
|
||||||
|
resp, err = http.DefaultClient.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
resp.Body.Close()
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHandler_CrossRepoIsolation addresses the intra-runner poisoning vector
|
||||||
|
// raised in GHSA-82g9-637c-2fx2: job containers can reach the cache server
|
||||||
|
// over the docker bridge, so IP allowlisting alone does not stop a malicious
|
||||||
|
// PR run from another repo. A cache entry created under repoA must be
|
||||||
|
// invisible to queries scoped to repoB.
|
||||||
|
func TestHandler_CrossRepoIsolation(t *testing.T) {
|
||||||
|
dir := filepath.Join(t.TempDir(), "artifactcache")
|
||||||
|
handler, err := StartHandler(dir, "", 0, "", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer handler.Close()
|
||||||
|
handler.RegisterJob("token-a", "owner/repoA")
|
||||||
|
handler.RegisterJob("token-b", "owner/repoB")
|
||||||
|
|
||||||
|
base := handler.ExternalURL() + apiPath
|
||||||
|
key := "shared-key"
|
||||||
|
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
|
||||||
|
content := []byte("repoA-payload")
|
||||||
|
|
||||||
|
clientA := &http.Client{Transport: &bearerTransport{token: "token-a"}}
|
||||||
|
clientB := &http.Client{Transport: &bearerTransport{token: "token-b"}}
|
||||||
|
|
||||||
|
// repoA reserves + uploads + commits.
|
||||||
|
reserveBody, err := json.Marshal(&Request{Key: key, Version: version, Size: int64(len(content))})
|
||||||
|
require.NoError(t, err)
|
||||||
|
resp, err := clientA.Post(base+"/caches", "application/json", bytes.NewReader(reserveBody))
|
||||||
|
require.NoError(t, err)
|
||||||
|
var reserved struct {
|
||||||
|
CacheID uint64 `json:"cacheId"`
|
||||||
|
}
|
||||||
|
require.NoError(t, json.NewDecoder(resp.Body).Decode(&reserved))
|
||||||
|
resp.Body.Close()
|
||||||
|
require.NotZero(t, reserved.CacheID)
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPatch, fmt.Sprintf("%s/caches/%d", base, reserved.CacheID), bytes.NewReader(content))
|
||||||
|
require.NoError(t, err)
|
||||||
|
req.Header.Set("Content-Range", fmt.Sprintf("bytes 0-%d/*", len(content)-1))
|
||||||
|
resp, err = clientA.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
resp.Body.Close()
|
||||||
|
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
resp, err = clientA.Post(fmt.Sprintf("%s/caches/%d", base, reserved.CacheID), "", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
resp.Body.Close()
|
||||||
|
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
// repoB with a matching key and version must NOT see repoA's cache.
|
||||||
|
resp, err = clientB.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, key, version))
|
||||||
|
require.NoError(t, err)
|
||||||
|
resp.Body.Close()
|
||||||
|
assert.Equal(t, http.StatusNoContent, resp.StatusCode)
|
||||||
|
|
||||||
|
// repoA still sees its own cache.
|
||||||
|
resp, err = clientA.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, key, version))
|
||||||
|
require.NoError(t, err)
|
||||||
|
resp.Body.Close()
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
// repoB cannot upload to repoA's reserved id either (forbidden, not 401).
|
||||||
|
req, err = http.NewRequest(http.MethodPatch, fmt.Sprintf("%s/caches/%d", base, reserved.CacheID), bytes.NewReader([]byte("poison")))
|
||||||
|
require.NoError(t, err)
|
||||||
|
req.Header.Set("Content-Range", "bytes 0-5/*")
|
||||||
|
resp, err = clientB.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
resp.Body.Close()
|
||||||
|
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHandler_ArtifactSignature verifies that archive downloads reject
|
||||||
|
// missing / tampered / expired signatures, so a leaked archiveLocation stops
|
||||||
|
// working after artifactURLTTL even if the bearer token is still registered.
|
||||||
|
func TestHandler_ArtifactSignature(t *testing.T) {
|
||||||
|
dir := filepath.Join(t.TempDir(), "artifactcache")
|
||||||
|
handler, err := StartHandler(dir, "", 0, "", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer handler.Close()
|
||||||
|
handler.RegisterJob(testToken, testRepo)
|
||||||
|
|
||||||
|
base := handler.ExternalURL() + apiPath
|
||||||
|
|
||||||
|
t.Run("missing signature", func(t *testing.T) {
|
||||||
|
resp, err := testClient.Get(fmt.Sprintf("%s/artifacts/%d", base, 1))
|
||||||
|
require.NoError(t, err)
|
||||||
|
resp.Body.Close()
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("tampered signature", func(t *testing.T) {
|
||||||
|
good := handler.signedArtifactURL(1, time.Now().Add(artifactURLTTL))
|
||||||
|
bad := good[:len(good)-4] + "dead"
|
||||||
|
resp, err := testClient.Get(bad)
|
||||||
|
require.NoError(t, err)
|
||||||
|
resp.Body.Close()
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("expired signature", func(t *testing.T) {
|
||||||
|
expired := handler.signedArtifactURL(1, time.Now().Add(-time.Second))
|
||||||
|
resp, err := testClient.Get(expired)
|
||||||
|
require.NoError(t, err)
|
||||||
|
resp.Body.Close()
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("signature from a different server", func(t *testing.T) {
|
||||||
|
dir2 := filepath.Join(t.TempDir(), "artifactcache2")
|
||||||
|
other, err := StartHandler(dir2, "", 0, "", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer other.Close()
|
||||||
|
otherURL := other.signedArtifactURL(1, time.Now().Add(artifactURLTTL))
|
||||||
|
// Rewrite the host so the request still lands on our handler, but
|
||||||
|
// the signature was computed with a different secret.
|
||||||
|
parts := strings.SplitN(otherURL, apiPath, 2)
|
||||||
|
forged := base + parts[1]
|
||||||
|
resp, err := testClient.Get(forged)
|
||||||
|
require.NoError(t, err)
|
||||||
|
resp.Body.Close()
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHandler_SecretPersistsAcrossRestarts is the property that lets
|
||||||
|
// gitea-runner cache-server be pointed at via cfg.Cache.ExternalServer: a
|
||||||
|
// restart must not invalidate signed URLs the handler has already issued
|
||||||
|
// (within their expiry window).
|
||||||
|
func TestHandler_SecretPersistsAcrossRestarts(t *testing.T) {
|
||||||
|
dir := filepath.Join(t.TempDir(), "artifactcache")
|
||||||
|
|
||||||
|
first, err := StartHandler(dir, "127.0.0.1", 0, "", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
exp := time.Now().Add(artifactURLTTL).Unix()
|
||||||
|
sig := first.computeSignature(42, exp)
|
||||||
|
require.NoError(t, first.Close())
|
||||||
|
|
||||||
|
second, err := StartHandler(dir, "127.0.0.1", 0, "", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer second.Close()
|
||||||
|
|
||||||
|
assert.Equal(t, sig, second.computeSignature(42, exp))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHandler_ArtifactSignatureDownload is a happy-path round trip that
|
||||||
|
// ensures a real reserve/upload/commit/find/download flow still works after
|
||||||
|
// the auth refactor.
|
||||||
|
func TestHandler_ArtifactSignatureDownload(t *testing.T) {
|
||||||
|
dir := filepath.Join(t.TempDir(), "artifactcache")
|
||||||
|
handler, err := StartHandler(dir, "", 0, "", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer handler.Close()
|
||||||
|
handler.RegisterJob(testToken, testRepo)
|
||||||
|
|
||||||
|
base := handler.ExternalURL() + apiPath
|
||||||
|
key := "download-key"
|
||||||
|
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
|
||||||
|
content := []byte("hello")
|
||||||
|
uploadCacheNormally(t, base, key, version, content)
|
||||||
|
|
||||||
|
resp, err := testClient.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, key, version))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
var hit struct {
|
||||||
|
ArchiveLocation string `json:"archiveLocation"`
|
||||||
|
}
|
||||||
|
require.NoError(t, json.NewDecoder(resp.Body).Decode(&hit))
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
|
require.Contains(t, hit.ArchiveLocation, "sig=")
|
||||||
|
require.Contains(t, hit.ArchiveLocation, "exp=")
|
||||||
|
|
||||||
|
// Download without any Authorization header — the signature alone must
|
||||||
|
// be enough, because @actions/cache downloads archiveLocation unauth'd.
|
||||||
|
dl, err := http.Get(hit.ArchiveLocation)
|
||||||
|
require.NoError(t, err)
|
||||||
|
body, err := io.ReadAll(dl.Body)
|
||||||
|
dl.Body.Close()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, http.StatusOK, dl.StatusCode)
|
||||||
|
assert.Equal(t, content, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHandler_RegisterJob_RefCounted verifies that a duplicate RegisterJob
|
||||||
|
// for the same token does not silently revoke the first registration on the
|
||||||
|
// first revoker call. This matters if a runner ever re-registers a token
|
||||||
|
// (restart mid-task, retry), which must not kill the live job's auth.
|
||||||
|
func TestHandler_RegisterJob_RefCounted(t *testing.T) {
|
||||||
|
dir := filepath.Join(t.TempDir(), "artifactcache")
|
||||||
|
handler, err := StartHandler(dir, "", 0, "", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer handler.Close()
|
||||||
|
|
||||||
|
first := handler.RegisterJob("shared", testRepo)
|
||||||
|
second := handler.RegisterJob("shared", testRepo)
|
||||||
|
|
||||||
|
base := handler.ExternalURL() + apiPath
|
||||||
|
probe := func() int {
|
||||||
|
req, err := http.NewRequest(http.MethodGet, base+"/cache?keys=x&version=v", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
req.Header.Set("Authorization", "Bearer shared")
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
resp.Body.Close()
|
||||||
|
return resp.StatusCode
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NotEqual(t, http.StatusUnauthorized, probe())
|
||||||
|
first()
|
||||||
|
assert.NotEqual(t, http.StatusUnauthorized, probe(),
|
||||||
|
"token must stay valid while another registration holds the refcount")
|
||||||
|
second()
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, probe(),
|
||||||
|
"token is revoked only after every revoker has run")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHandler_GC_PerRepoDedup ensures duplicate-pruning does not evict
|
||||||
|
// another repo's entry. Two repos reserve the same (key, version); after the
|
||||||
|
// keepOld window, GC must keep the one from each repo.
|
||||||
|
func TestHandler_GC_PerRepoDedup(t *testing.T) {
|
||||||
|
dir := filepath.Join(t.TempDir(), "artifactcache")
|
||||||
|
handler, err := StartHandler(dir, "", 0, "", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer handler.Close()
|
||||||
|
handler.RegisterJob("tok-a", "owner/repoA")
|
||||||
|
handler.RegisterJob("tok-b", "owner/repoB")
|
||||||
|
|
||||||
|
key := "shared-dedup-key"
|
||||||
|
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
|
||||||
|
|
||||||
|
// Seed one completed cache per repo directly via the DB, bypassing the
|
||||||
|
// HTTP round trip so we can precisely control UsedAt.
|
||||||
|
db, err := handler.openDB()
|
||||||
|
require.NoError(t, err)
|
||||||
|
now := time.Now().Unix()
|
||||||
|
stale := time.Now().Add(-keepOld - time.Minute).Unix()
|
||||||
|
a := &Cache{Repo: "owner/repoA", Key: key, Version: version, Complete: true, CreatedAt: stale, UsedAt: stale, Size: 1}
|
||||||
|
b := &Cache{Repo: "owner/repoB", Key: key, Version: version, Complete: true, CreatedAt: now, UsedAt: now, Size: 1}
|
||||||
|
require.NoError(t, insertCache(db, a))
|
||||||
|
require.NoError(t, insertCache(db, b))
|
||||||
|
// Write the backing blobs so the dedup deletion has something to remove.
|
||||||
|
require.NoError(t, handler.storage.Write(a.ID, 0, strings.NewReader("a")))
|
||||||
|
_, err = handler.storage.Commit(a.ID, 1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, handler.storage.Write(b.ID, 0, strings.NewReader("b")))
|
||||||
|
_, err = handler.storage.Commit(b.ID, 1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, db.Close())
|
||||||
|
|
||||||
|
// Force GC to run regardless of the cooldown.
|
||||||
|
handler.gcAt = time.Time{}
|
||||||
|
handler.gcCache()
|
||||||
|
|
||||||
|
db, err = handler.openDB()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
var after []Cache
|
||||||
|
require.NoError(t, db.Find(&after, bolthold.Where("Key").Eq(key).And("Version").Eq(version)))
|
||||||
|
|
||||||
|
repos := make(map[string]bool)
|
||||||
|
for _, c := range after {
|
||||||
|
repos[c.Repo] = true
|
||||||
|
}
|
||||||
|
assert.True(t, repos["owner/repoA"], "repoA's cache must survive dedup against repoB")
|
||||||
|
assert.True(t, repos["owner/repoB"], "repoB's cache must survive dedup against repoA")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHandler_InternalAPI_Disabled verifies that without an internalSecret
|
||||||
|
// the control-plane routes are 404 — operators can't accidentally hit
|
||||||
|
// register/revoke when the feature is off.
|
||||||
|
func TestHandler_InternalAPI_Disabled(t *testing.T) {
|
||||||
|
dir := filepath.Join(t.TempDir(), "artifactcache")
|
||||||
|
handler, err := StartHandler(dir, "", 0, "", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer handler.Close()
|
||||||
|
|
||||||
|
for _, ep := range []string{"/_internal/register", "/_internal/revoke"} {
|
||||||
|
resp, err := http.Post(handler.ExternalURL()+ep, "application/json", strings.NewReader(`{}`))
|
||||||
|
require.NoError(t, err)
|
||||||
|
resp.Body.Close()
|
||||||
|
assert.Equal(t, http.StatusNotFound, resp.StatusCode, ep)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHandler_InternalAPI_AuthAndUsage covers the control-plane: bad/missing
|
||||||
|
// secret → 401, malformed body → 400, happy path round-trips a token through
|
||||||
|
// register → cache-API accepts it → revoke → cache-API rejects it.
|
||||||
|
func TestHandler_InternalAPI_AuthAndUsage(t *testing.T) {
|
||||||
|
dir := filepath.Join(t.TempDir(), "artifactcache")
|
||||||
|
const secret = "internal-secret"
|
||||||
|
handler, err := StartHandler(dir, "", 0, secret, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer handler.Close()
|
||||||
|
|
||||||
|
base := handler.ExternalURL()
|
||||||
|
|
||||||
|
post := func(path, bearer, body string) int {
|
||||||
|
req, err := http.NewRequest(http.MethodPost, base+path, strings.NewReader(body))
|
||||||
|
require.NoError(t, err)
|
||||||
|
if bearer != "" {
|
||||||
|
req.Header.Set("Authorization", "Bearer "+bearer)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
resp.Body.Close()
|
||||||
|
return resp.StatusCode
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("missing secret 401", func(t *testing.T) {
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, post("/_internal/register", "", `{"token":"x","repo":"r"}`))
|
||||||
|
})
|
||||||
|
t.Run("wrong secret 401", func(t *testing.T) {
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, post("/_internal/register", "wrong", `{"token":"x","repo":"r"}`))
|
||||||
|
})
|
||||||
|
t.Run("malformed body 400", func(t *testing.T) {
|
||||||
|
assert.Equal(t, http.StatusBadRequest, post("/_internal/register", secret, `not json`))
|
||||||
|
})
|
||||||
|
t.Run("missing token 400", func(t *testing.T) {
|
||||||
|
assert.Equal(t, http.StatusBadRequest, post("/_internal/register", secret, `{"repo":"r"}`))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("register then revoke round-trip", func(t *testing.T) {
|
||||||
|
probe := func(token string) int {
|
||||||
|
req, _ := http.NewRequest(http.MethodGet, base+apiPath+"/cache?keys=k&version=v", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
resp.Body.Close()
|
||||||
|
return resp.StatusCode
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, probe("via-internal-api"))
|
||||||
|
assert.Equal(t, http.StatusOK, post("/_internal/register", secret, `{"token":"via-internal-api","repo":"owner/repo"}`))
|
||||||
|
assert.NotEqual(t, http.StatusUnauthorized, probe("via-internal-api"))
|
||||||
|
assert.Equal(t, http.StatusOK, post("/_internal/revoke", secret, `{"token":"via-internal-api"}`))
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, probe("via-internal-api"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ func (c *Request) ToCache() *Cache {
|
|||||||
|
|
||||||
type Cache struct {
|
type Cache struct {
|
||||||
ID uint64 `json:"id" boltholdKey:"ID"`
|
ID uint64 `json:"id" boltholdKey:"ID"`
|
||||||
|
Repo string `json:"repo" boltholdIndex:"Repo"`
|
||||||
Key string `json:"key" boltholdIndex:"Key"`
|
Key string `json:"key" boltholdIndex:"Key"`
|
||||||
Version string `json:"version" boltholdIndex:"Version"`
|
Version string `json:"version" boltholdIndex:"Version"`
|
||||||
Size int64 `json:"cacheSize"`
|
Size int64 `json:"cacheSize"`
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
|
||||||
"github.com/julienschmidt/httprouter"
|
"github.com/julienschmidt/httprouter"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"testing/fstest"
|
"testing/fstest"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
"gitea.com/gitea/act_runner/act/runner"
|
"gitea.com/gitea/runner/act/runner"
|
||||||
|
|
||||||
"github.com/julienschmidt/httprouter"
|
"github.com/julienschmidt/httprouter"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@@ -202,7 +202,7 @@ func TestListArtifactContainer(t *testing.T) {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(1, len(response.Value)) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.Len(response.Value, 1)
|
||||||
assert.Equal("some/file", response.Value[0].Path)
|
assert.Equal("some/file", response.Value[0].Path)
|
||||||
assert.Equal("file", response.Value[0].ItemType)
|
assert.Equal("file", response.Value[0].ItemType)
|
||||||
assert.Equal("http://localhost/artifact/1/some/file/.", response.Value[0].ContentLocation)
|
assert.Equal("http://localhost/artifact/1/some/file/.", response.Value[0].ContentLocation)
|
||||||
@@ -283,7 +283,7 @@ func runTestJobFile(ctx context.Context, t *testing.T, tjfi TestJobFileInfo) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
workdir, err := filepath.Abs(tjfi.workdir)
|
workdir, err := filepath.Abs(tjfi.workdir)
|
||||||
assert.Nil(t, err, workdir) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err, workdir) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
fullWorkflowPath := filepath.Join(workdir, tjfi.workflowPath)
|
fullWorkflowPath := filepath.Join(workdir, tjfi.workflowPath)
|
||||||
runnerConfig := &runner.Config{
|
runnerConfig := &runner.Config{
|
||||||
Workdir: workdir,
|
Workdir: workdir,
|
||||||
@@ -299,16 +299,16 @@ func runTestJobFile(ctx context.Context, t *testing.T, tjfi TestJobFileInfo) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
runner, err := runner.New(runnerConfig)
|
runner, err := runner.New(runnerConfig)
|
||||||
assert.Nil(t, err, tjfi.workflowPath) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err, tjfi.workflowPath) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
planner, err := model.NewWorkflowPlanner(fullWorkflowPath, true)
|
planner, err := model.NewWorkflowPlanner(fullWorkflowPath, true)
|
||||||
assert.Nil(t, err, fullWorkflowPath) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err, fullWorkflowPath) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
plan, err := planner.PlanEvent(tjfi.eventName)
|
plan, err := planner.PlanEvent(tjfi.eventName)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = runner.NewPlanExecutor(plan)(ctx)
|
err = runner.NewPlanExecutor(plan)(ctx)
|
||||||
if tjfi.errorMessage == "" {
|
if tjfi.errorMessage == "" {
|
||||||
assert.Nil(t, err, fullWorkflowPath) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err, fullWorkflowPath) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
} else {
|
} else {
|
||||||
assert.Error(t, err, tjfi.errorMessage) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.Error(t, err, tjfi.errorMessage) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ func TestCartesianProduct(t *testing.T) {
|
|||||||
"baz": {false, true},
|
"baz": {false, true},
|
||||||
}
|
}
|
||||||
output = CartesianProduct(input)
|
output = CartesianProduct(input)
|
||||||
assert.Len(output, 0) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.Empty(output)
|
||||||
|
|
||||||
input = map[string][]any{}
|
input = map[string][]any{}
|
||||||
output = CartesianProduct(input)
|
output = CartesianProduct(input)
|
||||||
assert.Len(output, 0) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.Empty(output)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ func NewParallelExecutor(parallel int, executors ...Executor) Executor {
|
|||||||
log.Debugf("Worker %d executing task %d", workerID, taskCount)
|
log.Debugf("Worker %d executing task %d", workerID, taskCount)
|
||||||
// Recover from panics in executors to avoid crashing the worker
|
// Recover from panics in executors to avoid crashing the worker
|
||||||
// goroutine which would leave the runner process hung.
|
// goroutine which would leave the runner process hung.
|
||||||
// https://gitea.com/gitea/act_runner/issues/371
|
// https://gitea.com/gitea/runner/issues/371
|
||||||
errs <- func() (err error) {
|
errs <- func() (err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
|
|||||||
@@ -21,11 +21,11 @@ func TestNewWorkflow(t *testing.T) {
|
|||||||
|
|
||||||
// empty
|
// empty
|
||||||
emptyWorkflow := NewPipelineExecutor()
|
emptyWorkflow := NewPipelineExecutor()
|
||||||
assert.Nil(emptyWorkflow(ctx)) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(emptyWorkflow(ctx)) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
// error case
|
// error case
|
||||||
errorWorkflow := NewErrorExecutor(errors.New("test error"))
|
errorWorkflow := NewErrorExecutor(errors.New("test error"))
|
||||||
assert.NotNil(errorWorkflow(ctx)) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.Error(errorWorkflow(ctx)) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
// multiple success case
|
// multiple success case
|
||||||
runcount := 0
|
runcount := 0
|
||||||
@@ -38,7 +38,7 @@ func TestNewWorkflow(t *testing.T) {
|
|||||||
runcount++
|
runcount++
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
assert.Nil(successWorkflow(ctx)) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(successWorkflow(ctx)) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
assert.Equal(2, runcount)
|
assert.Equal(2, runcount)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ func TestNewConditionalExecutor(t *testing.T) {
|
|||||||
return nil
|
return nil
|
||||||
})(ctx)
|
})(ctx)
|
||||||
|
|
||||||
assert.Nil(err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
assert.Equal(0, trueCount)
|
assert.Equal(0, trueCount)
|
||||||
assert.Equal(1, falseCount)
|
assert.Equal(1, falseCount)
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ func TestNewConditionalExecutor(t *testing.T) {
|
|||||||
return nil
|
return nil
|
||||||
})(ctx)
|
})(ctx)
|
||||||
|
|
||||||
assert.Nil(err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
assert.Equal(1, trueCount)
|
assert.Equal(1, trueCount)
|
||||||
assert.Equal(1, falseCount)
|
assert.Equal(1, falseCount)
|
||||||
}
|
}
|
||||||
@@ -105,7 +105,7 @@ func TestNewParallelExecutor(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(int32(3), count.Load(), "should run all 3 executors")
|
assert.Equal(int32(3), count.Load(), "should run all 3 executors")
|
||||||
assert.Equal(int32(2), maxCount.Load(), "should run at most 2 executors in parallel")
|
assert.Equal(int32(2), maxCount.Load(), "should run at most 2 executors in parallel")
|
||||||
assert.Nil(err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
// Reset to test running the executor with 0 parallelism
|
// Reset to test running the executor with 0 parallelism
|
||||||
count.Store(0)
|
count.Store(0)
|
||||||
@@ -116,7 +116,7 @@ func TestNewParallelExecutor(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(int32(3), count.Load(), "should run all 3 executors")
|
assert.Equal(int32(3), count.Load(), "should run all 3 executors")
|
||||||
assert.Equal(int32(1), maxCount.Load(), "should run at most 1 executors in parallel")
|
assert.Equal(int32(1), maxCount.Load(), "should run at most 1 executors in parallel")
|
||||||
assert.Nil(errSingle) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(errSingle)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewParallelExecutorFailed(t *testing.T) {
|
func TestNewParallelExecutorFailed(t *testing.T) {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5"
|
"github.com/go-git/go-git/v5"
|
||||||
"github.com/go-git/go-git/v5/config"
|
"github.com/go-git/go-git/v5/config"
|
||||||
@@ -32,12 +32,21 @@ var (
|
|||||||
githubHTTPRegex = regexp.MustCompile(`^https?://.*github.com.*/(.+)/(.+?)(?:.git)?$`)
|
githubHTTPRegex = regexp.MustCompile(`^https?://.*github.com.*/(.+)/(.+?)(?:.git)?$`)
|
||||||
githubSSHRegex = regexp.MustCompile(`github.com[:/](.+)/(.+?)(?:.git)?$`)
|
githubSSHRegex = regexp.MustCompile(`github.com[:/](.+)/(.+?)(?:.git)?$`)
|
||||||
|
|
||||||
cloneLock sync.Mutex
|
cloneLocks sync.Map // key: clone target directory; value: *sync.Mutex
|
||||||
|
|
||||||
ErrShortRef = errors.New("short SHA references are not supported")
|
ErrShortRef = errors.New("short SHA references are not supported")
|
||||||
ErrNoRepo = errors.New("unable to find git repo")
|
ErrNoRepo = errors.New("unable to find git repo")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// acquireCloneLock returns an unlock function after locking the per-directory mutex for dir.
|
||||||
|
// Only concurrent operations targeting the same directory are erialized; clones into different directories run in parallel.
|
||||||
|
func acquireCloneLock(dir string) func() {
|
||||||
|
v, _ := cloneLocks.LoadOrStore(dir, &sync.Mutex{})
|
||||||
|
mu := v.(*sync.Mutex)
|
||||||
|
mu.Lock()
|
||||||
|
return mu.Unlock
|
||||||
|
}
|
||||||
|
|
||||||
type Error struct {
|
type Error struct {
|
||||||
err error
|
err error
|
||||||
commit string
|
commit string
|
||||||
@@ -277,6 +286,7 @@ func CloneIfRequired(ctx context.Context, refName plumbing.ReferenceName, input
|
|||||||
|
|
||||||
func gitOptions(token string) (fetchOptions git.FetchOptions, pullOptions git.PullOptions) {
|
func gitOptions(token string) (fetchOptions git.FetchOptions, pullOptions git.PullOptions) {
|
||||||
fetchOptions.RefSpecs = []config.RefSpec{"refs/*:refs/*", "HEAD:refs/heads/HEAD"}
|
fetchOptions.RefSpecs = []config.RefSpec{"refs/*:refs/*", "HEAD:refs/heads/HEAD"}
|
||||||
|
fetchOptions.Force = true
|
||||||
pullOptions.Force = true
|
pullOptions.Force = true
|
||||||
|
|
||||||
if token != "" {
|
if token != "" {
|
||||||
@@ -292,16 +302,13 @@ func gitOptions(token string) (fetchOptions git.FetchOptions, pullOptions git.Pu
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewGitCloneExecutor creates an executor to clone git repos
|
// NewGitCloneExecutor creates an executor to clone git repos
|
||||||
//
|
|
||||||
//nolint:gocyclo // function handles many cases
|
|
||||||
func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor {
|
func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
logger.Infof(" \u2601 git clone '%s' # ref=%s", input.URL, input.Ref)
|
logger.Infof(" \u2601 git clone '%s' # ref=%s", input.URL, input.Ref)
|
||||||
logger.Debugf(" cloning %s to %s", input.URL, input.Dir)
|
logger.Debugf(" cloning %s to %s", input.URL, input.Dir)
|
||||||
|
|
||||||
cloneLock.Lock()
|
defer acquireCloneLock(input.Dir)()
|
||||||
defer cloneLock.Unlock()
|
|
||||||
|
|
||||||
refName := plumbing.ReferenceName("refs/heads/" + input.Ref)
|
refName := plumbing.ReferenceName("refs/heads/" + input.Ref)
|
||||||
r, err := CloneIfRequired(ctx, refName, input, logger)
|
r, err := CloneIfRequired(ctx, refName, input, logger)
|
||||||
|
|||||||
@@ -10,8 +10,11 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -220,6 +223,62 @@ func TestGitCloneExecutor(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGitCloneExecutorNonFastForwardRef(t *testing.T) {
|
||||||
|
// Simulate the scenario where a remote ref (e.g. a GitHub PR head ref) changes
|
||||||
|
// non-fast-forward between two fetches. Before the fix, the fetch used Force=false,
|
||||||
|
// causing go-git to return ErrForceNeeded and short-circuit the checkout.
|
||||||
|
|
||||||
|
gitConfig()
|
||||||
|
|
||||||
|
// Create a bare "remote" repo with an initial commit on main and a feature branch.
|
||||||
|
remoteDir := t.TempDir()
|
||||||
|
require.NoError(t, gitCmd("init", "--bare", "--initial-branch=main", remoteDir))
|
||||||
|
|
||||||
|
// We need a working clone to push commits from.
|
||||||
|
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"))
|
||||||
|
|
||||||
|
// Create a feature branch (simulates refs/pull/N/head).
|
||||||
|
require.NoError(t, gitCmd("-C", workDir, "checkout", "-b", "feature"))
|
||||||
|
require.NoError(t, gitCmd("-C", workDir, "commit", "--allow-empty", "-m", "feature-1"))
|
||||||
|
require.NoError(t, gitCmd("-C", workDir, "push", "origin", "feature"))
|
||||||
|
|
||||||
|
// First clone via the executor — should succeed and cache the repo.
|
||||||
|
cloneDir := t.TempDir()
|
||||||
|
clone := NewGitCloneExecutor(NewGitCloneExecutorInput{
|
||||||
|
URL: remoteDir,
|
||||||
|
Ref: "main",
|
||||||
|
Dir: cloneDir,
|
||||||
|
})
|
||||||
|
require.NoError(t, clone(context.Background()))
|
||||||
|
|
||||||
|
// Now force-push the feature branch to a non-fast-forward commit (simulates
|
||||||
|
// a PR rebase). This makes refs/heads/feature non-fast-forward.
|
||||||
|
require.NoError(t, gitCmd("-C", workDir, "checkout", "main"))
|
||||||
|
require.NoError(t, gitCmd("-C", workDir, "branch", "-D", "feature"))
|
||||||
|
require.NoError(t, gitCmd("-C", workDir, "checkout", "-b", "feature"))
|
||||||
|
require.NoError(t, gitCmd("-C", workDir, "commit", "--allow-empty", "-m", "feature-rewritten"))
|
||||||
|
require.NoError(t, gitCmd("-C", workDir, "push", "--force", "origin", "feature"))
|
||||||
|
|
||||||
|
// Also advance main so we can verify the clone picks up the new commit.
|
||||||
|
require.NoError(t, gitCmd("-C", workDir, "checkout", "main"))
|
||||||
|
require.NoError(t, gitCmd("-C", workDir, "commit", "--allow-empty", "-m", "second"))
|
||||||
|
require.NoError(t, gitCmd("-C", workDir, "push", "origin", "main"))
|
||||||
|
|
||||||
|
// Second clone to the same directory — before the fix this returned ErrForceNeeded
|
||||||
|
// and left the working tree at the old commit.
|
||||||
|
err := clone(context.Background())
|
||||||
|
require.NoError(t, err, "fetch with non-fast-forward refs must not fail when Force=true")
|
||||||
|
|
||||||
|
// Verify the working tree was actually updated to the latest main commit.
|
||||||
|
out, err := exec.Command("git", "-C", cloneDir, "log", "--oneline", "-1", "--format=%s").Output()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "second", strings.TrimSpace(string(out)), "working tree should be at the latest commit")
|
||||||
|
}
|
||||||
|
|
||||||
func gitConfig() {
|
func gitConfig() {
|
||||||
if os.Getenv("GITHUB_ACTIONS") == "true" {
|
if os.Getenv("GITHUB_ACTIONS") == "true" {
|
||||||
var err error
|
var err error
|
||||||
@@ -246,3 +305,61 @@ func gitCmd(args ...string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAcquireCloneLock(t *testing.T) {
|
||||||
|
t.Run("same directory serializes", func(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
|
||||||
|
unlock1 := acquireCloneLock(dir)
|
||||||
|
|
||||||
|
secondAcquired := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
unlock := acquireCloneLock(dir)
|
||||||
|
close(secondAcquired)
|
||||||
|
unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-secondAcquired:
|
||||||
|
t.Fatal("second acquire should block while first holds the lock")
|
||||||
|
case <-time.After(50 * time.Millisecond):
|
||||||
|
}
|
||||||
|
|
||||||
|
unlock1()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-secondAcquired:
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("second acquire should proceed after first releases the lock")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("different directories do not block", func(t *testing.T) {
|
||||||
|
dirA := t.TempDir()
|
||||||
|
dirB := t.TempDir()
|
||||||
|
|
||||||
|
unlockA := acquireCloneLock(dirA)
|
||||||
|
defer unlockA()
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
unlock := acquireCloneLock(dirB)
|
||||||
|
unlock()
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("acquire on a different directory must not block")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("same directory reuses the same mutex", func(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
|
||||||
|
v1, _ := cloneLocks.LoadOrStore(dir, &sync.Mutex{})
|
||||||
|
v2, _ := cloneLocks.LoadOrStore(dir, &sync.Mutex{})
|
||||||
|
require.Same(t, v1, v2)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,13 +6,21 @@ package container
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
|
||||||
"github.com/docker/go-connections/nat"
|
"github.com/docker/go-connections/nat"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ExitCodeError reports a non-zero process exit code from a container command.
|
||||||
|
type ExitCodeError int
|
||||||
|
|
||||||
|
func (e ExitCodeError) Error() string {
|
||||||
|
return fmt.Sprintf("Process completed with exit code %d.", int(e))
|
||||||
|
}
|
||||||
|
|
||||||
// NewContainerInput the input for the New function
|
// NewContainerInput the input for the New function
|
||||||
type NewContainerInput struct {
|
type NewContainerInput struct {
|
||||||
Image string
|
Image string
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/config"
|
"github.com/docker/cli/cli/config"
|
||||||
"github.com/docker/cli/cli/config/credentials"
|
"github.com/docker/cli/cli/config/credentials"
|
||||||
|
|||||||
@@ -12,11 +12,10 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/pkg/archive"
|
"github.com/docker/docker/pkg/archive"
|
||||||
// github.com/docker/docker/builder/dockerignore is deprecated
|
|
||||||
"github.com/moby/buildkit/frontend/dockerfile/dockerignore"
|
"github.com/moby/buildkit/frontend/dockerfile/dockerignore"
|
||||||
"github.com/moby/patternmatcher"
|
"github.com/moby/patternmatcher"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -324,8 +324,6 @@ type containerConfig struct {
|
|||||||
// parse parses the args for the specified command and generates a Config,
|
// parse parses the args for the specified command and generates a Config,
|
||||||
// a HostConfig and returns them with the specified command.
|
// a HostConfig and returns them with the specified command.
|
||||||
// If the specified args are not valid, it will return an error.
|
// If the specified args are not valid, it will return an error.
|
||||||
//
|
|
||||||
//nolint:gocyclo // function handles many cases
|
|
||||||
func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*containerConfig, error) {
|
func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*containerConfig, error) {
|
||||||
var (
|
var (
|
||||||
attachStdin = copts.attach.Get("stdin")
|
attachStdin = copts.attach.Get("stdin")
|
||||||
|
|||||||
@@ -194,7 +194,6 @@ func TestParseRunWithInvalidArgs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:gocyclo // function handles many cases
|
|
||||||
func TestParseWithVolumes(t *testing.T) {
|
func TestParseWithVolumes(t *testing.T) {
|
||||||
// A single volume
|
// A single volume
|
||||||
arr, tryit := setupPlatformVolume([]string{`/tmp`}, []string{`c:\tmp`})
|
arr, tryit := setupPlatformVolume([]string{`/tmp`}, []string{`c:\tmp`})
|
||||||
@@ -632,7 +631,7 @@ func TestParseModes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// uts ko
|
// uts ko
|
||||||
_, _, _, err = parseRun([]string{"--uts=container:", "img", "cmd"}) //nolint:dogsled // ignoring multiple returns in test helpers
|
_, _, _, err = parseRun([]string{"--uts=container:", "img", "cmd"})
|
||||||
assert.ErrorContains(t, err, "--uts: invalid UTS mode")
|
assert.ErrorContains(t, err, "--uts: invalid UTS mode")
|
||||||
|
|
||||||
// uts ok
|
// uts ok
|
||||||
@@ -693,7 +692,7 @@ func TestParseRestartPolicy(t *testing.T) {
|
|||||||
|
|
||||||
func TestParseRestartPolicyAutoRemove(t *testing.T) {
|
func TestParseRestartPolicyAutoRemove(t *testing.T) {
|
||||||
expected := "Conflicting options: --restart and --rm"
|
expected := "Conflicting options: --restart and --rm"
|
||||||
_, _, _, err := parseRun([]string{"--rm", "--restart=always", "img", "cmd"}) //nolint:dogsled // ignoring multiple returns in test helpers
|
_, _, _, err := parseRun([]string{"--rm", "--restart=always", "img", "cmd"})
|
||||||
if err == nil || err.Error() != expected {
|
if err == nil || err.Error() != expected {
|
||||||
t.Fatalf("Expected error %v, but got none", expected)
|
t.Fatalf("Expected error %v, but got none", expected)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,17 +29,17 @@ func TestImageExistsLocally(t *testing.T) {
|
|||||||
|
|
||||||
// Test if image exists with specific tag
|
// Test if image exists with specific tag
|
||||||
invalidImageTag, err := ImageExistsLocally(ctx, "library/alpine:this-random-tag-will-never-exist", "linux/amd64")
|
invalidImageTag, err := ImageExistsLocally(ctx, "library/alpine:this-random-tag-will-never-exist", "linux/amd64")
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
assert.Equal(t, false, invalidImageTag) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.False(t, invalidImageTag)
|
||||||
|
|
||||||
// Test if image exists with specific architecture (image platform)
|
// Test if image exists with specific architecture (image platform)
|
||||||
invalidImagePlatform, err := ImageExistsLocally(ctx, "alpine:latest", "windows/amd64")
|
invalidImagePlatform, err := ImageExistsLocally(ctx, "alpine:latest", "windows/amd64")
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
assert.Equal(t, false, invalidImagePlatform) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.False(t, invalidImagePlatform)
|
||||||
|
|
||||||
// pull an image
|
// pull an image
|
||||||
cli, err := client.NewClientWithOpts(client.FromEnv)
|
cli, err := client.NewClientWithOpts(client.FromEnv)
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
cli.NegotiateAPIVersion(context.Background())
|
cli.NegotiateAPIVersion(context.Background())
|
||||||
|
|
||||||
// Chose alpine latest because it's so small
|
// Chose alpine latest because it's so small
|
||||||
@@ -47,25 +47,25 @@ func TestImageExistsLocally(t *testing.T) {
|
|||||||
readerDefault, err := cli.ImagePull(ctx, "node:16-buster-slim", types.ImagePullOptions{
|
readerDefault, err := cli.ImagePull(ctx, "node:16-buster-slim", types.ImagePullOptions{
|
||||||
Platform: "linux/amd64",
|
Platform: "linux/amd64",
|
||||||
})
|
})
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
defer readerDefault.Close()
|
defer readerDefault.Close()
|
||||||
_, err = io.ReadAll(readerDefault)
|
_, err = io.ReadAll(readerDefault)
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
imageDefaultArchExists, err := ImageExistsLocally(ctx, "node:16-buster-slim", "linux/amd64")
|
imageDefaultArchExists, err := ImageExistsLocally(ctx, "node:16-buster-slim", "linux/amd64")
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
assert.Equal(t, true, imageDefaultArchExists) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.True(t, imageDefaultArchExists)
|
||||||
|
|
||||||
// Validate if another architecture platform can be pulled
|
// Validate if another architecture platform can be pulled
|
||||||
readerArm64, err := cli.ImagePull(ctx, "node:16-buster-slim", types.ImagePullOptions{
|
readerArm64, err := cli.ImagePull(ctx, "node:16-buster-slim", types.ImagePullOptions{
|
||||||
Platform: "linux/arm64",
|
Platform: "linux/arm64",
|
||||||
})
|
})
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
defer readerArm64.Close()
|
defer readerArm64.Close()
|
||||||
_, err = io.ReadAll(readerArm64)
|
_, err = io.ReadAll(readerArm64)
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
imageArm64Exists, err := ImageExistsLocally(ctx, "node:16-buster-slim", "linux/arm64")
|
imageArm64Exists, err := ImageExistsLocally(ctx, "node:16-buster-slim", "linux/arm64")
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
assert.Equal(t, true, imageArm64Exists) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.True(t, imageArm64Exists)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ package container
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
|
||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ func TestGetImagePullOptions(t *testing.T) {
|
|||||||
config.SetDir("/non-existent/docker")
|
config.SetDir("/non-existent/docker")
|
||||||
|
|
||||||
options, err := getImagePullOptions(ctx, NewDockerPullExecutorInput{})
|
options, err := getImagePullOptions(ctx, NewDockerPullExecutorInput{})
|
||||||
assert.Nil(t, err, "Failed to create ImagePullOptions") //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err, "Failed to create ImagePullOptions") //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
assert.Equal(t, "", options.RegistryAuth, "RegistryAuth should be empty if no username or password is set") //nolint:testifylint // pre-existing issue from nektos/act
|
assert.Equal(t, "", options.RegistryAuth, "RegistryAuth should be empty if no username or password is set") //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
options, err = getImagePullOptions(ctx, NewDockerPullExecutorInput{
|
options, err = getImagePullOptions(ctx, NewDockerPullExecutorInput{
|
||||||
@@ -51,7 +51,7 @@ func TestGetImagePullOptions(t *testing.T) {
|
|||||||
Username: "username",
|
Username: "username",
|
||||||
Password: "password",
|
Password: "password",
|
||||||
})
|
})
|
||||||
assert.Nil(t, err, "Failed to create ImagePullOptions") //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err, "Failed to create ImagePullOptions") //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
assert.Equal(t, "eyJ1c2VybmFtZSI6InVzZXJuYW1lIiwicGFzc3dvcmQiOiJwYXNzd29yZCJ9", options.RegistryAuth, "Username and Password should be provided")
|
assert.Equal(t, "eyJ1c2VybmFtZSI6InVzZXJuYW1lIiwicGFzc3dvcmQiOiJwYXNzd29yZCJ9", options.RegistryAuth, "Username and Password should be provided")
|
||||||
|
|
||||||
config.SetDir("testdata/docker-pull-options")
|
config.SetDir("testdata/docker-pull-options")
|
||||||
@@ -59,6 +59,6 @@ func TestGetImagePullOptions(t *testing.T) {
|
|||||||
options, err = getImagePullOptions(ctx, NewDockerPullExecutorInput{
|
options, err = getImagePullOptions(ctx, NewDockerPullExecutorInput{
|
||||||
Image: "nektos/act",
|
Image: "nektos/act",
|
||||||
})
|
})
|
||||||
assert.Nil(t, err, "Failed to create ImagePullOptions") //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err, "Failed to create ImagePullOptions") //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
assert.Equal(t, "eyJ1c2VybmFtZSI6InVzZXJuYW1lIiwicGFzc3dvcmQiOiJwYXNzd29yZFxuIiwic2VydmVyYWRkcmVzcyI6Imh0dHBzOi8vaW5kZXguZG9ja2VyLmlvL3YxLyJ9", options.RegistryAuth, "RegistryAuth should be taken from local docker config")
|
assert.Equal(t, "eyJ1c2VybmFtZSI6InVzZXJuYW1lIiwicGFzc3dvcmQiOiJwYXNzd29yZFxuIiwic2VydmVyYWRkcmVzcyI6Imh0dHBzOi8vaW5kZXguZG9ja2VyLmlvL3YxLyJ9", options.RegistryAuth, "RegistryAuth should be taken from local docker config")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
"gitea.com/gitea/act_runner/act/filecollector"
|
"gitea.com/gitea/runner/act/filecollector"
|
||||||
|
|
||||||
"github.com/Masterminds/semver"
|
"github.com/Masterminds/semver"
|
||||||
"github.com/docker/cli/cli/compose/loader"
|
"github.com/docker/cli/cli/compose/loader"
|
||||||
@@ -633,14 +633,10 @@ func (cr *containerReference) exec(cmd []string, env map[string]string, user, wo
|
|||||||
return fmt.Errorf("failed to inspect exec: %w", err)
|
return fmt.Errorf("failed to inspect exec: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch inspectResp.ExitCode {
|
if inspectResp.ExitCode == 0 {
|
||||||
case 0:
|
|
||||||
return nil
|
return nil
|
||||||
case 127:
|
|
||||||
return fmt.Errorf("exitcode '%d': command not found, please refer to https://github.com/nektos/act/issues/107 for more information", inspectResp.ExitCode)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("exitcode '%d': failure", inspectResp.ExitCode)
|
|
||||||
}
|
}
|
||||||
|
return ExitCodeError(inspectResp.ExitCode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -930,7 +926,7 @@ func (cr *containerReference) wait() common.Executor {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("exit with `FAILURE`: %v", statusCode)
|
return ExitCodeError(statusCode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
@@ -23,6 +23,7 @@ import (
|
|||||||
"github.com/sirupsen/logrus/hooks/test"
|
"github.com/sirupsen/logrus/hooks/test"
|
||||||
"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 TestDocker(t *testing.T) {
|
func TestDocker(t *testing.T) {
|
||||||
@@ -85,6 +86,11 @@ func (m *mockDockerClient) ContainerExecInspect(ctx context.Context, execID stri
|
|||||||
return args.Get(0).(types.ContainerExecInspect), args.Error(1)
|
return args.Get(0).(types.ContainerExecInspect), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockDockerClient) ContainerWait(ctx context.Context, containerID string, condition container.WaitCondition) (<-chan container.WaitResponse, <-chan error) {
|
||||||
|
args := m.Called(ctx, containerID, condition)
|
||||||
|
return args.Get(0).(<-chan container.WaitResponse), args.Get(1).(<-chan error)
|
||||||
|
}
|
||||||
|
|
||||||
func (m *mockDockerClient) CopyToContainer(ctx context.Context, id, path string, content io.Reader, options types.CopyToContainerOptions) error {
|
func (m *mockDockerClient) CopyToContainer(ctx context.Context, id, path string, content io.Reader, options types.CopyToContainerOptions) error {
|
||||||
args := m.Called(ctx, id, path, content, options)
|
args := m.Called(ctx, id, path, content, options)
|
||||||
return args.Error(0)
|
return args.Error(0)
|
||||||
@@ -174,12 +180,43 @@ func TestDockerExecFailure(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
err := cr.exec([]string{""}, map[string]string{}, "user", "workdir")(ctx)
|
err := cr.exec([]string{""}, map[string]string{}, "user", "workdir")(ctx)
|
||||||
assert.Error(t, err, "exit with `FAILURE`: 1") //nolint:testifylint // pre-existing issue from nektos/act
|
var exitErr ExitCodeError
|
||||||
|
require.ErrorAs(t, err, &exitErr)
|
||||||
|
assert.Equal(t, ExitCodeError(1), exitErr)
|
||||||
|
assert.Equal(t, "Process completed with exit code 1.", err.Error())
|
||||||
|
|
||||||
conn.AssertExpectations(t)
|
conn.AssertExpectations(t)
|
||||||
client.AssertExpectations(t)
|
client.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDockerWaitFailure(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
statusCh := make(chan container.WaitResponse, 1)
|
||||||
|
statusCh <- container.WaitResponse{StatusCode: 2}
|
||||||
|
errCh := make(chan error, 1)
|
||||||
|
|
||||||
|
client := &mockDockerClient{}
|
||||||
|
client.On("ContainerWait", ctx, "123", container.WaitConditionNotRunning).
|
||||||
|
Return((<-chan container.WaitResponse)(statusCh), (<-chan error)(errCh))
|
||||||
|
|
||||||
|
cr := &containerReference{
|
||||||
|
id: "123",
|
||||||
|
cli: client,
|
||||||
|
input: &NewContainerInput{
|
||||||
|
Image: "image",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cr.wait()(ctx)
|
||||||
|
var exitErr ExitCodeError
|
||||||
|
require.ErrorAs(t, err, &exitErr)
|
||||||
|
assert.Equal(t, ExitCodeError(2), exitErr)
|
||||||
|
assert.Equal(t, "Process completed with exit code 2.", err.Error())
|
||||||
|
|
||||||
|
client.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
func TestDockerCopyTarStream(t *testing.T) {
|
func TestDockerCopyTarStream(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ func TestGetSocketAndHostWithSocket(t *testing.T) {
|
|||||||
ret, err := GetSocketAndHost(socketURI)
|
ret, err := GetSocketAndHost(socketURI)
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
assert.Equal(t, SocketAndHost{socketURI, dockerHost}, ret)
|
assert.Equal(t, SocketAndHost{socketURI, dockerHost}, ret)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ func TestGetSocketAndHostNoSocket(t *testing.T) {
|
|||||||
ret, err := GetSocketAndHost("")
|
ret, err := GetSocketAndHost("")
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
assert.Equal(t, SocketAndHost{dockerHost, dockerHost}, ret)
|
assert.Equal(t, SocketAndHost{dockerHost, dockerHost}, ret)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,8 +57,8 @@ func TestGetSocketAndHostOnlySocket(t *testing.T) {
|
|||||||
ret, err := GetSocketAndHost(socketURI)
|
ret, err := GetSocketAndHost(socketURI)
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assert.NoError(t, err, "Expected no error from GetSocketAndHost") //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err, "Expected no error from GetSocketAndHost") //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
assert.Equal(t, true, defaultSocketFound, "Expected to find default socket") //nolint:testifylint // pre-existing issue from nektos/act
|
assert.True(t, defaultSocketFound, "Expected to find default socket")
|
||||||
assert.Equal(t, socketURI, ret.Socket, "Expected socket to match common location")
|
assert.Equal(t, socketURI, ret.Socket, "Expected socket to match common location")
|
||||||
assert.Equal(t, defaultSocket, ret.Host, "Expected ret.Host to match default socket location")
|
assert.Equal(t, defaultSocket, ret.Host, "Expected ret.Host to match default socket location")
|
||||||
}
|
}
|
||||||
@@ -73,7 +73,7 @@ func TestGetSocketAndHostDontMount(t *testing.T) {
|
|||||||
ret, err := GetSocketAndHost("-")
|
ret, err := GetSocketAndHost("-")
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
assert.Equal(t, SocketAndHost{"-", dockerHost}, ret)
|
assert.Equal(t, SocketAndHost{"-", dockerHost}, ret)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,8 +87,8 @@ func TestGetSocketAndHostNoHostNoSocket(t *testing.T) {
|
|||||||
ret, err := GetSocketAndHost("")
|
ret, err := GetSocketAndHost("")
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assert.Equal(t, true, found, "Expected a default socket to be found") //nolint:testifylint // pre-existing issue from nektos/act
|
assert.True(t, found, "Expected a default socket to be found")
|
||||||
assert.Nil(t, err, "Expected no error from GetSocketAndHost") //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err, "Expected no error from GetSocketAndHost") //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
assert.Equal(t, SocketAndHost{defaultSocket, defaultSocket}, ret, "Expected to match default socket location")
|
assert.Equal(t, SocketAndHost{defaultSocket, defaultSocket}, ret, "Expected to match default socket location")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,8 +112,8 @@ func TestGetSocketAndHostNoHostNoSocketDefaultLocation(t *testing.T) {
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assert.Equal(t, unixSocket, defaultSocket, "Expected default socket to match common socket location")
|
assert.Equal(t, unixSocket, defaultSocket, "Expected default socket to match common socket location")
|
||||||
assert.Equal(t, true, found, "Expected default socket to be found") //nolint:testifylint // pre-existing issue from nektos/act
|
assert.True(t, found, "Expected default socket to be found")
|
||||||
assert.Nil(t, err, "Expected no error from GetSocketAndHost") //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err, "Expected no error from GetSocketAndHost") //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
assert.Equal(t, SocketAndHost{unixSocket, unixSocket}, ret, "Expected to match default socket location")
|
assert.Equal(t, SocketAndHost{unixSocket, unixSocket}, ret, "Expected to match default socket location")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +128,7 @@ func TestGetSocketAndHostNoHostInvalidSocket(t *testing.T) {
|
|||||||
ret, err := GetSocketAndHost(mySocket)
|
ret, err := GetSocketAndHost(mySocket)
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assert.Equal(t, false, found, "Expected no default socket to be found") //nolint:testifylint // pre-existing issue from nektos/act
|
assert.False(t, found, "Expected no default socket to be found")
|
||||||
assert.Equal(t, "", defaultSocket, "Expected no default socket to be found") //nolint:testifylint // pre-existing issue from nektos/act
|
assert.Equal(t, "", defaultSocket, "Expected no default socket to be found") //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
assert.Equal(t, SocketAndHost{}, ret, "Expected to match default socket location")
|
assert.Equal(t, SocketAndHost{}, ret, "Expected to match default socket location")
|
||||||
assert.Error(t, err, "Expected an error in invalid state")
|
assert.Error(t, err, "Expected an error in invalid state")
|
||||||
@@ -147,8 +147,8 @@ func TestGetSocketAndHostOnlySocketValidButUnusualLocation(t *testing.T) {
|
|||||||
// Assert
|
// Assert
|
||||||
// Default socket locations
|
// Default socket locations
|
||||||
assert.Equal(t, "", defaultSocket, "Expect default socket location to be empty") //nolint:testifylint // pre-existing issue from nektos/act
|
assert.Equal(t, "", defaultSocket, "Expect default socket location to be empty") //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
assert.Equal(t, false, found, "Expected no default socket to be found") //nolint:testifylint // pre-existing issue from nektos/act
|
assert.False(t, found, "Expected no default socket to be found")
|
||||||
// Sane default
|
// Sane default
|
||||||
assert.Nil(t, err, "Expect no error from GetSocketAndHost") //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err, "Expect no error from GetSocketAndHost") //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
assert.Equal(t, socketURI, ret.Host, "Expect host to default to unusual socket")
|
assert.Equal(t, socketURI, ret.Host, "Expect host to default to unusual socket")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ package container
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
"github.com/docker/docker/api/types/volume"
|
"github.com/docker/docker/api/types/volume"
|
||||||
|
|||||||
@@ -16,12 +16,14 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
"gitea.com/gitea/act_runner/act/filecollector"
|
"gitea.com/gitea/runner/act/filecollector"
|
||||||
"gitea.com/gitea/act_runner/act/lookpath"
|
"gitea.com/gitea/runner/act/lookpath"
|
||||||
|
|
||||||
"github.com/go-git/go-billy/v5/helper/polyfill"
|
"github.com/go-git/go-billy/v5/helper/polyfill"
|
||||||
"github.com/go-git/go-billy/v5/osfs"
|
"github.com/go-git/go-billy/v5/osfs"
|
||||||
@@ -34,9 +36,15 @@ type HostEnvironment struct {
|
|||||||
TmpDir string
|
TmpDir string
|
||||||
ToolCache string
|
ToolCache string
|
||||||
Workdir string
|
Workdir string
|
||||||
ActPath string
|
// BindWorkdir is true when the app runner mounts the workspace on the host and
|
||||||
CleanUp func()
|
// deletes the task directory after the job; host teardown must not remove Workdir.
|
||||||
StdOut io.Writer
|
BindWorkdir bool
|
||||||
|
ActPath string
|
||||||
|
CleanUp func()
|
||||||
|
StdOut io.Writer
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
runningPIDs map[int]struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *HostEnvironment) Create(_, _ []string) common.Executor {
|
func (e *HostEnvironment) Create(_, _ []string) common.Executor {
|
||||||
@@ -344,8 +352,30 @@ func (e *HostEnvironment) exec(ctx context.Context, command []string, cmdline st
|
|||||||
if ppty != nil {
|
if ppty != nil {
|
||||||
go writeKeepAlive(ppty)
|
go writeKeepAlive(ppty)
|
||||||
}
|
}
|
||||||
err = cmd.Run()
|
// Split Start/Wait so the PID can be registered before the process can exit;
|
||||||
|
// cmd.Run() would block until exit, by which time the PID may have been reused.
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if cmd.Process != nil {
|
||||||
|
e.mu.Lock()
|
||||||
|
if e.runningPIDs == nil {
|
||||||
|
e.runningPIDs = map[int]struct{}{}
|
||||||
|
}
|
||||||
|
e.runningPIDs[cmd.Process.Pid] = struct{}{}
|
||||||
|
e.mu.Unlock()
|
||||||
|
defer func(pid int) {
|
||||||
|
e.mu.Lock()
|
||||||
|
delete(e.runningPIDs, pid)
|
||||||
|
e.mu.Unlock()
|
||||||
|
}(cmd.Process.Pid)
|
||||||
|
}
|
||||||
|
err = cmd.Wait()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
var exitErr *exec.ExitError
|
||||||
|
if errors.As(err, &exitErr) {
|
||||||
|
return ExitCodeError(exitErr.ExitCode())
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if tty != nil {
|
if tty != nil {
|
||||||
@@ -385,12 +415,83 @@ func (e *HostEnvironment) UpdateFromEnv(srcPath string, env *map[string]string)
|
|||||||
return parseEnvFile(e, srcPath, env)
|
return parseEnvFile(e, srcPath, env)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func removePathWithRetry(ctx context.Context, path string) error {
|
||||||
|
if path == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
attempts := 1
|
||||||
|
delay := time.Duration(0)
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
attempts = 5
|
||||||
|
delay = 200 * time.Millisecond
|
||||||
|
}
|
||||||
|
var lastErr error
|
||||||
|
for i := 0; i < attempts; i++ {
|
||||||
|
if i > 0 {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-time.After(delay):
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastErr = os.RemoveAll(path)
|
||||||
|
if lastErr == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lastErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *HostEnvironment) terminateRunningProcesses(ctx context.Context) {
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.mu.Lock()
|
||||||
|
pids := make([]int, 0, len(e.runningPIDs))
|
||||||
|
for pid := range e.runningPIDs {
|
||||||
|
pids = append(pids, pid)
|
||||||
|
}
|
||||||
|
e.mu.Unlock()
|
||||||
|
|
||||||
|
if len(pids) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger := common.Logger(ctx)
|
||||||
|
for _, pid := range pids {
|
||||||
|
// Best-effort: forcibly terminate process tree to release file handles
|
||||||
|
// so that workspace cleanup can succeed on Windows.
|
||||||
|
cmd := exec.CommandContext(ctx, "taskkill", "/PID", strconv.Itoa(pid), "/T", "/F")
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
logger.Debugf("taskkill failed for pid=%d: %v output=%s", pid, err, strings.TrimSpace(string(out)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (e *HostEnvironment) Remove() common.Executor {
|
func (e *HostEnvironment) Remove() common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
|
// Ensure any lingering child processes are ended before attempting
|
||||||
|
// to remove the workspace (Windows file locks otherwise prevent cleanup).
|
||||||
|
e.terminateRunningProcesses(ctx)
|
||||||
|
|
||||||
|
// Only removes per-job misc state. Must not remove the cache/toolcache root.
|
||||||
if e.CleanUp != nil {
|
if e.CleanUp != nil {
|
||||||
e.CleanUp()
|
e.CleanUp()
|
||||||
}
|
}
|
||||||
return os.RemoveAll(e.Path)
|
logger := common.Logger(ctx)
|
||||||
|
var errs []error
|
||||||
|
if err := removePathWithRetry(ctx, e.Path); err != nil {
|
||||||
|
logger.Warnf("failed to remove host misc state %s: %v", e.Path, err)
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
if !e.BindWorkdir && e.Workdir != "" {
|
||||||
|
if err := removePathWithRetry(ctx, e.Workdir); err != nil {
|
||||||
|
logger.Warnf("failed to remove host workspace %s: %v", e.Workdir, err)
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors.Join(errs...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,9 +11,14 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Type assert HostEnvironment implements ExecutionsEnvironment
|
// Type assert HostEnvironment implements ExecutionsEnvironment
|
||||||
@@ -69,3 +74,76 @@ func TestGetContainerArchive(t *testing.T) {
|
|||||||
_, err = reader.Next()
|
_, err = reader.Next()
|
||||||
assert.ErrorIs(t, err, io.EOF)
|
assert.ErrorIs(t, err, io.EOF)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHostEnvironmentExecExitCode(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("uses POSIX shell")
|
||||||
|
}
|
||||||
|
dir := t.TempDir()
|
||||||
|
ctx := context.Background()
|
||||||
|
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: io.Discard,
|
||||||
|
Workdir: filepath.Join(dir, "path"),
|
||||||
|
}
|
||||||
|
for _, p := range []string{e.Path, e.TmpDir, e.ToolCache, e.ActPath} {
|
||||||
|
assert.NoError(t, os.MkdirAll(p, 0o700)) //nolint:testifylint // test setup
|
||||||
|
}
|
||||||
|
|
||||||
|
err := e.Exec([]string{"sh", "-c", "exit 3"}, map[string]string{"PATH": os.Getenv("PATH")}, "", "")(ctx)
|
||||||
|
var exitErr ExitCodeError
|
||||||
|
require.ErrorAs(t, err, &exitErr)
|
||||||
|
assert.Equal(t, ExitCodeError(3), exitErr)
|
||||||
|
assert.Equal(t, "Process completed with exit code 3.", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHostEnvironmentRemoveCleansWorkdir(t *testing.T) {
|
||||||
|
logger := logrus.New()
|
||||||
|
ctx := common.WithLogger(context.Background(), logrus.NewEntry(logger))
|
||||||
|
base := t.TempDir()
|
||||||
|
miscRoot := filepath.Join(base, "misc")
|
||||||
|
path := filepath.Join(miscRoot, "hostexecutor")
|
||||||
|
require.NoError(t, os.MkdirAll(path, 0o700))
|
||||||
|
workdir := filepath.Join(base, "workspace", "owner", "repo")
|
||||||
|
require.NoError(t, os.MkdirAll(workdir, 0o700))
|
||||||
|
|
||||||
|
e := &HostEnvironment{
|
||||||
|
Path: path,
|
||||||
|
Workdir: workdir,
|
||||||
|
BindWorkdir: false,
|
||||||
|
CleanUp: func() {
|
||||||
|
_ = os.RemoveAll(miscRoot)
|
||||||
|
},
|
||||||
|
StdOut: os.Stdout,
|
||||||
|
}
|
||||||
|
require.NoError(t, e.Remove()(ctx))
|
||||||
|
_, err := os.Stat(workdir)
|
||||||
|
assert.ErrorIs(t, err, os.ErrNotExist)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHostEnvironmentRemoveSkipsWorkdirWhenBindWorkdir(t *testing.T) {
|
||||||
|
logger := logrus.New()
|
||||||
|
ctx := common.WithLogger(context.Background(), logrus.NewEntry(logger))
|
||||||
|
base := t.TempDir()
|
||||||
|
miscRoot := filepath.Join(base, "misc")
|
||||||
|
path := filepath.Join(miscRoot, "hostexecutor")
|
||||||
|
require.NoError(t, os.MkdirAll(path, 0o700))
|
||||||
|
workdir := filepath.Join(base, "workspace", "123", "owner", "repo")
|
||||||
|
require.NoError(t, os.MkdirAll(workdir, 0o700))
|
||||||
|
|
||||||
|
e := &HostEnvironment{
|
||||||
|
Path: path,
|
||||||
|
Workdir: workdir,
|
||||||
|
BindWorkdir: true,
|
||||||
|
CleanUp: func() {
|
||||||
|
_ = os.RemoveAll(miscRoot)
|
||||||
|
},
|
||||||
|
StdOut: os.Stdout,
|
||||||
|
}
|
||||||
|
require.NoError(t, e.Remove()(ctx))
|
||||||
|
_, err := os.Stat(workdir)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
func parseEnvFile(e Container, srcPath string, env *map[string]string) common.Executor {
|
func parseEnvFile(e Container, srcPath string, env *map[string]string) common.Executor {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/format/gitignore"
|
"github.com/go-git/go-git/v5/plumbing/format/gitignore"
|
||||||
"github.com/rhysd/actionlint"
|
"github.com/rhysd/actionlint"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
@@ -43,7 +43,7 @@ func TestFunctionContains(t *testing.T) {
|
|||||||
for _, tt := range table {
|
for _, tt := range table {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
|
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
assert.Equal(t, tt.expected, output)
|
assert.Equal(t, tt.expected, output)
|
||||||
})
|
})
|
||||||
@@ -72,7 +72,7 @@ func TestFunctionStartsWith(t *testing.T) {
|
|||||||
for _, tt := range table {
|
for _, tt := range table {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
|
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
assert.Equal(t, tt.expected, output)
|
assert.Equal(t, tt.expected, output)
|
||||||
})
|
})
|
||||||
@@ -101,7 +101,7 @@ func TestFunctionEndsWith(t *testing.T) {
|
|||||||
for _, tt := range table {
|
for _, tt := range table {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
|
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
assert.Equal(t, tt.expected, output)
|
assert.Equal(t, tt.expected, output)
|
||||||
})
|
})
|
||||||
@@ -128,7 +128,7 @@ func TestFunctionJoin(t *testing.T) {
|
|||||||
for _, tt := range table {
|
for _, tt := range table {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
|
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
assert.Equal(t, tt.expected, output)
|
assert.Equal(t, tt.expected, output)
|
||||||
})
|
})
|
||||||
@@ -154,7 +154,7 @@ func TestFunctionToJSON(t *testing.T) {
|
|||||||
for _, tt := range table {
|
for _, tt := range table {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
|
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
assert.Equal(t, tt.expected, output)
|
assert.Equal(t, tt.expected, output)
|
||||||
})
|
})
|
||||||
@@ -177,7 +177,7 @@ func TestFunctionFromJSON(t *testing.T) {
|
|||||||
for _, tt := range table {
|
for _, tt := range table {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
|
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
assert.Equal(t, tt.expected, output)
|
assert.Equal(t, tt.expected, output)
|
||||||
})
|
})
|
||||||
@@ -205,9 +205,9 @@ func TestFunctionHashFiles(t *testing.T) {
|
|||||||
for _, tt := range table {
|
for _, tt := range table {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
workdir, err := filepath.Abs("testdata")
|
workdir, err := filepath.Abs("testdata")
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
output, err := NewInterpeter(env, Config{WorkingDir: workdir}).Evaluate(tt.input, DefaultStatusCheckNone)
|
output, err := NewInterpeter(env, Config{WorkingDir: workdir}).Evaluate(tt.input, DefaultStatusCheckNone)
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
assert.Equal(t, tt.expected, output)
|
assert.Equal(t, tt.expected, output)
|
||||||
})
|
})
|
||||||
@@ -248,7 +248,7 @@ func TestFunctionFormat(t *testing.T) {
|
|||||||
if tt.error != nil {
|
if tt.error != nil {
|
||||||
assert.Equal(t, tt.error, err.Error())
|
assert.Equal(t, tt.error, err.Error())
|
||||||
} else {
|
} else {
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
assert.Equal(t, tt.expected, output)
|
assert.Equal(t, tt.expected, output)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
|
|
||||||
"github.com/rhysd/actionlint"
|
"github.com/rhysd/actionlint"
|
||||||
)
|
)
|
||||||
@@ -156,7 +156,6 @@ func (impl *interperterImpl) evaluateNode(exprNode actionlint.ExprNode) (any, er
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:gocyclo // function handles many cases
|
|
||||||
func (impl *interperterImpl) evaluateVariable(variableNode *actionlint.VariableNode) (any, error) {
|
func (impl *interperterImpl) evaluateVariable(variableNode *actionlint.VariableNode) (any, error) {
|
||||||
switch strings.ToLower(variableNode.Name) {
|
switch strings.ToLower(variableNode.Name) {
|
||||||
case "github":
|
case "github":
|
||||||
@@ -584,7 +583,6 @@ func (impl *interperterImpl) evaluateLogicalCompare(compareNode *actionlint.Logi
|
|||||||
return nil, fmt.Errorf("Unable to compare incompatibles types '%s' and '%s'", leftValue.Kind(), rightValue.Kind())
|
return nil, fmt.Errorf("Unable to compare incompatibles types '%s' and '%s'", leftValue.Kind(), rightValue.Kind())
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:gocyclo // function handles many cases
|
|
||||||
func (impl *interperterImpl) evaluateFuncCall(funcCallNode *actionlint.FuncCallNode) (any, error) {
|
func (impl *interperterImpl) evaluateFuncCall(funcCallNode *actionlint.FuncCallNode) (any, error) {
|
||||||
args := make([]reflect.Value, 0)
|
args := make([]reflect.Value, 0)
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
@@ -35,7 +35,7 @@ func TestLiterals(t *testing.T) {
|
|||||||
for _, tt := range table {
|
for _, tt := range table {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
|
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
assert.Equal(t, tt.expected, output)
|
assert.Equal(t, tt.expected, output)
|
||||||
})
|
})
|
||||||
@@ -105,10 +105,10 @@ func TestOperators(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
|
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
|
||||||
if tt.error != "" {
|
if tt.error != "" {
|
||||||
assert.NotNil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.Error(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
assert.Equal(t, tt.error, err.Error())
|
assert.Equal(t, tt.error, err.Error())
|
||||||
} else {
|
} else {
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, tt.expected, output)
|
assert.Equal(t, tt.expected, output)
|
||||||
@@ -157,7 +157,7 @@ func TestOperatorsCompare(t *testing.T) {
|
|||||||
for _, tt := range table {
|
for _, tt := range table {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
|
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
assert.Equal(t, tt.expected, output)
|
assert.Equal(t, tt.expected, output)
|
||||||
})
|
})
|
||||||
@@ -520,7 +520,7 @@ func TestOperatorsBooleanEvaluation(t *testing.T) {
|
|||||||
for _, tt := range table {
|
for _, tt := range table {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
|
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
if expected, ok := tt.expected.(float64); ok && math.IsNaN(expected) {
|
if expected, ok := tt.expected.(float64); ok && math.IsNaN(expected) {
|
||||||
assert.True(t, math.IsNaN(output.(float64)))
|
assert.True(t, math.IsNaN(output.(float64)))
|
||||||
@@ -624,7 +624,7 @@ func TestContexts(t *testing.T) {
|
|||||||
for _, tt := range table {
|
for _, tt := range table {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
|
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
assert.Equal(t, tt.expected, output)
|
assert.Equal(t, tt.expected, output)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -128,7 +128,6 @@ func (*DefaultFs) Readlink(path string) (string, error) {
|
|||||||
return os.Readlink(path)
|
return os.Readlink(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:gocyclo // function handles many cases
|
|
||||||
func (fc *FileCollector) CollectFiles(ctx context.Context, submodulePath []string) filepath.WalkFunc {
|
func (fc *FileCollector) CollectFiles(ctx context.Context, submodulePath []string) filepath.WalkFunc {
|
||||||
i, _ := fc.Fs.OpenGitIndex(path.Join(fc.SrcPath, path.Join(submodulePath...)))
|
i, _ := fc.Fs.OpenGitIndex(path.Join(fc.SrcPath, path.Join(submodulePath...)))
|
||||||
return func(file string, fi os.FileInfo, err error) error {
|
return func(file string, fi os.FileInfo, err error) error {
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
"gitea.com/gitea/act_runner/act/common/git"
|
"gitea.com/gitea/runner/act/common/git"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GithubContext struct {
|
type GithubContext struct {
|
||||||
|
|||||||
@@ -61,8 +61,6 @@ type WorkflowFiles struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewWorkflowPlanner will load a specific workflow, all workflows from a directory or all workflows from a directory and its subdirectories
|
// NewWorkflowPlanner will load a specific workflow, all workflows from a directory or all workflows from a directory and its subdirectories
|
||||||
//
|
|
||||||
//nolint:gocyclo // function handles many cases
|
|
||||||
func NewWorkflowPlanner(path string, noWorkflowRecurse bool) (WorkflowPlanner, error) {
|
func NewWorkflowPlanner(path string, noWorkflowRecurse bool) (WorkflowPlanner, error) {
|
||||||
path, err := filepath.Abs(path)
|
path, err := filepath.Abs(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -57,11 +57,11 @@ func TestWorkflow(t *testing.T) {
|
|||||||
|
|
||||||
// Check that an invalid job id returns error
|
// Check that an invalid job id returns error
|
||||||
result, err := createStages(&workflow, "invalid_job_id")
|
result, err := createStages(&workflow, "invalid_job_id")
|
||||||
assert.NotNil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.Error(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
assert.Nil(t, result)
|
assert.Nil(t, result)
|
||||||
|
|
||||||
// Check that an valid job id returns non-error
|
// Check that an valid job id returns non-error
|
||||||
result, err = createStages(&workflow, "valid_job")
|
result, err = createStages(&workflow, "valid_job")
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
assert.NotNil(t, result)
|
assert.NotNil(t, result)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"go.yaml.in/yaml/v4"
|
"go.yaml.in/yaml/v4"
|
||||||
@@ -440,8 +440,6 @@ func (j *Job) Matrix() map[string][]any {
|
|||||||
|
|
||||||
// GetMatrixes returns the matrix cross product
|
// GetMatrixes returns the matrix cross product
|
||||||
// It skips includes and hard fails excludes for non-existing keys
|
// It skips includes and hard fails excludes for non-existing keys
|
||||||
//
|
|
||||||
//nolint:gocyclo // function handles many cases
|
|
||||||
func (j *Job) GetMatrixes() ([]map[string]any, error) {
|
func (j *Job) GetMatrixes() ([]map[string]any, error) {
|
||||||
matrixes := make([]map[string]any, 0)
|
matrixes := make([]map[string]any, 0)
|
||||||
if j.Strategy != nil {
|
if j.Strategy != nil {
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ jobs:
|
|||||||
assert.NoError(t, err, "read workflow should succeed") //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err, "read workflow should succeed") //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
newSchedules = workflow.OnSchedule()
|
newSchedules = workflow.OnSchedule()
|
||||||
assert.Len(t, newSchedules, 0) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.Empty(t, newSchedules)
|
||||||
|
|
||||||
yaml = `
|
yaml = `
|
||||||
name: local-action-docker-url
|
name: local-action-docker-url
|
||||||
@@ -74,7 +74,7 @@ jobs:
|
|||||||
assert.NoError(t, err, "read workflow should succeed") //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err, "read workflow should succeed") //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
newSchedules = workflow.OnSchedule()
|
newSchedules = workflow.OnSchedule()
|
||||||
assert.Len(t, newSchedules, 0) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.Empty(t, newSchedules)
|
||||||
|
|
||||||
yaml = `
|
yaml = `
|
||||||
name: local-action-docker-url
|
name: local-action-docker-url
|
||||||
@@ -91,7 +91,7 @@ jobs:
|
|||||||
assert.NoError(t, err, "read workflow should succeed") //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err, "read workflow should succeed") //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
newSchedules = workflow.OnSchedule()
|
newSchedules = workflow.OnSchedule()
|
||||||
assert.Len(t, newSchedules, 0) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.Empty(t, newSchedules)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReadWorkflow_StringEvent(t *testing.T) {
|
func TestReadWorkflow_StringEvent(t *testing.T) {
|
||||||
@@ -870,7 +870,7 @@ jobs:
|
|||||||
assert.Nil(t, matrix, "matrix should be nil for jobs without strategy")
|
assert.Nil(t, matrix, "matrix should be nil for jobs without strategy")
|
||||||
} else {
|
} else {
|
||||||
assert.NotNil(t, matrix, "matrix should not be nil")
|
assert.NotNil(t, matrix, "matrix should not be nil")
|
||||||
assert.Equal(t, tt.wantLen, len(matrix), "matrix should have expected number of keys") //nolint:testifylint // pre-existing issue from nektos/act
|
assert.Len(t, matrix, tt.wantLen, "matrix should have expected number of keys")
|
||||||
if tt.checkFn != nil {
|
if tt.checkFn != nil {
|
||||||
tt.checkFn(t, matrix)
|
tt.checkFn(t, matrix)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
"gitea.com/gitea/act_runner/act/container"
|
"gitea.com/gitea/runner/act/container"
|
||||||
"gitea.com/gitea/act_runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
|
|
||||||
"github.com/kballard/go-shellquote"
|
"github.com/kballard/go-shellquote"
|
||||||
)
|
)
|
||||||
@@ -265,8 +265,6 @@ func removeGitIgnore(ctx context.Context, directory string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: break out parts of function to reduce complexicity
|
// TODO: break out parts of function to reduce complexicity
|
||||||
//
|
|
||||||
//nolint:gocyclo // function handles many cases
|
|
||||||
func execAsDocker(ctx context.Context, step actionStep, actionName, basedir string, localAction bool) error {
|
func execAsDocker(ctx context.Context, step actionStep, actionName, basedir string, localAction bool) error {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
rc := step.getRunContext()
|
rc := step.getRunContext()
|
||||||
@@ -429,7 +427,7 @@ func newStepContainer(ctx context.Context, step step, image string, cmd, entrypo
|
|||||||
Image: image,
|
Image: image,
|
||||||
Username: rc.Config.Secrets["DOCKER_USERNAME"],
|
Username: rc.Config.Secrets["DOCKER_USERNAME"],
|
||||||
Password: rc.Config.Secrets["DOCKER_PASSWORD"],
|
Password: rc.Config.Secrets["DOCKER_PASSWORD"],
|
||||||
Name: createSimpleContainerName(rc.jobContainerName(), "STEP-"+stepModel.ID),
|
Name: createContainerName(rc.jobContainerName(), "STEP-"+stepModel.ID),
|
||||||
Env: envList,
|
Env: envList,
|
||||||
Mounts: mounts,
|
Mounts: mounts,
|
||||||
NetworkMode: networkMode,
|
NetworkMode: networkMode,
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
"gitea.com/gitea/act_runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func evaluateCompositeInputAndEnv(ctx context.Context, parent *RunContext, step actionStep) map[string]string {
|
func evaluateCompositeInputAndEnv(ctx context.Context, parent *RunContext, step actionStep) map[string]string {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
@@ -137,7 +137,7 @@ runs:
|
|||||||
|
|
||||||
action, err := readActionImpl(context.Background(), tt.step, "actionDir", "actionPath", readFile, writeFile)
|
action, err := readActionImpl(context.Background(), tt.step, "actionDir", "actionPath", readFile, writeFile)
|
||||||
|
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
assert.Equal(t, tt.expected, action)
|
assert.Equal(t, tt.expected, action)
|
||||||
|
|
||||||
closerMock.AssertExpectations(t)
|
closerMock.AssertExpectations(t)
|
||||||
@@ -247,7 +247,7 @@ func TestActionRunner(t *testing.T) {
|
|||||||
|
|
||||||
err := runActionImpl(tt.step, "dir", newRemoteAction("org/repo/path@ref"))(ctx)
|
err := runActionImpl(tt.step, "dir", newRemoteAction("org/repo/path@ref"))(ctx)
|
||||||
|
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
cm.AssertExpectations(t)
|
cm.AssertExpectations(t)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
var commandPatternGA *regexp.Regexp
|
var commandPatternGA *regexp.Regexp
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
"gitea.com/gitea/act_runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus/hooks/test"
|
"github.com/sirupsen/logrus/hooks/test"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
"gitea.com/gitea/act_runner/act/container"
|
"gitea.com/gitea/runner/act/container"
|
||||||
|
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
"gitea.com/gitea/act_runner/act/container"
|
"gitea.com/gitea/runner/act/container"
|
||||||
"gitea.com/gitea/act_runner/act/exprparser"
|
"gitea.com/gitea/runner/act/exprparser"
|
||||||
"gitea.com/gitea/act_runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
|
|
||||||
_ "embed"
|
_ "embed"
|
||||||
|
|
||||||
@@ -405,7 +405,6 @@ func escapeFormatString(in string) string {
|
|||||||
return strings.ReplaceAll(strings.ReplaceAll(in, "{", "{{"), "}", "}}")
|
return strings.ReplaceAll(strings.ReplaceAll(in, "{", "{{"), "}", "}}")
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:gocyclo // function handles many cases
|
|
||||||
func rewriteSubExpression(ctx context.Context, in string, forceFormat bool) (string, error) { //nolint:unparam // pre-existing issue from nektos/act
|
func rewriteSubExpression(ctx context.Context, in string, forceFormat bool) (string, error) { //nolint:unparam // pre-existing issue from nektos/act
|
||||||
if !strings.Contains(in, "${{") || !strings.Contains(in, "}}") {
|
if !strings.Contains(in, "${{") || !strings.Contains(in, "}}") {
|
||||||
return in, nil
|
return in, nil
|
||||||
@@ -472,7 +471,6 @@ func rewriteSubExpression(ctx context.Context, in string, forceFormat bool) (str
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:gocyclo // function handles many cases
|
|
||||||
func getEvaluatorInputs(ctx context.Context, rc *RunContext, step step, ghc *model.GithubContext) map[string]any {
|
func getEvaluatorInputs(ctx context.Context, rc *RunContext, step step, ghc *model.GithubContext) map[string]any {
|
||||||
inputs := map[string]any{}
|
inputs := map[string]any{}
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/exprparser"
|
"gitea.com/gitea/runner/act/exprparser"
|
||||||
"gitea.com/gitea/act_runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
|
|
||||||
assert "github.com/stretchr/testify/assert"
|
assert "github.com/stretchr/testify/assert"
|
||||||
yaml "go.yaml.in/yaml/v4"
|
yaml "go.yaml.in/yaml/v4"
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
"gitea.com/gitea/act_runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
type jobInfo interface {
|
type jobInfo interface {
|
||||||
@@ -24,7 +24,13 @@ type jobInfo interface {
|
|||||||
result(result string)
|
result(result string)
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:contextcheck,gocyclo // composes many step executors
|
// reportStepError emits the GitHub Actions ##[error] annotation and records
|
||||||
|
// the error against the job so the job is reported as failed.
|
||||||
|
func reportStepError(ctx context.Context, err error) {
|
||||||
|
common.Logger(ctx).Errorf("##[error]%v", err)
|
||||||
|
common.SetJobError(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executor {
|
func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executor {
|
||||||
steps := make([]common.Executor, 0)
|
steps := make([]common.Executor, 0)
|
||||||
preSteps := make([]common.Executor, 0)
|
preSteps := make([]common.Executor, 0)
|
||||||
@@ -33,7 +39,7 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
|
|||||||
steps = append(steps, func(ctx context.Context) error {
|
steps = append(steps, func(ctx context.Context) error {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
if len(info.matrix()) > 0 {
|
if len(info.matrix()) > 0 {
|
||||||
logger.Infof("\U0001F9EA Matrix: %v", info.matrix())
|
logger.Infof("Matrix: %v", info.matrix())
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@@ -76,33 +82,36 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
|
|||||||
|
|
||||||
preExec := step.pre()
|
preExec := step.pre()
|
||||||
preSteps = append(preSteps, useStepLogger(rc, stepModel, stepStagePre, func(ctx context.Context) error {
|
preSteps = append(preSteps, useStepLogger(rc, stepModel, stepStagePre, func(ctx context.Context) error {
|
||||||
logger := common.Logger(ctx)
|
|
||||||
preErr := preExec(ctx)
|
preErr := preExec(ctx)
|
||||||
if preErr != nil {
|
if preErr != nil {
|
||||||
logger.Errorf("%v", preErr)
|
reportStepError(ctx, preErr)
|
||||||
common.SetJobError(ctx, preErr)
|
|
||||||
} else if ctx.Err() != nil {
|
} else if ctx.Err() != nil {
|
||||||
logger.Errorf("%v", ctx.Err())
|
reportStepError(ctx, ctx.Err())
|
||||||
common.SetJobError(ctx, ctx.Err())
|
|
||||||
}
|
}
|
||||||
return preErr
|
return preErr
|
||||||
}))
|
}))
|
||||||
|
|
||||||
stepExec := step.main()
|
stepExec := step.main()
|
||||||
steps = append(steps, useStepLogger(rc, stepModel, stepStageMain, func(ctx context.Context) error {
|
steps = append(steps, useStepLogger(rc, stepModel, stepStageMain, func(ctx context.Context) error {
|
||||||
logger := common.Logger(ctx)
|
|
||||||
err := stepExec(ctx)
|
err := stepExec(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("%v", err)
|
reportStepError(ctx, err)
|
||||||
common.SetJobError(ctx, err)
|
|
||||||
} else if ctx.Err() != nil {
|
} else if ctx.Err() != nil {
|
||||||
logger.Errorf("%v", ctx.Err())
|
reportStepError(ctx, ctx.Err())
|
||||||
common.SetJobError(ctx, ctx.Err())
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}))
|
}))
|
||||||
|
|
||||||
postExec := useStepLogger(rc, stepModel, stepStagePost, step.post())
|
postFn := step.post()
|
||||||
|
postExec := useStepLogger(rc, stepModel, stepStagePost, func(ctx context.Context) error {
|
||||||
|
err := postFn(ctx)
|
||||||
|
if err != nil {
|
||||||
|
reportStepError(ctx, err)
|
||||||
|
} else if ctx.Err() != nil {
|
||||||
|
reportStepError(ctx, ctx.Err())
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
})
|
||||||
if postExecutor != nil {
|
if postExecutor != nil {
|
||||||
// run the post executor in reverse order
|
// run the post executor in reverse order
|
||||||
postExecutor = postExec.Finally(postExecutor)
|
postExecutor = postExec.Finally(postExecutor)
|
||||||
@@ -137,7 +146,7 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
|
|||||||
// if !rc.IsHostEnv(ctx) && rc.Config.ContainerNetworkMode == "" {
|
// if !rc.IsHostEnv(ctx) && rc.Config.ContainerNetworkMode == "" {
|
||||||
// // clean network in docker mode only
|
// // clean network in docker mode only
|
||||||
// // if the value of `ContainerNetworkMode` is empty string,
|
// // if the value of `ContainerNetworkMode` is empty string,
|
||||||
// // it means that the network to which containers are connecting is created by `act_runner`,
|
// // it means that the network to which containers are connecting is created by `runner`,
|
||||||
// // so, we should remove the network at last.
|
// // so, we should remove the network at last.
|
||||||
// networkName, _ := rc.networkName()
|
// networkName, _ := rc.networkName()
|
||||||
// logger.Infof("Cleaning up network for job %s, and network name is: %s", rc.JobName, networkName)
|
// logger.Infof("Cleaning up network for job %s, and network name is: %s", rc.JobName, networkName)
|
||||||
@@ -157,7 +166,7 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
|
|||||||
pipeline = append(pipeline, steps...)
|
pipeline = append(pipeline, steps...)
|
||||||
|
|
||||||
return common.NewPipelineExecutor(info.startContainer(), common.NewPipelineExecutor(pipeline...).
|
return common.NewPipelineExecutor(info.startContainer(), common.NewPipelineExecutor(pipeline...).
|
||||||
Finally(func(ctx context.Context) error { //nolint:contextcheck // intentionally detaches from canceled parent
|
Finally(func(ctx context.Context) error {
|
||||||
var cancel context.CancelFunc
|
var cancel context.CancelFunc
|
||||||
if ctx.Err() == context.Canceled {
|
if ctx.Err() == context.Canceled {
|
||||||
// in case of an aborted run, we still should execute the
|
// in case of an aborted run, we still should execute the
|
||||||
@@ -197,7 +206,7 @@ func setJobResult(ctx context.Context, info jobInfo, rc *RunContext, success boo
|
|||||||
jobResultMessage = "failed"
|
jobResultMessage = "failed"
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.WithField("jobResult", jobResult).Infof("\U0001F3C1 Job %s", jobResultMessage)
|
logger.WithField("jobResult", jobResult).Infof("Job %s", jobResultMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setJobOutputs(ctx context.Context, rc *RunContext) {
|
func setJobOutputs(ctx context.Context, rc *RunContext) {
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
"gitea.com/gitea/act_runner/act/container"
|
"gitea.com/gitea/runner/act/container"
|
||||||
"gitea.com/gitea/act_runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
@@ -331,7 +331,7 @@ func TestNewJobExecutor(t *testing.T) {
|
|||||||
|
|
||||||
executor := newJobExecutor(jim, sfm, rc)
|
executor := newJobExecutor(jim, sfm, rc)
|
||||||
err := executor(ctx)
|
err := executor(ctx)
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
assert.Equal(t, tt.executedSteps, executorOrder)
|
assert.Equal(t, tt.executedSteps, executorOrder)
|
||||||
|
|
||||||
jim.AssertExpectations(t)
|
jim.AssertExpectations(t)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/filecollector"
|
"gitea.com/gitea/runner/act/filecollector"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LocalRepositoryCache struct {
|
type LocalRepositoryCache struct {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
@@ -30,6 +30,11 @@ const (
|
|||||||
gray = 37
|
gray = 37
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
rawOutputField = "raw_output"
|
||||||
|
scriptLineCyanField = "script_line_cyan"
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
colors []int
|
colors []int
|
||||||
nextColor int
|
nextColor int
|
||||||
@@ -161,6 +166,8 @@ func withStepLogger(ctx context.Context, stepNumber int, stepID, stepName, stage
|
|||||||
|
|
||||||
type entryProcessor func(entry *logrus.Entry) *logrus.Entry
|
type entryProcessor func(entry *logrus.Entry) *logrus.Entry
|
||||||
|
|
||||||
|
// valueMasker applies secrets and ::add-mask:: patterns to every log entry, including
|
||||||
|
// raw_output (command/stream) lines; there is no bypass by field.
|
||||||
func valueMasker(insecureSecrets bool, secrets map[string]string) entryProcessor {
|
func valueMasker(insecureSecrets bool, secrets map[string]string) entryProcessor {
|
||||||
return func(entry *logrus.Entry) *logrus.Entry {
|
return func(entry *logrus.Entry) *logrus.Entry {
|
||||||
if insecureSecrets {
|
if insecureSecrets {
|
||||||
@@ -227,8 +234,12 @@ func (f *jobLogFormatter) printColored(b *bytes.Buffer, entry *logrus.Entry) {
|
|||||||
debugFlag = "[DEBUG] "
|
debugFlag = "[DEBUG] "
|
||||||
}
|
}
|
||||||
|
|
||||||
if entry.Data["raw_output"] == true {
|
if entry.Data[rawOutputField] == true {
|
||||||
fmt.Fprintf(b, "\x1b[%dm|\x1b[0m %s", f.color, entry.Message)
|
if entry.Data[scriptLineCyanField] == true {
|
||||||
|
fmt.Fprintf(b, "\x1b[%dm|\x1b[0m \x1b[36;1m%s\x1b[0m", f.color, entry.Message)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(b, "\x1b[%dm|\x1b[0m %s", f.color, entry.Message)
|
||||||
|
}
|
||||||
} else if entry.Data["dryrun"] == true {
|
} else if entry.Data["dryrun"] == true {
|
||||||
fmt.Fprintf(b, "\x1b[1m\x1b[%dm\x1b[7m*DRYRUN*\x1b[0m \x1b[%dm[%s] \x1b[0m%s%s", gray, f.color, job, debugFlag, entry.Message)
|
fmt.Fprintf(b, "\x1b[1m\x1b[%dm\x1b[7m*DRYRUN*\x1b[0m \x1b[%dm[%s] \x1b[0m%s%s", gray, f.color, job, debugFlag, entry.Message)
|
||||||
} else {
|
} else {
|
||||||
@@ -251,7 +262,7 @@ func (f *jobLogFormatter) print(b *bytes.Buffer, entry *logrus.Entry) {
|
|||||||
debugFlag = "[DEBUG] "
|
debugFlag = "[DEBUG] "
|
||||||
}
|
}
|
||||||
|
|
||||||
if entry.Data["raw_output"] == true {
|
if entry.Data[rawOutputField] == true {
|
||||||
fmt.Fprintf(b, "[%s] | %s", job, entry.Message)
|
fmt.Fprintf(b, "[%s] | %s", job, entry.Message)
|
||||||
} else if entry.Data["dryrun"] == true {
|
} else if entry.Data["dryrun"] == true {
|
||||||
fmt.Fprintf(b, "*DRYRUN* [%s] %s%s", job, debugFlag, entry.Message)
|
fmt.Fprintf(b, "*DRYRUN* [%s] %s%s", job, debugFlag, entry.Message)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ package runner
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"go.yaml.in/yaml/v4"
|
"go.yaml.in/yaml/v4"
|
||||||
@@ -60,7 +60,7 @@ func TestMaxParallelStrategy(t *testing.T) {
|
|||||||
matrixes, err := job.GetMatrixes()
|
matrixes, err := job.GetMatrixes()
|
||||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
assert.NotNil(t, matrixes)
|
assert.NotNil(t, matrixes)
|
||||||
assert.Equal(t, 5, len(matrixes)) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.Len(t, matrixes, 5)
|
||||||
assert.Equal(t, tt.expectedMaxParallel, job.Strategy.MaxParallel)
|
assert.Equal(t, tt.expectedMaxParallel, job.Strategy.MaxParallel)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
"gitea.com/gitea/act_runner/act/common/git"
|
"gitea.com/gitea/runner/act/common/git"
|
||||||
"gitea.com/gitea/act_runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newLocalReusableWorkflowExecutor(rc *RunContext) common.Executor {
|
func newLocalReusableWorkflowExecutor(rc *RunContext) common.Executor {
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
"gitea.com/gitea/act_runner/act/container"
|
"gitea.com/gitea/runner/act/container"
|
||||||
"gitea.com/gitea/act_runner/act/exprparser"
|
"gitea.com/gitea/runner/act/exprparser"
|
||||||
"gitea.com/gitea/act_runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
|
|
||||||
"github.com/docker/go-connections/nat"
|
"github.com/docker/go-connections/nat"
|
||||||
"github.com/opencontainers/selinux/go-selinux"
|
"github.com/opencontainers/selinux/go-selinux"
|
||||||
@@ -101,8 +101,7 @@ func (rc *RunContext) jobContainerName() string {
|
|||||||
if rc.caller != nil {
|
if rc.caller != nil {
|
||||||
nameParts = append(nameParts, "CALLED-BY-"+rc.caller.runContext.JobName)
|
nameParts = append(nameParts, "CALLED-BY-"+rc.caller.runContext.JobName)
|
||||||
}
|
}
|
||||||
// return createSimpleContainerName(rc.Config.ContainerNamePrefix, "WORKFLOW-"+rc.Run.Workflow.Name, "JOB-"+rc.Name)
|
return createContainerName(nameParts...) // For Gitea
|
||||||
return createSimpleContainerName(nameParts...) // For Gitea
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// networkNameForGitea return the name of the network
|
// networkNameForGitea return the name of the network
|
||||||
@@ -221,11 +220,12 @@ 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,
|
||||||
ActPath: actPath,
|
BindWorkdir: rc.Config.BindWorkdir,
|
||||||
|
ActPath: actPath,
|
||||||
CleanUp: func() {
|
CleanUp: func() {
|
||||||
os.RemoveAll(miscpath)
|
os.RemoveAll(miscpath)
|
||||||
},
|
},
|
||||||
@@ -260,7 +260,6 @@ func (rc *RunContext) startHostEnvironment() common.Executor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:gocyclo // function handles many cases
|
|
||||||
func (rc *RunContext) startJobContainer() common.Executor {
|
func (rc *RunContext) startJobContainer() common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
@@ -383,7 +382,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
|||||||
if createAndDeleteNetwork {
|
if createAndDeleteNetwork {
|
||||||
// clean network if it has been created by act
|
// clean network if it has been created by act
|
||||||
// if using service containers
|
// if using service containers
|
||||||
// it means that the network to which containers are connecting is created by `act_runner`,
|
// it means that the network to which containers are connecting is created by `runner`,
|
||||||
// so, we should remove the network at last.
|
// so, we should remove the network at last.
|
||||||
logger.Infof("Cleaning up network for job %s, and network name is: %s", rc.JobName, networkName)
|
logger.Infof("Cleaning up network for job %s, and network name is: %s", rc.JobName, networkName)
|
||||||
if err := container.NewDockerNetworkRemoveExecutor(networkName)(ctx); err != nil {
|
if err := container.NewDockerNetworkRemoveExecutor(networkName)(ctx); err != nil {
|
||||||
@@ -731,7 +730,7 @@ func (rc *RunContext) isEnabled(ctx context.Context) (bool, error) {
|
|||||||
jobType, jobTypeErr := job.Type()
|
jobType, jobTypeErr := job.Type()
|
||||||
|
|
||||||
if runJobErr != nil {
|
if runJobErr != nil {
|
||||||
return false, fmt.Errorf(" \u274C Error in if-expression: \"if: %s\" (%s)", job.If.Value, runJobErr)
|
return false, fmt.Errorf("if-expression %q evaluation failed: %s", job.If.Value, runJobErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if jobType == model.JobTypeInvalid {
|
if jobType == model.JobTypeInvalid {
|
||||||
@@ -769,7 +768,6 @@ func mergeMaps(maps ...map[string]string) map[string]string {
|
|||||||
return rtnMap
|
return rtnMap
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deprecated: use createSimpleContainerName
|
|
||||||
func createContainerName(parts ...string) string {
|
func createContainerName(parts ...string) string {
|
||||||
name := strings.Join(parts, "-")
|
name := strings.Join(parts, "-")
|
||||||
pattern := regexp.MustCompile("[^a-zA-Z0-9]")
|
pattern := regexp.MustCompile("[^a-zA-Z0-9]")
|
||||||
@@ -783,22 +781,6 @@ func createContainerName(parts ...string) string {
|
|||||||
return fmt.Sprintf("%s-%x", trimmedName, hash)
|
return fmt.Sprintf("%s-%x", trimmedName, hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createSimpleContainerName(parts ...string) string {
|
|
||||||
pattern := regexp.MustCompile("[^a-zA-Z0-9-]")
|
|
||||||
name := make([]string, 0, len(parts))
|
|
||||||
for _, v := range parts {
|
|
||||||
v = pattern.ReplaceAllString(v, "-")
|
|
||||||
v = strings.Trim(v, "-")
|
|
||||||
for strings.Contains(v, "--") {
|
|
||||||
v = strings.ReplaceAll(v, "--", "-")
|
|
||||||
}
|
|
||||||
if v != "" {
|
|
||||||
name = append(name, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return strings.Join(name, "_")
|
|
||||||
}
|
|
||||||
|
|
||||||
func trimToLen(s string, l int) string {
|
func trimToLen(s string, l int) string {
|
||||||
if l < 0 {
|
if l < 0 {
|
||||||
l = 0
|
l = 0
|
||||||
@@ -826,7 +808,6 @@ func (rc *RunContext) getStepsContext() map[string]*model.StepResult {
|
|||||||
return rc.StepResults
|
return rc.StepResults
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:gocyclo // function handles many cases
|
|
||||||
func (rc *RunContext) getGithubContext(ctx context.Context) *model.GithubContext {
|
func (rc *RunContext) getGithubContext(ctx context.Context) *model.GithubContext {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
ghc := &model.GithubContext{
|
ghc := &model.GithubContext{
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/exprparser"
|
"gitea.com/gitea/runner/act/exprparser"
|
||||||
"gitea.com/gitea/act_runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
assert "github.com/stretchr/testify/assert"
|
assert "github.com/stretchr/testify/assert"
|
||||||
@@ -282,7 +282,7 @@ func TestGetGitHubContext(t *testing.T) {
|
|||||||
log.SetLevel(log.DebugLevel)
|
log.SetLevel(log.DebugLevel)
|
||||||
|
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
rc := &RunContext{
|
rc := &RunContext{
|
||||||
Config: &Config{
|
Config: &Config{
|
||||||
@@ -622,23 +622,16 @@ func TestRunContextGetEnv(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_createSimpleContainerName(t *testing.T) {
|
func TestCreateContainerNameBoundedForLongMatrixInput(t *testing.T) {
|
||||||
tests := []struct {
|
longMatrixValue := strings.Repeat("os=ubuntu-latest-go=1.24-node=22-", 20)
|
||||||
parts []string
|
name := createContainerName(
|
||||||
want string
|
"gitea",
|
||||||
}{
|
"WORKFLOW-super-long-workflow-name",
|
||||||
{
|
"JOB-build-matrix-"+longMatrixValue,
|
||||||
parts: []string{"a--a", "BB正", "c-C"},
|
)
|
||||||
want: "a-a_BB_c-C",
|
|
||||||
},
|
assert.LessOrEqual(t, len(name), 128)
|
||||||
{
|
assert.LessOrEqual(t, len(name+"-env"), 255)
|
||||||
parts: []string{"a-a", "", "-"},
|
assert.LessOrEqual(t, len(name+"-network"), 255)
|
||||||
want: "a-a",
|
assert.LessOrEqual(t, len(name+"-job1234567890"), 255)
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(strings.Join(tt.parts, " "), func(t *testing.T) {
|
|
||||||
assert.Equalf(t, tt.want, createSimpleContainerName(tt.parts...), "createSimpleContainerName(%v)", tt.parts)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
"gitea.com/gitea/act_runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
|
|
||||||
docker_container "github.com/docker/docker/api/types/container"
|
docker_container "github.com/docker/docker/api/types/container"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@@ -137,8 +137,6 @@ func (runner *runnerImpl) configure() (Runner, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewPlanExecutor ...
|
// NewPlanExecutor ...
|
||||||
//
|
|
||||||
//nolint:gocyclo // function handles many cases
|
|
||||||
func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor {
|
func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor {
|
||||||
maxJobNameLen := 0
|
maxJobNameLen := 0
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
"gitea.com/gitea/act_runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@@ -87,7 +87,7 @@ func TestGraphMissingEvent(t *testing.T) {
|
|||||||
plan, err := planner.PlanEvent("push")
|
plan, err := planner.PlanEvent("push")
|
||||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
assert.NotNil(t, plan)
|
assert.NotNil(t, plan)
|
||||||
assert.Equal(t, 0, len(plan.Stages)) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.Empty(t, plan.Stages)
|
||||||
|
|
||||||
assert.Contains(t, buf.String(), "no events found for workflow: no-event.yml")
|
assert.Contains(t, buf.String(), "no events found for workflow: no-event.yml")
|
||||||
log.SetOutput(out)
|
log.SetOutput(out)
|
||||||
@@ -100,7 +100,7 @@ func TestGraphMissingFirst(t *testing.T) {
|
|||||||
plan, err := planner.PlanEvent("push")
|
plan, err := planner.PlanEvent("push")
|
||||||
assert.EqualError(t, err, "unable to build dependency graph for no first (no-first.yml)") //nolint:testifylint // pre-existing issue from nektos/act
|
assert.EqualError(t, err, "unable to build dependency graph for no first (no-first.yml)") //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
assert.NotNil(t, plan)
|
assert.NotNil(t, plan)
|
||||||
assert.Equal(t, 0, len(plan.Stages)) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.Empty(t, plan.Stages)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGraphWithMissing(t *testing.T) {
|
func TestGraphWithMissing(t *testing.T) {
|
||||||
@@ -114,7 +114,7 @@ func TestGraphWithMissing(t *testing.T) {
|
|||||||
|
|
||||||
plan, err := planner.PlanEvent("push")
|
plan, err := planner.PlanEvent("push")
|
||||||
assert.NotNil(t, plan)
|
assert.NotNil(t, plan)
|
||||||
assert.Equal(t, 0, len(plan.Stages)) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.Empty(t, plan.Stages)
|
||||||
assert.EqualError(t, err, "unable to build dependency graph for missing (missing.yml)") //nolint:testifylint // pre-existing issue from nektos/act
|
assert.EqualError(t, err, "unable to build dependency graph for missing (missing.yml)") //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
assert.Contains(t, buf.String(), "unable to build dependency graph for missing (missing.yml)")
|
assert.Contains(t, buf.String(), "unable to build dependency graph for missing (missing.yml)")
|
||||||
log.SetOutput(out)
|
log.SetOutput(out)
|
||||||
@@ -134,7 +134,7 @@ func TestGraphWithSomeMissing(t *testing.T) {
|
|||||||
plan, err := planner.PlanAll()
|
plan, err := planner.PlanAll()
|
||||||
assert.Error(t, err, "unable to build dependency graph for no first (no-first.yml)") //nolint:testifylint // pre-existing issue from nektos/act
|
assert.Error(t, err, "unable to build dependency graph for no first (no-first.yml)") //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
assert.NotNil(t, plan)
|
assert.NotNil(t, plan)
|
||||||
assert.Equal(t, 1, len(plan.Stages)) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.Len(t, plan.Stages, 1)
|
||||||
assert.Contains(t, buf.String(), "unable to build dependency graph for missing (missing.yml)")
|
assert.Contains(t, buf.String(), "unable to build dependency graph for missing (missing.yml)")
|
||||||
assert.Contains(t, buf.String(), "unable to build dependency graph for no first (no-first.yml)")
|
assert.Contains(t, buf.String(), "unable to build dependency graph for no first (no-first.yml)")
|
||||||
log.SetOutput(out)
|
log.SetOutput(out)
|
||||||
@@ -159,7 +159,7 @@ func TestGraphEvent(t *testing.T) {
|
|||||||
plan, err = planner.PlanEvent("release")
|
plan, err = planner.PlanEvent("release")
|
||||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
assert.NotNil(t, plan)
|
assert.NotNil(t, plan)
|
||||||
assert.Equal(t, 0, len(plan.Stages)) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.Empty(t, plan.Stages)
|
||||||
}
|
}
|
||||||
|
|
||||||
type TestJobFileInfo struct {
|
type TestJobFileInfo struct {
|
||||||
@@ -177,7 +177,7 @@ func (j *TestJobFileInfo) runTest(ctx context.Context, t *testing.T, cfg *Config
|
|||||||
log.SetLevel(logLevel)
|
log.SetLevel(logLevel)
|
||||||
|
|
||||||
workdir, err := filepath.Abs(j.workdir)
|
workdir, err := filepath.Abs(j.workdir)
|
||||||
assert.Nil(t, err, workdir) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err, workdir) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
fullWorkflowPath := filepath.Join(workdir, j.workflowPath)
|
fullWorkflowPath := filepath.Join(workdir, j.workflowPath)
|
||||||
runnerConfig := &Config{
|
runnerConfig := &Config{
|
||||||
@@ -197,17 +197,17 @@ func (j *TestJobFileInfo) runTest(ctx context.Context, t *testing.T, cfg *Config
|
|||||||
}
|
}
|
||||||
|
|
||||||
runner, err := New(runnerConfig)
|
runner, err := New(runnerConfig)
|
||||||
assert.Nil(t, err, j.workflowPath) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err, j.workflowPath) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
planner, err := model.NewWorkflowPlanner(fullWorkflowPath, true)
|
planner, err := model.NewWorkflowPlanner(fullWorkflowPath, true)
|
||||||
assert.Nil(t, err, fullWorkflowPath) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err, fullWorkflowPath) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
plan, err := planner.PlanEvent(j.eventName)
|
plan, err := planner.PlanEvent(j.eventName)
|
||||||
assert.True(t, (err == nil) != (plan == nil), "PlanEvent should return either a plan or an error") //nolint:testifylint // pre-existing issue from nektos/act
|
assert.True(t, (err == nil) != (plan == nil), "PlanEvent should return either a plan or an error") //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
if err == nil && plan != nil {
|
if err == nil && plan != nil {
|
||||||
err = runner.NewPlanExecutor(plan)(ctx)
|
err = runner.NewPlanExecutor(plan)(ctx)
|
||||||
if j.errorMessage == "" {
|
if j.errorMessage == "" {
|
||||||
assert.Nil(t, err, fullWorkflowPath) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err, fullWorkflowPath) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
} else {
|
} else {
|
||||||
assert.Error(t, err, j.errorMessage) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.Error(t, err, j.errorMessage) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
"gitea.com/gitea/act_runner/act/container"
|
"gitea.com/gitea/runner/act/container"
|
||||||
"gitea.com/gitea/act_runner/act/exprparser"
|
"gitea.com/gitea/runner/act/exprparser"
|
||||||
"gitea.com/gitea/act_runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
type step interface {
|
type step interface {
|
||||||
@@ -107,7 +107,7 @@ func runStepExecutor(step step, stage stepStage, executor common.Executor) commo
|
|||||||
if strings.Contains(stepString, "::add-mask::") {
|
if strings.Contains(stepString, "::add-mask::") {
|
||||||
stepString = "add-mask command"
|
stepString = "add-mask command"
|
||||||
}
|
}
|
||||||
logger.Infof("\u2B50 Run %s %s", stage, stepString)
|
logger.Infof("Run %s %s", stage, stepString)
|
||||||
|
|
||||||
// Prepare and clean Runner File Commands
|
// Prepare and clean Runner File Commands
|
||||||
actPath := rc.JobContainer.GetActPath()
|
actPath := rc.JobContainer.GetActPath()
|
||||||
@@ -158,7 +158,7 @@ func runStepExecutor(step step, stage stepStage, executor common.Executor) commo
|
|||||||
err = executor(timeoutctx)
|
err = executor(timeoutctx)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
logger.WithField("stepResult", stepResult.Outcome).Infof(" \u2705 Success - %s %s", stage, stepString)
|
logger.WithField("stepResult", stepResult.Outcome).Infof("Success - %s %s", stage, stepString)
|
||||||
} else {
|
} else {
|
||||||
stepResult.Outcome = model.StepStatusFailure
|
stepResult.Outcome = model.StepStatusFailure
|
||||||
|
|
||||||
@@ -169,6 +169,7 @@ func runStepExecutor(step step, stage stepStage, executor common.Executor) commo
|
|||||||
}
|
}
|
||||||
|
|
||||||
if continueOnError {
|
if continueOnError {
|
||||||
|
logger.Errorf("##[error]%v", err)
|
||||||
logger.Infof("Failed but continue next step")
|
logger.Infof("Failed but continue next step")
|
||||||
err = nil
|
err = nil
|
||||||
stepResult.Conclusion = model.StepStatusSuccess
|
stepResult.Conclusion = model.StepStatusSuccess
|
||||||
@@ -176,7 +177,9 @@ func runStepExecutor(step step, stage stepStage, executor common.Executor) commo
|
|||||||
stepResult.Conclusion = model.StepStatusFailure
|
stepResult.Conclusion = model.StepStatusFailure
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.WithField("stepResult", stepResult.Outcome).Errorf(" \u274C Failure - %s %s", stage, stepString)
|
// Infof: Errorf entries are promoted to the user log by the reporter,
|
||||||
|
// which would duplicate the ##[error] annotation emitted elsewhere.
|
||||||
|
logger.WithField("stepResult", stepResult.Outcome).Infof("Failure - %s %s", stage, stepString)
|
||||||
}
|
}
|
||||||
// Process Runner File Commands
|
// Process Runner File Commands
|
||||||
orgerr := err
|
orgerr := err
|
||||||
@@ -268,7 +271,7 @@ func isStepEnabled(ctx context.Context, expr string, step step, stage stepStage)
|
|||||||
|
|
||||||
runStep, err := EvalBool(ctx, rc.NewStepExpressionEvaluator(ctx, step), expr, defaultStatusCheck)
|
runStep, err := EvalBool(ctx, rc.NewStepExpressionEvaluator(ctx, step), expr, defaultStatusCheck)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf(" \u274C Error in if-expression: \"if: %s\" (%s)", expr, err)
|
return false, fmt.Errorf("if-expression %q evaluation failed: %s", expr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return runStep, nil
|
return runStep, nil
|
||||||
@@ -284,7 +287,7 @@ func isContinueOnError(ctx context.Context, expr string, step step, _ stepStage)
|
|||||||
|
|
||||||
continueOnError, err := EvalBool(ctx, rc.NewStepExpressionEvaluator(ctx, step), expr, exprparser.DefaultStatusCheckNone)
|
continueOnError, err := EvalBool(ctx, rc.NewStepExpressionEvaluator(ctx, step), expr, exprparser.DefaultStatusCheckNone)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf(" \u274C Error in continue-on-error-expression: \"continue-on-error: %s\" (%s)", expr, err)
|
return false, fmt.Errorf("continue-on-error expression %q evaluation failed: %s", expr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return continueOnError, nil
|
return continueOnError, nil
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
"gitea.com/gitea/act_runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
type stepActionLocal struct {
|
type stepActionLocal struct {
|
||||||
@@ -44,6 +44,10 @@ func (sal *stepActionLocal) main() common.Executor {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
printRunActionHeader(ctx, sal.Step, sal.env, sal.getRunContext())
|
||||||
|
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
|
||||||
|
defer rawLogger.Infof("::endgroup::")
|
||||||
|
|
||||||
actionDir := filepath.Join(sal.getRunContext().Config.Workdir, sal.Step.Uses)
|
actionDir := filepath.Join(sal.getRunContext().Config.Workdir, sal.Step.Uses)
|
||||||
|
|
||||||
localReader := func(ctx context.Context) actionYamlReader {
|
localReader := func(ctx context.Context) actionYamlReader {
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
"gitea.com/gitea/act_runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
@@ -97,10 +97,10 @@ func TestStepActionLocalTest(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
err := sal.pre()(ctx)
|
err := sal.pre()(ctx)
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
err = sal.main()(ctx)
|
err = sal.main()(ctx)
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
cm.AssertExpectations(t)
|
cm.AssertExpectations(t)
|
||||||
salm.AssertExpectations(t)
|
salm.AssertExpectations(t)
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
"gitea.com/gitea/act_runner/act/common/git"
|
"gitea.com/gitea/runner/act/common/git"
|
||||||
"gitea.com/gitea/act_runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
|
|
||||||
gogit "github.com/go-git/go-git/v5"
|
gogit "github.com/go-git/go-git/v5"
|
||||||
)
|
)
|
||||||
@@ -39,7 +39,6 @@ type stepActionRemote struct {
|
|||||||
|
|
||||||
var stepActionRemoteNewCloneExecutor = git.NewGitCloneExecutor
|
var stepActionRemoteNewCloneExecutor = git.NewGitCloneExecutor
|
||||||
|
|
||||||
//nolint:gocyclo // function handles many cases
|
|
||||||
func (sar *stepActionRemote) prepareActionExecutor() common.Executor {
|
func (sar *stepActionRemote) prepareActionExecutor() common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
if sar.remoteAction != nil && sar.action != nil {
|
if sar.remoteAction != nil && sar.action != nil {
|
||||||
@@ -166,6 +165,10 @@ func (sar *stepActionRemote) main() common.Executor {
|
|||||||
return common.NewPipelineExecutor(
|
return common.NewPipelineExecutor(
|
||||||
sar.prepareActionExecutor(),
|
sar.prepareActionExecutor(),
|
||||||
runStepExecutor(sar, stepStageMain, func(ctx context.Context) error {
|
runStepExecutor(sar, stepStageMain, func(ctx context.Context) error {
|
||||||
|
printRunActionHeader(ctx, sar.Step, sar.env, sar.RunContext)
|
||||||
|
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
|
||||||
|
defer rawLogger.Infof("::endgroup::")
|
||||||
|
|
||||||
github := sar.getGithubContext(ctx)
|
github := sar.getGithubContext(ctx)
|
||||||
if sar.remoteAction.IsCheckout() && isLocalCheckout(github, sar.Step) && !sar.RunContext.Config.NoSkipCheckout {
|
if sar.remoteAction.IsCheckout() && isLocalCheckout(github, sar.Step) && !sar.RunContext.Config.NoSkipCheckout {
|
||||||
if sar.RunContext.Config.BindWorkdir {
|
if sar.RunContext.Config.BindWorkdir {
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
"gitea.com/gitea/act_runner/act/common/git"
|
"gitea.com/gitea/runner/act/common/git"
|
||||||
"gitea.com/gitea/act_runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
@@ -272,8 +272,8 @@ func TestStepActionRemotePre(t *testing.T) {
|
|||||||
|
|
||||||
err := sar.pre()(ctx)
|
err := sar.pre()(ctx)
|
||||||
|
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
assert.Equal(t, true, clonedAction) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.True(t, clonedAction)
|
||||||
|
|
||||||
sarm.AssertExpectations(t)
|
sarm.AssertExpectations(t)
|
||||||
})
|
})
|
||||||
@@ -343,8 +343,8 @@ func TestStepActionRemotePreThroughAction(t *testing.T) {
|
|||||||
|
|
||||||
err := sar.pre()(ctx)
|
err := sar.pre()(ctx)
|
||||||
|
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
assert.Equal(t, true, clonedAction) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.True(t, clonedAction)
|
||||||
|
|
||||||
sarm.AssertExpectations(t)
|
sarm.AssertExpectations(t)
|
||||||
})
|
})
|
||||||
@@ -419,7 +419,7 @@ func TestStepActionRemotePreThroughActionToken(t *testing.T) {
|
|||||||
|
|
||||||
err := sar.pre()(ctx)
|
err := sar.pre()(ctx)
|
||||||
|
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
// Verify that the clone was called (URL should be redirected to github.com)
|
// Verify that the clone was called (URL should be redirected to github.com)
|
||||||
assert.True(t, actualURL != "", "Expected clone to be called") //nolint:testifylint // pre-existing issue from nektos/act
|
assert.True(t, actualURL != "", "Expected clone to be called") //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
assert.Equal(t, "https://github.com/org/repo", actualURL, "URL should be redirected to github.com")
|
assert.Equal(t, "https://github.com/org/repo", actualURL, "URL should be redirected to github.com")
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
"gitea.com/gitea/act_runner/act/container"
|
"gitea.com/gitea/runner/act/container"
|
||||||
"gitea.com/gitea/act_runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
|
|
||||||
"github.com/kballard/go-shellquote"
|
"github.com/kballard/go-shellquote"
|
||||||
)
|
)
|
||||||
@@ -116,6 +116,10 @@ func (sd *stepDocker) newStepContainer(ctx context.Context, image string, cmd, e
|
|||||||
envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_TEMP", "/tmp"))
|
envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_TEMP", "/tmp"))
|
||||||
|
|
||||||
binds, mounts := rc.GetBindsAndMounts()
|
binds, mounts := rc.GetBindsAndMounts()
|
||||||
|
networkMode := "container:" + rc.jobContainerName()
|
||||||
|
if rc.IsHostEnv(ctx) {
|
||||||
|
networkMode = "default"
|
||||||
|
}
|
||||||
stepContainer := ContainerNewContainer(&container.NewContainerInput{
|
stepContainer := ContainerNewContainer(&container.NewContainerInput{
|
||||||
Cmd: cmd,
|
Cmd: cmd,
|
||||||
Entrypoint: entrypoint,
|
Entrypoint: entrypoint,
|
||||||
@@ -123,10 +127,10 @@ func (sd *stepDocker) newStepContainer(ctx context.Context, image string, cmd, e
|
|||||||
Image: image,
|
Image: image,
|
||||||
Username: rc.Config.Secrets["DOCKER_USERNAME"],
|
Username: rc.Config.Secrets["DOCKER_USERNAME"],
|
||||||
Password: rc.Config.Secrets["DOCKER_PASSWORD"],
|
Password: rc.Config.Secrets["DOCKER_PASSWORD"],
|
||||||
Name: createSimpleContainerName(rc.jobContainerName(), "STEP-"+step.ID),
|
Name: createContainerName(rc.jobContainerName(), "STEP-"+step.ID),
|
||||||
Env: envList,
|
Env: envList,
|
||||||
Mounts: mounts,
|
Mounts: mounts,
|
||||||
NetworkMode: "container:" + rc.jobContainerName(),
|
NetworkMode: networkMode,
|
||||||
Binds: binds,
|
Binds: binds,
|
||||||
Stdout: logWriter,
|
Stdout: logWriter,
|
||||||
Stderr: logWriter,
|
Stderr: logWriter,
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/container"
|
"gitea.com/gitea/runner/act/container"
|
||||||
"gitea.com/gitea/act_runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
@@ -101,7 +102,7 @@ func TestStepDockerMain(t *testing.T) {
|
|||||||
cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/pathcmd.txt").Return(io.NopCloser(&bytes.Buffer{}), nil)
|
cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/pathcmd.txt").Return(io.NopCloser(&bytes.Buffer{}), nil)
|
||||||
|
|
||||||
err := sd.main()(ctx)
|
err := sd.main()(ctx)
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
assert.Equal(t, "node:14", input.Image)
|
assert.Equal(t, "node:14", input.Image)
|
||||||
|
|
||||||
@@ -113,8 +114,86 @@ func TestStepDockerPrePost(t *testing.T) {
|
|||||||
sd := &stepDocker{}
|
sd := &stepDocker{}
|
||||||
|
|
||||||
err := sd.pre()(ctx)
|
err := sd.pre()(ctx)
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
err = sd.post()(ctx)
|
err = sd.post()(ctx)
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStepDockerNewStepContainerNetworkMode(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
platform string
|
||||||
|
expectDefault bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "docker mode attaches to job container network",
|
||||||
|
platform: "node:14",
|
||||||
|
expectDefault: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "host mode uses default network",
|
||||||
|
platform: "-self-hosted",
|
||||||
|
expectDefault: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
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()
|
||||||
|
|
||||||
|
platform := tc.platform
|
||||||
|
sd := &stepDocker{
|
||||||
|
RunContext: &RunContext{
|
||||||
|
StepResults: map[string]*model.StepResult{},
|
||||||
|
Config: &Config{
|
||||||
|
PlatformPicker: func(_ []string) string {
|
||||||
|
return platform
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Run: &model.Run{
|
||||||
|
JobID: "1",
|
||||||
|
Workflow: &model.Workflow{
|
||||||
|
Jobs: map[string]*model.Job{
|
||||||
|
"1": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
JobContainer: cm,
|
||||||
|
},
|
||||||
|
Step: &model.Step{
|
||||||
|
ID: "1",
|
||||||
|
Uses: "docker://alpine:3.20",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
sd.RunContext.ExprEval = sd.RunContext.NewExpressionEvaluator(ctx)
|
||||||
|
|
||||||
|
assert.Equal(t, tc.expectDefault, sd.RunContext.IsHostEnv(ctx),
|
||||||
|
"IsHostEnv mismatch for platform %q", tc.platform)
|
||||||
|
|
||||||
|
_ = sd.newStepContainer(ctx, "alpine:3.20", []string{"echo", "hello"}, nil)
|
||||||
|
|
||||||
|
if tc.expectDefault {
|
||||||
|
assert.Equal(t, "default", captured.NetworkMode,
|
||||||
|
"host-mode step container must use 'default' network, got %q",
|
||||||
|
captured.NetworkMode)
|
||||||
|
} else {
|
||||||
|
assert.True(t, strings.HasPrefix(captured.NetworkMode, "container:"),
|
||||||
|
"docker-mode step container must attach to job container network, got %q",
|
||||||
|
captured.NetworkMode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ package runner
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
type stepFactory interface {
|
type stepFactory interface {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ package runner
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
@@ -67,7 +67,7 @@ func TestStepFactoryNewStep(t *testing.T) {
|
|||||||
step, err := sf.newStep(tt.model, &RunContext{})
|
step, err := sf.newStep(tt.model, &RunContext{})
|
||||||
|
|
||||||
assert.True(t, tt.check((step)))
|
assert.True(t, tt.check((step)))
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,23 +9,27 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
"gitea.com/gitea/act_runner/act/container"
|
"gitea.com/gitea/runner/act/container"
|
||||||
"gitea.com/gitea/act_runner/act/lookpath"
|
"gitea.com/gitea/runner/act/lookpath"
|
||||||
"gitea.com/gitea/act_runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
|
|
||||||
"github.com/kballard/go-shellquote"
|
"github.com/kballard/go-shellquote"
|
||||||
|
yaml "go.yaml.in/yaml/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
type stepRun struct {
|
type stepRun struct {
|
||||||
Step *model.Step
|
Step *model.Step
|
||||||
RunContext *RunContext
|
RunContext *RunContext
|
||||||
cmd []string
|
cmd []string
|
||||||
cmdline string
|
cmdline string
|
||||||
env map[string]string
|
env map[string]string
|
||||||
WorkingDirectory string
|
WorkingDirectory string
|
||||||
|
interpolatedScript string
|
||||||
|
shellCommand string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sr *stepRun) pre() common.Executor {
|
func (sr *stepRun) pre() common.Executor {
|
||||||
@@ -39,15 +43,154 @@ func (sr *stepRun) main() common.Executor {
|
|||||||
return runStepExecutor(sr, stepStageMain, common.NewPipelineExecutor(
|
return runStepExecutor(sr, stepStageMain, common.NewPipelineExecutor(
|
||||||
sr.setupShellCommandExecutor(),
|
sr.setupShellCommandExecutor(),
|
||||||
func(ctx context.Context) error {
|
func(ctx context.Context) error {
|
||||||
sr.getRunContext().ApplyExtraPath(ctx, &sr.env)
|
rc := sr.getRunContext()
|
||||||
if he, ok := sr.getRunContext().JobContainer.(*container.HostEnvironment); ok && he != nil {
|
// Apply ::add-path:: effects before printing so PATH is accurate in the env: block.
|
||||||
|
rc.ApplyExtraPath(ctx, &sr.env)
|
||||||
|
sr.printRunScriptActionDetails(ctx)
|
||||||
|
if he, ok := rc.JobContainer.(*container.HostEnvironment); ok && he != nil {
|
||||||
return he.ExecWithCmdLine(sr.cmd, sr.cmdline, sr.env, "", sr.WorkingDirectory)(ctx)
|
return he.ExecWithCmdLine(sr.cmd, sr.cmdline, sr.env, "", sr.WorkingDirectory)(ctx)
|
||||||
}
|
}
|
||||||
return sr.getRunContext().JobContainer.Exec(sr.cmd, sr.env, "", sr.WorkingDirectory)(ctx)
|
return rc.JobContainer.Exec(sr.cmd, sr.env, "", sr.WorkingDirectory)(ctx)
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// printRunScriptActionDetails mirrors actions/runner ScriptHandler.PrintActionDetails
|
||||||
|
// for script steps.
|
||||||
|
func (sr *stepRun) printRunScriptActionDetails(ctx context.Context) {
|
||||||
|
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
|
||||||
|
scriptLineLogger := rawLogger.WithField(scriptLineCyanField, true)
|
||||||
|
|
||||||
|
normalized := strings.TrimRight(strings.ReplaceAll(sr.interpolatedScript, "\r\n", "\n"), "\n")
|
||||||
|
|
||||||
|
rawLogger.Infof("::group::Run %s", sr.runScriptGroupTitle(normalized))
|
||||||
|
|
||||||
|
if normalized != "" {
|
||||||
|
for line := range strings.SplitSeq(normalized, "\n") {
|
||||||
|
scriptLineLogger.Info(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rawLogger.Infof("shell: %s", sr.shellCommand)
|
||||||
|
|
||||||
|
printStepEnvBlock(ctx, sr.Step, sr.env, sr.getRunContext())
|
||||||
|
rawLogger.Infof("::endgroup::")
|
||||||
|
}
|
||||||
|
|
||||||
|
// printRunActionHeader mirrors actions/runner's "Run <action>" header for `uses:` steps,
|
||||||
|
// including the with: inputs and the step-level env: block. The caller is responsible
|
||||||
|
// for emitting ::endgroup:: after the action finishes.
|
||||||
|
func printRunActionHeader(ctx context.Context, step *model.Step, env map[string]string, rc *RunContext) {
|
||||||
|
if step == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
|
||||||
|
|
||||||
|
title := step.Uses
|
||||||
|
if step.Name != "" {
|
||||||
|
title = step.Name
|
||||||
|
}
|
||||||
|
rawLogger.Infof("::group::Run %s", title)
|
||||||
|
|
||||||
|
if len(step.With) > 0 {
|
||||||
|
rawLogger.Infof("with:")
|
||||||
|
for _, k := range slices.Sorted(maps.Keys(step.With)) {
|
||||||
|
rawLogger.Infof(" %s: %s", k, step.With[k])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
printStepEnvBlock(ctx, step, env, rc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// printStepEnvBlock emits the declared-env block (YAML order, internal vars filtered)
|
||||||
|
// shared by the run: and uses: "Run" headers.
|
||||||
|
func printStepEnvBlock(ctx context.Context, step *model.Step, env map[string]string, rc *RunContext) {
|
||||||
|
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
|
||||||
|
caseInsensitive := rc != nil && rc.JobContainer != nil && rc.JobContainer.IsEnvironmentCaseInsensitive()
|
||||||
|
var visible []string
|
||||||
|
for _, k := range stepDeclaredEnvKeysInOrder(step) {
|
||||||
|
if !isInternalEnvKey(k, caseInsensitive) {
|
||||||
|
visible = append(visible, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(visible) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rawLogger.Infof("env:")
|
||||||
|
envLookup := env
|
||||||
|
if caseInsensitive {
|
||||||
|
envLookup = make(map[string]string, len(env))
|
||||||
|
for k, v := range env {
|
||||||
|
envLookup[strings.ToUpper(k)] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, k := range visible {
|
||||||
|
lookupKey := k
|
||||||
|
if caseInsensitive {
|
||||||
|
lookupKey = strings.ToUpper(k)
|
||||||
|
}
|
||||||
|
rawLogger.Infof(" %s: %s", k, envLookup[lookupKey])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isInternalEnvKey matches actions/runner's filtered set of vars that are hidden
|
||||||
|
// from the "Run" header's env: block because they are injected by the runner itself.
|
||||||
|
func isInternalEnvKey(k string, caseInsensitive bool) bool {
|
||||||
|
upper := k
|
||||||
|
if caseInsensitive {
|
||||||
|
upper = strings.ToUpper(k)
|
||||||
|
}
|
||||||
|
switch upper {
|
||||||
|
case "PATH", "HOME", "CI":
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return strings.HasPrefix(upper, "GITHUB_") ||
|
||||||
|
strings.HasPrefix(upper, "GITEA_") ||
|
||||||
|
strings.HasPrefix(upper, "RUNNER_") ||
|
||||||
|
strings.HasPrefix(upper, "INPUT_")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sr *stepRun) runScriptGroupTitle(normalizedScript string) string {
|
||||||
|
trimmed := strings.TrimLeft(normalizedScript, " \t\r\n")
|
||||||
|
if idx := strings.IndexAny(trimmed, "\r\n"); idx >= 0 {
|
||||||
|
trimmed = trimmed[:idx]
|
||||||
|
}
|
||||||
|
if trimmed != "" {
|
||||||
|
return trimmed
|
||||||
|
}
|
||||||
|
if sr.Step != nil {
|
||||||
|
if sr.Step.Name != "" {
|
||||||
|
return sr.Step.Name
|
||||||
|
}
|
||||||
|
return sr.Step.ID
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// stepDeclaredEnvKeysInOrder walks the raw YAML Env mapping so keys are emitted in
|
||||||
|
// the order the workflow author wrote them; step.Environment() decodes into a Go map
|
||||||
|
// and loses ordering.
|
||||||
|
func stepDeclaredEnvKeysInOrder(step *model.Step) []string {
|
||||||
|
if step == nil || step.Env.Kind != yaml.MappingNode {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
content := step.Env.Content
|
||||||
|
keys := make([]string, 0, len(content)/2)
|
||||||
|
seen := make(map[string]struct{}, len(content)/2)
|
||||||
|
for i := 0; i+1 < len(content); i += 2 {
|
||||||
|
k := content[i]
|
||||||
|
if k.Kind != yaml.ScalarNode || k.Tag == "!!merge" || k.Value == "<<" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, dup := seen[k.Value]; dup {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[k.Value] = struct{}{}
|
||||||
|
keys = append(keys, k.Value)
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
func (sr *stepRun) post() common.Executor {
|
func (sr *stepRun) post() common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
@@ -111,8 +254,10 @@ func (sr *stepRun) setupShellCommand(ctx context.Context) (name, script string,
|
|||||||
step := sr.Step
|
step := sr.Step
|
||||||
|
|
||||||
script = sr.RunContext.NewStepExpressionEvaluator(ctx, sr).Interpolate(ctx, step.Run)
|
script = sr.RunContext.NewStepExpressionEvaluator(ctx, sr).Interpolate(ctx, step.Run)
|
||||||
|
sr.interpolatedScript = script
|
||||||
|
|
||||||
scCmd := step.ShellCommand()
|
scCmd := step.ShellCommand()
|
||||||
|
sr.shellCommand = scCmd
|
||||||
|
|
||||||
name = getScriptName(sr.RunContext, step)
|
name = getScriptName(sr.RunContext, step)
|
||||||
|
|
||||||
|
|||||||
182
act/runner/step_run_print_test.go
Normal file
182
act/runner/step_run_print_test.go
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// Copyright 2026 The nektos/act Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package runner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
"gitea.com/gitea/runner/act/model"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
yaml "go.yaml.in/yaml/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRunScriptGroupTitle(t *testing.T) {
|
||||||
|
sr := &stepRun{Step: &model.Step{Name: "Build"}}
|
||||||
|
assert.Equal(t, "make build", sr.runScriptGroupTitle("make build"))
|
||||||
|
assert.Equal(t, "echo one", sr.runScriptGroupTitle(" \techo one\necho two"))
|
||||||
|
assert.Equal(t, "Build", sr.runScriptGroupTitle(""))
|
||||||
|
|
||||||
|
sr = &stepRun{Step: &model.Step{ID: "s1"}}
|
||||||
|
assert.Equal(t, "s1", sr.runScriptGroupTitle("\n \n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStepDeclaredEnvOrderPreservesYAML(t *testing.T) {
|
||||||
|
raw := `id: s1
|
||||||
|
run: "echo 1"
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: tok
|
||||||
|
PATH: /custom/bin
|
||||||
|
MY_VAR: hello
|
||||||
|
`
|
||||||
|
var step model.Step
|
||||||
|
require.NoError(t, yaml.Unmarshal([]byte(raw), &step))
|
||||||
|
assert.Equal(t, []string{"GITHUB_TOKEN", "PATH", "MY_VAR"}, stepDeclaredEnvKeysInOrder(&step))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStepDeclaredEnvKeysInOrderEmpty(t *testing.T) {
|
||||||
|
assert.Nil(t, stepDeclaredEnvKeysInOrder(nil))
|
||||||
|
assert.Empty(t, stepDeclaredEnvKeysInOrder(&model.Step{}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStepDeclaredEnvKeysIgnoreYAMLMergeKey(t *testing.T) {
|
||||||
|
doc := `
|
||||||
|
common: &common
|
||||||
|
COMMON_A: a
|
||||||
|
COMMON_B: b
|
||||||
|
step:
|
||||||
|
env:
|
||||||
|
LOCAL_BEFORE: before
|
||||||
|
<<: *common
|
||||||
|
COMMON_B: overridden
|
||||||
|
LOCAL_AFTER: after
|
||||||
|
`
|
||||||
|
var root struct {
|
||||||
|
Step model.Step `yaml:"step"`
|
||||||
|
}
|
||||||
|
require.NoError(t, yaml.Unmarshal([]byte(doc), &root))
|
||||||
|
|
||||||
|
keys := stepDeclaredEnvKeysInOrder(&root.Step)
|
||||||
|
assert.Equal(t, []string{"LOCAL_BEFORE", "COMMON_B", "LOCAL_AFTER"}, keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrintRunScriptActionDetailsGolden(t *testing.T) {
|
||||||
|
raw := `id: s1
|
||||||
|
name: Build
|
||||||
|
run: |
|
||||||
|
echo one
|
||||||
|
echo two
|
||||||
|
shell: pwsh
|
||||||
|
env:
|
||||||
|
PATH_PREFIX: /custom/bin
|
||||||
|
GITHUB_TOKEN: tok
|
||||||
|
GREETING: hello
|
||||||
|
`
|
||||||
|
var step model.Step
|
||||||
|
require.NoError(t, yaml.Unmarshal([]byte(raw), &step))
|
||||||
|
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
logger := logrus.New()
|
||||||
|
logger.SetOutput(buf)
|
||||||
|
logger.SetLevel(logrus.InfoLevel)
|
||||||
|
logger.SetFormatter(&jobLogFormatter{color: cyan})
|
||||||
|
entry := logger.WithFields(logrus.Fields{"job": "j1"})
|
||||||
|
ctx := common.WithLogger(context.Background(), entry)
|
||||||
|
|
||||||
|
sr := &stepRun{
|
||||||
|
Step: &step,
|
||||||
|
RunContext: &RunContext{},
|
||||||
|
shellCommand: "pwsh -command . '{0}'",
|
||||||
|
interpolatedScript: "echo one\necho two\n",
|
||||||
|
env: map[string]string{
|
||||||
|
"PATH_PREFIX": "/custom/bin",
|
||||||
|
"GITHUB_TOKEN": "tok",
|
||||||
|
"GREETING": "hello",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
sr.printRunScriptActionDetails(ctx)
|
||||||
|
|
||||||
|
want := strings.Join([]string{
|
||||||
|
"[j1] | ::group::Run echo one",
|
||||||
|
"[j1] | echo one",
|
||||||
|
"[j1] | echo two",
|
||||||
|
"[j1] | shell: pwsh -command . '{0}'",
|
||||||
|
"[j1] | env:",
|
||||||
|
"[j1] | PATH_PREFIX: /custom/bin",
|
||||||
|
"[j1] | GREETING: hello",
|
||||||
|
"[j1] | ::endgroup::",
|
||||||
|
"",
|
||||||
|
}, "\n")
|
||||||
|
assert.Equal(t, want, buf.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrintRunActionHeaderGolden(t *testing.T) {
|
||||||
|
raw := `id: s1
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: "0"
|
||||||
|
token: secret
|
||||||
|
env:
|
||||||
|
CUSTOM: value
|
||||||
|
GITHUB_TOKEN: tok
|
||||||
|
`
|
||||||
|
var step model.Step
|
||||||
|
require.NoError(t, yaml.Unmarshal([]byte(raw), &step))
|
||||||
|
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
logger := logrus.New()
|
||||||
|
logger.SetOutput(buf)
|
||||||
|
logger.SetLevel(logrus.InfoLevel)
|
||||||
|
logger.SetFormatter(&jobLogFormatter{color: cyan})
|
||||||
|
entry := logger.WithFields(logrus.Fields{"job": "j1"})
|
||||||
|
ctx := common.WithLogger(context.Background(), entry)
|
||||||
|
|
||||||
|
printRunActionHeader(ctx, &step, map[string]string{"CUSTOM": "value", "GITHUB_TOKEN": "tok"}, &RunContext{})
|
||||||
|
|
||||||
|
want := strings.Join([]string{
|
||||||
|
"[j1] | ::group::Run actions/checkout@v4",
|
||||||
|
"[j1] | with:",
|
||||||
|
"[j1] | fetch-depth: 0",
|
||||||
|
"[j1] | token: secret",
|
||||||
|
"[j1] | env:",
|
||||||
|
"[j1] | CUSTOM: value",
|
||||||
|
"",
|
||||||
|
}, "\n")
|
||||||
|
assert.Equal(t, want, buf.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsInternalEnvKey(t *testing.T) {
|
||||||
|
for _, k := range []string{"PATH", "HOME", "CI", "GITHUB_TOKEN", "GITEA_ACTIONS", "RUNNER_OS", "INPUT_FOO"} {
|
||||||
|
assert.True(t, isInternalEnvKey(k, false), k)
|
||||||
|
}
|
||||||
|
for _, k := range []string{"PATH_PREFIX", "MY_VAR", "GREETING", "HOMEPAGE"} {
|
||||||
|
assert.False(t, isInternalEnvKey(k, false), k)
|
||||||
|
}
|
||||||
|
assert.True(t, isInternalEnvKey("path", true))
|
||||||
|
assert.False(t, isInternalEnvKey("path", false))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrintColoredScriptLineCyan(t *testing.T) {
|
||||||
|
f := &jobLogFormatter{color: cyan}
|
||||||
|
entry := &logrus.Entry{
|
||||||
|
Level: logrus.InfoLevel,
|
||||||
|
Message: "echo one",
|
||||||
|
Data: logrus.Fields{
|
||||||
|
"job": "j1",
|
||||||
|
rawOutputField: true,
|
||||||
|
scriptLineCyanField: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
f.printColored(buf, entry)
|
||||||
|
assert.Equal(t, "\x1b[36m|\x1b[0m \x1b[36;1mecho one\x1b[0m", buf.String())
|
||||||
|
}
|
||||||
@@ -10,8 +10,8 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/container"
|
"gitea.com/gitea/runner/act/container"
|
||||||
"gitea.com/gitea/act_runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
@@ -81,7 +81,7 @@ func TestStepRun(t *testing.T) {
|
|||||||
cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/pathcmd.txt").Return(io.NopCloser(&bytes.Buffer{}), nil)
|
cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/pathcmd.txt").Return(io.NopCloser(&bytes.Buffer{}), nil)
|
||||||
|
|
||||||
err := sr.main()(ctx)
|
err := sr.main()(ctx)
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
cm.AssertExpectations(t)
|
cm.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
@@ -91,8 +91,8 @@ func TestStepRunPrePost(t *testing.T) {
|
|||||||
sr := &stepRun{}
|
sr := &stepRun{}
|
||||||
|
|
||||||
err := sr.pre()(ctx)
|
err := sr.pre()(ctx)
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
err = sr.post()(ctx)
|
err = sr.post()(ctx)
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
"gitea.com/gitea/act_runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -156,7 +156,7 @@ func TestSetupEnv(t *testing.T) {
|
|||||||
sm.On("getEnv").Return(&env)
|
sm.On("getEnv").Return(&env)
|
||||||
|
|
||||||
err := setupEnv(context.Background(), sm)
|
err := setupEnv(context.Background(), sm)
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
// These are commit or system specific
|
// These are commit or system specific
|
||||||
delete((env), "GITHUB_REF")
|
delete((env), "GITHUB_REF")
|
||||||
@@ -318,35 +318,35 @@ func TestIsContinueOnError(t *testing.T) {
|
|||||||
step := createTestStep(t, "name: test")
|
step := createTestStep(t, "name: test")
|
||||||
continueOnError, err := isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain)
|
continueOnError, err := isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain)
|
||||||
assertObject.False(continueOnError)
|
assertObject.False(continueOnError)
|
||||||
assertObject.Nil(err) //nolint:testifylint // pre-existing issue from nektos/act
|
assertObject.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
// explcit true
|
// explcit true
|
||||||
step = createTestStep(t, "continue-on-error: true")
|
step = createTestStep(t, "continue-on-error: true")
|
||||||
continueOnError, err = isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain)
|
continueOnError, err = isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain)
|
||||||
assertObject.True(continueOnError)
|
assertObject.True(continueOnError)
|
||||||
assertObject.Nil(err) //nolint:testifylint // pre-existing issue from nektos/act
|
assertObject.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
// explicit false
|
// explicit false
|
||||||
step = createTestStep(t, "continue-on-error: false")
|
step = createTestStep(t, "continue-on-error: false")
|
||||||
continueOnError, err = isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain)
|
continueOnError, err = isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain)
|
||||||
assertObject.False(continueOnError)
|
assertObject.False(continueOnError)
|
||||||
assertObject.Nil(err) //nolint:testifylint // pre-existing issue from nektos/act
|
assertObject.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
// expression true
|
// expression true
|
||||||
step = createTestStep(t, "continue-on-error: ${{ 'test' == 'test' }}")
|
step = createTestStep(t, "continue-on-error: ${{ 'test' == 'test' }}")
|
||||||
continueOnError, err = isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain)
|
continueOnError, err = isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain)
|
||||||
assertObject.True(continueOnError)
|
assertObject.True(continueOnError)
|
||||||
assertObject.Nil(err) //nolint:testifylint // pre-existing issue from nektos/act
|
assertObject.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
// expression false
|
// expression false
|
||||||
step = createTestStep(t, "continue-on-error: ${{ 'test' != 'test' }}")
|
step = createTestStep(t, "continue-on-error: ${{ 'test' != 'test' }}")
|
||||||
continueOnError, err = isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain)
|
continueOnError, err = isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain)
|
||||||
assertObject.False(continueOnError)
|
assertObject.False(continueOnError)
|
||||||
assertObject.Nil(err) //nolint:testifylint // pre-existing issue from nektos/act
|
assertObject.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
// expression parse error
|
// expression parse error
|
||||||
step = createTestStep(t, "continue-on-error: ${{ 'test' != test }}")
|
step = createTestStep(t, "continue-on-error: ${{ 'test' != test }}")
|
||||||
continueOnError, err = isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain)
|
continueOnError, err = isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain)
|
||||||
assertObject.False(continueOnError)
|
assertObject.False(continueOnError)
|
||||||
assertObject.NotNil(err) //nolint:testifylint // pre-existing issue from nektos/act
|
assertObject.Error(err)
|
||||||
}
|
}
|
||||||
|
|||||||
43
act/runner/testdata/actions/node12/package-lock.json
generated
vendored
43
act/runner/testdata/actions/node12/package-lock.json
generated
vendored
@@ -13,19 +13,29 @@
|
|||||||
"@actions/github": "^4.0.0"
|
"@actions/github": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vercel/ncc": "^0.24.1"
|
"@vercel/ncc": "^0.38.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@actions/core": {
|
"node_modules/@actions/core": {
|
||||||
"version": "1.10.0",
|
"version": "1.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz",
|
||||||
"integrity": "sha512-2aZDDa3zrrZbP5ZYg159sNoLRb61nQ7awl5pSvIq5Qpj81vwDzdMRKzkWJGJuwVvWpvZKx7vspJALyvaaIQyug==",
|
"integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/http-client": "^2.0.1",
|
"@actions/exec": "^1.1.1",
|
||||||
"uuid": "^8.3.2"
|
"@actions/http-client": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@actions/exec": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@actions/io": "^1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@actions/github": {
|
"node_modules/@actions/github": {
|
||||||
@@ -55,6 +65,12 @@
|
|||||||
"tunnel": "^0.0.6"
|
"tunnel": "^0.0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@actions/io": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@octokit/auth-token": {
|
"node_modules/@octokit/auth-token": {
|
||||||
"version": "2.5.0",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz",
|
||||||
@@ -157,10 +173,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vercel/ncc": {
|
"node_modules/@vercel/ncc": {
|
||||||
"version": "0.24.1",
|
"version": "0.38.4",
|
||||||
"resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.24.1.tgz",
|
"resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.38.4.tgz",
|
||||||
"integrity": "sha512-r9m7brz2hNmq5TF3sxrK4qR/FhXn44XIMglQUir4sT7Sh5GOaYXlMYikHFwJStf8rmQGTlvOoBXt4yHVonRG8A==",
|
"integrity": "sha512-8LwjnlP39s08C08J5NstzriPvW1SP8Zfpp1BvC2sI35kPeZnHfxVkCwu4/+Wodgnd60UtT1n8K8zw+Mp7J9JmQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"ncc": "dist/ncc/cli.js"
|
"ncc": "dist/ncc/cli.js"
|
||||||
}
|
}
|
||||||
@@ -228,14 +245,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
|
||||||
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
|
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
|
||||||
},
|
},
|
||||||
"node_modules/uuid": {
|
|
||||||
"version": "8.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
|
||||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
|
||||||
"bin": {
|
|
||||||
"uuid": "dist/bin/uuid"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/webidl-conversions": {
|
"node_modules/webidl-conversions": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
"@actions/github": "^4.0.0"
|
"@actions/github": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vercel/ncc": "^0.24.1"
|
"@vercel/ncc": "^0.38.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
|
|||||||
43
act/runner/testdata/actions/node16/package-lock.json
generated
vendored
43
act/runner/testdata/actions/node16/package-lock.json
generated
vendored
@@ -13,19 +13,29 @@
|
|||||||
"@actions/github": "^4.0.0"
|
"@actions/github": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vercel/ncc": "^0.24.1"
|
"@vercel/ncc": "^0.38.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16"
|
"node": ">=16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@actions/core": {
|
"node_modules/@actions/core": {
|
||||||
"version": "1.10.0",
|
"version": "1.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz",
|
||||||
"integrity": "sha512-2aZDDa3zrrZbP5ZYg159sNoLRb61nQ7awl5pSvIq5Qpj81vwDzdMRKzkWJGJuwVvWpvZKx7vspJALyvaaIQyug==",
|
"integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/http-client": "^2.0.1",
|
"@actions/exec": "^1.1.1",
|
||||||
"uuid": "^8.3.2"
|
"@actions/http-client": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@actions/exec": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@actions/io": "^1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@actions/github": {
|
"node_modules/@actions/github": {
|
||||||
@@ -55,6 +65,12 @@
|
|||||||
"tunnel": "^0.0.6"
|
"tunnel": "^0.0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@actions/io": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@octokit/auth-token": {
|
"node_modules/@octokit/auth-token": {
|
||||||
"version": "2.5.0",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz",
|
||||||
@@ -157,10 +173,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vercel/ncc": {
|
"node_modules/@vercel/ncc": {
|
||||||
"version": "0.24.1",
|
"version": "0.38.4",
|
||||||
"resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.24.1.tgz",
|
"resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.38.4.tgz",
|
||||||
"integrity": "sha512-r9m7brz2hNmq5TF3sxrK4qR/FhXn44XIMglQUir4sT7Sh5GOaYXlMYikHFwJStf8rmQGTlvOoBXt4yHVonRG8A==",
|
"integrity": "sha512-8LwjnlP39s08C08J5NstzriPvW1SP8Zfpp1BvC2sI35kPeZnHfxVkCwu4/+Wodgnd60UtT1n8K8zw+Mp7J9JmQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"ncc": "dist/ncc/cli.js"
|
"ncc": "dist/ncc/cli.js"
|
||||||
}
|
}
|
||||||
@@ -228,14 +245,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
|
||||||
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
|
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
|
||||||
},
|
},
|
||||||
"node_modules/uuid": {
|
|
||||||
"version": "8.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
|
||||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
|
||||||
"bin": {
|
|
||||||
"uuid": "dist/bin/uuid"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/webidl-conversions": {
|
"node_modules/webidl-conversions": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
"@actions/github": "^4.0.0"
|
"@actions/github": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vercel/ncc": "^0.24.1"
|
"@vercel/ncc": "^0.38.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16"
|
"node": ">=16"
|
||||||
|
|||||||
47
act/runner/testdata/actions/node20/package-lock.json
generated
vendored
47
act/runner/testdata/actions/node20/package-lock.json
generated
vendored
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "node16",
|
"name": "node20",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "node16",
|
"name": "node20",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -13,19 +13,29 @@
|
|||||||
"@actions/github": "^4.0.0"
|
"@actions/github": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vercel/ncc": "^0.24.1"
|
"@vercel/ncc": "^0.38.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@actions/core": {
|
"node_modules/@actions/core": {
|
||||||
"version": "1.10.0",
|
"version": "1.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz",
|
||||||
"integrity": "sha512-2aZDDa3zrrZbP5ZYg159sNoLRb61nQ7awl5pSvIq5Qpj81vwDzdMRKzkWJGJuwVvWpvZKx7vspJALyvaaIQyug==",
|
"integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/http-client": "^2.0.1",
|
"@actions/exec": "^1.1.1",
|
||||||
"uuid": "^8.3.2"
|
"@actions/http-client": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@actions/exec": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@actions/io": "^1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@actions/github": {
|
"node_modules/@actions/github": {
|
||||||
@@ -55,6 +65,12 @@
|
|||||||
"tunnel": "^0.0.6"
|
"tunnel": "^0.0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@actions/io": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@octokit/auth-token": {
|
"node_modules/@octokit/auth-token": {
|
||||||
"version": "2.5.0",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz",
|
||||||
@@ -157,10 +173,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vercel/ncc": {
|
"node_modules/@vercel/ncc": {
|
||||||
"version": "0.24.1",
|
"version": "0.38.4",
|
||||||
"resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.24.1.tgz",
|
"resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.38.4.tgz",
|
||||||
"integrity": "sha512-r9m7brz2hNmq5TF3sxrK4qR/FhXn44XIMglQUir4sT7Sh5GOaYXlMYikHFwJStf8rmQGTlvOoBXt4yHVonRG8A==",
|
"integrity": "sha512-8LwjnlP39s08C08J5NstzriPvW1SP8Zfpp1BvC2sI35kPeZnHfxVkCwu4/+Wodgnd60UtT1n8K8zw+Mp7J9JmQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"ncc": "dist/ncc/cli.js"
|
"ncc": "dist/ncc/cli.js"
|
||||||
}
|
}
|
||||||
@@ -228,14 +245,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
|
||||||
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
|
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
|
||||||
},
|
},
|
||||||
"node_modules/uuid": {
|
|
||||||
"version": "8.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
|
||||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
|
||||||
"bin": {
|
|
||||||
"uuid": "dist/bin/uuid"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/webidl-conversions": {
|
"node_modules/webidl-conversions": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
"@actions/github": "^4.0.0"
|
"@actions/github": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vercel/ncc": "^0.24.1"
|
"@vercel/ncc": "^0.38.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ runs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
- uses: ./localdockerimagetest_
|
- uses: ./localdockerimagetest_
|
||||||
# Also test a remote docker action here
|
# Also test a remote docker action here
|
||||||
- uses: actions/hello-world-docker-action@v1
|
- uses: actions/hello-world-docker-action@v2
|
||||||
with:
|
with:
|
||||||
who-to-greet: 'Mona the Octocat'
|
who-to-greet: 'Mona the Octocat'
|
||||||
# Test if GITHUB_ACTION_PATH is set correctly after all steps
|
# Test if GITHUB_ACTION_PATH is set correctly after all steps
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ func CompilePattern(rawpattern string) (*WorkflowPattern, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:gocyclo // function handles many cases
|
|
||||||
func PatternToRegex(pattern string) (string, error) {
|
func PatternToRegex(pattern string) (string, error) {
|
||||||
var rpattern strings.Builder
|
var rpattern strings.Builder
|
||||||
rpattern.WriteString("^")
|
rpattern.WriteString("^")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Usage Examples for `act_runner`
|
# Usage Examples for `gitea-runner`
|
||||||
|
|
||||||
Welcome to our collection of usage and deployment examples specifically designed for Gitea setups. Whether you're a beginner or an experienced user, you'll find practical resources here that you can directly apply to enhance your Gitea experience. We encourage you to contribute your own insights and knowledge to make this collection even more comprehensive and valuable.
|
Welcome to our collection of usage and deployment examples specifically designed for Gitea setups. Whether you're a beginner or an experienced user, you'll find practical resources here that you can directly apply to enhance your Gitea experience. We encourage you to contribute your own insights and knowledge to make this collection even more comprehensive and valuable.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
### Running `act_runner` using `docker-compose`
|
### Running `gitea-runner` using `docker-compose`
|
||||||
|
|
||||||
```yml
|
```yml
|
||||||
...
|
...
|
||||||
@@ -19,15 +19,15 @@
|
|||||||
# - GITEA_RUNNER_REGISTRATION_TOKEN=<user-defined registration token>
|
# - GITEA_RUNNER_REGISTRATION_TOKEN=<user-defined registration token>
|
||||||
|
|
||||||
runner:
|
runner:
|
||||||
image: gitea/act_runner
|
image: gitea/runner
|
||||||
restart: always
|
restart: always
|
||||||
depends_on:
|
depends_on:
|
||||||
gitea:
|
gitea:
|
||||||
# required so runner can attach to gitea, see "healthcheck"
|
# required so runner can attach to gitea, see "healthcheck"
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
restart: true
|
restart: true
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/act_runner:/data
|
- ./data/runner:/data
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
environment:
|
environment:
|
||||||
- GITEA_INSTANCE_URL=<instance url>
|
- GITEA_INSTANCE_URL=<instance url>
|
||||||
@@ -38,18 +38,18 @@
|
|||||||
- GITEA_RUNNER_REGISTRATION_TOKEN=<registration token>
|
- GITEA_RUNNER_REGISTRATION_TOKEN=<registration token>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Running `act_runner` using Docker-in-Docker (DIND)
|
### Running `gitea-runner` using Docker-in-Docker (DIND)
|
||||||
|
|
||||||
```yml
|
```yml
|
||||||
...
|
...
|
||||||
runner:
|
runner:
|
||||||
image: gitea/act_runner:latest-dind-rootless
|
image: gitea/runner:latest-dind-rootless
|
||||||
restart: always
|
restart: always
|
||||||
privileged: true
|
privileged: true
|
||||||
depends_on:
|
depends_on:
|
||||||
- gitea
|
- gitea
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/act_runner:/data
|
- ./data/runner:/data
|
||||||
environment:
|
environment:
|
||||||
- GITEA_INSTANCE_URL=<instance url>
|
- GITEA_INSTANCE_URL=<instance url>
|
||||||
- DOCKER_HOST=unix:///var/run/user/1000/docker.sock
|
- DOCKER_HOST=unix:///var/run/user/1000/docker.sock
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
### Run `act_runner` in a Docker Container
|
### Run `gitea-runner` in a Docker Container
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker run -e GITEA_INSTANCE_URL=http://192.168.8.18:3000 -e GITEA_RUNNER_REGISTRATION_TOKEN=<runner_token> -v /var/run/docker.sock:/var/run/docker.sock -v $PWD/data:/data --name my_runner gitea/act_runner:nightly
|
docker run -e GITEA_INSTANCE_URL=http://192.168.8.18:3000 -e GITEA_RUNNER_REGISTRATION_TOKEN=<runner_token> -v /var/run/docker.sock:/var/run/docker.sock -v $PWD/data:/data --name my_runner gitea/runner:nightly
|
||||||
```
|
```
|
||||||
|
|
||||||
The `/data` directory inside the docker container contains the runner API keys after registration.
|
The `/data` directory inside the docker container contains the runner API keys after registration.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
## Kubernetes Docker in Docker Deployment with `act_runner`
|
## Kubernetes Docker in Docker Deployment with `gitea-runner`
|
||||||
|
|
||||||
NOTE: Docker in Docker (dind) requires elevated privileges on Kubernetes. The current way to achieve this is to set the pod `SecurityContext` to `privileged`. Keep in mind that this is a potential security issue that has the potential for a malicious application to break out of the container context.
|
NOTE: Docker in Docker (dind) requires elevated privileges on Kubernetes. The current way to achieve this is to set the pod `SecurityContext` to `privileged`. Keep in mind that this is a potential security issue that has the potential for a malicious application to break out of the container context.
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
kind: PersistentVolumeClaim
|
kind: PersistentVolumeClaim
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
metadata:
|
metadata:
|
||||||
name: act-runner-vol
|
name: runner-vol
|
||||||
spec:
|
spec:
|
||||||
accessModes:
|
accessModes:
|
||||||
- ReadWriteOnce
|
- ReadWriteOnce
|
||||||
@@ -13,7 +13,7 @@ spec:
|
|||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
data:
|
data:
|
||||||
# The registration token can be obtained from the web UI, API or command-line.
|
# The registration token can be obtained from the web UI, API or command-line.
|
||||||
# You can also set a pre-defined global runner registration token for the Gitea instance via
|
# You can also set a pre-defined global runner registration token for the Gitea instance via
|
||||||
# `GITEA_RUNNER_REGISTRATION_TOKEN`/`GITEA_RUNNER_REGISTRATION_TOKEN_FILE` environment variable.
|
# `GITEA_RUNNER_REGISTRATION_TOKEN`/`GITEA_RUNNER_REGISTRATION_TOKEN_FILE` environment variable.
|
||||||
token: << base64 encoded registration token >>
|
token: << base64 encoded registration token >>
|
||||||
kind: Secret
|
kind: Secret
|
||||||
@@ -25,19 +25,19 @@ apiVersion: apps/v1
|
|||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
app: act-runner
|
app: runner
|
||||||
name: act-runner
|
name: runner
|
||||||
spec:
|
spec:
|
||||||
replicas: 1
|
replicas: 1
|
||||||
selector:
|
selector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
app: act-runner
|
app: runner
|
||||||
strategy: {}
|
strategy: {}
|
||||||
template:
|
template:
|
||||||
metadata:
|
metadata:
|
||||||
creationTimestamp: null
|
creationTimestamp: null
|
||||||
labels:
|
labels:
|
||||||
app: act-runner
|
app: runner
|
||||||
spec:
|
spec:
|
||||||
restartPolicy: Always
|
restartPolicy: Always
|
||||||
volumes:
|
volumes:
|
||||||
@@ -45,10 +45,10 @@ spec:
|
|||||||
emptyDir: {}
|
emptyDir: {}
|
||||||
- name: runner-data
|
- name: runner-data
|
||||||
persistentVolumeClaim:
|
persistentVolumeClaim:
|
||||||
claimName: act-runner-vol
|
claimName: runner-vol
|
||||||
containers:
|
containers:
|
||||||
- name: runner
|
- name: runner
|
||||||
image: gitea/act_runner:nightly
|
image: gitea/runner:nightly
|
||||||
command: ["sh", "-c", "while ! nc -z localhost 2376 </dev/null; do echo 'waiting for docker daemon...'; sleep 5; done; /sbin/tini -- run.sh"]
|
command: ["sh", "-c", "while ! nc -z localhost 2376 </dev/null; do echo 'waiting for docker daemon...'; sleep 5; done; /sbin/tini -- run.sh"]
|
||||||
env:
|
env:
|
||||||
- name: DOCKER_HOST
|
- name: DOCKER_HOST
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
kind: PersistentVolumeClaim
|
kind: PersistentVolumeClaim
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
metadata:
|
metadata:
|
||||||
name: act-runner-vol
|
name: runner-vol
|
||||||
spec:
|
spec:
|
||||||
accessModes:
|
accessModes:
|
||||||
- ReadWriteOnce
|
- ReadWriteOnce
|
||||||
@@ -13,7 +13,7 @@ spec:
|
|||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
data:
|
data:
|
||||||
# The registration token can be obtained from the web UI, API or command-line.
|
# The registration token can be obtained from the web UI, API or command-line.
|
||||||
# You can also set a pre-defined global runner registration token for the Gitea instance via
|
# You can also set a pre-defined global runner registration token for the Gitea instance via
|
||||||
# `GITEA_RUNNER_REGISTRATION_TOKEN`/`GITEA_RUNNER_REGISTRATION_TOKEN_FILE` environment variable.
|
# `GITEA_RUNNER_REGISTRATION_TOKEN`/`GITEA_RUNNER_REGISTRATION_TOKEN_FILE` environment variable.
|
||||||
token: << base64 encoded registration token >>
|
token: << base64 encoded registration token >>
|
||||||
kind: Secret
|
kind: Secret
|
||||||
@@ -25,32 +25,32 @@ apiVersion: apps/v1
|
|||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
app: act-runner
|
app: runner
|
||||||
name: act-runner
|
name: runner
|
||||||
spec:
|
spec:
|
||||||
replicas: 1
|
replicas: 1
|
||||||
selector:
|
selector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
app: act-runner
|
app: runner
|
||||||
strategy: {}
|
strategy: {}
|
||||||
template:
|
template:
|
||||||
metadata:
|
metadata:
|
||||||
creationTimestamp: null
|
creationTimestamp: null
|
||||||
labels:
|
labels:
|
||||||
app: act-runner
|
app: runner
|
||||||
spec:
|
spec:
|
||||||
restartPolicy: Always
|
restartPolicy: Always
|
||||||
volumes:
|
volumes:
|
||||||
- name: runner-data
|
- name: runner-data
|
||||||
persistentVolumeClaim:
|
persistentVolumeClaim:
|
||||||
claimName: act-runner-vol
|
claimName: runner-vol
|
||||||
securityContext:
|
securityContext:
|
||||||
fsGroup: 1000
|
fsGroup: 1000
|
||||||
containers:
|
containers:
|
||||||
- name: runner
|
- name: runner
|
||||||
image: gitea/act_runner:nightly-dind-rootless
|
image: gitea/runner:nightly-dind-rootless
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
# command: ["sh", "-c", "while ! nc -z localhost 2376 </dev/null; do echo 'waiting for docker daemon...'; sleep 5; done; /sbin/tini -- /opt/act/run.sh"]
|
# command: ["sh", "-c", "while ! nc -z localhost 2376 </dev/null; do echo 'waiting for docker daemon...'; sleep 5; done; /sbin/tini -- run.sh"]
|
||||||
env:
|
env:
|
||||||
- name: DOCKER_HOST
|
- name: DOCKER_HOST
|
||||||
value: tcp://localhost:2376
|
value: tcp://localhost:2376
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
## `act_runner` on Virtual or Physical Servers
|
## `gitea-runner` on Virtual or Physical Servers
|
||||||
|
|
||||||
Files in this directory:
|
Files in this directory:
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
## Using Rootless Docker with`act_runner`
|
## Using Rootless Docker with`gitea-runner`
|
||||||
|
|
||||||
Here is a simple example of how to set up `act_runner` with rootless Docker. It has been created with Debian, but other Linux should work the same way.
|
Here is a simple example of how to set up `gitea-runner` with rootless Docker. It has been created with Debian, but other Linux should work the same way.
|
||||||
|
|
||||||
Note: This procedure needs a real login shell -- using `sudo su` or other method of accessing the account will fail some of the steps below.
|
Note: This procedure needs a real login shell -- using `sudo su` or other method of accessing the account will fail some of the steps below.
|
||||||
|
|
||||||
As `root`:
|
As `root`:
|
||||||
|
|
||||||
- Create a user to run both `docker` and `act_runner`. In this example, we use a non-privileged account called `rootless`.
|
- Create a user to run both `docker` and `gitea-runner`. In this example, we use a non-privileged account called `rootless`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
useradd -m rootless
|
useradd -m rootless
|
||||||
@@ -38,36 +38,36 @@ export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
|
|||||||
```
|
```
|
||||||
|
|
||||||
- Reboot. Ensure that the Docker process is working.
|
- Reboot. Ensure that the Docker process is working.
|
||||||
- Create a directory for saving `act_runner` data between restarts
|
- Create a directory for saving `gitea-runner` data between restarts
|
||||||
|
|
||||||
`mkdir /home/rootless/act_runner`
|
`mkdir /home/rootless/gitea-runner`
|
||||||
|
|
||||||
- Register the runner from the data directory
|
- Register the runner from the data directory
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /home/rootless/act_runner
|
cd /home/rootless/gitea-runner
|
||||||
act_runner register
|
gitea-runner register
|
||||||
```
|
```
|
||||||
|
|
||||||
- Generate a `act_runner` configuration file in the data directory. Edit the file to adjust for the system.
|
- Generate a `gitea-runner` configuration file in the data directory. Edit the file to adjust for the system.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
act_runner generate-config >/home/rootless/act_runner/config
|
gitea-runner generate-config >/home/rootless/gitea-runner/config
|
||||||
```
|
```
|
||||||
|
|
||||||
- Create a new user-level`systemd` unit file as `/home/rootless/.config/systemd/user/act_runner.service` with the following contents:
|
- Create a new user-level`systemd` unit file as `/home/rootless/.config/systemd/user/gitea-runner.service` with the following contents:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
Description=Gitea Actions runner
|
Description=Gitea Actions runner
|
||||||
Documentation=https://gitea.com/gitea/act_runner
|
Documentation=https://gitea.com/gitea/runner
|
||||||
After=docker.service
|
After=docker.service
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Environment=PATH=/home/rootless/bin:/sbin:/usr/sbin:/home/rootless/bin:/home/rootless/bin:/home/rootless/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games
|
Environment=PATH=/home/rootless/bin:/sbin:/usr/sbin:/home/rootless/bin:/home/rootless/bin:/home/rootless/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games
|
||||||
Environment=DOCKER_HOST=unix:///run/user/1001/docker.sock
|
Environment=DOCKER_HOST=unix:///run/user/1001/docker.sock
|
||||||
ExecStart=/usr/bin/act_runner daemon -c /home/rootless/act_runner/config
|
ExecStart=/usr/bin/gitea-runner daemon -c /home/rootless/gitea-runner/config
|
||||||
ExecReload=/bin/kill -s HUP $MAINPID
|
ExecReload=/bin/kill -s HUP $MAINPID
|
||||||
WorkingDirectory=/home/rootless/act_runner
|
WorkingDirectory=/home/rootless/gitea-runner
|
||||||
TimeoutSec=0
|
TimeoutSec=0
|
||||||
RestartSec=2
|
RestartSec=2
|
||||||
Restart=always
|
Restart=always
|
||||||
@@ -88,8 +88,8 @@ export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
|
|||||||
|
|
||||||
- Reboot
|
- Reboot
|
||||||
|
|
||||||
After the system restarts, check that the`act_runner` is working and that the runner is connected to Gitea.
|
After the system restarts, check that the`gitea-runner` is working and that the runner is connected to Gitea.
|
||||||
|
|
||||||
````bash
|
````bash
|
||||||
systemctl --user status act_runner
|
systemctl --user status gitea-runner
|
||||||
journalctl --user -xeu act_runner
|
journalctl --user -xeu gitea-runner
|
||||||
|
|||||||
38
go.mod
38
go.mod
@@ -1,22 +1,22 @@
|
|||||||
module gitea.com/gitea/act_runner
|
module gitea.com/gitea/runner
|
||||||
|
|
||||||
go 1.26.0
|
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.1
|
connectrpc.com/connect v1.19.2
|
||||||
github.com/avast/retry-go/v4 v4.7.0
|
github.com/avast/retry-go/v4 v4.7.0
|
||||||
github.com/docker/docker v25.0.13+incompatible
|
github.com/docker/docker v25.0.15+incompatible
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/mattn/go-isatty v0.0.20
|
github.com/mattn/go-isatty v0.0.22
|
||||||
github.com/sirupsen/logrus v1.9.4
|
github.com/sirupsen/logrus v1.9.4
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
go.yaml.in/yaml/v4 v4.0.0-rc.3
|
go.yaml.in/yaml/v4 v4.0.0-rc.3
|
||||||
golang.org/x/term v0.40.0
|
golang.org/x/term v0.42.0
|
||||||
golang.org/x/time v0.14.0 // indirect
|
golang.org/x/time v0.14.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.11
|
google.golang.org/protobuf v1.36.11
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
gotest.tools/v3 v3.5.2
|
gotest.tools/v3 v3.5.2
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,21 +24,21 @@ require (
|
|||||||
github.com/Masterminds/semver v1.5.0
|
github.com/Masterminds/semver v1.5.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 v25.0.3+incompatible
|
github.com/docker/cli v25.0.7+incompatible
|
||||||
github.com/docker/go-connections v0.6.0
|
github.com/docker/go-connections v0.6.0
|
||||||
github.com/go-git/go-billy/v5 v5.7.0
|
github.com/go-git/go-billy/v5 v5.8.0
|
||||||
github.com/go-git/go-git/v5 v5.16.5
|
github.com/go-git/go-git/v5 v5.18.0
|
||||||
github.com/gobwas/glob v0.2.3
|
github.com/gobwas/glob v0.2.3
|
||||||
github.com/imdario/mergo v0.3.16
|
github.com/imdario/mergo v0.3.16
|
||||||
github.com/julienschmidt/httprouter v1.3.0
|
github.com/julienschmidt/httprouter v1.3.0
|
||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||||
github.com/moby/buildkit v0.13.2
|
github.com/moby/buildkit v0.13.2
|
||||||
github.com/moby/patternmatcher v0.6.0
|
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.13.1
|
github.com/opencontainers/selinux v1.13.1
|
||||||
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.11
|
github.com/rhysd/actionlint v1.7.12
|
||||||
github.com/spf13/pflag v1.0.10
|
github.com/spf13/pflag v1.0.10
|
||||||
github.com/timshannon/bolthold v0.0.0-20240314194003-30aac6950928
|
github.com/timshannon/bolthold v0.0.0-20240314194003-30aac6950928
|
||||||
go.etcd.io/bbolt v1.4.3
|
go.etcd.io/bbolt v1.4.3
|
||||||
@@ -62,7 +62,7 @@ require (
|
|||||||
github.com/docker/docker-credential-helpers v0.9.5 // indirect
|
github.com/docker/docker-credential-helpers v0.9.5 // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
github.com/fatih/color v1.18.0 // indirect
|
github.com/fatih/color v1.19.0 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
@@ -77,7 +77,7 @@ require (
|
|||||||
github.com/klauspost/compress v1.18.4 // indirect
|
github.com/klauspost/compress v1.18.4 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.20 // indirect
|
github.com/mattn/go-runewidth v0.0.21 // indirect
|
||||||
github.com/mattn/go-shellwords v1.0.12 // indirect
|
github.com/mattn/go-shellwords v1.0.12 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.1.2 // indirect
|
github.com/mitchellh/mapstructure v1.1.2 // indirect
|
||||||
github.com/moby/sys/sequential v0.6.0 // indirect
|
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||||
@@ -105,16 +105,12 @@ require (
|
|||||||
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
||||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||||
golang.org/x/crypto v0.48.0 // indirect
|
golang.org/x/crypto v0.49.0 // indirect
|
||||||
golang.org/x/net v0.50.0 // indirect
|
golang.org/x/net v0.52.0 // indirect
|
||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
golang.org/x/sys v0.41.0 // indirect
|
golang.org/x/sys v0.43.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
||||||
google.golang.org/grpc v1.67.0 // indirect
|
google.golang.org/grpc v1.67.0 // indirect
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
// Remove after github.com/docker/distribution is updated to support distribution/reference v0.6.0
|
|
||||||
// (pulled in via moby/buildkit, breaks on undefined: reference.SplitHostname)
|
|
||||||
replace github.com/distribution/reference v0.6.0 => github.com/distribution/reference v0.5.0
|
|
||||||
|
|||||||
48
go.sum
48
go.sum
@@ -1,7 +1,7 @@
|
|||||||
code.gitea.io/actions-proto-go v0.4.1 h1:l0EYhjsgpUe/1VABo2eK7zcoNX2W44WOnb0MSLrKfls=
|
code.gitea.io/actions-proto-go v0.4.1 h1:l0EYhjsgpUe/1VABo2eK7zcoNX2W44WOnb0MSLrKfls=
|
||||||
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.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14=
|
connectrpc.com/connect v1.19.2 h1:McQ83FGdzL+t60peksi0gXC7MQ/iLKgLduAnThbM0mo=
|
||||||
connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
|
connectrpc.com/connect v1.19.2/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
|
||||||
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=
|
||||||
@@ -49,12 +49,16 @@ github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1
|
|||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
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.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
|
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||||
github.com/distribution/reference v0.5.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 v25.0.3+incompatible h1:KLeNs7zws74oFuVhgZQ5ONGZiXUUdgsdy6/EsX/6284=
|
github.com/docker/cli v25.0.7+incompatible h1:scW/AbGafKmANsonsFckFHTwpz2QypoPA/zpoLnDs/E=
|
||||||
github.com/docker/cli v25.0.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
github.com/docker/cli v25.0.7+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||||
github.com/docker/docker v25.0.13+incompatible h1:YeBrkUd3q0ZoRDNoEzuopwCLU+uD8GZahDHwBdsTnkU=
|
github.com/docker/docker v25.0.13+incompatible h1:YeBrkUd3q0ZoRDNoEzuopwCLU+uD8GZahDHwBdsTnkU=
|
||||||
github.com/docker/docker v25.0.13+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
github.com/docker/docker v25.0.13+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
|
github.com/docker/docker v25.0.14+incompatible h1:+HNue3fKbqiDHYFAriyiMjfS5u25zB0E2/R8f42lOMc=
|
||||||
|
github.com/docker/docker v25.0.14+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
|
github.com/docker/docker v25.0.15+incompatible h1:JhRD6vZdk0Ms3SEMztefBISJL13NbxudQnGix6l+T5M=
|
||||||
|
github.com/docker/docker v25.0.15+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=
|
github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=
|
||||||
github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
|
github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
|
||||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||||
@@ -67,18 +71,20 @@ github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc
|
|||||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||||
|
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
||||||
|
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||||
github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM=
|
github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0=
|
||||||
github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E=
|
github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY=
|
||||||
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.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s=
|
github.com/go-git/go-git/v5 v5.18.0 h1:O831KI+0PR51hM2kep6T8k+w0/LIAD490gvqMCvL5hM=
|
||||||
github.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M=
|
github.com/go-git/go-git/v5 v5.18.0/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo=
|
||||||
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=
|
||||||
@@ -131,8 +137,12 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
|
|||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||||
|
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||||
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
|
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
|
||||||
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
|
github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
|
||||||
|
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
|
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
|
||||||
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
||||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||||
@@ -141,6 +151,8 @@ github.com/moby/buildkit v0.13.2 h1:nXNszM4qD9E7QtG7bFWPnDI1teUQFQglBzon/IU3SzI=
|
|||||||
github.com/moby/buildkit v0.13.2/go.mod h1:2cyVOv9NoHM7arphK9ZfHIWKn9YVZRFd1wXB8kKmEzY=
|
github.com/moby/buildkit v0.13.2/go.mod h1:2cyVOv9NoHM7arphK9ZfHIWKn9YVZRFd1wXB8kKmEzY=
|
||||||
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
|
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
|
||||||
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||||
|
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
|
||||||
|
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||||
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
|
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
|
||||||
@@ -177,6 +189,8 @@ github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzM
|
|||||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||||
github.com/rhysd/actionlint v1.7.11 h1:m+aSuCpCIClS8X02xMG4Z8s87fCHPsAtYkAoWGQZgEE=
|
github.com/rhysd/actionlint v1.7.11 h1:m+aSuCpCIClS8X02xMG4Z8s87fCHPsAtYkAoWGQZgEE=
|
||||||
github.com/rhysd/actionlint v1.7.11/go.mod h1:8n50YougV9+50niD7oxgDTZ1KbN/ZnKiQ2xpLFeVhsI=
|
github.com/rhysd/actionlint v1.7.11/go.mod h1:8n50YougV9+50niD7oxgDTZ1KbN/ZnKiQ2xpLFeVhsI=
|
||||||
|
github.com/rhysd/actionlint v1.7.12 h1:vQ4GeJN86C0QH+gTUQcs8McmK62OLT3kmakPMtEWYnY=
|
||||||
|
github.com/rhysd/actionlint v1.7.12/go.mod h1:krOUhujIsJusovkaYzQ/VNH8PFexjNKqU0q5XI/4w+g=
|
||||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
@@ -257,6 +271,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
|||||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
|
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||||
|
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
@@ -268,11 +284,15 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
|
|||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||||
|
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||||
|
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -287,9 +307,17 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||||
|
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||||
|
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
||||||
|
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
||||||
|
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
||||||
|
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
|||||||
@@ -4,12 +4,13 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/artifactcache"
|
"gitea.com/gitea/runner/act/artifactcache"
|
||||||
"gitea.com/gitea/act_runner/internal/pkg/config"
|
"gitea.com/gitea/runner/internal/pkg/config"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -47,10 +48,15 @@ func runCacheServer(configFile *string, cacheArgs *cacheServerArgs) func(cmd *co
|
|||||||
port = cacheArgs.Port
|
port = cacheArgs.Port
|
||||||
}
|
}
|
||||||
|
|
||||||
|
secret := cfg.Cache.ExternalSecret
|
||||||
|
if secret == "" {
|
||||||
|
return errors.New("cache.external_secret must be set for cache-server; configure the same value on each runner that points at this server via cache.external_server")
|
||||||
|
}
|
||||||
cacheHandler, err := artifactcache.StartHandler(
|
cacheHandler, err := artifactcache.StartHandler(
|
||||||
dir,
|
dir,
|
||||||
host,
|
host,
|
||||||
port,
|
port,
|
||||||
|
secret,
|
||||||
log.StandardLogger().WithField("module", "cache_request"),
|
log.StandardLogger().WithField("module", "cache_request"),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user