mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-05-08 08:13:25 +02:00
Compare commits
2 Commits
main
...
f9bfeb85d9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9bfeb85d9 | ||
|
|
81f3d3ef3f |
@@ -40,7 +40,7 @@ cpu.out
|
||||
*.db
|
||||
*.log
|
||||
|
||||
/gitea-runner
|
||||
/runner
|
||||
/debug
|
||||
|
||||
/bin
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
go-version-file: "go.mod"
|
||||
- name: Import GPG key
|
||||
id: import_gpg
|
||||
uses: crazy-max/ghaction-import-gpg@v7
|
||||
uses: crazy-max/ghaction-import-gpg@v6
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
passphrase: ${{ secrets.PASSPHRASE }}
|
||||
@@ -71,12 +71,17 @@ jobs:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
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"
|
||||
id: docker_meta
|
||||
uses: https://github.com/docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.DOCKER_ORG }}/runner
|
||||
${{ env.DOCKER_ORG }}/${{ steps.repo_meta.outputs.REPO_NAME }}
|
||||
tags: |
|
||||
type=semver,pattern={{major}}.{{minor}}.{{patch}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
name: checks
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
- push
|
||||
- pull_request
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
@@ -19,4 +17,4 @@ jobs:
|
||||
- name: build
|
||||
run: make build
|
||||
- name: test
|
||||
run: make test
|
||||
run: make test
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,4 +1,4 @@
|
||||
/gitea-runner
|
||||
/runner
|
||||
.env
|
||||
.runner
|
||||
coverage.txt
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
version: 2
|
||||
|
||||
project_name: gitea-runner
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
@@ -88,7 +86,7 @@ blobs:
|
||||
provider: s3
|
||||
bucket: "{{ .Env.S3_BUCKET }}"
|
||||
region: "{{ .Env.S3_REGION }}"
|
||||
directory: "gitea-runner/{{.Version}}"
|
||||
directory: "runner/{{.Version}}"
|
||||
extra_files:
|
||||
- glob: ./**.xz
|
||||
- glob: ./**.sha256
|
||||
|
||||
10
Dockerfile
10
Dockerfile
@@ -17,11 +17,11 @@ RUN make clean && make build
|
||||
### DIND VARIANT
|
||||
#
|
||||
#
|
||||
FROM docker:29-dind AS dind
|
||||
FROM docker:28-dind AS dind
|
||||
|
||||
RUN apk add --no-cache s6 bash git tzdata
|
||||
|
||||
COPY --from=builder /opt/src/runner/gitea-runner /usr/local/bin/gitea-runner
|
||||
COPY --from=builder /opt/src/runner/runner /usr/local/bin/runner
|
||||
COPY scripts/run.sh /usr/local/bin/run.sh
|
||||
COPY scripts/s6 /etc/s6
|
||||
|
||||
@@ -32,12 +32,12 @@ ENTRYPOINT ["s6-svscan","/etc/s6"]
|
||||
### DIND-ROOTLESS VARIANT
|
||||
#
|
||||
#
|
||||
FROM docker:29-dind-rootless AS dind-rootless
|
||||
FROM docker:28-dind-rootless AS dind-rootless
|
||||
|
||||
USER root
|
||||
RUN apk add --no-cache s6 bash git tzdata
|
||||
|
||||
COPY --from=builder /opt/src/runner/gitea-runner /usr/local/bin/gitea-runner
|
||||
COPY --from=builder /opt/src/runner/runner /usr/local/bin/runner
|
||||
COPY scripts/run.sh /usr/local/bin/run.sh
|
||||
COPY scripts/s6 /etc/s6
|
||||
|
||||
@@ -56,7 +56,7 @@ ENTRYPOINT ["s6-svscan","/etc/s6"]
|
||||
FROM alpine AS basic
|
||||
RUN apk add --no-cache tini bash git tzdata
|
||||
|
||||
COPY --from=builder /opt/src/runner/gitea-runner /usr/local/bin/gitea-runner
|
||||
COPY --from=builder /opt/src/runner/runner /usr/local/bin/runner
|
||||
COPY scripts/run.sh /usr/local/bin/run.sh
|
||||
|
||||
VOLUME /data
|
||||
|
||||
4
Makefile
4
Makefile
@@ -1,5 +1,5 @@
|
||||
DIST := dist
|
||||
EXECUTABLE := gitea-runner
|
||||
EXECUTABLE := runner
|
||||
DIST_DIRS := $(DIST)/binaries $(DIST)/release
|
||||
GO ?= go
|
||||
SHASUM ?= shasum -a 256
|
||||
@@ -86,7 +86,7 @@ go-check:
|
||||
$(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 '.' ' ');))
|
||||
@if [ "$(GO_VERSION)" -lt "$(MIN_GO_VERSION)" ]; then \
|
||||
echo "Gitea Runner requires Go $(MIN_GO_VERSION_STR) or greater to build. You can get it at https://go.dev/dl/"; \
|
||||
echo "Act Runner requires Go $(MIN_GO_VERSION_STR) or greater to build. You can get it at https://go.dev/dl/"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
|
||||
63
README.md
63
README.md
@@ -1,4 +1,6 @@
|
||||
# Gitea Runner
|
||||
# act runner
|
||||
|
||||
Act runner is a runner for Gitea.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -8,7 +10,7 @@ Docker Engine Community version is required for docker mode. To install Docker C
|
||||
|
||||
### Download pre-built binary
|
||||
|
||||
Visit [here](https://dl.gitea.com/gitea-runner/) and download the right version for your platform.
|
||||
Visit [here](https://dl.gitea.com/runner/) and download the right version for your platform.
|
||||
|
||||
### Build from source
|
||||
|
||||
@@ -34,7 +36,7 @@ ENABLED=true
|
||||
### Register
|
||||
|
||||
```bash
|
||||
./gitea-runner register
|
||||
./runner register
|
||||
```
|
||||
|
||||
And you will be asked to input:
|
||||
@@ -66,7 +68,7 @@ INFO Runner registered successfully.
|
||||
You can also register with command line arguments.
|
||||
|
||||
```bash
|
||||
./gitea-runner register --instance http://192.168.8.8:3000 --token <my_runner_token> --no-interactive
|
||||
./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.
|
||||
@@ -74,7 +76,7 @@ If the registry succeed, it will run immediately. Next time, you could run the r
|
||||
### Run
|
||||
|
||||
```bash
|
||||
./gitea-runner daemon
|
||||
./runner daemon
|
||||
```
|
||||
|
||||
### Run with docker
|
||||
@@ -83,60 +85,23 @@ If the registry succeed, it will run immediately. Next time, you could run the r
|
||||
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
|
||||
|
||||
The runner is configured with a YAML file. Generate a starting point (this matches what ships in the tree):
|
||||
You can also configure the runner with a configuration file.
|
||||
The configuration file is a YAML file, you can generate a sample configuration file with `./runner generate-config`.
|
||||
|
||||
```bash
|
||||
./gitea-runner generate-config > config.yaml
|
||||
./runner generate-config > config.yaml
|
||||
```
|
||||
|
||||
Pass it with `-c` / `--config` on any command that loads configuration (`register`, `daemon`, `cache-server`):
|
||||
You can specify the configuration file path with `-c`/`--config` argument.
|
||||
|
||||
```bash
|
||||
./gitea-runner -c config.yaml register
|
||||
./gitea-runner -c config.yaml daemon
|
||||
./gitea-runner -c config.yaml cache-server
|
||||
./runner -c config.yaml register # register with config file
|
||||
./runner -c config.yaml daemon # run with config file
|
||||
```
|
||||
|
||||
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
|
||||
You can read the latest version of the configuration file online at [config.example.yaml](internal/pkg/config/config.example.yaml).
|
||||
|
||||
### Example Deployments
|
||||
|
||||
|
||||
@@ -5,24 +5,17 @@
|
||||
package artifactcache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
@@ -35,36 +28,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
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
|
||||
urlBase = "/_apis/artifactcache"
|
||||
)
|
||||
|
||||
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 {
|
||||
dir string
|
||||
storage *Storage
|
||||
@@ -77,36 +43,10 @@ type Handler struct {
|
||||
gcAt time.Time
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// StartHandler opens the on-disk cache store and starts the HTTP server.
|
||||
//
|
||||
// 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,
|
||||
}
|
||||
func StartHandler(dir, outboundIP string, port uint16, logger logrus.FieldLogger) (*Handler, error) {
|
||||
h := &Handler{}
|
||||
|
||||
if logger == nil {
|
||||
discard := logrus.New()
|
||||
@@ -143,37 +83,19 @@ func StartHandler(dir, outboundIP string, port uint16, internalSecret string, lo
|
||||
h.outboundIP = ip.String()
|
||||
}
|
||||
|
||||
secret, err := loadOrCreateSecret(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.secret = secret
|
||||
|
||||
router := httprouter.New()
|
||||
router.GET(apiPath+"/cache", h.bearerAuth(h.find))
|
||||
router.POST(apiPath+"/caches", h.bearerAuth(h.reserve))
|
||||
router.PATCH(apiPath+"/caches/:id", h.bearerAuth(h.upload))
|
||||
router.POST(apiPath+"/caches/:id", h.bearerAuth(h.commit))
|
||||
router.POST(apiPath+"/clean", h.bearerAuth(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))
|
||||
router.GET(urlBase+"/cache", h.middleware(h.find))
|
||||
router.POST(urlBase+"/caches", h.middleware(h.reserve))
|
||||
router.PATCH(urlBase+"/caches/:id", h.middleware(h.upload))
|
||||
router.POST(urlBase+"/caches/:id", h.middleware(h.commit))
|
||||
router.GET(urlBase+"/artifacts/:id", h.middleware(h.get))
|
||||
router.POST(urlBase+"/clean", h.middleware(h.clean))
|
||||
|
||||
h.router = router
|
||||
|
||||
h.gcCache()
|
||||
|
||||
// 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))
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) // listen on all interfaces
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -199,91 +121,6 @@ func (h *Handler) ExternalURL() string {
|
||||
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 {
|
||||
if h == nil {
|
||||
return nil
|
||||
@@ -323,7 +160,6 @@ func (h *Handler) openDB() (*bolthold.Store, error) {
|
||||
|
||||
// GET /_apis/artifactcache/cache
|
||||
func (h *Handler) find(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
cred := credFromContext(r.Context())
|
||||
keys := strings.Split(r.URL.Query().Get("keys"), ",")
|
||||
// cache keys are case insensitive
|
||||
for i, key := range keys {
|
||||
@@ -338,7 +174,7 @@ func (h *Handler) find(w http.ResponseWriter, r *http.Request, _ httprouter.Para
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
cache, err := findCache(db, cred.Repo, keys, version)
|
||||
cache, err := findCache(db, keys, version)
|
||||
if err != nil {
|
||||
h.responseJSON(w, r, 500, err)
|
||||
return
|
||||
@@ -358,14 +194,13 @@ func (h *Handler) find(w http.ResponseWriter, r *http.Request, _ httprouter.Para
|
||||
}
|
||||
h.responseJSON(w, r, 200, map[string]any{
|
||||
"result": "hit",
|
||||
"archiveLocation": h.signedArtifactURL(cache.ID, time.Now().Add(artifactURLTTL)),
|
||||
"archiveLocation": fmt.Sprintf("%s%s/artifacts/%d", h.ExternalURL(), urlBase, cache.ID),
|
||||
"cacheKey": cache.Key,
|
||||
})
|
||||
}
|
||||
|
||||
// POST /_apis/artifactcache/caches
|
||||
func (h *Handler) reserve(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
cred := credFromContext(r.Context())
|
||||
api := &Request{}
|
||||
if err := json.NewDecoder(r.Body).Decode(api); err != nil {
|
||||
h.responseJSON(w, r, 400, err)
|
||||
@@ -375,7 +210,6 @@ func (h *Handler) reserve(w http.ResponseWriter, r *http.Request, _ httprouter.P
|
||||
api.Key = strings.ToLower(api.Key)
|
||||
|
||||
cache := api.ToCache()
|
||||
cache.Repo = cred.Repo
|
||||
db, err := h.openDB()
|
||||
if err != nil {
|
||||
h.responseJSON(w, r, 500, err)
|
||||
@@ -397,7 +231,6 @@ func (h *Handler) reserve(w http.ResponseWriter, r *http.Request, _ httprouter.P
|
||||
|
||||
// PATCH /_apis/artifactcache/caches/:id
|
||||
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)
|
||||
if err != nil {
|
||||
h.responseJSON(w, r, 400, err)
|
||||
@@ -420,11 +253,6 @@ func (h *Handler) upload(w http.ResponseWriter, r *http.Request, params httprout
|
||||
return
|
||||
}
|
||||
|
||||
if cache.Repo != cred.Repo {
|
||||
h.responseJSON(w, r, 403, fmt.Errorf("cache %d: forbidden", id))
|
||||
return
|
||||
}
|
||||
|
||||
if cache.Complete {
|
||||
h.responseJSON(w, r, 400, fmt.Errorf("cache %v %q: already complete", cache.ID, cache.Key))
|
||||
return
|
||||
@@ -444,7 +272,6 @@ func (h *Handler) upload(w http.ResponseWriter, r *http.Request, params httprout
|
||||
|
||||
// POST /_apis/artifactcache/caches/:id
|
||||
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)
|
||||
if err != nil {
|
||||
h.responseJSON(w, r, 400, err)
|
||||
@@ -467,11 +294,6 @@ func (h *Handler) commit(w http.ResponseWriter, r *http.Request, params httprout
|
||||
return
|
||||
}
|
||||
|
||||
if cache.Repo != cred.Repo {
|
||||
h.responseJSON(w, r, 403, fmt.Errorf("cache %d: forbidden", id))
|
||||
return
|
||||
}
|
||||
|
||||
if cache.Complete {
|
||||
h.responseJSON(w, r, 400, fmt.Errorf("cache %v %q: already complete", cache.ID, cache.Key))
|
||||
return
|
||||
@@ -504,10 +326,6 @@ func (h *Handler) commit(w http.ResponseWriter, r *http.Request, params httprout
|
||||
}
|
||||
|
||||
// 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) {
|
||||
id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
|
||||
if err != nil {
|
||||
@@ -526,158 +344,21 @@ func (h *Handler) clean(w http.ResponseWriter, r *http.Request, _ httprouter.Par
|
||||
h.responseJSON(w, r, 200)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
func (h *Handler) middleware(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)
|
||||
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
|
||||
}
|
||||
h.logger.Debugf("%s %s", r.Method, r.RequestURI)
|
||||
handler(w, r, params)
|
||||
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.
|
||||
func findCache(db *bolthold.Store, repo string, keys []string, version string) (*Cache, error) {
|
||||
func findCache(db *bolthold.Store, keys []string, version string) (*Cache, error) {
|
||||
cache := &Cache{}
|
||||
for _, prefix := range keys {
|
||||
// if a key in the list matches exactly, don't return partial matches
|
||||
if err := db.FindOne(cache,
|
||||
bolthold.Where("Repo").Eq(repo).
|
||||
And("Key").Eq(prefix).
|
||||
bolthold.Where("Key").Eq(prefix).
|
||||
And("Version").Eq(version).
|
||||
And("Complete").Eq(true).
|
||||
SortBy("CreatedAt").Reverse()); err == nil || !errors.Is(err, bolthold.ErrNotFound) {
|
||||
@@ -692,8 +373,7 @@ func findCache(db *bolthold.Store, repo string, keys []string, version string) (
|
||||
continue
|
||||
}
|
||||
if err := db.FindOne(cache,
|
||||
bolthold.Where("Repo").Eq(repo).
|
||||
And("Key").RegExp(re).
|
||||
bolthold.Where("Key").RegExp(re).
|
||||
And("Version").Eq(version).
|
||||
And("Complete").Eq(true).
|
||||
SortBy("CreatedAt").Reverse()); err != nil {
|
||||
@@ -739,6 +419,7 @@ const (
|
||||
keepOld = 5 * time.Minute
|
||||
)
|
||||
|
||||
//nolint:gocyclo // function handles many cases
|
||||
func (h *Handler) gcCache() {
|
||||
if h.gcing.Load() {
|
||||
return
|
||||
@@ -813,16 +494,12 @@ func (h *Handler) gcCache() {
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Remove the old caches with the same key and version, keep the latest one.
|
||||
// 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(
|
||||
&Cache{},
|
||||
bolthold.Where("Complete").Eq(true),
|
||||
"Repo", "Key", "Version",
|
||||
"Key", "Version",
|
||||
); err != nil {
|
||||
h.logger.Warnf("find aggregate caches: %v", err)
|
||||
} else {
|
||||
@@ -856,7 +533,7 @@ func (h *Handler) responseJSON(w http.ResponseWriter, r *http.Request, code int,
|
||||
if len(v) == 0 || v[0] == nil {
|
||||
data, _ = json.Marshal(struct{}{})
|
||||
} else if err, ok := v[0].(error); ok {
|
||||
h.logger.Errorf("%v %v: %v", r.Method, r.URL.Path, err)
|
||||
h.logger.Errorf("%v %v: %v", r.Method, r.RequestURI, err)
|
||||
data, _ = json.Marshal(map[string]any{
|
||||
"error": err.Error(),
|
||||
})
|
||||
|
||||
@@ -22,38 +22,12 @@ import (
|
||||
"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) {
|
||||
dir := filepath.Join(t.TempDir(), "artifactcache")
|
||||
handler, err := StartHandler(dir, "", 0, "", nil)
|
||||
handler, err := StartHandler(dir, "", 0, nil)
|
||||
require.NoError(t, err)
|
||||
handler.RegisterJob(testToken, testRepo)
|
||||
|
||||
base := fmt.Sprintf("%s%s", handler.ExternalURL(), apiPath)
|
||||
base := fmt.Sprintf("%s%s", handler.ExternalURL(), urlBase)
|
||||
|
||||
defer func() {
|
||||
t.Run("inpect db", func(t *testing.T) {
|
||||
@@ -71,10 +45,7 @@ func TestHandler(t *testing.T) {
|
||||
require.NoError(t, handler.Close())
|
||||
assert.Nil(t, handler.server)
|
||||
assert.Nil(t, handler.listener)
|
||||
resp, err := testClient.Post(fmt.Sprintf("%s/caches/%d", base, 1), "", nil)
|
||||
if err == nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
_, err := http.Post(fmt.Sprintf("%s/caches/%d", base, 1), "", nil) //nolint:bodyclose // pre-existing issue from nektos/act
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}()
|
||||
@@ -82,9 +53,8 @@ func TestHandler(t *testing.T) {
|
||||
t.Run("get not exist", func(t *testing.T) {
|
||||
key := strings.ToLower(t.Name())
|
||||
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
|
||||
resp, err := testClient.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, key, version))
|
||||
resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, key, version)) //nolint:bodyclose // pre-existing issue from nektos/act
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, 204, resp.StatusCode)
|
||||
})
|
||||
|
||||
@@ -98,18 +68,16 @@ func TestHandler(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("clean", func(t *testing.T) {
|
||||
resp, err := testClient.Post(base+"/clean", "", nil)
|
||||
resp, err := http.Post(base+"/clean", "", nil) //nolint:bodyclose // pre-existing issue from nektos/act
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("reserve with bad request", func(t *testing.T) {
|
||||
body := []byte(`invalid json`)
|
||||
require.NoError(t, err)
|
||||
resp, err := testClient.Post(base+"/caches", "application/json", bytes.NewReader(body))
|
||||
resp, err := http.Post(base+"/caches", "application/json", bytes.NewReader(body)) //nolint:bodyclose // pre-existing issue from nektos/act
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, 400, resp.StatusCode)
|
||||
})
|
||||
|
||||
@@ -126,9 +94,8 @@ func TestHandler(t *testing.T) {
|
||||
Size: 100,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
resp, err := testClient.Post(base+"/caches", "application/json", bytes.NewReader(body))
|
||||
resp, err := http.Post(base+"/caches", "application/json", bytes.NewReader(body)) //nolint:bodyclose // pre-existing issue from nektos/act
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
require.NoError(t, json.NewDecoder(resp.Body).Decode(&first))
|
||||
@@ -141,9 +108,8 @@ func TestHandler(t *testing.T) {
|
||||
Size: 100,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
resp, err := testClient.Post(base+"/caches", "application/json", bytes.NewReader(body))
|
||||
resp, err := http.Post(base+"/caches", "application/json", bytes.NewReader(body)) //nolint:bodyclose // pre-existing issue from nektos/act
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
require.NoError(t, json.NewDecoder(resp.Body).Decode(&second))
|
||||
@@ -159,9 +125,8 @@ func TestHandler(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
req.Header.Set("Content-Range", "bytes 0-99/*")
|
||||
resp, err := testClient.Do(req)
|
||||
resp, err := http.DefaultClient.Do(req) //nolint:bodyclose // pre-existing issue from nektos/act
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, 400, resp.StatusCode)
|
||||
})
|
||||
|
||||
@@ -171,9 +136,8 @@ func TestHandler(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
req.Header.Set("Content-Range", "bytes 0-99/*")
|
||||
resp, err := testClient.Do(req)
|
||||
resp, err := http.DefaultClient.Do(req) //nolint:bodyclose // pre-existing issue from nektos/act
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, 400, resp.StatusCode)
|
||||
})
|
||||
|
||||
@@ -191,9 +155,8 @@ func TestHandler(t *testing.T) {
|
||||
Size: 100,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
resp, err := testClient.Post(base+"/caches", "application/json", bytes.NewReader(body))
|
||||
resp, err := http.Post(base+"/caches", "application/json", bytes.NewReader(body)) //nolint:bodyclose // pre-existing issue from nektos/act
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
got := struct {
|
||||
@@ -208,15 +171,13 @@ func TestHandler(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
req.Header.Set("Content-Range", "bytes 0-99/*")
|
||||
resp, err := testClient.Do(req)
|
||||
resp, err := http.DefaultClient.Do(req) //nolint:bodyclose // pre-existing issue from nektos/act
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
}
|
||||
{
|
||||
resp, err := testClient.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil)
|
||||
resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil) //nolint:bodyclose // pre-existing issue from nektos/act
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
}
|
||||
{
|
||||
@@ -225,9 +186,8 @@ func TestHandler(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
req.Header.Set("Content-Range", "bytes 0-99/*")
|
||||
resp, err := testClient.Do(req)
|
||||
resp, err := http.DefaultClient.Do(req) //nolint:bodyclose // pre-existing issue from nektos/act
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, 400, resp.StatusCode)
|
||||
}
|
||||
})
|
||||
@@ -246,9 +206,8 @@ func TestHandler(t *testing.T) {
|
||||
Size: 100,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
resp, err := testClient.Post(base+"/caches", "application/json", bytes.NewReader(body))
|
||||
resp, err := http.Post(base+"/caches", "application/json", bytes.NewReader(body)) //nolint:bodyclose // pre-existing issue from nektos/act
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
got := struct {
|
||||
@@ -263,27 +222,24 @@ func TestHandler(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
req.Header.Set("Content-Range", "bytes xx-99/*")
|
||||
resp, err := testClient.Do(req)
|
||||
resp, err := http.DefaultClient.Do(req) //nolint:bodyclose // pre-existing issue from nektos/act
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, 400, resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("commit with bad id", func(t *testing.T) {
|
||||
{
|
||||
resp, err := testClient.Post(base+"/caches/invalid_id", "", nil)
|
||||
resp, err := http.Post(base+"/caches/invalid_id", "", nil) //nolint:bodyclose // pre-existing issue from nektos/act
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, 400, resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("commit with not exist id", func(t *testing.T) {
|
||||
{
|
||||
resp, err := testClient.Post(fmt.Sprintf("%s/caches/%d", base, 100), "", nil)
|
||||
resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, 100), "", nil) //nolint:bodyclose // pre-existing issue from nektos/act
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, 400, resp.StatusCode)
|
||||
}
|
||||
})
|
||||
@@ -302,9 +258,8 @@ func TestHandler(t *testing.T) {
|
||||
Size: 100,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
resp, err := testClient.Post(base+"/caches", "application/json", bytes.NewReader(body))
|
||||
resp, err := http.Post(base+"/caches", "application/json", bytes.NewReader(body)) //nolint:bodyclose // pre-existing issue from nektos/act
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
got := struct {
|
||||
@@ -319,21 +274,18 @@ func TestHandler(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
req.Header.Set("Content-Range", "bytes 0-99/*")
|
||||
resp, err := testClient.Do(req)
|
||||
resp, err := http.DefaultClient.Do(req) //nolint:bodyclose // pre-existing issue from nektos/act
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
}
|
||||
{
|
||||
resp, err := testClient.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil)
|
||||
resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil) //nolint:bodyclose // pre-existing issue from nektos/act
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
}
|
||||
{
|
||||
resp, err := testClient.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil)
|
||||
resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil) //nolint:bodyclose // pre-existing issue from nektos/act
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, 400, resp.StatusCode)
|
||||
}
|
||||
})
|
||||
@@ -352,9 +304,8 @@ func TestHandler(t *testing.T) {
|
||||
Size: 100,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
resp, err := testClient.Post(base+"/caches", "application/json", bytes.NewReader(body))
|
||||
resp, err := http.Post(base+"/caches", "application/json", bytes.NewReader(body)) //nolint:bodyclose // pre-existing issue from nektos/act
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
got := struct {
|
||||
@@ -369,37 +320,32 @@ func TestHandler(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
req.Header.Set("Content-Range", "bytes 0-59/*")
|
||||
resp, err := testClient.Do(req)
|
||||
resp, err := http.DefaultClient.Do(req) //nolint:bodyclose // pre-existing issue from nektos/act
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
}
|
||||
{
|
||||
resp, err := testClient.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil)
|
||||
resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil) //nolint:bodyclose // pre-existing issue from nektos/act
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, 500, resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("get with bad id", func(t *testing.T) {
|
||||
resp, err := testClient.Get(base + "/artifacts/invalid_id")
|
||||
resp, err := http.Get(base + "/artifacts/invalid_id") //nolint:bodyclose // pre-existing issue from nektos/act
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, 400, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("get with not exist id", func(t *testing.T) {
|
||||
resp, err := testClient.Get(signArtifactURL(handler, 100))
|
||||
resp, err := http.Get(fmt.Sprintf("%s/artifacts/%d", base, 100)) //nolint:bodyclose // pre-existing issue from nektos/act
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, 404, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("get with not exist id", func(t *testing.T) {
|
||||
resp, err := testClient.Get(signArtifactURL(handler, 100))
|
||||
resp, err := http.Get(fmt.Sprintf("%s/artifacts/%d", base, 100)) //nolint:bodyclose // pre-existing issue from nektos/act
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, 404, resp.StatusCode)
|
||||
})
|
||||
|
||||
@@ -429,9 +375,8 @@ func TestHandler(t *testing.T) {
|
||||
key + "_a",
|
||||
}, ",")
|
||||
|
||||
resp, err := testClient.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKeys, version))
|
||||
resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKeys, version)) //nolint:bodyclose // pre-existing issue from nektos/act
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
/*
|
||||
@@ -450,9 +395,8 @@ func TestHandler(t *testing.T) {
|
||||
assert.Equal(t, "hit", got.Result)
|
||||
assert.Equal(t, keys[except], got.CacheKey)
|
||||
|
||||
contentResp, err := testClient.Get(got.ArchiveLocation)
|
||||
contentResp, err := http.Get(got.ArchiveLocation) //nolint:bodyclose // pre-existing issue from nektos/act
|
||||
require.NoError(t, err)
|
||||
defer contentResp.Body.Close()
|
||||
require.Equal(t, 200, contentResp.StatusCode)
|
||||
content, err := io.ReadAll(contentResp.Body)
|
||||
require.NoError(t, err)
|
||||
@@ -469,9 +413,8 @@ func TestHandler(t *testing.T) {
|
||||
|
||||
{
|
||||
reqKey := key + "_aBc"
|
||||
resp, err := testClient.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKey, version))
|
||||
resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKey, version)) //nolint:bodyclose // pre-existing issue from nektos/act
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, 200, resp.StatusCode)
|
||||
got := struct {
|
||||
Result string `json:"result"`
|
||||
@@ -509,9 +452,8 @@ func TestHandler(t *testing.T) {
|
||||
key + "_a_b",
|
||||
}, ",")
|
||||
|
||||
resp, err := testClient.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKeys, version))
|
||||
resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKeys, version)) //nolint:bodyclose // pre-existing issue from nektos/act
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
/*
|
||||
@@ -528,9 +470,8 @@ func TestHandler(t *testing.T) {
|
||||
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
|
||||
assert.Equal(t, keys[expect], got.CacheKey)
|
||||
|
||||
contentResp, err := testClient.Get(got.ArchiveLocation)
|
||||
contentResp, err := http.Get(got.ArchiveLocation) //nolint:bodyclose // pre-existing issue from nektos/act
|
||||
require.NoError(t, err)
|
||||
defer contentResp.Body.Close()
|
||||
require.Equal(t, 200, contentResp.StatusCode)
|
||||
content, err := io.ReadAll(contentResp.Body)
|
||||
require.NoError(t, err)
|
||||
@@ -563,9 +504,8 @@ func TestHandler(t *testing.T) {
|
||||
key + "_a_b",
|
||||
}, ",")
|
||||
|
||||
resp, err := testClient.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKeys, version))
|
||||
resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKeys, version)) //nolint:bodyclose // pre-existing issue from nektos/act
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
/*
|
||||
@@ -583,9 +523,8 @@ func TestHandler(t *testing.T) {
|
||||
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
|
||||
assert.Equal(t, keys[expect], got.CacheKey)
|
||||
|
||||
contentResp, err := testClient.Get(got.ArchiveLocation)
|
||||
contentResp, err := http.Get(got.ArchiveLocation) //nolint:bodyclose // pre-existing issue from nektos/act
|
||||
require.NoError(t, err)
|
||||
defer contentResp.Body.Close()
|
||||
require.Equal(t, 200, contentResp.StatusCode)
|
||||
content, err := io.ReadAll(contentResp.Body)
|
||||
require.NoError(t, err)
|
||||
@@ -602,9 +541,8 @@ func uploadCacheNormally(t *testing.T, base, key, version string, content []byte
|
||||
Size: int64(len(content)),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
resp, err := testClient.Post(base+"/caches", "application/json", bytes.NewReader(body))
|
||||
resp, err := http.Post(base+"/caches", "application/json", bytes.NewReader(body)) //nolint:bodyclose // pre-existing issue from nektos/act
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
got := struct {
|
||||
@@ -619,22 +557,19 @@ func uploadCacheNormally(t *testing.T, base, key, version string, content []byte
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
req.Header.Set("Content-Range", "bytes 0-99/*")
|
||||
resp, err := testClient.Do(req)
|
||||
resp, err := http.DefaultClient.Do(req) //nolint:bodyclose // pre-existing issue from nektos/act
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
}
|
||||
{
|
||||
resp, err := testClient.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil)
|
||||
resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil) //nolint:bodyclose // pre-existing issue from nektos/act
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
}
|
||||
var archiveLocation string
|
||||
{
|
||||
resp, err := testClient.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, key, version))
|
||||
resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, key, version)) //nolint:bodyclose // pre-existing issue from nektos/act
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, 200, resp.StatusCode)
|
||||
got := struct {
|
||||
Result string `json:"result"`
|
||||
@@ -647,9 +582,8 @@ func uploadCacheNormally(t *testing.T, base, key, version string, content []byte
|
||||
archiveLocation = got.ArchiveLocation
|
||||
}
|
||||
{
|
||||
resp, err := testClient.Get(archiveLocation)
|
||||
resp, err := http.Get(archiveLocation) //nolint:bodyclose // pre-existing issue from nektos/act
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, 200, resp.StatusCode)
|
||||
got, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
@@ -659,7 +593,7 @@ func uploadCacheNormally(t *testing.T, base, key, version string, content []byte
|
||||
|
||||
func TestHandler_gcCache(t *testing.T) {
|
||||
dir := filepath.Join(t.TempDir(), "artifactcache")
|
||||
handler, err := StartHandler(dir, "", 0, "", nil)
|
||||
handler, err := StartHandler(dir, "", 0, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
defer func() {
|
||||
@@ -765,421 +699,3 @@ func TestHandler_gcCache(t *testing.T) {
|
||||
}
|
||||
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,7 +29,6 @@ func (c *Request) ToCache() *Cache {
|
||||
|
||||
type Cache struct {
|
||||
ID uint64 `json:"id" boltholdKey:"ID"`
|
||||
Repo string `json:"repo" boltholdIndex:"Repo"`
|
||||
Key string `json:"key" boltholdIndex:"Key"`
|
||||
Version string `json:"version" boltholdIndex:"Version"`
|
||||
Size int64 `json:"cacheSize"`
|
||||
|
||||
30
act/artifactcache/testdata/example/example.yaml
vendored
Normal file
30
act/artifactcache/testdata/example/example.yaml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
# Copied from https://github.com/actions/cache#example-cache-workflow
|
||||
name: Caching Primes
|
||||
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- run: env
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Cache Primes
|
||||
id: cache-primes
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: prime-numbers
|
||||
key: ${{ runner.os }}-primes-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-primes
|
||||
${{ runner.os }}
|
||||
|
||||
- name: Generate Prime Numbers
|
||||
if: steps.cache-primes.outputs.cache-hit != 'true'
|
||||
run: cat /proc/sys/kernel/random/uuid > prime-numbers
|
||||
|
||||
- name: Use Prime Numbers
|
||||
run: cat prime-numbers
|
||||
@@ -202,7 +202,7 @@ func TestListArtifactContainer(t *testing.T) {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
assert.Len(response.Value, 1)
|
||||
assert.Equal(1, len(response.Value)) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Equal("some/file", response.Value[0].Path)
|
||||
assert.Equal("file", response.Value[0].ItemType)
|
||||
assert.Equal("http://localhost/artifact/1/some/file/.", response.Value[0].ContentLocation)
|
||||
@@ -260,7 +260,7 @@ func TestArtifactFlow(t *testing.T) {
|
||||
defer cancel()
|
||||
|
||||
platforms := map[string]string{
|
||||
"ubuntu-latest": "node:24-bookworm", // Don't use node:24-bookworm-slim because it doesn't have curl command, which is used in the tests
|
||||
"ubuntu-latest": "node:16-buster", // Don't use node:16-buster-slim because it doesn't have curl command, which is used in the tests
|
||||
}
|
||||
|
||||
tables := []TestJobFileInfo{
|
||||
@@ -283,7 +283,7 @@ func runTestJobFile(ctx context.Context, t *testing.T, tjfi TestJobFileInfo) {
|
||||
}
|
||||
|
||||
workdir, err := filepath.Abs(tjfi.workdir)
|
||||
assert.NoError(t, err, workdir) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err, workdir) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
fullWorkflowPath := filepath.Join(workdir, tjfi.workflowPath)
|
||||
runnerConfig := &runner.Config{
|
||||
Workdir: workdir,
|
||||
@@ -299,16 +299,16 @@ func runTestJobFile(ctx context.Context, t *testing.T, tjfi TestJobFileInfo) {
|
||||
}
|
||||
|
||||
runner, err := runner.New(runnerConfig)
|
||||
assert.NoError(t, err, tjfi.workflowPath) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err, tjfi.workflowPath) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
planner, err := model.NewWorkflowPlanner(fullWorkflowPath, true)
|
||||
assert.NoError(t, err, fullWorkflowPath) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err, fullWorkflowPath) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
plan, err := planner.PlanEvent(tjfi.eventName)
|
||||
if err == nil {
|
||||
err = runner.NewPlanExecutor(plan)(ctx)
|
||||
if tjfi.errorMessage == "" {
|
||||
assert.NoError(t, err, fullWorkflowPath) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err, fullWorkflowPath) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
} else {
|
||||
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},
|
||||
}
|
||||
output = CartesianProduct(input)
|
||||
assert.Empty(output)
|
||||
assert.Len(output, 0) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
input = map[string][]any{}
|
||||
output = CartesianProduct(input)
|
||||
assert.Empty(output)
|
||||
assert.Len(output, 0) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
}
|
||||
|
||||
@@ -21,11 +21,11 @@ func TestNewWorkflow(t *testing.T) {
|
||||
|
||||
// empty
|
||||
emptyWorkflow := NewPipelineExecutor()
|
||||
assert.NoError(emptyWorkflow(ctx)) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(emptyWorkflow(ctx)) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
// error case
|
||||
errorWorkflow := NewErrorExecutor(errors.New("test error"))
|
||||
assert.Error(errorWorkflow(ctx)) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.NotNil(errorWorkflow(ctx)) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
// multiple success case
|
||||
runcount := 0
|
||||
@@ -38,7 +38,7 @@ func TestNewWorkflow(t *testing.T) {
|
||||
runcount++
|
||||
return nil
|
||||
})
|
||||
assert.NoError(successWorkflow(ctx)) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(successWorkflow(ctx)) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Equal(2, runcount)
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ func TestNewConditionalExecutor(t *testing.T) {
|
||||
return nil
|
||||
})(ctx)
|
||||
|
||||
assert.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Equal(0, trueCount)
|
||||
assert.Equal(1, falseCount)
|
||||
|
||||
@@ -74,7 +74,7 @@ func TestNewConditionalExecutor(t *testing.T) {
|
||||
return nil
|
||||
})(ctx)
|
||||
|
||||
assert.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Equal(1, trueCount)
|
||||
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(2), maxCount.Load(), "should run at most 2 executors in parallel")
|
||||
assert.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
// Reset to test running the executor with 0 parallelism
|
||||
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(1), maxCount.Load(), "should run at most 1 executors in parallel")
|
||||
assert.NoError(errSingle)
|
||||
assert.Nil(errSingle) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
}
|
||||
|
||||
func TestNewParallelExecutorFailed(t *testing.T) {
|
||||
|
||||
@@ -32,23 +32,12 @@ var (
|
||||
githubHTTPRegex = regexp.MustCompile(`^https?://.*github.com.*/(.+)/(.+?)(?:.git)?$`)
|
||||
githubSSHRegex = regexp.MustCompile(`github.com[:/](.+)/(.+?)(?:.git)?$`)
|
||||
|
||||
cloneLocks sync.Map // key: clone target directory; value: *sync.Mutex
|
||||
cloneLock sync.Mutex
|
||||
|
||||
ErrShortRef = errors.New("short SHA references are not supported")
|
||||
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 serialized; clones into different directories run in parallel.
|
||||
// Callers reading files inside dir (e.g. tarring a checked-out action into a job container) must hold this lock too,
|
||||
// otherwise a concurrent NewGitCloneExecutor on the same dir can mutate the worktree mid-read.
|
||||
func AcquireCloneLock(dir string) func() {
|
||||
v, _ := cloneLocks.LoadOrStore(dir, &sync.Mutex{})
|
||||
mu := v.(*sync.Mutex)
|
||||
mu.Lock()
|
||||
return mu.Unlock
|
||||
}
|
||||
|
||||
type Error struct {
|
||||
err error
|
||||
commit string
|
||||
@@ -288,7 +277,6 @@ func CloneIfRequired(ctx context.Context, refName plumbing.ReferenceName, input
|
||||
|
||||
func gitOptions(token string) (fetchOptions git.FetchOptions, pullOptions git.PullOptions) {
|
||||
fetchOptions.RefSpecs = []config.RefSpec{"refs/*:refs/*", "HEAD:refs/heads/HEAD"}
|
||||
fetchOptions.Force = true
|
||||
pullOptions.Force = true
|
||||
|
||||
if token != "" {
|
||||
@@ -304,13 +292,16 @@ func gitOptions(token string) (fetchOptions git.FetchOptions, pullOptions git.Pu
|
||||
}
|
||||
|
||||
// NewGitCloneExecutor creates an executor to clone git repos
|
||||
//
|
||||
//nolint:gocyclo // function handles many cases
|
||||
func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
logger := common.Logger(ctx)
|
||||
logger.Infof("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)
|
||||
|
||||
defer AcquireCloneLock(input.Dir)()
|
||||
cloneLock.Lock()
|
||||
defer cloneLock.Unlock()
|
||||
|
||||
refName := plumbing.ReferenceName("refs/heads/" + input.Ref)
|
||||
r, err := CloneIfRequired(ctx, refName, input, logger)
|
||||
|
||||
@@ -10,11 +10,8 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -223,62 +220,6 @@ 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() {
|
||||
if os.Getenv("GITHUB_ACTIONS") == "true" {
|
||||
var err error
|
||||
@@ -305,61 +246,3 @@ func gitCmd(args ...string) error {
|
||||
}
|
||||
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,7 +6,6 @@ package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"gitea.com/gitea/runner/act/common"
|
||||
@@ -14,13 +13,6 @@ import (
|
||||
"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
|
||||
type NewContainerInput struct {
|
||||
Image string
|
||||
|
||||
@@ -25,9 +25,9 @@ func NewDockerBuildExecutor(input NewDockerBuildExecutorInput) common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
logger := common.Logger(ctx)
|
||||
if input.Platform != "" {
|
||||
logger.Infof("docker build -t %s --platform %s %s", input.ImageTag, input.Platform, input.ContextDir)
|
||||
logger.Infof("%sdocker build -t %s --platform %s %s", logPrefix, input.ImageTag, input.Platform, input.ContextDir)
|
||||
} else {
|
||||
logger.Infof("docker build -t %s %s", input.ImageTag, input.ContextDir)
|
||||
logger.Infof("%sdocker build -t %s %s", logPrefix, input.ImageTag, input.ContextDir)
|
||||
}
|
||||
if common.Dryrun(ctx) {
|
||||
return nil
|
||||
|
||||
@@ -324,6 +324,8 @@ type containerConfig struct {
|
||||
// parse parses the args for the specified command and generates a Config,
|
||||
// a HostConfig and returns them with the specified command.
|
||||
// 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) {
|
||||
var (
|
||||
attachStdin = copts.attach.Get("stdin")
|
||||
|
||||
@@ -194,6 +194,7 @@ func TestParseRunWithInvalidArgs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:gocyclo // function handles many cases
|
||||
func TestParseWithVolumes(t *testing.T) {
|
||||
// A single volume
|
||||
arr, tryit := setupPlatformVolume([]string{`/tmp`}, []string{`c:\tmp`})
|
||||
@@ -631,7 +632,7 @@ func TestParseModes(t *testing.T) {
|
||||
}
|
||||
|
||||
// uts ko
|
||||
_, _, _, err = parseRun([]string{"--uts=container:", "img", "cmd"})
|
||||
_, _, _, err = parseRun([]string{"--uts=container:", "img", "cmd"}) //nolint:dogsled // ignoring multiple returns in test helpers
|
||||
assert.ErrorContains(t, err, "--uts: invalid UTS mode")
|
||||
|
||||
// uts ok
|
||||
@@ -692,7 +693,7 @@ func TestParseRestartPolicy(t *testing.T) {
|
||||
|
||||
func TestParseRestartPolicyAutoRemove(t *testing.T) {
|
||||
expected := "Conflicting options: --restart and --rm"
|
||||
_, _, _, err := parseRun([]string{"--rm", "--restart=always", "img", "cmd"})
|
||||
_, _, _, err := parseRun([]string{"--rm", "--restart=always", "img", "cmd"}) //nolint:dogsled // ignoring multiple returns in test helpers
|
||||
if err == nil || err.Error() != expected {
|
||||
t.Fatalf("Expected error %v, but got none", expected)
|
||||
}
|
||||
|
||||
@@ -29,43 +29,43 @@ func TestImageExistsLocally(t *testing.T) {
|
||||
|
||||
// Test if image exists with specific tag
|
||||
invalidImageTag, err := ImageExistsLocally(ctx, "library/alpine:this-random-tag-will-never-exist", "linux/amd64")
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.False(t, invalidImageTag)
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Equal(t, false, invalidImageTag) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
// Test if image exists with specific architecture (image platform)
|
||||
invalidImagePlatform, err := ImageExistsLocally(ctx, "alpine:latest", "windows/amd64")
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.False(t, invalidImagePlatform)
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Equal(t, false, invalidImagePlatform) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
// pull an image
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
cli.NegotiateAPIVersion(context.Background())
|
||||
|
||||
// Chose alpine latest because it's so small
|
||||
// maybe we should build an image instead so that tests aren't reliable on dockerhub
|
||||
readerDefault, err := cli.ImagePull(ctx, "node:24-bookworm-slim", types.ImagePullOptions{
|
||||
readerDefault, err := cli.ImagePull(ctx, "node:16-buster-slim", types.ImagePullOptions{
|
||||
Platform: "linux/amd64",
|
||||
})
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
defer readerDefault.Close()
|
||||
_, err = io.ReadAll(readerDefault)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
imageDefaultArchExists, err := ImageExistsLocally(ctx, "node:24-bookworm-slim", "linux/amd64")
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.True(t, imageDefaultArchExists)
|
||||
imageDefaultArchExists, err := ImageExistsLocally(ctx, "node:16-buster-slim", "linux/amd64")
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Equal(t, true, imageDefaultArchExists) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
// Validate if another architecture platform can be pulled
|
||||
readerArm64, err := cli.ImagePull(ctx, "node:24-bookworm-slim", types.ImagePullOptions{
|
||||
readerArm64, err := cli.ImagePull(ctx, "node:16-buster-slim", types.ImagePullOptions{
|
||||
Platform: "linux/arm64",
|
||||
})
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
defer readerArm64.Close()
|
||||
_, err = io.ReadAll(readerArm64)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
imageArm64Exists, err := ImageExistsLocally(ctx, "node:24-bookworm-slim", "linux/arm64")
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.True(t, imageArm64Exists)
|
||||
imageArm64Exists, err := ImageExistsLocally(ctx, "node:16-buster-slim", "linux/arm64")
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Equal(t, true, imageArm64Exists) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ type dockerMessage struct {
|
||||
Progress string `json:"progress"`
|
||||
}
|
||||
|
||||
const logPrefix = " \U0001F433 "
|
||||
|
||||
func logDockerResponse(logger logrus.FieldLogger, dockerResponse io.ReadCloser, isError bool) error {
|
||||
if dockerResponse == nil {
|
||||
return nil
|
||||
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
func NewDockerPullExecutor(input NewDockerPullExecutorInput) common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
logger := common.Logger(ctx)
|
||||
logger.Debugf("docker pull %v", input.Image)
|
||||
logger.Debugf("%sdocker pull %v", logPrefix, input.Image)
|
||||
|
||||
if common.Dryrun(ctx) {
|
||||
return nil
|
||||
|
||||
@@ -43,7 +43,7 @@ func TestGetImagePullOptions(t *testing.T) {
|
||||
config.SetDir("/non-existent/docker")
|
||||
|
||||
options, err := getImagePullOptions(ctx, NewDockerPullExecutorInput{})
|
||||
assert.NoError(t, err, "Failed to create ImagePullOptions") //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(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
|
||||
|
||||
options, err = getImagePullOptions(ctx, NewDockerPullExecutorInput{
|
||||
@@ -51,7 +51,7 @@ func TestGetImagePullOptions(t *testing.T) {
|
||||
Username: "username",
|
||||
Password: "password",
|
||||
})
|
||||
assert.NoError(t, err, "Failed to create ImagePullOptions") //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(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")
|
||||
|
||||
config.SetDir("testdata/docker-pull-options")
|
||||
@@ -59,6 +59,6 @@ func TestGetImagePullOptions(t *testing.T) {
|
||||
options, err = getImagePullOptions(ctx, NewDockerPullExecutorInput{
|
||||
Image: "nektos/act",
|
||||
})
|
||||
assert.NoError(t, err, "Failed to create ImagePullOptions") //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(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")
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ func NewContainer(input *NewContainerInput) ExecutionsEnvironment {
|
||||
|
||||
func (cr *containerReference) ConnectToNetwork(name string) common.Executor {
|
||||
return common.
|
||||
NewDebugExecutor("docker network connect %s %s", name, cr.input.Name).
|
||||
NewDebugExecutor("%sdocker network connect %s %s", logPrefix, name, cr.input.Name).
|
||||
Then(
|
||||
common.NewPipelineExecutor(
|
||||
cr.connect(),
|
||||
@@ -90,7 +90,7 @@ func supportsContainerImagePlatform(ctx context.Context, cli client.APIClient) b
|
||||
|
||||
func (cr *containerReference) Create(capAdd, capDrop []string) common.Executor {
|
||||
return common.
|
||||
NewInfoExecutor("docker create image=%s platform=%s entrypoint=%+q cmd=%+q network=%+q", cr.input.Image, cr.input.Platform, cr.input.Entrypoint, cr.input.Cmd, cr.input.NetworkMode).
|
||||
NewInfoExecutor("%sdocker create image=%s platform=%s entrypoint=%+q cmd=%+q network=%+q", logPrefix, cr.input.Image, cr.input.Platform, cr.input.Entrypoint, cr.input.Cmd, cr.input.NetworkMode).
|
||||
Then(
|
||||
common.NewPipelineExecutor(
|
||||
cr.connect(),
|
||||
@@ -102,7 +102,7 @@ func (cr *containerReference) Create(capAdd, capDrop []string) common.Executor {
|
||||
|
||||
func (cr *containerReference) Start(attach bool) common.Executor {
|
||||
return common.
|
||||
NewInfoExecutor("docker run image=%s platform=%s entrypoint=%+q cmd=%+q network=%+q", cr.input.Image, cr.input.Platform, cr.input.Entrypoint, cr.input.Cmd, cr.input.NetworkMode).
|
||||
NewInfoExecutor("%sdocker run image=%s platform=%s entrypoint=%+q cmd=%+q network=%+q", logPrefix, cr.input.Image, cr.input.Platform, cr.input.Entrypoint, cr.input.Cmd, cr.input.NetworkMode).
|
||||
Then(
|
||||
common.NewPipelineExecutor(
|
||||
cr.connect(),
|
||||
@@ -125,7 +125,7 @@ func (cr *containerReference) Start(attach bool) common.Executor {
|
||||
|
||||
func (cr *containerReference) Pull(forcePull bool) common.Executor {
|
||||
return common.
|
||||
NewInfoExecutor("docker pull image=%s platform=%s username=%s forcePull=%t", cr.input.Image, cr.input.Platform, cr.input.Username, forcePull).
|
||||
NewInfoExecutor("%sdocker pull image=%s platform=%s username=%s forcePull=%t", logPrefix, cr.input.Image, cr.input.Platform, cr.input.Username, forcePull).
|
||||
Then(
|
||||
NewDockerPullExecutor(NewDockerPullExecutorInput{
|
||||
Image: cr.input.Image,
|
||||
@@ -147,7 +147,7 @@ func (cr *containerReference) Copy(destPath string, files ...*FileEntry) common.
|
||||
|
||||
func (cr *containerReference) CopyDir(destPath, srcPath string, useGitIgnore bool) common.Executor {
|
||||
return common.NewPipelineExecutor(
|
||||
common.NewInfoExecutor("docker cp src=%s dst=%s", srcPath, destPath),
|
||||
common.NewInfoExecutor("%sdocker cp src=%s dst=%s", logPrefix, srcPath, destPath),
|
||||
cr.copyDir(destPath, srcPath, useGitIgnore),
|
||||
func(ctx context.Context) error {
|
||||
// If this fails, then folders have wrong permissions on non root container
|
||||
@@ -177,7 +177,7 @@ func (cr *containerReference) UpdateFromImageEnv(env *map[string]string) common.
|
||||
|
||||
func (cr *containerReference) Exec(command []string, env map[string]string, user, workdir string) common.Executor {
|
||||
return common.NewPipelineExecutor(
|
||||
common.NewInfoExecutor("docker exec cmd=[%s] user=%s workdir=%s", strings.Join(command, " "), user, workdir),
|
||||
common.NewInfoExecutor("%sdocker exec cmd=[%s] user=%s workdir=%s", logPrefix, strings.Join(command, " "), user, workdir),
|
||||
cr.connect(),
|
||||
cr.find(),
|
||||
cr.exec(command, env, user, workdir),
|
||||
@@ -633,10 +633,14 @@ func (cr *containerReference) exec(cmd []string, env map[string]string, user, wo
|
||||
return fmt.Errorf("failed to inspect exec: %w", err)
|
||||
}
|
||||
|
||||
if inspectResp.ExitCode == 0 {
|
||||
switch inspectResp.ExitCode {
|
||||
case 0:
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -926,7 +930,7 @@ func (cr *containerReference) wait() common.Executor {
|
||||
return nil
|
||||
}
|
||||
|
||||
return ExitCodeError(statusCode)
|
||||
return fmt.Errorf("exit with `FAILURE`: %v", statusCode)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
"github.com/sirupsen/logrus/hooks/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDocker(t *testing.T) {
|
||||
@@ -86,11 +85,6 @@ func (m *mockDockerClient) ContainerExecInspect(ctx context.Context, execID stri
|
||||
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 {
|
||||
args := m.Called(ctx, id, path, content, options)
|
||||
return args.Error(0)
|
||||
@@ -180,43 +174,12 @@ func TestDockerExecFailure(t *testing.T) {
|
||||
}
|
||||
|
||||
err := cr.exec([]string{""}, map[string]string{}, "user", "workdir")(ctx)
|
||||
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())
|
||||
assert.Error(t, err, "exit with `FAILURE`: 1") //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
conn.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) {
|
||||
ctx := context.Background()
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ func TestGetSocketAndHostWithSocket(t *testing.T) {
|
||||
ret, err := GetSocketAndHost(socketURI)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Equal(t, SocketAndHost{socketURI, dockerHost}, ret)
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func TestGetSocketAndHostNoSocket(t *testing.T) {
|
||||
ret, err := GetSocketAndHost("")
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Equal(t, SocketAndHost{dockerHost, dockerHost}, ret)
|
||||
}
|
||||
|
||||
@@ -57,8 +57,8 @@ func TestGetSocketAndHostOnlySocket(t *testing.T) {
|
||||
ret, err := GetSocketAndHost(socketURI)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err, "Expected no error from GetSocketAndHost") //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.True(t, defaultSocketFound, "Expected to find default socket")
|
||||
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.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")
|
||||
}
|
||||
@@ -73,7 +73,7 @@ func TestGetSocketAndHostDontMount(t *testing.T) {
|
||||
ret, err := GetSocketAndHost("-")
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Equal(t, SocketAndHost{"-", dockerHost}, ret)
|
||||
}
|
||||
|
||||
@@ -87,8 +87,8 @@ func TestGetSocketAndHostNoHostNoSocket(t *testing.T) {
|
||||
ret, err := GetSocketAndHost("")
|
||||
|
||||
// Assert
|
||||
assert.True(t, found, "Expected a default socket to be found")
|
||||
assert.NoError(t, err, "Expected no error from GetSocketAndHost") //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Equal(t, true, found, "Expected a default socket to be found") //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(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")
|
||||
}
|
||||
|
||||
@@ -112,8 +112,8 @@ func TestGetSocketAndHostNoHostNoSocketDefaultLocation(t *testing.T) {
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, unixSocket, defaultSocket, "Expected default socket to match common socket location")
|
||||
assert.True(t, found, "Expected default socket to be found")
|
||||
assert.NoError(t, err, "Expected no error from GetSocketAndHost") //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Equal(t, true, found, "Expected default socket to be found") //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(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")
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ func TestGetSocketAndHostNoHostInvalidSocket(t *testing.T) {
|
||||
ret, err := GetSocketAndHost(mySocket)
|
||||
|
||||
// Assert
|
||||
assert.False(t, found, "Expected no default socket to be found")
|
||||
assert.Equal(t, false, found, "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.Error(t, err, "Expected an error in invalid state")
|
||||
@@ -147,8 +147,8 @@ func TestGetSocketAndHostOnlySocketValidButUnusualLocation(t *testing.T) {
|
||||
// Assert
|
||||
// Default socket locations
|
||||
assert.Equal(t, "", defaultSocket, "Expect default socket location to be empty") //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.False(t, found, "Expected no default socket to be found")
|
||||
assert.Equal(t, false, found, "Expected no default socket to be found") //nolint:testifylint // pre-existing issue from nektos/act
|
||||
// Sane default
|
||||
assert.NoError(t, err, "Expect no error from GetSocketAndHost") //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(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")
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func NewDockerVolumeRemoveExecutor(volumeName string, force bool) common.Executo
|
||||
func removeExecutor(volume string, force bool) common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
logger := common.Logger(ctx)
|
||||
logger.Debugf("docker volume rm %s", volume)
|
||||
logger.Debugf("%sdocker volume rm %s", logPrefix, volume)
|
||||
|
||||
if common.Dryrun(ctx) {
|
||||
return nil
|
||||
|
||||
@@ -16,9 +16,7 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitea.com/gitea/runner/act/common"
|
||||
@@ -36,15 +34,9 @@ type HostEnvironment struct {
|
||||
TmpDir string
|
||||
ToolCache string
|
||||
Workdir string
|
||||
// BindWorkdir is true when the app runner mounts the workspace on the host and
|
||||
// deletes the task directory after the job; host teardown must not remove Workdir.
|
||||
BindWorkdir bool
|
||||
ActPath string
|
||||
CleanUp func()
|
||||
StdOut io.Writer
|
||||
|
||||
mu sync.Mutex
|
||||
runningPIDs map[int]struct{}
|
||||
ActPath string
|
||||
CleanUp func()
|
||||
StdOut io.Writer
|
||||
}
|
||||
|
||||
func (e *HostEnvironment) Create(_, _ []string) common.Executor {
|
||||
@@ -352,30 +344,8 @@ func (e *HostEnvironment) exec(ctx context.Context, command []string, cmdline st
|
||||
if ppty != nil {
|
||||
go writeKeepAlive(ppty)
|
||||
}
|
||||
// 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()
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
var exitErr *exec.ExitError
|
||||
if errors.As(err, &exitErr) {
|
||||
return ExitCodeError(exitErr.ExitCode())
|
||||
}
|
||||
return err
|
||||
}
|
||||
if tty != nil {
|
||||
@@ -415,83 +385,12 @@ func (e *HostEnvironment) UpdateFromEnv(srcPath string, env *map[string]string)
|
||||
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 {
|
||||
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 {
|
||||
e.CleanUp()
|
||||
}
|
||||
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...)
|
||||
return os.RemoveAll(e.Path)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,14 +11,9 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"gitea.com/gitea/runner/act/common"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Type assert HostEnvironment implements ExecutionsEnvironment
|
||||
@@ -74,76 +69,3 @@ func TestGetContainerArchive(t *testing.T) {
|
||||
_, err = reader.Next()
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ func TestFunctionContains(t *testing.T) {
|
||||
for _, tt := range table {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
assert.Equal(t, tt.expected, output)
|
||||
})
|
||||
@@ -72,7 +72,7 @@ func TestFunctionStartsWith(t *testing.T) {
|
||||
for _, tt := range table {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
assert.Equal(t, tt.expected, output)
|
||||
})
|
||||
@@ -101,7 +101,7 @@ func TestFunctionEndsWith(t *testing.T) {
|
||||
for _, tt := range table {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
assert.Equal(t, tt.expected, output)
|
||||
})
|
||||
@@ -128,7 +128,7 @@ func TestFunctionJoin(t *testing.T) {
|
||||
for _, tt := range table {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
assert.Equal(t, tt.expected, output)
|
||||
})
|
||||
@@ -154,7 +154,7 @@ func TestFunctionToJSON(t *testing.T) {
|
||||
for _, tt := range table {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
assert.Equal(t, tt.expected, output)
|
||||
})
|
||||
@@ -177,7 +177,7 @@ func TestFunctionFromJSON(t *testing.T) {
|
||||
for _, tt := range table {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
assert.Equal(t, tt.expected, output)
|
||||
})
|
||||
@@ -205,9 +205,9 @@ func TestFunctionHashFiles(t *testing.T) {
|
||||
for _, tt := range table {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
workdir, err := filepath.Abs("testdata")
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
output, err := NewInterpeter(env, Config{WorkingDir: workdir}).Evaluate(tt.input, DefaultStatusCheckNone)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
assert.Equal(t, tt.expected, output)
|
||||
})
|
||||
@@ -248,7 +248,7 @@ func TestFunctionFormat(t *testing.T) {
|
||||
if tt.error != nil {
|
||||
assert.Equal(t, tt.error, err.Error())
|
||||
} else {
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Equal(t, tt.expected, output)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -156,6 +156,7 @@ func (impl *interperterImpl) evaluateNode(exprNode actionlint.ExprNode) (any, er
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:gocyclo // function handles many cases
|
||||
func (impl *interperterImpl) evaluateVariable(variableNode *actionlint.VariableNode) (any, error) {
|
||||
switch strings.ToLower(variableNode.Name) {
|
||||
case "github":
|
||||
@@ -583,6 +584,7 @@ func (impl *interperterImpl) evaluateLogicalCompare(compareNode *actionlint.Logi
|
||||
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) {
|
||||
args := make([]reflect.Value, 0)
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ func TestLiterals(t *testing.T) {
|
||||
for _, tt := range table {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
assert.Equal(t, tt.expected, output)
|
||||
})
|
||||
@@ -105,10 +105,10 @@ func TestOperators(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
|
||||
if tt.error != "" {
|
||||
assert.Error(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.NotNil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Equal(t, tt.error, err.Error())
|
||||
} else {
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.expected, output)
|
||||
@@ -157,7 +157,7 @@ func TestOperatorsCompare(t *testing.T) {
|
||||
for _, tt := range table {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
assert.Equal(t, tt.expected, output)
|
||||
})
|
||||
@@ -520,7 +520,7 @@ func TestOperatorsBooleanEvaluation(t *testing.T) {
|
||||
for _, tt := range table {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
if expected, ok := tt.expected.(float64); ok && math.IsNaN(expected) {
|
||||
assert.True(t, math.IsNaN(output.(float64)))
|
||||
@@ -624,7 +624,7 @@ func TestContexts(t *testing.T) {
|
||||
for _, tt := range table {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
assert.Equal(t, tt.expected, output)
|
||||
})
|
||||
|
||||
@@ -73,16 +73,10 @@ func (cc *CopyCollector) WriteFile(fpath string, fi fs.FileInfo, linkName string
|
||||
if err := os.MkdirAll(filepath.Dir(fdestpath), 0o777); err != nil {
|
||||
return err
|
||||
}
|
||||
// Remove any existing destination so we can overwrite read-only files
|
||||
// (e.g. git pack files at mode 0444 trip EACCES on macOS and "Access is
|
||||
// denied" on Windows when reopened with O_WRONLY) and so os.Symlink does
|
||||
// not fail with EEXIST. os.Remove clears the Windows read-only attribute
|
||||
// internally; on Unix unlink only needs write permission on the parent.
|
||||
_ = os.Remove(fdestpath)
|
||||
if f == nil {
|
||||
return os.Symlink(linkName, fdestpath)
|
||||
}
|
||||
df, err := os.OpenFile(fdestpath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, fi.Mode())
|
||||
df, err := os.OpenFile(fdestpath, os.O_CREATE|os.O_WRONLY, fi.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -134,6 +128,7 @@ func (*DefaultFs) Readlink(path string) (string, error) {
|
||||
return os.Readlink(path)
|
||||
}
|
||||
|
||||
//nolint:gocyclo // function handles many cases
|
||||
func (fc *FileCollector) CollectFiles(ctx context.Context, submodulePath []string) filepath.WalkFunc {
|
||||
i, _ := fc.Fs.OpenGitIndex(path.Join(fc.SrcPath, path.Join(submodulePath...)))
|
||||
return func(file string, fi os.FileInfo, err error) error {
|
||||
|
||||
@@ -8,9 +8,7 @@ import (
|
||||
"archive/tar"
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -22,7 +20,6 @@ import (
|
||||
"github.com/go-git/go-git/v5/plumbing/format/index"
|
||||
"github.com/go-git/go-git/v5/storage/filesystem"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type memoryFs struct {
|
||||
@@ -177,47 +174,3 @@ func TestSymlinks(t *testing.T) {
|
||||
assert.Equal(t, ".env", files["test.env"].Linkname)
|
||||
assert.ErrorIs(t, err, io.EOF, "tar must be read cleanly to EOF")
|
||||
}
|
||||
|
||||
// Regression for https://gitea.com/gitea/runner/issues/876 and /941:
|
||||
// re-copying an action directory must overwrite a pre-existing read-only
|
||||
// file (e.g. a git pack .idx at mode 0444) instead of failing with EACCES
|
||||
// on macOS or "Access is denied" on Windows.
|
||||
func TestCopyCollectorWriteFileOverwritesReadOnlyFile(t *testing.T) {
|
||||
dst := t.TempDir()
|
||||
target := filepath.Join(dst, "sub", "pack.idx")
|
||||
require.NoError(t, os.MkdirAll(filepath.Dir(target), 0o755))
|
||||
require.NoError(t, os.WriteFile(target, []byte("old"), 0o444))
|
||||
|
||||
src := filepath.Join(t.TempDir(), "pack.idx")
|
||||
require.NoError(t, os.WriteFile(src, []byte("new"), 0o444))
|
||||
fi, err := os.Stat(src)
|
||||
require.NoError(t, err)
|
||||
|
||||
cc := &CopyCollector{DstDir: dst}
|
||||
require.NoError(t, cc.WriteFile("sub/pack.idx", fi, "", strings.NewReader("new")))
|
||||
|
||||
got, err := os.ReadFile(target)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "new", string(got))
|
||||
}
|
||||
|
||||
// Without the destination removal, os.Symlink fails with EEXIST when the
|
||||
// path already holds a regular file from an earlier copy of the action.
|
||||
func TestCopyCollectorWriteFileOverwritesFileWithSymlink(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("creating symlinks requires elevated privileges on Windows")
|
||||
}
|
||||
dst := t.TempDir()
|
||||
target := filepath.Join(dst, "link")
|
||||
require.NoError(t, os.WriteFile(target, []byte("stale"), 0o644))
|
||||
|
||||
fi, err := os.Lstat(target)
|
||||
require.NoError(t, err)
|
||||
|
||||
cc := &CopyCollector{DstDir: dst}
|
||||
require.NoError(t, cc.WriteFile("link", fi, "target", nil))
|
||||
|
||||
resolved, err := os.Readlink(target)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "target", resolved)
|
||||
}
|
||||
|
||||
@@ -61,6 +61,8 @@ type WorkflowFiles struct {
|
||||
}
|
||||
|
||||
// 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) {
|
||||
path, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
|
||||
@@ -57,11 +57,11 @@ func TestWorkflow(t *testing.T) {
|
||||
|
||||
// Check that an invalid job id returns error
|
||||
result, err := createStages(&workflow, "invalid_job_id")
|
||||
assert.Error(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.NotNil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, result)
|
||||
|
||||
// Check that an valid job id returns non-error
|
||||
result, err = createStages(&workflow, "valid_job")
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.NotNil(t, result)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ jobs:
|
||||
with-volumes:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: node:24-bookworm-slim
|
||||
image: node:16-buster-slim
|
||||
volumes:
|
||||
- my_docker_volume:/path/to/volume
|
||||
- /path/to/nonexist/directory
|
||||
|
||||
@@ -440,6 +440,8 @@ func (j *Job) Matrix() map[string][]any {
|
||||
|
||||
// GetMatrixes returns the matrix cross product
|
||||
// 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) {
|
||||
matrixes := make([]map[string]any, 0)
|
||||
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
|
||||
|
||||
newSchedules = workflow.OnSchedule()
|
||||
assert.Empty(t, newSchedules)
|
||||
assert.Len(t, newSchedules, 0) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
yaml = `
|
||||
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
|
||||
|
||||
newSchedules = workflow.OnSchedule()
|
||||
assert.Empty(t, newSchedules)
|
||||
assert.Len(t, newSchedules, 0) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
yaml = `
|
||||
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
|
||||
|
||||
newSchedules = workflow.OnSchedule()
|
||||
assert.Empty(t, newSchedules)
|
||||
assert.Len(t, newSchedules, 0) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
}
|
||||
|
||||
func TestReadWorkflow_StringEvent(t *testing.T) {
|
||||
@@ -870,7 +870,7 @@ jobs:
|
||||
assert.Nil(t, matrix, "matrix should be nil for jobs without strategy")
|
||||
} else {
|
||||
assert.NotNil(t, matrix, "matrix should not be nil")
|
||||
assert.Len(t, matrix, tt.wantLen, "matrix should have expected number of keys")
|
||||
assert.Equal(t, tt.wantLen, len(matrix), "matrix should have expected number of keys") //nolint:testifylint // pre-existing issue from nektos/act
|
||||
if tt.checkFn != nil {
|
||||
tt.checkFn(t, matrix)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"gitea.com/gitea/runner/act/common"
|
||||
"gitea.com/gitea/runner/act/common/git"
|
||||
"gitea.com/gitea/runner/act/container"
|
||||
"gitea.com/gitea/runner/act/model"
|
||||
|
||||
@@ -45,11 +44,6 @@ type runAction func(step actionStep, actionDir string, remoteAction *remoteActio
|
||||
//go:embed res/trampoline.js
|
||||
var trampoline embed.FS
|
||||
|
||||
var (
|
||||
ContainerImageExistsLocally = container.ImageExistsLocally
|
||||
ContainerNewDockerBuildExecutor = container.NewDockerBuildExecutor
|
||||
)
|
||||
|
||||
func readActionImpl(ctx context.Context, step *model.Step, actionDir, actionPath string, readFile actionYamlReader, writeFile fileWriter) (*model.Action, error) {
|
||||
logger := common.Logger(ctx)
|
||||
allErrors := []error{}
|
||||
@@ -154,8 +148,6 @@ func maybeCopyToActionDir(ctx context.Context, step actionStep, actionDir, actio
|
||||
return rc.JobContainer.CopyTarStream(ctx, containerActionDirCopy, ta)
|
||||
}
|
||||
|
||||
defer git.AcquireCloneLock(actionDir)()
|
||||
|
||||
if err := removeGitIgnore(ctx, actionDir); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -205,7 +197,7 @@ func runActionImpl(step actionStep, actionDir string, remoteAction *remoteAction
|
||||
if remoteAction == nil {
|
||||
location = containerActionDir
|
||||
}
|
||||
return execAsDocker(ctx, step, actionName, actionDir, location, remoteAction == nil)
|
||||
return execAsDocker(ctx, step, actionName, location, remoteAction == nil)
|
||||
case x.IsComposite():
|
||||
if err := maybeCopyToActionDir(ctx, step, actionDir, actionPath, containerActionDir); err != nil {
|
||||
return err
|
||||
@@ -273,7 +265,9 @@ func removeGitIgnore(ctx context.Context, directory string) error {
|
||||
}
|
||||
|
||||
// TODO: break out parts of function to reduce complexicity
|
||||
func execAsDocker(ctx context.Context, step actionStep, actionName, actionDir, basedir string, localAction bool) error {
|
||||
//
|
||||
//nolint:gocyclo // function handles many cases
|
||||
func execAsDocker(ctx context.Context, step actionStep, actionName, basedir string, localAction bool) error {
|
||||
logger := common.Logger(ctx)
|
||||
rc := step.getRunContext()
|
||||
action := step.getActionModel()
|
||||
@@ -292,12 +286,12 @@ func execAsDocker(ctx context.Context, step actionStep, actionName, actionDir, b
|
||||
image = strings.ToLower(image)
|
||||
contextDir, fileName := filepath.Split(filepath.Join(basedir, action.Runs.Image))
|
||||
|
||||
anyArchExists, err := ContainerImageExistsLocally(ctx, image, "any")
|
||||
anyArchExists, err := container.ImageExistsLocally(ctx, image, "any")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
correctArchExists, err := ContainerImageExistsLocally(ctx, image, rc.Config.ContainerArchitecture)
|
||||
correctArchExists, err := container.ImageExistsLocally(ctx, image, rc.Config.ContainerArchitecture)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -329,21 +323,13 @@ func execAsDocker(ctx context.Context, step actionStep, actionName, actionDir, b
|
||||
}
|
||||
defer buildContext.Close()
|
||||
}
|
||||
prepImage = ContainerNewDockerBuildExecutor(container.NewDockerBuildExecutorInput{
|
||||
prepImage = container.NewDockerBuildExecutor(container.NewDockerBuildExecutorInput{
|
||||
ContextDir: contextDir,
|
||||
Dockerfile: fileName,
|
||||
ImageTag: image,
|
||||
BuildContext: buildContext,
|
||||
Platform: rc.Config.ContainerArchitecture,
|
||||
})
|
||||
if buildContext == nil {
|
||||
// Held across the whole build: the daemon drains contextDir lazily.
|
||||
inner := prepImage
|
||||
prepImage = func(ctx context.Context) error {
|
||||
defer git.AcquireCloneLock(actionDir)()
|
||||
return inner(ctx)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.Debugf("image '%s' for architecture '%s' already exists", image, rc.Config.ContainerArchitecture)
|
||||
}
|
||||
@@ -443,7 +429,7 @@ func newStepContainer(ctx context.Context, step step, image string, cmd, entrypo
|
||||
Image: image,
|
||||
Username: rc.Config.Secrets["DOCKER_USERNAME"],
|
||||
Password: rc.Config.Secrets["DOCKER_PASSWORD"],
|
||||
Name: createContainerName(rc.jobContainerName(), "STEP-"+stepModel.ID),
|
||||
Name: createSimpleContainerName(rc.jobContainerName(), "STEP-"+stepModel.ID),
|
||||
Env: envList,
|
||||
Mounts: mounts,
|
||||
NetworkMode: networkMode,
|
||||
|
||||
@@ -9,13 +9,8 @@ import (
|
||||
"io"
|
||||
"io/fs"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.com/gitea/runner/act/common"
|
||||
"gitea.com/gitea/runner/act/common/git"
|
||||
"gitea.com/gitea/runner/act/container"
|
||||
"gitea.com/gitea/runner/act/model"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -142,7 +137,7 @@ runs:
|
||||
|
||||
action, err := readActionImpl(context.Background(), tt.step, "actionDir", "actionPath", readFile, writeFile)
|
||||
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Equal(t, tt.expected, action)
|
||||
|
||||
closerMock.AssertExpectations(t)
|
||||
@@ -252,158 +247,8 @@ func TestActionRunner(t *testing.T) {
|
||||
|
||||
err := runActionImpl(tt.step, "dir", newRemoteAction("org/repo/path@ref"))(ctx)
|
||||
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
cm.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaybeCopyToActionDirHoldsCloneLock(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
actionDir := t.TempDir()
|
||||
|
||||
releaseCopy := make(chan struct{})
|
||||
release := sync.OnceFunc(func() { close(releaseCopy) })
|
||||
defer release()
|
||||
|
||||
copyEntered := make(chan struct{})
|
||||
|
||||
cm := &containerMock{}
|
||||
cm.On("CopyDir", "/var/run/act/actions/", actionDir+"/", false).Return(func(ctx context.Context) error {
|
||||
close(copyEntered)
|
||||
<-releaseCopy
|
||||
return nil
|
||||
})
|
||||
|
||||
step := &stepActionRemote{
|
||||
Step: &model.Step{Uses: "remote/action@v1"},
|
||||
RunContext: &RunContext{
|
||||
Config: &Config{},
|
||||
JobContainer: cm,
|
||||
},
|
||||
}
|
||||
|
||||
copyDone := make(chan error, 1)
|
||||
go func() {
|
||||
copyDone <- maybeCopyToActionDir(ctx, step, actionDir, "", "/var/run/act/actions/")
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-copyEntered:
|
||||
case err := <-copyDone:
|
||||
t.Fatalf("maybeCopyToActionDir returned before CopyDir was entered: %v", err)
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("CopyDir was not entered within 1 second")
|
||||
}
|
||||
|
||||
peerAcquired := make(chan struct{})
|
||||
go func() {
|
||||
unlock := git.AcquireCloneLock(actionDir)
|
||||
close(peerAcquired)
|
||||
unlock()
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-peerAcquired:
|
||||
t.Fatal("peer AcquireCloneLock returned while CopyDir was running")
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
}
|
||||
|
||||
release()
|
||||
|
||||
select {
|
||||
case err := <-copyDone:
|
||||
if err != nil {
|
||||
t.Fatalf("maybeCopyToActionDir returned error: %v", err)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("maybeCopyToActionDir did not return after CopyDir was unblocked")
|
||||
}
|
||||
|
||||
select {
|
||||
case <-peerAcquired:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("peer AcquireCloneLock did not proceed after lock released")
|
||||
}
|
||||
|
||||
cm.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestExecAsDockerHoldsCloneLockForRemoteUncached(t *testing.T) {
|
||||
actionDir := t.TempDir()
|
||||
|
||||
unlockOnce := sync.OnceFunc(git.AcquireCloneLock(actionDir))
|
||||
defer unlockOnce()
|
||||
|
||||
innerEntered := make(chan struct{})
|
||||
releaseInner := make(chan struct{})
|
||||
releaseOnce := sync.OnceFunc(func() { close(releaseInner) })
|
||||
defer releaseOnce()
|
||||
|
||||
origImageExists := ContainerImageExistsLocally
|
||||
ContainerImageExistsLocally = func(_ context.Context, _, _ string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
defer func() { ContainerImageExistsLocally = origImageExists }()
|
||||
|
||||
origBuildExec := ContainerNewDockerBuildExecutor
|
||||
ContainerNewDockerBuildExecutor = func(_ container.NewDockerBuildExecutorInput) common.Executor {
|
||||
return func(_ context.Context) error {
|
||||
close(innerEntered)
|
||||
<-releaseInner
|
||||
return nil
|
||||
}
|
||||
}
|
||||
defer func() { ContainerNewDockerBuildExecutor = origBuildExec }()
|
||||
|
||||
step := &stepActionRemote{
|
||||
Step: &model.Step{ID: "1", Uses: "remote/action@v1", With: map[string]string{}},
|
||||
RunContext: &RunContext{
|
||||
Config: &Config{},
|
||||
Run: &model.Run{
|
||||
JobID: "1",
|
||||
Workflow: &model.Workflow{
|
||||
Name: "wf",
|
||||
Jobs: map[string]*model.Job{"1": {}},
|
||||
},
|
||||
},
|
||||
JobContainer: &containerMock{},
|
||||
},
|
||||
action: &model.Action{Runs: model.ActionRuns{Using: "docker", Image: "Dockerfile"}},
|
||||
env: map[string]string{},
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- execAsDocker(ctx, step, "test-action", actionDir, actionDir, false) }()
|
||||
|
||||
select {
|
||||
case <-innerEntered:
|
||||
t.Fatal("inner build executor ran before clone lock was released")
|
||||
case err := <-done:
|
||||
t.Fatalf("execAsDocker returned before inner was entered: %v", err)
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
}
|
||||
|
||||
unlockOnce()
|
||||
|
||||
select {
|
||||
case <-innerEntered:
|
||||
case err := <-done:
|
||||
t.Fatalf("execAsDocker returned without entering inner: %v", err)
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("inner build executor not entered after lock released")
|
||||
}
|
||||
|
||||
cancel()
|
||||
releaseOnce()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("execAsDocker did not return after inner was released and ctx was canceled")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ func (rc *RunContext) setOutput(ctx context.Context, kvPairs map[string]string,
|
||||
|
||||
result, ok := rc.StepResults[stepID]
|
||||
if !ok {
|
||||
logger.Infof("No outputs registered for step '%s'", stepID)
|
||||
logger.Infof(" \U00002757 no outputs used step '%s'", stepID)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -405,6 +405,7 @@ func escapeFormatString(in string) string {
|
||||
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
|
||||
if !strings.Contains(in, "${{") || !strings.Contains(in, "}}") {
|
||||
return in, nil
|
||||
@@ -471,6 +472,7 @@ func rewriteSubExpression(ctx context.Context, in string, forceFormat bool) (str
|
||||
return out, nil
|
||||
}
|
||||
|
||||
//nolint:gocyclo // function handles many cases
|
||||
func getEvaluatorInputs(ctx context.Context, rc *RunContext, step step, ghc *model.GithubContext) map[string]any {
|
||||
inputs := map[string]any{}
|
||||
|
||||
|
||||
@@ -24,13 +24,7 @@ type jobInfo interface {
|
||||
result(result string)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
//nolint:contextcheck,gocyclo // composes many step executors
|
||||
func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executor {
|
||||
steps := make([]common.Executor, 0)
|
||||
preSteps := make([]common.Executor, 0)
|
||||
@@ -39,7 +33,7 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
|
||||
steps = append(steps, func(ctx context.Context) error {
|
||||
logger := common.Logger(ctx)
|
||||
if len(info.matrix()) > 0 {
|
||||
logger.Infof("Matrix: %v", info.matrix())
|
||||
logger.Infof("\U0001F9EA Matrix: %v", info.matrix())
|
||||
}
|
||||
return nil
|
||||
})
|
||||
@@ -82,36 +76,33 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
|
||||
|
||||
preExec := step.pre()
|
||||
preSteps = append(preSteps, useStepLogger(rc, stepModel, stepStagePre, func(ctx context.Context) error {
|
||||
logger := common.Logger(ctx)
|
||||
preErr := preExec(ctx)
|
||||
if preErr != nil {
|
||||
reportStepError(ctx, preErr)
|
||||
logger.Errorf("%v", preErr)
|
||||
common.SetJobError(ctx, preErr)
|
||||
} else if ctx.Err() != nil {
|
||||
reportStepError(ctx, ctx.Err())
|
||||
logger.Errorf("%v", ctx.Err())
|
||||
common.SetJobError(ctx, ctx.Err())
|
||||
}
|
||||
return preErr
|
||||
}))
|
||||
|
||||
stepExec := step.main()
|
||||
steps = append(steps, useStepLogger(rc, stepModel, stepStageMain, func(ctx context.Context) error {
|
||||
logger := common.Logger(ctx)
|
||||
err := stepExec(ctx)
|
||||
if err != nil {
|
||||
reportStepError(ctx, err)
|
||||
logger.Errorf("%v", err)
|
||||
common.SetJobError(ctx, err)
|
||||
} else if ctx.Err() != nil {
|
||||
reportStepError(ctx, ctx.Err())
|
||||
logger.Errorf("%v", ctx.Err())
|
||||
common.SetJobError(ctx, ctx.Err())
|
||||
}
|
||||
return nil
|
||||
}))
|
||||
|
||||
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
|
||||
})
|
||||
postExec := useStepLogger(rc, stepModel, stepStagePost, step.post())
|
||||
if postExecutor != nil {
|
||||
// run the post executor in reverse order
|
||||
postExecutor = postExec.Finally(postExecutor)
|
||||
@@ -166,7 +157,7 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
|
||||
pipeline = append(pipeline, steps...)
|
||||
|
||||
return common.NewPipelineExecutor(info.startContainer(), common.NewPipelineExecutor(pipeline...).
|
||||
Finally(func(ctx context.Context) error {
|
||||
Finally(func(ctx context.Context) error { //nolint:contextcheck // intentionally detaches from canceled parent
|
||||
var cancel context.CancelFunc
|
||||
if ctx.Err() == context.Canceled {
|
||||
// in case of an aborted run, we still should execute the
|
||||
@@ -206,7 +197,7 @@ func setJobResult(ctx context.Context, info jobInfo, rc *RunContext, success boo
|
||||
jobResultMessage = "failed"
|
||||
}
|
||||
|
||||
logger.WithField("jobResult", jobResult).Infof("Job %s", jobResultMessage)
|
||||
logger.WithField("jobResult", jobResult).Infof("\U0001F3C1 Job %s", jobResultMessage)
|
||||
}
|
||||
|
||||
func setJobOutputs(ctx context.Context, rc *RunContext) {
|
||||
|
||||
@@ -331,7 +331,7 @@ func TestNewJobExecutor(t *testing.T) {
|
||||
|
||||
executor := newJobExecutor(jim, sfm, rc)
|
||||
err := executor(ctx)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Equal(t, tt.executedSteps, executorOrder)
|
||||
|
||||
jim.AssertExpectations(t)
|
||||
|
||||
@@ -30,11 +30,6 @@ const (
|
||||
gray = 37
|
||||
)
|
||||
|
||||
const (
|
||||
rawOutputField = "raw_output"
|
||||
scriptLineCyanField = "script_line_cyan"
|
||||
)
|
||||
|
||||
var (
|
||||
colors []int
|
||||
nextColor int
|
||||
@@ -166,8 +161,6 @@ func withStepLogger(ctx context.Context, stepNumber int, stepID, stepName, stage
|
||||
|
||||
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 {
|
||||
return func(entry *logrus.Entry) *logrus.Entry {
|
||||
if insecureSecrets {
|
||||
@@ -234,12 +227,8 @@ func (f *jobLogFormatter) printColored(b *bytes.Buffer, entry *logrus.Entry) {
|
||||
debugFlag = "[DEBUG] "
|
||||
}
|
||||
|
||||
if entry.Data[rawOutputField] == true {
|
||||
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)
|
||||
}
|
||||
if entry.Data["raw_output"] == true {
|
||||
fmt.Fprintf(b, "\x1b[%dm|\x1b[0m %s", f.color, entry.Message)
|
||||
} 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)
|
||||
} else {
|
||||
@@ -262,7 +251,7 @@ func (f *jobLogFormatter) print(b *bytes.Buffer, entry *logrus.Entry) {
|
||||
debugFlag = "[DEBUG] "
|
||||
}
|
||||
|
||||
if entry.Data[rawOutputField] == true {
|
||||
if entry.Data["raw_output"] == true {
|
||||
fmt.Fprintf(b, "[%s] | %s", job, entry.Message)
|
||||
} else if entry.Data["dryrun"] == true {
|
||||
fmt.Fprintf(b, "*DRYRUN* [%s] %s%s", job, debugFlag, entry.Message)
|
||||
|
||||
@@ -60,7 +60,7 @@ func TestMaxParallelStrategy(t *testing.T) {
|
||||
matrixes, err := job.GetMatrixes()
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.NotNil(t, matrixes)
|
||||
assert.Len(t, matrixes, 5)
|
||||
assert.Equal(t, 5, len(matrixes)) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Equal(t, tt.expectedMaxParallel, job.Strategy.MaxParallel)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,11 +7,15 @@ package runner
|
||||
import (
|
||||
"archive/tar"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"gitea.com/gitea/runner/act/common"
|
||||
"gitea.com/gitea/runner/act/common/git"
|
||||
@@ -47,7 +51,7 @@ func newLocalReusableWorkflowExecutor(rc *RunContext) common.Executor {
|
||||
token := rc.Config.GetToken()
|
||||
|
||||
return common.NewPipelineExecutor(
|
||||
cloneRemoteReusableWorkflow(rc, remoteReusableWorkflow.CloneURL(), remoteReusableWorkflow.Ref, workflowDir, token),
|
||||
newMutexExecutor(cloneIfRequired(rc, *remoteReusableWorkflow, workflowDir, token)),
|
||||
newReusableWorkflowExecutor(rc, workflowDir, remoteReusableWorkflow.FilePath()),
|
||||
)
|
||||
}
|
||||
@@ -81,7 +85,7 @@ func newRemoteReusableWorkflowExecutor(rc *RunContext) common.Executor {
|
||||
token := getGitCloneToken(rc.Config, remoteReusableWorkflow.CloneURL())
|
||||
|
||||
return common.NewPipelineExecutor(
|
||||
cloneRemoteReusableWorkflow(rc, remoteReusableWorkflow.CloneURL(), remoteReusableWorkflow.Ref, workflowDir, token),
|
||||
newMutexExecutor(cloneIfRequired(rc, *remoteReusableWorkflow, workflowDir, token)),
|
||||
newReusableWorkflowExecutor(rc, workflowDir, remoteReusableWorkflow.FilePath()),
|
||||
)
|
||||
}
|
||||
@@ -121,37 +125,46 @@ func newActionCacheReusableWorkflowExecutor(rc *RunContext, filename string, rem
|
||||
}
|
||||
}
|
||||
|
||||
// cloneRemoteReusableWorkflow always invokes the clone executor — moving refs
|
||||
// (branches, tags) must be re-resolved each run, matching GitHub Actions.
|
||||
//
|
||||
// Callers must not change remoteReusableWorkflow.URL, because:
|
||||
// 1. Gitea doesn't support specifying GithubContext.ServerURL by the GITHUB_SERVER_URL env
|
||||
// 2. Gitea has already full URL with rc.Config.GitHubInstance when calling newRemoteReusableWorkflowWithPlat
|
||||
//
|
||||
// remoteReusableWorkflow.URL = rc.getGithubContext(ctx).ServerURL
|
||||
func cloneRemoteReusableWorkflow(rc *RunContext, cloneURL, ref, targetDirectory, token string) common.Executor {
|
||||
var executorLock sync.Mutex
|
||||
|
||||
func newMutexExecutor(executor common.Executor) common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
cloneURL = rc.NewExpressionEvaluator(ctx).Interpolate(ctx, cloneURL)
|
||||
return git.NewGitCloneExecutor(git.NewGitCloneExecutorInput{
|
||||
URL: cloneURL,
|
||||
Ref: ref,
|
||||
Dir: targetDirectory,
|
||||
Token: token,
|
||||
OfflineMode: rc.Config.ActionOfflineMode,
|
||||
})(ctx)
|
||||
executorLock.Lock()
|
||||
defer executorLock.Unlock()
|
||||
|
||||
return executor(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
var modelNewWorkflowPlanner = model.NewWorkflowPlanner
|
||||
func cloneIfRequired(rc *RunContext, remoteReusableWorkflow remoteReusableWorkflow, targetDirectory, token string) common.Executor {
|
||||
return common.NewConditionalExecutor(
|
||||
func(ctx context.Context) bool {
|
||||
_, err := os.Stat(targetDirectory)
|
||||
notExists := errors.Is(err, fs.ErrNotExist)
|
||||
return notExists
|
||||
},
|
||||
func(ctx context.Context) error {
|
||||
// interpolate the cloneURL
|
||||
cloneURL := rc.NewExpressionEvaluator(ctx).Interpolate(ctx, remoteReusableWorkflow.CloneURL())
|
||||
// Do not change the remoteReusableWorkflow.URL, because:
|
||||
// 1. Gitea doesn't support specifying GithubContext.ServerURL by the GITHUB_SERVER_URL env
|
||||
// 2. Gitea has already full URL with rc.Config.GitHubInstance when calling newRemoteReusableWorkflowWithPlat
|
||||
// remoteReusableWorkflow.URL = rc.getGithubContext(ctx).ServerURL
|
||||
return git.NewGitCloneExecutor(git.NewGitCloneExecutorInput{
|
||||
URL: cloneURL,
|
||||
Ref: remoteReusableWorkflow.Ref,
|
||||
Dir: targetDirectory,
|
||||
Token: token,
|
||||
OfflineMode: rc.Config.ActionOfflineMode,
|
||||
})(ctx)
|
||||
},
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
func newReusableWorkflowExecutor(rc *RunContext, directory, workflow string) common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
// Scoped to the yaml read so concurrent invocations don't serialize
|
||||
// on the whole job run.
|
||||
planner, err := func() (model.WorkflowPlanner, error) {
|
||||
defer git.AcquireCloneLock(directory)()
|
||||
return modelNewWorkflowPlanner(path.Join(directory, workflow), true)
|
||||
}()
|
||||
planner, err := model.NewWorkflowPlanner(path.Join(directory, workflow), true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -285,7 +298,7 @@ func setReusedWorkflowCallerResult(rc *RunContext, runner Runner) common.Executo
|
||||
rc.caller.setReusedWorkflowJobResult(rc.JobName, reusedWorkflowJobResult)
|
||||
} else {
|
||||
rc.result(reusedWorkflowJobResult)
|
||||
logger.WithField("jobResult", reusedWorkflowJobResult).Infof("Job %s", reusedWorkflowJobResultMessage)
|
||||
logger.WithField("jobResult", reusedWorkflowJobResult).Infof("\U0001F3C1 Job %s", reusedWorkflowJobResultMessage)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.com/gitea/runner/act/common/git"
|
||||
"gitea.com/gitea/runner/act/model"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Regression test for go-gitea/gitea#37483: a remote reusable workflow at a moving
|
||||
// ref (branch/tag) must reflect the new tip on every invocation, not stay pinned
|
||||
// to the cache populated on the first run.
|
||||
func TestReusableWorkflowCachedBranchRefRefreshes(t *testing.T) {
|
||||
if _, err := exec.LookPath("git"); err != nil {
|
||||
t.Skip("git not available in PATH")
|
||||
}
|
||||
|
||||
remoteDir := t.TempDir()
|
||||
gitMust(t, "", "init", "--bare", "--initial-branch=master", remoteDir)
|
||||
|
||||
workDir := t.TempDir()
|
||||
gitMust(t, "", "clone", remoteDir, workDir)
|
||||
gitMust(t, workDir, "config", "user.email", "test@test")
|
||||
gitMust(t, workDir, "config", "user.name", "test")
|
||||
gitMust(t, workDir, "checkout", "-b", "master")
|
||||
|
||||
const workflowPath = ".gitea/workflows/reusable.yml"
|
||||
tmpl := func(tag string) string {
|
||||
return "name: reusable\non:\n workflow_call:\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - run: echo " + tag + "\n"
|
||||
}
|
||||
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(workDir, ".gitea/workflows"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(workDir, workflowPath), []byte(tmpl("v1")), 0o644))
|
||||
gitMust(t, workDir, "add", workflowPath)
|
||||
gitMust(t, workDir, "commit", "-m", "v1")
|
||||
gitMust(t, workDir, "push", "-u", "origin", "master")
|
||||
|
||||
rc := &RunContext{
|
||||
Config: &Config{},
|
||||
Run: &model.Run{
|
||||
JobID: "j1",
|
||||
Workflow: &model.Workflow{
|
||||
Name: "wf",
|
||||
Jobs: map[string]*model.Job{"j1": {}},
|
||||
},
|
||||
},
|
||||
}
|
||||
cacheDir := t.TempDir()
|
||||
|
||||
require.NoError(t, cloneRemoteReusableWorkflow(rc, remoteDir, "master", cacheDir, "")(context.Background()))
|
||||
got, err := os.ReadFile(filepath.Join(cacheDir, workflowPath))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tmpl("v1"), string(got))
|
||||
|
||||
// Branch tip moves; cache key (cacheDir) does not.
|
||||
require.NoError(t, os.WriteFile(filepath.Join(workDir, workflowPath), []byte(tmpl("v2")), 0o644))
|
||||
gitMust(t, workDir, "commit", "-am", "v2")
|
||||
gitMust(t, workDir, "push", "origin", "master")
|
||||
|
||||
require.NoError(t, cloneRemoteReusableWorkflow(rc, remoteDir, "master", cacheDir, "")(context.Background()))
|
||||
got, err = os.ReadFile(filepath.Join(cacheDir, workflowPath))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tmpl("v2"), string(got), "cached workflow file must reflect the updated branch tip")
|
||||
}
|
||||
|
||||
func TestNewReusableWorkflowExecutorHoldsCloneLock(t *testing.T) {
|
||||
workflowDir := t.TempDir()
|
||||
|
||||
unlockOnce := sync.OnceFunc(git.AcquireCloneLock(workflowDir))
|
||||
defer unlockOnce()
|
||||
|
||||
plannerCalled := make(chan struct{})
|
||||
|
||||
origPlanner := modelNewWorkflowPlanner
|
||||
modelNewWorkflowPlanner = func(string, bool) (model.WorkflowPlanner, error) {
|
||||
close(plannerCalled)
|
||||
return nil, errors.New("stop")
|
||||
}
|
||||
defer func() { modelNewWorkflowPlanner = origPlanner }()
|
||||
|
||||
rc := &RunContext{
|
||||
Config: &Config{},
|
||||
Run: &model.Run{Workflow: &model.Workflow{Jobs: map[string]*model.Job{}}},
|
||||
}
|
||||
exec := newReusableWorkflowExecutor(rc, workflowDir, "reusable.yml")
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- exec(context.Background()) }()
|
||||
|
||||
select {
|
||||
case <-plannerCalled:
|
||||
t.Fatal("planner ran while clone lock was held")
|
||||
case err := <-done:
|
||||
t.Fatalf("executor returned before planner was reached: %v", err)
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
}
|
||||
|
||||
unlockOnce()
|
||||
|
||||
select {
|
||||
case <-plannerCalled:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("planner not called after lock was released")
|
||||
}
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
require.Error(t, err)
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("executor did not return after planner ran")
|
||||
}
|
||||
}
|
||||
|
||||
func gitMust(t *testing.T, dir string, args ...string) {
|
||||
t.Helper()
|
||||
cmd := exec.Command("git", args...)
|
||||
if dir != "" {
|
||||
cmd.Dir = dir
|
||||
}
|
||||
out, err := cmd.CombinedOutput()
|
||||
require.NoError(t, err, "git %v: %s", args, string(out))
|
||||
}
|
||||
@@ -101,7 +101,8 @@ func (rc *RunContext) jobContainerName() string {
|
||||
if rc.caller != nil {
|
||||
nameParts = append(nameParts, "CALLED-BY-"+rc.caller.runContext.JobName)
|
||||
}
|
||||
return createContainerName(nameParts...) // For Gitea
|
||||
// return createSimpleContainerName(rc.Config.ContainerNamePrefix, "WORKFLOW-"+rc.Run.Workflow.Name, "JOB-"+rc.Name)
|
||||
return createSimpleContainerName(nameParts...) // For Gitea
|
||||
}
|
||||
|
||||
// networkNameForGitea return the name of the network
|
||||
@@ -193,7 +194,7 @@ func (rc *RunContext) GetBindsAndMounts() ([]string, map[string]string) {
|
||||
func (rc *RunContext) startHostEnvironment() common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
logger := common.Logger(ctx)
|
||||
rawLogger := logger.WithField(rawOutputField, true)
|
||||
rawLogger := logger.WithField("raw_output", true)
|
||||
logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool {
|
||||
if rc.Config.LogOutput {
|
||||
rawLogger.Infof("%s", s)
|
||||
@@ -220,12 +221,11 @@ func (rc *RunContext) startHostEnvironment() common.Executor {
|
||||
}
|
||||
toolCache := filepath.Join(cacheDir, "tool_cache")
|
||||
rc.JobContainer = &container.HostEnvironment{
|
||||
Path: path,
|
||||
TmpDir: runnerTmp,
|
||||
ToolCache: toolCache,
|
||||
Workdir: rc.Config.Workdir,
|
||||
BindWorkdir: rc.Config.BindWorkdir,
|
||||
ActPath: actPath,
|
||||
Path: path,
|
||||
TmpDir: runnerTmp,
|
||||
ToolCache: toolCache,
|
||||
Workdir: rc.Config.Workdir,
|
||||
ActPath: actPath,
|
||||
CleanUp: func() {
|
||||
os.RemoveAll(miscpath)
|
||||
},
|
||||
@@ -260,24 +260,12 @@ func (rc *RunContext) startHostEnvironment() common.Executor {
|
||||
}
|
||||
}
|
||||
|
||||
// printStartJobContainerGroup mirrors actions/runner's "Starting job container"
|
||||
// section: emit the group header and summary, return a closer for ::endgroup::.
|
||||
func printStartJobContainerGroup(ctx context.Context, image, name, network string) func() {
|
||||
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
|
||||
rawLogger.Infof("::group::Starting job container")
|
||||
rawLogger.Infof("image: %s", image)
|
||||
rawLogger.Infof("name: %s", name)
|
||||
rawLogger.Infof("network: %s", network)
|
||||
return func() {
|
||||
rawLogger.Infof("::endgroup::")
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:gocyclo // function handles many cases
|
||||
func (rc *RunContext) startJobContainer() common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
logger := common.Logger(ctx)
|
||||
image := rc.platformImage(ctx)
|
||||
rawLogger := logger.WithField(rawOutputField, true)
|
||||
rawLogger := logger.WithField("raw_output", true)
|
||||
logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool {
|
||||
if rc.Config.LogOutput {
|
||||
rawLogger.Infof("%s", s)
|
||||
@@ -292,6 +280,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
||||
return fmt.Errorf("failed to handle credentials: %s", err)
|
||||
}
|
||||
|
||||
logger.Infof("\U0001f680 Start image=%s", image)
|
||||
name := rc.jobContainerName()
|
||||
// For gitea, to support --volumes-from <container_name_or_id> in options.
|
||||
// We need to set the container name to the environment variable.
|
||||
@@ -436,7 +425,6 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
||||
return errors.New("Failed to create job container")
|
||||
}
|
||||
|
||||
defer printStartJobContainerGroup(ctx, image, name, networkName)()
|
||||
return common.NewPipelineExecutor(
|
||||
rc.pullServicesImages(rc.Config.ForcePull),
|
||||
rc.JobContainer.Pull(rc.Config.ForcePull),
|
||||
@@ -743,7 +731,7 @@ func (rc *RunContext) isEnabled(ctx context.Context) (bool, error) {
|
||||
jobType, jobTypeErr := job.Type()
|
||||
|
||||
if runJobErr != nil {
|
||||
return false, fmt.Errorf("if-expression %q evaluation failed: %s", job.If.Value, runJobErr)
|
||||
return false, fmt.Errorf(" \u274C Error in if-expression: \"if: %s\" (%s)", job.If.Value, runJobErr)
|
||||
}
|
||||
|
||||
if jobType == model.JobTypeInvalid {
|
||||
@@ -766,7 +754,7 @@ func (rc *RunContext) isEnabled(ctx context.Context) (bool, error) {
|
||||
img := rc.platformImage(ctx)
|
||||
if img == "" {
|
||||
for _, platformName := range rc.runsOnPlatformNames(ctx) {
|
||||
l.Infof("Skipping unsupported platform -- Try running with `-P %+v=...`", platformName)
|
||||
l.Infof("\U0001F6A7 Skipping unsupported platform -- Try running with `-P %+v=...`", platformName)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
@@ -781,6 +769,7 @@ func mergeMaps(maps ...map[string]string) map[string]string {
|
||||
return rtnMap
|
||||
}
|
||||
|
||||
// Deprecated: use createSimpleContainerName
|
||||
func createContainerName(parts ...string) string {
|
||||
name := strings.Join(parts, "-")
|
||||
pattern := regexp.MustCompile("[^a-zA-Z0-9]")
|
||||
@@ -794,6 +783,22 @@ func createContainerName(parts ...string) string {
|
||||
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 {
|
||||
if l < 0 {
|
||||
l = 0
|
||||
@@ -821,6 +826,7 @@ func (rc *RunContext) getStepsContext() map[string]*model.StepResult {
|
||||
return rc.StepResults
|
||||
}
|
||||
|
||||
//nolint:gocyclo // function handles many cases
|
||||
func (rc *RunContext) getGithubContext(ctx context.Context) *model.GithubContext {
|
||||
logger := common.Logger(ctx)
|
||||
ghc := &model.GithubContext{
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -13,7 +12,6 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.com/gitea/runner/act/common"
|
||||
"gitea.com/gitea/runner/act/exprparser"
|
||||
"gitea.com/gitea/runner/act/model"
|
||||
|
||||
@@ -284,7 +282,7 @@ func TestGetGitHubContext(t *testing.T) {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
rc := &RunContext{
|
||||
Config: &Config{
|
||||
@@ -624,38 +622,23 @@ func TestRunContextGetEnv(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateContainerNameBoundedForLongMatrixInput(t *testing.T) {
|
||||
longMatrixValue := strings.Repeat("os=ubuntu-latest-go=1.24-node=22-", 20)
|
||||
name := createContainerName(
|
||||
"gitea",
|
||||
"WORKFLOW-super-long-workflow-name",
|
||||
"JOB-build-matrix-"+longMatrixValue,
|
||||
)
|
||||
|
||||
assert.LessOrEqual(t, len(name), 128)
|
||||
assert.LessOrEqual(t, len(name+"-env"), 255)
|
||||
assert.LessOrEqual(t, len(name+"-network"), 255)
|
||||
assert.LessOrEqual(t, len(name+"-job1234567890"), 255)
|
||||
}
|
||||
|
||||
func TestPrintStartJobContainerGroupGolden(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
logger := log.New()
|
||||
logger.SetOutput(buf)
|
||||
logger.SetLevel(log.InfoLevel)
|
||||
logger.SetFormatter(&jobLogFormatter{color: cyan})
|
||||
entry := logger.WithFields(log.Fields{"job": "j1"})
|
||||
ctx := common.WithLogger(context.Background(), entry)
|
||||
|
||||
printStartJobContainerGroup(ctx, "node:20", "GITEA-WORKFLOW-build-JOB-test", "gitea-runner-network")()
|
||||
|
||||
want := strings.Join([]string{
|
||||
"[j1] | ::group::Starting job container",
|
||||
"[j1] | image: node:20",
|
||||
"[j1] | name: GITEA-WORKFLOW-build-JOB-test",
|
||||
"[j1] | network: gitea-runner-network",
|
||||
"[j1] | ::endgroup::",
|
||||
"",
|
||||
}, "\n")
|
||||
assert.Equal(t, want, buf.String())
|
||||
func Test_createSimpleContainerName(t *testing.T) {
|
||||
tests := []struct {
|
||||
parts []string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
parts: []string{"a--a", "BB正", "c-C"},
|
||||
want: "a-a_BB_c-C",
|
||||
},
|
||||
{
|
||||
parts: []string{"a-a", "", "-"},
|
||||
want: "a-a",
|
||||
},
|
||||
}
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,6 +137,8 @@ func (runner *runnerImpl) configure() (Runner, error) {
|
||||
}
|
||||
|
||||
// NewPlanExecutor ...
|
||||
//
|
||||
//nolint:gocyclo // function handles many cases
|
||||
func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor {
|
||||
maxJobNameLen := 0
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
baseImage = "node:24-bookworm-slim"
|
||||
baseImage = "node:16-buster-slim"
|
||||
platforms map[string]string
|
||||
logLevel = log.DebugLevel
|
||||
workdir = "testdata"
|
||||
@@ -87,7 +87,7 @@ func TestGraphMissingEvent(t *testing.T) {
|
||||
plan, err := planner.PlanEvent("push")
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.NotNil(t, plan)
|
||||
assert.Empty(t, plan.Stages)
|
||||
assert.Equal(t, 0, len(plan.Stages)) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
assert.Contains(t, buf.String(), "no events found for workflow: no-event.yml")
|
||||
log.SetOutput(out)
|
||||
@@ -100,7 +100,7 @@ func TestGraphMissingFirst(t *testing.T) {
|
||||
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.NotNil(t, plan)
|
||||
assert.Empty(t, plan.Stages)
|
||||
assert.Equal(t, 0, len(plan.Stages)) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
}
|
||||
|
||||
func TestGraphWithMissing(t *testing.T) {
|
||||
@@ -114,7 +114,7 @@ func TestGraphWithMissing(t *testing.T) {
|
||||
|
||||
plan, err := planner.PlanEvent("push")
|
||||
assert.NotNil(t, plan)
|
||||
assert.Empty(t, plan.Stages)
|
||||
assert.Equal(t, 0, len(plan.Stages)) //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)")
|
||||
log.SetOutput(out)
|
||||
@@ -134,7 +134,7 @@ func TestGraphWithSomeMissing(t *testing.T) {
|
||||
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.NotNil(t, plan)
|
||||
assert.Len(t, plan.Stages, 1)
|
||||
assert.Equal(t, 1, len(plan.Stages)) //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 no first (no-first.yml)")
|
||||
log.SetOutput(out)
|
||||
@@ -159,7 +159,7 @@ func TestGraphEvent(t *testing.T) {
|
||||
plan, err = planner.PlanEvent("release")
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.NotNil(t, plan)
|
||||
assert.Empty(t, plan.Stages)
|
||||
assert.Equal(t, 0, len(plan.Stages)) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
}
|
||||
|
||||
type TestJobFileInfo struct {
|
||||
@@ -177,7 +177,7 @@ func (j *TestJobFileInfo) runTest(ctx context.Context, t *testing.T, cfg *Config
|
||||
log.SetLevel(logLevel)
|
||||
|
||||
workdir, err := filepath.Abs(j.workdir)
|
||||
assert.NoError(t, err, workdir) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err, workdir) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
fullWorkflowPath := filepath.Join(workdir, j.workflowPath)
|
||||
runnerConfig := &Config{
|
||||
@@ -197,17 +197,17 @@ func (j *TestJobFileInfo) runTest(ctx context.Context, t *testing.T, cfg *Config
|
||||
}
|
||||
|
||||
runner, err := New(runnerConfig)
|
||||
assert.NoError(t, err, j.workflowPath) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err, j.workflowPath) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
planner, err := model.NewWorkflowPlanner(fullWorkflowPath, true)
|
||||
assert.NoError(t, err, fullWorkflowPath) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err, fullWorkflowPath) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
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
|
||||
if err == nil && plan != nil {
|
||||
err = runner.NewPlanExecutor(plan)(ctx)
|
||||
if j.errorMessage == "" {
|
||||
assert.NoError(t, err, fullWorkflowPath) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err, fullWorkflowPath) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
} else {
|
||||
assert.Error(t, err, j.errorMessage) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
}
|
||||
@@ -230,9 +230,11 @@ func TestRunEvent(t *testing.T) {
|
||||
tables := []TestJobFileInfo{
|
||||
// Shells
|
||||
{workdir, "shells/defaults", "push", "", platforms, secrets},
|
||||
// TODO: figure out why it fails
|
||||
// {workdir, "shells/custom", "push", "", map[string]string{"ubuntu-latest": "catthehacker/ubuntu:pwsh-latest"}, }, // custom image with pwsh
|
||||
{workdir, "shells/pwsh", "push", "", map[string]string{"ubuntu-latest": "catthehacker/ubuntu:pwsh-latest"}, secrets}, // custom image with pwsh
|
||||
{workdir, "shells/bash", "push", "", platforms, secrets},
|
||||
{workdir, "shells/python", "push", "", map[string]string{"ubuntu-latest": "node:24-bookworm"}, secrets}, // slim doesn't have python
|
||||
{workdir, "shells/python", "push", "", map[string]string{"ubuntu-latest": "node:16-buster"}, secrets}, // slim doesn't have python
|
||||
{workdir, "shells/sh", "push", "", platforms, secrets},
|
||||
|
||||
// Local action
|
||||
@@ -461,7 +463,7 @@ func TestDryrunEvent(t *testing.T) {
|
||||
{workdir, "shells/defaults", "push", "", platforms, secrets},
|
||||
{workdir, "shells/pwsh", "push", "", map[string]string{"ubuntu-latest": "catthehacker/ubuntu:pwsh-latest"}, secrets}, // custom image with pwsh
|
||||
{workdir, "shells/bash", "push", "", platforms, secrets},
|
||||
{workdir, "shells/python", "push", "", map[string]string{"ubuntu-latest": "node:24-bookworm"}, secrets}, // slim doesn't have python
|
||||
{workdir, "shells/python", "push", "", map[string]string{"ubuntu-latest": "node:16-buster"}, secrets}, // slim doesn't have python
|
||||
{workdir, "shells/sh", "push", "", platforms, secrets},
|
||||
|
||||
// Local action
|
||||
@@ -591,7 +593,7 @@ func TestRunWithService(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
platforms := map[string]string{
|
||||
"ubuntu-latest": "node:24-bookworm-slim",
|
||||
"ubuntu-latest": "node:12.20.1-buster-slim",
|
||||
}
|
||||
|
||||
workflowPath := "services"
|
||||
|
||||
@@ -107,7 +107,7 @@ func runStepExecutor(step step, stage stepStage, executor common.Executor) commo
|
||||
if strings.Contains(stepString, "::add-mask::") {
|
||||
stepString = "add-mask command"
|
||||
}
|
||||
logger.Infof("Run %s %s", stage, stepString)
|
||||
logger.Infof("\u2B50 Run %s %s", stage, stepString)
|
||||
|
||||
// Prepare and clean Runner File Commands
|
||||
actPath := rc.JobContainer.GetActPath()
|
||||
@@ -158,7 +158,7 @@ func runStepExecutor(step step, stage stepStage, executor common.Executor) commo
|
||||
err = executor(timeoutctx)
|
||||
|
||||
if err == nil {
|
||||
logger.WithField("stepResult", stepResult.Outcome).Infof("Success - %s %s", stage, stepString)
|
||||
logger.WithField("stepResult", stepResult.Outcome).Infof(" \u2705 Success - %s %s", stage, stepString)
|
||||
} else {
|
||||
stepResult.Outcome = model.StepStatusFailure
|
||||
|
||||
@@ -169,7 +169,6 @@ func runStepExecutor(step step, stage stepStage, executor common.Executor) commo
|
||||
}
|
||||
|
||||
if continueOnError {
|
||||
logger.Errorf("##[error]%v", err)
|
||||
logger.Infof("Failed but continue next step")
|
||||
err = nil
|
||||
stepResult.Conclusion = model.StepStatusSuccess
|
||||
@@ -177,9 +176,7 @@ func runStepExecutor(step step, stage stepStage, executor common.Executor) commo
|
||||
stepResult.Conclusion = model.StepStatusFailure
|
||||
}
|
||||
|
||||
// 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)
|
||||
logger.WithField("stepResult", stepResult.Outcome).Errorf(" \u274C Failure - %s %s", stage, stepString)
|
||||
}
|
||||
// Process Runner File Commands
|
||||
orgerr := err
|
||||
@@ -271,7 +268,7 @@ func isStepEnabled(ctx context.Context, expr string, step step, stage stepStage)
|
||||
|
||||
runStep, err := EvalBool(ctx, rc.NewStepExpressionEvaluator(ctx, step), expr, defaultStatusCheck)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("if-expression %q evaluation failed: %s", expr, err)
|
||||
return false, fmt.Errorf(" \u274C Error in if-expression: \"if: %s\" (%s)", expr, err)
|
||||
}
|
||||
|
||||
return runStep, nil
|
||||
@@ -287,7 +284,7 @@ func isContinueOnError(ctx context.Context, expr string, step step, _ stepStage)
|
||||
|
||||
continueOnError, err := EvalBool(ctx, rc.NewStepExpressionEvaluator(ctx, step), expr, exprparser.DefaultStatusCheckNone)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("continue-on-error expression %q evaluation failed: %s", expr, err)
|
||||
return false, fmt.Errorf(" \u274C Error in continue-on-error-expression: \"continue-on-error: %s\" (%s)", expr, err)
|
||||
}
|
||||
|
||||
return continueOnError, nil
|
||||
|
||||
@@ -44,10 +44,6 @@ func (sal *stepActionLocal) main() common.Executor {
|
||||
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)
|
||||
|
||||
localReader := func(ctx context.Context) actionYamlReader {
|
||||
|
||||
@@ -97,10 +97,10 @@ func TestStepActionLocalTest(t *testing.T) {
|
||||
})
|
||||
|
||||
err := sal.pre()(ctx)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
err = sal.main()(ctx)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
cm.AssertExpectations(t)
|
||||
salm.AssertExpectations(t)
|
||||
|
||||
@@ -39,6 +39,7 @@ type stepActionRemote struct {
|
||||
|
||||
var stepActionRemoteNewCloneExecutor = git.NewGitCloneExecutor
|
||||
|
||||
//nolint:gocyclo // function handles many cases
|
||||
func (sar *stepActionRemote) prepareActionExecutor() common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
if sar.remoteAction != nil && sar.action != nil {
|
||||
@@ -145,7 +146,6 @@ func (sar *stepActionRemote) prepareActionExecutor() common.Executor {
|
||||
return common.NewPipelineExecutor(
|
||||
ntErr,
|
||||
func(ctx context.Context) error {
|
||||
defer git.AcquireCloneLock(actionDir)()
|
||||
actionModel, err := sar.readAction(ctx, sar.Step, actionDir, sar.remoteAction.Path, remoteReader(ctx), os.WriteFile)
|
||||
sar.action = actionModel
|
||||
return err
|
||||
@@ -166,10 +166,6 @@ func (sar *stepActionRemote) main() common.Executor {
|
||||
return common.NewPipelineExecutor(
|
||||
sar.prepareActionExecutor(),
|
||||
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)
|
||||
if sar.remoteAction.IsCheckout() && isLocalCheckout(github, sar.Step) && !sar.RunContext.Config.NoSkipCheckout {
|
||||
if sar.RunContext.Config.BindWorkdir {
|
||||
|
||||
@@ -272,8 +272,8 @@ func TestStepActionRemotePre(t *testing.T) {
|
||||
|
||||
err := sar.pre()(ctx)
|
||||
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.True(t, clonedAction)
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Equal(t, true, clonedAction) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
sarm.AssertExpectations(t)
|
||||
})
|
||||
@@ -343,8 +343,8 @@ func TestStepActionRemotePreThroughAction(t *testing.T) {
|
||||
|
||||
err := sar.pre()(ctx)
|
||||
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.True(t, clonedAction)
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Equal(t, true, clonedAction) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
sarm.AssertExpectations(t)
|
||||
})
|
||||
@@ -419,7 +419,7 @@ func TestStepActionRemotePreThroughActionToken(t *testing.T) {
|
||||
|
||||
err := sar.pre()(ctx)
|
||||
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
// 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.Equal(t, "https://github.com/org/repo", actualURL, "URL should be redirected to github.com")
|
||||
|
||||
@@ -116,10 +116,6 @@ func (sd *stepDocker) newStepContainer(ctx context.Context, image string, cmd, e
|
||||
envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_TEMP", "/tmp"))
|
||||
|
||||
binds, mounts := rc.GetBindsAndMounts()
|
||||
networkMode := "container:" + rc.jobContainerName()
|
||||
if rc.IsHostEnv(ctx) {
|
||||
networkMode = "default"
|
||||
}
|
||||
stepContainer := ContainerNewContainer(&container.NewContainerInput{
|
||||
Cmd: cmd,
|
||||
Entrypoint: entrypoint,
|
||||
@@ -127,10 +123,10 @@ func (sd *stepDocker) newStepContainer(ctx context.Context, image string, cmd, e
|
||||
Image: image,
|
||||
Username: rc.Config.Secrets["DOCKER_USERNAME"],
|
||||
Password: rc.Config.Secrets["DOCKER_PASSWORD"],
|
||||
Name: createContainerName(rc.jobContainerName(), "STEP-"+step.ID),
|
||||
Name: createSimpleContainerName(rc.jobContainerName(), "STEP-"+step.ID),
|
||||
Env: envList,
|
||||
Mounts: mounts,
|
||||
NetworkMode: networkMode,
|
||||
NetworkMode: "container:" + rc.jobContainerName(),
|
||||
Binds: binds,
|
||||
Stdout: logWriter,
|
||||
Stderr: logWriter,
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.com/gitea/runner/act/container"
|
||||
@@ -102,7 +101,7 @@ func TestStepDockerMain(t *testing.T) {
|
||||
cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/pathcmd.txt").Return(io.NopCloser(&bytes.Buffer{}), nil)
|
||||
|
||||
err := sd.main()(ctx)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
assert.Equal(t, "node:14", input.Image)
|
||||
|
||||
@@ -114,86 +113,8 @@ func TestStepDockerPrePost(t *testing.T) {
|
||||
sd := &stepDocker{}
|
||||
|
||||
err := sd.pre()(ctx)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
err = sd.post()(ctx)
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ func TestStepFactoryNewStep(t *testing.T) {
|
||||
step, err := sf.newStep(tt.model, &RunContext{})
|
||||
|
||||
assert.True(t, tt.check((step)))
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"gitea.com/gitea/runner/act/common"
|
||||
@@ -18,18 +17,15 @@ import (
|
||||
"gitea.com/gitea/runner/act/model"
|
||||
|
||||
"github.com/kballard/go-shellquote"
|
||||
yaml "go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
type stepRun struct {
|
||||
Step *model.Step
|
||||
RunContext *RunContext
|
||||
cmd []string
|
||||
cmdline string
|
||||
env map[string]string
|
||||
WorkingDirectory string
|
||||
interpolatedScript string
|
||||
shellCommand string
|
||||
Step *model.Step
|
||||
RunContext *RunContext
|
||||
cmd []string
|
||||
cmdline string
|
||||
env map[string]string
|
||||
WorkingDirectory string
|
||||
}
|
||||
|
||||
func (sr *stepRun) pre() common.Executor {
|
||||
@@ -43,154 +39,15 @@ func (sr *stepRun) main() common.Executor {
|
||||
return runStepExecutor(sr, stepStageMain, common.NewPipelineExecutor(
|
||||
sr.setupShellCommandExecutor(),
|
||||
func(ctx context.Context) error {
|
||||
rc := sr.getRunContext()
|
||||
// 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 {
|
||||
sr.getRunContext().ApplyExtraPath(ctx, &sr.env)
|
||||
if he, ok := sr.getRunContext().JobContainer.(*container.HostEnvironment); ok && he != nil {
|
||||
return he.ExecWithCmdLine(sr.cmd, sr.cmdline, sr.env, "", sr.WorkingDirectory)(ctx)
|
||||
}
|
||||
return rc.JobContainer.Exec(sr.cmd, sr.env, "", sr.WorkingDirectory)(ctx)
|
||||
return sr.getRunContext().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 {
|
||||
return func(ctx context.Context) error {
|
||||
return nil
|
||||
@@ -254,10 +111,8 @@ func (sr *stepRun) setupShellCommand(ctx context.Context) (name, script string,
|
||||
step := sr.Step
|
||||
|
||||
script = sr.RunContext.NewStepExpressionEvaluator(ctx, sr).Interpolate(ctx, step.Run)
|
||||
sr.interpolatedScript = script
|
||||
|
||||
scCmd := step.ShellCommand()
|
||||
sr.shellCommand = scCmd
|
||||
|
||||
name = getScriptName(sr.RunContext, step)
|
||||
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
// 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())
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
err := sr.main()(ctx)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
cm.AssertExpectations(t)
|
||||
}
|
||||
@@ -91,8 +91,8 @@ func TestStepRunPrePost(t *testing.T) {
|
||||
sr := &stepRun{}
|
||||
|
||||
err := sr.pre()(ctx)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
err = sr.post()(ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ func TestSetupEnv(t *testing.T) {
|
||||
sm.On("getEnv").Return(&env)
|
||||
|
||||
err := setupEnv(context.Background(), sm)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
// These are commit or system specific
|
||||
delete((env), "GITHUB_REF")
|
||||
@@ -318,35 +318,35 @@ func TestIsContinueOnError(t *testing.T) {
|
||||
step := createTestStep(t, "name: test")
|
||||
continueOnError, err := isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain)
|
||||
assertObject.False(continueOnError)
|
||||
assertObject.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assertObject.Nil(err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
// explcit true
|
||||
step = createTestStep(t, "continue-on-error: true")
|
||||
continueOnError, err = isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain)
|
||||
assertObject.True(continueOnError)
|
||||
assertObject.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assertObject.Nil(err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
// explicit false
|
||||
step = createTestStep(t, "continue-on-error: false")
|
||||
continueOnError, err = isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain)
|
||||
assertObject.False(continueOnError)
|
||||
assertObject.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assertObject.Nil(err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
// expression true
|
||||
step = createTestStep(t, "continue-on-error: ${{ 'test' == 'test' }}")
|
||||
continueOnError, err = isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain)
|
||||
assertObject.True(continueOnError)
|
||||
assertObject.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assertObject.Nil(err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
// expression false
|
||||
step = createTestStep(t, "continue-on-error: ${{ 'test' != 'test' }}")
|
||||
continueOnError, err = isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain)
|
||||
assertObject.False(continueOnError)
|
||||
assertObject.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assertObject.Nil(err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
// expression parse error
|
||||
step = createTestStep(t, "continue-on-error: ${{ 'test' != test }}")
|
||||
continueOnError, err = isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain)
|
||||
assertObject.False(continueOnError)
|
||||
assertObject.Error(err)
|
||||
assertObject.NotNil(err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: 'Test'
|
||||
description: 'Test'
|
||||
runs:
|
||||
using: 'node24'
|
||||
using: 'node12'
|
||||
main: 'index.js'
|
||||
|
||||
@@ -1 +1 @@
|
||||
FROM ubuntu:24.04
|
||||
FROM ubuntu:18.04
|
||||
@@ -1,5 +1,5 @@
|
||||
# Container image that runs your code
|
||||
FROM node:24-bookworm-slim
|
||||
FROM node:12-buster-slim
|
||||
|
||||
# Copies your code file from your action repository to the filesystem path `/` of the container
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Container image that runs your code
|
||||
FROM node:24-bookworm-slim
|
||||
FROM node:16-buster-slim
|
||||
|
||||
# Copies your code file from your action repository to the filesystem path `/` of the container
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
|
||||
@@ -8,7 +8,7 @@ inputs:
|
||||
default: World
|
||||
runs:
|
||||
using: docker
|
||||
image: docker://node:24-bookworm-slim
|
||||
image: docker://node:16-buster-slim
|
||||
entrypoint: /bin/sh -c
|
||||
env:
|
||||
TEST: enabled
|
||||
|
||||
13
act/runner/testdata/actions/node12/action.yml
vendored
Normal file
13
act/runner/testdata/actions/node12/action.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
name: 'Hello World'
|
||||
description: 'Greet someone and record the time'
|
||||
inputs:
|
||||
who-to-greet: # id of input
|
||||
description: 'Who to greet'
|
||||
required: true
|
||||
default: 'World'
|
||||
outputs:
|
||||
time: # id of output
|
||||
description: 'The time we greeted you'
|
||||
runs:
|
||||
using: 'node12'
|
||||
main: 'dist/index.js'
|
||||
15
act/runner/testdata/actions/node12/index.js
vendored
Normal file
15
act/runner/testdata/actions/node12/index.js
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
const core = require('@actions/core');
|
||||
const github = require('@actions/github');
|
||||
|
||||
try {
|
||||
// `who-to-greet` input defined in action metadata file
|
||||
const nameToGreet = core.getInput('who-to-greet');
|
||||
console.log(`Hello ${nameToGreet}!`);
|
||||
const time = (new Date()).toTimeString();
|
||||
core.setOutput("time", time);
|
||||
// Get the JSON webhook payload for the event that triggered the workflow
|
||||
const payload = JSON.stringify(github.context.payload, undefined, 2)
|
||||
console.log(`The event payload: ${payload}`);
|
||||
} catch (error) {
|
||||
core.setFailed(error.message);
|
||||
}
|
||||
1
act/runner/testdata/actions/node12/node_modules/.bin/ncc
generated
vendored
Symbolic link
1
act/runner/testdata/actions/node12/node_modules/.bin/ncc
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../@vercel/ncc/dist/ncc/cli.js
|
||||
1
act/runner/testdata/actions/node12/node_modules/.bin/uuid
generated
vendored
Symbolic link
1
act/runner/testdata/actions/node12/node_modules/.bin/uuid
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../uuid/dist/bin/uuid
|
||||
244
act/runner/testdata/actions/node12/node_modules/.package-lock.json
generated
vendored
Normal file
244
act/runner/testdata/actions/node12/node_modules/.package-lock.json
generated
vendored
Normal file
@@ -0,0 +1,244 @@
|
||||
{
|
||||
"name": "node12",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"node_modules/@actions/core": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.0.tgz",
|
||||
"integrity": "sha512-2aZDDa3zrrZbP5ZYg159sNoLRb61nQ7awl5pSvIq5Qpj81vwDzdMRKzkWJGJuwVvWpvZKx7vspJALyvaaIQyug==",
|
||||
"dependencies": {
|
||||
"@actions/http-client": "^2.0.1",
|
||||
"uuid": "^8.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@actions/github": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@actions/github/-/github-4.0.0.tgz",
|
||||
"integrity": "sha512-Ej/Y2E+VV6sR9X7pWL5F3VgEWrABaT292DRqRU6R4hnQjPtC/zD3nagxVdXWiRQvYDh8kHXo7IDmG42eJ/dOMA==",
|
||||
"dependencies": {
|
||||
"@actions/http-client": "^1.0.8",
|
||||
"@octokit/core": "^3.0.0",
|
||||
"@octokit/plugin-paginate-rest": "^2.2.3",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@actions/github/node_modules/@actions/http-client": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz",
|
||||
"integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==",
|
||||
"dependencies": {
|
||||
"tunnel": "0.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@actions/http-client": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.1.1.tgz",
|
||||
"integrity": "sha512-qhrkRMB40bbbLo7gF+0vu+X+UawOvQQqNAA/5Unx774RS8poaOhThDOG6BGmxvAnxhQnDp2BG/ZUm65xZILTpw==",
|
||||
"dependencies": {
|
||||
"tunnel": "^0.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/auth-token": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz",
|
||||
"integrity": "sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^6.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/core": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.6.0.tgz",
|
||||
"integrity": "sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q==",
|
||||
"dependencies": {
|
||||
"@octokit/auth-token": "^2.4.4",
|
||||
"@octokit/graphql": "^4.5.8",
|
||||
"@octokit/request": "^5.6.3",
|
||||
"@octokit/request-error": "^2.0.5",
|
||||
"@octokit/types": "^6.0.3",
|
||||
"before-after-hook": "^2.2.0",
|
||||
"universal-user-agent": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/endpoint": {
|
||||
"version": "6.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz",
|
||||
"integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^6.0.3",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"universal-user-agent": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/graphql": {
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz",
|
||||
"integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==",
|
||||
"dependencies": {
|
||||
"@octokit/request": "^5.6.0",
|
||||
"@octokit/types": "^6.0.3",
|
||||
"universal-user-agent": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/openapi-types": {
|
||||
"version": "12.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz",
|
||||
"integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ=="
|
||||
},
|
||||
"node_modules/@octokit/plugin-paginate-rest": {
|
||||
"version": "2.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.21.3.tgz",
|
||||
"integrity": "sha512-aCZTEf0y2h3OLbrgKkrfFdjRL6eSOo8komneVQJnYecAxIej7Bafor2xhuDJOIFau4pk0i/P28/XgtbyPF0ZHw==",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^6.40.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@octokit/core": ">=2"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/plugin-rest-endpoint-methods": {
|
||||
"version": "4.15.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.15.1.tgz",
|
||||
"integrity": "sha512-4gQg4ySoW7ktKB0Mf38fHzcSffVZd6mT5deJQtpqkuPuAqzlED5AJTeW8Uk7dPRn7KaOlWcXB0MedTFJU1j4qA==",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^6.13.0",
|
||||
"deprecation": "^2.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@octokit/core": ">=3"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/request": {
|
||||
"version": "5.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.3.tgz",
|
||||
"integrity": "sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==",
|
||||
"dependencies": {
|
||||
"@octokit/endpoint": "^6.0.1",
|
||||
"@octokit/request-error": "^2.1.0",
|
||||
"@octokit/types": "^6.16.1",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"node-fetch": "^2.6.7",
|
||||
"universal-user-agent": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/request-error": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz",
|
||||
"integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^6.0.3",
|
||||
"deprecation": "^2.0.0",
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/types": {
|
||||
"version": "6.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz",
|
||||
"integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==",
|
||||
"dependencies": {
|
||||
"@octokit/openapi-types": "^12.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vercel/ncc": {
|
||||
"version": "0.24.1",
|
||||
"resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.24.1.tgz",
|
||||
"integrity": "sha512-r9m7brz2hNmq5TF3sxrK4qR/FhXn44XIMglQUir4sT7Sh5GOaYXlMYikHFwJStf8rmQGTlvOoBXt4yHVonRG8A==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"ncc": "dist/ncc/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/before-after-hook": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
|
||||
"integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="
|
||||
},
|
||||
"node_modules/deprecation": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
|
||||
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="
|
||||
},
|
||||
"node_modules/is-plain-object": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
|
||||
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.6.12",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz",
|
||||
"integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
|
||||
},
|
||||
"node_modules/tunnel": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
|
||||
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
|
||||
"engines": {
|
||||
"node": ">=0.6.11 <=0.7.0 || >=0.7.3"
|
||||
}
|
||||
},
|
||||
"node_modules/universal-user-agent": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
|
||||
"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": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
||||
}
|
||||
}
|
||||
}
|
||||
9
act/runner/testdata/actions/node12/node_modules/@actions/core/LICENSE.md
generated
vendored
Normal file
9
act/runner/testdata/actions/node12/node_modules/@actions/core/LICENSE.md
generated
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright 2019 GitHub
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
15
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/command.d.ts
generated
vendored
Normal file
15
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/command.d.ts
generated
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
export interface CommandProperties {
|
||||
[key: string]: any;
|
||||
}
|
||||
/**
|
||||
* Commands
|
||||
*
|
||||
* Command Format:
|
||||
* ::name key=value,key=value::message
|
||||
*
|
||||
* Examples:
|
||||
* ::warning::This is the message
|
||||
* ::set-env name=MY_VAR::some value
|
||||
*/
|
||||
export declare function issueCommand(command: string, properties: CommandProperties, message: any): void;
|
||||
export declare function issue(name: string, message?: string): void;
|
||||
92
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/command.js
generated
vendored
Normal file
92
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/command.js
generated
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.issue = exports.issueCommand = void 0;
|
||||
const os = __importStar(require("os"));
|
||||
const utils_1 = require("./utils");
|
||||
/**
|
||||
* Commands
|
||||
*
|
||||
* Command Format:
|
||||
* ::name key=value,key=value::message
|
||||
*
|
||||
* Examples:
|
||||
* ::warning::This is the message
|
||||
* ::set-env name=MY_VAR::some value
|
||||
*/
|
||||
function issueCommand(command, properties, message) {
|
||||
const cmd = new Command(command, properties, message);
|
||||
process.stdout.write(cmd.toString() + os.EOL);
|
||||
}
|
||||
exports.issueCommand = issueCommand;
|
||||
function issue(name, message = '') {
|
||||
issueCommand(name, {}, message);
|
||||
}
|
||||
exports.issue = issue;
|
||||
const CMD_STRING = '::';
|
||||
class Command {
|
||||
constructor(command, properties, message) {
|
||||
if (!command) {
|
||||
command = 'missing.command';
|
||||
}
|
||||
this.command = command;
|
||||
this.properties = properties;
|
||||
this.message = message;
|
||||
}
|
||||
toString() {
|
||||
let cmdStr = CMD_STRING + this.command;
|
||||
if (this.properties && Object.keys(this.properties).length > 0) {
|
||||
cmdStr += ' ';
|
||||
let first = true;
|
||||
for (const key in this.properties) {
|
||||
if (this.properties.hasOwnProperty(key)) {
|
||||
const val = this.properties[key];
|
||||
if (val) {
|
||||
if (first) {
|
||||
first = false;
|
||||
}
|
||||
else {
|
||||
cmdStr += ',';
|
||||
}
|
||||
cmdStr += `${key}=${escapeProperty(val)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
cmdStr += `${CMD_STRING}${escapeData(this.message)}`;
|
||||
return cmdStr;
|
||||
}
|
||||
}
|
||||
function escapeData(s) {
|
||||
return utils_1.toCommandValue(s)
|
||||
.replace(/%/g, '%25')
|
||||
.replace(/\r/g, '%0D')
|
||||
.replace(/\n/g, '%0A');
|
||||
}
|
||||
function escapeProperty(s) {
|
||||
return utils_1.toCommandValue(s)
|
||||
.replace(/%/g, '%25')
|
||||
.replace(/\r/g, '%0D')
|
||||
.replace(/\n/g, '%0A')
|
||||
.replace(/:/g, '%3A')
|
||||
.replace(/,/g, '%2C');
|
||||
}
|
||||
//# sourceMappingURL=command.js.map
|
||||
1
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/command.js.map
generated
vendored
Normal file
1
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/command.js.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"command.js","sourceRoot":"","sources":["../src/command.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAAA,uCAAwB;AACxB,mCAAsC;AAWtC;;;;;;;;;GASG;AACH,SAAgB,YAAY,CAC1B,OAAe,EACf,UAA6B,EAC7B,OAAY;IAEZ,MAAM,GAAG,GAAG,IAAI,OAAO,CAAC,OAAO,EAAE,UAAU,EAAE,OAAO,CAAC,CAAA;IACrD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAA;AAC/C,CAAC;AAPD,oCAOC;AAED,SAAgB,KAAK,CAAC,IAAY,EAAE,OAAO,GAAG,EAAE;IAC9C,YAAY,CAAC,IAAI,EAAE,EAAE,EAAE,OAAO,CAAC,CAAA;AACjC,CAAC;AAFD,sBAEC;AAED,MAAM,UAAU,GAAG,IAAI,CAAA;AAEvB,MAAM,OAAO;IAKX,YAAY,OAAe,EAAE,UAA6B,EAAE,OAAe;QACzE,IAAI,CAAC,OAAO,EAAE;YACZ,OAAO,GAAG,iBAAiB,CAAA;SAC5B;QAED,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;QACtB,IAAI,CAAC,UAAU,GAAG,UAAU,CAAA;QAC5B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;IACxB,CAAC;IAED,QAAQ;QACN,IAAI,MAAM,GAAG,UAAU,GAAG,IAAI,CAAC,OAAO,CAAA;QAEtC,IAAI,IAAI,CAAC,UAAU,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE;YAC9D,MAAM,IAAI,GAAG,CAAA;YACb,IAAI,KAAK,GAAG,IAAI,CAAA;YAChB,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,UAAU,EAAE;gBACjC,IAAI,IAAI,CAAC,UAAU,CAAC,cAAc,CAAC,GAAG,CAAC,EAAE;oBACvC,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAA;oBAChC,IAAI,GAAG,EAAE;wBACP,IAAI,KAAK,EAAE;4BACT,KAAK,GAAG,KAAK,CAAA;yBACd;6BAAM;4BACL,MAAM,IAAI,GAAG,CAAA;yBACd;wBAED,MAAM,IAAI,GAAG,GAAG,IAAI,cAAc,CAAC,GAAG,CAAC,EAAE,CAAA;qBAC1C;iBACF;aACF;SACF;QAED,MAAM,IAAI,GAAG,UAAU,GAAG,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAA;QACpD,OAAO,MAAM,CAAA;IACf,CAAC;CACF;AAED,SAAS,UAAU,CAAC,CAAM;IACxB,OAAO,sBAAc,CAAC,CAAC,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC;SACpB,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC;SACrB,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;AAC1B,CAAC;AAED,SAAS,cAAc,CAAC,CAAM;IAC5B,OAAO,sBAAc,CAAC,CAAC,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC;SACpB,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC;SACrB,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC;SACpB,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;AACzB,CAAC"}
|
||||
198
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/core.d.ts
generated
vendored
Normal file
198
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/core.d.ts
generated
vendored
Normal file
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* Interface for getInput options
|
||||
*/
|
||||
export interface InputOptions {
|
||||
/** Optional. Whether the input is required. If required and not present, will throw. Defaults to false */
|
||||
required?: boolean;
|
||||
/** Optional. Whether leading/trailing whitespace will be trimmed for the input. Defaults to true */
|
||||
trimWhitespace?: boolean;
|
||||
}
|
||||
/**
|
||||
* The code to exit an action
|
||||
*/
|
||||
export declare enum ExitCode {
|
||||
/**
|
||||
* A code indicating that the action was successful
|
||||
*/
|
||||
Success = 0,
|
||||
/**
|
||||
* A code indicating that the action was a failure
|
||||
*/
|
||||
Failure = 1
|
||||
}
|
||||
/**
|
||||
* Optional properties that can be sent with annotatation commands (notice, error, and warning)
|
||||
* See: https://docs.github.com/en/rest/reference/checks#create-a-check-run for more information about annotations.
|
||||
*/
|
||||
export interface AnnotationProperties {
|
||||
/**
|
||||
* A title for the annotation.
|
||||
*/
|
||||
title?: string;
|
||||
/**
|
||||
* The path of the file for which the annotation should be created.
|
||||
*/
|
||||
file?: string;
|
||||
/**
|
||||
* The start line for the annotation.
|
||||
*/
|
||||
startLine?: number;
|
||||
/**
|
||||
* The end line for the annotation. Defaults to `startLine` when `startLine` is provided.
|
||||
*/
|
||||
endLine?: number;
|
||||
/**
|
||||
* The start column for the annotation. Cannot be sent when `startLine` and `endLine` are different values.
|
||||
*/
|
||||
startColumn?: number;
|
||||
/**
|
||||
* The start column for the annotation. Cannot be sent when `startLine` and `endLine` are different values.
|
||||
* Defaults to `startColumn` when `startColumn` is provided.
|
||||
*/
|
||||
endColumn?: number;
|
||||
}
|
||||
/**
|
||||
* Sets env variable for this action and future actions in the job
|
||||
* @param name the name of the variable to set
|
||||
* @param val the value of the variable. Non-string values will be converted to a string via JSON.stringify
|
||||
*/
|
||||
export declare function exportVariable(name: string, val: any): void;
|
||||
/**
|
||||
* Registers a secret which will get masked from logs
|
||||
* @param secret value of the secret
|
||||
*/
|
||||
export declare function setSecret(secret: string): void;
|
||||
/**
|
||||
* Prepends inputPath to the PATH (for this action and future actions)
|
||||
* @param inputPath
|
||||
*/
|
||||
export declare function addPath(inputPath: string): void;
|
||||
/**
|
||||
* Gets the value of an input.
|
||||
* Unless trimWhitespace is set to false in InputOptions, the value is also trimmed.
|
||||
* Returns an empty string if the value is not defined.
|
||||
*
|
||||
* @param name name of the input to get
|
||||
* @param options optional. See InputOptions.
|
||||
* @returns string
|
||||
*/
|
||||
export declare function getInput(name: string, options?: InputOptions): string;
|
||||
/**
|
||||
* Gets the values of an multiline input. Each value is also trimmed.
|
||||
*
|
||||
* @param name name of the input to get
|
||||
* @param options optional. See InputOptions.
|
||||
* @returns string[]
|
||||
*
|
||||
*/
|
||||
export declare function getMultilineInput(name: string, options?: InputOptions): string[];
|
||||
/**
|
||||
* Gets the input value of the boolean type in the YAML 1.2 "core schema" specification.
|
||||
* Support boolean input list: `true | True | TRUE | false | False | FALSE` .
|
||||
* The return value is also in boolean type.
|
||||
* ref: https://yaml.org/spec/1.2/spec.html#id2804923
|
||||
*
|
||||
* @param name name of the input to get
|
||||
* @param options optional. See InputOptions.
|
||||
* @returns boolean
|
||||
*/
|
||||
export declare function getBooleanInput(name: string, options?: InputOptions): boolean;
|
||||
/**
|
||||
* Sets the value of an output.
|
||||
*
|
||||
* @param name name of the output to set
|
||||
* @param value value to store. Non-string values will be converted to a string via JSON.stringify
|
||||
*/
|
||||
export declare function setOutput(name: string, value: any): void;
|
||||
/**
|
||||
* Enables or disables the echoing of commands into stdout for the rest of the step.
|
||||
* Echoing is disabled by default if ACTIONS_STEP_DEBUG is not set.
|
||||
*
|
||||
*/
|
||||
export declare function setCommandEcho(enabled: boolean): void;
|
||||
/**
|
||||
* Sets the action status to failed.
|
||||
* When the action exits it will be with an exit code of 1
|
||||
* @param message add error issue message
|
||||
*/
|
||||
export declare function setFailed(message: string | Error): void;
|
||||
/**
|
||||
* Gets whether Actions Step Debug is on or not
|
||||
*/
|
||||
export declare function isDebug(): boolean;
|
||||
/**
|
||||
* Writes debug message to user log
|
||||
* @param message debug message
|
||||
*/
|
||||
export declare function debug(message: string): void;
|
||||
/**
|
||||
* Adds an error issue
|
||||
* @param message error issue message. Errors will be converted to string via toString()
|
||||
* @param properties optional properties to add to the annotation.
|
||||
*/
|
||||
export declare function error(message: string | Error, properties?: AnnotationProperties): void;
|
||||
/**
|
||||
* Adds a warning issue
|
||||
* @param message warning issue message. Errors will be converted to string via toString()
|
||||
* @param properties optional properties to add to the annotation.
|
||||
*/
|
||||
export declare function warning(message: string | Error, properties?: AnnotationProperties): void;
|
||||
/**
|
||||
* Adds a notice issue
|
||||
* @param message notice issue message. Errors will be converted to string via toString()
|
||||
* @param properties optional properties to add to the annotation.
|
||||
*/
|
||||
export declare function notice(message: string | Error, properties?: AnnotationProperties): void;
|
||||
/**
|
||||
* Writes info to log with console.log.
|
||||
* @param message info message
|
||||
*/
|
||||
export declare function info(message: string): void;
|
||||
/**
|
||||
* Begin an output group.
|
||||
*
|
||||
* Output until the next `groupEnd` will be foldable in this group
|
||||
*
|
||||
* @param name The name of the output group
|
||||
*/
|
||||
export declare function startGroup(name: string): void;
|
||||
/**
|
||||
* End an output group.
|
||||
*/
|
||||
export declare function endGroup(): void;
|
||||
/**
|
||||
* Wrap an asynchronous function call in a group.
|
||||
*
|
||||
* Returns the same type as the function itself.
|
||||
*
|
||||
* @param name The name of the group
|
||||
* @param fn The function to wrap in the group
|
||||
*/
|
||||
export declare function group<T>(name: string, fn: () => Promise<T>): Promise<T>;
|
||||
/**
|
||||
* Saves state for current action, the state can only be retrieved by this action's post job execution.
|
||||
*
|
||||
* @param name name of the state to store
|
||||
* @param value value to store. Non-string values will be converted to a string via JSON.stringify
|
||||
*/
|
||||
export declare function saveState(name: string, value: any): void;
|
||||
/**
|
||||
* Gets the value of an state set by this action's main execution.
|
||||
*
|
||||
* @param name name of the state to get
|
||||
* @returns string
|
||||
*/
|
||||
export declare function getState(name: string): string;
|
||||
export declare function getIDToken(aud?: string): Promise<string>;
|
||||
/**
|
||||
* Summary exports
|
||||
*/
|
||||
export { summary } from './summary';
|
||||
/**
|
||||
* @deprecated use core.summary
|
||||
*/
|
||||
export { markdownSummary } from './summary';
|
||||
/**
|
||||
* Path exports
|
||||
*/
|
||||
export { toPosixPath, toWin32Path, toPlatformPath } from './path-utils';
|
||||
336
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/core.js
generated
vendored
Normal file
336
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/core.js
generated
vendored
Normal file
@@ -0,0 +1,336 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getIDToken = exports.getState = exports.saveState = exports.group = exports.endGroup = exports.startGroup = exports.info = exports.notice = exports.warning = exports.error = exports.debug = exports.isDebug = exports.setFailed = exports.setCommandEcho = exports.setOutput = exports.getBooleanInput = exports.getMultilineInput = exports.getInput = exports.addPath = exports.setSecret = exports.exportVariable = exports.ExitCode = void 0;
|
||||
const command_1 = require("./command");
|
||||
const file_command_1 = require("./file-command");
|
||||
const utils_1 = require("./utils");
|
||||
const os = __importStar(require("os"));
|
||||
const path = __importStar(require("path"));
|
||||
const oidc_utils_1 = require("./oidc-utils");
|
||||
/**
|
||||
* The code to exit an action
|
||||
*/
|
||||
var ExitCode;
|
||||
(function (ExitCode) {
|
||||
/**
|
||||
* A code indicating that the action was successful
|
||||
*/
|
||||
ExitCode[ExitCode["Success"] = 0] = "Success";
|
||||
/**
|
||||
* A code indicating that the action was a failure
|
||||
*/
|
||||
ExitCode[ExitCode["Failure"] = 1] = "Failure";
|
||||
})(ExitCode = exports.ExitCode || (exports.ExitCode = {}));
|
||||
//-----------------------------------------------------------------------
|
||||
// Variables
|
||||
//-----------------------------------------------------------------------
|
||||
/**
|
||||
* Sets env variable for this action and future actions in the job
|
||||
* @param name the name of the variable to set
|
||||
* @param val the value of the variable. Non-string values will be converted to a string via JSON.stringify
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function exportVariable(name, val) {
|
||||
const convertedVal = utils_1.toCommandValue(val);
|
||||
process.env[name] = convertedVal;
|
||||
const filePath = process.env['GITHUB_ENV'] || '';
|
||||
if (filePath) {
|
||||
return file_command_1.issueFileCommand('ENV', file_command_1.prepareKeyValueMessage(name, val));
|
||||
}
|
||||
command_1.issueCommand('set-env', { name }, convertedVal);
|
||||
}
|
||||
exports.exportVariable = exportVariable;
|
||||
/**
|
||||
* Registers a secret which will get masked from logs
|
||||
* @param secret value of the secret
|
||||
*/
|
||||
function setSecret(secret) {
|
||||
command_1.issueCommand('add-mask', {}, secret);
|
||||
}
|
||||
exports.setSecret = setSecret;
|
||||
/**
|
||||
* Prepends inputPath to the PATH (for this action and future actions)
|
||||
* @param inputPath
|
||||
*/
|
||||
function addPath(inputPath) {
|
||||
const filePath = process.env['GITHUB_PATH'] || '';
|
||||
if (filePath) {
|
||||
file_command_1.issueFileCommand('PATH', inputPath);
|
||||
}
|
||||
else {
|
||||
command_1.issueCommand('add-path', {}, inputPath);
|
||||
}
|
||||
process.env['PATH'] = `${inputPath}${path.delimiter}${process.env['PATH']}`;
|
||||
}
|
||||
exports.addPath = addPath;
|
||||
/**
|
||||
* Gets the value of an input.
|
||||
* Unless trimWhitespace is set to false in InputOptions, the value is also trimmed.
|
||||
* Returns an empty string if the value is not defined.
|
||||
*
|
||||
* @param name name of the input to get
|
||||
* @param options optional. See InputOptions.
|
||||
* @returns string
|
||||
*/
|
||||
function getInput(name, options) {
|
||||
const val = process.env[`INPUT_${name.replace(/ /g, '_').toUpperCase()}`] || '';
|
||||
if (options && options.required && !val) {
|
||||
throw new Error(`Input required and not supplied: ${name}`);
|
||||
}
|
||||
if (options && options.trimWhitespace === false) {
|
||||
return val;
|
||||
}
|
||||
return val.trim();
|
||||
}
|
||||
exports.getInput = getInput;
|
||||
/**
|
||||
* Gets the values of an multiline input. Each value is also trimmed.
|
||||
*
|
||||
* @param name name of the input to get
|
||||
* @param options optional. See InputOptions.
|
||||
* @returns string[]
|
||||
*
|
||||
*/
|
||||
function getMultilineInput(name, options) {
|
||||
const inputs = getInput(name, options)
|
||||
.split('\n')
|
||||
.filter(x => x !== '');
|
||||
if (options && options.trimWhitespace === false) {
|
||||
return inputs;
|
||||
}
|
||||
return inputs.map(input => input.trim());
|
||||
}
|
||||
exports.getMultilineInput = getMultilineInput;
|
||||
/**
|
||||
* Gets the input value of the boolean type in the YAML 1.2 "core schema" specification.
|
||||
* Support boolean input list: `true | True | TRUE | false | False | FALSE` .
|
||||
* The return value is also in boolean type.
|
||||
* ref: https://yaml.org/spec/1.2/spec.html#id2804923
|
||||
*
|
||||
* @param name name of the input to get
|
||||
* @param options optional. See InputOptions.
|
||||
* @returns boolean
|
||||
*/
|
||||
function getBooleanInput(name, options) {
|
||||
const trueValue = ['true', 'True', 'TRUE'];
|
||||
const falseValue = ['false', 'False', 'FALSE'];
|
||||
const val = getInput(name, options);
|
||||
if (trueValue.includes(val))
|
||||
return true;
|
||||
if (falseValue.includes(val))
|
||||
return false;
|
||||
throw new TypeError(`Input does not meet YAML 1.2 "Core Schema" specification: ${name}\n` +
|
||||
`Support boolean input list: \`true | True | TRUE | false | False | FALSE\``);
|
||||
}
|
||||
exports.getBooleanInput = getBooleanInput;
|
||||
/**
|
||||
* Sets the value of an output.
|
||||
*
|
||||
* @param name name of the output to set
|
||||
* @param value value to store. Non-string values will be converted to a string via JSON.stringify
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function setOutput(name, value) {
|
||||
const filePath = process.env['GITHUB_OUTPUT'] || '';
|
||||
if (filePath) {
|
||||
return file_command_1.issueFileCommand('OUTPUT', file_command_1.prepareKeyValueMessage(name, value));
|
||||
}
|
||||
process.stdout.write(os.EOL);
|
||||
command_1.issueCommand('set-output', { name }, utils_1.toCommandValue(value));
|
||||
}
|
||||
exports.setOutput = setOutput;
|
||||
/**
|
||||
* Enables or disables the echoing of commands into stdout for the rest of the step.
|
||||
* Echoing is disabled by default if ACTIONS_STEP_DEBUG is not set.
|
||||
*
|
||||
*/
|
||||
function setCommandEcho(enabled) {
|
||||
command_1.issue('echo', enabled ? 'on' : 'off');
|
||||
}
|
||||
exports.setCommandEcho = setCommandEcho;
|
||||
//-----------------------------------------------------------------------
|
||||
// Results
|
||||
//-----------------------------------------------------------------------
|
||||
/**
|
||||
* Sets the action status to failed.
|
||||
* When the action exits it will be with an exit code of 1
|
||||
* @param message add error issue message
|
||||
*/
|
||||
function setFailed(message) {
|
||||
process.exitCode = ExitCode.Failure;
|
||||
error(message);
|
||||
}
|
||||
exports.setFailed = setFailed;
|
||||
//-----------------------------------------------------------------------
|
||||
// Logging Commands
|
||||
//-----------------------------------------------------------------------
|
||||
/**
|
||||
* Gets whether Actions Step Debug is on or not
|
||||
*/
|
||||
function isDebug() {
|
||||
return process.env['RUNNER_DEBUG'] === '1';
|
||||
}
|
||||
exports.isDebug = isDebug;
|
||||
/**
|
||||
* Writes debug message to user log
|
||||
* @param message debug message
|
||||
*/
|
||||
function debug(message) {
|
||||
command_1.issueCommand('debug', {}, message);
|
||||
}
|
||||
exports.debug = debug;
|
||||
/**
|
||||
* Adds an error issue
|
||||
* @param message error issue message. Errors will be converted to string via toString()
|
||||
* @param properties optional properties to add to the annotation.
|
||||
*/
|
||||
function error(message, properties = {}) {
|
||||
command_1.issueCommand('error', utils_1.toCommandProperties(properties), message instanceof Error ? message.toString() : message);
|
||||
}
|
||||
exports.error = error;
|
||||
/**
|
||||
* Adds a warning issue
|
||||
* @param message warning issue message. Errors will be converted to string via toString()
|
||||
* @param properties optional properties to add to the annotation.
|
||||
*/
|
||||
function warning(message, properties = {}) {
|
||||
command_1.issueCommand('warning', utils_1.toCommandProperties(properties), message instanceof Error ? message.toString() : message);
|
||||
}
|
||||
exports.warning = warning;
|
||||
/**
|
||||
* Adds a notice issue
|
||||
* @param message notice issue message. Errors will be converted to string via toString()
|
||||
* @param properties optional properties to add to the annotation.
|
||||
*/
|
||||
function notice(message, properties = {}) {
|
||||
command_1.issueCommand('notice', utils_1.toCommandProperties(properties), message instanceof Error ? message.toString() : message);
|
||||
}
|
||||
exports.notice = notice;
|
||||
/**
|
||||
* Writes info to log with console.log.
|
||||
* @param message info message
|
||||
*/
|
||||
function info(message) {
|
||||
process.stdout.write(message + os.EOL);
|
||||
}
|
||||
exports.info = info;
|
||||
/**
|
||||
* Begin an output group.
|
||||
*
|
||||
* Output until the next `groupEnd` will be foldable in this group
|
||||
*
|
||||
* @param name The name of the output group
|
||||
*/
|
||||
function startGroup(name) {
|
||||
command_1.issue('group', name);
|
||||
}
|
||||
exports.startGroup = startGroup;
|
||||
/**
|
||||
* End an output group.
|
||||
*/
|
||||
function endGroup() {
|
||||
command_1.issue('endgroup');
|
||||
}
|
||||
exports.endGroup = endGroup;
|
||||
/**
|
||||
* Wrap an asynchronous function call in a group.
|
||||
*
|
||||
* Returns the same type as the function itself.
|
||||
*
|
||||
* @param name The name of the group
|
||||
* @param fn The function to wrap in the group
|
||||
*/
|
||||
function group(name, fn) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
startGroup(name);
|
||||
let result;
|
||||
try {
|
||||
result = yield fn();
|
||||
}
|
||||
finally {
|
||||
endGroup();
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
exports.group = group;
|
||||
//-----------------------------------------------------------------------
|
||||
// Wrapper action state
|
||||
//-----------------------------------------------------------------------
|
||||
/**
|
||||
* Saves state for current action, the state can only be retrieved by this action's post job execution.
|
||||
*
|
||||
* @param name name of the state to store
|
||||
* @param value value to store. Non-string values will be converted to a string via JSON.stringify
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function saveState(name, value) {
|
||||
const filePath = process.env['GITHUB_STATE'] || '';
|
||||
if (filePath) {
|
||||
return file_command_1.issueFileCommand('STATE', file_command_1.prepareKeyValueMessage(name, value));
|
||||
}
|
||||
command_1.issueCommand('save-state', { name }, utils_1.toCommandValue(value));
|
||||
}
|
||||
exports.saveState = saveState;
|
||||
/**
|
||||
* Gets the value of an state set by this action's main execution.
|
||||
*
|
||||
* @param name name of the state to get
|
||||
* @returns string
|
||||
*/
|
||||
function getState(name) {
|
||||
return process.env[`STATE_${name}`] || '';
|
||||
}
|
||||
exports.getState = getState;
|
||||
function getIDToken(aud) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
return yield oidc_utils_1.OidcClient.getIDToken(aud);
|
||||
});
|
||||
}
|
||||
exports.getIDToken = getIDToken;
|
||||
/**
|
||||
* Summary exports
|
||||
*/
|
||||
var summary_1 = require("./summary");
|
||||
Object.defineProperty(exports, "summary", { enumerable: true, get: function () { return summary_1.summary; } });
|
||||
/**
|
||||
* @deprecated use core.summary
|
||||
*/
|
||||
var summary_2 = require("./summary");
|
||||
Object.defineProperty(exports, "markdownSummary", { enumerable: true, get: function () { return summary_2.markdownSummary; } });
|
||||
/**
|
||||
* Path exports
|
||||
*/
|
||||
var path_utils_1 = require("./path-utils");
|
||||
Object.defineProperty(exports, "toPosixPath", { enumerable: true, get: function () { return path_utils_1.toPosixPath; } });
|
||||
Object.defineProperty(exports, "toWin32Path", { enumerable: true, get: function () { return path_utils_1.toWin32Path; } });
|
||||
Object.defineProperty(exports, "toPlatformPath", { enumerable: true, get: function () { return path_utils_1.toPlatformPath; } });
|
||||
//# sourceMappingURL=core.js.map
|
||||
1
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/core.js.map
generated
vendored
Normal file
1
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/core.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
2
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/file-command.d.ts
generated
vendored
Normal file
2
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/file-command.d.ts
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export declare function issueFileCommand(command: string, message: any): void;
|
||||
export declare function prepareKeyValueMessage(key: string, value: any): string;
|
||||
58
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/file-command.js
generated
vendored
Normal file
58
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/file-command.js
generated
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
"use strict";
|
||||
// For internal use, subject to change.
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.prepareKeyValueMessage = exports.issueFileCommand = void 0;
|
||||
// We use any as a valid input type
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
const fs = __importStar(require("fs"));
|
||||
const os = __importStar(require("os"));
|
||||
const uuid_1 = require("uuid");
|
||||
const utils_1 = require("./utils");
|
||||
function issueFileCommand(command, message) {
|
||||
const filePath = process.env[`GITHUB_${command}`];
|
||||
if (!filePath) {
|
||||
throw new Error(`Unable to find environment variable for file command ${command}`);
|
||||
}
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error(`Missing file at path: ${filePath}`);
|
||||
}
|
||||
fs.appendFileSync(filePath, `${utils_1.toCommandValue(message)}${os.EOL}`, {
|
||||
encoding: 'utf8'
|
||||
});
|
||||
}
|
||||
exports.issueFileCommand = issueFileCommand;
|
||||
function prepareKeyValueMessage(key, value) {
|
||||
const delimiter = `ghadelimiter_${uuid_1.v4()}`;
|
||||
const convertedValue = utils_1.toCommandValue(value);
|
||||
// These should realistically never happen, but just in case someone finds a
|
||||
// way to exploit uuid generation let's not allow keys or values that contain
|
||||
// the delimiter.
|
||||
if (key.includes(delimiter)) {
|
||||
throw new Error(`Unexpected input: name should not contain the delimiter "${delimiter}"`);
|
||||
}
|
||||
if (convertedValue.includes(delimiter)) {
|
||||
throw new Error(`Unexpected input: value should not contain the delimiter "${delimiter}"`);
|
||||
}
|
||||
return `${key}<<${delimiter}${os.EOL}${convertedValue}${os.EOL}${delimiter}`;
|
||||
}
|
||||
exports.prepareKeyValueMessage = prepareKeyValueMessage;
|
||||
//# sourceMappingURL=file-command.js.map
|
||||
1
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/file-command.js.map
generated
vendored
Normal file
1
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/file-command.js.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"file-command.js","sourceRoot":"","sources":["../src/file-command.ts"],"names":[],"mappings":";AAAA,uCAAuC;;;;;;;;;;;;;;;;;;;;;;AAEvC,mCAAmC;AACnC,uDAAuD;AAEvD,uCAAwB;AACxB,uCAAwB;AACxB,+BAAiC;AACjC,mCAAsC;AAEtC,SAAgB,gBAAgB,CAAC,OAAe,EAAE,OAAY;IAC5D,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,OAAO,EAAE,CAAC,CAAA;IACjD,IAAI,CAAC,QAAQ,EAAE;QACb,MAAM,IAAI,KAAK,CACb,wDAAwD,OAAO,EAAE,CAClE,CAAA;KACF;IACD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE;QAC5B,MAAM,IAAI,KAAK,CAAC,yBAAyB,QAAQ,EAAE,CAAC,CAAA;KACrD;IAED,EAAE,CAAC,cAAc,CAAC,QAAQ,EAAE,GAAG,sBAAc,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,EAAE;QACjE,QAAQ,EAAE,MAAM;KACjB,CAAC,CAAA;AACJ,CAAC;AAdD,4CAcC;AAED,SAAgB,sBAAsB,CAAC,GAAW,EAAE,KAAU;IAC5D,MAAM,SAAS,GAAG,gBAAgB,SAAM,EAAE,EAAE,CAAA;IAC5C,MAAM,cAAc,GAAG,sBAAc,CAAC,KAAK,CAAC,CAAA;IAE5C,4EAA4E;IAC5E,6EAA6E;IAC7E,iBAAiB;IACjB,IAAI,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE;QAC3B,MAAM,IAAI,KAAK,CACb,4DAA4D,SAAS,GAAG,CACzE,CAAA;KACF;IAED,IAAI,cAAc,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE;QACtC,MAAM,IAAI,KAAK,CACb,6DAA6D,SAAS,GAAG,CAC1E,CAAA;KACF;IAED,OAAO,GAAG,GAAG,KAAK,SAAS,GAAG,EAAE,CAAC,GAAG,GAAG,cAAc,GAAG,EAAE,CAAC,GAAG,GAAG,SAAS,EAAE,CAAA;AAC9E,CAAC;AApBD,wDAoBC"}
|
||||
7
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/oidc-utils.d.ts
generated
vendored
Normal file
7
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/oidc-utils.d.ts
generated
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
export declare class OidcClient {
|
||||
private static createHttpClient;
|
||||
private static getRequestToken;
|
||||
private static getIDTokenUrl;
|
||||
private static getCall;
|
||||
static getIDToken(audience?: string): Promise<string>;
|
||||
}
|
||||
77
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/oidc-utils.js
generated
vendored
Normal file
77
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/oidc-utils.js
generated
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.OidcClient = void 0;
|
||||
const http_client_1 = require("@actions/http-client");
|
||||
const auth_1 = require("@actions/http-client/lib/auth");
|
||||
const core_1 = require("./core");
|
||||
class OidcClient {
|
||||
static createHttpClient(allowRetry = true, maxRetry = 10) {
|
||||
const requestOptions = {
|
||||
allowRetries: allowRetry,
|
||||
maxRetries: maxRetry
|
||||
};
|
||||
return new http_client_1.HttpClient('actions/oidc-client', [new auth_1.BearerCredentialHandler(OidcClient.getRequestToken())], requestOptions);
|
||||
}
|
||||
static getRequestToken() {
|
||||
const token = process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN'];
|
||||
if (!token) {
|
||||
throw new Error('Unable to get ACTIONS_ID_TOKEN_REQUEST_TOKEN env variable');
|
||||
}
|
||||
return token;
|
||||
}
|
||||
static getIDTokenUrl() {
|
||||
const runtimeUrl = process.env['ACTIONS_ID_TOKEN_REQUEST_URL'];
|
||||
if (!runtimeUrl) {
|
||||
throw new Error('Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable');
|
||||
}
|
||||
return runtimeUrl;
|
||||
}
|
||||
static getCall(id_token_url) {
|
||||
var _a;
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const httpclient = OidcClient.createHttpClient();
|
||||
const res = yield httpclient
|
||||
.getJson(id_token_url)
|
||||
.catch(error => {
|
||||
throw new Error(`Failed to get ID Token. \n
|
||||
Error Code : ${error.statusCode}\n
|
||||
Error Message: ${error.result.message}`);
|
||||
});
|
||||
const id_token = (_a = res.result) === null || _a === void 0 ? void 0 : _a.value;
|
||||
if (!id_token) {
|
||||
throw new Error('Response json body do not have ID Token field');
|
||||
}
|
||||
return id_token;
|
||||
});
|
||||
}
|
||||
static getIDToken(audience) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
try {
|
||||
// New ID Token is requested from action service
|
||||
let id_token_url = OidcClient.getIDTokenUrl();
|
||||
if (audience) {
|
||||
const encodedAudience = encodeURIComponent(audience);
|
||||
id_token_url = `${id_token_url}&audience=${encodedAudience}`;
|
||||
}
|
||||
core_1.debug(`ID token url is ${id_token_url}`);
|
||||
const id_token = yield OidcClient.getCall(id_token_url);
|
||||
core_1.setSecret(id_token);
|
||||
return id_token;
|
||||
}
|
||||
catch (error) {
|
||||
throw new Error(`Error message: ${error.message}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
exports.OidcClient = OidcClient;
|
||||
//# sourceMappingURL=oidc-utils.js.map
|
||||
1
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/oidc-utils.js.map
generated
vendored
Normal file
1
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/oidc-utils.js.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"oidc-utils.js","sourceRoot":"","sources":["../src/oidc-utils.ts"],"names":[],"mappings":";;;;;;;;;;;;AAGA,sDAA+C;AAC/C,wDAAqE;AACrE,iCAAuC;AAKvC,MAAa,UAAU;IACb,MAAM,CAAC,gBAAgB,CAC7B,UAAU,GAAG,IAAI,EACjB,QAAQ,GAAG,EAAE;QAEb,MAAM,cAAc,GAAmB;YACrC,YAAY,EAAE,UAAU;YACxB,UAAU,EAAE,QAAQ;SACrB,CAAA;QAED,OAAO,IAAI,wBAAU,CACnB,qBAAqB,EACrB,CAAC,IAAI,8BAAuB,CAAC,UAAU,CAAC,eAAe,EAAE,CAAC,CAAC,EAC3D,cAAc,CACf,CAAA;IACH,CAAC;IAEO,MAAM,CAAC,eAAe;QAC5B,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,gCAAgC,CAAC,CAAA;QAC3D,IAAI,CAAC,KAAK,EAAE;YACV,MAAM,IAAI,KAAK,CACb,2DAA2D,CAC5D,CAAA;SACF;QACD,OAAO,KAAK,CAAA;IACd,CAAC;IAEO,MAAM,CAAC,aAAa;QAC1B,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,8BAA8B,CAAC,CAAA;QAC9D,IAAI,CAAC,UAAU,EAAE;YACf,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAA;SAC3E;QACD,OAAO,UAAU,CAAA;IACnB,CAAC;IAEO,MAAM,CAAO,OAAO,CAAC,YAAoB;;;YAC/C,MAAM,UAAU,GAAG,UAAU,CAAC,gBAAgB,EAAE,CAAA;YAEhD,MAAM,GAAG,GAAG,MAAM,UAAU;iBACzB,OAAO,CAAgB,YAAY,CAAC;iBACpC,KAAK,CAAC,KAAK,CAAC,EAAE;gBACb,MAAM,IAAI,KAAK,CACb;uBACa,KAAK,CAAC,UAAU;yBACd,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,CACtC,CAAA;YACH,CAAC,CAAC,CAAA;YAEJ,MAAM,QAAQ,SAAG,GAAG,CAAC,MAAM,0CAAE,KAAK,CAAA;YAClC,IAAI,CAAC,QAAQ,EAAE;gBACb,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAA;aACjE;YACD,OAAO,QAAQ,CAAA;;KAChB;IAED,MAAM,CAAO,UAAU,CAAC,QAAiB;;YACvC,IAAI;gBACF,gDAAgD;gBAChD,IAAI,YAAY,GAAW,UAAU,CAAC,aAAa,EAAE,CAAA;gBACrD,IAAI,QAAQ,EAAE;oBACZ,MAAM,eAAe,GAAG,kBAAkB,CAAC,QAAQ,CAAC,CAAA;oBACpD,YAAY,GAAG,GAAG,YAAY,aAAa,eAAe,EAAE,CAAA;iBAC7D;gBAED,YAAK,CAAC,mBAAmB,YAAY,EAAE,CAAC,CAAA;gBAExC,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC,YAAY,CAAC,CAAA;gBACvD,gBAAS,CAAC,QAAQ,CAAC,CAAA;gBACnB,OAAO,QAAQ,CAAA;aAChB;YAAC,OAAO,KAAK,EAAE;gBACd,MAAM,IAAI,KAAK,CAAC,kBAAkB,KAAK,CAAC,OAAO,EAAE,CAAC,CAAA;aACnD;QACH,CAAC;KAAA;CACF;AAzED,gCAyEC"}
|
||||
25
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/path-utils.d.ts
generated
vendored
Normal file
25
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/path-utils.d.ts
generated
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* toPosixPath converts the given path to the posix form. On Windows, \\ will be
|
||||
* replaced with /.
|
||||
*
|
||||
* @param pth. Path to transform.
|
||||
* @return string Posix path.
|
||||
*/
|
||||
export declare function toPosixPath(pth: string): string;
|
||||
/**
|
||||
* toWin32Path converts the given path to the win32 form. On Linux, / will be
|
||||
* replaced with \\.
|
||||
*
|
||||
* @param pth. Path to transform.
|
||||
* @return string Win32 path.
|
||||
*/
|
||||
export declare function toWin32Path(pth: string): string;
|
||||
/**
|
||||
* toPlatformPath converts the given path to a platform-specific path. It does
|
||||
* this by replacing instances of / and \ with the platform-specific path
|
||||
* separator.
|
||||
*
|
||||
* @param pth The path to platformize.
|
||||
* @return string The platform-specific path.
|
||||
*/
|
||||
export declare function toPlatformPath(pth: string): string;
|
||||
58
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/path-utils.js
generated
vendored
Normal file
58
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/path-utils.js
generated
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.toPlatformPath = exports.toWin32Path = exports.toPosixPath = void 0;
|
||||
const path = __importStar(require("path"));
|
||||
/**
|
||||
* toPosixPath converts the given path to the posix form. On Windows, \\ will be
|
||||
* replaced with /.
|
||||
*
|
||||
* @param pth. Path to transform.
|
||||
* @return string Posix path.
|
||||
*/
|
||||
function toPosixPath(pth) {
|
||||
return pth.replace(/[\\]/g, '/');
|
||||
}
|
||||
exports.toPosixPath = toPosixPath;
|
||||
/**
|
||||
* toWin32Path converts the given path to the win32 form. On Linux, / will be
|
||||
* replaced with \\.
|
||||
*
|
||||
* @param pth. Path to transform.
|
||||
* @return string Win32 path.
|
||||
*/
|
||||
function toWin32Path(pth) {
|
||||
return pth.replace(/[/]/g, '\\');
|
||||
}
|
||||
exports.toWin32Path = toWin32Path;
|
||||
/**
|
||||
* toPlatformPath converts the given path to a platform-specific path. It does
|
||||
* this by replacing instances of / and \ with the platform-specific path
|
||||
* separator.
|
||||
*
|
||||
* @param pth The path to platformize.
|
||||
* @return string The platform-specific path.
|
||||
*/
|
||||
function toPlatformPath(pth) {
|
||||
return pth.replace(/[/\\]/g, path.sep);
|
||||
}
|
||||
exports.toPlatformPath = toPlatformPath;
|
||||
//# sourceMappingURL=path-utils.js.map
|
||||
1
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/path-utils.js.map
generated
vendored
Normal file
1
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/path-utils.js.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"path-utils.js","sourceRoot":"","sources":["../src/path-utils.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAAA,2CAA4B;AAE5B;;;;;;GAMG;AACH,SAAgB,WAAW,CAAC,GAAW;IACrC,OAAO,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAA;AAClC,CAAC;AAFD,kCAEC;AAED;;;;;;GAMG;AACH,SAAgB,WAAW,CAAC,GAAW;IACrC,OAAO,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;AAClC,CAAC;AAFD,kCAEC;AAED;;;;;;;GAOG;AACH,SAAgB,cAAc,CAAC,GAAW;IACxC,OAAO,GAAG,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,CAAA;AACxC,CAAC;AAFD,wCAEC"}
|
||||
202
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/summary.d.ts
generated
vendored
Normal file
202
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/summary.d.ts
generated
vendored
Normal file
@@ -0,0 +1,202 @@
|
||||
export declare const SUMMARY_ENV_VAR = "GITHUB_STEP_SUMMARY";
|
||||
export declare const SUMMARY_DOCS_URL = "https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary";
|
||||
export declare type SummaryTableRow = (SummaryTableCell | string)[];
|
||||
export interface SummaryTableCell {
|
||||
/**
|
||||
* Cell content
|
||||
*/
|
||||
data: string;
|
||||
/**
|
||||
* Render cell as header
|
||||
* (optional) default: false
|
||||
*/
|
||||
header?: boolean;
|
||||
/**
|
||||
* Number of columns the cell extends
|
||||
* (optional) default: '1'
|
||||
*/
|
||||
colspan?: string;
|
||||
/**
|
||||
* Number of rows the cell extends
|
||||
* (optional) default: '1'
|
||||
*/
|
||||
rowspan?: string;
|
||||
}
|
||||
export interface SummaryImageOptions {
|
||||
/**
|
||||
* The width of the image in pixels. Must be an integer without a unit.
|
||||
* (optional)
|
||||
*/
|
||||
width?: string;
|
||||
/**
|
||||
* The height of the image in pixels. Must be an integer without a unit.
|
||||
* (optional)
|
||||
*/
|
||||
height?: string;
|
||||
}
|
||||
export interface SummaryWriteOptions {
|
||||
/**
|
||||
* Replace all existing content in summary file with buffer contents
|
||||
* (optional) default: false
|
||||
*/
|
||||
overwrite?: boolean;
|
||||
}
|
||||
declare class Summary {
|
||||
private _buffer;
|
||||
private _filePath?;
|
||||
constructor();
|
||||
/**
|
||||
* Finds the summary file path from the environment, rejects if env var is not found or file does not exist
|
||||
* Also checks r/w permissions.
|
||||
*
|
||||
* @returns step summary file path
|
||||
*/
|
||||
private filePath;
|
||||
/**
|
||||
* Wraps content in an HTML tag, adding any HTML attributes
|
||||
*
|
||||
* @param {string} tag HTML tag to wrap
|
||||
* @param {string | null} content content within the tag
|
||||
* @param {[attribute: string]: string} attrs key-value list of HTML attributes to add
|
||||
*
|
||||
* @returns {string} content wrapped in HTML element
|
||||
*/
|
||||
private wrap;
|
||||
/**
|
||||
* Writes text in the buffer to the summary buffer file and empties buffer. Will append by default.
|
||||
*
|
||||
* @param {SummaryWriteOptions} [options] (optional) options for write operation
|
||||
*
|
||||
* @returns {Promise<Summary>} summary instance
|
||||
*/
|
||||
write(options?: SummaryWriteOptions): Promise<Summary>;
|
||||
/**
|
||||
* Clears the summary buffer and wipes the summary file
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
clear(): Promise<Summary>;
|
||||
/**
|
||||
* Returns the current summary buffer as a string
|
||||
*
|
||||
* @returns {string} string of summary buffer
|
||||
*/
|
||||
stringify(): string;
|
||||
/**
|
||||
* If the summary buffer is empty
|
||||
*
|
||||
* @returns {boolen} true if the buffer is empty
|
||||
*/
|
||||
isEmptyBuffer(): boolean;
|
||||
/**
|
||||
* Resets the summary buffer without writing to summary file
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
emptyBuffer(): Summary;
|
||||
/**
|
||||
* Adds raw text to the summary buffer
|
||||
*
|
||||
* @param {string} text content to add
|
||||
* @param {boolean} [addEOL=false] (optional) append an EOL to the raw text (default: false)
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addRaw(text: string, addEOL?: boolean): Summary;
|
||||
/**
|
||||
* Adds the operating system-specific end-of-line marker to the buffer
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addEOL(): Summary;
|
||||
/**
|
||||
* Adds an HTML codeblock to the summary buffer
|
||||
*
|
||||
* @param {string} code content to render within fenced code block
|
||||
* @param {string} lang (optional) language to syntax highlight code
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addCodeBlock(code: string, lang?: string): Summary;
|
||||
/**
|
||||
* Adds an HTML list to the summary buffer
|
||||
*
|
||||
* @param {string[]} items list of items to render
|
||||
* @param {boolean} [ordered=false] (optional) if the rendered list should be ordered or not (default: false)
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addList(items: string[], ordered?: boolean): Summary;
|
||||
/**
|
||||
* Adds an HTML table to the summary buffer
|
||||
*
|
||||
* @param {SummaryTableCell[]} rows table rows
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addTable(rows: SummaryTableRow[]): Summary;
|
||||
/**
|
||||
* Adds a collapsable HTML details element to the summary buffer
|
||||
*
|
||||
* @param {string} label text for the closed state
|
||||
* @param {string} content collapsable content
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addDetails(label: string, content: string): Summary;
|
||||
/**
|
||||
* Adds an HTML image tag to the summary buffer
|
||||
*
|
||||
* @param {string} src path to the image you to embed
|
||||
* @param {string} alt text description of the image
|
||||
* @param {SummaryImageOptions} options (optional) addition image attributes
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addImage(src: string, alt: string, options?: SummaryImageOptions): Summary;
|
||||
/**
|
||||
* Adds an HTML section heading element
|
||||
*
|
||||
* @param {string} text heading text
|
||||
* @param {number | string} [level=1] (optional) the heading level, default: 1
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addHeading(text: string, level?: number | string): Summary;
|
||||
/**
|
||||
* Adds an HTML thematic break (<hr>) to the summary buffer
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addSeparator(): Summary;
|
||||
/**
|
||||
* Adds an HTML line break (<br>) to the summary buffer
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addBreak(): Summary;
|
||||
/**
|
||||
* Adds an HTML blockquote to the summary buffer
|
||||
*
|
||||
* @param {string} text quote text
|
||||
* @param {string} cite (optional) citation url
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addQuote(text: string, cite?: string): Summary;
|
||||
/**
|
||||
* Adds an HTML anchor tag to the summary buffer
|
||||
*
|
||||
* @param {string} text link text/content
|
||||
* @param {string} href hyperlink
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addLink(text: string, href: string): Summary;
|
||||
}
|
||||
/**
|
||||
* @deprecated use `core.summary`
|
||||
*/
|
||||
export declare const markdownSummary: Summary;
|
||||
export declare const summary: Summary;
|
||||
export {};
|
||||
283
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/summary.js
generated
vendored
Normal file
283
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/summary.js
generated
vendored
Normal file
@@ -0,0 +1,283 @@
|
||||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.summary = exports.markdownSummary = exports.SUMMARY_DOCS_URL = exports.SUMMARY_ENV_VAR = void 0;
|
||||
const os_1 = require("os");
|
||||
const fs_1 = require("fs");
|
||||
const { access, appendFile, writeFile } = fs_1.promises;
|
||||
exports.SUMMARY_ENV_VAR = 'GITHUB_STEP_SUMMARY';
|
||||
exports.SUMMARY_DOCS_URL = 'https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary';
|
||||
class Summary {
|
||||
constructor() {
|
||||
this._buffer = '';
|
||||
}
|
||||
/**
|
||||
* Finds the summary file path from the environment, rejects if env var is not found or file does not exist
|
||||
* Also checks r/w permissions.
|
||||
*
|
||||
* @returns step summary file path
|
||||
*/
|
||||
filePath() {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
if (this._filePath) {
|
||||
return this._filePath;
|
||||
}
|
||||
const pathFromEnv = process.env[exports.SUMMARY_ENV_VAR];
|
||||
if (!pathFromEnv) {
|
||||
throw new Error(`Unable to find environment variable for $${exports.SUMMARY_ENV_VAR}. Check if your runtime environment supports job summaries.`);
|
||||
}
|
||||
try {
|
||||
yield access(pathFromEnv, fs_1.constants.R_OK | fs_1.constants.W_OK);
|
||||
}
|
||||
catch (_a) {
|
||||
throw new Error(`Unable to access summary file: '${pathFromEnv}'. Check if the file has correct read/write permissions.`);
|
||||
}
|
||||
this._filePath = pathFromEnv;
|
||||
return this._filePath;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Wraps content in an HTML tag, adding any HTML attributes
|
||||
*
|
||||
* @param {string} tag HTML tag to wrap
|
||||
* @param {string | null} content content within the tag
|
||||
* @param {[attribute: string]: string} attrs key-value list of HTML attributes to add
|
||||
*
|
||||
* @returns {string} content wrapped in HTML element
|
||||
*/
|
||||
wrap(tag, content, attrs = {}) {
|
||||
const htmlAttrs = Object.entries(attrs)
|
||||
.map(([key, value]) => ` ${key}="${value}"`)
|
||||
.join('');
|
||||
if (!content) {
|
||||
return `<${tag}${htmlAttrs}>`;
|
||||
}
|
||||
return `<${tag}${htmlAttrs}>${content}</${tag}>`;
|
||||
}
|
||||
/**
|
||||
* Writes text in the buffer to the summary buffer file and empties buffer. Will append by default.
|
||||
*
|
||||
* @param {SummaryWriteOptions} [options] (optional) options for write operation
|
||||
*
|
||||
* @returns {Promise<Summary>} summary instance
|
||||
*/
|
||||
write(options) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const overwrite = !!(options === null || options === void 0 ? void 0 : options.overwrite);
|
||||
const filePath = yield this.filePath();
|
||||
const writeFunc = overwrite ? writeFile : appendFile;
|
||||
yield writeFunc(filePath, this._buffer, { encoding: 'utf8' });
|
||||
return this.emptyBuffer();
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Clears the summary buffer and wipes the summary file
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
clear() {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
return this.emptyBuffer().write({ overwrite: true });
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Returns the current summary buffer as a string
|
||||
*
|
||||
* @returns {string} string of summary buffer
|
||||
*/
|
||||
stringify() {
|
||||
return this._buffer;
|
||||
}
|
||||
/**
|
||||
* If the summary buffer is empty
|
||||
*
|
||||
* @returns {boolen} true if the buffer is empty
|
||||
*/
|
||||
isEmptyBuffer() {
|
||||
return this._buffer.length === 0;
|
||||
}
|
||||
/**
|
||||
* Resets the summary buffer without writing to summary file
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
emptyBuffer() {
|
||||
this._buffer = '';
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Adds raw text to the summary buffer
|
||||
*
|
||||
* @param {string} text content to add
|
||||
* @param {boolean} [addEOL=false] (optional) append an EOL to the raw text (default: false)
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addRaw(text, addEOL = false) {
|
||||
this._buffer += text;
|
||||
return addEOL ? this.addEOL() : this;
|
||||
}
|
||||
/**
|
||||
* Adds the operating system-specific end-of-line marker to the buffer
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addEOL() {
|
||||
return this.addRaw(os_1.EOL);
|
||||
}
|
||||
/**
|
||||
* Adds an HTML codeblock to the summary buffer
|
||||
*
|
||||
* @param {string} code content to render within fenced code block
|
||||
* @param {string} lang (optional) language to syntax highlight code
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addCodeBlock(code, lang) {
|
||||
const attrs = Object.assign({}, (lang && { lang }));
|
||||
const element = this.wrap('pre', this.wrap('code', code), attrs);
|
||||
return this.addRaw(element).addEOL();
|
||||
}
|
||||
/**
|
||||
* Adds an HTML list to the summary buffer
|
||||
*
|
||||
* @param {string[]} items list of items to render
|
||||
* @param {boolean} [ordered=false] (optional) if the rendered list should be ordered or not (default: false)
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addList(items, ordered = false) {
|
||||
const tag = ordered ? 'ol' : 'ul';
|
||||
const listItems = items.map(item => this.wrap('li', item)).join('');
|
||||
const element = this.wrap(tag, listItems);
|
||||
return this.addRaw(element).addEOL();
|
||||
}
|
||||
/**
|
||||
* Adds an HTML table to the summary buffer
|
||||
*
|
||||
* @param {SummaryTableCell[]} rows table rows
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addTable(rows) {
|
||||
const tableBody = rows
|
||||
.map(row => {
|
||||
const cells = row
|
||||
.map(cell => {
|
||||
if (typeof cell === 'string') {
|
||||
return this.wrap('td', cell);
|
||||
}
|
||||
const { header, data, colspan, rowspan } = cell;
|
||||
const tag = header ? 'th' : 'td';
|
||||
const attrs = Object.assign(Object.assign({}, (colspan && { colspan })), (rowspan && { rowspan }));
|
||||
return this.wrap(tag, data, attrs);
|
||||
})
|
||||
.join('');
|
||||
return this.wrap('tr', cells);
|
||||
})
|
||||
.join('');
|
||||
const element = this.wrap('table', tableBody);
|
||||
return this.addRaw(element).addEOL();
|
||||
}
|
||||
/**
|
||||
* Adds a collapsable HTML details element to the summary buffer
|
||||
*
|
||||
* @param {string} label text for the closed state
|
||||
* @param {string} content collapsable content
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addDetails(label, content) {
|
||||
const element = this.wrap('details', this.wrap('summary', label) + content);
|
||||
return this.addRaw(element).addEOL();
|
||||
}
|
||||
/**
|
||||
* Adds an HTML image tag to the summary buffer
|
||||
*
|
||||
* @param {string} src path to the image you to embed
|
||||
* @param {string} alt text description of the image
|
||||
* @param {SummaryImageOptions} options (optional) addition image attributes
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addImage(src, alt, options) {
|
||||
const { width, height } = options || {};
|
||||
const attrs = Object.assign(Object.assign({}, (width && { width })), (height && { height }));
|
||||
const element = this.wrap('img', null, Object.assign({ src, alt }, attrs));
|
||||
return this.addRaw(element).addEOL();
|
||||
}
|
||||
/**
|
||||
* Adds an HTML section heading element
|
||||
*
|
||||
* @param {string} text heading text
|
||||
* @param {number | string} [level=1] (optional) the heading level, default: 1
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addHeading(text, level) {
|
||||
const tag = `h${level}`;
|
||||
const allowedTag = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tag)
|
||||
? tag
|
||||
: 'h1';
|
||||
const element = this.wrap(allowedTag, text);
|
||||
return this.addRaw(element).addEOL();
|
||||
}
|
||||
/**
|
||||
* Adds an HTML thematic break (<hr>) to the summary buffer
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addSeparator() {
|
||||
const element = this.wrap('hr', null);
|
||||
return this.addRaw(element).addEOL();
|
||||
}
|
||||
/**
|
||||
* Adds an HTML line break (<br>) to the summary buffer
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addBreak() {
|
||||
const element = this.wrap('br', null);
|
||||
return this.addRaw(element).addEOL();
|
||||
}
|
||||
/**
|
||||
* Adds an HTML blockquote to the summary buffer
|
||||
*
|
||||
* @param {string} text quote text
|
||||
* @param {string} cite (optional) citation url
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addQuote(text, cite) {
|
||||
const attrs = Object.assign({}, (cite && { cite }));
|
||||
const element = this.wrap('blockquote', text, attrs);
|
||||
return this.addRaw(element).addEOL();
|
||||
}
|
||||
/**
|
||||
* Adds an HTML anchor tag to the summary buffer
|
||||
*
|
||||
* @param {string} text link text/content
|
||||
* @param {string} href hyperlink
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addLink(text, href) {
|
||||
const element = this.wrap('a', text, { href });
|
||||
return this.addRaw(element).addEOL();
|
||||
}
|
||||
}
|
||||
const _summary = new Summary();
|
||||
/**
|
||||
* @deprecated use `core.summary`
|
||||
*/
|
||||
exports.markdownSummary = _summary;
|
||||
exports.summary = _summary;
|
||||
//# sourceMappingURL=summary.js.map
|
||||
1
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/summary.js.map
generated
vendored
Normal file
1
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/summary.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
14
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/utils.d.ts
generated
vendored
Normal file
14
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/utils.d.ts
generated
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
import { AnnotationProperties } from './core';
|
||||
import { CommandProperties } from './command';
|
||||
/**
|
||||
* Sanitizes an input into a string so it can be passed into issueCommand safely
|
||||
* @param input input to sanitize into a string
|
||||
*/
|
||||
export declare function toCommandValue(input: any): string;
|
||||
/**
|
||||
*
|
||||
* @param annotationProperties
|
||||
* @returns The command properties to send with the actual annotation command
|
||||
* See IssueCommandProperties: https://github.com/actions/runner/blob/main/src/Runner.Worker/ActionCommandManager.cs#L646
|
||||
*/
|
||||
export declare function toCommandProperties(annotationProperties: AnnotationProperties): CommandProperties;
|
||||
40
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/utils.js
generated
vendored
Normal file
40
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/utils.js
generated
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
"use strict";
|
||||
// We use any as a valid input type
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.toCommandProperties = exports.toCommandValue = void 0;
|
||||
/**
|
||||
* Sanitizes an input into a string so it can be passed into issueCommand safely
|
||||
* @param input input to sanitize into a string
|
||||
*/
|
||||
function toCommandValue(input) {
|
||||
if (input === null || input === undefined) {
|
||||
return '';
|
||||
}
|
||||
else if (typeof input === 'string' || input instanceof String) {
|
||||
return input;
|
||||
}
|
||||
return JSON.stringify(input);
|
||||
}
|
||||
exports.toCommandValue = toCommandValue;
|
||||
/**
|
||||
*
|
||||
* @param annotationProperties
|
||||
* @returns The command properties to send with the actual annotation command
|
||||
* See IssueCommandProperties: https://github.com/actions/runner/blob/main/src/Runner.Worker/ActionCommandManager.cs#L646
|
||||
*/
|
||||
function toCommandProperties(annotationProperties) {
|
||||
if (!Object.keys(annotationProperties).length) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
title: annotationProperties.title,
|
||||
file: annotationProperties.file,
|
||||
line: annotationProperties.startLine,
|
||||
endLine: annotationProperties.endLine,
|
||||
col: annotationProperties.startColumn,
|
||||
endColumn: annotationProperties.endColumn
|
||||
};
|
||||
}
|
||||
exports.toCommandProperties = toCommandProperties;
|
||||
//# sourceMappingURL=utils.js.map
|
||||
1
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/utils.js.map
generated
vendored
Normal file
1
act/runner/testdata/actions/node12/node_modules/@actions/core/lib/utils.js.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":";AAAA,mCAAmC;AACnC,uDAAuD;;;AAKvD;;;GAGG;AACH,SAAgB,cAAc,CAAC,KAAU;IACvC,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,EAAE;QACzC,OAAO,EAAE,CAAA;KACV;SAAM,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,YAAY,MAAM,EAAE;QAC/D,OAAO,KAAe,CAAA;KACvB;IACD,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;AAC9B,CAAC;AAPD,wCAOC;AAED;;;;;GAKG;AACH,SAAgB,mBAAmB,CACjC,oBAA0C;IAE1C,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC,MAAM,EAAE;QAC7C,OAAO,EAAE,CAAA;KACV;IAED,OAAO;QACL,KAAK,EAAE,oBAAoB,CAAC,KAAK;QACjC,IAAI,EAAE,oBAAoB,CAAC,IAAI;QAC/B,IAAI,EAAE,oBAAoB,CAAC,SAAS;QACpC,OAAO,EAAE,oBAAoB,CAAC,OAAO;QACrC,GAAG,EAAE,oBAAoB,CAAC,WAAW;QACrC,SAAS,EAAE,oBAAoB,CAAC,SAAS;KAC1C,CAAA;AACH,CAAC;AAfD,kDAeC"}
|
||||
46
act/runner/testdata/actions/node12/node_modules/@actions/core/package.json
generated
vendored
Normal file
46
act/runner/testdata/actions/node12/node_modules/@actions/core/package.json
generated
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "@actions/core",
|
||||
"version": "1.10.0",
|
||||
"description": "Actions core lib",
|
||||
"keywords": [
|
||||
"github",
|
||||
"actions",
|
||||
"core"
|
||||
],
|
||||
"homepage": "https://github.com/actions/toolkit/tree/main/packages/core",
|
||||
"license": "MIT",
|
||||
"main": "lib/core.js",
|
||||
"types": "lib/core.d.ts",
|
||||
"directories": {
|
||||
"lib": "lib",
|
||||
"test": "__tests__"
|
||||
},
|
||||
"files": [
|
||||
"lib",
|
||||
"!.DS_Store"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/actions/toolkit.git",
|
||||
"directory": "packages/core"
|
||||
},
|
||||
"scripts": {
|
||||
"audit-moderate": "npm install && npm audit --json --audit-level=moderate > audit.json",
|
||||
"test": "echo \"Error: run tests from root\" && exit 1",
|
||||
"tsc": "tsc"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/actions/toolkit/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/http-client": "^2.0.1",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^12.0.2",
|
||||
"@types/uuid": "^8.3.4"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user