147 Commits

Author SHA1 Message Date
Renovate Bot
589db33e70 fix(deps): update module github.com/docker/cli to v25.0.7+incompatible (#855)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [github.com/docker/cli](https://github.com/docker/cli) | `v25.0.3+incompatible` → `v25.0.7+incompatible` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fdocker%2fcli/v25.0.7+incompatible?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fdocker%2fcli/v25.0.3+incompatible/v25.0.7+incompatible?slim=true) |

---

### Release Notes

<details>
<summary>docker/cli (github.com/docker/cli)</summary>

### [`v25.0.7+incompatible`](https://github.com/docker/cli/compare/v25.0.6...v25.0.7)

[Compare Source](https://github.com/docker/cli/compare/v25.0.6...v25.0.7)

### [`v25.0.6+incompatible`](https://github.com/docker/cli/compare/v25.0.5...v25.0.6)

[Compare Source](https://github.com/docker/cli/compare/v25.0.5...v25.0.6)

### [`v25.0.5+incompatible`](https://github.com/docker/cli/compare/v25.0.4...v25.0.5)

[Compare Source](https://github.com/docker/cli/compare/v25.0.4...v25.0.5)

### [`v25.0.4+incompatible`](https://github.com/docker/cli/compare/v25.0.3...v25.0.4)

[Compare Source](https://github.com/docker/cli/compare/v25.0.3...v25.0.4)

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xNDEuNSIsInVwZGF0ZWRJblZlciI6IjQzLjE0MS41IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/855
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-04-27 01:01:35 +00:00
Renovate Bot
1032f857a1 fix(deps): update module connectrpc.com/connect to v1.19.2 (#854)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [connectrpc.com/connect](https://github.com/connectrpc/connect-go) | `v1.19.1` → `v1.19.2` | ![age](https://developer.mend.io/api/mc/badges/age/go/connectrpc.com%2fconnect/v1.19.2?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/connectrpc.com%2fconnect/v1.19.1/v1.19.2?slim=true) |

---

### Release Notes

<details>
<summary>connectrpc/connect-go (connectrpc.com/connect)</summary>

### [`v1.19.2`](https://github.com/connectrpc/connect-go/releases/tag/v1.19.2)

[Compare Source](https://github.com/connectrpc/connect-go/compare/v1.19.1...v1.19.2)

#### What's Changed

##### Governance

- Add [@&#8203;timostamm](https://github.com/timostamm) as a maintainer in [#&#8203;905](https://github.com/connectrpc/connect-go/pull/905) 🎉

##### Bugfixes

- Use 'deadline\_exceeded' instead of 'canceled' on HTTP/2 cancelation when appropriate by [@&#8203;jhump](https://github.com/jhump) in [#&#8203;904](https://github.com/connectrpc/connect-go/pull/904)
- Fix nil pointer deref in duplexHTTPCall under concurrent Send + CloseAndReceive by [@&#8203;simonferquel](https://github.com/simonferquel) in [#&#8203;919](https://github.com/connectrpc/connect-go/pull/919)

##### Other changes

- Refactor memhttptest to work with Go 1.25 synctest by [@&#8203;codefromthecrypt](https://github.com/codefromthecrypt) in [#&#8203;881](https://github.com/connectrpc/connect-go/pull/881)
- Doc clarifications by [@&#8203;emcfarlane](https://github.com/emcfarlane) ([#&#8203;911](https://github.com/connectrpc/connect-go/issues/911), [#&#8203;912](https://github.com/connectrpc/connect-go/issues/912)) and [@&#8203;stefanvanburen](https://github.com/stefanvanburen) ([#&#8203;906](https://github.com/connectrpc/connect-go/issues/906))

#### New Contributors

- [@&#8203;codefromthecrypt](https://github.com/codefromthecrypt) made their first contribution in [#&#8203;881](https://github.com/connectrpc/connect-go/pull/881)
- [@&#8203;simonferquel](https://github.com/simonferquel) made their first contribution in [#&#8203;919](https://github.com/connectrpc/connect-go/pull/919)

**Full Changelog**:

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xNDEuNSIsInVwZGF0ZWRJblZlciI6IjQzLjE0MS41IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/854
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-04-27 01:01:16 +00:00
silverwind
e56b984c04 fix: wait for docker supervise dir before s6-svwait (#851)
`s6-svscan` starts services in parallel, so `act_runner/run` could invoke `s6-svwait` before s6 had created the docker service's `supervise/` directory, failing with `s6-svwait: fatal: unable to s6_svstatus_read: No such file or directory`. Poll for the directory before waiting.

Fixes #760

---
This PR was written with the help of Claude Opus 4.7

Reviewed-on: https://gitea.com/gitea/runner/pulls/851
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
2026-04-26 22:50:48 +00:00
silverwind
fa5334eb24 fix: Heartbeat ReportState for long-running silent jobs (#852)
Fixes #826.

Regressed in f2d54556 (#819, "perf: reduce runner-to-server connection load with adaptive reporting and polling"). That change added an early-return in `ReportState` whenever there was no state change and no pending outputs, so jobs that produce no log output and no step transitions for many minutes (e.g. a Linux kernel build) stop heartbeating. The server eventually marks the task as orphaned and cancels it while the runner is still executing.

The fix tracks the last successful `UpdateTask` time in an atomic and keeps the no-op skip only while the previous report is younger than `stateReportInterval`. The periodic state ticker fires at exactly `stateReportInterval`, so silent jobs now heartbeat each tick; redundant sends from a `stateNotify` firing right after a tick are still suppressed, preserving the perf intent of #819.

Test added: `TestReporter_StateHeartbeat` asserts the skip path within the interval and the heartbeat path after the interval elapses.

---
This PR was written with the help of Claude Opus 4.7

Reviewed-on: https://gitea.com/gitea/runner/pulls/852
Reviewed-by: Nicolas <bircni@icloud.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: ChristopherHX <38043+christopherhx@noreply.gitea.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
2026-04-26 22:49:39 +00:00
Michael Hoang
7c6f1261d4 fix: fetch when other refs get force-pushed (#846)
Due to `NewGitCloneExecutor` fetching all the refs (rather than `GoGitActionCache`), if any refs move in a non-fast-forward fashion, this causes the entire action update to fail. As GitHub has special refs like `pull/N/merge` which are guaranteed to move in a non-fast-forward fashion, this leads actions from GitHub usually failing to update.

```
  ☁  git clone 'https://github.com/Mic92/update-flake-inputs-gitea' # ref=main
  cloning https://github.com/Mic92/update-flake-inputs-gitea to /var/lib/gitea-runner/nix0/.cache/act/9b0155f2957ac84c749f9ecc8afaec823af5ef2e67a104ac655623aee12ca5b2
Non-terminating error while running 'git clone': some refs were not updated
```

With the repo https://github.com/Mic92/update-flake-inputs-gitea, you can notice that it only has a `main` branch that moves in a fast-forward fashion and no tags that could've been force pushed.

Fixes #726

---------

Co-authored-by: Michael Hoang <enzime@users.noreply.github.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/846
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Reviewed-by: Nicolas <bircni@icloud.com>
Co-authored-by: Michael Hoang <194829+enzime@noreply.gitea.com>
Co-committed-by: Michael Hoang <194829+enzime@noreply.gitea.com>
2026-04-26 11:08:23 +00:00
silverwind
fbd6316928 Merge pull request 'Align root files with gitea' (#844) from silverwind/act_runner:align-makefile-gitea into main
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/844
Reviewed-by: Bo-Yi Wu (吳柏毅) <appleboy.tw@gmail.com>
2026-04-24 12:59:45 +00:00
silverwind
ade5b8202e Merge branch 'main' into align-makefile-gitea 2026-04-24 12:23:17 +00:00
silverwind
a31f3962c0 Add .dockerignore and align .editorconfig
Co-Authored-By: Claude (Opus 4.7) <noreply@anthropic.com>
2026-04-24 13:08:16 +02:00
silverwind
04244fc3f7 Add AGENTS.md and CLAUDE.md
Co-Authored-By: Claude (Opus 4.7) <noreply@anthropic.com>
2026-04-24 13:04:47 +02:00
silverwind
cb58492678 Merge pull request 'upgrade go git and yaml' (#842) from lunny/upgrade_dep into main
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/842
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
2026-04-24 11:03:23 +00:00
silverwind
9faadad0ce Add help target and target descriptions
Co-Authored-By: Claude (Opus 4.7) <noreply@anthropic.com>
2026-04-24 13:03:23 +02:00
silverwind
352096c5bf Align Makefile with gitea
- Fix `GXZ_PAGAGE` typo to `GXZ_PACKAGE`.
- Move `gxz`/`xgo` tool installs from `deps-backend` to `deps-tools`
  and add `golangci-lint` there; `deps-backend` is just `go mod download`.
- Use `--color=always` + `printf` in `tidy-check` to match `fmt-check`.
- Use STATIC-gated `EXTLDFLAGS` instead of a uname-based toggle, and
  move `-s -w` out of `EXTLDFLAGS` to match gitea's `-ldflags` layout.
- Pass `-s -w -linkmode external -extldflags "-static"` explicitly for
  release-linux / release-windows; add `-s -w` to release-darwin.

Co-Authored-By: Claude (Opus 4.7) <noreply@anthropic.com>
2026-04-24 13:00:37 +02:00
Lunny Xiao
b5c50bb3ab upgrade go git 2026-04-23 14:38:25 -07:00
silverwind
8af9a2b47a Merge gitea/act into act/ folder' (#827)
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/827
Reviewed-by: ChristopherHX <38043+christopherhx@noreply.gitea.com>
2026-04-23 16:41:15 +00:00
silverwind
fab2d6ae04 Merge gitea/act into act/
Merges the `gitea.com/gitea/act` fork into this repository as the `act/`
directory and consumes it as a local package. The `replace github.com/nektos/act
=> gitea.com/gitea/act` directive is removed; act's dependencies are merged
into the root `go.mod`.

- Imports rewritten: `github.com/nektos/act/pkg/...` → `gitea.com/gitea/act_runner/act/...`
  (flattened — `pkg/` boundary dropped to match the layout forgejo-runner adopted).
- Dropped act's CLI (`cmd/`, `main.go`) and all upstream project files; kept
  the library tree + `LICENSE`.
- Added `// Copyright <year> The Gitea Authors ...` / `// Copyright <year> nektos`
  headers to 104 `.go` files.
- Pre-existing act lint violations annotated inline with
  `//nolint:<linter> // pre-existing issue from nektos/act`.
  `.golangci.yml` is unchanged vs `main`.
- Makefile test target: `-race -short` (matches forgejo-runner).
- Pre-existing integration test failures fixed: race in parallel executor
  (atomic counters); TestSetupEnv / command_test / expression_test /
  run_context_test updated to match gitea fork runtime; TestJobExecutor and
  TestActionCache gated on `testing.Short()`.

Full `gitea/act` commit history is reachable via the second parent.

Co-Authored-By: Claude (Opus 4.7) <noreply@anthropic.com>
2026-04-22 22:29:06 +02:00
Pascal Zimmermann
15dd63a839 feat: Add support for Dynamic Matrix Strategies with Job Outputs (#149)
# Matrix Strategy: Support for Scalar Values and Template Expressions

## 🎯 Overview

This Pull Request implements support for **scalar values and template expressions** in matrix strategies. Previously, every matrix value had to be declared as a YAML array — a bare scalar like `os: ubuntu-latest` or `version: ${{ fromJSON(...) }}` caused silent failures. Now, scalars are automatically wrapped into single-element arrays, enabling the `${{ fromJSON(...) }}` pattern that is commonly used in GitHub Actions workflows for dynamic matrix generation.

## ⚠️ Semantic Changes

Two subtle behavior changes are introduced that are worth understanding:

### 1. Scalar matrix values now produce a 1-element matrix expansion

**Before this PR:** A scalar value like

```yaml
strategy:
  matrix:
    os: ubuntu-latest   # no brackets
```

failed to decode as `map[string][]interface{}`, so `Matrix()` returned `nil`. The job ran **once, without any matrix context** (`matrix.os` was undefined).

**After this PR:** The same input is wrapped to `["ubuntu-latest"]`. The job runs **one matrix iteration** with `matrix.os = "ubuntu-latest"` set.

> **Impact on existing workflows:** Workflows that accidentally used bare scalars instead of arrays will now run with a matrix context where they previously did not. The functional result is usually the same, but `matrix.*` variables are now populated. This is considered an improvement, but is a semantic change.

---

### 2. Unevaluated template expressions fall back to a literal string value

The actual resolution of `${{ fromJSON(needs.job1.outputs.versions) }}` into an array happens **before** `Matrix()` is called — in `EvaluateYamlNode` inside `pkg/runner/runner.go`. This PR does not change that evaluation path.

If evaluation succeeds → the YAML node already contains a proper array → `Matrix()` decodes it normally.

If evaluation **fails or is skipped** (e.g., expression not yet resolved) → the scalar string is now wrapped as `["${{ fromJSON(...) }}"]` → the job runs **one iteration with the literal unexpanded string** as the matrix value.

> **Note:** The test `TestJobMatrix/matrix_with_template_expression` covers this fallback path, not the successful resolution path. The literal-wrap behavior is the fallback, not the primary mechanism for template expression support.

---

## 🔑 Key Changes

### 1. Flexible Matrix Decoding (`pkg/model/workflow.go`)

#### New Helper Function: `normalizeMatrixValue`
```go
func normalizeMatrixValue(key string, val interface{}) ([]interface{}, error)
```
- Converts a single matrix value to the expected array format
- Handles three cases:
  1. **Arrays**: Pass through unchanged
  2. **Scalars**: Wrap in single-element array (including template expressions)
  3. **Invalid types** (maps, etc.): Return error to detect misconfigurations early

**Supported scalar types:**
- `string` — including unevaluated template expressions
- `int`, `float64` — numeric values
- `bool` — boolean values
- `nil` — null values

#### Enhanced `Matrix()` Method

**Before:**
```go
func (j *Job) Matrix() map[string][]interface{} {
    if j.Strategy.RawMatrix.Kind == yaml.MappingNode {
        var val map[string][]interface{}
        if !decodeNode(j.Strategy.RawMatrix, &val) {
            return nil
        }
        return val
    }
    return nil
}
```

**After:**
```go
func (j *Job) Matrix() map[string][]interface{} {
    if j.Strategy == nil || j.Strategy.RawMatrix.Kind != yaml.MappingNode {
        return nil
    }

    // First decode to flexible map[string]interface{} to handle scalars and arrays
    var flexVal map[string]interface{}
    err := j.Strategy.RawMatrix.Decode(&flexVal)
    if err != nil {
        // If that fails, try the strict array format
        var val map[string][]interface{}
        if !decodeNode(j.Strategy.RawMatrix, &val) {
            return nil
        }
        return val
    }

    // Convert flexible format to expected format with validation
    val := make(map[string][]interface{})
    for k, v := range flexVal {
        normalized, err := normalizeMatrixValue(k, v)
        if err != nil {
            log.Errorf("matrix validation error: %v", err)
            return nil
        }
        val[k] = normalized
    }
    return val
}
```

**Key improvements:**
- Handles unevaluated template expressions as scalar strings
- Automatic conversion to array format for consistent processing
- Validation to catch nested maps and invalid configurations
- Fallback to strict array format decoding for backward compatibility
- Early nil check for strategy to prevent nil pointer panics

---

## 🎁 Use Cases

### Example 1: Template Expression in Matrix (primary motivation)
```yaml
jobs:
  setup:
    runs-on: ubuntu-latest
    outputs:
      versions: ${{ steps.version-map.outputs.versions }}
    steps:
      - id: version-map
        run: echo "versions=[\"1.17\",\"1.18\",\"1.19\"]" >> $GITHUB_OUTPUT

  test:
    needs: setup
    strategy:
      matrix:
        # EvaluateYamlNode resolves this to an array before Matrix() is called
        version: ${{ fromJSON(needs.setup.outputs.versions) }}
    runs-on: ubuntu-latest
    steps:
      - run: echo "Testing version ${{ matrix.version }}"
```

### Example 2: Mixed Scalar and Array Values
```yaml
jobs:
  build:
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest]  # Array — unchanged
        go-version: 1.19                      # Scalar — wrapped to [1.19]
        node: [14, 16, 18]                    # Array — unchanged
    runs-on: ${{ matrix.os }}
```

### Example 3: Single Value Matrix
```yaml
jobs:
  deploy:
    strategy:
      matrix:
        environment: production  # Scalar — wrapped to ["production"]
    runs-on: ubuntu-latest
```

---

##  Benefits

-  **Feature Parity with GitHub Actions**: Supports template expressions in matrix strategies
- 🔒 **Robust Validation**: Detects misconfigured matrices (nested maps) early
- 🔄 **Backward Compatible**: Existing array-based workflows continue to work
- 📝 **Clear Error Messages**: Helpful validation messages for debugging
- 🛡️ **Type Safety**: Handles all YAML scalar types correctly

---

## 🚀 Breaking Changes

**Technically none in a strictly backward-incompatible sense**, but two semantic changes exist (see [⚠️ Semantic Changes](#️-semantic-changes-read-before-merging) above):

1. Scalar matrix values now iterate (previously they caused a `nil` matrix / no iteration)
2. Unevaluated template strings now produce a literal iteration instead of skipping

Both changes are improvements in practice, but downstream users should be aware.

---

## 📚 Related Work

- Related Gitea Server PR: [go-gitea/gitea#36564](https://github.com/go-gitea/gitea/pull/36564)
- Enables dynamic matrix generation from job outputs
- Supports advanced CI/CD patterns used in GitHub Actions

## 🔗 References

- [GitHub Actions: Matrix strategies](https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs)
- [GitHub Actions: Expression syntax](https://docs.github.com/en/actions/learn-github-actions/expressions)
- [GitHub Actions: `fromJSON` in matrix context](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstrategymatrix)

Reviewed-on: https://gitea.com/gitea/act/pulls/149
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: Pascal Zimmermann <pascal.zimmermann@theiotstudio.com>
Co-committed-by: Pascal Zimmermann <pascal.zimmermann@theiotstudio.com>
2026-04-19 22:36:34 +00:00
Bo-Yi Wu
9aafec169b perf: use single poller with semaphore-based capacity control (#822)
## Background

#819 replaced the shared `rate.Limiter` with per-worker exponential backoff counters to add jitter and adaptive polling. Before #819, the poller used:

```go
limiter := rate.NewLimiter(rate.Every(p.cfg.Runner.FetchInterval), 1)
```

This limiter was **shared across all N polling goroutines with burst=1**, effectively serializing their `FetchTask` calls — so even with `capacity=60`, the runner issued roughly one `FetchTask` per `FetchInterval` total.

#819 replaced this with independent per-worker `consecutiveEmpty` / `consecutiveErrors` counters. Each goroutine now backs off **independently**, which inadvertently removed the cross-worker serialization. With `capacity=N`, the runner now has N goroutines each polling on their own schedule — a regression from the pre-#819 baseline for any runner with `capacity > 1`.

(Thanks to @ChristopherHX for catching this in review.)

## Problem

With the post-#819 code:

- `capacity=N` maintains **N persistent polling goroutines**, each calling `FetchTask` independently
- At idle, N goroutines each wake up and send a `FetchTask` RPC per `FetchInterval`
- At full load, N goroutines **continue polling** even though no slot is available to run a new task — every one of those RPCs is wasted
- The `Shutdown()` timeout branch has a pre-existing bug: the "non-blocking check" is actually a blocking receive, so `shutdownJobs()` is never reached on timeout

## Real-World Impact: 3 Runners × capacity=60

Current production environment: 3 runners each with `capacity=60`.

| Metric | Post-#819 (current) | This PR | Reduction |
|--------|---------------------|---------|-----------|
| Polling goroutines (total) | 3 × 60 = **180** | 3 × 1 = **3** | **98.3%** (177 fewer) |
| FetchTask RPCs per poll cycle (idle) | **180** | **3** | **98.3%** |
| FetchTask RPCs per poll cycle (full load) | **180** (all wasted) | **0** (blocked on semaphore) | **100%** |
| Concurrent connections to Gitea | **180** | **3** | **98.3%** |
| Backoff state objects | 180 (per-worker) | 3 (one per runner) | Simplified |

### Idle scenario

All 180 goroutines wake up every `FetchInterval`, each sending a `FetchTask` RPC that returns empty. Server handles 180 RPCs per cycle for zero useful work. After this PR: **3 RPCs per cycle** — one per runner.

> Note: pre-#819 idle behavior was already ~3 RPCs/cycle due to the shared `rate.Limiter`. This PR restores that property while also addressing the full-load case below.

### Full-load scenario (all 180 slots occupied)

All 180 goroutines **continue polling** even though no slot is available. Every RPC is wasted. After this PR: all 3 pollers are **blocked on the semaphore** — **zero RPCs** until a task completes.

> This is a scenario neither the pre-#819 shared limiter nor the post-#819 per-worker backoff handles — both still issue `FetchTask` RPCs when no slot is free. The semaphore is the only approach of the three that ties polling to available capacity.

## Why Not Just Revert to `rate.Limiter`?

Reverting would restore the serialized behavior but is not the right long-term fix:

- **`rate.Limiter` has no concept of available capacity.** At full load it still hands out tokens and issues `FetchTask` RPCs that can't be acted on. The semaphore blocks polling entirely in that case — zero wasted RPCs.
- **It composes poorly with adaptive backoff from #819.** A shared limiter and per-worker backoff pull in different directions.
- **N goroutines serializing on a shared limiter means N-1 of them exist only to wait in line.** A single poller expresses the same behavior more directly.

The semaphore approach ties polling to capacity explicitly: `acquire slot → fetch → dispatch → release`. That invariant becomes structural rather than emergent from a rate limiter.

## Solution

Replace N polling goroutines with a **single polling loop** that uses a buffered channel as a semaphore to control concurrent task execution:

```go
// New: poller.go Poll()
sem := make(chan struct{}, p.cfg.Runner.Capacity)
for {
    select {
    case sem <- struct{}{}:       // Acquire slot (blocks at capacity)
    case <-p.pollingCtx.Done():
        return
    }
    task, ok := p.fetchTask(...)  // Single FetchTask RPC
    if !ok {
        <-sem                     // Release slot on empty response
        // backoff...
        continue
    }
    go func(t *runnerv1.Task) {   // Dispatch task
        defer func() { <-sem }()  // Release slot when done
        p.runTaskWithRecover(p.jobsCtx, t)
    }(task)
}
```

The exponential backoff and jitter from #819 are preserved — just driven by a single `workerState` instead of N per-worker states.

## Shutdown Bug Fix

Fixed a pre-existing bug in `Shutdown()` where the timeout branch could never force-cancel running jobs:

```go
// Before (BROKEN): blocking receive, shutdownJobs() never reached
_, ok := <-p.done   // blocks until p.done is closed
if !ok { return nil }
p.shutdownJobs()    // dead code when jobs are still running

// After (FIXED): proper non-blocking check
select {
case <-p.done:
    return nil
default:
}
p.shutdownJobs()    // now correctly reached on timeout
```

## Code Changes

| Area | Detail |
|------|--------|
| `Poller.runner` | `*run.Runner` → `TaskRunner` interface (enables mock-based testing) |
| `Poll()` | N goroutines → single loop with buffered-channel semaphore |
| `PollOnce()` | Inlined from removed `pollOnce()` |
| `waitBackoff()` | New helper, eliminates duplicated backoff logic |
| `resetBackoff()` | New method on `workerState`, also resets stale `lastBackoff` metric |
| `Shutdown()` | Fixed blocking receive → proper non-blocking select |
| Removed | `poll()`, `pollOnce()` private methods (-2 methods, -42 lines) |

## Test Coverage

Added `TestPoller_ConcurrencyLimitedByCapacity` which verifies:

- With `capacity=3`, at most 3 tasks execute concurrently (`maxConcurrent <= 3`)
- Tasks actually overlap in execution (`maxConcurrent >= 2`)
- `FetchTask` is never called concurrently — confirms single poller (`maxFetchConcur == 1`)
- All 6 tasks complete successfully (`totalCompleted == 6`)
- Mock runner respects context cancellation, enabling shutdown path verification

```
=== RUN   TestPoller_ConcurrencyLimitedByCapacity
--- PASS: TestPoller_ConcurrencyLimitedByCapacity (0.10s)
PASS
ok  	gitea.com/gitea/act_runner/internal/app/poll	0.59s
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/822
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-committed-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-04-19 08:10:23 +00:00
silverwind
f923badec7 Use golangci-lint fmt to format code (#163)
Use `golangci-lint fmt` to format code, upgrading `.golangci.yml` to v2 and mirroring the linter configuration used by https://github.com/go-gitea/gitea. `gci` now handles import ordering into standard, project-local, blank, and default groups.

Mirrors https://github.com/go-gitea/gitea/pull/37194.

Changes:
- Upgrade `.golangci.yml` to v2 format with the same linter set as gitea (minus `prealloc`, `unparam`, `testifylint`, `nilnil` which produced too many pre-existing issues)
- Add path-based exclusions (`bodyclose`, `gosec` in tests; `gosec:G115`/`G117` globally)
- Run lint via `make lint-go` in CI instead of `golangci/golangci-lint-action`, matching the pattern used by other Gitea repos
- Apply safe auto-fixes (`modernize`, `perfsprint`, `usetesting`, etc.)
- Add explanations to existing `//nolint` directives
- Remove dead code (unused `newRemoteReusableWorkflow` and `networkName`), duplicate imports, and shadowed `max` builtins
- Replace deprecated `docker/distribution/reference` with `distribution/reference`
- Fix `Deprecated:` comment casing and simplify nil/len checks

---
This PR was written with the help of Claude Opus 4.7

Reviewed-on: https://gitea.com/gitea/act/pulls/163
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
2026-04-18 09:10:09 +00:00
silverwind
48944e136c Use golangci-lint fmt to format code (#823)
Use `golangci-lint fmt` to format code, replacing the previous gofumpt-based formatter. https://github.com/daixiang0/gci is used to order the imports.

Also drops the `gitea-vet` dependency since `gci` now handles import ordering.

Mirrors https://github.com/go-gitea/gitea/pull/37194.

---
This PR was written with the help of Claude Opus 4.7

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/823
Reviewed-by: Nicolas <173651+bircni@noreply.gitea.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
2026-04-17 22:05:01 +00:00
Bo-Yi Wu
40dcee0991 chore(deps): upgrade golangci-lint from v2.10.1 to v2.11.4 (#821)
## Summary
- Bump golangci-lint from v2.10.1 to v2.11.4
- Remove unused `//nolint:revive` directive on metrics package declaration (detected by stricter nolintlint in new version)

## Changes between v2.10.1 and v2.11.4
- **v2.11.0** — Multiple linter dependency upgrades, Go 1.26 support
- **v2.11.2** — Bug fix for `fmt` with path
- **v2.11.3** — gosec update
- **v2.11.4** — Dependency updates (sqlclosecheck, noctx, etc.)

No breaking changes.

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/821
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-committed-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-04-15 03:56:34 +00:00
Bo-Yi Wu
f33e5a6245 feat: add Prometheus metrics endpoint for runner observability (#820)
## What

Add an optional Prometheus `/metrics` HTTP endpoint to `act_runner` so operators can observe runner health, polling behavior, job outcomes, and RPC latency without scraping logs.

New surface:

- `internal/pkg/metrics/metrics.go` — metric definitions, custom `Registry`, static Go/process collectors, label constants, `ResultToStatusLabel` helper.
- `internal/pkg/metrics/server.go` — hardened `http.Server` serving `/metrics` and `/healthz` with Slowloris-safe timeouts (`ReadHeaderTimeout` 5s, `ReadTimeout`/`WriteTimeout` 10s, `IdleTimeout` 60s) and a 5s graceful shutdown.
- `daemon.go` wires it up behind `cfg.Metrics.Enabled` (disabled by default).
- `poller.go` / `reporter.go` / `runner.go` instrument their existing hot paths with counters/histograms/gauges — no behavior change.

Metrics exported (namespace `act_runner_`):

| Subsystem | Metric | Type | Labels |
|---|---|---|---|
| — | `info` | Gauge | `version`, `name` |
| — | `capacity`, `uptime_seconds` | Gauge | — |
| `poll` | `fetch_total`, `client_errors_total` | Counter | `result` / `method` |
| `poll` | `fetch_duration_seconds`, `backoff_seconds` | Histogram / Gauge | — |
| `job` | `total` | Counter | `status` |
| `job` | `duration_seconds`, `running`, `capacity_utilization_ratio` | Histogram / GaugeFunc | — |
| `report` | `log_total`, `state_total` | Counter | `result` |
| `report` | `log_duration_seconds`, `state_duration_seconds` | Histogram | — |
| `report` | `log_buffer_rows` | Gauge | — |
| — | `go_*`, `process_*` | standard collectors | — |

All label values are predefined constants — **no high-cardinality labels** (no task IDs, repo URLs, branches, tokens, or secrets) so scraping is safe and bounded.

## Why

Teams self-hosting Gitea + `act_runner` at scale need to answer basic SRE questions that are currently invisible:

- How often are RPCs failing? Which RPC? (`act_runner_client_errors_total`)
- Are runners saturated? (`act_runner_job_capacity_utilization_ratio`, `act_runner_job_running`)
- How long do jobs take? (`act_runner_job_duration_seconds`)
- Is polling backing off? (`act_runner_poll_backoff_seconds`, `act_runner_poll_fetch_total{result=\"error\"}`)
- Are log/state reports slow? (`act_runner_report_{log,state}_duration_seconds`)
- Is the log buffer draining? (`act_runner_report_log_buffer_rows`)

Today operators have to grep logs. This PR makes all of the above first-class metrics so they can feed dashboards and alerts (`rate(act_runner_client_errors_total[5m]) > 0.1`, capacity saturation alerts, etc.).

The endpoint is **disabled by default** and binds to `127.0.0.1:9101` when enabled, so it's opt-in and safe for existing deployments.

## How

### Config

```yaml
metrics:
  enabled: false           # opt-in
  addr: 127.0.0.1:9101     # change to 0.0.0.0:9101 only behind a reverse proxy
```

`config.example.yaml` documents both fields plus a security note about binding externally without auth.

### Wiring

1. `daemon.go` calls `metrics.Init()` (guarded by `sync.Once`), sets `act_runner_info`, `act_runner_capacity`, registers uptime + running-jobs GaugeFuncs, then starts the server goroutine with the daemon context — it shuts down cleanly on `ctx.Done()`.
2. `poller.fetchTask` observes RPC latency / result / error counters. `DeadlineExceeded` (long-poll idle) is treated as an empty result and **not** observed into the histogram so the 5s timeout doesn't swamp the buckets.
3. `poller.pollOnce` reports `poll_backoff_seconds` using the pre-jitter base interval (the true backoff level), and only when it changes — prevents noisy no-op gauge updates at the `FetchIntervalMax` plateau.
4. `reporter.ReportLog` / `ReportState` record duration histograms and success/error counters; `log_buffer_rows` is updated only when the value changes, guarded by the already-held `clientM`.
5. `runner.Run` observes `job_duration_seconds` and increments `job_total` by outcome via `metrics.ResultToStatusLabel`.

### Safety / security review

- All timeouts set; Slowloris-safe.
- Custom `prometheus.NewRegistry()` — no global registration side-effects.
- No sensitive data in labels (reviewed every instrumentation site).
- Single new dependency: `github.com/prometheus/client_golang v1.23.2`.
- Endpoint is unauthenticated by design and documented as such; default localhost bind mitigates exposure. Operators exposing externally should front it with a reverse proxy.

## Verification

### Unit tests

\`\`\`bash
go build ./...
go vet ./...
go test ./...
\`\`\`

### Manual smoke test

1. Enable metrics in `config.yaml`:
   \`\`\`yaml
   metrics:
     enabled: true
     addr: 127.0.0.1:9101
   \`\`\`
2. Start the runner against a Gitea instance: \`./act_runner daemon\`.
3. Scrape the endpoint:
   \`\`\`bash
   curl -s http://127.0.0.1:9101/metrics | grep '^act_runner_'
   curl -s http://127.0.0.1:9101/healthz   # → ok
   \`\`\`
4. Confirm the static series appear immediately: \`act_runner_info\`, \`act_runner_capacity\`, \`act_runner_uptime_seconds\`, \`act_runner_job_running\`, \`act_runner_job_capacity_utilization_ratio\`.
5. Trigger a workflow and confirm counters increment: \`act_runner_poll_fetch_total{result=\"task\"}\`, \`act_runner_job_total{status=\"success\"}\`, \`act_runner_report_log_total{result=\"success\"}\`.
6. Leave the runner idle and confirm \`act_runner_poll_backoff_seconds\` settles (and does **not** churn on every poll).
7. Ctrl-C and confirm a clean \"metrics server shutdown\" log line (no port-in-use error on restart within 5s).

### Prometheus integration

Add to \`prometheus.yml\`:

\`\`\`yaml
scrape_configs:
  - job_name: act_runner
    static_configs:
      - targets: ['127.0.0.1:9101']
\`\`\`

Sample alert to try:

\`\`\`
sum(rate(act_runner_client_errors_total[5m])) by (method) > 0.1
\`\`\`

## Out of scope (follow-ups)

- TLS and auth on the metrics endpoint (mitigated today by localhost default; add when operators need external scraping).
- Per-task labels (intentionally avoided for cardinality safety).

---

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/820
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-committed-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-04-15 01:27:34 +00:00
Bo-Yi Wu
f2d545565f perf: reduce runner-to-server connection load with adaptive reporting and polling (#819)
## Summary

Many teams self-host Gitea + Act Runner at scale. The current runner design causes excessive HTTP requests to the Gitea server, leading to high server load. This PR addresses three root causes: aggressive fixed-interval polling, per-task status reporting every 1 second regardless of activity, and unoptimized HTTP client configuration.

## Problem

The original architecture has these issues:

**1. Fixed 1-second reporting interval (RunDaemon)**

- Every running task calls ReportLog + ReportState every 1 second (2 HTTP requests/sec/task)
- These requests are sent even when there are no new log rows or state changes
- With 200 runners × 3 tasks each = **1,200 req/sec just for status reporting**

**2. Fixed 2-second polling interval (no backoff)**

- Idle runners poll FetchTask every 2 seconds forever, even when no jobs are queued
- No exponential backoff or jitter — all runners can synchronize after network recovery (thundering herd)
- 200 idle runners = **100 req/sec doing nothing useful**

**3. HTTP client not tuned**

- Uses http.DefaultClient with MaxIdleConnsPerHost=2, causing frequent TCP/TLS reconnects
- Creates two separate http.Client instances (one for Ping, one for Runner service) instead of sharing

**Total: ~1,300 req/sec for 200 runners with 3 tasks each**

## Solution

### Adaptive Event-Driven Log Reporting

Replace the recursive `time.AfterFunc(1s)` pattern in RunDaemon with a goroutine-based select event loop using three trigger mechanisms:

| Trigger | Default | Purpose |
|---------|---------|---------|
| `log_report_max_latency` | 3s | Guarantee even a single log line is delivered within this time |
| `log_report_interval` | 5s | Periodic sweep — steady-state cadence |
| `log_report_batch_size` | 100 rows | Immediate flush during bursty output (e.g., npm install) |

**Key design**: `log_report_max_latency` (3s) must be less than `log_report_interval` (5s) so the max-latency timer fires before the periodic ticker for single-line scenarios.

State reporting is decoupled to its own `state_report_interval` (default 5s), with immediate flush on step transitions (start/stop) via a stateNotify channel for responsive frontend UX.

Additionally:
- Skip ReportLog when `len(rows) == 0` (no pending log rows)
- Skip ReportState when `stateChanged == false && len(outputs) == 0` (nothing changed)
- Move expensive `proto.Clone` after the early-return check to avoid deep copies on no-op paths

### Polling Backoff with Jitter

Replace fixed `rate.Limiter` with adaptive exponential backoff:
- Track `consecutiveEmpty` and `consecutiveErrors` counters
- Interval doubles with each empty/error response: `base × 2^(n-1)`, capped at `fetch_interval_max` (default 60s)
- Add ±20% random jitter to prevent thundering herd
- Fetch first, sleep after ��� preserves burst=1 behavior for immediate first fetch on startup and after task completion

### HTTP Client Tuning

- Configure custom `http.Transport` with `MaxIdleConnsPerHost=10` (was 2)
- Share a single `http.Client` between PingService and RunnerService
- Add `IdleConnTimeout=90s` for clean connection lifecycle

## Load Reduction

For 200 runners × 3 tasks (70% with active log output):

| Component | Before | After | Reduction |
|-----------|--------|-------|-----------|
| Polling (idle) | 100 req/s | ~3.4 req/s | 97% |
| Log reporting | 420 req/s | ~84 req/s | 80% |
| State reporting | 126 req/s | ~25 req/s | 80% |
| **Total** | **~1,300 req/s** | **~113 req/s** | **~91%** |

## Frontend UX Impact

| Scenario | Before | After | Notes |
|----------|--------|-------|-------|
| Continuous output (npm install) | ~1s | ~5s | Periodic ticker sweep |
| Single line then silence | ~1s | ≤3s | maxLatencyTimer guarantee |
| Bursty output (100+ lines) | ~1s | <1s | Batch size immediate flush |
| Step start/stop | ~1s | <1s | stateNotify immediate flush |
| Job completion | ~1s | ~1s | Close() retry unchanged |

## New Configuration Options

All have safe defaults — existing config files need no changes:

```yaml
runner:
  fetch_interval_max: 60s        # Max backoff interval when idle
  log_report_interval: 5s        # Periodic log flush interval
  log_report_max_latency: 3s     # Max time a log row waits (must be < log_report_interval)
  log_report_batch_size: 100     # Immediate flush threshold
  state_report_interval: 5s      # State flush interval (step transitions are always immediate)
```

Config validation warns on invalid combinations:
- `fetch_interval_max < fetch_interval` → auto-corrected
- `log_report_max_latency >= log_report_interval` → warning (timer would be redundant)

## Test Plan

- [x] `go build ./...` passes
- [x] `go test ./...` passes (all existing + 3 new tests)
- [x] `golangci-lint run` — 0 issues
- [x] TestReporter_MaxLatencyTimer — verifies single log line flushed by maxLatencyTimer before logTicker
- [x] TestReporter_BatchSizeFlush — verifies batch size threshold triggers immediate flush
- [x] TestReporter_StateNotifyFlush — verifies step transition triggers immediate state flush
- [x] TestReporter_EphemeralRunnerDeletion — verifies Close/RunDaemon race safety
- [x] TestReporter_RunDaemonClose_Race — verifies concurrent Close safety

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/819
Reviewed-by: Nicolas <173651+bircni@noreply.gitea.com>
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-committed-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-04-14 11:29:25 +00:00
Lunny Xiao
90c1275f0e Upgrade yaml (#816)
~wait https://gitea.com/gitea/act/pulls/157~

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/816
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
2026-03-28 16:18:47 +00:00
Lunny Xiao
3232358e71 downgrade yaml from rc.4 to rc.3 and upgrade action lint (#158)
Reviewed-on: https://gitea.com/gitea/act/pulls/158
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
2026-03-28 04:13:07 +00:00
Lunny Xiao
2e98baa34a Upgrade yaml (#157)
Reviewed-on: https://gitea.com/gitea/act/pulls/157
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
2026-03-28 03:31:27 +00:00
Zettat123
505907eb2a Add run_attempt to context (#632)
Blocked by https://gitea.com/gitea/act/pulls/126
Fix https://github.com/go-gitea/gitea/issues/33135

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/632
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2026-03-26 20:07:22 +00:00
silverwind
9933ea0d92 feat: add configurable bind_workdir option with workspace cleanup for DinD setups (#810)
## Summary

Adds a `container.bind_workdir` config option that exposes the nektos/act `BindWorkdir` setting. When enabled, workspaces are bind-mounted from the host filesystem instead of Docker volumes, which is required for DinD setups where jobs use `docker compose` with bind mounts (e.g. `.:/app`).

Each job gets an isolated workspace at `/workspace/<task_id>/<owner>/<repo>` to prevent concurrent jobs from the same repo interfering with each other. The task directory is cleaned up after job execution.

### Configuration

```yaml
container:
  bind_workdir: true
```

When using this with DinD, also mount the workspace parent into the runner container and add it to `valid_volumes`:
```yaml
container:
  valid_volumes:
    - /workspace/**
```

*This PR was authored by Claude (AI assistant)*

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/810
Reviewed-by: ChristopherHX <38043+christopherhx@noreply.gitea.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
2026-03-03 10:15:06 +00:00
RoboMagus
5dd5436169 Semver tags for Docker images (#720)
The main Gitea docker image is already distributed with proper semver tags, such that users can pin to e.g. the minor version and still pull in patch releases.  This is something that has been lacking on the act runner images.

This PR expands the docker image tag versioning strategy such that when `v0.2.13` is released the following image tags are produced:

Basic:
 - `0`
 - `0.2`
 - `0.2.13`
 - `latest`

DinD:
 - `0-dind`
 - `0.2-dind`
 - `0.2.13-dind`
 - `latest-dind`

DinD-Rootless:
 - `0-dind-rootless`
 - `0.2-dind-rootless`
 - `0.2.13-dind-rootless`
 - `latest-dind-rootless`

To verify the `docker/metadata-action` produces the expected results in a Gitea workflow environment I've executed a release workflow. Results can be seen in [this run](https://gitea.com/RoboMagus/gitea_act_runner/actions/runs/14). (Note though that as the repository name of my fork changed from `act_runner` to `gitea_act_runner` this is reflected in the produced docker tags in this test run!)

---------

Co-authored-by: RoboMagus <68224306+RoboMagus@users.noreply.github.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: techknowlogick <techknowlogick@noreply.gitea.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/720
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: RoboMagus <robomagus@noreply.gitea.com>
Co-committed-by: RoboMagus <robomagus@noreply.gitea.com>
2026-02-25 19:09:53 +00:00
Lunny Xiao
28740d7788 Upgrade go mod (#154)
Reviewed-on: https://gitea.com/gitea/act/pulls/154
Reviewed-by: silverwind <silverwind@noreply.gitea.com>
2026-02-22 20:39:43 +00:00
Lunny Xiao
ddf9159a8f Remove jobparser because of moving to Gitea main project (#155)
Reviewed-on: https://gitea.com/gitea/act/pulls/155
Reviewed-by: silverwind <silverwind@noreply.gitea.com>
Reviewed-by: ChristopherHX <christopherhx@noreply.gitea.com>
2026-02-22 19:04:16 +00:00
silverwind
43e6958fa3 Recover from panics in parallel executor workers (#153)
The worker goroutines in `NewParallelExecutor` had no panic recovery. A panic in any executor (e.g. expression evaluation) would crash the entire runner process, leaving all steps stuck in the UI because the final status was never reported back.

Add `defer`/`recover` in the worker goroutines to convert panics into errors that propagate through the normal error channel.

Issue is present in latest `nektos/act` as well.

Fixes https://gitea.com/gitea/act_runner/issues/371

(Partially generated by Claude)

Reviewed-on: https://gitea.com/gitea/act/pulls/153
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
2026-02-18 03:29:07 +00:00
Pascal Zimmermann
c0f19d9a26 fix: Max parallel Support for Matrix Jobs and Remote Action Tests (#150)
## Summary

This PR fixes the `max-parallel` strategy configuration for matrix jobs and resolves all failing tests in `step_action_remote_test.go`. The implementation ensures that matrix jobs respect the `max-parallel` setting, preventing resource exhaustion when running GitHub Actions workflows.

## Problem Statement

### Issue 1: max-parallel Not Working Correctly
Matrix jobs were running in parallel regardless of the `max-parallel` setting in the strategy configuration. This caused:
- Resource contention on limited runners
- Unpredictable job execution behavior
- Inability to control concurrency for resource-intensive workflows

### Issue 2: Failing Remote Action Tests
All tests in `step_action_remote_test.go` were failing due to:
- Missing `ActionCacheDir` configuration
- Incorrect mock expectations using fixed strings instead of hash-based paths
- Incompatibility with the hash-based action cache implementation

## Changes

### 1. max-parallel Implementation (`pkg/runner/runner.go`)

#### Robust Initialization
Added fallback logic to ensure `MaxParallel` is always properly initialized:
```go
if job.Strategy.MaxParallel == 0 {
    job.Strategy.MaxParallel = job.Strategy.GetMaxParallel()
}
```

#### Eliminated Unnecessary Nesting
Fixed inefficient nested parallelization when only one pipeline element exists:
```go
if len(pipeline) == 1 {
    // Execute directly without additional wrapper
    log.Debugf("Single pipeline element, executing directly")
    return pipeline[0](ctx)
}
```

#### Enhanced Logging
Added comprehensive debug and info logging:
- Shows which `maxParallel` value is being used
- Logs adjustments based on matrix size
- Reports final parallelization decisions

### 2. Worker Logging (`pkg/common/executor.go`)

Enhanced `NewParallelExecutor` with detailed worker activity logging:
```go
log.Infof("NewParallelExecutor: Creating %d workers for %d executors", parallel, len(executors))

for i := 0; i < parallel; i++ {
    go func(workerID int, work <-chan Executor, errs chan<- error) {
        log.Debugf("Worker %d started", workerID)
        taskCount := 0
        for executor := range work {
            taskCount++
            log.Debugf("Worker %d executing task %d", workerID, taskCount)
            errs <- executor(ctx)
        }
        log.Debugf("Worker %d finished (%d tasks executed)", workerID, taskCount)
    }(i, work, errs)
}
```

**Benefits:**
- Easy verification of correct parallelization
- Better debugging capabilities
- Clear visibility into worker activity

### 3. Documentation (`pkg/model/workflow.go`)

Added clarifying comment to ensure strategy values are always set:
```go
// Always set these values, even if there's an error later
j.Strategy.FailFast = j.Strategy.GetFailFast()
j.Strategy.MaxParallel = j.Strategy.GetMaxParallel()
```

### 4. Test Fixes (`pkg/runner/step_action_remote_test.go`)

#### Added Missing Configuration
All tests now include `ActionCacheDir`:
```go
Config: &Config{
    GitHubInstance: "github.com",
    ActionCacheDir: "/tmp/test-cache",
}
```

#### Hash-Based Suffix Matchers
Updated mocks to use hash-based paths instead of fixed strings:
```go
// Before
sarm.On("readAction", sar.Step, suffixMatcher("org-repo-path@ref"), ...)

// After
sarm.On("readAction", sar.Step, suffixMatcher(sar.Step.UsesHash()), ...)
```

#### Flexible Exec Matcher for Post Tests
Implemented flexible path matching for hash-based action directories:
```go
execMatcher := mock.MatchedBy(func(args []string) bool {
    if len(args) != 2 {
        return false
    }
    return args[0] == "node" && strings.Contains(args[1], "post.js")
})
```

#### Token Test Improvements
- Uses unique cache directory to force cloning
- Validates URL redirection to github.com
- Accepts realistic token behavior

### 5. New Tests

#### Unit Tests (`pkg/runner/max_parallel_test.go`)
Tests various `max-parallel` configurations:
- `max-parallel: 1` → Sequential execution
- `max-parallel: 2` → Max 2 parallel jobs
- `max-parallel: 4` (default) → Max 4 parallel jobs
- `max-parallel: 10` → Max 10 parallel jobs

#### Concurrency Test (`pkg/common/executor_max_parallel_test.go`)
Verifies that `max-parallel: 2` actually limits concurrent execution using atomic counters.

## Expected Behavior

### Before
-  Jobs ran in parallel regardless of `max-parallel` setting
-  Unnecessary nested parallelization (8 workers for 1 element)
-  No logging to debug parallelization issues
-  All `step_action_remote_test.go` tests failing

### After
-  `max-parallel: 1` → Jobs run strictly sequentially
-  `max-parallel: N` → Maximum N jobs run simultaneously
-  Efficient single-level parallelization for matrix jobs
-  Comprehensive logging for debugging
-  All tests passing (10/10)

## Example Log Output

With `max-parallel: 2` and 6 matrix jobs:
```
[DEBUG] Using job.Strategy.MaxParallel: 2
[INFO] Running job with maxParallel=2 for 6 matrix combinations
[DEBUG] Single pipeline element, executing directly
[INFO] NewParallelExecutor: Creating 2 workers for 6 executors
[DEBUG] Worker 0 started
[DEBUG] Worker 1 started
[DEBUG] Worker 0 executing task 1
[DEBUG] Worker 1 executing task 1
...
[DEBUG] Worker 0 finished (3 tasks executed)
[DEBUG] Worker 1 finished (3 tasks executed)
```

## Test Results

All tests pass successfully:
```
 TestStepActionRemote (3 sub-tests)
 TestStepActionRemotePre
 TestStepActionRemotePreThroughAction
 TestStepActionRemotePreThroughActionToken
 TestStepActionRemotePost (4 sub-tests)
 TestMaxParallelStrategy
 TestMaxParallel2Quick

Total: 12/12 tests passing
```

## Breaking Changes

None. This PR is fully backward compatible. All changes improve existing behavior without altering the API.

## Impact

-  Fixes resource management for CI/CD pipelines
-  Prevents runner exhaustion on limited infrastructure
-  Enables sequential execution for resource-intensive jobs
-  Improves debugging with detailed logging
-  Ensures test suite reliability

## Files Modified

### Core Implementation
- `pkg/runner/runner.go` - max-parallel fix + logging
- `pkg/common/executor.go` - Worker logging
- `pkg/model/workflow.go` - Documentation

### Tests
- `pkg/runner/step_action_remote_test.go` - Fixed all 10 tests
- `pkg/runner/max_parallel_test.go` - **NEW** - Unit tests
- `pkg/common/executor_max_parallel_test.go` - **NEW** - Concurrency test

## Verification

### Manual Testing
```bash
# Build
go build -o dist/act main.go

# Test with max-parallel: 2
./dist/act -W test-max-parallel-2.yml -v

# Expected: Only 2 jobs run simultaneously
```

### Automated Testing
```bash
# Run all tests
go test ./pkg/runner -run TestStepActionRemote -v
go test ./pkg/runner -run TestMaxParallel -v
go test ./pkg/common -run TestMaxParallel -v
```

## Related Issues

Fixes issues where matrix jobs in Gitea ignored the `max-parallel` strategy setting, causing resource contention and unpredictable behavior.

Reviewed-on: https://gitea.com/gitea/act/pulls/150
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: silverwind <silverwind@noreply.gitea.com>
Co-authored-by: Pascal Zimmermann <pascal.zimmermann@theiotstudio.com>
Co-committed-by: Pascal Zimmermann <pascal.zimmermann@theiotstudio.com>
2026-02-11 00:43:38 +00:00
wrzooz
495185446f Fixed typo (#151)
Reviewed-on: https://gitea.com/gitea/act/pulls/151
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.com>
Co-authored-by: wrzooz <wrzooz@noreply.gitea.com>
Co-committed-by: wrzooz <wrzooz@noreply.gitea.com>
2026-01-22 08:09:32 +00:00
Christopher Homberger
3a07d231a0 Fix yaml with prefixed newline broken setjob + yaml v4 (#144)
* go-yaml v3 **and** v4 workaround
* avoid yaml.Marshal broken indention

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/act/pulls/144
Reviewed-by: wxiaoguang <wxiaoguang@noreply.gitea.com>
Reviewed-by: Zettat123 <zettat123@noreply.gitea.com>
Co-authored-by: Christopher Homberger <christopher.homberger@web.de>
Co-committed-by: Christopher Homberger <christopher.homberger@web.de>
2025-12-09 02:28:56 +00:00
Zettat123
5417d3ac67 Interpolate uses for remote reusable workflows (#145)
Related to #127

Reviewed-on: https://gitea.com/gitea/act/pulls/145
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: ChristopherHX <christopherhx@noreply.gitea.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2025-12-02 19:36:38 +00:00
Zettat123
f56fd693ee Add run_attempt to context (#126)
Fix https://github.com/go-gitea/gitea/issues/33135

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/act/pulls/126
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2025-11-14 20:14:23 +00:00
Zettat123
34f68b3c18 Support cloning actions and workflows from private repos (#123)
Related to https://github.com/go-gitea/gitea/pull/32562

Resolve https://gitea.com/gitea/act_runner/issues/102

To support using actions and workflows from private repositories, we need to enable act_runner to clone private repositories.
~~But it is not easy to know if a repository is private and whether a token is required when cloning. In this PR, I added a new option `RetryToken`. By default, token is empty. When cloning a repo returns an `authentication required` error, `act_runner` will try to clone the repo again using `RetryToken` as the token.~~

In this PR, I added a new `getGitCloneToken` function. This function returns `GITEA_TOKEN` for cloning remote actions or remote reusable workflows when the cloneURL is from the same Gitea instance that the runner is registered to. Otherwise, it returns an empty string as token for cloning public repos from other instances (such as GitHub).

Thanks @ChristopherHX for https://gitea.com/gitea/act/pulls/123#issuecomment-1046171 and https://gitea.com/gitea/act/pulls/123#issuecomment-1046285.

Reviewed-on: https://gitea.com/gitea/act/pulls/123
Reviewed-by: ChristopherHX <christopherhx@noreply.gitea.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2025-10-14 02:37:09 +00:00
Zettat123
ac6e4b7517 Support inputs context when parsing jobs (#143)
Reusable workflows or manually triggered workflows may get data from [`inputs` context](https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#inputs-context).

For example:

```
name: Build

run-name: Build app on ${{ inputs.os }}

on:
  workflow_dispatch:
    inputs:
      os:
        description: Select the OS
        required: true
        type: choice
        options:
          - windows
          - linux

...
```

Reviewed-on: https://gitea.com/gitea/act/pulls/143
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2025-10-03 18:05:12 +00:00
Christopher Homberger
91852faf93 refactor: simplify adding new node versions add node 24 (#140)
* backport

Reviewed-on: https://gitea.com/gitea/act/pulls/140
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Christopher Homberger <christopher.homberger@web.de>
Co-committed-by: Christopher Homberger <christopher.homberger@web.de>
2025-08-21 16:04:08 +00:00
ChristopherHX
39509e9ad0 Support Simplified Concurrency (#139)
- update RawConcurrency struct to parse and serialize string-based concurrency notation
- update EvaluateConcurrency to handle new RawConcurrency format

Reviewed-on: https://gitea.com/gitea/act/pulls/139
Reviewed-by: Zettat123 <zettat123@noreply.gitea.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: ChristopherHX <christopher.homberger@web.de>
Co-committed-by: ChristopherHX <christopher.homberger@web.de>
2025-07-29 09:14:47 +00:00
badhezi
9924aea786 Evaluate run-name field for workflows (#137)
To support https://github.com/go-gitea/gitea/pull/34301

Reviewed-on: https://gitea.com/gitea/act/pulls/137
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: badhezi <zlilaharon@gmail.com>
Co-committed-by: badhezi <zlilaharon@gmail.com>
2025-05-12 17:17:50 +00:00
Jack Jackson
65c232c4a5 Parse permissions (#133)
Resurrecting [this PR](https://gitea.com/gitea/act/pulls/73) as the original author has [lost motivation](https://github.com/go-gitea/gitea/pull/25664#issuecomment-2737099259) (though, to be clear - all credit belongs to them, all mistakes are mine and mine alone!)

Co-authored-by: Søren L. Hansen <sorenisanerd@gmail.com>
Reviewed-on: https://gitea.com/gitea/act/pulls/133
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Jack Jackson <scubbojj@gmail.com>
Co-committed-by: Jack Jackson <scubbojj@gmail.com>
2025-03-24 18:17:06 +00:00
Guillaume S.
5da4954b65 fix handle missing yaml.ScalarNode (#129)
This bug was reported on https://github.com/go-gitea/gitea/issues/33657
Rewrite of (see below) was missing in this commit 6cdf1e5788
```go
case string:
    acts[act] = []string{b}
```

Reviewed-on: https://gitea.com/gitea/act/pulls/129
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Guillaume S. <me@gsvd.dev>
Co-committed-by: Guillaume S. <me@gsvd.dev>
2025-02-26 06:19:02 +00:00
Zettat123
ec091ad269 Support concurrency (#124)
To support `concurrency` syntax for Gitea Actions

Gitea PR: https://github.com/go-gitea/gitea/pull/32751

Reviewed-on: https://gitea.com/gitea/act/pulls/124
Reviewed-by: Lunny Xiao <lunny@noreply.gitea.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2025-02-11 02:51:48 +00:00
Zettat123
1656206765 Improve the support for reusable workflows (#122)
Fix [#32439](https://github.com/go-gitea/gitea/issues/32439)

- Support reusable workflows with conditional jobs
- Support nesting reusable workflows

Reviewed-on: https://gitea.com/gitea/act/pulls/122
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: Jason Song <wolfogre@noreply.gitea.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2024-11-23 14:14:17 +00:00
Lunny Xiao
6cdf1e5788 Fix ParseRawOn sequence problem (#119)
Fix https://gitea.com/gitea/act/actions/runs/277/jobs/0

Reviewed-on: https://gitea.com/gitea/act/pulls/119
2024-10-05 19:29:55 +00:00
Lunny Xiao
ab381649da Add parsing for workflow dispatch (#118)
Reviewed-on: https://gitea.com/gitea/act/pulls/118
2024-10-03 02:56:58 +00:00
Jason Song
38e7e9e939 Use hashed uses string as cache dir name (#117)
Reviewed-on: https://gitea.com/gitea/act/pulls/117
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
2024-09-24 06:53:41 +00:00
Zettat123
2ab806053c Check all job results when calling reusable workflows (#116)
Fix [#31900](https://github.com/go-gitea/gitea/issues/31900)

Reviewed-on: https://gitea.com/gitea/act/pulls/116
Reviewed-by: Jason Song <wolfogre@noreply.gitea.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2024-09-24 06:52:45 +00:00
Zettat123
6a090f67e5 Support some GITEA_ environment variables (#112)
Fix https://gitea.com/gitea/act_runner/issues/575

Reviewed-on: https://gitea.com/gitea/act/pulls/112
Reviewed-by: Jason Song <i@wolfogre.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2024-07-29 04:17:45 +00:00
Jason Song
517d11c671 Reduce log noise (#108)
Cannot guarantee that all noisy logs can be removed at once.

Comment them instead of removing them to make it easier to merge upstream.

What have been removed in this PR are those that are very very long and almost unreadable logs, like

<img width="839" alt="image" src="/attachments/b59e1dcc-4edd-4f81-b939-83dcc45f2ed2">

Reviewed-on: https://gitea.com/gitea/act/pulls/108
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
2024-04-10 06:55:46 +00:00
Jason Song
e1b1e81124 Revert "Pass 'sleep' as container command rather than entrypoint (#86)" (#107)
This reverts #86.

Some images use a custom entry point for specific usage, then `[entrypoint] [cmd]` like `helm /bin/sleep 1` will failed.

It causes https://gitea.com/gitea/helm-chart/actions/runs/755 since the image is `alpine/helm`.

```yaml
  check-and-test:
    runs-on: ubuntu-latest
    container: alpine/helm:3.14.3
```

Reviewed-on: https://gitea.com/gitea/act/pulls/107
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
2024-04-10 06:53:28 +00:00
Zettat123
64876e3696 Interpolate job name with matrix (#106)
Fix https://github.com/go-gitea/gitea/issues/28207

Reviewed-on: https://gitea.com/gitea/act/pulls/106
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2024-04-07 03:34:53 +00:00
Jason Song
3fa1dba92b Merge tag 'nektos/v0.2.61' 2024-04-01 14:23:16 +08:00
Zettat123
9725f60394 Support reusing workflows with absolute URLs (#104)
Resolve https://gitea.com/gitea/act_runner/issues/507

Reviewed-on: https://gitea.com/gitea/act/pulls/104
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2024-03-29 06:15:28 +00:00
Thomas E Lackey
a79d81989f Pass 'sleep' as container command rather than entrypoint (#86)
The current code overrides the container's entrypoint with `sleep`.  Unfortunately, that prevents initialization scripts, such as to initialize Docker-in-Docker, from running.

The change simply moves the `sleep` to the command, rather than entrypoint, directive.

For most containers of this sort, the entrypoint script performs initialization, and then ends with `$@` to execute whatever command is passed.

If the container has no entrypoint, the command is executed directly.  As a result, this should be a transparent change for most use cases, while allowing the container's entrypoint to be used when present.

Reviewed-on: https://gitea.com/gitea/act/pulls/86
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Thomas E Lackey <telackey@bozemanpass.com>
Co-committed-by: Thomas E Lackey <telackey@bozemanpass.com>
2024-03-27 10:17:48 +00:00
Zettat123
655f578563 Remove the network when there is no service (#103)
Reviewed-on: https://gitea.com/gitea/act/pulls/103
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2024-03-27 10:07:29 +00:00
Zettat123
0054a45d1b Fix bugs related to services (#100)
Related to #99

- use `networkNameForGitea` function instead of `networkName` to get network name
- add the missing `Cmd` and `AutoRemove` when creating service containers

Reviewed-on: https://gitea.com/gitea/act/pulls/100
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2024-03-26 10:14:06 +00:00
Jason Song
79a7577c15 Merge tag 'nektos/v0.2.60' 2024-03-25 16:58:11 +08:00
Jason Song
a28ebf0a48 Improve workflows (#98)
Starting from setup-go v4, it will cache build dependencies by default, see https://github.com/actions/setup-go#caching-dependency-files-and-build-outputs.

Also bump some versions.

Reviewed-on: https://gitea.com/gitea/act/pulls/98
2024-03-25 15:54:51 +08:00
Jason Song
2b860ce371 Remove emojis in command outputs (#97)
Remove emojis in command outputs; leave others since they don't matter.

Help https://github.com/go-gitea/gitea/pull/29777

Reviewed-on: https://gitea.com/gitea/act/pulls/97
2024-03-25 15:54:39 +08:00
Zettat123
3a9e7d18de Support cloning remote actions from insecure Gitea instances (#92)
Related to https://github.com/go-gitea/gitea/issues/28693

Reviewed-on: https://gitea.com/gitea/act/pulls/92
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2024-03-25 15:54:09 +08:00
Claudio Nicora
b4edc952d9 Patched options() to let container options propagate to job containers (#80)
This PR let "general" container config to be propagated to each job container.

See:
- https://gitea.com/gitea/act_runner/issues/265#issuecomment-744382
- https://gitea.com/gitea/act_runner/issues/79
- https://gitea.com/gitea/act_runner/issues/378

Reviewed-on: https://gitea.com/gitea/act/pulls/80
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Claudio Nicora <claudio.nicora@gmail.com>
Co-committed-by: Claudio Nicora <claudio.nicora@gmail.com>
2024-03-25 15:43:14 +08:00
sillyguodong
f1213213d8 Make runs-on support variable expression (#91)
Partial implementation of https://gitea.com/gitea/act_runner/issues/445, the Gitea side also needs a PR for the entire functionality.
Gitea side: https://github.com/go-gitea/gitea/pull/29468

Co-authored-by: Jason Song <i@wolfogre.com>
Reviewed-on: https://gitea.com/gitea/act/pulls/91
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: sillyguodong <gedong_1994@163.com>
Co-committed-by: sillyguodong <gedong_1994@163.com>
2024-03-25 15:42:59 +08:00
Jason Song
15045b4fc0 Merge pull request 'Fix panic in extractFromImageEnv' (#81) from wolfogre/act:bugfix/panic_extractFromImageEnv into main
Reviewed-on: https://gitea.com/gitea/act/pulls/81
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
2023-10-31 14:48:40 +00:00
Jason Song
67918333fa fix: panic 2023-10-31 22:32:21 +08:00
Lunny Xiao
c93462e19f Merge pull request 'bump nektos to 0.2.52' (#79) from bump-nektos into main
Reviewed-on: https://gitea.com/gitea/act/pulls/79
Reviewed-by: John Olheiser <john+gitea@jolheiser.com>
2023-10-13 01:20:21 +00:00
techknowlogick
f3264cac20 Merge remote-tracking branch 'upstream/master' into bump-nektos 2023-10-11 15:28:38 -04:00
techknowlogick
4699c3b689 Merge nektos/act/v0.2.51 2023-09-24 15:09:26 -04:00
Jason Song
22d91e3ac3 Merge tag 'nektos/v0.2.49'
Conflicts:
	cmd/input.go
	go.mod
	go.sum
	pkg/exprparser/interpreter.go
	pkg/model/workflow.go
	pkg/runner/expression.go
	pkg/runner/job_executor.go
	pkg/runner/runner.go
2023-08-02 11:52:14 +08:00
sillyguodong
cdc6d4bc6a Support expression in uses (#75)
Since actions can specify the download source via a url prefix. The prefix may contain some sensitive information that needs to be stored in secrets or variable context, so we need to interpolate the expression value for`uses` firstly.

Reviewed-on: https://gitea.com/gitea/act/pulls/75
Co-authored-by: sillyguodong <gedong_1994@163.com>
Co-committed-by: sillyguodong <gedong_1994@163.com>
2023-07-17 03:46:34 +00:00
Zettat123
2069b04779 Fix missed ValidVolumes for docker steps (#74)
Fixes https://gitea.com/gitea/act_runner/issues/277

Thanks @ChristopherHX for finding the cause of the bug.

Reviewed-on: https://gitea.com/gitea/act/pulls/74
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2023-07-11 02:08:22 +00:00
sati.ac
3813f40cba use remoteAction.URL if not empty (#71)
Fixes https://github.com/go-gitea/gitea/issues/25615

Reviewed-on: https://gitea.com/gitea/act/pulls/71
Co-authored-by: sati.ac <sati.ac@noreply.gitea.com>
Co-committed-by: sati.ac <sati.ac@noreply.gitea.com>
2023-07-03 03:43:44 +00:00
Jason Song
eb19987893 Revert "Support for multiple default URLs for getting actions (#58)" (#70)
Follow https://github.com/go-gitea/gitea/pull/25581 .

Reviewed-on: https://gitea.com/gitea/act/pulls/70
2023-06-30 07:45:13 +00:00
Zettat123
545802b97b Fix the error when removing network in self-hosted mode (#69)
Fixes https://gitea.com/gitea/act_runner/issues/255

Reviewed-on: https://gitea.com/gitea/act/pulls/69
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2023-06-28 02:27:12 +00:00
Tomasz Duda
515c2c429d fix action cloning, set correct server_url for act_runner exec (#68)
1. Newest act is not able to clone action based on --default-actions-url
It might be side effect of https://gitea.com/gitea/act/pulls/67.
2. Set correct server_url, api_url, graphql_url for act_runner exec

Reviewed-on: https://gitea.com/gitea/act/pulls/68
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Tomasz Duda <tomaszduda23@gmail.com>
Co-committed-by: Tomasz Duda <tomaszduda23@gmail.com>
2023-06-20 07:36:10 +00:00
Zettat123
a165e17878 Add support for glob syntax when checking volumes (#64)
Follow #60

Reviewed-on: https://gitea.com/gitea/act/pulls/64
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2023-06-16 05:24:01 +00:00
Zettat123
56e103b4ba Fix the missing URL when using remote reusable workflow (#67)
Reviewed-on: https://gitea.com/gitea/act/pulls/67
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2023-06-16 05:12:43 +00:00
Marius Zwicker
422cbdf446 Allow to override location of action cache dir (#65)
Adds an explicit config option to specify the directory
below which action contents will be cached. If left empty
the previous location at `$XDG_CACHE_HOME/act` or
`$HOME/.cache/act` will be used respectively.

Required to resolve gitea/act_runner#235

Co-authored-by: Marius Zwicker <marius@mlba-team.de>
Reviewed-on: https://gitea.com/gitea/act/pulls/65
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Marius Zwicker <emzeat@noreply.gitea.com>
Co-committed-by: Marius Zwicker <emzeat@noreply.gitea.com>
2023-06-16 03:41:39 +00:00
Jason Song
8c56bd3aa5 Merge tag 'nektos/v0.2.46' 2023-06-16 11:08:39 +08:00
a1012112796
a94498b482 fix local workflow for act_runner exec (#63)
by the way, export `ACT_SKIP_CHECKOUT` as a env verb for user to do some special config of local test.

example usage:

7a3ab0fdbc

Reviewed-on: https://gitea.com/gitea/act/pulls/63
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: a1012112796 <1012112796@qq.com>
Co-committed-by: a1012112796 <1012112796@qq.com>
2023-06-13 03:46:26 +00:00
sillyguodong
fe76a035ad Follow upstream support for variables (#66)
Because the upstream [PR](https://github.com/nektos/act/pull/1833) already supports variables, so this PR revert #43 (commit de529139af), and cherry-pick commit [6ce45e3](6ce45e3f24).

Co-authored-by: Kuan Yong <wong0514@gmail.com>
Reviewed-on: https://gitea.com/gitea/act/pulls/66
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: sillyguodong <gedong_1994@163.com>
Co-committed-by: sillyguodong <gedong_1994@163.com>
2023-06-12 06:54:17 +00:00
sillyguodong
6ce5c93cc8 Put the job container name into the env context (#62)
Related: https://gitea.com/gitea/act_runner/issues/189#issuecomment-740636
Refer to [Docker Doc](https://docs.docker.com/engine/reference/commandline/run/#volumes-from), the `--volumes-from` flag is used when running or creating a new container and takes the name or ID of the container from which you want to share volumes. Here's the syntax:
```
docker run --volumes-from <container_name_or_id> <image>
```
So put the job container name into the `env` context in this PR.

Co-authored-by: Jason Song <i@wolfogre.com>
Reviewed-on: https://gitea.com/gitea/act/pulls/62
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: sillyguodong <gedong_1994@163.com>
Co-committed-by: sillyguodong <gedong_1994@163.com>
2023-06-06 00:21:31 +00:00
Zettat123
92b4d73376 Check volumes (#60)
This PR adds a `ValidVolumes` config. Users can specify the volumes (including bind mounts) that can be mounted to containers by this config.

Options related to volumes:
- [jobs.<job_id>.container.volumes](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idcontainervolumes)
- [jobs.<job_id>.services.<service_id>.volumes](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idservicesservice_idvolumes)

In addition, volumes specified by `options` will also be checked.

Currently, the following default volumes (see a72822b3f8/pkg/runner/run_context.go (L116-L166)) will be added to `ValidVolumes`:
- `act-toolcache`
- `<container-name>` and `<container-name>-env`
- `/var/run/docker.sock` (We need to add a new configuration to control whether the docker daemon can be mounted)

Co-authored-by: Jason Song <i@wolfogre.com>
Reviewed-on: https://gitea.com/gitea/act/pulls/60
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2023-06-05 09:21:59 +00:00
Zettat123
183bb7af1b Support for multiple default URLs for getting actions (#58)
Partially resolve https://github.com/go-gitea/gitea/issues/24789.

`act_runner`  needs to be improved to parse `gitea_default_actions_url` after this PR merged (https://gitea.com/gitea/act_runner/pulls/200)

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/act/pulls/58
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2023-06-05 09:07:17 +00:00
sillyguodong
a72822b3f8 Fix some options issue. (#59)
- Fix https://gitea.com/gitea/act_runner/issues/220
ignore `--network` and `--net` in `options`.
- Fix https://gitea.com/gitea/act_runner/issues/222
add opts of `mergo.WithAppendSlice` when excute `mergo.Merge()`.

Reviewed-on: https://gitea.com/gitea/act/pulls/59
Reviewed-by: Zettat123 <zettat123@noreply.gitea.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: sillyguodong <gedong_1994@163.com>
Co-committed-by: sillyguodong <gedong_1994@163.com>
2023-05-31 10:33:39 +00:00
sillyguodong
9283cfc9b1 Fix container network issue (#56)
Follow: https://gitea.com/gitea/act_runner/pulls/184
Close https://gitea.com/gitea/act_runner/issues/177

#### changes:
- `act` create new networks only if the value of `NeedCreateNetwork` is true, and remove these networks at last. `NeedCreateNetwork` is passed by `act_runner`. 'NeedCreateNetwork' is true only if  `container.network` in the configuration file of the `act_runner` is empty.
- In the `docker create` phase, specify the network to which containers will connect. Because, if not specify , container will connect to `bridge` network which is created automatically by Docker.
  - If the network is user defined network ( the value of `container.network` is empty or `<custom-network>`.  Because, the network created by `act` is also user defined network.), will also specify alias by `--network-alias`. The alias of service is `<service-id>`. So we can be access service container by `<service-id>:<port>` in the steps of job.
- Won't try to `docker network connect ` network after `docker start` any more.
  - Because on the one hand,  `docker network connect` applies only to user defined networks, if try to `docker network connect host <container-name>` will return error.
  - On the other hand, we just specify network in the stage of `docker create`, the same effect can be achieved.
- Won't try to remove containers and networks berfore  the stage of `docker start`, because the name of these containers and netwoks won't be repeat.

Co-authored-by: Jason Song <i@wolfogre.com>
Reviewed-on: https://gitea.com/gitea/act/pulls/56
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: sillyguodong <gedong_1994@163.com>
Co-committed-by: sillyguodong <gedong_1994@163.com>
2023-05-16 14:03:55 +08:00
Zettat123
27846050ae Force privileged to false when runner's config is false (#57)
The runner's `privileged` config can be bypassed. Currently, even if the runner's `privileged` config is false, users can still enable the privileged mode by using `--privileged` in the container's option string. Therefore, if runner's config is false, the `--privileged` in options string should be ignored.

Reviewed-on: https://gitea.com/gitea/act/pulls/57
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2023-05-16 11:21:18 +08:00
Zettat123
ed9b6643ca Do not set the default network to host (#55)
In [nektos/act/pull/1739](https://github.com/nektos/act/pull/1739), the container network mode defaults to `host` if the network option isn't specified in `options`.  When calling `ConnectToNetwork`, the `host` network mode may cause the error:
`Error response from daemon: container sharing network namespace with another container or host cannot be connected to any other network`
see the code: a94a01bff2/pkg/container/docker_run.go (L51-L68)

To avoid the error, this logic needs to be removed to keep the default network mode as `bridge`.

Reviewed-on: https://gitea.com/gitea/act/pulls/55
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2023-05-09 16:41:31 +08:00
Jason Song
a94a01bff2 Fix regression after merging upstream (#54)
Related to 229dbaf153

Reviewed-on: https://gitea.com/gitea/act/pulls/54
2023-05-04 17:54:09 +08:00
Jason Song
229dbaf153 Merge tag 'nektos/v0.2.45' 2023-05-04 17:45:53 +08:00
Zettat123
a18648ee73 Support services credentials (#51)
If a service's image is from a container registry requires authentication, `act_runner` will need `credentials` to pull the image, see [documentation](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idservicesservice_idcredentials).
Currently, `act_runner` incorrectly uses the `credentials` of `containers` to pull services' images and the `credentials` of services won't be used, see the related code: 0c1f2edb99/pkg/runner/run_context.go (L228-L269)

Co-authored-by: Jason Song <i@wolfogre.com>
Reviewed-on: https://gitea.com/gitea/act/pulls/51
Reviewed-by: Jason Song <i@wolfogre.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2023-04-25 14:45:39 +08:00
sillyguodong
518d8c96f3 Keep the order of on when parsing workflow (#46)
Keep the order of `on` when parsing workflow, and fix the occasional unit test failure of `actions` like https://gitea.com/gitea/act/actions/runs/68

Co-authored-by: Jason Song <i@wolfogre.com>
Reviewed-on: https://gitea.com/gitea/act/pulls/46
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: sillyguodong <gedong_1994@163.com>
Co-committed-by: sillyguodong <gedong_1994@163.com>
2023-04-24 23:16:41 +08:00
Zettat123
0c1f2edb99 Support specifying command for services (#50)
This PR is to support overwriting the default `CMD` command of `services` containers.

This is a Gitea specific feature and GitHub Actions doesn't support this syntax.

Reviewed-on: https://gitea.com/gitea/act/pulls/50
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2023-04-23 14:55:17 +08:00
Zettat123
721857e4a0 Remove empty steps when decoding Job (#49)
Follow #48
Empty steps are invalid, so remove them when decoding `Job` from YAML.

Reviewed-on: https://gitea.com/gitea/act/pulls/49
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2023-04-21 20:21:15 +08:00
Zettat123
6b1010ad07 Fix potential panic caused by nil Step (#48)
```yml
jobs:
  job1:
    steps:
      - run: echo HelloWorld
      - # empty step
```

If a job contains an empty step, `Job.Steps` will have a nil element and will cause panic when calling `Step.String()`.

See [the code of gitea](948a9ee5e8/models/actions/task.go (L300-L301))

Reviewed-on: https://gitea.com/gitea/act/pulls/48
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2023-04-21 14:45:38 +08:00
Zettat123
e12252a43a Support intepolation for env of services (#47)
Reviewed-on: https://gitea.com/gitea/act/pulls/47
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2023-04-20 16:24:31 +08:00
Zettat123
8609522aa4 Support services options (#45)
Reviewed-on: https://gitea.com/gitea/act/pulls/45
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2023-04-19 21:53:57 +08:00
Zettat123
6a876c4f99 Add go build tag to docker_network.go (#44)
Fix the build failure in https://gitea.com/gitea/act_runner/actions/runs/278/jobs/0

Reviewed-on: https://gitea.com/gitea/act/pulls/44
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2023-04-19 16:19:38 +08:00
sillyguodong
de529139af Support configuration variables (#43)
related to: https://gitea.com/gitea/act_runner/issues/127

This PR make `act` support the expression like `${{ vars.YOUR_CUSTOM_VARIABLES }}`.

Reviewed-on: https://gitea.com/gitea/act/pulls/43
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: sillyguodong <gedong_1994@163.com>
Co-committed-by: sillyguodong <gedong_1994@163.com>
2023-04-19 15:22:56 +08:00
Zettat123
d3a56cdb69 Support services (#42)
Replace #5

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Jason Song <i@wolfogre.com>
Reviewed-on: https://gitea.com/gitea/act/pulls/42
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2023-04-19 11:23:28 +08:00
Galen Abell
9bdddf18e0 Parse secret inputs in reusable workflows (#41)
Secrets can be passed to reusable workflows, either explicitly by key or
implicitly by `inherit`:

https://docs.github.com/en/actions/using-workflows/reusing-workflows#using-inputs-and-secrets-in-a-reusable-workflow

Reviewed-on: https://gitea.com/gitea/act/pulls/41
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Galen Abell <galen@galenabell.com>
Co-committed-by: Galen Abell <galen@galenabell.com>
2023-04-17 13:41:02 +08:00
Zettat123
ac1ba34518 Fix incorrect job result status (#40)
Fix [#24039(GitHub)](https://github.com/go-gitea/gitea/issues/24039)

At present, if a job fails in the `Set up job`, the result status of the job will still be `success`. The reason is that the `pre` steps don't call `SetJobError`, so the `jobError` will be nil when `post` steps setting the job result. See 5c4a96bcb7/pkg/runner/job_executor.go (L99)

Reviewed-on: https://gitea.com/gitea/act/pulls/40
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2023-04-14 15:42:03 +08:00
Jason Song
5c4a96bcb7 Avoid using log.Fatal in pkg/* (#39)
Follow https://github.com/nektos/act/pull/1705

Reviewed-on: https://gitea.com/gitea/act/pulls/39
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Jason Song <i@wolfogre.com>
Co-committed-by: Jason Song <i@wolfogre.com>
2023-04-07 16:31:03 +08:00
Zettat123
62abf4fe11 Add token for getting reusable workflows from local private repos (#38)
Partially fixes https://gitea.com/gitea/act_runner/issues/91

If the repository is private, we need to provide the token to the caller workflows to access the called reusable workflows from the same repository.

Reviewed-on: https://gitea.com/gitea/act/pulls/38
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2023-04-06 14:16:20 +08:00
Zettat123
cfedc518ca Add With field to jobparser.Job (#37)
Partially Fixes [gitea/act_runner#91 comment](https://gitea.com/gitea/act_runner/issues/91#issuecomment-734544)

nektos/act has added `With` to support reusable workflows (see [code](68c72b9a51/pkg/model/workflow.go (L160)))

GitHub actions also support [`jobs.<job_id>.with`](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idwith)

Reviewed-on: https://gitea.com/gitea/act/pulls/37
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2023-04-04 10:59:53 +08:00
Zettat123
5e76853b55 Support reusable workflow (#34)
Fix https://gitea.com/gitea/act_runner/issues/80
Fix https://gitea.com/gitea/act_runner/issues/85

To support reusable workflows, I made some improvements:
- read `yml` files from both `.gitea/workflows` and `.github/workflows`
- clone repository for local reusable workflows because the runner doesn't have the code in its local directory
- fix the incorrect clone url like `https://https://gitea.com`

Co-authored-by: Jason Song <i@wolfogre.com>
Reviewed-on: https://gitea.com/gitea/act/pulls/34
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2023-03-29 13:59:22 +08:00
Jason Song
2eb4de02ee Expose SetJob to make EraseNeeds work (#35)
Related to #33

Reviewed-on: https://gitea.com/gitea/act/pulls/35
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
2023-03-29 13:57:29 +08:00
Jason Song
342ad6a51a Keep the order of jobs in the workflow file when parsing (#33)
Keep the order of jobs in the workflow file when parsing, and it will make it possible for Gitea to show jobs in the original order on UI.

Reviewed-on: https://gitea.com/gitea/act/pulls/33
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
2023-03-28 11:38:40 +08:00
Jason Song
568f053723 Revert "Erase needs of job in SingleWorkflow (#9)" (#32)
This reverts commit 1ba076d321.

`EraseNeeds` Shouldn't be used in `jobparser.Parse`, it's for 023e61e678/models/actions/run.go (L200)

Or Gitea won't be able to get `Needs` of jobs.

Reviewed-on: https://gitea.com/gitea/act/pulls/32
Reviewed-by: Zettat123 <zettat123@noreply.gitea.io>
2023-03-27 17:46:50 +08:00
Bo-Yi.Wu
8f12a6c947 chore: update go-git dependency in go.mod (#30)
- Replace `go-git` with a forked version in `go.mod`

Signed-off-by: Bo-Yi.Wu <appleboy.tw@gmail.com>

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/act/pulls/30
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Bo-Yi.Wu <appleboy.tw@gmail.com>
Co-committed-by: Bo-Yi.Wu <appleboy.tw@gmail.com>
2023-03-26 21:27:19 +08:00
Lunny Xiao
83fb85f702 Fix bug (#31)
Reviewed-on: https://gitea.com/gitea/act/pulls/31
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-committed-by: Lunny Xiao <xiaolunwen@gmail.com>
2023-03-26 21:01:46 +08:00
Zettat123
3daf313205 chore(yaml): Improve ParseRawOn (#28)
See [act_runner #71 comment](https://gitea.com/gitea/act_runner/issues/71#issuecomment-733806), we need to handle `nil interface{}` in `ParseRawOn` function

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/act/pulls/28
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: appleboy <appleboy.tw@gmail.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2023-03-25 12:13:50 +08:00
Lunny Xiao
7c5400d75b ParseRawOn support schedules (#29)
Fix gitea/act_runner#71

Reviewed-on: https://gitea.com/gitea/act/pulls/29
Reviewed-by: Jason Song <i@wolfogre.com>
Reviewed-by: Zettat123 <zettat123@noreply.gitea.io>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-committed-by: Lunny Xiao <xiaolunwen@gmail.com>
2023-03-24 20:15:46 +08:00
Jason Song
929ea6df75 Support gitea context (#27)
And we will be able to use context like `${{ gitea.repository }}` in workflows yaml files, it's same as `${{ github.repository }}`

Reviewed-on: https://gitea.com/gitea/act/pulls/27
Reviewed-by: Zettat123 <zettat123@noreply.gitea.io>
2023-03-23 12:14:28 +08:00
Zettat123
f6a8a0e643 Add extra path env for running go actions (#26)
At present, the runner can't run go actions even if the go environment has been set by the `setup-go` action. The reason is that `setup-go` will add the go related paths to [`GITHUB_PATH`](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-system-path) but in #22 I forgot to apply them before running go actions. After adding the `ApplyExtraPath` function, the `setup-go` action runs properly.

Reviewed-on: https://gitea.com/gitea/act/pulls/26
Reviewed-by: Jason Song <i@wolfogre.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2023-03-21 15:31:30 +08:00
a1012112796
556fd20aed make sure special logs be sent to gitea's server (#25)
example:
https://gitea.com/a1012112796/test_action/actions/runs/7

![image](/attachments/a8931f2f-096f-41fd-8f9f-0c8322ee985a)

TODO: special handle them on ui

Signed-off-by: a1012112796 <1012112796@qq.com>

Reviewed-on: https://gitea.com/gitea/act/pulls/25
Reviewed-by: Jason Song <i@wolfogre.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: a1012112796 <1012112796@qq.com>
Co-committed-by: a1012112796 <1012112796@qq.com>
2023-03-17 23:01:31 +08:00
Jason Song
a8298365fe Fix incompatibility caused by tracking upstream add actions to test it (#24)
Reviewed-on: https://gitea.com/gitea/act/pulls/24
2023-03-16 15:00:11 +08:00
Jason Song
1dda0aec69 Merge tag 'nektos/v0.2.43'
Conflicts:
	pkg/container/docker_run.go
	pkg/runner/action.go
	pkg/runner/logger.go
	pkg/runner/run_context.go
	pkg/runner/runner.go
	pkg/runner/step_action_remote_test.go
2023-03-16 11:45:29 +08:00
Jason Song
49e204166d Update forking fules (#23)
As the title.

Reviewed-on: https://gitea.com/gitea/act/pulls/23
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
2023-03-16 10:46:41 +08:00
Zettat123
a36b003f7a Improve running with go (#22)
Close #21

I have tested this PR and run Go actions successfully on:
- Windows host
- Docker on Windows
- Linux host
- Docker on Linux

Before running Go actions, we need to make sure that Go has been installed on the host or the Docker image.

Reviewed-on: https://gitea.com/gitea/act/pulls/22
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2023-03-14 16:55:36 +08:00
Zettat123
0671d16694 Fix missing ActionRunsUsingGo (#20)
- Allow `using: "go"` when unmarshalling YAML.
- Add `ActionRunsUsingGo` to returned errors.

Co-authored-by: Zettat123 <zettat123@gmail.com>
Reviewed-on: https://gitea.com/gitea/act/pulls/20
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Zettat123 <zettat123@noreply.gitea.io>
Co-committed-by: Zettat123 <zettat123@noreply.gitea.io>
2023-03-09 22:51:58 +08:00
a1012112796
881dbdb81b make log level configable (#19)
relatd: https://gitea.com/gitea/act_runner/pulls/39
Reviewed-on: https://gitea.com/gitea/act/pulls/19
Reviewed-by: Jason Song <i@wolfogre.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: a1012112796 <1012112796@qq.com>
Co-committed-by: a1012112796 <1012112796@qq.com>
2023-03-08 14:46:39 +08:00
Jason Song
1252e551b8 Replace more strings.ReplaceAll to safeFilename (#18)
Follow #16 #17

Reviewed-on: https://gitea.com/gitea/act/pulls/18
2023-02-24 14:20:34 +08:00
Jason Song
c614d8b96c Replace more strings.ReplaceAll to safeFilename (#17)
Follow #16.

Reviewed-on: https://gitea.com/gitea/act/pulls/17
2023-02-24 12:11:30 +08:00
Jason Song
84b6649b8b Safe filename (#16)
Fix #15.

Reviewed-on: https://gitea.com/gitea/act/pulls/16
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Jason Song <i@wolfogre.com>
Co-committed-by: Jason Song <i@wolfogre.com>
2023-02-24 10:17:36 +08:00
Jason Song
dca7801682 Support uses http(s)://host/owner/repo as actions (#14)
Examples:

```yaml
jobs:
  my_first_job:
    steps:
      - name: My first step
        uses: https://gitea.com/actions/heroku@main
      - name: My second step
        uses: http://example.com/actions/heroku@v2.0.1
```

Reviewed-on: https://gitea.com/gitea/act/pulls/14
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Jason Song <i@wolfogre.com>
Co-committed-by: Jason Song <i@wolfogre.com>
2023-02-15 16:28:33 +08:00
Lunny Xiao
4b99ed8916 Support go run on action (#12)
Reviewed-on: https://gitea.com/gitea/act/pulls/12
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-committed-by: Lunny Xiao <xiaolunwen@gmail.com>
2023-02-15 16:10:15 +08:00
Lunny Xiao
e46ede1b17 parse raw on (#11)
Reviewed-on: https://gitea.com/gitea/act/pulls/11
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-committed-by: Lunny Xiao <xiaolunwen@gmail.com>
2023-01-31 15:49:55 +08:00
Jason Song
1ba076d321 Erase needs of job in SingleWorkflow (#9)
Reviewed-on: https://gitea.com/gitea/act/pulls/9
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Jason Song <i@wolfogre.com>
Co-committed-by: Jason Song <i@wolfogre.com>
2023-01-30 11:42:19 +08:00
appleboy
0efa2d5e63 fix(test): needs condition. (#8)
as title.

Signed-off-by: Bo-Yi.Wu <appleboy.tw@gmail.com>

Co-authored-by: Bo-Yi.Wu <appleboy.tw@gmail.com>
Reviewed-on: https://gitea.com/gitea/act/pulls/8
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
2023-01-21 17:09:51 +08:00
Jason Song
0a37a03f2e Clone actions without token (#6)
Shouldn't provide token when cloning actions, the token comes from the instance which triggered the task, it might be not the instance which provides actions.

For GitHub, they are the same, always github.com. But for Gitea, tasks triggered by a.com can clone actions from b.com.

Reviewed-on: https://gitea.com/gitea/act/pulls/6
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Jason Song <i@wolfogre.com>
Co-committed-by: Jason Song <i@wolfogre.com>
2023-01-06 13:34:38 +08:00
appleboy
88cce47022 feat(workflow): support schedule event (#4)
fix https://gitea.com/gitea/act/issues/3

Signed-off-by: Bo-Yi.Wu <appleboy.tw@gmail.com>

Co-authored-by: Bo-Yi.Wu <appleboy.tw@gmail.com>
Reviewed-on: https://gitea.com/gitea/act/pulls/4
2022-12-10 09:14:14 +08:00
Jason Song
7920109e89 Merge tag 'nektos/v0.2.34' 2022-12-05 17:08:17 +08:00
Jason Song
4cacc14d22 feat: adjust container name format (#1)
Co-authored-by: Jason Song <i@wolfogre.com>
Reviewed-on: https://gitea.com/gitea/act/pulls/1
2022-11-24 14:45:32 +08:00
Jason Song
c6b8548d35 feat: support PlatformPicker 2022-11-22 16:39:19 +08:00
Jason Song
64cae197a4 Support step number 2022-11-22 16:11:35 +08:00
Jason Song
7fb84a54a8 chore: update LICENSE 2022-11-22 15:26:02 +08:00
Jason Song
70cc6c017b docs: add naming rule for git ref 2022-11-22 15:05:12 +08:00
Lunny Xiao
d7e9ea75fc disable graphql url because gitea doesn't support that 2022-11-22 14:42:48 +08:00
Jason Song
b9c20dcaa4 feat: support more options of containers 2022-11-22 14:42:12 +08:00
Jason Song
97629ae8af fix: set logger with trace level 2022-11-22 14:41:57 +08:00
Lunny Xiao
b9a9812ad9 Fix API 2022-11-22 14:22:03 +08:00
Lunny Xiao
113c3e98fb support bot site 2022-11-22 14:17:06 +08:00
Jason Song
7815eec33b Add custom enhancements 2022-11-22 14:16:35 +08:00
Jason Song
c051090583 Add description of branchs 2022-11-22 14:02:01 +08:00
fuxiaohei
0fa1fe0310 feat: add logger hook for standalone job logger 2022-11-22 14:00:13 +08:00
1969 changed files with 13371 additions and 244926 deletions

View File

@@ -1,6 +0,0 @@
[codespell]
# Ref: https://github.com/codespell-project/codespell#using-a-config-file
skip = .git*,go.sum,package-lock.json,*.min.*,.codespellrc,testdata,./pkg/runner/hashfiles/index.js
check-hidden = true
ignore-regex = .*Te\{0\}st.*
# ignore-words-list =

52
.dockerignore Normal file
View File

@@ -0,0 +1,52 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# IntelliJ
.idea
# Goland's output filename can not be set manually
/go_build_*
# MS VSCode
.vscode
__debug_bin*
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
*coverage.out
coverage.all
coverage.txt
cpu.out
*.db
*.log
/act_runner
/debug
/bin
/dist
/.env
/.runner
/config.yaml
/Dockerfile
.DS_Store

View File

@@ -12,5 +12,8 @@ insert_final_newline = true
[*.{go}] [*.{go}]
indent_style = tab indent_style = tab
[go.*]
indent_style = tab
[Makefile] [Makefile]
indent_style = tab indent_style = tab

View File

@@ -1,156 +0,0 @@
name: checks
on: [pull_request, workflow_dispatch]
concurrency:
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
env:
ACT_OWNER: ${{ github.repository_owner }}
ACT_REPOSITORY: ${{ github.repository }}
CGO_ENABLED: 0
NO_QEMU: 1
NO_EXTERNAL_IP: 1
DOOD: 1
jobs:
lint:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
- uses: golangci/golangci-lint-action@v8.0.0
with:
version: v2.1.6
- uses: megalinter/megalinter/flavors/go@v9.1.0
env:
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VALIDATE_ALL_CODEBASE: false
GITHUB_STATUS_REPORTER: ${{ !env.ACT }}
GITHUB_COMMENT_REPORTER: ${{ !env.ACT }}
test-linux:
name: test-linux
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 2
- name: Cleanup Docker Engine
run: |
docker ps -a --format '{{ if eq (truncate .Names 4) "act-" }}
{{ .ID }}
{{end}}' | xargs -r docker rm -f || :
docker volume ls --format '{{ if eq (truncate .Name 4) "act-" }}
{{ .Name }}
{{ end }}' | xargs -r docker volume rm -f || :
docker images --format '{{ if eq (truncate .Repository 4) "act-" }}
{{ .ID }}
{{ end }}' | xargs -r docker rmi -f || :
docker images -q | xargs -r docker rmi || :
- name: Set up QEMU
if: '!env.NO_QEMU'
uses: docker/setup-qemu-action@v3
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
- uses: actions/cache@v4
if: ${{ !env.ACT }}
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Install gotestfmt
run: go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@v2.5.0
# Regressions by Gitea Actions CI Migration
# GITHUB_REPOSITORY contains the server url
# ACTIONS_RUNTIME_URL provided to every step, act does not override
- name: Run Tests
run: |
unset ACTIONS_RUNTIME_URL
unset ACTIONS_RESULTS_URL
unset ACTIONS_RUNTIME_TOKEN
export GITHUB_REPOSITORY="${GITHUB_REPOSITORY#${SERVER_URL%/}/}"
export ACT_REPOSITORY="${GITHUB_REPOSITORY#${SERVER_URL%/}/}"
export ACT_OWNER="${ACT_OWNER#${SERVER_URL%/}/}"
env
go test -json -v -cover -coverpkg=./... -coverprofile=coverage.txt -covermode=atomic -timeout 20m ./... | gotestfmt -hide successful-packages,empty-packages 2>&1
env:
SERVER_URL: ${{ github.server_url }}
- name: Run act from cli
run: go run main.go -P ubuntu-latest=node:16-buster-slim -C ./pkg/runner/testdata/ -W ./basic/push.yml
- name: Run act from cli without docker support
run: go run -tags WITHOUT_DOCKER main.go -P ubuntu-latest=-self-hosted -C ./pkg/runner/testdata/ -W ./local-action-js/push.yml
snapshot:
name: snapshot
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
- uses: actions/cache@v4
if: ${{ !env.ACT }}
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: GoReleaser
id: goreleaser
uses: goreleaser/goreleaser-action@v6
with:
version: v2
args: release --snapshot --clean
- name: Setup Node
continue-on-error: true
uses: actions/setup-node@v6
with:
node-version: 20
- name: Install @actions/artifact@2.1.0
continue-on-error: true
run: npm install @actions/artifact@2.1.0
- name: Upload All
uses: actions/github-script@v8
continue-on-error: true
with:
script: |
// We do not use features depending on GITHUB_API_URL so we can hardcode it to avoid the GHES no support error
process.env["GITHUB_SERVER_URL"] = "https://github.com";
const {DefaultArtifactClient} = require('@actions/artifact');
const aartifact = new DefaultArtifactClient();
var artifacts = JSON.parse(process.env.ARTIFACTS);
for(var artifact of artifacts) {
if(artifact.type === "Binary") {
const {id, size} = await aartifact.uploadArtifact(
// name of the artifact
`${artifact.name}-${artifact.target}`,
// files to include (supports absolute and relative paths)
[artifact.path],
process.cwd(),
{
// optional: how long to retain the artifact
// if unspecified, defaults to repository/org retention settings (the limit of this value)
retentionDays: 10
}
);
console.log(`Created artifact with id: ${id} (bytes: ${size}`);
}
}
env:
ARTIFACTS: ${{ steps.goreleaser.outputs.artifacts }}
- name: Chocolatey
uses: ./.github/actions/choco
with:
version: v0.0.0-pr

View File

@@ -39,6 +39,15 @@ jobs:
GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
release-image: release-image:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
variant:
- target: basic
tag_suffix: ""
- target: dind
tag_suffix: "-dind"
- target: dind-rootless
tag_suffix: "-dind-rootless"
container: container:
image: catthehacker/ubuntu:act-latest image: catthehacker/ubuntu:act-latest
env: env:
@@ -62,50 +71,33 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Get Meta - name: Repo Meta
id: meta id: repo_meta
run: | run: |
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
echo REPO_VERSION=${GITHUB_REF_NAME#v} >> $GITHUB_OUTPUT
- name: "Docker meta"
id: docker_meta
uses: https://github.com/docker/metadata-action@v5
with:
images: |
${{ env.DOCKER_ORG }}/${{ steps.repo_meta.outputs.REPO_NAME }}
tags: |
type=semver,pattern={{major}}.{{minor}}.{{patch}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
flavor: |
latest=true
suffix=${{ matrix.variant.tag_suffix }},onlatest=true
- name: Build and push - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
target: basic target: ${{ matrix.variant.target }}
platforms: | platforms: |
linux/amd64 linux/amd64
linux/arm64 linux/arm64
push: true push: true
tags: | tags: ${{ steps.docker_meta.outputs.tags }}
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}
- name: Build and push dind
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
target: dind
platforms: |
linux/amd64
linux/arm64
push: true
tags: |
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}-dind
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}-dind
- name: Build and push dind-rootless
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
target: dind-rootless
platforms: |
linux/amd64
linux/arm64
push: true
tags: |
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}-dind-rootless
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}-dind-rootless

View File

@@ -1,72 +0,0 @@
name: release
on:
push:
tags:
- v*
jobs:
release:
# TODO use environment to scope secrets
name: release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
- uses: actions/cache@v4
if: ${{ !env.ACT }}
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
version: latest
args: release --clean -f ./.goreleaser.yml -f ./.goreleaser.gitea.yml
env:
GITEA_TOKEN: ${{ secrets.GORELEASER_GITHUB_TOKEN || github.token }}
- name: Winget
uses: vedantmgoyal2009/winget-releaser@v2
with:
identifier: nektos.act
installers-regex: '_Windows_\w+\.zip$'
token: ${{ secrets.WINGET_TOKEN }}
if: env.ENABLED
env:
ENABLED: ${{ secrets.WINGET_TOKEN && '1' || '' }}
- name: Chocolatey
uses: ./.github/actions/choco
with:
version: ${{ github.ref }}
apiKey: ${{ secrets.CHOCO_APIKEY }}
push: true
if: env.ENABLED
env:
ENABLED: ${{ secrets.CHOCO_APIKEY && '1' || '' }}
# TODO use ssh deployment key
- name: GitHub CLI extension
uses: actions/github-script@v8
with:
github-token: ${{ secrets.CLI_GITHUB_TOKEN || secrets.GORELEASER_GITHUB_TOKEN }}
script: |
const mainRef = (await github.rest.git.getRef({
owner: context.repo.owner,
repo: 'gh-act',
ref: 'heads/main',
})).data;
console.log(mainRef);
github.rest.git.createRef({
owner: 'nektos',
repo: 'gh-act',
ref: context.ref,
sha: mainRef.object.sha,
});
if: env.ENABLED
env:
ENABLED: ${{ (secrets.CLI_GITHUB_TOKEN || secrets.GORELEASER_GITHUB_TOKEN) && '1' || '' }}

View File

@@ -1,88 +0,0 @@
name: Bug report
description: Use this template for reporting bugs/issues.
labels:
- 'kind/bug'
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: textarea
id: act-debug
attributes:
label: Bug report info
render: plain text
description: |
Output of `act --bug-report`
placeholder: |
act --bug-report
validations:
required: true
- type: textarea
id: act-command
attributes:
label: Command used with act
description: |
Please paste your whole command
placeholder: |
act -P ubuntu-latest=node:12 -v -d ...
render: sh
validations:
required: true
- type: textarea
id: what-happened
attributes:
label: Describe issue
description: |
Also tell us what did you expect to happen?
placeholder: |
Describe issue
validations:
required: true
- type: input
id: repo
attributes:
label: Link to GitHub repository
description: |
Provide link to GitHub repository, you can skip it if the repository is private or you don't have it on GitHub, otherwise please provide it as it might help us troubleshoot problem
placeholder: |
https://github.com/nektos/act
validations:
required: false
- type: textarea
id: workflow
attributes:
label: Workflow content
description: |
Please paste your **whole** workflow here
placeholder: |
name: My workflow
on: ['push', 'schedule']
jobs:
test:
runs-on: ubuntu-latest
env:
KEY: VAL
[...]
render: yml
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output
description: |
Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. Please verify that the log output doesn't contain any sensitive data.
render: sh
placeholder: |
Use `act -v` for verbose output
validations:
required: true
- type: textarea
id: additional-info
attributes:
label: Additional information
placeholder: |
Additional information that doesn't fit elsewhere
validations:
required: false

View File

@@ -1,8 +0,0 @@
blank_issues_enabled: true
contact_links:
- name: Start a discussion
url: https://github.com/actions-oss/act-cli/discussions/new
about: You can ask for help here!
- name: Want to contribute to act?
url: https://github.com/actions-oss/act-cli/blob/main/CONTRIBUTING.md
about: Be sure to read contributing guidelines!

View File

@@ -1,28 +0,0 @@
name: Feature request
description: Use this template for requesting a feature/enhancement.
labels:
- 'kind/feature-request'
body:
- type: markdown
attributes:
value: |
Please note that incompatibility with GitHub Actions should be opened as a bug report, not a new feature.
- type: input
id: act-version
attributes:
label: Act version
description: |
What version of `act` are you using? Version can be obtained via `act --version`
If you've built it from source, please provide commit hash
placeholder: |
act --version
validations:
required: true
- type: textarea
id: feature
attributes:
label: Feature description
description: Describe feature that you would like to see
placeholder: ...
validations:
required: true

View File

@@ -1,20 +0,0 @@
FROM alpine:3.21
ARG CHOCOVERSION=1.1.0
RUN apk add --no-cache bash ca-certificates git \
&& apk --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/edge/community add mono mono-dev \
&& cert-sync /etc/ssl/certs/ca-certificates.crt \
&& wget "https://github.com/chocolatey/choco/archive/${CHOCOVERSION}.tar.gz" -O- | tar -xzf - \
&& cd choco-"${CHOCOVERSION}" \
&& chmod +x build.sh zip.sh \
&& ./build.sh -v \
&& mv ./code_drop/chocolatey/console /opt/chocolatey \
&& mkdir -p /opt/chocolatey/lib \
&& rm -rf /choco-"${CHOCOVERSION}" \
&& apk del mono-dev \
&& rm -rf /var/cache/apk/*
ENV ChocolateyInstall=/opt/chocolatey
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -1,16 +0,0 @@
name: 'Chocolatey Packager'
description: 'Create the choco package and push it'
inputs:
version:
description: 'Version of package'
required: false
apiKey:
description: 'API Key for chocolately'
required: false
push:
description: 'Option for if package is going to be pushed'
required: false
default: 'false'
runs:
using: 'docker'
image: 'Dockerfile'

View File

@@ -1,31 +0,0 @@
#!/bin/bash
set -e
function choco {
mono /opt/chocolatey/choco.exe "$@" --allow-unofficial --nocolor
}
function get_version {
local version=${INPUT_VERSION:-$(git describe --tags)}
version=(${version//[!0-9.-]/})
local version_parts=(${version//-/ })
version=${version_parts[0]}
if [ ${#version_parts[@]} -gt 1 ]; then
version=${version_parts}.${version_parts[1]}
fi
echo "$version"
}
## Determine the version to pack
VERSION=$(get_version)
echo "Packing version ${VERSION} of act"
rm -f act-cli.*.nupkg
mkdir -p tools
cp LICENSE tools/LICENSE.txt
cp VERIFICATION tools/VERIFICATION.txt
cp dist/act-cli_windows_amd64*/act.exe tools/
choco pack act-cli.nuspec --version ${VERSION}
if [[ "$INPUT_PUSH" == "true" ]]; then
choco push act-cli.${VERSION}.nupkg --api-key ${INPUT_APIKEY} -s https://push.chocolatey.org/ --timeout 180
fi

View File

@@ -1,23 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: 'monthly'
groups:
dependencies:
patterns:
- '*'
- package-ecosystem: 'gomod'
directory: '/'
schedule:
interval: 'monthly'
groups:
dependencies:
patterns:
- '*'

View File

@@ -1 +0,0 @@
test-*.yml

View File

@@ -1,151 +0,0 @@
name: checks
on: [pull_request, workflow_dispatch]
concurrency:
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
env:
ACT_OWNER: ${{ github.repository_owner }}
ACT_REPOSITORY: ${{ github.repository }}
CGO_ENABLED: 0
jobs:
lint:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
- uses: golangci/golangci-lint-action@v8.0.0
with:
version: v2.1.6
- uses: megalinter/megalinter/flavors/go@v9.1.0
env:
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VALIDATE_ALL_CODEBASE: false
GITHUB_STATUS_REPORTER: ${{ !env.ACT }}
GITHUB_COMMENT_REPORTER: ${{ !env.ACT }}
test-linux:
name: test-linux
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 2
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
- uses: actions/cache@v4
if: ${{ !env.ACT }}
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Install gotestfmt
run: go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@v2.5.0
- name: Run Tests
run: go test -json -v -cover -coverpkg=./... -coverprofile=coverage.txt -covermode=atomic -timeout 20m ./... | gotestfmt -hide successful-packages,empty-packages 2>&1
- name: Run act from cli
run: go run main.go -P ubuntu-latest=node:16-buster-slim -C ./pkg/runner/testdata/ -W ./basic/push.yml
- name: Run act from cli without docker support
run: go run -tags WITHOUT_DOCKER main.go -P ubuntu-latest=-self-hosted -C ./pkg/runner/testdata/ -W ./local-action-js/push.yml
- name: Upload Codecov report
uses: codecov/codecov-action@v5
with:
files: coverage.txt
fail_ci_if_error: true # optional (default = false)
token: ${{ secrets.CODECOV_TOKEN }}
test-host:
strategy:
matrix:
os:
- windows-latest
- macos-latest
name: test-host-${{matrix.os}}
runs-on: ${{matrix.os}}
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 2
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
- name: Install gotestfmt
run: go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@v2.5.0
- name: Run Tests
run: go test -v -cover -coverpkg=./... -coverprofile=coverage.txt -covermode=atomic -timeout 20m -run ^TestRunEventHostEnvironment$ ./...
shell: bash
snapshot:
name: snapshot
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
- uses: actions/cache@v4
if: ${{ !env.ACT }}
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: GoReleaser
id: goreleaser
uses: goreleaser/goreleaser-action@v6
with:
version: v2
args: release --snapshot --clean
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 20
- name: Install @actions/artifact
run: npm install @actions/artifact
- name: Upload All
uses: actions/github-script@v8
with:
script: |
const {DefaultArtifactClient} = require('@actions/artifact');
const aartifact = new DefaultArtifactClient();
var artifacts = JSON.parse(process.env.ARTIFACTS);
for(var artifact of artifacts) {
if(artifact.type === "Binary") {
const {id, size} = await aartifact.uploadArtifact(
// name of the artifact
`${artifact.name}-${artifact.target}`,
// files to include (supports absolute and relative paths)
[artifact.path],
process.cwd(),
{
// optional: how long to retain the artifact
// if unspecified, defaults to repository/org retention settings (the limit of this value)
retentionDays: 10
}
);
console.log(`Created artifact with id: ${id} (bytes: ${size}`);
}
}
env:
ARTIFACTS: ${{ steps.goreleaser.outputs.artifacts }}
- name: Chocolatey
uses: ./.github/actions/choco
with:
version: v0.0.0-pr

View File

@@ -1,23 +0,0 @@
# Codespell configuration is within .codespellrc
---
name: Codespell
on:
push:
branches: [master]
pull_request:
branches: [master]
permissions:
contents: read
jobs:
codespell:
name: Check for spelling errors
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Codespell
uses: codespell-project/actions-codespell@v2

View File

@@ -1,30 +0,0 @@
name: promote
on:
schedule:
- cron: '0 2 1 * *'
workflow_dispatch: {}
jobs:
release:
if: vars.ENABLE_PROMOTE || github.event_name != 'schedule'
name: promote
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
ref: master
token: ${{ secrets.GORELEASER_GITHUB_TOKEN }}
- uses: fregante/setup-git-user@v2
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
- uses: actions/cache@v4
if: ${{ !env.ACT }}
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- run: make promote

View File

@@ -1,72 +0,0 @@
name: release
on:
push:
tags:
- v*
jobs:
release:
# TODO use environment to scope secrets
name: release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
- uses: actions/cache@v4
if: ${{ !env.ACT }}
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GORELEASER_GITHUB_TOKEN || github.token }}
- name: Winget
uses: vedantmgoyal2009/winget-releaser@v2
with:
identifier: nektos.act
installers-regex: '_Windows_\w+\.zip$'
token: ${{ secrets.WINGET_TOKEN }}
if: env.ENABLED
env:
ENABLED: ${{ secrets.WINGET_TOKEN && '1' || '' }}
- name: Chocolatey
uses: ./.github/actions/choco
with:
version: ${{ github.ref }}
apiKey: ${{ secrets.CHOCO_APIKEY }}
push: true
if: env.ENABLED
env:
ENABLED: ${{ secrets.CHOCO_APIKEY && '1' || '' }}
# TODO use ssh deployment key
- name: GitHub CLI extension
uses: actions/github-script@v8
with:
github-token: ${{ secrets.CLI_GITHUB_TOKEN || secrets.GORELEASER_GITHUB_TOKEN }}
script: |
const mainRef = (await github.rest.git.getRef({
owner: context.repo.owner,
repo: 'gh-act',
ref: 'heads/main',
})).data;
console.log(mainRef);
github.rest.git.createRef({
owner: 'nektos',
repo: 'gh-act',
ref: context.ref,
sha: mainRef.object.sha,
});
if: env.ENABLED
env:
ENABLED: ${{ (secrets.CLI_GITHUB_TOKEN || secrets.GORELEASER_GITHUB_TOKEN) && '1' || '' }}

View File

@@ -1,23 +0,0 @@
name: 'Close stale issues'
on:
schedule:
- cron: '0 0 * * *'
jobs:
stale:
name: Stale
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v10
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'Issue is stale and will be closed in 14 days unless there is new activity'
stale-pr-message: 'PR is stale and will be closed in 14 days unless there is new activity'
stale-issue-label: 'stale'
exempt-issue-labels: 'stale-exempt,kind/feature-request'
stale-pr-label: 'stale'
exempt-pr-labels: 'stale-exempt'
remove-stale-when-updated: 'True'
operations-per-run: 500
days-before-stale: 180
days-before-close: 14

1
.gitignore vendored
View File

@@ -2,7 +2,6 @@
.env .env
.runner .runner
coverage.txt coverage.txt
/gitea-vet
/config.yaml /config.yaml
# Jetbrains # Jetbrains

View File

@@ -1,2 +0,0 @@
b910a42edfab7a02b08a52ecef203fd419725642:pkg/container/testdata/docker-pull-options/config.json:generic-api-key:4
710a3ac94c3dc0eaf680d417c87f37f92b4887f4:pkg/container/docker_pull_test.go:generic-api-key:45

View File

@@ -13,6 +13,7 @@ linters:
- forbidigo - forbidigo
- gocheckcompilerdirectives - gocheckcompilerdirectives
- gocritic - gocritic
- goheader
- govet - govet
- ineffassign - ineffassign
- mirror - mirror
@@ -35,23 +36,61 @@ linters:
rules: rules:
main: main:
deny: deny:
- pkg: io/ioutil
desc: use os or io instead
- pkg: golang.org/x/exp
desc: it's experimental and unreliable
- pkg: github.com/pkg/errors - pkg: github.com/pkg/errors
desc: Please use "errors" package from standard library desc: use builtin errors package instead
- pkg: gotest.tools/v3 nolintlint:
desc: Please keep tests unified using only github.com/stretchr/testify allow-unused: false
- pkg: log require-explanation: true
desc: Please keep logging unified using only github.com/sirupsen/logrus require-specific: true
gocritic: gocritic:
enabled-checks:
- equalFold
disabled-checks: disabled-checks:
- ifElseChain - ifElseChain
gocyclo: revive:
min-complexity: 20 severity: error
importas: rules:
alias: - name: blank-imports
- pkg: github.com/sirupsen/logrus - name: constant-logical-expr
alias: log - name: context-as-argument
- pkg: github.com/stretchr/testify/assert - name: context-keys-type
alias: assert - name: dot-imports
- name: empty-lines
- name: error-return
- name: error-strings
- name: exported
- name: identical-branches
- name: if-return
- name: increment-decrement
- name: modifies-value-receiver
- name: package-comments
- name: redefines-builtin-id
- name: superfluous-else
- name: time-naming
- name: unexported-return
- name: var-declaration
- name: var-naming
staticcheck:
checks:
- all
- -ST1005
usetesting:
os-temp-dir: true
perfsprint:
concat-loop: false
govet:
enable:
- nilness
- unusedwrite
goheader:
values:
regexp:
HEADER: 'Copyright \d{4} The Gitea Authors\. All rights reserved\.(\nCopyright [^\n]+)*\nSPDX-License-Identifier: MIT'
template: '{{ HEADER }}'
exclusions: exclusions:
generated: lax generated: lax
presets: presets:
@@ -59,21 +98,28 @@ linters:
- common-false-positives - common-false-positives
- legacy - legacy
- std-error-handling - std-error-handling
paths: rules:
- report - linters:
- third_party$ - forbidigo
- builtin$ path: cmd
- examples$
issues: issues:
max-issues-per-linter: 0 max-issues-per-linter: 0
max-same-issues: 0 max-same-issues: 0
formatters: formatters:
enable: enable:
- goimports - gci
- gofumpt
settings:
gci:
custom-order: true
sections:
- standard
- prefix(gitea.com/gitea/act_runner)
- blank
- default
gofumpt:
extra-rules: true
exclusions: exclusions:
generated: lax generated: lax
paths: run:
- report timeout: 10m
- third_party$
- builtin$
- examples$

View File

@@ -1,3 +0,0 @@
gitea_urls:
api: https://gitea.com/api/v1/
download: https://gitea.com/

View File

@@ -1,54 +0,0 @@
version: 2
before:
hooks:
- go mod tidy
builds:
- env:
- CGO_ENABLED=0
goos:
- darwin
- linux
- windows
goarch:
- amd64
- '386'
- arm64
- arm
- riscv64
goarm:
- '6'
- '7'
ignore:
- goos: windows
goarm: '6'
binary: act
checksum:
name_template: 'checksums.txt'
archives:
- name_template: >-
{{ .ProjectName }}_
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
format_overrides:
- goos: windows
formats:
- zip
changelog:
groups:
- title: 'New Features'
regexp: "^.*feat[(\\w)]*:+.*$"
order: 0
- title: 'Bug fixes'
regexp: "^.*fix[(\\w)]*:+.*$"
order: 1
- title: 'Documentation updates'
regexp: "^.*docs[(\\w)]*:+.*$"
order: 2
- title: 'Other'
order: 999
release:
prerelease: auto
mode: append

View File

@@ -1,12 +0,0 @@
# Default state for all rules
default: true
# MD013/line-length - Line length
MD013:
line_length: 1024
# MD033/no-inline-html - Inline HTML
MD033: false
# MD041/first-line-heading/first-line-h1 - First line in a file should be a top-level heading
MD041: false

View File

@@ -1,20 +0,0 @@
---
APPLY_FIXES: none
DISABLE:
- ACTION
- BASH
- COPYPASTE
- DOCKERFILE
- GO
- JAVASCRIPT
- SPELL
DISABLE_LINTERS:
- YAML_YAMLLINT
- MARKDOWN_MARKDOWN_TABLE_FORMATTER
- MARKDOWN_MARKDOWN_LINK_CHECK
- REPOSITORY_CHECKOV
- REPOSITORY_TRIVY
FILTER_REGEX_EXCLUDE: (.*testdata/*|install.sh|pkg/container/docker_cli.go|pkg/container/DOCKER_LICENSE|VERSION)
MARKDOWN_MARKDOWNLINT_CONFIG_FILE: .markdownlint.yml
PARALLEL: false
PRINT_ALPACA: false

View File

@@ -1,98 +0,0 @@
pull_request_rules:
- name: warn on conflicts
conditions:
- -draft
- -closed
- -merged
- conflict
actions:
comment:
message: '@{{author}} this pull request is now in conflict 😩'
label:
add:
- conflict
- name: remove conflict label if not needed
conditions:
- -conflict
actions:
label:
remove:
- conflict
- name: warn on needs-work
conditions:
- -draft
- -closed
- -merged
- or:
- check-failure=lint
- check-failure=test-linux
- check-failure=codecov/patch
- check-failure=codecov/project
- check-failure=snapshot
actions:
comment:
message: '@{{author}} this pull request has failed checks 🛠'
label:
add:
- needs-work
- name: remove needs-work label if not needed
conditions:
- check-success=lint
- check-success=test-linux
- check-success=codecov/patch
- check-success=codecov/project
- check-success=snapshot
actions:
label:
remove:
- needs-work
- name: Automatic maintainer assignment
conditions:
- '-approved-reviews-by=@nektos/act-maintainers'
- -draft
- -merged
- -closed
- -conflict
- check-success=lint
- check-success=test-linux
- check-success=codecov/patch
- check-success=codecov/project
- check-success=snapshot
actions:
request_reviews:
teams:
- '@nektos/act-maintainers'
- name: Automatic merge on approval
conditions: []
actions:
queue:
queue_rules:
- name: default
queue_conditions:
- '#changes-requested-reviews-by=0'
- or:
- 'approved-reviews-by=@nektos/act-committers'
- 'author~=^dependabot(|-preview)\[bot\]$'
- and:
- 'approved-reviews-by=@nektos/act-maintainers'
- '#approved-reviews-by>=2'
- and:
- 'author=@nektos/act-maintainers'
- 'approved-reviews-by=@nektos/act-maintainers'
- '#approved-reviews-by>=1'
- -draft
- -merged
- -closed
- check-success=lint
- check-success=test-linux
- check-success=codecov/patch
- check-success=codecov/project
- check-success=snapshot
merge_conditions:
- check-success=lint
- check-success=test-linux
- check-success=codecov/patch
- check-success=codecov/project
- check-success=snapshot
merge_method: squash

View File

@@ -1,2 +0,0 @@
**/testdata
pkg/runner/res

View File

@@ -1,7 +0,0 @@
overrides:
- files: '*.yml'
options:
singleQuote: true
- files: '*.json'
options:
singleQuote: false

View File

@@ -1,9 +0,0 @@
{
"recommendations": [
"editorconfig.editorconfig",
"golang.go",
"davidanson.vscode-markdownlint",
"esbenp.prettier-vscode",
"redhat.vscode-yaml"
]
}

14
.vscode/settings.json vendored
View File

@@ -1,14 +0,0 @@
{
"go.lintTool": "golangci-lint",
"go.lintFlags": ["--fix"],
"go.testTimeout": "300s",
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[markdown]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[yaml]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}

10
AGENTS.md Normal file
View File

@@ -0,0 +1,10 @@
- Use `make help` to find available development targets
- Run `make fmt` to format `.go` files, and run `make lint-go` to lint them
- Run `make tidy` after any `go.mod` changes
- Run single go unit tests with `go test -run '^TestName$' ./modulepath/`
- Add the current year into the copyright header of new `.go` files
- Ensure no trailing whitespace in edited files
- Never force-push, amend, or squash unless asked. Use new commits and normal push for pull request updates
- Preserve existing code comments, do not remove or rewrite comments that are still relevant
- Include authorship attribution in issue and pull request comments
- Add `Co-Authored-By` lines to all commits, indicating name and model used

1
CLAUDE.md Normal file
View File

@@ -0,0 +1 @@
@AGENTS.md

View File

@@ -1 +0,0 @@
* @nektos/act-maintainers

View File

@@ -1,69 +0,0 @@
# Contributing to Act
Help wanted! We'd love your contributions to Act. Please review the following guidelines before contributing. Also, feel free to propose changes to these guidelines by updating this file and submitting a pull request.
- [I have a question...](#questions)
- [I found a bug...](#bugs)
- [I have a feature request...](#features)
- [I have a contribution to share...](#process)
## <a id="questions"></a> Have a Question?
Please don't open a GitHub issue for questions about how to use `act`, as the goal is to use issues for managing bugs and feature requests. Issues that are related to general support will be closed and redirected to our gitter room.
For all support related questions, please ask the question in discussions: [actions-oss/act-cli](https://github.com/actions-oss/act-cli/discussions).
## <a id="bugs"></a> Found a Bug?
If you've identified a bug in `act`, please [submit an issue](#issue) to our GitHub repo: [actions-oss/act-cli](https://github.com/actions-oss/act-cli/issues/new). Please also feel free to submit a [Pull Request](#pr) with a fix for the bug!
## <a id="features"></a> Have a Feature Request?
All feature requests should start with [submitting an issue](#issue) documenting the user story and acceptance criteria. Again, feel free to submit a [Pull Request](#pr) with a proposed implementation of the feature.
## <a id="process"></a> Ready to Contribute
### <a id="issue"></a> Create an issue
Before submitting a new issue, please search the issues to make sure there isn't a similar issue doesn't already exist.
Assuming no existing issues exist, please ensure you include required information when submitting the issue to ensure we can quickly reproduce your issue.
We may have additional questions and will communicate through the GitHub issue, so please respond back to our questions to help reproduce and resolve the issue as quickly as possible.
New issues can be created with in our [GitHub repo](https://github.com/actions-oss/act-cli/issues/new).
### <a id="pr"></a>Pull Requests
Pull requests should target the `master` branch. Please also reference the issue from the description of the pull request using [special keyword syntax](https://help.github.com/articles/closing-issues-via-commit-messages/) to auto close the issue when the PR is merged. For example, include the phrase `fixes #14` in the PR description to have issue #14 auto close. Please send documentation updates for the [act user guide](https://actions-oss.github.io/act-docs/) to [actions-oss/act-docs](https://github.com/actions-oss/act-docs).
### <a id="style"></a> Styleguide
When submitting code, please make every effort to follow existing conventions and style in order to keep the code as readable as possible. Here are a few points to keep in mind:
- Please run `go fmt ./...` before committing to ensure code aligns with go standards.
- We use [`golangci-lint`](https://golangci-lint.run/) for linting Go code, run `golangci-lint run --fix` before submitting PR. Editors such as Visual Studio Code or JetBrains IntelliJ; with Go support plugin will offer `golangci-lint` automatically.
- There are additional linters and formatters for files such as Markdown documents or YAML/JSON:
- Please refer to the [Makefile](Makefile) or [`lint` job in our workflow](.github/workflows/checks.yml) to see how to those linters/formatters work.
- You can lint codebase by running `go run main.go -j lint --env RUN_LOCAL=true` or `act -j lint --env RUN_LOCAL=true`
- In `Makefile`, there are tools that require `npx` which is shipped with `nodejs`.
- Our `Makefile` exports `GITHUB_TOKEN` from `~/.config/github/token`, you have been warned.
- You can run `make pr` to cleanup dependencies, format/lint code and run tests.
- All dependencies must be defined in the `go.mod` file.
- Advanced IDEs and code editors (like VSCode) will take care of that, but to be sure, run `go mod tidy` to validate dependencies.
- For details on the approved style, check out [Effective Go](https://golang.org/doc/effective_go.html).
- Before running tests, please be aware that they are multi-architecture so for them to not fail, you need to run `docker run --privileged --rm tonistiigi/binfmt --install amd64,arm64` before ([more info available in #765](https://github.com/nektos/act/issues/765)).
Also, consider the original design principles:
- **Polyglot** - There will be no prescribed language or framework for developing the microservices. The only requirement will be that the service will be run inside a container and exposed via an HTTP endpoint.
- **Cloud Provider** - At this point, the tool will assume AWS for the cloud provider and will not be written in a cloud agnostic manner. However, this does not preclude refactoring to add support for other providers at a later time.
- **Declarative** - All resource administration will be handled in a declarative vs. imperative manner. A file will be used to declared the desired state of the resources and the tool will simply assert the actual state matches the desired state. The tool will accomplish this by generating CloudFormation templates.
- **Stateless** - The tool will not maintain its own state. Rather, it will rely on the CloudFormation stacks to determine the state of the platform.
- **Secure** - All security will be managed by AWS IAM credentials. No additional authentication or authorization mechanisms will be introduced.
### License
By contributing your code, you agree to license your contribution under the terms of the [MIT License](LICENSE).
All files are released with the MIT license.

View File

@@ -1,18 +1,16 @@
DIST := dist DIST := dist
EXECUTABLE := act_runner EXECUTABLE := act_runner
GOFMT ?= gofumpt -l
DIST_DIRS := $(DIST)/binaries $(DIST)/release DIST_DIRS := $(DIST)/binaries $(DIST)/release
GO ?= go GO ?= go
SHASUM ?= shasum -a 256 SHASUM ?= shasum -a 256
HAS_GO = $(shell hash $(GO) > /dev/null 2>&1 && echo "GO" || echo "NOGO" ) HAS_GO = $(shell hash $(GO) > /dev/null 2>&1 && echo "GO" || echo "NOGO" )
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
XGO_VERSION := go-1.26.x XGO_VERSION := go-1.26.x
GXZ_PAGAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.10 GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.10
LINUX_ARCHS ?= linux/amd64,linux/arm64 LINUX_ARCHS ?= linux/amd64,linux/arm64
DARWIN_ARCHS ?= darwin-12/amd64,darwin-12/arm64 DARWIN_ARCHS ?= darwin-12/amd64,darwin-12/arm64
WINDOWS_ARCHS ?= windows/amd64 WINDOWS_ARCHS ?= windows/amd64
GO_FMT_FILES := $(shell find . -type f -name "*.go" ! -name "generated.*")
GOFILES := $(shell find . -type f -name "*.go" -o -name "go.mod" ! -name "generated.*") GOFILES := $(shell find . -type f -name "*.go" -o -name "go.mod" ! -name "generated.*")
DOCKER_IMAGE ?= gitea/act_runner DOCKER_IMAGE ?= gitea/act_runner
@@ -20,13 +18,13 @@ DOCKER_TAG ?= nightly
DOCKER_REF := $(DOCKER_IMAGE):$(DOCKER_TAG) DOCKER_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)
DOCKER_ROOTLESS_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)-dind-rootless DOCKER_ROOTLESS_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)-dind-rootless
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.10.1 GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.4
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
ifneq ($(shell uname), Darwin) STATIC ?=
EXTLDFLAGS = -extldflags "-static" $(null) EXTLDFLAGS ?=
else ifneq ($(STATIC),)
EXTLDFLAGS = EXTLDFLAGS = -extldflags "-static"
endif endif
ifeq ($(HAS_GO), GO) ifeq ($(HAS_GO), GO)
@@ -68,19 +66,19 @@ else
endif endif
endif endif
GO_PACKAGES_TO_VET ?= $(filter-out gitea.com/gitea/act_runner/internal/pkg/client/mocks,$(shell $(GO) list ./...))
TAGS ?= TAGS ?=
LDFLAGS ?= -X "gitea.com/gitea/act_runner/internal/pkg/ver.version=v$(RELASE_VERSION)" LDFLAGS ?= -X "gitea.com/gitea/act_runner/internal/pkg/ver.version=v$(RELASE_VERSION)"
.PHONY: all
all: build all: build
fmt: .PHONY: help
@hash gofumpt > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ help: Makefile ## print Makefile help information.
$(GO) install mvdan.cc/gofumpt@latest; \ @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m[TARGETS] default target: build\033[0m\n\n\033[35mTargets:\033[0m\n"} /^[0-9A-Za-z._-]+:.*?##/ { printf " \033[36m%-45s\033[0m %s\n", $$1, $$2 }' Makefile
fi
$(GOFMT) -w $(GO_FMT_FILES) .PHONY: fmt
fmt: ## format the Go code
$(GO) run $(GOLANGCI_LINT_PACKAGE) fmt
.PHONY: go-check .PHONY: go-check
go-check: go-check:
@@ -93,23 +91,24 @@ go-check:
fi fi
.PHONY: fmt-check .PHONY: fmt-check
fmt-check: fmt-check: fmt
@hash gofumpt > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @diff=$$(git diff --color=always); \
$(GO) install mvdan.cc/gofumpt@latest; \
fi
@diff=$$($(GOFMT) -d $(GO_FMT_FILES)); \
if [ -n "$$diff" ]; then \ if [ -n "$$diff" ]; then \
echo "Please run 'make fmt' and commit the result:"; \ echo "Please run 'make fmt' and commit the result:"; \
echo "$${diff}"; \ printf "%s" "$${diff}"; \
exit 1; \ exit 1; \
fi; fi
.PHONY: deps-tools .PHONY: deps-tools
deps-tools: ## install tool dependencies deps-tools: ## install tool dependencies
$(GO) install $(GOVULNCHECK_PACKAGE) $(GO) install $(GOLANGCI_LINT_PACKAGE) & \
$(GO) install $(GXZ_PACKAGE) & \
$(GO) install $(XGO_PACKAGE) & \
$(GO) install $(GOVULNCHECK_PACKAGE) & \
wait
.PHONY: lint .PHONY: lint
lint: lint-go vet lint: lint-go ## lint everything
.PHONY: lint-go .PHONY: lint-go
lint-go: ## lint go files lint-go: ## lint go files
@@ -124,64 +123,59 @@ security-check: deps-tools
GOEXPERIMENT= $(GO) run $(GOVULNCHECK_PACKAGE) -show color ./... || true GOEXPERIMENT= $(GO) run $(GOVULNCHECK_PACKAGE) -show color ./... || true
.PHONY: tidy .PHONY: tidy
tidy: tidy: ## run go mod tidy
$(GO) mod tidy $(GO) mod tidy
.PHONY: tidy-check .PHONY: tidy-check
tidy-check: tidy tidy-check: tidy
@diff=$$(git diff -- go.mod go.sum); \ @diff=$$(git diff --color=always -- go.mod go.sum); \
if [ -n "$$diff" ]; then \ if [ -n "$$diff" ]; then \
echo "Please run 'make tidy' and commit the result:"; \ echo "Please run 'make tidy' and commit the result:"; \
echo "$${diff}"; \ printf "%s" "$${diff}"; \
exit 1; \ exit 1; \
fi fi
test: fmt-check security-check .PHONY: test
@$(GO) test -race -v -cover -coverprofile coverage.txt ./... && echo "\n==>\033[32m Ok\033[m\n" || exit 1 test: fmt-check security-check ## test everything
@$(GO) test -race -short -v -cover -coverprofile coverage.txt ./... && echo "\n==>\033[32m Ok\033[m\n" || exit 1
.PHONY: vet .PHONY: install
vet: install: $(GOFILES) ## install the act_runner binary via `go install`
@echo "Running go vet..." $(GO) install -v -tags '$(TAGS)' -ldflags '-s -w $(EXTLDFLAGS) $(LDFLAGS)'
@$(GO) build code.gitea.io/gitea-vet
@$(GO) vet -vettool=gitea-vet $(GO_PACKAGES_TO_VET)
install: $(GOFILES) .PHONY: build
$(GO) install -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' build: go-check $(EXECUTABLE) ## build the act_runner binary
build: go-check $(EXECUTABLE)
$(EXECUTABLE): $(GOFILES) $(EXECUTABLE): $(GOFILES)
$(GO) build -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o $@ $(GO) build -v -tags '$(TAGS)' -ldflags '-s -w $(EXTLDFLAGS) $(LDFLAGS)' -o $@
.PHONY: deps-backend .PHONY: deps-backend
deps-backend: deps-backend: ## install backend dependencies
$(GO) mod download $(GO) mod download
$(GO) install $(GXZ_PAGAGE)
$(GO) install $(XGO_PACKAGE)
.PHONY: release .PHONY: release
release: release-windows release-linux release-darwin release-copy release-compress release-check release: release-windows release-linux release-darwin release-copy release-compress release-check ## build release artifacts
$(DIST_DIRS): $(DIST_DIRS):
mkdir -p $(DIST_DIRS) mkdir -p $(DIST_DIRS)
.PHONY: release-windows .PHONY: release-windows
release-windows: | $(DIST_DIRS) release-windows: | $(DIST_DIRS)
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets '$(WINDOWS_ARCHS)' -out $(EXECUTABLE)-$(VERSION) . CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w -linkmode external -extldflags "-static" $(LDFLAGS)' -targets '$(WINDOWS_ARCHS)' -out $(EXECUTABLE)-$(VERSION) .
ifeq ($(CI),true) ifeq ($(CI),true)
cp -r /build/* $(DIST)/binaries/ cp -r /build/* $(DIST)/binaries/
endif endif
.PHONY: release-linux .PHONY: release-linux
release-linux: | $(DIST_DIRS) release-linux: | $(DIST_DIRS)
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets '$(LINUX_ARCHS)' -out $(EXECUTABLE)-$(VERSION) . CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w -linkmode external -extldflags "-static" $(LDFLAGS)' -targets '$(LINUX_ARCHS)' -out $(EXECUTABLE)-$(VERSION) .
ifeq ($(CI),true) ifeq ($(CI),true)
cp -r /build/* $(DIST)/binaries/ cp -r /build/* $(DIST)/binaries/
endif endif
.PHONY: release-darwin .PHONY: release-darwin
release-darwin: | $(DIST_DIRS) release-darwin: | $(DIST_DIRS)
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '$(LDFLAGS)' -targets '$(DARWIN_ARCHS)' -out $(EXECUTABLE)-$(VERSION) . CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w $(LDFLAGS)' -targets '$(DARWIN_ARCHS)' -out $(EXECUTABLE)-$(VERSION) .
ifeq ($(CI),true) ifeq ($(CI),true)
cp -r /build/* $(DIST)/binaries/ cp -r /build/* $(DIST)/binaries/
endif endif
@@ -196,18 +190,20 @@ release-check: | $(DIST_DIRS)
.PHONY: release-compress .PHONY: release-compress
release-compress: | $(DIST_DIRS) release-compress: | $(DIST_DIRS)
cd $(DIST)/release/; for file in `find . -type f -name "*"`; do echo "compressing $${file}" && $(GO) run $(GXZ_PAGAGE) -k -9 $${file}; done; cd $(DIST)/release/; for file in `find . -type f -name "*"`; do echo "compressing $${file}" && $(GO) run $(GXZ_PACKAGE) -k -9 $${file}; done;
.PHONY: docker .PHONY: docker
docker: docker: ## build the docker image
if ! docker buildx version >/dev/null 2>&1; then \ if ! docker buildx version >/dev/null 2>&1; then \
ARG_DISABLE_CONTENT_TRUST=--disable-content-trust=false; \ ARG_DISABLE_CONTENT_TRUST=--disable-content-trust=false; \
fi; \ fi; \
docker build $${ARG_DISABLE_CONTENT_TRUST} -t $(DOCKER_REF) . docker build $${ARG_DISABLE_CONTENT_TRUST} -t $(DOCKER_REF) .
clean: .PHONY: clean
clean: ## delete binary and coverage files
$(GO) clean -x -i ./... $(GO) clean -x -i ./...
rm -rf coverage.txt $(EXECUTABLE) $(DIST) rm -rf coverage.txt $(EXECUTABLE) $(DIST)
version: .PHONY: version
version: ## print the version
@echo $(VERSION) @echo $(VERSION)

View File

@@ -1,6 +1,6 @@
# act runner # act runner
Act runner is a runner for Gitea based on [Gitea fork](https://gitea.com/gitea/act) of [act](https://github.com/nektos/act). Act runner is a runner for Gitea.
## Installation ## Installation

View File

@@ -1,5 +0,0 @@
VERIFICATION
Verification is intended to assist the Chocolatey moderators and community
in verifying that this package's contents are trustworthy.
Checksums: https://github.com/nektos/act/releases, in the checksums.txt file

View File

@@ -1 +0,0 @@
0.4.0

View File

@@ -1,26 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Do not remove this test for UTF-8: if “Ω” doesnt appear as greek uppercase omega letter enclosed in quotation marks, you should use an editor that supports UTF-8, not this one. -->
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
<metadata>
<id>act-cli</id>
<version>0.0.0</version>
<packageSourceUrl>https://github.com/nektos/act</packageSourceUrl>
<owners>nektos</owners>
<title>act (GitHub Actions CLI)</title>
<authors>nektos</authors>
<projectUrl>https://github.com/nektos/act</projectUrl>
<iconUrl>https://raw.githubusercontent.com/wiki/nektos/act/img/logo-150.png</iconUrl>
<copyright>Nektos</copyright>
<licenseUrl>https://raw.githubusercontent.com/nektos/act/master/LICENSE</licenseUrl>
<requireLicenseAcceptance>true</requireLicenseAcceptance>
<projectSourceUrl>https://github.com/nektos/act</projectSourceUrl>
<docsUrl>https://raw.githubusercontent.com/nektos/act/master/README.md</docsUrl>
<bugTrackerUrl>https://github.com/nektos/act/issues</bugTrackerUrl>
<tags>act github-actions actions golang ci devops</tags>
<summary>Run your GitHub Actions locally 🚀</summary>
<description>Run your GitHub Actions locally 🚀</description>
</metadata>
<files>
<file src="tools/**" target="tools" />
</files>
</package>

22
act/LICENSE Normal file
View File

@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2022 The Gitea Authors
Copyright (c) 2019
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.

View File

@@ -1,3 +1,7 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// Copyright 2023 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// Package artifactcache provides a cache handler for the runner. // Package artifactcache provides a cache handler for the runner.
// //
// Inspired by https://github.com/sp-ricard-valverde/github-act-cache-server // Inspired by https://github.com/sp-ricard-valverde/github-act-cache-server

View File

@@ -1,3 +1,7 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// Copyright 2023 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package artifactcache package artifactcache
import ( import (
@@ -15,12 +19,12 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"gitea.com/gitea/act_runner/act/common"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/timshannon/bolthold" "github.com/timshannon/bolthold"
"go.etcd.io/bbolt" "go.etcd.io/bbolt"
"github.com/actions-oss/act-cli/pkg/common"
) )
const ( const (
@@ -39,7 +43,6 @@ type Handler struct {
gcAt time.Time gcAt time.Time
outboundIP string outboundIP string
externalAddress string
} }
func StartHandler(dir, outboundIP string, port uint16, logger logrus.FieldLogger) (*Handler, error) { func StartHandler(dir, outboundIP string, port uint16, logger logrus.FieldLogger) (*Handler, error) {
@@ -75,7 +78,7 @@ func StartHandler(dir, outboundIP string, port uint16, logger logrus.FieldLogger
if outboundIP != "" { if outboundIP != "" {
h.outboundIP = outboundIP h.outboundIP = outboundIP
} else if ip := common.GetOutboundIP(); ip == nil { } else if ip := common.GetOutboundIP(); ip == nil {
return nil, fmt.Errorf("unable to determine outbound IP address") return nil, errors.New("unable to determine outbound IP address")
} else { } else {
h.outboundIP = ip.String() h.outboundIP = ip.String()
} }
@@ -111,63 +114,7 @@ func StartHandler(dir, outboundIP string, port uint16, logger logrus.FieldLogger
return h, nil return h, nil
} }
func CreateHandler(dir, externalAddress string, logger logrus.FieldLogger) (*Handler, http.Handler, error) {
h := &Handler{}
if logger == nil {
discard := logrus.New()
discard.Out = io.Discard
logger = discard
}
logger = logger.WithField("module", "artifactcache")
h.logger = logger
if dir == "" {
home, err := os.UserHomeDir()
if err != nil {
return nil, nil, err
}
dir = filepath.Join(home, ".cache", "actcache")
}
if err := os.MkdirAll(dir, 0o755); err != nil {
return nil, nil, err
}
h.dir = dir
storage, err := NewStorage(filepath.Join(dir, "cache"))
if err != nil {
return nil, nil, err
}
h.storage = storage
if externalAddress != "" {
h.externalAddress = externalAddress
} else if ip := common.GetOutboundIP(); ip == nil {
return nil, nil, fmt.Errorf("unable to determine outbound IP address")
} else {
h.outboundIP = ip.String()
}
router := httprouter.New()
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()
return h, router, nil
}
func (h *Handler) ExternalURL() string { func (h *Handler) ExternalURL() string {
if h.externalAddress != "" {
return h.externalAddress
}
// TODO: make the external url configurable if necessary // TODO: make the external url configurable if necessary
return fmt.Sprintf("http://%s:%d", return fmt.Sprintf("http://%s:%d",
h.outboundIP, h.outboundIP,
@@ -284,7 +231,7 @@ func (h *Handler) reserve(w http.ResponseWriter, r *http.Request, _ httprouter.P
// PATCH /_apis/artifactcache/caches/:id // PATCH /_apis/artifactcache/caches/:id
func (h *Handler) upload(w http.ResponseWriter, r *http.Request, params httprouter.Params) { func (h *Handler) upload(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
id, err := strconv.ParseUint(params.ByName("id"), 10, 64) id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
if err != nil { if err != nil {
h.responseJSON(w, r, 400, err) h.responseJSON(w, r, 400, err)
return return
@@ -380,13 +327,13 @@ func (h *Handler) commit(w http.ResponseWriter, r *http.Request, params httprout
// GET /_apis/artifactcache/artifacts/:id // GET /_apis/artifactcache/artifacts/:id
func (h *Handler) get(w http.ResponseWriter, r *http.Request, params httprouter.Params) { func (h *Handler) get(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
id, err := strconv.ParseUint(params.ByName("id"), 10, 64) id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
if err != nil { if err != nil {
h.responseJSON(w, r, 400, err) h.responseJSON(w, r, 400, err)
return return
} }
h.useCache(id) h.useCache(id)
h.storage.Serve(w, r, id) h.storage.Serve(w, r, uint64(id))
} }
// POST /_apis/artifactcache/clean // POST /_apis/artifactcache/clean
@@ -420,7 +367,7 @@ func findCache(db *bolthold.Store, keys []string, version string) (*Cache, error
} }
return cache, nil return cache, nil
} }
prefixPattern := fmt.Sprintf("^%s", regexp.QuoteMeta(prefix)) prefixPattern := "^" + regexp.QuoteMeta(prefix)
re, err := regexp.Compile(prefixPattern) re, err := regexp.Compile(prefixPattern)
if err != nil { if err != nil {
continue continue
@@ -437,7 +384,7 @@ func findCache(db *bolthold.Store, keys []string, version string) (*Cache, error
} }
return cache, nil return cache, nil
} }
return nil, nil return nil, nil //nolint:nilnil // pre-existing issue from nektos/act
} }
func insertCache(db *bolthold.Store, cache *Cache) error { func insertCache(db *bolthold.Store, cache *Cache) error {
@@ -451,7 +398,7 @@ func insertCache(db *bolthold.Store, cache *Cache) error {
return nil return nil
} }
func (h *Handler) useCache(id uint64) { func (h *Handler) useCache(id int64) {
db, err := h.openDB() db, err := h.openDB()
if err != nil { if err != nil {
return return
@@ -472,6 +419,7 @@ const (
keepOld = 5 * time.Minute keepOld = 5 * time.Minute
) )
//nolint:gocyclo // function handles many cases
func (h *Handler) gcCache() { func (h *Handler) gcCache() {
if h.gcing.Load() { if h.gcing.Load() {
return return

View File

@@ -1,3 +1,7 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// Copyright 2023 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package artifactcache package artifactcache
import ( import (
@@ -41,7 +45,7 @@ func TestHandler(t *testing.T) {
require.NoError(t, handler.Close()) require.NoError(t, handler.Close())
assert.Nil(t, handler.server) assert.Nil(t, handler.server)
assert.Nil(t, handler.listener) assert.Nil(t, handler.listener)
_, err := http.Post(fmt.Sprintf("%s/caches/%d", base, 1), "", nil) _, err := http.Post(fmt.Sprintf("%s/caches/%d", base, 1), "", nil) //nolint:bodyclose // pre-existing issue from nektos/act
assert.Error(t, err) assert.Error(t, err)
}) })
}() }()
@@ -49,7 +53,7 @@ func TestHandler(t *testing.T) {
t.Run("get not exist", func(t *testing.T) { t.Run("get not exist", func(t *testing.T) {
key := strings.ToLower(t.Name()) key := strings.ToLower(t.Name())
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20" version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, key, version)) 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) require.NoError(t, err)
require.Equal(t, 204, resp.StatusCode) require.Equal(t, 204, resp.StatusCode)
}) })
@@ -64,7 +68,7 @@ func TestHandler(t *testing.T) {
}) })
t.Run("clean", func(t *testing.T) { t.Run("clean", func(t *testing.T) {
resp, err := http.Post(fmt.Sprintf("%s/clean", base), "", nil) resp, err := http.Post(base+"/clean", "", nil) //nolint:bodyclose // pre-existing issue from nektos/act
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode) assert.Equal(t, 200, resp.StatusCode)
}) })
@@ -72,7 +76,7 @@ func TestHandler(t *testing.T) {
t.Run("reserve with bad request", func(t *testing.T) { t.Run("reserve with bad request", func(t *testing.T) {
body := []byte(`invalid json`) body := []byte(`invalid json`)
require.NoError(t, err) require.NoError(t, err)
resp, err := http.Post(fmt.Sprintf("%s/caches", base), "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) require.NoError(t, err)
assert.Equal(t, 400, resp.StatusCode) assert.Equal(t, 400, resp.StatusCode)
}) })
@@ -90,7 +94,7 @@ func TestHandler(t *testing.T) {
Size: 100, Size: 100,
}) })
require.NoError(t, err) require.NoError(t, err)
resp, err := http.Post(fmt.Sprintf("%s/caches", base), "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) require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode) assert.Equal(t, 200, resp.StatusCode)
@@ -104,7 +108,7 @@ func TestHandler(t *testing.T) {
Size: 100, Size: 100,
}) })
require.NoError(t, err) require.NoError(t, err)
resp, err := http.Post(fmt.Sprintf("%s/caches", base), "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) require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode) assert.Equal(t, 200, resp.StatusCode)
@@ -117,11 +121,11 @@ func TestHandler(t *testing.T) {
t.Run("upload with bad id", func(t *testing.T) { t.Run("upload with bad id", func(t *testing.T) {
req, err := http.NewRequest(http.MethodPatch, req, err := http.NewRequest(http.MethodPatch,
fmt.Sprintf("%s/caches/invalid_id", base), bytes.NewReader(nil)) base+"/caches/invalid_id", bytes.NewReader(nil))
require.NoError(t, err) require.NoError(t, err)
req.Header.Set("Content-Type", "application/octet-stream") req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("Content-Range", "bytes 0-99/*") req.Header.Set("Content-Range", "bytes 0-99/*")
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req) //nolint:bodyclose // pre-existing issue from nektos/act
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 400, resp.StatusCode) assert.Equal(t, 400, resp.StatusCode)
}) })
@@ -132,7 +136,7 @@ func TestHandler(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
req.Header.Set("Content-Type", "application/octet-stream") req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("Content-Range", "bytes 0-99/*") req.Header.Set("Content-Range", "bytes 0-99/*")
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req) //nolint:bodyclose // pre-existing issue from nektos/act
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 400, resp.StatusCode) assert.Equal(t, 400, resp.StatusCode)
}) })
@@ -151,7 +155,7 @@ func TestHandler(t *testing.T) {
Size: 100, Size: 100,
}) })
require.NoError(t, err) require.NoError(t, err)
resp, err := http.Post(fmt.Sprintf("%s/caches", base), "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) require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode) assert.Equal(t, 200, resp.StatusCode)
@@ -167,12 +171,12 @@ func TestHandler(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
req.Header.Set("Content-Type", "application/octet-stream") req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("Content-Range", "bytes 0-99/*") req.Header.Set("Content-Range", "bytes 0-99/*")
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req) //nolint:bodyclose // pre-existing issue from nektos/act
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode) assert.Equal(t, 200, resp.StatusCode)
} }
{ {
resp, err := http.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) require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode) assert.Equal(t, 200, resp.StatusCode)
} }
@@ -182,7 +186,7 @@ func TestHandler(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
req.Header.Set("Content-Type", "application/octet-stream") req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("Content-Range", "bytes 0-99/*") req.Header.Set("Content-Range", "bytes 0-99/*")
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req) //nolint:bodyclose // pre-existing issue from nektos/act
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 400, resp.StatusCode) assert.Equal(t, 400, resp.StatusCode)
} }
@@ -202,7 +206,7 @@ func TestHandler(t *testing.T) {
Size: 100, Size: 100,
}) })
require.NoError(t, err) require.NoError(t, err)
resp, err := http.Post(fmt.Sprintf("%s/caches", base), "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) require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode) assert.Equal(t, 200, resp.StatusCode)
@@ -218,7 +222,7 @@ func TestHandler(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
req.Header.Set("Content-Type", "application/octet-stream") req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("Content-Range", "bytes xx-99/*") req.Header.Set("Content-Range", "bytes xx-99/*")
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req) //nolint:bodyclose // pre-existing issue from nektos/act
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 400, resp.StatusCode) assert.Equal(t, 400, resp.StatusCode)
} }
@@ -226,7 +230,7 @@ func TestHandler(t *testing.T) {
t.Run("commit with bad id", func(t *testing.T) { t.Run("commit with bad id", func(t *testing.T) {
{ {
resp, err := http.Post(fmt.Sprintf("%s/caches/invalid_id", base), "", nil) resp, err := http.Post(base+"/caches/invalid_id", "", nil) //nolint:bodyclose // pre-existing issue from nektos/act
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 400, resp.StatusCode) assert.Equal(t, 400, resp.StatusCode)
} }
@@ -234,7 +238,7 @@ func TestHandler(t *testing.T) {
t.Run("commit with not exist id", func(t *testing.T) { t.Run("commit with not exist id", func(t *testing.T) {
{ {
resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, 100), "", nil) resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, 100), "", nil) //nolint:bodyclose // pre-existing issue from nektos/act
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 400, resp.StatusCode) assert.Equal(t, 400, resp.StatusCode)
} }
@@ -254,7 +258,7 @@ func TestHandler(t *testing.T) {
Size: 100, Size: 100,
}) })
require.NoError(t, err) require.NoError(t, err)
resp, err := http.Post(fmt.Sprintf("%s/caches", base), "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) require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode) assert.Equal(t, 200, resp.StatusCode)
@@ -270,17 +274,17 @@ func TestHandler(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
req.Header.Set("Content-Type", "application/octet-stream") req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("Content-Range", "bytes 0-99/*") req.Header.Set("Content-Range", "bytes 0-99/*")
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req) //nolint:bodyclose // pre-existing issue from nektos/act
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode) assert.Equal(t, 200, resp.StatusCode)
} }
{ {
resp, err := http.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) require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode) assert.Equal(t, 200, resp.StatusCode)
} }
{ {
resp, err := http.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) require.NoError(t, err)
assert.Equal(t, 400, resp.StatusCode) assert.Equal(t, 400, resp.StatusCode)
} }
@@ -300,7 +304,7 @@ func TestHandler(t *testing.T) {
Size: 100, Size: 100,
}) })
require.NoError(t, err) require.NoError(t, err)
resp, err := http.Post(fmt.Sprintf("%s/caches", base), "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) require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode) assert.Equal(t, 200, resp.StatusCode)
@@ -316,31 +320,31 @@ func TestHandler(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
req.Header.Set("Content-Type", "application/octet-stream") req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("Content-Range", "bytes 0-59/*") req.Header.Set("Content-Range", "bytes 0-59/*")
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req) //nolint:bodyclose // pre-existing issue from nektos/act
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode) assert.Equal(t, 200, resp.StatusCode)
} }
{ {
resp, err := http.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) require.NoError(t, err)
assert.Equal(t, 500, resp.StatusCode) assert.Equal(t, 500, resp.StatusCode)
} }
}) })
t.Run("get with bad id", func(t *testing.T) { t.Run("get with bad id", func(t *testing.T) {
resp, err := http.Get(fmt.Sprintf("%s/artifacts/invalid_id", base)) resp, err := http.Get(base + "/artifacts/invalid_id") //nolint:bodyclose // pre-existing issue from nektos/act
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 400, resp.StatusCode) require.Equal(t, 400, resp.StatusCode)
}) })
t.Run("get with not exist id", func(t *testing.T) { t.Run("get with not exist id", func(t *testing.T) {
resp, err := http.Get(fmt.Sprintf("%s/artifacts/%d", base, 100)) resp, err := http.Get(fmt.Sprintf("%s/artifacts/%d", base, 100)) //nolint:bodyclose // pre-existing issue from nektos/act
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 404, resp.StatusCode) require.Equal(t, 404, resp.StatusCode)
}) })
t.Run("get with not exist id", func(t *testing.T) { t.Run("get with not exist id", func(t *testing.T) {
resp, err := http.Get(fmt.Sprintf("%s/artifacts/%d", base, 100)) resp, err := http.Get(fmt.Sprintf("%s/artifacts/%d", base, 100)) //nolint:bodyclose // pre-existing issue from nektos/act
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 404, resp.StatusCode) require.Equal(t, 404, resp.StatusCode)
}) })
@@ -371,7 +375,7 @@ func TestHandler(t *testing.T) {
key + "_a", key + "_a",
}, ",") }, ",")
resp, err := http.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) require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode) require.Equal(t, 200, resp.StatusCode)
@@ -391,7 +395,7 @@ func TestHandler(t *testing.T) {
assert.Equal(t, "hit", got.Result) assert.Equal(t, "hit", got.Result)
assert.Equal(t, keys[except], got.CacheKey) assert.Equal(t, keys[except], got.CacheKey)
contentResp, err := http.Get(got.ArchiveLocation) contentResp, err := http.Get(got.ArchiveLocation) //nolint:bodyclose // pre-existing issue from nektos/act
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 200, contentResp.StatusCode) require.Equal(t, 200, contentResp.StatusCode)
content, err := io.ReadAll(contentResp.Body) content, err := io.ReadAll(contentResp.Body)
@@ -409,7 +413,7 @@ func TestHandler(t *testing.T) {
{ {
reqKey := key + "_aBc" reqKey := key + "_aBc"
resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKey, version)) 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) require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode) require.Equal(t, 200, resp.StatusCode)
got := struct { got := struct {
@@ -448,7 +452,7 @@ func TestHandler(t *testing.T) {
key + "_a_b", key + "_a_b",
}, ",") }, ",")
resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKeys, version)) 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) require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode) require.Equal(t, 200, resp.StatusCode)
@@ -466,7 +470,7 @@ func TestHandler(t *testing.T) {
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got)) require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
assert.Equal(t, keys[expect], got.CacheKey) assert.Equal(t, keys[expect], got.CacheKey)
contentResp, err := http.Get(got.ArchiveLocation) contentResp, err := http.Get(got.ArchiveLocation) //nolint:bodyclose // pre-existing issue from nektos/act
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 200, contentResp.StatusCode) require.Equal(t, 200, contentResp.StatusCode)
content, err := io.ReadAll(contentResp.Body) content, err := io.ReadAll(contentResp.Body)
@@ -500,7 +504,7 @@ func TestHandler(t *testing.T) {
key + "_a_b", key + "_a_b",
}, ",") }, ",")
resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKeys, version)) 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) require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode) require.Equal(t, 200, resp.StatusCode)
@@ -519,7 +523,7 @@ func TestHandler(t *testing.T) {
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got)) require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
assert.Equal(t, keys[expect], got.CacheKey) assert.Equal(t, keys[expect], got.CacheKey)
contentResp, err := http.Get(got.ArchiveLocation) contentResp, err := http.Get(got.ArchiveLocation) //nolint:bodyclose // pre-existing issue from nektos/act
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 200, contentResp.StatusCode) require.Equal(t, 200, contentResp.StatusCode)
content, err := io.ReadAll(contentResp.Body) content, err := io.ReadAll(contentResp.Body)
@@ -528,7 +532,7 @@ func TestHandler(t *testing.T) {
}) })
} }
func uploadCacheNormally(t *testing.T, base, key, version string, content []byte) { func uploadCacheNormally(t *testing.T, base, key, version string, content []byte) { //nolint:unparam // pre-existing issue from nektos/act
var id uint64 var id uint64
{ {
body, err := json.Marshal(&Request{ body, err := json.Marshal(&Request{
@@ -537,7 +541,7 @@ func uploadCacheNormally(t *testing.T, base, key, version string, content []byte
Size: int64(len(content)), Size: int64(len(content)),
}) })
require.NoError(t, err) require.NoError(t, err)
resp, err := http.Post(fmt.Sprintf("%s/caches", base), "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) require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode) assert.Equal(t, 200, resp.StatusCode)
@@ -553,18 +557,18 @@ func uploadCacheNormally(t *testing.T, base, key, version string, content []byte
require.NoError(t, err) require.NoError(t, err)
req.Header.Set("Content-Type", "application/octet-stream") req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("Content-Range", "bytes 0-99/*") req.Header.Set("Content-Range", "bytes 0-99/*")
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req) //nolint:bodyclose // pre-existing issue from nektos/act
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode) assert.Equal(t, 200, resp.StatusCode)
} }
{ {
resp, err := http.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) require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode) assert.Equal(t, 200, resp.StatusCode)
} }
var archiveLocation string var archiveLocation string
{ {
resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, key, version)) 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) require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode) require.Equal(t, 200, resp.StatusCode)
got := struct { got := struct {
@@ -578,7 +582,7 @@ func uploadCacheNormally(t *testing.T, base, key, version string, content []byte
archiveLocation = got.ArchiveLocation archiveLocation = got.ArchiveLocation
} }
{ {
resp, err := http.Get(archiveLocation) //nolint:gosec resp, err := http.Get(archiveLocation) //nolint:bodyclose // pre-existing issue from nektos/act
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode) require.Equal(t, 200, resp.StatusCode)
got, err := io.ReadAll(resp.Body) got, err := io.ReadAll(resp.Body)
@@ -695,13 +699,3 @@ func TestHandler_gcCache(t *testing.T) {
} }
require.NoError(t, db.Close()) require.NoError(t, db.Close())
} }
func TestCreateHandler(t *testing.T) {
dir := filepath.Join(t.TempDir(), "artifactcache")
handler, router, err := CreateHandler(dir, "http://localhost:8080", nil)
require.NoError(t, err)
require.NotNil(t, handler)
require.NotNil(t, router)
require.Equal(t, "http://localhost:8080", handler.ExternalURL())
}

View File

@@ -1,3 +1,7 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// Copyright 2023 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package artifactcache package artifactcache
type Request struct { type Request struct {

View File

@@ -1,3 +1,7 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// Copyright 2023 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package artifactcache package artifactcache
import ( import (
@@ -6,6 +10,7 @@ import (
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
) )
type Storage struct { type Storage struct {
@@ -103,11 +108,11 @@ func (s *Storage) Remove(id uint64) {
} }
func (s *Storage) filename(id uint64) string { func (s *Storage) filename(id uint64) string {
return filepath.Join(s.rootDir, fmt.Sprintf("%02x", id%0xff), fmt.Sprint(id)) return filepath.Join(s.rootDir, fmt.Sprintf("%02x", id%0xff), strconv.FormatUint(id, 10))
} }
func (s *Storage) tempDir(id uint64) string { func (s *Storage) tempDir(id uint64) string {
return filepath.Join(s.rootDir, "tmp", fmt.Sprint(id)) return filepath.Join(s.rootDir, "tmp", strconv.FormatUint(id, 10))
} }
func (s *Storage) tempName(id uint64, offset int64) string { func (s *Storage) tempName(id uint64, offset int64) string {

View File

@@ -1,3 +1,7 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// Copyright 2021 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package artifacts package artifacts
import ( import (
@@ -13,9 +17,9 @@ import (
"strings" "strings"
"time" "time"
"github.com/julienschmidt/httprouter" "gitea.com/gitea/act_runner/act/common"
"github.com/actions-oss/act-cli/pkg/common" "github.com/julienschmidt/httprouter"
) )
type FileContainerResourceURL struct { type FileContainerResourceURL struct {
@@ -55,8 +59,7 @@ type WriteFS interface {
OpenAppendable(name string) (WritableFile, error) OpenAppendable(name string) (WritableFile, error)
} }
type readWriteFSImpl struct { type readWriteFSImpl struct{}
}
func (fwfs readWriteFSImpl) Open(name string) (fs.File, error) { func (fwfs readWriteFSImpl) Open(name string) (fs.File, error) {
return os.Open(name) return os.Open(name)
@@ -74,7 +77,6 @@ func (fwfs readWriteFSImpl) OpenAppendable(name string) (WritableFile, error) {
return nil, err return nil, err
} }
file, err := os.OpenFile(name, os.O_CREATE|os.O_RDWR, 0o644) file, err := os.OpenFile(name, os.O_CREATE|os.O_RDWR, 0o644)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -88,7 +90,7 @@ func (fwfs readWriteFSImpl) OpenAppendable(name string) (WritableFile, error) {
var gzipExtension = ".gz__" var gzipExtension = ".gz__"
func safeResolve(baseDir string, relPath string) string { func safeResolve(baseDir, relPath string) string {
return filepath.Join(baseDir, filepath.Clean(filepath.Join(string(os.PathSeparator), relPath))) return filepath.Join(baseDir, filepath.Clean(filepath.Join(string(os.PathSeparator), relPath)))
} }
@@ -127,7 +129,6 @@ func uploads(router *httprouter.Router, baseDir string, fsys WriteFS) {
} }
return fsys.OpenWritable(safePath) return fsys.OpenWritable(safePath)
}() }()
if err != nil { if err != nil {
panic(err) panic(err)
} }
@@ -135,11 +136,11 @@ func uploads(router *httprouter.Router, baseDir string, fsys WriteFS) {
writer, ok := file.(io.Writer) writer, ok := file.(io.Writer)
if !ok { if !ok {
panic(errors.New("file is not writable")) panic(errors.New("File is not writable"))
} }
if req.Body == nil { if req.Body == nil {
panic(errors.New("no body given")) panic(errors.New("No body given"))
} }
_, err = io.Copy(writer, req.Body) _, err = io.Copy(writer, req.Body)
@@ -160,7 +161,7 @@ func uploads(router *httprouter.Router, baseDir string, fsys WriteFS) {
} }
}) })
router.PATCH("/_apis/pipelines/workflows/:runId/artifacts", func(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) { router.PATCH("/_apis/pipelines/workflows/:runId/artifacts", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
json, err := json.Marshal(ResponseMessage{ json, err := json.Marshal(ResponseMessage{
Message: "success", Message: "success",
}) })
@@ -214,7 +215,7 @@ func downloads(router *httprouter.Router, baseDir string, fsys fs.FS) {
safePath := safeResolve(baseDir, filepath.Join(container, itemPath)) safePath := safeResolve(baseDir, filepath.Join(container, itemPath))
var files []ContainerItem var files []ContainerItem
err := fs.WalkDir(fsys, safePath, func(path string, entry fs.DirEntry, _ error) error { err := fs.WalkDir(fsys, safePath, func(path string, entry fs.DirEntry, err error) error {
if !entry.IsDir() { if !entry.IsDir() {
rel, err := filepath.Rel(safePath, path) rel, err := filepath.Rel(safePath, path)
if err != nil { if err != nil {
@@ -253,7 +254,7 @@ func downloads(router *httprouter.Router, baseDir string, fsys fs.FS) {
} }
}) })
router.GET("/artifact/*path", func(w http.ResponseWriter, _ *http.Request, params httprouter.Params) { router.GET("/artifact/*path", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
path := params.ByName("path")[1:] path := params.ByName("path")[1:]
safePath := safeResolve(baseDir, path) safePath := safeResolve(baseDir, path)
@@ -275,7 +276,7 @@ func downloads(router *httprouter.Router, baseDir string, fsys fs.FS) {
}) })
} }
func Serve(ctx context.Context, artifactPath string, addr string, port string) context.CancelFunc { func Serve(ctx context.Context, artifactPath, addr, port string) context.CancelFunc {
serverContext, cancel := context.WithCancel(ctx) serverContext, cancel := context.WithCancel(ctx)
logger := common.Logger(serverContext) logger := common.Logger(serverContext)
@@ -289,7 +290,6 @@ func Serve(ctx context.Context, artifactPath string, addr string, port string) c
fsys := readWriteFSImpl{} fsys := readWriteFSImpl{}
uploads(router, artifactPath, fsys) uploads(router, artifactPath, fsys)
downloads(router, artifactPath, fsys) downloads(router, artifactPath, fsys)
RoutesV4(router, artifactPath, fsys, fsys)
server := &http.Server{ server := &http.Server{
Addr: fmt.Sprintf("%s:%s", addr, port), Addr: fmt.Sprintf("%s:%s", addr, port),
@@ -310,7 +310,7 @@ func Serve(ctx context.Context, artifactPath string, addr string, port string) c
<-serverContext.Done() <-serverContext.Done()
if err := server.Shutdown(ctx); err != nil { if err := server.Shutdown(ctx); err != nil {
logger.Errorf("failed shutdown gracefully - force shutdown: %v", err) logger.Errorf("Failed shutdown gracefully - force shutdown: %v", err)
server.Close() server.Close()
} }
}() }()

View File

@@ -1,3 +1,7 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// Copyright 2021 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package artifacts package artifacts
import ( import (
@@ -13,12 +17,12 @@ import (
"testing" "testing"
"testing/fstest" "testing/fstest"
"gitea.com/gitea/act_runner/act/model"
"gitea.com/gitea/act_runner/act/runner"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/actions-oss/act-cli/pkg/model"
"github.com/actions-oss/act-cli/pkg/runner"
) )
type writableMapFile struct { type writableMapFile struct {
@@ -39,7 +43,7 @@ type writeMapFS struct {
} }
func (fsys writeMapFS) OpenWritable(name string) (WritableFile, error) { func (fsys writeMapFS) OpenWritable(name string) (WritableFile, error) {
var file = &writableMapFile{ file := &writableMapFile{
MapFile: fstest.MapFile{ MapFile: fstest.MapFile{
Data: []byte("content2"), Data: []byte("content2"),
}, },
@@ -50,7 +54,7 @@ func (fsys writeMapFS) OpenWritable(name string) (WritableFile, error) {
} }
func (fsys writeMapFS) OpenAppendable(name string) (WritableFile, error) { func (fsys writeMapFS) OpenAppendable(name string) (WritableFile, error) {
var file = &writableMapFile{ file := &writableMapFile{
MapFile: fstest.MapFile{ MapFile: fstest.MapFile{
Data: []byte("content2"), Data: []byte("content2"),
}, },
@@ -63,12 +67,12 @@ func (fsys writeMapFS) OpenAppendable(name string) (WritableFile, error) {
func TestNewArtifactUploadPrepare(t *testing.T) { func TestNewArtifactUploadPrepare(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
var memfs = fstest.MapFS(map[string]*fstest.MapFile{}) memfs := fstest.MapFS(map[string]*fstest.MapFile{})
router := httprouter.New() router := httprouter.New()
uploads(router, "artifact/server/path", writeMapFS{memfs}) uploads(router, "artifact/server/path", writeMapFS{memfs})
req, _ := http.NewRequest("POST", "http://localhost/_apis/pipelines/workflows/1/artifacts", nil) req, _ := http.NewRequest(http.MethodPost, "http://localhost/_apis/pipelines/workflows/1/artifacts", nil)
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
router.ServeHTTP(rr, req) router.ServeHTTP(rr, req)
@@ -89,12 +93,12 @@ func TestNewArtifactUploadPrepare(t *testing.T) {
func TestArtifactUploadBlob(t *testing.T) { func TestArtifactUploadBlob(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
var memfs = fstest.MapFS(map[string]*fstest.MapFile{}) memfs := fstest.MapFS(map[string]*fstest.MapFile{})
router := httprouter.New() router := httprouter.New()
uploads(router, "artifact/server/path", writeMapFS{memfs}) uploads(router, "artifact/server/path", writeMapFS{memfs})
req, _ := http.NewRequest("PUT", "http://localhost/upload/1?itemPath=some/file", strings.NewReader("content")) req, _ := http.NewRequest(http.MethodPut, "http://localhost/upload/1?itemPath=some/file", strings.NewReader("content"))
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
router.ServeHTTP(rr, req) router.ServeHTTP(rr, req)
@@ -116,12 +120,12 @@ func TestArtifactUploadBlob(t *testing.T) {
func TestFinalizeArtifactUpload(t *testing.T) { func TestFinalizeArtifactUpload(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
var memfs = fstest.MapFS(map[string]*fstest.MapFile{}) memfs := fstest.MapFS(map[string]*fstest.MapFile{})
router := httprouter.New() router := httprouter.New()
uploads(router, "artifact/server/path", writeMapFS{memfs}) uploads(router, "artifact/server/path", writeMapFS{memfs})
req, _ := http.NewRequest("PATCH", "http://localhost/_apis/pipelines/workflows/1/artifacts", nil) req, _ := http.NewRequest(http.MethodPatch, "http://localhost/_apis/pipelines/workflows/1/artifacts", nil)
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
router.ServeHTTP(rr, req) router.ServeHTTP(rr, req)
@@ -142,7 +146,7 @@ func TestFinalizeArtifactUpload(t *testing.T) {
func TestListArtifacts(t *testing.T) { func TestListArtifacts(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
var memfs = fstest.MapFS(map[string]*fstest.MapFile{ memfs := fstest.MapFS(map[string]*fstest.MapFile{
"artifact/server/path/1/file.txt": { "artifact/server/path/1/file.txt": {
Data: []byte(""), Data: []byte(""),
}, },
@@ -151,7 +155,7 @@ func TestListArtifacts(t *testing.T) {
router := httprouter.New() router := httprouter.New()
downloads(router, "artifact/server/path", memfs) downloads(router, "artifact/server/path", memfs)
req, _ := http.NewRequest("GET", "http://localhost/_apis/pipelines/workflows/1/artifacts", nil) req, _ := http.NewRequest(http.MethodGet, "http://localhost/_apis/pipelines/workflows/1/artifacts", nil)
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
router.ServeHTTP(rr, req) router.ServeHTTP(rr, req)
@@ -174,7 +178,7 @@ func TestListArtifacts(t *testing.T) {
func TestListArtifactContainer(t *testing.T) { func TestListArtifactContainer(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
var memfs = fstest.MapFS(map[string]*fstest.MapFile{ memfs := fstest.MapFS(map[string]*fstest.MapFile{
"artifact/server/path/1/some/file": { "artifact/server/path/1/some/file": {
Data: []byte(""), Data: []byte(""),
}, },
@@ -183,7 +187,7 @@ func TestListArtifactContainer(t *testing.T) {
router := httprouter.New() router := httprouter.New()
downloads(router, "artifact/server/path", memfs) downloads(router, "artifact/server/path", memfs)
req, _ := http.NewRequest("GET", "http://localhost/download/1?itemPath=some/file", nil) req, _ := http.NewRequest(http.MethodGet, "http://localhost/download/1?itemPath=some/file", nil)
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
router.ServeHTTP(rr, req) router.ServeHTTP(rr, req)
@@ -198,7 +202,7 @@ func TestListArtifactContainer(t *testing.T) {
panic(err) panic(err)
} }
assert.Equal(1, len(response.Value)) assert.Equal(1, len(response.Value)) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal("some/file", response.Value[0].Path) assert.Equal("some/file", response.Value[0].Path)
assert.Equal("file", response.Value[0].ItemType) assert.Equal("file", response.Value[0].ItemType)
assert.Equal("http://localhost/artifact/1/some/file/.", response.Value[0].ContentLocation) assert.Equal("http://localhost/artifact/1/some/file/.", response.Value[0].ContentLocation)
@@ -207,7 +211,7 @@ func TestListArtifactContainer(t *testing.T) {
func TestDownloadArtifactFile(t *testing.T) { func TestDownloadArtifactFile(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
var memfs = fstest.MapFS(map[string]*fstest.MapFile{ memfs := fstest.MapFS(map[string]*fstest.MapFile{
"artifact/server/path/1/some/file": { "artifact/server/path/1/some/file": {
Data: []byte("content"), Data: []byte("content"),
}, },
@@ -216,7 +220,7 @@ func TestDownloadArtifactFile(t *testing.T) {
router := httprouter.New() router := httprouter.New()
downloads(router, "artifact/server/path", memfs) downloads(router, "artifact/server/path", memfs)
req, _ := http.NewRequest("GET", "http://localhost/artifact/1/some/file", nil) req, _ := http.NewRequest(http.MethodGet, "http://localhost/artifact/1/some/file", nil)
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
router.ServeHTTP(rr, req) router.ServeHTTP(rr, req)
@@ -249,9 +253,6 @@ func TestArtifactFlow(t *testing.T) {
if testing.Short() { if testing.Short() {
t.Skip("skipping integration test") t.Skip("skipping integration test")
} }
if _, ok := os.LookupEnv("NO_EXTERNAL_IP"); ok {
t.Skip("skipping test because QEMU is disabled")
}
ctx := context.Background() ctx := context.Background()
@@ -265,7 +266,6 @@ func TestArtifactFlow(t *testing.T) {
tables := []TestJobFileInfo{ tables := []TestJobFileInfo{
{"testdata", "upload-and-download", "push", "", platforms, ""}, {"testdata", "upload-and-download", "push", "", platforms, ""},
{"testdata", "GHSL-2023-004", "push", "", platforms, ""}, {"testdata", "GHSL-2023-004", "push", "", platforms, ""},
{"testdata", "v4", "push", "", platforms, ""},
} }
log.SetLevel(log.DebugLevel) log.SetLevel(log.DebugLevel)
@@ -276,14 +276,14 @@ func TestArtifactFlow(t *testing.T) {
func runTestJobFile(ctx context.Context, t *testing.T, tjfi TestJobFileInfo) { func runTestJobFile(ctx context.Context, t *testing.T, tjfi TestJobFileInfo) {
t.Run(tjfi.workflowPath, func(t *testing.T) { t.Run(tjfi.workflowPath, func(t *testing.T) {
fmt.Printf("::group::%s\n", tjfi.workflowPath) fmt.Printf("::group::%s\n", tjfi.workflowPath) //nolint:forbidigo // pre-existing issue from nektos/act
if err := os.RemoveAll(artifactsPath); err != nil { if err := os.RemoveAll(artifactsPath); err != nil {
panic(err) panic(err)
} }
workdir, err := filepath.Abs(tjfi.workdir) workdir, err := filepath.Abs(tjfi.workdir)
assert.Nil(t, err, workdir) assert.Nil(t, err, workdir) //nolint:testifylint // pre-existing issue from nektos/act
fullWorkflowPath := filepath.Join(workdir, tjfi.workflowPath) fullWorkflowPath := filepath.Join(workdir, tjfi.workflowPath)
runnerConfig := &runner.Config{ runnerConfig := &runner.Config{
Workdir: workdir, Workdir: workdir,
@@ -299,28 +299,30 @@ func runTestJobFile(ctx context.Context, t *testing.T, tjfi TestJobFileInfo) {
} }
runner, err := runner.New(runnerConfig) runner, err := runner.New(runnerConfig)
assert.Nil(t, err, tjfi.workflowPath) assert.Nil(t, err, tjfi.workflowPath) //nolint:testifylint // pre-existing issue from nektos/act
planner, err := model.NewWorkflowPlanner(fullWorkflowPath, model.PlannerConfig{}) planner, err := model.NewWorkflowPlanner(fullWorkflowPath, true)
assert.Nil(t, err, fullWorkflowPath) assert.Nil(t, err, fullWorkflowPath) //nolint:testifylint // pre-existing issue from nektos/act
plan, err := planner.PlanEvent(tjfi.eventName) plan, err := planner.PlanEvent(tjfi.eventName)
if err == nil { if err == nil {
err = runner.NewPlanExecutor(plan)(ctx) err = runner.NewPlanExecutor(plan)(ctx)
if tjfi.errorMessage == "" { if tjfi.errorMessage == "" {
assert.Nil(t, err, fullWorkflowPath) assert.Nil(t, err, fullWorkflowPath) //nolint:testifylint // pre-existing issue from nektos/act
} else { } else {
assert.Error(t, err, tjfi.errorMessage) assert.Error(t, err, tjfi.errorMessage) //nolint:testifylint // pre-existing issue from nektos/act
} }
} else { } else {
assert.Nil(t, plan) assert.Nil(t, plan)
} }
fmt.Println("::endgroup::") fmt.Println("::endgroup::") //nolint:forbidigo // pre-existing issue from nektos/act
}) })
} }
func TestMkdirFsImplSafeResolve(t *testing.T) { func TestMkdirFsImplSafeResolve(t *testing.T) {
assert := assert.New(t)
baseDir := "/foo/bar" baseDir := "/foo/bar"
tests := map[string]struct { tests := map[string]struct {
@@ -338,7 +340,6 @@ func TestMkdirFsImplSafeResolve(t *testing.T) {
for name, tc := range tests { for name, tc := range tests {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
assert := assert.New(t)
assert.Equal(tc.want, safeResolve(baseDir, tc.input)) assert.Equal(tc.want, safeResolve(baseDir, tc.input))
}) })
} }
@@ -347,7 +348,7 @@ func TestMkdirFsImplSafeResolve(t *testing.T) {
func TestDownloadArtifactFileUnsafePath(t *testing.T) { func TestDownloadArtifactFileUnsafePath(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
var memfs = fstest.MapFS(map[string]*fstest.MapFile{ memfs := fstest.MapFS(map[string]*fstest.MapFile{
"artifact/server/path/some/file": { "artifact/server/path/some/file": {
Data: []byte("content"), Data: []byte("content"),
}, },
@@ -356,7 +357,7 @@ func TestDownloadArtifactFileUnsafePath(t *testing.T) {
router := httprouter.New() router := httprouter.New()
downloads(router, "artifact/server/path", memfs) downloads(router, "artifact/server/path", memfs)
req, _ := http.NewRequest("GET", "http://localhost/artifact/2/../../some/file", nil) req, _ := http.NewRequest(http.MethodGet, "http://localhost/artifact/2/../../some/file", nil)
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
router.ServeHTTP(rr, req) router.ServeHTTP(rr, req)
@@ -373,12 +374,12 @@ func TestDownloadArtifactFileUnsafePath(t *testing.T) {
func TestArtifactUploadBlobUnsafePath(t *testing.T) { func TestArtifactUploadBlobUnsafePath(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
var memfs = fstest.MapFS(map[string]*fstest.MapFile{}) memfs := fstest.MapFS(map[string]*fstest.MapFile{})
router := httprouter.New() router := httprouter.New()
uploads(router, "artifact/server/path", writeMapFS{memfs}) uploads(router, "artifact/server/path", writeMapFS{memfs})
req, _ := http.NewRequest("PUT", "http://localhost/upload/1?itemPath=../../some/file", strings.NewReader("content")) req, _ := http.NewRequest(http.MethodPut, "http://localhost/upload/1?itemPath=../../some/file", strings.NewReader("content"))
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
router.ServeHTTP(rr, req) router.ServeHTTP(rr, req)

View File

@@ -1,9 +1,13 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// Copyright 2020 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package common package common
// CartesianProduct takes map of lists and returns list of unique tuples // CartesianProduct takes map of lists and returns list of unique tuples
func CartesianProduct(mapOfLists map[string][]interface{}) []map[string]interface{} { func CartesianProduct(mapOfLists map[string][]any) []map[string]any {
listNames := make([]string, 0) listNames := make([]string, 0)
lists := make([][]interface{}, 0) lists := make([][]any, 0)
for k, v := range mapOfLists { for k, v := range mapOfLists {
listNames = append(listNames, k) listNames = append(listNames, k)
lists = append(lists, v) lists = append(lists, v)
@@ -11,9 +15,9 @@ func CartesianProduct(mapOfLists map[string][]interface{}) []map[string]interfac
listCart := cartN(lists...) listCart := cartN(lists...)
rtn := make([]map[string]interface{}, 0) rtn := make([]map[string]any, 0)
for _, list := range listCart { for _, list := range listCart {
vMap := make(map[string]interface{}) vMap := make(map[string]any)
for i, v := range list { for i, v := range list {
vMap[listNames[i]] = v vMap[listNames[i]] = v
} }
@@ -22,7 +26,7 @@ func CartesianProduct(mapOfLists map[string][]interface{}) []map[string]interfac
return rtn return rtn
} }
func cartN(a ...[]interface{}) [][]interface{} { func cartN(a ...[]any) [][]any {
c := 1 c := 1
for _, a := range a { for _, a := range a {
c *= len(a) c *= len(a)
@@ -30,8 +34,8 @@ func cartN(a ...[]interface{}) [][]interface{} {
if c == 0 || len(a) == 0 { if c == 0 || len(a) == 0 {
return nil return nil
} }
p := make([][]interface{}, c) p := make([][]any, c)
b := make([]interface{}, c*len(a)) b := make([]any, c*len(a))
n := make([]int, len(a)) n := make([]int, len(a))
s := 0 s := 0
for i := range p { for i := range p {

View File

@@ -1,3 +1,7 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// Copyright 2020 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package common package common
import ( import (
@@ -8,7 +12,7 @@ import (
func TestCartesianProduct(t *testing.T) { func TestCartesianProduct(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
input := map[string][]interface{}{ input := map[string][]any{
"foo": {1, 2, 3, 4}, "foo": {1, 2, 3, 4},
"bar": {"a", "b", "c"}, "bar": {"a", "b", "c"},
"baz": {false, true}, "baz": {false, true},
@@ -25,15 +29,15 @@ func TestCartesianProduct(t *testing.T) {
assert.Contains(v, "baz") assert.Contains(v, "baz")
} }
input = map[string][]interface{}{ input = map[string][]any{
"foo": {1, 2, 3, 4}, "foo": {1, 2, 3, 4},
"bar": {}, "bar": {},
"baz": {false, true}, "baz": {false, true},
} }
output = CartesianProduct(input) output = CartesianProduct(input)
assert.Len(output, 0) assert.Len(output, 0) //nolint:testifylint // pre-existing issue from nektos/act
input = map[string][]interface{}{} input = map[string][]any{}
output = CartesianProduct(input) output = CartesianProduct(input)
assert.Len(output, 0) assert.Len(output, 0) //nolint:testifylint // pre-existing issue from nektos/act
} }

View File

@@ -1,3 +1,7 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// Copyright 2020 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package common package common
import ( import (
@@ -72,6 +76,7 @@ func (p *Pen) drawTopBars(buf io.Writer, labels ...string) {
} }
fmt.Fprintf(buf, "\n") fmt.Fprintf(buf, "\n")
} }
func (p *Pen) drawBottomBars(buf io.Writer, labels ...string) { func (p *Pen) drawBottomBars(buf io.Writer, labels ...string) {
style := styleDefs[p.style] style := styleDefs[p.style]
for _, label := range labels { for _, label := range labels {
@@ -83,6 +88,7 @@ func (p *Pen) drawBottomBars(buf io.Writer, labels ...string) {
} }
fmt.Fprintf(buf, "\n") fmt.Fprintf(buf, "\n")
} }
func (p *Pen) drawLabels(buf io.Writer, labels ...string) { func (p *Pen) drawLabels(buf io.Writer, labels ...string) {
style := styleDefs[p.style] style := styleDefs[p.style]
for _, label := range labels { for _, label := range labels {
@@ -125,11 +131,8 @@ func (p *Pen) DrawBoxes(labels ...string) *Drawing {
// Draw to writer // Draw to writer
func (d *Drawing) Draw(writer io.Writer, centerOnWidth int) { func (d *Drawing) Draw(writer io.Writer, centerOnWidth int) {
padSize := (centerOnWidth - d.GetWidth()) / 2 padSize := max((centerOnWidth-d.GetWidth())/2, 0)
if padSize < 0 { for l := range strings.SplitSeq(d.buf.String(), "\n") {
padSize = 0
}
for _, l := range strings.Split(d.buf.String(), "\n") {
if len(l) > 0 { if len(l) > 0 {
padding := strings.Repeat(" ", padSize) padding := strings.Repeat(" ", padSize)
fmt.Fprintf(writer, "%s%s\n", padding, l) fmt.Fprintf(writer, "%s%s\n", padding, l)

View File

@@ -1,3 +1,7 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Copyright 2020 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package common package common
import ( import (

View File

@@ -1,9 +1,13 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// Copyright 2020 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package common package common
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"runtime/debug"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@@ -19,7 +23,7 @@ func (w Warning) Error() string {
} }
// Warningf create a warning // Warningf create a warning
func Warningf(format string, args ...interface{}) Warning { func Warningf(format string, args ...any) Warning {
w := Warning{ w := Warning{
Message: fmt.Sprintf(format, args...), Message: fmt.Sprintf(format, args...),
} }
@@ -33,7 +37,7 @@ type Executor func(ctx context.Context) error
type Conditional func(ctx context.Context) bool type Conditional func(ctx context.Context) bool
// NewInfoExecutor is an executor that logs messages // NewInfoExecutor is an executor that logs messages
func NewInfoExecutor(format string, args ...interface{}) Executor { func NewInfoExecutor(format string, args ...any) Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
logger := Logger(ctx) logger := Logger(ctx)
logger.Infof(format, args...) logger.Infof(format, args...)
@@ -42,7 +46,7 @@ func NewInfoExecutor(format string, args ...interface{}) Executor {
} }
// NewDebugExecutor is an executor that logs messages // NewDebugExecutor is an executor that logs messages
func NewDebugExecutor(format string, args ...interface{}) Executor { func NewDebugExecutor(format string, args ...any) Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
logger := Logger(ctx) logger := Logger(ctx)
logger.Debugf(format, args...) logger.Debugf(format, args...)
@@ -53,7 +57,7 @@ func NewDebugExecutor(format string, args ...interface{}) Executor {
// NewPipelineExecutor creates a new executor from a series of other executors // NewPipelineExecutor creates a new executor from a series of other executors
func NewPipelineExecutor(executors ...Executor) Executor { func NewPipelineExecutor(executors ...Executor) Executor {
if len(executors) == 0 { if len(executors) == 0 {
return func(_ context.Context) error { return func(ctx context.Context) error {
return nil return nil
} }
} }
@@ -69,7 +73,7 @@ func NewPipelineExecutor(executors ...Executor) Executor {
} }
// NewConditionalExecutor creates a new executor based on conditions // NewConditionalExecutor creates a new executor based on conditions
func NewConditionalExecutor(conditional Conditional, trueExecutor Executor, falseExecutor Executor) Executor { func NewConditionalExecutor(conditional Conditional, trueExecutor, falseExecutor Executor) Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
if conditional(ctx) { if conditional(ctx) {
if trueExecutor != nil { if trueExecutor != nil {
@@ -86,7 +90,7 @@ func NewConditionalExecutor(conditional Conditional, trueExecutor Executor, fals
// NewErrorExecutor creates a new executor that always errors out // NewErrorExecutor creates a new executor that always errors out
func NewErrorExecutor(err error) Executor { func NewErrorExecutor(err error) Executor {
return func(_ context.Context) error { return func(ctx context.Context) error {
return err return err
} }
} }
@@ -102,22 +106,40 @@ func NewParallelExecutor(parallel int, executors ...Executor) Executor {
parallel = 1 parallel = 1
} }
log.Infof("NewParallelExecutor: Creating %d workers for %d executors", parallel, len(executors))
for i := 0; i < parallel; i++ { for i := 0; i < parallel; i++ {
go func(work <-chan Executor, errs chan<- error) { go func(workerID int, work <-chan Executor, errs chan<- error) {
log.Debugf("Worker %d started", workerID)
taskCount := 0
for executor := range work { for executor := range work {
errs <- executor(ctx) taskCount++
log.Debugf("Worker %d executing task %d", workerID, taskCount)
// Recover from panics in executors to avoid crashing the worker
// goroutine which would leave the runner process hung.
// https://gitea.com/gitea/act_runner/issues/371
errs <- func() (err error) {
defer func() {
if r := recover(); r != nil {
log.Errorf("panic in executor: %v\n%s", r, debug.Stack())
err = fmt.Errorf("panic: %v", r)
} }
}(work, errs) }()
return executor(ctx)
}()
}
log.Debugf("Worker %d finished (%d tasks executed)", workerID, taskCount)
}(i, work, errs)
} }
for i := 0; i < len(executors); i++ { for i := range executors {
work <- executors[i] work <- executors[i]
} }
close(work) close(work)
// Executor waits all executors to cleanup these resources. // Executor waits all executors to cleanup these resources.
var firstErr error var firstErr error
for i := 0; i < len(executors); i++ { for range executors {
err := <-errs err := <-errs
if firstErr == nil { if firstErr == nil {
firstErr = err firstErr = err
@@ -131,31 +153,6 @@ func NewParallelExecutor(parallel int, executors ...Executor) Executor {
} }
} }
func NewFieldExecutor(name string, value interface{}, exec Executor) Executor {
return func(ctx context.Context) error {
return exec(WithLogger(ctx, Logger(ctx).WithField(name, value)))
}
}
// Then runs another executor if this executor succeeds
func (e Executor) ThenError(then func(ctx context.Context, err error) error) Executor {
return func(ctx context.Context) error {
err := e(ctx)
if err != nil {
switch err.(type) {
case Warning:
Logger(ctx).Warning(err.Error())
default:
return then(ctx, err)
}
}
if ctx.Err() != nil {
return ctx.Err()
}
return then(ctx, err)
}
}
// Then runs another executor if this executor succeeds // Then runs another executor if this executor succeeds
func (e Executor) Then(then Executor) Executor { func (e Executor) Then(then Executor) Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
@@ -175,25 +172,6 @@ func (e Executor) Then(then Executor) Executor {
} }
} }
// Then runs another executor if this executor succeeds
func (e Executor) OnError(then Executor) Executor {
return func(ctx context.Context) error {
err := e(ctx)
if err != nil {
switch err.(type) {
case Warning:
Logger(ctx).Warning(err.Error())
default:
return errors.Join(err, then(ctx))
}
}
if ctx.Err() != nil {
return ctx.Err()
}
return nil
}
}
// If only runs this executor if conditional is true // If only runs this executor if conditional is true
func (e Executor) If(conditional Conditional) Executor { func (e Executor) If(conditional Conditional) Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
@@ -216,22 +194,20 @@ func (e Executor) IfNot(conditional Conditional) Executor {
// IfBool only runs this executor if conditional is true // IfBool only runs this executor if conditional is true
func (e Executor) IfBool(conditional bool) Executor { func (e Executor) IfBool(conditional bool) Executor {
return e.If(func(_ context.Context) bool { return e.If(func(ctx context.Context) bool {
return conditional return conditional
}) })
} }
// Finally adds an executor to run after other executor // Finally adds an executor to run after other executor
func (e Executor) Finally(finally Executor) Executor { func (e Executor) Finally(finally Executor) Executor {
return func(ctx context.Context) (err error) { return func(ctx context.Context) error {
defer func() { err := e(ctx)
err2 := finally(ctx) err2 := finally(ctx)
if err2 != nil { if err2 != nil {
err = fmt.Errorf("error occurred running finally: %v (original error: %v)", err2, err) return fmt.Errorf("Error occurred running finally: %v (original error: %v)", err2, err)
} }
}() return err
err = e(ctx)
return
} }
} }

View File

@@ -0,0 +1,89 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package common
import (
"context"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
// Simple fast test that verifies max-parallel: 2 limits concurrency
func TestMaxParallel2Quick(t *testing.T) {
ctx := context.Background()
var currentRunning atomic.Int32
var maxSimultaneous atomic.Int32
executors := make([]Executor, 4)
for i := range 4 {
executors[i] = func(ctx context.Context) error {
current := currentRunning.Add(1)
// Update max if needed
for {
maxValue := maxSimultaneous.Load()
if current <= maxValue || maxSimultaneous.CompareAndSwap(maxValue, current) {
break
}
}
time.Sleep(10 * time.Millisecond)
currentRunning.Add(-1)
return nil
}
}
err := NewParallelExecutor(2, executors...)(ctx)
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.LessOrEqual(t, maxSimultaneous.Load(), int32(2),
"Should not exceed max-parallel: 2")
}
// Test that verifies max-parallel: 1 enforces sequential execution
func TestMaxParallel1Sequential(t *testing.T) {
ctx := context.Background()
var currentRunning atomic.Int32
var maxSimultaneous atomic.Int32
var executionOrder []int
var orderMutex sync.Mutex
executors := make([]Executor, 5)
for i := range 5 {
taskID := i
executors[i] = func(ctx context.Context) error {
current := currentRunning.Add(1)
// Track execution order
orderMutex.Lock()
executionOrder = append(executionOrder, taskID)
orderMutex.Unlock()
// Update max if needed
for {
maxValue := maxSimultaneous.Load()
if current <= maxValue || maxSimultaneous.CompareAndSwap(maxValue, current) {
break
}
}
time.Sleep(20 * time.Millisecond)
currentRunning.Add(-1)
return nil
}
}
err := NewParallelExecutor(1, executors...)(ctx)
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, int32(1), maxSimultaneous.Load(),
"max-parallel: 1 should only run 1 task at a time")
assert.Len(t, executionOrder, 5, "All 5 tasks should have executed")
}

View File

@@ -0,0 +1,283 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package common
import (
"context"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
// TestMaxParallelJobExecution tests actual job execution with max-parallel
func TestMaxParallelJobExecution(t *testing.T) {
t.Run("MaxParallel=1 Sequential", func(t *testing.T) {
var currentRunning atomic.Int32
var maxConcurrent int32
var executionOrder []int
var mu sync.Mutex
executors := make([]Executor, 5)
for i := range 5 {
taskID := i
executors[i] = func(ctx context.Context) error {
current := currentRunning.Add(1)
// Track max concurrent
for {
maxValue := atomic.LoadInt32(&maxConcurrent)
if current <= maxValue || atomic.CompareAndSwapInt32(&maxConcurrent, maxValue, current) {
break
}
}
mu.Lock()
executionOrder = append(executionOrder, taskID)
mu.Unlock()
time.Sleep(10 * time.Millisecond)
currentRunning.Add(-1)
return nil
}
}
ctx := context.Background()
err := NewParallelExecutor(1, executors...)(ctx)
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, int32(1), maxConcurrent, "Should never exceed 1 concurrent execution")
assert.Len(t, executionOrder, 5, "All tasks should execute")
})
t.Run("MaxParallel=3 Limited", func(t *testing.T) {
var currentRunning atomic.Int32
var maxConcurrent int32
executors := make([]Executor, 10)
for i := range 10 {
executors[i] = func(ctx context.Context) error {
current := currentRunning.Add(1)
for {
maxValue := atomic.LoadInt32(&maxConcurrent)
if current <= maxValue || atomic.CompareAndSwapInt32(&maxConcurrent, maxValue, current) {
break
}
}
time.Sleep(20 * time.Millisecond)
currentRunning.Add(-1)
return nil
}
}
ctx := context.Background()
err := NewParallelExecutor(3, executors...)(ctx)
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.LessOrEqual(t, int(maxConcurrent), 3, "Should never exceed 3 concurrent executions")
assert.GreaterOrEqual(t, int(maxConcurrent), 1, "Should have at least 1 concurrent execution")
})
t.Run("MaxParallel=0 Uses1Worker", func(t *testing.T) {
var maxConcurrent int32
var currentRunning atomic.Int32
executors := make([]Executor, 5)
for i := range 5 {
executors[i] = func(ctx context.Context) error {
current := currentRunning.Add(1)
for {
maxValue := atomic.LoadInt32(&maxConcurrent)
if current <= maxValue || atomic.CompareAndSwapInt32(&maxConcurrent, maxValue, current) {
break
}
}
time.Sleep(10 * time.Millisecond)
currentRunning.Add(-1)
return nil
}
}
ctx := context.Background()
// When maxParallel is 0 or negative, it defaults to 1
err := NewParallelExecutor(0, executors...)(ctx)
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, int32(1), maxConcurrent, "Should use 1 worker when max-parallel is 0")
})
}
// TestMaxParallelWithErrors tests error handling with max-parallel
func TestMaxParallelWithErrors(t *testing.T) {
t.Run("OneTaskFailsOthersContinue", func(t *testing.T) {
var successCount int32
executors := make([]Executor, 5)
for i := range 5 {
taskID := i
executors[i] = func(ctx context.Context) error {
if taskID == 2 {
return assert.AnError
}
atomic.AddInt32(&successCount, 1)
return nil
}
}
ctx := context.Background()
err := NewParallelExecutor(2, executors...)(ctx)
// Should return the error from task 2
assert.Error(t, err) //nolint:testifylint // pre-existing issue from nektos/act
// Other tasks should still execute
assert.Equal(t, int32(4), successCount, "4 tasks should succeed")
})
t.Run("ContextCancellation", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
var startedCount int32
executors := make([]Executor, 10)
for i := range 10 {
executors[i] = func(ctx context.Context) error {
atomic.AddInt32(&startedCount, 1)
time.Sleep(100 * time.Millisecond)
return nil
}
}
// Cancel after a short delay
go func() {
time.Sleep(30 * time.Millisecond)
cancel()
}()
err := NewParallelExecutor(3, executors...)(ctx)
assert.Error(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.ErrorIs(t, err, context.Canceled) //nolint:testifylint // pre-existing issue from nektos/act
// Not all tasks should start due to cancellation (but timing may vary)
// Just verify cancellation occurred
t.Logf("Started %d tasks before cancellation", startedCount)
})
}
// TestMaxParallelPerformance tests performance characteristics
func TestMaxParallelPerformance(t *testing.T) {
if testing.Short() {
t.Skip("Skipping performance test in short mode")
}
t.Run("ParallelFasterThanSequential", func(t *testing.T) {
executors := make([]Executor, 10)
for i := range 10 {
executors[i] = func(ctx context.Context) error {
time.Sleep(50 * time.Millisecond)
return nil
}
}
ctx := context.Background()
// Sequential (max-parallel=1)
start := time.Now()
err := NewParallelExecutor(1, executors...)(ctx)
sequentialDuration := time.Since(start)
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
// Parallel (max-parallel=5)
start = time.Now()
err = NewParallelExecutor(5, executors...)(ctx)
parallelDuration := time.Since(start)
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
// Parallel should be significantly faster
assert.Less(t, parallelDuration, sequentialDuration/2,
"Parallel execution should be at least 2x faster")
})
t.Run("OptimalWorkerCount", func(t *testing.T) {
executors := make([]Executor, 20)
for i := range 20 {
executors[i] = func(ctx context.Context) error {
time.Sleep(10 * time.Millisecond)
return nil
}
}
ctx := context.Background()
// Test with different worker counts
workerCounts := []int{1, 2, 5, 10, 20}
durations := make(map[int]time.Duration)
for _, count := range workerCounts {
start := time.Now()
err := NewParallelExecutor(count, executors...)(ctx)
durations[count] = time.Since(start)
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
}
// More workers should generally be faster (up to a point)
assert.Less(t, durations[5], durations[1], "5 workers should be faster than 1")
assert.Less(t, durations[10], durations[2], "10 workers should be faster than 2")
})
}
// TestMaxParallelResourceSharing tests resource sharing scenarios
func TestMaxParallelResourceSharing(t *testing.T) {
t.Run("SharedResourceWithMutex", func(t *testing.T) {
var sharedCounter int
var mu sync.Mutex
executors := make([]Executor, 100)
for i := range 100 {
executors[i] = func(ctx context.Context) error {
mu.Lock()
sharedCounter++
mu.Unlock()
return nil
}
}
ctx := context.Background()
err := NewParallelExecutor(10, executors...)(ctx)
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, 100, sharedCounter, "All tasks should increment counter")
})
t.Run("ChannelCommunication", func(t *testing.T) {
resultChan := make(chan int, 50)
executors := make([]Executor, 50)
for i := range 50 {
taskID := i
executors[i] = func(ctx context.Context) error {
resultChan <- taskID
return nil
}
}
ctx := context.Background()
err := NewParallelExecutor(5, executors...)(ctx)
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
close(resultChan)
results := make(map[int]bool)
for result := range resultChan {
results[result] = true
}
assert.Len(t, results, 50, "All task IDs should be received")
})
}

158
act/common/executor_test.go Normal file
View File

@@ -0,0 +1,158 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// Copyright 2020 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package common
import (
"context"
"errors"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestNewWorkflow(t *testing.T) {
assert := assert.New(t)
ctx := context.Background()
// empty
emptyWorkflow := NewPipelineExecutor()
assert.Nil(emptyWorkflow(ctx)) //nolint:testifylint // pre-existing issue from nektos/act
// error case
errorWorkflow := NewErrorExecutor(errors.New("test error"))
assert.NotNil(errorWorkflow(ctx)) //nolint:testifylint // pre-existing issue from nektos/act
// multiple success case
runcount := 0
successWorkflow := NewPipelineExecutor(
func(ctx context.Context) error {
runcount++
return nil
},
func(ctx context.Context) error {
runcount++
return nil
})
assert.Nil(successWorkflow(ctx)) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(2, runcount)
}
func TestNewConditionalExecutor(t *testing.T) {
assert := assert.New(t)
ctx := context.Background()
trueCount := 0
falseCount := 0
err := NewConditionalExecutor(func(ctx context.Context) bool {
return false
}, func(ctx context.Context) error {
trueCount++
return nil
}, func(ctx context.Context) error {
falseCount++
return nil
})(ctx)
assert.Nil(err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(0, trueCount)
assert.Equal(1, falseCount)
err = NewConditionalExecutor(func(ctx context.Context) bool {
return true
}, func(ctx context.Context) error {
trueCount++
return nil
}, func(ctx context.Context) error {
falseCount++
return nil
})(ctx)
assert.Nil(err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(1, trueCount)
assert.Equal(1, falseCount)
}
func TestNewParallelExecutor(t *testing.T) {
assert := assert.New(t)
ctx := context.Background()
var count, activeCount, maxCount atomic.Int32
emptyWorkflow := NewPipelineExecutor(func(ctx context.Context) error {
count.Add(1)
active := activeCount.Add(1)
for {
m := maxCount.Load()
if active <= m || maxCount.CompareAndSwap(m, active) {
break
}
}
time.Sleep(2 * time.Second)
activeCount.Add(-1)
return nil
})
err := NewParallelExecutor(2, emptyWorkflow, emptyWorkflow, emptyWorkflow)(ctx)
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.Nil(err) //nolint:testifylint // pre-existing issue from nektos/act
// Reset to test running the executor with 0 parallelism
count.Store(0)
activeCount.Store(0)
maxCount.Store(0)
errSingle := NewParallelExecutor(0, emptyWorkflow, emptyWorkflow, emptyWorkflow)(ctx)
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.Nil(errSingle) //nolint:testifylint // pre-existing issue from nektos/act
}
func TestNewParallelExecutorFailed(t *testing.T) {
assert := assert.New(t)
ctx, cancel := context.WithCancel(context.Background())
cancel()
count := 0
errorWorkflow := NewPipelineExecutor(func(ctx context.Context) error {
count++
return errors.New("fake error")
})
err := NewParallelExecutor(1, errorWorkflow)(ctx)
assert.Equal(1, count)
assert.ErrorIs(context.Canceled, err)
}
func TestNewParallelExecutorCanceled(t *testing.T) {
assert := assert.New(t)
ctx, cancel := context.WithCancel(context.Background())
cancel()
errExpected := errors.New("fake error")
var count atomic.Int32
successWorkflow := NewPipelineExecutor(func(ctx context.Context) error {
count.Add(1)
return nil
})
errorWorkflow := NewPipelineExecutor(func(ctx context.Context) error {
count.Add(1)
return errExpected
})
err := NewParallelExecutor(3, errorWorkflow, successWorkflow, successWorkflow)(ctx)
assert.Equal(int32(3), count.Load())
assert.Error(errExpected, err) //nolint:testifylint // pre-existing issue from nektos/act
}

View File

@@ -1,3 +1,7 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Copyright 2020 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package common package common
import ( import (
@@ -7,7 +11,7 @@ import (
) )
// CopyFile copy file // CopyFile copy file
func CopyFile(source string, dest string) (err error) { func CopyFile(source, dest string) (err error) {
sourcefile, err := os.Open(source) sourcefile, err := os.Open(source)
if err != nil { if err != nil {
return err return err
@@ -30,11 +34,11 @@ func CopyFile(source string, dest string) (err error) {
} }
} }
return return err
} }
// CopyDir recursive copy of directory // CopyDir recursive copy of directory
func CopyDir(source string, dest string) (err error) { func CopyDir(source, dest string) (err error) {
// get properties of source dir // get properties of source dir
sourceinfo, err := os.Stat(source) sourceinfo, err := os.Stat(source)
if err != nil { if err != nil {
@@ -59,13 +63,13 @@ func CopyDir(source string, dest string) (err error) {
// create sub-directories - recursively // create sub-directories - recursively
err = CopyDir(sourcefilepointer, destinationfilepointer) err = CopyDir(sourcefilepointer, destinationfilepointer)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err) //nolint:forbidigo // pre-existing issue from nektos/act
} }
} else { } else {
// perform copy // perform copy
err = CopyFile(sourcefilepointer, destinationfilepointer) err = CopyFile(sourcefilepointer, destinationfilepointer)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err) //nolint:forbidigo // pre-existing issue from nektos/act
} }
} }
} }

View File

@@ -1,3 +1,7 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Copyright 2022 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git package git
import ( import (
@@ -11,6 +15,8 @@ import (
"strings" "strings"
"sync" "sync"
"gitea.com/gitea/act_runner/act/common"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing"
@@ -18,8 +24,6 @@ import (
"github.com/go-git/go-git/v5/plumbing/transport/http" "github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/mattn/go-isatty" "github.com/mattn/go-isatty"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/actions-oss/act-cli/pkg/common"
) )
var ( var (
@@ -52,7 +56,7 @@ func (e *Error) Commit() string {
} }
// FindGitRevision get the current git revision // FindGitRevision get the current git revision
func FindGitRevision(ctx context.Context, file string) (shortSha string, sha string, err error) { func FindGitRevision(ctx context.Context, file string) (shortSha, sha string, err error) {
logger := common.Logger(ctx) logger := common.Logger(ctx)
gitDir, err := git.PlainOpenWithOptions( gitDir, err := git.PlainOpenWithOptions(
@@ -62,7 +66,6 @@ func FindGitRevision(ctx context.Context, file string) (shortSha string, sha str
EnableDotGitCommonDir: true, EnableDotGitCommonDir: true,
}, },
) )
if err != nil { if err != nil {
logger.WithError(err).Error("path", file, "not located inside a git repository") logger.WithError(err).Error("path", file, "not located inside a git repository")
return "", "", err return "", "", err
@@ -74,7 +77,7 @@ func FindGitRevision(ctx context.Context, file string) (shortSha string, sha str
} }
if head.Hash().IsZero() { if head.Hash().IsZero() {
return "", "", fmt.Errorf("head sha1 could not be resolved") return "", "", errors.New("HEAD sha1 could not be resolved")
} }
hash := head.Hash().String() hash := head.Hash().String()
@@ -96,8 +99,8 @@ func FindGitRef(ctx context.Context, file string) (string, error) {
logger.Debugf("HEAD points to '%s'", ref) logger.Debugf("HEAD points to '%s'", ref)
// Prefer the git library to iterate over the references and find a matching tag or branch. // Prefer the git library to iterate over the references and find a matching tag or branch.
var refTag = "" refTag := ""
var refBranch = "" refBranch := ""
repo, err := git.PlainOpenWithOptions( repo, err := git.PlainOpenWithOptions(
file, file,
&git.PlainOpenOptions{ &git.PlainOpenOptions{
@@ -105,7 +108,6 @@ func FindGitRef(ctx context.Context, file string) (string, error) {
EnableDotGitCommonDir: true, EnableDotGitCommonDir: true,
}, },
) )
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -126,7 +128,7 @@ func FindGitRef(ctx context.Context, file string) (string, error) {
* it means we checked out a branch * it means we checked out a branch
* *
* If a branches matches first we must continue and check all tags (all references) * If a branches matches first we must continue and check all tags (all references)
* in case we match with a tag later in the iteration * in case we match with a tag later in the interation
*/ */
if r.Hash().String() == ref { if r.Hash().String() == ref {
if r.Name().IsTag() { if r.Name().IsTag() {
@@ -144,7 +146,6 @@ func FindGitRef(ctx context.Context, file string) (string, error) {
return nil return nil
}) })
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -198,37 +199,24 @@ func findGitRemoteURL(_ context.Context, file, remoteName string) (string, error
return remote.Config().URLs[0], nil return remote.Config().URLs[0], nil
} }
type findStringSubmatcher interface { func findGitSlug(url, githubInstance string) (string, string, error) { //nolint:unparam // pre-existing issue from nektos/act
FindStringSubmatch(string) []string if matches := codeCommitHTTPRegex.FindStringSubmatch(url); matches != nil {
}
func matchesRegex(url string, matchers ...findStringSubmatcher) []string {
for _, regex := range matchers {
if matches := regex.FindStringSubmatch(url); matches != nil {
return matches
}
}
return nil
}
// TODO deprecate and remove githubInstance parameter
func findGitSlug(url string, _ /* githubInstance */ string) (string, string, error) {
if matches := matchesRegex(url, codeCommitHTTPRegex, codeCommitSSHRegex); matches != nil {
return "CodeCommit", matches[2], nil return "CodeCommit", matches[2], nil
} } else if matches := codeCommitSSHRegex.FindStringSubmatch(url); matches != nil {
return "CodeCommit", matches[2], nil
if matches := matchesRegex(url, githubHTTPRegex, githubSSHRegex); matches != nil { } else if matches := githubHTTPRegex.FindStringSubmatch(url); matches != nil {
return "GitHub", fmt.Sprintf("%s/%s", matches[1], matches[2]), nil return "GitHub", fmt.Sprintf("%s/%s", matches[1], matches[2]), nil
} } else if matches := githubSSHRegex.FindStringSubmatch(url); matches != nil {
return "GitHub", fmt.Sprintf("%s/%s", matches[1], matches[2]), nil
if matches := matchesRegex(url, } else if githubInstance != "github.com" {
regexp.MustCompile(`^https?://(?:[^/]+)/([^/]+)/([^/]+)(?:.git)?$`), gheHTTPRegex := regexp.MustCompile(fmt.Sprintf(`^https?://%s/(.+)/(.+?)(?:.git)?$`, githubInstance))
regexp.MustCompile(`([^/]+)[:/]([^/]+)/([^/]+)(?:.git)?$`), gheSSHRegex := regexp.MustCompile(githubInstance + "[:/](.+)/(.+?)(?:.git)?$")
); matches != nil { if matches := gheHTTPRegex.FindStringSubmatch(url); matches != nil {
return "GitHubEnterprise", fmt.Sprintf("%s/%s", matches[1], matches[2]), nil
} else if matches := gheSSHRegex.FindStringSubmatch(url); matches != nil {
return "GitHubEnterprise", fmt.Sprintf("%s/%s", matches[1], matches[2]), nil return "GitHubEnterprise", fmt.Sprintf("%s/%s", matches[1], matches[2]), nil
} }
}
return "", url, nil return "", url, nil
} }
@@ -239,19 +227,13 @@ type NewGitCloneExecutorInput struct {
Dir string Dir string
Token string Token string
OfflineMode bool OfflineMode bool
// For Gitea
InsecureSkipTLS bool
} }
// CloneIfRequired ... // CloneIfRequired ...
func CloneIfRequired(ctx context.Context, refName plumbing.ReferenceName, input NewGitCloneExecutorInput, logger log.FieldLogger) (*git.Repository, error) { func CloneIfRequired(ctx context.Context, refName plumbing.ReferenceName, input NewGitCloneExecutorInput, logger log.FieldLogger) (*git.Repository, error) {
// If the remote URL has changed, remove the directory and clone again.
if r, err := git.PlainOpen(input.Dir); err == nil {
if remote, err := r.Remote("origin"); err == nil {
if len(remote.Config().URLs) > 0 && remote.Config().URLs[0] != input.URL {
_ = os.RemoveAll(input.Dir)
}
}
}
r, err := git.PlainOpen(input.Dir) r, err := git.PlainOpen(input.Dir)
if err != nil { if err != nil {
var progressWriter io.Writer var progressWriter io.Writer
@@ -261,7 +243,7 @@ func CloneIfRequired(ctx context.Context, refName plumbing.ReferenceName, input
} else if lgr, ok := logger.(*log.Logger); ok { } else if lgr, ok := logger.(*log.Logger); ok {
progressWriter = lgr.WriterLevel(log.DebugLevel) progressWriter = lgr.WriterLevel(log.DebugLevel)
} else { } else {
log.Errorf("unable to get writer from logger (type=%T)", logger) log.Errorf("Unable to get writer from logger (type=%T)", logger)
progressWriter = os.Stdout progressWriter = os.Stdout
} }
} }
@@ -269,6 +251,8 @@ func CloneIfRequired(ctx context.Context, refName plumbing.ReferenceName, input
cloneOptions := git.CloneOptions{ cloneOptions := git.CloneOptions{
URL: input.URL, URL: input.URL,
Progress: progressWriter, Progress: progressWriter,
InsecureSkipTLS: input.InsecureSkipTLS, // For Gitea
} }
if input.Token != "" { if input.Token != "" {
cloneOptions.Auth = &http.BasicAuth{ cloneOptions.Auth = &http.BasicAuth{
@@ -279,7 +263,7 @@ func CloneIfRequired(ctx context.Context, refName plumbing.ReferenceName, input
r, err = git.PlainCloneContext(ctx, input.Dir, false, &cloneOptions) r, err = git.PlainCloneContext(ctx, input.Dir, false, &cloneOptions)
if err != nil { if err != nil {
logger.Errorf("unable to clone %v %s: %v", input.URL, refName, err) logger.Errorf("Unable to clone %v %s: %v", input.URL, refName, err)
return nil, err return nil, err
} }
@@ -293,6 +277,7 @@ func CloneIfRequired(ctx context.Context, refName plumbing.ReferenceName, input
func gitOptions(token string) (fetchOptions git.FetchOptions, pullOptions git.PullOptions) { func gitOptions(token string) (fetchOptions git.FetchOptions, pullOptions git.PullOptions) {
fetchOptions.RefSpecs = []config.RefSpec{"refs/*:refs/*", "HEAD:refs/heads/HEAD"} fetchOptions.RefSpecs = []config.RefSpec{"refs/*:refs/*", "HEAD:refs/heads/HEAD"}
fetchOptions.Force = true
pullOptions.Force = true pullOptions.Force = true
if token != "" { if token != "" {
@@ -309,7 +294,7 @@ func gitOptions(token string) (fetchOptions git.FetchOptions, pullOptions git.Pu
// NewGitCloneExecutor creates an executor to clone git repos // NewGitCloneExecutor creates an executor to clone git repos
// //
//nolint:gocyclo //nolint:gocyclo // function handles many cases
func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor { func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
logger := common.Logger(ctx) logger := common.Logger(ctx)
@@ -319,7 +304,7 @@ func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor {
cloneLock.Lock() cloneLock.Lock()
defer cloneLock.Unlock() defer cloneLock.Unlock()
refName := plumbing.ReferenceName(fmt.Sprintf("refs/heads/%s", input.Ref)) refName := plumbing.ReferenceName("refs/heads/" + input.Ref)
r, err := CloneIfRequired(ctx, refName, input, logger) r, err := CloneIfRequired(ctx, refName, input, logger)
if err != nil { if err != nil {
return err return err
@@ -330,6 +315,11 @@ func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor {
// fetch latest changes // fetch latest changes
fetchOptions, pullOptions := gitOptions(input.Token) fetchOptions, pullOptions := gitOptions(input.Token)
if input.InsecureSkipTLS { // For Gitea
fetchOptions.InsecureSkipTLS = true
pullOptions.InsecureSkipTLS = true
}
if !isOfflineMode { if !isOfflineMode {
err = r.Fetch(&fetchOptions) err = r.Fetch(&fetchOptions)
if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) { if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) {
@@ -340,10 +330,10 @@ func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor {
var hash *plumbing.Hash var hash *plumbing.Hash
rev := plumbing.Revision(input.Ref) rev := plumbing.Revision(input.Ref)
if hash, err = r.ResolveRevision(rev); err != nil { if hash, err = r.ResolveRevision(rev); err != nil {
logger.Errorf("unable to resolve %s: %v", input.Ref, err) logger.Errorf("Unable to resolve %s: %v", input.Ref, err)
} }
if hash.String() != input.Ref && len(input.Ref) >= 4 && strings.HasPrefix(hash.String(), input.Ref) { if hash.String() != input.Ref && strings.HasPrefix(hash.String(), input.Ref) {
return &Error{ return &Error{
err: ErrShortRef, err: ErrShortRef,
commit: hash.String(), commit: hash.String(),
@@ -369,7 +359,7 @@ func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor {
} }
if hash, err = r.ResolveRevision(rev); err != nil { if hash, err = r.ResolveRevision(rev); err != nil {
logger.Errorf("unable to resolve %s: %v", input.Ref, err) logger.Errorf("Unable to resolve %s: %v", input.Ref, err)
return err return err
} }
@@ -390,7 +380,7 @@ func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor {
Branch: sourceRef, Branch: sourceRef,
Force: true, Force: true,
}); err != nil { }); err != nil {
logger.Errorf("unable to checkout %s: %v", sourceRef, err) logger.Errorf("Unable to checkout %s: %v", sourceRef, err)
return err return err
} }
} }
@@ -404,7 +394,7 @@ func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor {
if hash.String() != input.Ref && refType == "branch" { if hash.String() != input.Ref && refType == "branch" {
logger.Debugf("Provided ref is not a sha. Updating branch ref after pull") logger.Debugf("Provided ref is not a sha. Updating branch ref after pull")
if hash, err = r.ResolveRevision(rev); err != nil { if hash, err = r.ResolveRevision(rev); err != nil {
logger.Errorf("unable to resolve %s: %v", input.Ref, err) logger.Errorf("Unable to resolve %s: %v", input.Ref, err)
return err return err
} }
} }
@@ -412,7 +402,7 @@ func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor {
Hash: *hash, Hash: *hash,
Force: true, Force: true,
}); err != nil { }); err != nil {
logger.Errorf("unable to checkout %s: %v", *hash, err) logger.Errorf("Unable to checkout %s: %v", *hash, err)
return err return err
} }
@@ -420,7 +410,7 @@ func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor {
Mode: git.HardReset, Mode: git.HardReset,
Commit: *hash, Commit: *hash,
}); err != nil { }); err != nil {
logger.Errorf("unable to reset to %s: %v", hash.String(), err) logger.Errorf("Unable to reset to %s: %v", hash.String(), err)
return err return err
} }

View File

@@ -1,3 +1,7 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Copyright 2022 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git package git
import ( import (
@@ -6,51 +10,46 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strings"
"syscall" "syscall"
"testing" "testing"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/actions-oss/act-cli/pkg/common"
) )
func TestFindGitSlug(t *testing.T) { func TestFindGitSlug(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
var slugTests = []struct { slugTests := []struct {
url string // input url string // input
provider string // expected result provider string // expected result
slug string // expected result slug string // expected result
}{ }{
{"https://git-codecommit.us-east-1.amazonaws.com/v1/repos/my-repo-name", "CodeCommit", "my-repo-name"}, {"https://git-codecommit.us-east-1.amazonaws.com/v1/repos/my-repo-name", "CodeCommit", "my-repo-name"},
{"ssh://git-codecommit.us-west-2.amazonaws.com/v1/repos/my-repo", "CodeCommit", "my-repo"}, {"ssh://git-codecommit.us-west-2.amazonaws.com/v1/repos/my-repo", "CodeCommit", "my-repo"},
{"git@github.com:actions-oss/act-cli.git", "GitHub", "actions-oss/act-cli"}, {"git@github.com:nektos/act.git", "GitHub", "nektos/act"},
{"git@github.com:actions-oss/act-cli", "GitHub", "actions-oss/act-cli"}, {"git@github.com:nektos/act", "GitHub", "nektos/act"},
{"https://github.com/actions-oss/act-cli.git", "GitHub", "actions-oss/act-cli"}, {"https://github.com/nektos/act.git", "GitHub", "nektos/act"},
{"http://github.com/actions-oss/act-cli.git", "GitHub", "actions-oss/act-cli"}, {"http://github.com/nektos/act.git", "GitHub", "nektos/act"},
{"https://github.com/actions-oss/act-cli", "GitHub", "actions-oss/act-cli"}, {"https://github.com/nektos/act", "GitHub", "nektos/act"},
{"http://github.com/actions-oss/act-cli", "GitHub", "actions-oss/act-cli"}, {"http://github.com/nektos/act", "GitHub", "nektos/act"},
{"git+ssh://git@github.com/owner/repo.git", "GitHub", "owner/repo"}, {"git+ssh://git@github.com/owner/repo.git", "GitHub", "owner/repo"},
{"http://myotherrepo.com/act.git", "", "http://myotherrepo.com/act.git"}, {"http://myotherrepo.com/act.git", "", "http://myotherrepo.com/act.git"},
{"https://gitea.com/actions-oss/act-cli.git", "GitHubEnterprise", "actions-oss/act-cli.git"},
} }
for _, tt := range slugTests { for _, tt := range slugTests {
provider, slug, err := findGitSlug(tt.url, "github.com") provider, slug, err := findGitSlug(tt.url, "github.com")
assert.NoError(err) assert.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(tt.provider, provider) assert.Equal(tt.provider, provider)
assert.Equal(tt.slug, slug) assert.Equal(tt.slug, slug)
} }
} }
func testDir(t *testing.T) string { func testDir(t *testing.T) string {
basedir, err := os.MkdirTemp("", "act-test") return t.TempDir()
require.NoError(t, err)
t.Cleanup(func() { _ = os.RemoveAll(basedir) })
return basedir
} }
func cleanGitHooks(dir string) error { func cleanGitHooks(dir string) error {
@@ -80,23 +79,23 @@ func TestFindGitRemoteURL(t *testing.T) {
basedir := testDir(t) basedir := testDir(t)
gitConfig() gitConfig()
err := gitCmd("init", basedir) err := gitCmd("init", basedir)
assert.NoError(err) assert.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
err = cleanGitHooks(basedir) err = cleanGitHooks(basedir)
assert.NoError(err) assert.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
remoteURL := "https://git-codecommit.us-east-1.amazonaws.com/v1/repos/my-repo-name" remoteURL := "https://git-codecommit.us-east-1.amazonaws.com/v1/repos/my-repo-name"
err = gitCmd("-C", basedir, "remote", "add", "origin", remoteURL) err = gitCmd("-C", basedir, "remote", "add", "origin", remoteURL)
assert.NoError(err) assert.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
u, err := findGitRemoteURL(context.Background(), basedir, "origin") u, err := findGitRemoteURL(context.Background(), basedir, "origin")
assert.NoError(err) assert.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(remoteURL, u) assert.Equal(remoteURL, u)
remoteURL = "git@github.com/AwesomeOwner/MyAwesomeRepo.git" remoteURL = "git@github.com/AwesomeOwner/MyAwesomeRepo.git"
err = gitCmd("-C", basedir, "remote", "add", "upstream", remoteURL) err = gitCmd("-C", basedir, "remote", "add", "upstream", remoteURL)
assert.NoError(err) assert.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
u, err = findGitRemoteURL(context.Background(), basedir, "upstream") u, err = findGitRemoteURL(context.Background(), basedir, "upstream")
assert.NoError(err) assert.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(remoteURL, u) assert.Equal(remoteURL, u)
} }
@@ -109,8 +108,8 @@ func TestGitFindRef(t *testing.T) {
Assert func(t *testing.T, ref string, err error) Assert func(t *testing.T, ref string, err error)
}{ }{
"new_repo": { "new_repo": {
Prepare: func(_ *testing.T, _ string) {}, Prepare: func(t *testing.T, dir string) {},
Assert: func(t *testing.T, _ string, err error) { Assert: func(t *testing.T, ref string, err error) {
require.Error(t, err) require.Error(t, err)
}, },
}, },
@@ -213,15 +212,71 @@ func TestGitCloneExecutor(t *testing.T) {
err := clone(context.Background()) err := clone(context.Background())
if tt.Err != nil { if tt.Err != nil {
assert.Error(t, err) assert.Error(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, tt.Err, err) assert.Equal(t, tt.Err, err)
} else { } else {
assert.Empty(t, err) assert.Empty(t, err) //nolint:testifylint // pre-existing issue from nektos/act
} }
}) })
} }
} }
func TestGitCloneExecutorNonFastForwardRef(t *testing.T) {
// Simulate the scenario where a remote ref (e.g. a GitHub PR head ref) changes
// non-fast-forward between two fetches. Before the fix, the fetch used Force=false,
// causing go-git to return ErrForceNeeded and short-circuit the checkout.
gitConfig()
// Create a bare "remote" repo with an initial commit on main and a feature branch.
remoteDir := t.TempDir()
require.NoError(t, gitCmd("init", "--bare", "--initial-branch=main", remoteDir))
// We need a working clone to push commits from.
workDir := t.TempDir()
require.NoError(t, gitCmd("clone", remoteDir, workDir))
require.NoError(t, gitCmd("-C", workDir, "checkout", "-b", "main"))
require.NoError(t, gitCmd("-C", workDir, "commit", "--allow-empty", "-m", "initial"))
require.NoError(t, gitCmd("-C", workDir, "push", "-u", "origin", "main"))
// Create a feature branch (simulates refs/pull/N/head).
require.NoError(t, gitCmd("-C", workDir, "checkout", "-b", "feature"))
require.NoError(t, gitCmd("-C", workDir, "commit", "--allow-empty", "-m", "feature-1"))
require.NoError(t, gitCmd("-C", workDir, "push", "origin", "feature"))
// First clone via the executor — should succeed and cache the repo.
cloneDir := t.TempDir()
clone := NewGitCloneExecutor(NewGitCloneExecutorInput{
URL: remoteDir,
Ref: "main",
Dir: cloneDir,
})
require.NoError(t, clone(context.Background()))
// Now force-push the feature branch to a non-fast-forward commit (simulates
// a PR rebase). This makes refs/heads/feature non-fast-forward.
require.NoError(t, gitCmd("-C", workDir, "checkout", "main"))
require.NoError(t, gitCmd("-C", workDir, "branch", "-D", "feature"))
require.NoError(t, gitCmd("-C", workDir, "checkout", "-b", "feature"))
require.NoError(t, gitCmd("-C", workDir, "commit", "--allow-empty", "-m", "feature-rewritten"))
require.NoError(t, gitCmd("-C", workDir, "push", "--force", "origin", "feature"))
// Also advance main so we can verify the clone picks up the new commit.
require.NoError(t, gitCmd("-C", workDir, "checkout", "main"))
require.NoError(t, gitCmd("-C", workDir, "commit", "--allow-empty", "-m", "second"))
require.NoError(t, gitCmd("-C", workDir, "push", "origin", "main"))
// Second clone to the same directory — before the fix this returned ErrForceNeeded
// and left the working tree at the old commit.
err := clone(context.Background())
require.NoError(t, err, "fetch with non-fast-forward refs must not fail when Force=true")
// Verify the working tree was actually updated to the latest main commit.
out, err := exec.Command("git", "-C", cloneDir, "log", "--oneline", "-1", "--format=%s").Output()
require.NoError(t, err)
assert.Equal(t, "second", strings.TrimSpace(string(out)), "working tree should be at the latest commit")
}
func gitConfig() { func gitConfig() {
if os.Getenv("GITHUB_ACTIONS") == "true" { if os.Getenv("GITHUB_ACTIONS") == "true" {
var err error var err error
@@ -242,37 +297,9 @@ func gitCmd(args ...string) error {
err := cmd.Run() err := cmd.Run()
if exitError, ok := err.(*exec.ExitError); ok { if exitError, ok := err.(*exec.ExitError); ok {
if waitStatus, ok := exitError.Sys().(syscall.WaitStatus); ok { if waitStatus, ok := exitError.Sys().(syscall.WaitStatus); ok {
return fmt.Errorf("exit error %d", waitStatus.ExitStatus()) return fmt.Errorf("Exit error %d", waitStatus.ExitStatus())
} }
return exitError return exitError
} }
return nil return nil
} }
func TestCloneIfRequired(t *testing.T) {
tempDir := t.TempDir()
ctx := context.Background()
t.Run("clone", func(t *testing.T) {
repo, err := CloneIfRequired(ctx, "refs/heads/main", NewGitCloneExecutorInput{
URL: "https://github.com/actions/checkout",
Dir: tempDir,
}, common.Logger(ctx))
assert.NoError(t, err)
assert.NotNil(t, repo)
})
t.Run("clone different remote", func(t *testing.T) {
repo, err := CloneIfRequired(ctx, "refs/heads/main", NewGitCloneExecutorInput{
URL: "https://github.com/actions/setup-go",
Dir: tempDir,
}, common.Logger(ctx))
require.NoError(t, err)
require.NotNil(t, repo)
remote, err := repo.Remote("origin")
require.NoError(t, err)
require.Len(t, remote.Config().URLs, 1)
assert.Equal(t, "https://github.com/actions/setup-go", remote.Config().URLs[0])
})
}

34
act/common/job_error.go Normal file
View File

@@ -0,0 +1,34 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Copyright 2021 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package common
import (
"context"
)
type jobErrorContextKey string
const jobErrorContextKeyVal = jobErrorContextKey("job.error")
// JobError returns the job error for current context if any
func JobError(ctx context.Context) error {
val := ctx.Value(jobErrorContextKeyVal)
if val != nil {
if container, ok := val.(map[string]error); ok {
return container["error"]
}
}
return nil
}
func SetJobError(ctx context.Context, err error) {
ctx.Value(jobErrorContextKeyVal).(map[string]error)["error"] = err
}
// WithJobErrorContainer adds a value to the context as a container for an error
func WithJobErrorContainer(ctx context.Context) context.Context {
container := map[string]error{}
return context.WithValue(ctx, jobErrorContextKeyVal, container)
}

View File

@@ -1,3 +1,7 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Copyright 2020 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package common package common
import ( import (

View File

@@ -1,3 +1,7 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Copyright 2020 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package common package common
import ( import (
@@ -18,7 +22,7 @@ func TestLineWriter(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
write := func(s string) { write := func(s string) {
n, err := lineWriter.Write([]byte(s)) n, err := lineWriter.Write([]byte(s))
assert.NoError(err) assert.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(len(s), n, s) assert.Equal(len(s), n, s)
} }

52
act/common/logger.go Normal file
View File

@@ -0,0 +1,52 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Copyright 2020 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package common
import (
"context"
"github.com/sirupsen/logrus"
)
type loggerContextKey string
const loggerContextKeyVal = loggerContextKey("logrus.FieldLogger")
// Logger returns the appropriate logger for current context
func Logger(ctx context.Context) logrus.FieldLogger {
val := ctx.Value(loggerContextKeyVal)
if val != nil {
if logger, ok := val.(logrus.FieldLogger); ok {
return logger
}
}
return logrus.StandardLogger()
}
// WithLogger adds a value to the context for the logger
func WithLogger(ctx context.Context, logger logrus.FieldLogger) context.Context {
return context.WithValue(ctx, loggerContextKeyVal, logger)
}
type loggerHookKey string
const loggerHookKeyVal = loggerHookKey("logrus.Hook")
// LoggerHook returns the appropriate logger hook for current context
// the hook affects job logger, not global logger
func LoggerHook(ctx context.Context) logrus.Hook {
val := ctx.Value(loggerHookKeyVal)
if val != nil {
if hook, ok := val.(logrus.Hook); ok {
return hook
}
}
return nil
}
// WithLoggerHook adds a value to the context for the logger hook
func WithLoggerHook(ctx context.Context, hook logrus.Hook) context.Context {
return context.WithValue(ctx, loggerHookKeyVal, hook)
}

View File

@@ -1,3 +1,7 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// Copyright 2021 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package common package common
import ( import (

View File

@@ -1,13 +1,16 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// Copyright 2023 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package container package container
import ( import (
"context" "context"
"io" "io"
"os"
"github.com/actions-oss/act-cli/pkg/common" "gitea.com/gitea/act_runner/act/common"
"github.com/docker/go-connections/nat" "github.com/docker/go-connections/nat"
"golang.org/x/term"
) )
// NewContainerInput the input for the New function // NewContainerInput the input for the New function
@@ -32,21 +35,26 @@ type NewContainerInput struct {
NetworkAliases []string NetworkAliases []string
ExposedPorts nat.PortSet ExposedPorts nat.PortSet
PortBindings nat.PortMap PortBindings nat.PortMap
// Gitea specific
AutoRemove bool
ValidVolumes []string
} }
// FileEntry is a file to copy to a container // FileEntry is a file to copy to a container
type FileEntry struct { type FileEntry struct {
Name string Name string
Mode uint32 Mode int64
Body string Body string
} }
// Container for managing docker run containers // Container for managing docker run containers
type Container interface { type Container interface {
Create(capAdd []string, capDrop []string) common.Executor Create(capAdd, capDrop []string) common.Executor
ConnectToNetwork(name string) common.Executor
Copy(destPath string, files ...*FileEntry) common.Executor Copy(destPath string, files ...*FileEntry) common.Executor
CopyTarStream(ctx context.Context, destPath string, tarStream io.Reader) error CopyTarStream(ctx context.Context, destPath string, tarStream io.Reader) error
CopyDir(destPath string, srcPath string, useGitIgnore bool) common.Executor CopyDir(destPath, srcPath string, useGitIgnore bool) common.Executor
GetContainerArchive(ctx context.Context, srcPath string) (io.ReadCloser, error) GetContainerArchive(ctx context.Context, srcPath string) (io.ReadCloser, error)
Pull(forcePull bool) common.Executor Pull(forcePull bool) common.Executor
Start(attach bool) common.Executor Start(attach bool) common.Executor
@@ -56,7 +64,6 @@ type Container interface {
Remove() common.Executor Remove() common.Executor
Close() common.Executor Close() common.Executor
ReplaceLogWriter(io.Writer, io.Writer) (io.Writer, io.Writer) ReplaceLogWriter(io.Writer, io.Writer) (io.Writer, io.Writer)
GetHealth(ctx context.Context) Health
} }
// NewDockerBuildExecutorInput the input for the NewDockerBuildExecutor function // NewDockerBuildExecutorInput the input for the NewDockerBuildExecutor function
@@ -76,21 +83,3 @@ type NewDockerPullExecutorInput struct {
Username string Username string
Password string Password string
} }
type Health int
const (
HealthStarting Health = iota
HealthHealthy
HealthUnHealthy
)
var containerAllocateTerminal bool
func init() {
containerAllocateTerminal = term.IsTerminal(int(os.Stdout.Fd()))
}
func SetContainerAllocateTerminal(val bool) {
containerAllocateTerminal = val
}

View File

@@ -1,3 +1,7 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// Copyright 2021 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows || netbsd)) //go:build !(WITHOUT_DOCKER || !(linux || darwin || windows || netbsd))
package container package container
@@ -6,7 +10,8 @@ import (
"context" "context"
"strings" "strings"
"github.com/actions-oss/act-cli/pkg/common" "gitea.com/gitea/act_runner/act/common"
"github.com/docker/cli/cli/config" "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/credentials" "github.com/docker/cli/cli/config/credentials"
"github.com/docker/docker/api/types/registry" "github.com/docker/docker/api/types/registry"

View File

@@ -1,21 +1,24 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// Copyright 2020 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows || netbsd)) //go:build !(WITHOUT_DOCKER || !(linux || darwin || windows || netbsd))
package container package container
import ( import (
"context" "context"
"errors"
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"github.com/docker/docker/api/types/build" "gitea.com/gitea/act_runner/act/common"
"github.com/moby/go-archive"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/archive"
// github.com/docker/docker/builder/dockerignore is deprecated
"github.com/moby/buildkit/frontend/dockerfile/dockerignore"
"github.com/moby/patternmatcher" "github.com/moby/patternmatcher"
"github.com/moby/patternmatcher/ignorefile"
"github.com/actions-oss/act-cli/pkg/common"
) )
// NewDockerBuildExecutor function to create a run executor for the container // NewDockerBuildExecutor function to create a run executor for the container
@@ -40,7 +43,7 @@ func NewDockerBuildExecutor(input NewDockerBuildExecutorInput) common.Executor {
logger.Debugf("Building image from '%v'", input.ContextDir) logger.Debugf("Building image from '%v'", input.ContextDir)
tags := []string{input.ImageTag} tags := []string{input.ImageTag}
options := build.ImageBuildOptions{ options := types.ImageBuildOptions{
Tags: tags, Tags: tags,
Remove: true, Remove: true,
Platform: input.Platform, Platform: input.Platform,
@@ -62,15 +65,19 @@ func NewDockerBuildExecutor(input NewDockerBuildExecutorInput) common.Executor {
logger.Debugf("Creating image from context dir '%s' with tag '%s' and platform '%s'", input.ContextDir, input.ImageTag, input.Platform) logger.Debugf("Creating image from context dir '%s' with tag '%s' and platform '%s'", input.ContextDir, input.ImageTag, input.Platform)
resp, err := cli.ImageBuild(ctx, buildContext, options) resp, err := cli.ImageBuild(ctx, buildContext, options)
err = errors.Join(err, logDockerResponse(logger, resp.Body, err != nil)) err = logDockerResponse(logger, resp.Body, err != nil)
if err != nil {
return err return err
} }
return nil
} }
func createBuildContext(ctx context.Context, contextDir string, relDockerfile string) (io.ReadCloser, error) { }
func createBuildContext(ctx context.Context, contextDir, relDockerfile string) (io.ReadCloser, error) {
common.Logger(ctx).Debugf("Creating archive for build context dir '%s' with relative dockerfile '%s'", contextDir, relDockerfile) common.Logger(ctx).Debugf("Creating archive for build context dir '%s' with relative dockerfile '%s'", contextDir, relDockerfile)
// And canonicalize dockerfile name to a platform-independent one // And canonicalize dockerfile name to a platform-independent one
relDockerfile = filepath.ToSlash(relDockerfile) relDockerfile = archive.CanonicalTarNameForPath(relDockerfile)
f, err := os.Open(filepath.Join(contextDir, ".dockerignore")) f, err := os.Open(filepath.Join(contextDir, ".dockerignore"))
if err != nil && !os.IsNotExist(err) { if err != nil && !os.IsNotExist(err) {
@@ -80,7 +87,7 @@ func createBuildContext(ctx context.Context, contextDir string, relDockerfile st
var excludes []string var excludes []string
if err == nil { if err == nil {
excludes, err = ignorefile.ReadAll(f) excludes, err = dockerignore.ReadAll(f) //nolint:staticcheck // pre-existing issue from nektos/act
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -93,7 +100,7 @@ func createBuildContext(ctx context.Context, contextDir string, relDockerfile st
// removed. The daemon will remove them for us, if needed, after it // removed. The daemon will remove them for us, if needed, after it
// parses the Dockerfile. Ignore errors here, as they will have been // parses the Dockerfile. Ignore errors here, as they will have been
// caught by validateContextDirectory above. // caught by validateContextDirectory above.
var includes = []string{"."} includes := []string{"."}
keepThem1, _ := patternmatcher.Matches(".dockerignore", excludes) keepThem1, _ := patternmatcher.Matches(".dockerignore", excludes)
keepThem2, _ := patternmatcher.Matches(relDockerfile, excludes) keepThem2, _ := patternmatcher.Matches(relDockerfile, excludes)
if keepThem1 || keepThem2 { if keepThem1 || keepThem2 {

View File

@@ -1,3 +1,7 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Copyright 2022 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows || netbsd)) //go:build !(WITHOUT_DOCKER || !(linux || darwin || windows || netbsd))
// This file is exact copy of https://github.com/docker/cli/blob/9ac8584acfd501c3f4da0e845e3a40ed15c85041/cli/command/container/opts.go // This file is exact copy of https://github.com/docker/cli/blob/9ac8584acfd501c3f4da0e845e3a40ed15c85041/cli/command/container/opts.go
@@ -7,7 +11,7 @@
// See DOCKER_LICENSE for the full license text. // See DOCKER_LICENSE for the full license text.
// //
//nolint:unparam,errcheck,depguard,deadcode,unused //nolint:errcheck,depguard,unused // verbatim copy from docker/cli with minimal changes
package container package container
import ( import (
@@ -19,6 +23,7 @@ import (
"path/filepath" "path/filepath"
"reflect" "reflect"
"regexp" "regexp"
"slices"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -37,9 +42,7 @@ import (
"github.com/spf13/pflag" "github.com/spf13/pflag"
) )
var ( var deviceCgroupRuleRegexp = regexp.MustCompile(`^[acb] ([0-9]+|\*):([0-9]+|\*) [rwm]{1,3}$`)
deviceCgroupRuleRegexp = regexp.MustCompile(`^[acb] ([0-9]+|\*):([0-9]+|\*) [rwm]{1,3}$`)
)
// containerOptions is a data object with all the options for creating a container // containerOptions is a data object with all the options for creating a container
type containerOptions struct { type containerOptions struct {
@@ -108,7 +111,7 @@ type containerOptions struct {
cpusetCpus string cpusetCpus string
cpusetMems string cpusetMems string
blkioWeight uint16 blkioWeight uint16
ioMaxBandwidth uint64 ioMaxBandwidth opts.MemBytes
ioMaxIOps uint64 ioMaxIOps uint64
swappiness int64 swappiness int64
netMode opts.NetworkOpt netMode opts.NetworkOpt
@@ -285,7 +288,7 @@ func addFlags(flags *pflag.FlagSet) *containerOptions {
flags.Var(&copts.deviceReadIOps, "device-read-iops", "Limit read rate (IO per second) from a device") flags.Var(&copts.deviceReadIOps, "device-read-iops", "Limit read rate (IO per second) from a device")
flags.Var(&copts.deviceWriteBps, "device-write-bps", "Limit write rate (bytes per second) to a device") flags.Var(&copts.deviceWriteBps, "device-write-bps", "Limit write rate (bytes per second) to a device")
flags.Var(&copts.deviceWriteIOps, "device-write-iops", "Limit write rate (IO per second) to a device") flags.Var(&copts.deviceWriteIOps, "device-write-iops", "Limit write rate (IO per second) to a device")
flags.Uint64Var(&copts.ioMaxBandwidth, "io-maxbandwidth", 0, "Maximum IO bandwidth limit for the system drive (Windows only)") flags.Var(&copts.ioMaxBandwidth, "io-maxbandwidth", "Maximum IO bandwidth limit for the system drive (Windows only)")
flags.SetAnnotation("io-maxbandwidth", "ostype", []string{"windows"}) flags.SetAnnotation("io-maxbandwidth", "ostype", []string{"windows"})
flags.Uint64Var(&copts.ioMaxIOps, "io-maxiops", 0, "Maximum IOps limit for the system drive (Windows only)") flags.Uint64Var(&copts.ioMaxIOps, "io-maxiops", 0, "Maximum IOps limit for the system drive (Windows only)")
flags.SetAnnotation("io-maxiops", "ostype", []string{"windows"}) flags.SetAnnotation("io-maxiops", "ostype", []string{"windows"})
@@ -322,7 +325,7 @@ type containerConfig struct {
// a HostConfig and returns them with the specified command. // a HostConfig and returns them with the specified command.
// If the specified args are not valid, it will return an error. // If the specified args are not valid, it will return an error.
// //
//nolint:gocyclo //nolint:gocyclo // function handles many cases
func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*containerConfig, error) { func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*containerConfig, error) {
var ( var (
attachStdin = copts.attach.Get("stdin") attachStdin = copts.attach.Get("stdin")
@@ -388,7 +391,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
// Can't evaluate options passed into --tmpfs until we actually mount // Can't evaluate options passed into --tmpfs until we actually mount
tmpfs := make(map[string]string) tmpfs := make(map[string]string)
for _, t := range copts.tmpfs.GetSlice() { for _, t := range copts.tmpfs.GetAll() {
if arr := strings.SplitN(t, ":", 2); len(arr) > 1 { if arr := strings.SplitN(t, ":", 2); len(arr) > 1 {
tmpfs[arr[0]] = arr[1] tmpfs[arr[0]] = arr[1]
} else { } else {
@@ -412,7 +415,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
entrypoint = []string{""} entrypoint = []string{""}
} }
publishOpts := copts.publish.GetSlice() publishOpts := copts.publish.GetAll()
var ( var (
ports map[nat.Port]struct{} ports map[nat.Port]struct{}
portBindings map[nat.Port][]nat.PortBinding portBindings map[nat.Port][]nat.PortBinding
@@ -430,7 +433,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
} }
// Merge in exposed ports to the map of published ports // Merge in exposed ports to the map of published ports
for _, e := range copts.expose.GetSlice() { for _, e := range copts.expose.GetAll() {
if strings.Contains(e, ":") { if strings.Contains(e, ":") {
return nil, errors.Errorf("invalid port format for --expose: %s", e) return nil, errors.Errorf("invalid port format for --expose: %s", e)
} }
@@ -459,7 +462,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
// parsing flags, we haven't yet sent a _ping to the daemon to determine // parsing flags, we haven't yet sent a _ping to the daemon to determine
// what operating system it is. // what operating system it is.
deviceMappings := []container.DeviceMapping{} deviceMappings := []container.DeviceMapping{}
for _, device := range copts.devices.GetSlice() { for _, device := range copts.devices.GetAll() {
var ( var (
validated string validated string
deviceMapping container.DeviceMapping deviceMapping container.DeviceMapping
@@ -477,13 +480,13 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
} }
// collect all the environment variables for the container // collect all the environment variables for the container
envVariables, err := opts.ReadKVEnvStrings(copts.envFile.GetSlice(), copts.env.GetSlice()) envVariables, err := opts.ReadKVEnvStrings(copts.envFile.GetAll(), copts.env.GetAll())
if err != nil { if err != nil {
return nil, err return nil, err
} }
// collect all the labels for the container // collect all the labels for the container
labels, err := opts.ReadKVStrings(copts.labelsFile.GetSlice(), copts.labels.GetSlice()) labels, err := opts.ReadKVStrings(copts.labelsFile.GetAll(), copts.labels.GetAll())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -513,19 +516,19 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
return nil, err return nil, err
} }
loggingOpts, err := parseLoggingOpts(copts.loggingDriver, copts.loggingOpts.GetSlice()) loggingOpts, err := parseLoggingOpts(copts.loggingDriver, copts.loggingOpts.GetAll())
if err != nil { if err != nil {
return nil, err return nil, err
} }
securityOpts, err := parseSecurityOpts(copts.securityOpt.GetSlice()) securityOpts, err := parseSecurityOpts(copts.securityOpt.GetAll())
if err != nil { if err != nil {
return nil, err return nil, err
} }
securityOpts, maskedPaths, readonlyPaths := parseSystemPaths(securityOpts) securityOpts, maskedPaths, readonlyPaths := parseSystemPaths(securityOpts)
storageOpts, err := parseStorageOpts(copts.storageOpt.GetSlice()) storageOpts, err := parseStorageOpts(copts.storageOpt.GetAll())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -559,7 +562,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
return nil, errors.Errorf("--health-retries cannot be negative") return nil, errors.Errorf("--health-retries cannot be negative")
} }
if copts.healthStartPeriod < 0 { if copts.healthStartPeriod < 0 {
return nil, fmt.Errorf("--health-start-period cannot be negative") return nil, errors.New("--health-start-period cannot be negative")
} }
healthConfig = &container.HealthConfig{ healthConfig = &container.HealthConfig{
@@ -597,9 +600,9 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
BlkioDeviceReadIOps: copts.deviceReadIOps.GetList(), BlkioDeviceReadIOps: copts.deviceReadIOps.GetList(),
BlkioDeviceWriteIOps: copts.deviceWriteIOps.GetList(), BlkioDeviceWriteIOps: copts.deviceWriteIOps.GetList(),
IOMaximumIOps: copts.ioMaxIOps, IOMaximumIOps: copts.ioMaxIOps,
IOMaximumBandwidth: copts.ioMaxBandwidth, IOMaximumBandwidth: uint64(copts.ioMaxBandwidth),
Ulimits: copts.ulimits.GetList(), Ulimits: copts.ulimits.GetList(),
DeviceCgroupRules: copts.deviceCgroupRules.GetSlice(), DeviceCgroupRules: copts.deviceCgroupRules.GetAll(),
Devices: deviceMappings, Devices: deviceMappings,
DeviceRequests: copts.gpus.Value(), DeviceRequests: copts.gpus.Value(),
} }
@@ -640,7 +643,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
AutoRemove: copts.autoRemove, AutoRemove: copts.autoRemove,
Privileged: copts.privileged, Privileged: copts.privileged,
PortBindings: portBindings, PortBindings: portBindings,
Links: copts.links.GetSlice(), Links: copts.links.GetAll(),
PublishAllPorts: copts.publishAll, PublishAllPorts: copts.publishAll,
// Make sure the dns fields are never nil. // Make sure the dns fields are never nil.
// New containers don't ever have those fields nil, // New containers don't ever have those fields nil,
@@ -650,17 +653,17 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
DNS: copts.dns.GetAllOrEmpty(), DNS: copts.dns.GetAllOrEmpty(),
DNSSearch: copts.dnsSearch.GetAllOrEmpty(), DNSSearch: copts.dnsSearch.GetAllOrEmpty(),
DNSOptions: copts.dnsOptions.GetAllOrEmpty(), DNSOptions: copts.dnsOptions.GetAllOrEmpty(),
ExtraHosts: copts.extraHosts.GetSlice(), ExtraHosts: copts.extraHosts.GetAll(),
VolumesFrom: copts.volumesFrom.GetSlice(), VolumesFrom: copts.volumesFrom.GetAll(),
IpcMode: container.IpcMode(copts.ipcMode), IpcMode: container.IpcMode(copts.ipcMode),
NetworkMode: container.NetworkMode(copts.netMode.NetworkMode()), NetworkMode: container.NetworkMode(copts.netMode.NetworkMode()),
PidMode: pidMode, PidMode: pidMode,
UTSMode: utsMode, UTSMode: utsMode,
UsernsMode: usernsMode, UsernsMode: usernsMode,
CgroupnsMode: cgroupnsMode, CgroupnsMode: cgroupnsMode,
CapAdd: strslice.StrSlice(copts.capAdd.GetSlice()), CapAdd: strslice.StrSlice(copts.capAdd.GetAll()),
CapDrop: strslice.StrSlice(copts.capDrop.GetSlice()), CapDrop: strslice.StrSlice(copts.capDrop.GetAll()),
GroupAdd: copts.groupAdd.GetSlice(), GroupAdd: copts.groupAdd.GetAll(),
RestartPolicy: restartPolicy, RestartPolicy: restartPolicy,
SecurityOpt: securityOpts, SecurityOpt: securityOpts,
StorageOpt: storageOpts, StorageOpt: storageOpts,
@@ -679,7 +682,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
} }
if copts.autoRemove && !hostConfig.RestartPolicy.IsNone() { if copts.autoRemove && !hostConfig.RestartPolicy.IsNone() {
return nil, errors.Errorf("conflicting options: --restart and --rm") return nil, errors.Errorf("Conflicting options: --restart and --rm")
} }
// only set this value if the user provided the flag, else it should default to nil // only set this value if the user provided the flag, else it should default to nil
@@ -778,11 +781,11 @@ func applyContainerOptions(n *opts.NetworkAttachmentOpts, copts *containerOption
} }
if copts.aliases.Len() > 0 { if copts.aliases.Len() > 0 {
n.Aliases = make([]string, copts.aliases.Len()) n.Aliases = make([]string, copts.aliases.Len())
copy(n.Aliases, copts.aliases.GetSlice()) copy(n.Aliases, copts.aliases.GetAll())
} }
if copts.links.Len() > 0 { if copts.links.Len() > 0 {
n.Links = make([]string, copts.links.Len()) n.Links = make([]string, copts.links.Len())
copy(n.Links, copts.links.GetSlice()) copy(n.Links, copts.links.GetAll())
} }
if copts.ipv4Address != "" { if copts.ipv4Address != "" {
n.IPv4Address = copts.ipv4Address n.IPv4Address = copts.ipv4Address
@@ -794,7 +797,7 @@ func applyContainerOptions(n *opts.NetworkAttachmentOpts, copts *containerOption
// TODO should linkLocalIPs be added to the _first_ network only, or to _all_ networks? (should this be a per-network option as well?) // TODO should linkLocalIPs be added to the _first_ network only, or to _all_ networks? (should this be a per-network option as well?)
if copts.linkLocalIPs.Len() > 0 { if copts.linkLocalIPs.Len() > 0 {
n.LinkLocalIPs = make([]string, copts.linkLocalIPs.Len()) n.LinkLocalIPs = make([]string, copts.linkLocalIPs.Len())
copy(n.LinkLocalIPs, copts.linkLocalIPs.GetSlice()) copy(n.LinkLocalIPs, copts.linkLocalIPs.GetAll())
} }
return nil return nil
} }
@@ -836,7 +839,7 @@ func convertToStandardNotation(ports []string) ([]string, error) {
for _, publish := range ports { for _, publish := range ports {
if strings.Contains(publish, "=") { if strings.Contains(publish, "=") {
params := map[string]string{"protocol": "tcp"} params := map[string]string{"protocol": "tcp"}
for _, param := range strings.Split(publish, ",") { for param := range strings.SplitSeq(publish, ",") {
opt := strings.Split(param, "=") opt := strings.Split(param, "=")
if len(opt) < 2 { if len(opt) < 2 {
return optsList, errors.Errorf("invalid publish opts format (should be name=value but got '%s')", param) return optsList, errors.Errorf("invalid publish opts format (should be name=value but got '%s')", param)
@@ -868,7 +871,7 @@ func parseSecurityOpts(securityOpts []string) ([]string, error) {
if strings.Contains(opt, ":") { if strings.Contains(opt, ":") {
con = strings.SplitN(opt, ":", 2) con = strings.SplitN(opt, ":", 2)
} else { } else {
return securityOpts, errors.Errorf("invalid --security-opt: %q", opt) return securityOpts, errors.Errorf("Invalid --security-opt: %q", opt)
} }
} }
if con[0] == "seccomp" && con[1] != "unconfined" { if con[0] == "seccomp" && con[1] != "unconfined" {
@@ -987,7 +990,7 @@ func validateDeviceCgroupRule(val string) (string, error) {
// validDeviceMode checks if the mode for device is valid or not. // validDeviceMode checks if the mode for device is valid or not.
// Valid mode is a composition of r (read), w (write), and m (mknod). // Valid mode is a composition of r (read), w (write), and m (mknod).
func validDeviceMode(mode string) bool { func validDeviceMode(mode string) bool {
var legalDeviceMode = map[rune]bool{ legalDeviceMode := map[rune]bool{
'r': true, 'r': true,
'w': true, 'w': true,
'm': true, 'm': true,
@@ -1005,7 +1008,7 @@ func validDeviceMode(mode string) bool {
} }
// validateDevice validates a path for devices // validateDevice validates a path for devices
func validateDevice(val string, serverOS string) (string, error) { func validateDevice(val, serverOS string) (string, error) {
switch serverOS { switch serverOS {
case "linux": case "linux":
return validateLinuxPath(val, validDeviceMode) return validateLinuxPath(val, validDeviceMode)
@@ -1066,11 +1069,9 @@ func validateLinuxPath(val string, validator func(string) bool) (string, error)
// validateAttach validates that the specified string is a valid attach option. // validateAttach validates that the specified string is a valid attach option.
func validateAttach(val string) (string, error) { func validateAttach(val string) (string, error) {
s := strings.ToLower(val) s := strings.ToLower(val)
for _, str := range []string{"stdin", "stdout", "stderr"} { if slices.Contains([]string{"stdin", "stdout", "stderr"}, s) {
if s == str {
return s, nil return s, nil
} }
}
return val, errors.Errorf("valid streams are STDIN, STDOUT and STDERR") return val, errors.Errorf("valid streams are STDIN, STDOUT and STDERR")
} }

View File

@@ -1,3 +1,7 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Copyright 2022 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// This file is exact copy of https://github.com/docker/cli/blob/9ac8584acfd501c3f4da0e845e3a40ed15c85041/cli/command/container/opts_test.go with: // This file is exact copy of https://github.com/docker/cli/blob/9ac8584acfd501c3f4da0e845e3a40ed15c85041/cli/command/container/opts_test.go with:
// * appended with license information // * appended with license information
// * commented out case 'invalid-mixed-network-types' in test TestParseNetworkConfig // * commented out case 'invalid-mixed-network-types' in test TestParseNetworkConfig
@@ -6,7 +10,7 @@
// See DOCKER_LICENSE for the full license text. // See DOCKER_LICENSE for the full license text.
// //
//nolint:unparam,whitespace,depguard,dupl,gocritic //nolint:depguard,gocritic // verbatim copy from docker/cli tests
package container package container
import ( import (
@@ -73,21 +77,21 @@ func setupRunFlags() (*pflag.FlagSet, *containerOptions) {
return flags, copts return flags, copts
} }
func mustParse(t *testing.T, args string) (*container.Config, *container.HostConfig, *networktypes.NetworkingConfig) { func mustParse(t *testing.T, args string) (*container.Config, *container.HostConfig) {
t.Helper() t.Helper()
config, hostConfig, networkingConfig, err := parseRun(append(strings.Split(args, " "), "ubuntu", "bash")) config, hostConfig, _, err := parseRun(append(strings.Split(args, " "), "ubuntu", "bash"))
assert.NilError(t, err) assert.NilError(t, err)
return config, hostConfig, networkingConfig return config, hostConfig
} }
func TestParseRunLinks(t *testing.T) { func TestParseRunLinks(t *testing.T) {
if _, hostConfig, _ := mustParse(t, "--link a:b"); len(hostConfig.Links) == 0 || hostConfig.Links[0] != "a:b" { if _, hostConfig := mustParse(t, "--link a:b"); len(hostConfig.Links) == 0 || hostConfig.Links[0] != "a:b" {
t.Fatalf("Error parsing links. Expected []string{\"a:b\"}, received: %v", hostConfig.Links) t.Fatalf("Error parsing links. Expected []string{\"a:b\"}, received: %v", hostConfig.Links)
} }
if _, hostConfig, _ := mustParse(t, "--link a:b --link c:d"); len(hostConfig.Links) < 2 || hostConfig.Links[0] != "a:b" || hostConfig.Links[1] != "c:d" { if _, hostConfig := mustParse(t, "--link a:b --link c:d"); len(hostConfig.Links) < 2 || hostConfig.Links[0] != "a:b" || hostConfig.Links[1] != "c:d" {
t.Fatalf("Error parsing links. Expected []string{\"a:b\", \"c:d\"}, received: %v", hostConfig.Links) t.Fatalf("Error parsing links. Expected []string{\"a:b\", \"c:d\"}, received: %v", hostConfig.Links)
} }
if _, hostConfig, _ := mustParse(t, ""); len(hostConfig.Links) != 0 { if _, hostConfig := mustParse(t, ""); len(hostConfig.Links) != 0 {
t.Fatalf("Error parsing links. No link expected, received: %v", hostConfig.Links) t.Fatalf("Error parsing links. No link expected, received: %v", hostConfig.Links)
} }
} }
@@ -136,7 +140,7 @@ func TestParseRunAttach(t *testing.T) {
} }
for _, tc := range tests { for _, tc := range tests {
t.Run(tc.input, func(t *testing.T) { t.Run(tc.input, func(t *testing.T) {
config, _, _ := mustParse(t, tc.input) config, _ := mustParse(t, tc.input)
assert.Equal(t, config.AttachStdin, tc.expected.AttachStdin) assert.Equal(t, config.AttachStdin, tc.expected.AttachStdin)
assert.Equal(t, config.AttachStdout, tc.expected.AttachStdout) assert.Equal(t, config.AttachStdout, tc.expected.AttachStdout)
assert.Equal(t, config.AttachStderr, tc.expected.AttachStderr) assert.Equal(t, config.AttachStderr, tc.expected.AttachStderr)
@@ -190,12 +194,11 @@ func TestParseRunWithInvalidArgs(t *testing.T) {
} }
} }
//nolint:gocyclo //nolint:gocyclo // function handles many cases
func TestParseWithVolumes(t *testing.T) { func TestParseWithVolumes(t *testing.T) {
// A single volume // A single volume
arr, tryit := setupPlatformVolume([]string{`/tmp`}, []string{`c:\tmp`}) arr, tryit := setupPlatformVolume([]string{`/tmp`}, []string{`c:\tmp`})
if config, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds != nil { if config, hostConfig := mustParse(t, tryit); hostConfig.Binds != nil {
t.Fatalf("Error parsing volume flags, %q should not mount-bind anything. Received %v", tryit, hostConfig.Binds) t.Fatalf("Error parsing volume flags, %q should not mount-bind anything. Received %v", tryit, hostConfig.Binds)
} else if _, exists := config.Volumes[arr[0]]; !exists { } else if _, exists := config.Volumes[arr[0]]; !exists {
t.Fatalf("Error parsing volume flags, %q is missing from volumes. Received %v", tryit, config.Volumes) t.Fatalf("Error parsing volume flags, %q is missing from volumes. Received %v", tryit, config.Volumes)
@@ -203,7 +206,7 @@ func TestParseWithVolumes(t *testing.T) {
// Two volumes // Two volumes
arr, tryit = setupPlatformVolume([]string{`/tmp`, `/var`}, []string{`c:\tmp`, `c:\var`}) arr, tryit = setupPlatformVolume([]string{`/tmp`, `/var`}, []string{`c:\tmp`, `c:\var`})
if config, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds != nil { if config, hostConfig := mustParse(t, tryit); hostConfig.Binds != nil {
t.Fatalf("Error parsing volume flags, %q should not mount-bind anything. Received %v", tryit, hostConfig.Binds) t.Fatalf("Error parsing volume flags, %q should not mount-bind anything. Received %v", tryit, hostConfig.Binds)
} else if _, exists := config.Volumes[arr[0]]; !exists { } else if _, exists := config.Volumes[arr[0]]; !exists {
t.Fatalf("Error parsing volume flags, %s is missing from volumes. Received %v", arr[0], config.Volumes) t.Fatalf("Error parsing volume flags, %s is missing from volumes. Received %v", arr[0], config.Volumes)
@@ -213,13 +216,13 @@ func TestParseWithVolumes(t *testing.T) {
// A single bind mount // A single bind mount
arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`}, []string{os.Getenv("TEMP") + `:c:\containerTmp`}) arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`}, []string{os.Getenv("TEMP") + `:c:\containerTmp`})
if config, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds == nil || hostConfig.Binds[0] != arr[0] { if config, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || hostConfig.Binds[0] != arr[0] {
t.Fatalf("Error parsing volume flags, %q should mount-bind the path before the colon into the path after the colon. Received %v %v", arr[0], hostConfig.Binds, config.Volumes) t.Fatalf("Error parsing volume flags, %q should mount-bind the path before the colon into the path after the colon. Received %v %v", arr[0], hostConfig.Binds, config.Volumes)
} }
// Two bind mounts. // Two bind mounts.
arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`, `/hostVar:/containerVar`}, []string{os.Getenv("ProgramData") + `:c:\ContainerPD`, os.Getenv("TEMP") + `:c:\containerTmp`}) arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`, `/hostVar:/containerVar`}, []string{os.Getenv("ProgramData") + `:c:\ContainerPD`, os.Getenv("TEMP") + `:c:\containerTmp`})
if _, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil { if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil {
t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds) t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds)
} }
@@ -228,26 +231,26 @@ func TestParseWithVolumes(t *testing.T) {
arr, tryit = setupPlatformVolume( arr, tryit = setupPlatformVolume(
[]string{`/hostTmp:/containerTmp:ro`, `/hostVar:/containerVar:rw`}, []string{`/hostTmp:/containerTmp:ro`, `/hostVar:/containerVar:rw`},
[]string{os.Getenv("TEMP") + `:c:\containerTmp:rw`, os.Getenv("ProgramData") + `:c:\ContainerPD:rw`}) []string{os.Getenv("TEMP") + `:c:\containerTmp:rw`, os.Getenv("ProgramData") + `:c:\ContainerPD:rw`})
if _, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil { if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil {
t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds) t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds)
} }
// Similar to previous test but with alternate modes which are only supported by Linux // Similar to previous test but with alternate modes which are only supported by Linux
if runtime.GOOS != "windows" { if runtime.GOOS != "windows" {
arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:ro,Z`, `/hostVar:/containerVar:rw,Z`}, []string{}) arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:ro,Z`, `/hostVar:/containerVar:rw,Z`}, []string{})
if _, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil { if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil {
t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds) t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds)
} }
arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:Z`, `/hostVar:/containerVar:z`}, []string{}) arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:Z`, `/hostVar:/containerVar:z`}, []string{})
if _, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil { if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil {
t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds) t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds)
} }
} }
// One bind mount and one volume // One bind mount and one volume
arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`, `/containerVar`}, []string{os.Getenv("TEMP") + `:c:\containerTmp`, `c:\containerTmp`}) arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`, `/containerVar`}, []string{os.Getenv("TEMP") + `:c:\containerTmp`, `c:\containerTmp`})
if config, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds == nil || len(hostConfig.Binds) > 1 || hostConfig.Binds[0] != arr[0] { if config, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || len(hostConfig.Binds) > 1 || hostConfig.Binds[0] != arr[0] {
t.Fatalf("Error parsing volume flags, %s and %s should only one and only one bind mount %s. Received %s", arr[0], arr[1], arr[0], hostConfig.Binds) t.Fatalf("Error parsing volume flags, %s and %s should only one and only one bind mount %s. Received %s", arr[0], arr[1], arr[0], hostConfig.Binds)
} else if _, exists := config.Volumes[arr[1]]; !exists { } else if _, exists := config.Volumes[arr[1]]; !exists {
t.Fatalf("Error parsing volume flags %s and %s. %s is missing from volumes. Received %v", arr[0], arr[1], arr[1], config.Volumes) t.Fatalf("Error parsing volume flags %s and %s. %s is missing from volumes. Received %v", arr[0], arr[1], arr[1], config.Volumes)
@@ -256,18 +259,17 @@ func TestParseWithVolumes(t *testing.T) {
// Root to non-c: drive letter (Windows specific) // Root to non-c: drive letter (Windows specific)
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
arr, tryit = setupPlatformVolume([]string{}, []string{os.Getenv("SystemDrive") + `\:d:`}) arr, tryit = setupPlatformVolume([]string{}, []string{os.Getenv("SystemDrive") + `\:d:`})
if config, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds == nil || len(hostConfig.Binds) > 1 || hostConfig.Binds[0] != arr[0] || len(config.Volumes) != 0 { if config, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || len(hostConfig.Binds) > 1 || hostConfig.Binds[0] != arr[0] || len(config.Volumes) != 0 {
t.Fatalf("Error parsing %s. Should have a single bind mount and no volumes", arr[0]) t.Fatalf("Error parsing %s. Should have a single bind mount and no volumes", arr[0])
} }
} }
} }
// setupPlatformVolume takes two arrays of volume specs - a Unix style // setupPlatformVolume takes two arrays of volume specs - a Unix style
// spec and a Windows style spec. Depending on the platform being unit tested, // spec and a Windows style spec. Depending on the platform being unit tested,
// it returns one of them, along with a volume string that would be passed // it returns one of them, along with a volume string that would be passed
// on the docker CLI (e.g. -v /bar -v /foo). // on the docker CLI (e.g. -v /bar -v /foo).
func setupPlatformVolume(u []string, w []string) ([]string, string) { func setupPlatformVolume(u, w []string) ([]string, string) {
var a []string var a []string
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
a = w a = w
@@ -300,9 +302,9 @@ func TestParseWithMacAddress(t *testing.T) {
if _, _, _, err := parseRun([]string{invalidMacAddress, "img", "cmd"}); err != nil && err.Error() != "invalidMacAddress is not a valid mac address" { if _, _, _, err := parseRun([]string{invalidMacAddress, "img", "cmd"}); err != nil && err.Error() != "invalidMacAddress is not a valid mac address" {
t.Fatalf("Expected an error with %v mac-address, got %v", invalidMacAddress, err) t.Fatalf("Expected an error with %v mac-address, got %v", invalidMacAddress, err)
} }
config, hostConfig, _ := mustParse(t, validMacAddress) if config, _ := mustParse(t, validMacAddress); config.MacAddress != "92:d0:c6:0a:29:33" { //nolint:staticcheck // pre-existing issue from nektos/act
fmt.Printf("MacAddress: %+v\n", hostConfig) t.Fatalf("Expected the config to have '92:d0:c6:0a:29:33' as MacAddress, got '%v'", config.MacAddress) //nolint:staticcheck // pre-existing issue from nektos/act
assert.Equal(t, "92:d0:c6:0a:29:33", config.MacAddress) //nolint:staticcheck }
} }
func TestRunFlagsParseWithMemory(t *testing.T) { func TestRunFlagsParseWithMemory(t *testing.T) {
@@ -311,7 +313,7 @@ func TestRunFlagsParseWithMemory(t *testing.T) {
err := flags.Parse(args) err := flags.Parse(args)
assert.ErrorContains(t, err, `invalid argument "invalid" for "-m, --memory" flag`) assert.ErrorContains(t, err, `invalid argument "invalid" for "-m, --memory" flag`)
_, hostconfig, _ := mustParse(t, "--memory=1G") _, hostconfig := mustParse(t, "--memory=1G")
assert.Check(t, is.Equal(int64(1073741824), hostconfig.Memory)) assert.Check(t, is.Equal(int64(1073741824), hostconfig.Memory))
} }
@@ -321,10 +323,10 @@ func TestParseWithMemorySwap(t *testing.T) {
err := flags.Parse(args) err := flags.Parse(args)
assert.ErrorContains(t, err, `invalid argument "invalid" for "--memory-swap" flag`) assert.ErrorContains(t, err, `invalid argument "invalid" for "--memory-swap" flag`)
_, hostconfig, _ := mustParse(t, "--memory-swap=1G") _, hostconfig := mustParse(t, "--memory-swap=1G")
assert.Check(t, is.Equal(int64(1073741824), hostconfig.MemorySwap)) assert.Check(t, is.Equal(int64(1073741824), hostconfig.MemorySwap))
_, hostconfig, _ = mustParse(t, "--memory-swap=-1") _, hostconfig = mustParse(t, "--memory-swap=-1")
assert.Check(t, is.Equal(int64(-1), hostconfig.MemorySwap)) assert.Check(t, is.Equal(int64(-1), hostconfig.MemorySwap))
} }
@@ -339,14 +341,14 @@ func TestParseHostname(t *testing.T) {
hostnameWithDomain := "--hostname=hostname.domainname" hostnameWithDomain := "--hostname=hostname.domainname"
hostnameWithDomainTld := "--hostname=hostname.domainname.tld" hostnameWithDomainTld := "--hostname=hostname.domainname.tld"
for hostname, expectedHostname := range validHostnames { for hostname, expectedHostname := range validHostnames {
if config, _, _ := mustParse(t, fmt.Sprintf("--hostname=%s", hostname)); config.Hostname != expectedHostname { if config, _ := mustParse(t, "--hostname="+hostname); config.Hostname != expectedHostname {
t.Fatalf("Expected the config to have 'hostname' as %q, got %q", expectedHostname, config.Hostname) t.Fatalf("Expected the config to have 'hostname' as %q, got %q", expectedHostname, config.Hostname)
} }
} }
if config, _, _ := mustParse(t, hostnameWithDomain); config.Hostname != "hostname.domainname" || config.Domainname != "" { if config, _ := mustParse(t, hostnameWithDomain); config.Hostname != "hostname.domainname" || config.Domainname != "" {
t.Fatalf("Expected the config to have 'hostname' as hostname.domainname, got %q", config.Hostname) t.Fatalf("Expected the config to have 'hostname' as hostname.domainname, got %q", config.Hostname)
} }
if config, _, _ := mustParse(t, hostnameWithDomainTld); config.Hostname != "hostname.domainname.tld" || config.Domainname != "" { if config, _ := mustParse(t, hostnameWithDomainTld); config.Hostname != "hostname.domainname.tld" || config.Domainname != "" {
t.Fatalf("Expected the config to have 'hostname' as hostname.domainname.tld, got %q", config.Hostname) t.Fatalf("Expected the config to have 'hostname' as hostname.domainname.tld, got %q", config.Hostname)
} }
} }
@@ -360,14 +362,14 @@ func TestParseHostnameDomainname(t *testing.T) {
"domainname-63-bytes-long-should-be-valid-and-without-any-errors": "domainname-63-bytes-long-should-be-valid-and-without-any-errors", "domainname-63-bytes-long-should-be-valid-and-without-any-errors": "domainname-63-bytes-long-should-be-valid-and-without-any-errors",
} }
for domainname, expectedDomainname := range validDomainnames { for domainname, expectedDomainname := range validDomainnames {
if config, _, _ := mustParse(t, "--domainname="+domainname); config.Domainname != expectedDomainname { if config, _ := mustParse(t, "--domainname="+domainname); config.Domainname != expectedDomainname {
t.Fatalf("Expected the config to have 'domainname' as %q, got %q", expectedDomainname, config.Domainname) t.Fatalf("Expected the config to have 'domainname' as %q, got %q", expectedDomainname, config.Domainname)
} }
} }
if config, _, _ := mustParse(t, "--hostname=some.prefix --domainname=domainname"); config.Hostname != "some.prefix" || config.Domainname != "domainname" { if config, _ := mustParse(t, "--hostname=some.prefix --domainname=domainname"); config.Hostname != "some.prefix" || config.Domainname != "domainname" {
t.Fatalf("Expected the config to have 'hostname' as 'some.prefix' and 'domainname' as 'domainname', got %q and %q", config.Hostname, config.Domainname) t.Fatalf("Expected the config to have 'hostname' as 'some.prefix' and 'domainname' as 'domainname', got %q and %q", config.Hostname, config.Domainname)
} }
if config, _, _ := mustParse(t, "--hostname=another-prefix --domainname=domainname.tld"); config.Hostname != "another-prefix" || config.Domainname != "domainname.tld" { if config, _ := mustParse(t, "--hostname=another-prefix --domainname=domainname.tld"); config.Hostname != "another-prefix" || config.Domainname != "domainname.tld" {
t.Fatalf("Expected the config to have 'hostname' as 'another-prefix' and 'domainname' as 'domainname.tld', got %q and %q", config.Hostname, config.Domainname) t.Fatalf("Expected the config to have 'hostname' as 'another-prefix' and 'domainname' as 'domainname.tld', got %q and %q", config.Hostname, config.Domainname)
} }
} }
@@ -376,8 +378,6 @@ func TestParseWithExpose(t *testing.T) {
invalids := map[string]string{ invalids := map[string]string{
":": "invalid port format for --expose: :", ":": "invalid port format for --expose: :",
"8080:9090": "invalid port format for --expose: 8080:9090", "8080:9090": "invalid port format for --expose: 8080:9090",
"/tcp": "invalid range format for --expose: /tcp, error: empty string specified for ports",
"/udp": "invalid range format for --expose: /udp, error: empty string specified for ports",
"NaN/tcp": `invalid range format for --expose: NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`, "NaN/tcp": `invalid range format for --expose: NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`,
"NaN-NaN/tcp": `invalid range format for --expose: NaN-NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`, "NaN-NaN/tcp": `invalid range format for --expose: NaN-NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`,
"8080-NaN/tcp": `invalid range format for --expose: 8080-NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`, "8080-NaN/tcp": `invalid range format for --expose: 8080-NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`,
@@ -461,7 +461,6 @@ func TestParseDevice(t *testing.T) {
t.Fatalf("Expected %v, got %v", deviceMapping, hostconfig.Devices) t.Fatalf("Expected %v, got %v", deviceMapping, hostconfig.Devices)
} }
} }
} }
func TestParseNetworkConfig(t *testing.T) { func TestParseNetworkConfig(t *testing.T) {
@@ -633,7 +632,7 @@ func TestParseModes(t *testing.T) {
} }
// uts ko // uts ko
_, _, _, err = parseRun([]string{"--uts=container:", "img", "cmd"}) //nolint:dogsled _, _, _, err = parseRun([]string{"--uts=container:", "img", "cmd"}) //nolint:dogsled // ignoring multiple returns in test helpers
assert.ErrorContains(t, err, "--uts: invalid UTS mode") assert.ErrorContains(t, err, "--uts: invalid UTS mode")
// uts ok // uts ok
@@ -677,7 +676,7 @@ func TestParseRestartPolicy(t *testing.T) {
}, },
} }
for restart, expectedError := range invalids { for restart, expectedError := range invalids {
if _, _, _, err := parseRun([]string{fmt.Sprintf("--restart=%s", restart), "img", "cmd"}); err == nil || err.Error() != expectedError { if _, _, _, err := parseRun([]string{"--restart=" + restart, "img", "cmd"}); err == nil || err.Error() != expectedError {
t.Fatalf("Expected an error with message '%v' for %v, got %v", expectedError, restart, err) t.Fatalf("Expected an error with message '%v' for %v, got %v", expectedError, restart, err)
} }
} }
@@ -693,8 +692,8 @@ func TestParseRestartPolicy(t *testing.T) {
} }
func TestParseRestartPolicyAutoRemove(t *testing.T) { func TestParseRestartPolicyAutoRemove(t *testing.T) {
expected := "conflicting options: --restart and --rm" expected := "Conflicting options: --restart and --rm"
_, _, _, err := parseRun([]string{"--rm", "--restart=always", "img", "cmd"}) //nolint:dogsled _, _, _, err := parseRun([]string{"--rm", "--restart=always", "img", "cmd"}) //nolint:dogsled // ignoring multiple returns in test helpers
if err == nil || err.Error() != expected { if err == nil || err.Error() != expected {
t.Fatalf("Expected error %v, but got none", expected) t.Fatalf("Expected error %v, but got none", expected)
} }
@@ -754,7 +753,7 @@ func TestParseLoggingOpts(t *testing.T) {
} }
} }
func TestParseEnvfileVariables(t *testing.T) { func TestParseEnvfileVariables(t *testing.T) { //nolint:dupl // pre-existing issue from nektos/act
e := "open nonexistent: no such file or directory" e := "open nonexistent: no such file or directory"
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
e = "open nonexistent: The system cannot find the file specified." e = "open nonexistent: The system cannot find the file specified."
@@ -797,7 +796,7 @@ func TestParseEnvfileVariablesWithBOMUnicode(t *testing.T) {
} }
// UTF16 with BOM // UTF16 with BOM
e := "invalid env file" e := "contains invalid utf8 bytes at line"
if _, _, _, err := parseRun([]string{"--env-file=testdata/utf16.env", "img", "cmd"}); err == nil || !strings.Contains(err.Error(), e) { if _, _, _, err := parseRun([]string{"--env-file=testdata/utf16.env", "img", "cmd"}); err == nil || !strings.Contains(err.Error(), e) {
t.Fatalf("Expected an error with message '%s', got %v", e, err) t.Fatalf("Expected an error with message '%s', got %v", e, err)
} }
@@ -807,7 +806,7 @@ func TestParseEnvfileVariablesWithBOMUnicode(t *testing.T) {
} }
} }
func TestParseLabelfileVariables(t *testing.T) { func TestParseLabelfileVariables(t *testing.T) { //nolint:dupl // pre-existing issue from nektos/act
e := "open nonexistent: no such file or directory" e := "open nonexistent: no such file or directory"
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
e = "open nonexistent: The system cannot find the file specified." e = "open nonexistent: The system cannot find the file specified."
@@ -966,7 +965,6 @@ func TestConvertToStandardNotation(t *testing.T) {
for key, ports := range valid { for key, ports := range valid {
convertedPorts, err := convertToStandardNotation(ports) convertedPorts, err := convertToStandardNotation(ports)
if err != nil { if err != nil {
assert.NilError(t, err) assert.NilError(t, err)
} }

View File

@@ -1,3 +1,7 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// Copyright 2020 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows || netbsd)) //go:build !(WITHOUT_DOCKER || !(linux || darwin || windows || netbsd))
package container package container
@@ -6,21 +10,21 @@ import (
"context" "context"
"fmt" "fmt"
cerrdefs "github.com/containerd/errdefs" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/image" "github.com/docker/docker/client"
) )
// ImageExistsLocally returns a boolean indicating if an image with the // ImageExistsLocally returns a boolean indicating if an image with the
// requested name, tag and architecture exists in the local docker image store // requested name, tag and architecture exists in the local docker image store
func ImageExistsLocally(ctx context.Context, imageName string, platform string) (bool, error) { func ImageExistsLocally(ctx context.Context, imageName, platform string) (bool, error) {
cli, err := GetDockerClient(ctx) cli, err := GetDockerClient(ctx)
if err != nil { if err != nil {
return false, err return false, err
} }
defer cli.Close() defer cli.Close()
inspectImage, err := cli.ImageInspect(ctx, imageName) inspectImage, _, err := cli.ImageInspectWithRaw(ctx, imageName)
if cerrdefs.IsNotFound(err) { if client.IsErrNotFound(err) {
return false, nil return false, nil
} else if err != nil { } else if err != nil {
return false, err return false, err
@@ -35,21 +39,21 @@ func ImageExistsLocally(ctx context.Context, imageName string, platform string)
// RemoveImage removes image from local store, the function is used to run different // RemoveImage removes image from local store, the function is used to run different
// container image architectures // container image architectures
func RemoveImage(ctx context.Context, imageName string, force bool, pruneChildren bool) (bool, error) { func RemoveImage(ctx context.Context, imageName string, force, pruneChildren bool) (bool, error) {
cli, err := GetDockerClient(ctx) cli, err := GetDockerClient(ctx)
if err != nil { if err != nil {
return false, err return false, err
} }
defer cli.Close() defer cli.Close()
inspectImage, err := cli.ImageInspect(ctx, imageName) inspectImage, _, err := cli.ImageInspectWithRaw(ctx, imageName)
if cerrdefs.IsNotFound(err) { if client.IsErrNotFound(err) {
return false, nil return false, nil
} else if err != nil { } else if err != nil {
return false, err return false, err
} }
if _, err = cli.ImageRemove(ctx, inspectImage.ID, image.RemoveOptions{ if _, err = cli.ImageRemove(ctx, inspectImage.ID, types.ImageRemoveOptions{
Force: force, Force: force,
PruneChildren: pruneChildren, PruneChildren: pruneChildren,
}); err != nil { }); err != nil {

View File

@@ -1,12 +1,15 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Copyright 2020 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package container package container
import ( import (
"context" "context"
"io" "io"
"os"
"testing" "testing"
"github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types"
"github.com/docker/docker/client" "github.com/docker/docker/client"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -26,59 +29,43 @@ func TestImageExistsLocally(t *testing.T) {
// Test if image exists with specific tag // Test if image exists with specific tag
invalidImageTag, err := ImageExistsLocally(ctx, "library/alpine:this-random-tag-will-never-exist", "linux/amd64") invalidImageTag, err := ImageExistsLocally(ctx, "library/alpine:this-random-tag-will-never-exist", "linux/amd64")
assert.Nil(t, err) assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, false, invalidImageTag) assert.Equal(t, false, invalidImageTag) //nolint:testifylint // pre-existing issue from nektos/act
// Test if image exists with specific architecture (image platform) // Test if image exists with specific architecture (image platform)
invalidImagePlatform, err := ImageExistsLocally(ctx, "alpine:latest", "windows/amd64") invalidImagePlatform, err := ImageExistsLocally(ctx, "alpine:latest", "windows/amd64")
assert.Nil(t, err) assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, false, invalidImagePlatform) assert.Equal(t, false, invalidImagePlatform) //nolint:testifylint // pre-existing issue from nektos/act
// pull an image // pull an image
cli, err := client.NewClientWithOpts(client.FromEnv) cli, err := client.NewClientWithOpts(client.FromEnv)
assert.Nil(t, err) assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
cli.NegotiateAPIVersion(context.Background()) cli.NegotiateAPIVersion(context.Background())
// Chose alpine latest because it's so small // Chose alpine latest because it's so small
// maybe we should build an image instead so that tests aren't reliable on dockerhub // maybe we should build an image instead so that tests aren't reliable on dockerhub
readerDefault, err := cli.ImagePull(ctx, "node:16-buster-slim", image.PullOptions{ readerDefault, err := cli.ImagePull(ctx, "node:16-buster-slim", types.ImagePullOptions{
Platform: "linux/amd64", Platform: "linux/amd64",
}) })
assert.Nil(t, err) assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
defer readerDefault.Close() defer readerDefault.Close()
_, err = io.ReadAll(readerDefault) _, err = io.ReadAll(readerDefault)
assert.Nil(t, err) assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
imageDefaultArchExists, err := ImageExistsLocally(ctx, "node:16-buster-slim", "linux/amd64") imageDefaultArchExists, err := ImageExistsLocally(ctx, "node:16-buster-slim", "linux/amd64")
assert.Nil(t, err) assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, true, imageDefaultArchExists) assert.Equal(t, true, imageDefaultArchExists) //nolint:testifylint // pre-existing issue from nektos/act
}
func TestImageExistsLocallyQemu(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
if _, ok := os.LookupEnv("NO_QEMU"); ok {
t.Skip("skipping test because QEMU is disabled")
}
ctx := context.Background()
// pull an image
cli, err := client.NewClientWithOpts(client.FromEnv)
assert.Nil(t, err)
cli.NegotiateAPIVersion(context.Background())
// Validate if another architecture platform can be pulled // Validate if another architecture platform can be pulled
readerArm64, err := cli.ImagePull(ctx, "node:16-buster-slim", image.PullOptions{ readerArm64, err := cli.ImagePull(ctx, "node:16-buster-slim", types.ImagePullOptions{
Platform: "linux/arm64", Platform: "linux/arm64",
}) })
assert.Nil(t, err) assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
defer readerArm64.Close() defer readerArm64.Close()
_, err = io.ReadAll(readerArm64) _, err = io.ReadAll(readerArm64)
assert.Nil(t, err) assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
imageArm64Exists, err := ImageExistsLocally(ctx, "node:16-buster-slim", "linux/arm64") imageArm64Exists, err := ImageExistsLocally(ctx, "node:16-buster-slim", "linux/arm64")
assert.Nil(t, err) assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, true, imageArm64Exists) assert.Equal(t, true, imageArm64Exists) //nolint:testifylint // pre-existing issue from nektos/act
} }

View File

@@ -1,3 +1,7 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Copyright 2020 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows || netbsd)) //go:build !(WITHOUT_DOCKER || !(linux || darwin || windows || netbsd))
package container package container
@@ -74,7 +78,7 @@ func logDockerResponse(logger logrus.FieldLogger, dockerResponse io.ReadCloser,
return nil return nil
} }
func writeLog(logger logrus.FieldLogger, isError bool, format string, args ...interface{}) { func writeLog(logger logrus.FieldLogger, isError bool, format string, args ...any) {
if isError { if isError {
logger.Errorf(format, args...) logger.Errorf(format, args...)
} else { } else {

View File

@@ -1,3 +1,7 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// Copyright 2023 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows || netbsd)) //go:build !(WITHOUT_DOCKER || !(linux || darwin || windows || netbsd))
package container package container
@@ -5,8 +9,9 @@ package container
import ( import (
"context" "context"
"github.com/actions-oss/act-cli/pkg/common" "gitea.com/gitea/act_runner/act/common"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types"
) )
func NewDockerNetworkCreateExecutor(name string) common.Executor { func NewDockerNetworkCreateExecutor(name string) common.Executor {
@@ -18,11 +23,12 @@ func NewDockerNetworkCreateExecutor(name string) common.Executor {
defer cli.Close() defer cli.Close()
// Only create the network if it doesn't exist // Only create the network if it doesn't exist
networks, err := cli.NetworkList(ctx, network.ListOptions{}) networks, err := cli.NetworkList(ctx, types.NetworkListOptions{})
if err != nil { if err != nil {
return err return err
} }
common.Logger(ctx).Debugf("%v", networks) // For Gitea, reduce log noise
// common.Logger(ctx).Debugf("%v", networks)
for _, network := range networks { for _, network := range networks {
if network.Name == name { if network.Name == name {
common.Logger(ctx).Debugf("Network %v exists", name) common.Logger(ctx).Debugf("Network %v exists", name)
@@ -30,7 +36,7 @@ func NewDockerNetworkCreateExecutor(name string) common.Executor {
} }
} }
_, err = cli.NetworkCreate(ctx, name, network.CreateOptions{ _, err = cli.NetworkCreate(ctx, name, types.NetworkCreate{
Driver: "bridge", Driver: "bridge",
Scope: "local", Scope: "local",
}) })
@@ -50,22 +56,23 @@ func NewDockerNetworkRemoveExecutor(name string) common.Executor {
} }
defer cli.Close() defer cli.Close()
// Make sure that all network of the specified name are removed // Make shure that all network of the specified name are removed
// cli.NetworkRemove refuses to remove a network if there are duplicates // cli.NetworkRemove refuses to remove a network if there are duplicates
networks, err := cli.NetworkList(ctx, network.ListOptions{}) networks, err := cli.NetworkList(ctx, types.NetworkListOptions{})
if err != nil { if err != nil {
return err return err
} }
common.Logger(ctx).Debugf("%v", networks) // For Gitea, reduce log noise
for _, net := range networks { // common.Logger(ctx).Debugf("%v", networks)
if net.Name == name { for _, network := range networks {
result, err := cli.NetworkInspect(ctx, net.ID, network.InspectOptions{}) if network.Name == name {
result, err := cli.NetworkInspect(ctx, network.ID, types.NetworkInspectOptions{})
if err != nil { if err != nil {
return err return err
} }
if len(result.Containers) == 0 { if len(result.Containers) == 0 {
if err = cli.NetworkRemove(ctx, net.ID); err != nil { if err = cli.NetworkRemove(ctx, network.ID); err != nil {
common.Logger(ctx).Debugf("%v", err) common.Logger(ctx).Debugf("%v", err)
} }
} else { } else {

View File

@@ -1,3 +1,7 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// Copyright 2020 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows || netbsd)) //go:build !(WITHOUT_DOCKER || !(linux || darwin || windows || netbsd))
package container package container
@@ -9,11 +13,11 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/distribution/reference" "gitea.com/gitea/act_runner/act/common"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/registry"
"github.com/actions-oss/act-cli/pkg/common" "github.com/distribution/reference"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/registry"
) )
// NewDockerPullExecutor function to create a run executor for the container // NewDockerPullExecutor function to create a run executor for the container
@@ -74,8 +78,8 @@ func NewDockerPullExecutor(input NewDockerPullExecutorInput) common.Executor {
} }
} }
func getImagePullOptions(ctx context.Context, input NewDockerPullExecutorInput) (image.PullOptions, error) { func getImagePullOptions(ctx context.Context, input NewDockerPullExecutorInput) (types.ImagePullOptions, error) {
imagePullOptions := image.PullOptions{ imagePullOptions := types.ImagePullOptions{
Platform: input.Platform, Platform: input.Platform,
} }
logger := common.Logger(ctx) logger := common.Logger(ctx)

View File

@@ -1,3 +1,7 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// Copyright 2020 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package container package container
import ( import (
@@ -5,7 +9,6 @@ import (
"testing" "testing"
"github.com/docker/cli/cli/config" "github.com/docker/cli/cli/config"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
assert "github.com/stretchr/testify/assert" assert "github.com/stretchr/testify/assert"
) )
@@ -40,15 +43,15 @@ func TestGetImagePullOptions(t *testing.T) {
config.SetDir("/non-existent/docker") config.SetDir("/non-existent/docker")
options, err := getImagePullOptions(ctx, NewDockerPullExecutorInput{}) options, err := getImagePullOptions(ctx, NewDockerPullExecutorInput{})
assert.Nil(t, err, "Failed to create ImagePullOptions") 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") assert.Equal(t, "", options.RegistryAuth, "RegistryAuth should be empty if no username or password is set") //nolint:testifylint // pre-existing issue from nektos/act
options, err = getImagePullOptions(ctx, NewDockerPullExecutorInput{ options, err = getImagePullOptions(ctx, NewDockerPullExecutorInput{
Image: "", Image: "",
Username: "username", Username: "username",
Password: "password", Password: "password",
}) })
assert.Nil(t, err, "Failed to create ImagePullOptions") 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") assert.Equal(t, "eyJ1c2VybmFtZSI6InVzZXJuYW1lIiwicGFzc3dvcmQiOiJwYXNzd29yZCJ9", options.RegistryAuth, "Username and Password should be provided")
config.SetDir("testdata/docker-pull-options") config.SetDir("testdata/docker-pull-options")
@@ -56,6 +59,6 @@ func TestGetImagePullOptions(t *testing.T) {
options, err = getImagePullOptions(ctx, NewDockerPullExecutorInput{ options, err = getImagePullOptions(ctx, NewDockerPullExecutorInput{
Image: "nektos/act", Image: "nektos/act",
}) })
assert.Nil(t, err, "Failed to create ImagePullOptions") 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") assert.Equal(t, "eyJ1c2VybmFtZSI6InVzZXJuYW1lIiwicGFzc3dvcmQiOiJwYXNzd29yZFxuIiwic2VydmVyYWRkcmVzcyI6Imh0dHBzOi8vaW5kZXguZG9ja2VyLmlvL3YxLyJ9", options.RegistryAuth, "RegistryAuth should be taken from local docker config")
} }

View File

@@ -1,3 +1,7 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Copyright 2020 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows || netbsd)) //go:build !(WITHOUT_DOCKER || !(linux || darwin || windows || netbsd))
package container package container
@@ -15,28 +19,29 @@ import (
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
"time"
"dario.cat/mergo" "gitea.com/gitea/act_runner/act/common"
"gitea.com/gitea/act_runner/act/filecollector"
"github.com/Masterminds/semver" "github.com/Masterminds/semver"
"github.com/docker/cli/cli/compose/loader"
"github.com/docker/cli/cli/connhelper" "github.com/docker/cli/cli/connhelper"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/system"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/docker/docker/pkg/stdcopy" "github.com/docker/docker/pkg/stdcopy"
"github.com/go-git/go-billy/v5/helper/polyfill" "github.com/go-git/go-billy/v5/helper/polyfill"
"github.com/go-git/go-billy/v5/osfs" "github.com/go-git/go-billy/v5/osfs"
"github.com/go-git/go-git/v5/plumbing/format/gitignore" "github.com/go-git/go-git/v5/plumbing/format/gitignore"
"github.com/gobwas/glob"
"github.com/imdario/mergo"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"github.com/kballard/go-shellquote" "github.com/kballard/go-shellquote"
specs "github.com/opencontainers/image-spec/specs-go/v1" specs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"golang.org/x/term"
"github.com/actions-oss/act-cli/pkg/common"
"github.com/actions-oss/act-cli/pkg/filecollector"
) )
// NewContainer creates a reference to a container // NewContainer creates a reference to a container
@@ -46,6 +51,25 @@ func NewContainer(input *NewContainerInput) ExecutionsEnvironment {
return cr return cr
} }
func (cr *containerReference) ConnectToNetwork(name string) common.Executor {
return common.
NewDebugExecutor("%sdocker network connect %s %s", logPrefix, name, cr.input.Name).
Then(
common.NewPipelineExecutor(
cr.connect(),
cr.connectToNetwork(name, cr.input.NetworkAliases),
).IfNot(common.Dryrun),
)
}
func (cr *containerReference) connectToNetwork(name string, aliases []string) common.Executor {
return func(ctx context.Context) error {
return cr.cli.NetworkConnect(ctx, name, cr.input.Name, &network.EndpointSettings{
Aliases: aliases,
})
}
}
// supportsContainerImagePlatform returns true if the underlying Docker server // supportsContainerImagePlatform returns true if the underlying Docker server
// API version is 1.41 and beyond // API version is 1.41 and beyond
func supportsContainerImagePlatform(ctx context.Context, cli client.APIClient) bool { func supportsContainerImagePlatform(ctx context.Context, cli client.APIClient) bool {
@@ -64,7 +88,7 @@ func supportsContainerImagePlatform(ctx context.Context, cli client.APIClient) b
return constraint.Check(sv) return constraint.Check(sv)
} }
func (cr *containerReference) Create(capAdd []string, capDrop []string) common.Executor { func (cr *containerReference) Create(capAdd, capDrop []string) common.Executor {
return common. return common.
NewInfoExecutor("%sdocker create image=%s platform=%s entrypoint=%+q cmd=%+q network=%+q", logPrefix, cr.input.Image, cr.input.Platform, cr.input.Entrypoint, cr.input.Cmd, cr.input.NetworkMode). NewInfoExecutor("%sdocker create image=%s platform=%s entrypoint=%+q cmd=%+q network=%+q", logPrefix, cr.input.Image, cr.input.Platform, cr.input.Entrypoint, cr.input.Cmd, cr.input.NetworkMode).
Then( Then(
@@ -121,7 +145,7 @@ func (cr *containerReference) Copy(destPath string, files ...*FileEntry) common.
).IfNot(common.Dryrun) ).IfNot(common.Dryrun)
} }
func (cr *containerReference) CopyDir(destPath string, srcPath string, useGitIgnore bool) common.Executor { func (cr *containerReference) CopyDir(destPath, srcPath string, useGitIgnore bool) common.Executor {
return common.NewPipelineExecutor( return common.NewPipelineExecutor(
common.NewInfoExecutor("%sdocker cp src=%s dst=%s", logPrefix, srcPath, destPath), common.NewInfoExecutor("%sdocker cp src=%s dst=%s", logPrefix, srcPath, destPath),
cr.copyDir(destPath, srcPath, useGitIgnore), cr.copyDir(destPath, srcPath, useGitIgnore),
@@ -137,7 +161,7 @@ func (cr *containerReference) CopyDir(destPath string, srcPath string, useGitIgn
func (cr *containerReference) GetContainerArchive(ctx context.Context, srcPath string) (io.ReadCloser, error) { func (cr *containerReference) GetContainerArchive(ctx context.Context, srcPath string) (io.ReadCloser, error) {
if common.Dryrun(ctx) { if common.Dryrun(ctx) {
return nil, fmt.Errorf("dryrun is not supported in GetContainerArchive") return nil, errors.New("DRYRUN is not supported in GetContainerArchive")
} }
a, _, err := cr.cli.CopyFromContainer(ctx, cr.id, srcPath) a, _, err := cr.cli.CopyFromContainer(ctx, cr.id, srcPath)
return a, err return a, err
@@ -156,7 +180,7 @@ func (cr *containerReference) Exec(command []string, env map[string]string, user
common.NewInfoExecutor("%sdocker exec cmd=[%s] user=%s workdir=%s", logPrefix, strings.Join(command, " "), user, workdir), common.NewInfoExecutor("%sdocker exec cmd=[%s] user=%s workdir=%s", logPrefix, strings.Join(command, " "), user, workdir),
cr.connect(), cr.connect(),
cr.find(), cr.find(),
cr.execExt(command, env, user, workdir), cr.exec(command, env, user, workdir),
).IfNot(common.Dryrun) ).IfNot(common.Dryrun)
} }
@@ -169,31 +193,7 @@ func (cr *containerReference) Remove() common.Executor {
).IfNot(common.Dryrun) ).IfNot(common.Dryrun)
} }
func (cr *containerReference) GetHealth(ctx context.Context) Health { func (cr *containerReference) ReplaceLogWriter(stdout, stderr io.Writer) (io.Writer, io.Writer) {
resp, err := cr.cli.ContainerInspect(ctx, cr.id)
logger := common.Logger(ctx)
if err != nil {
logger.Errorf("failed to query container health %s", err)
return HealthUnHealthy
}
if resp.Config == nil || resp.Config.Healthcheck == nil || resp.State == nil || resp.State.Health == nil || len(resp.Config.Healthcheck.Test) == 1 && strings.EqualFold(resp.Config.Healthcheck.Test[0], "NONE") {
logger.Debugf("no container health check defined")
return HealthHealthy
}
logger.Infof("container health of %s (%s) is %s", cr.id, resp.Config.Image, resp.State.Health.Status)
switch resp.State.Health.Status {
case "starting":
return HealthStarting
case "healthy":
return HealthHealthy
case "unhealthy":
return HealthUnHealthy
}
return HealthUnHealthy
}
func (cr *containerReference) ReplaceLogWriter(stdout io.Writer, stderr io.Writer) (io.Writer, io.Writer) {
out := cr.input.Stdout out := cr.input.Stdout
err := cr.input.Stderr err := cr.input.Stderr
@@ -237,7 +237,7 @@ func GetDockerClient(ctx context.Context) (cli client.APIClient, err error) {
return cli, nil return cli, nil
} }
func GetHostInfo(ctx context.Context) (info system.Info, err error) { func GetHostInfo(ctx context.Context) (info types.Info, err error) { //nolint:staticcheck // pre-existing issue from nektos/act
var cli client.APIClient var cli client.APIClient
cli, err = GetDockerClient(ctx) cli, err = GetDockerClient(ctx)
if err != nil { if err != nil {
@@ -290,7 +290,7 @@ func (cr *containerReference) connect() common.Executor {
} }
func (cr *containerReference) Close() common.Executor { func (cr *containerReference) Close() common.Executor {
return func(_ context.Context) error { return func(ctx context.Context) error {
if cr.cli != nil { if cr.cli != nil {
err := cr.cli.Close() err := cr.cli.Close()
cr.cli = nil cr.cli = nil
@@ -307,7 +307,7 @@ func (cr *containerReference) find() common.Executor {
if cr.id != "" { if cr.id != "" {
return nil return nil
} }
containers, err := cr.cli.ContainerList(ctx, container.ListOptions{ containers, err := cr.cli.ContainerList(ctx, types.ContainerListOptions{ //nolint:staticcheck // pre-existing issue from nektos/act
All: true, All: true,
}) })
if err != nil { if err != nil {
@@ -335,7 +335,7 @@ func (cr *containerReference) remove() common.Executor {
} }
logger := common.Logger(ctx) logger := common.Logger(ctx)
err := cr.cli.ContainerRemove(ctx, cr.id, container.RemoveOptions{ err := cr.cli.ContainerRemove(ctx, cr.id, types.ContainerRemoveOptions{ //nolint:staticcheck // pre-existing issue from nektos/act
RemoveVolumes: true, RemoveVolumes: true,
Force: true, Force: true,
}) })
@@ -363,30 +363,50 @@ func (cr *containerReference) mergeContainerConfigs(ctx context.Context, config
optionsArgs, err := shellquote.Split(input.Options) optionsArgs, err := shellquote.Split(input.Options)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("cannot split container options: '%s': '%w'", input.Options, err) return nil, nil, fmt.Errorf("Cannot split container options: '%s': '%w'", input.Options, err)
} }
err = flags.Parse(optionsArgs) err = flags.Parse(optionsArgs)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("cannot parse container options: '%s': '%w'", input.Options, err) return nil, nil, fmt.Errorf("Cannot parse container options: '%s': '%w'", input.Options, err)
} }
// FIXME: If everything is fine after gitea/act v0.260.0, remove the following comment.
// In the old fork version, the code is
// if len(copts.netMode.Value()) == 0 {
// if err = copts.netMode.Set("host"); err != nil {
// return nil, nil, fmt.Errorf("Cannot parse networkmode=host. This is an internal error and should not happen: '%w'", err)
// }
// }
// And it has been commented with:
// If a service container's network is set to `host`, the container will not be able to
// connect to the specified network created for the job container and the service containers.
// So comment out the following code.
// Not the if it's necessary to comment it in the new version,
// since it's cr.input.NetworkMode now.
if len(copts.netMode.Value()) == 0 { if len(copts.netMode.Value()) == 0 {
if err = copts.netMode.Set(cr.input.NetworkMode); err != nil { if err = copts.netMode.Set(cr.input.NetworkMode); err != nil {
return nil, nil, fmt.Errorf("cannot parse networkmode=%s. This is an internal error and should not happen: '%w'", cr.input.NetworkMode, err) return nil, nil, fmt.Errorf("Cannot parse networkmode=%s. This is an internal error and should not happen: '%w'", cr.input.NetworkMode, err)
} }
} }
// If the `privileged` config has been disabled, `copts.privileged` need to be forced to false,
// even if the user specifies `--privileged` in the options string.
if !hostConfig.Privileged {
copts.privileged = false
}
containerConfig, err := parse(flags, copts, runtime.GOOS) containerConfig, err := parse(flags, copts, runtime.GOOS)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("cannot process container options: '%s': '%w'", input.Options, err) return nil, nil, fmt.Errorf("Cannot process container options: '%s': '%w'", input.Options, err)
} }
logger.Debugf("Custom container.Config from options ==> %+v", containerConfig.Config) logger.Debugf("Custom container.Config from options ==> %+v", containerConfig.Config)
err = mergo.Merge(config, containerConfig.Config, mergo.WithOverride) err = mergo.Merge(config, containerConfig.Config, mergo.WithOverride, mergo.WithAppendSlice)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("cannot merge container.Config options: '%s': '%w'", input.Options, err) return nil, nil, fmt.Errorf("Cannot merge container.Config options: '%s': '%w'", input.Options, err)
} }
logger.Debugf("Merged container.Config ==> %+v", config) logger.Debugf("Merged container.Config ==> %+v", config)
@@ -396,24 +416,29 @@ func (cr *containerReference) mergeContainerConfigs(ctx context.Context, config
hostConfig.Mounts = append(hostConfig.Mounts, containerConfig.HostConfig.Mounts...) hostConfig.Mounts = append(hostConfig.Mounts, containerConfig.HostConfig.Mounts...)
binds := hostConfig.Binds binds := hostConfig.Binds
mounts := hostConfig.Mounts mounts := hostConfig.Mounts
networkMode := hostConfig.NetworkMode
err = mergo.Merge(hostConfig, containerConfig.HostConfig, mergo.WithOverride) err = mergo.Merge(hostConfig, containerConfig.HostConfig, mergo.WithOverride)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("cannot merge container.HostConfig options: '%s': '%w'", input.Options, err) return nil, nil, fmt.Errorf("Cannot merge container.HostConfig options: '%s': '%w'", input.Options, err)
} }
hostConfig.Binds = binds hostConfig.Binds = binds
hostConfig.Mounts = mounts hostConfig.Mounts = mounts
if len(copts.netMode.Value()) > 0 {
logger.Warn("--network and --net in the options will be ignored.")
}
hostConfig.NetworkMode = networkMode
logger.Debugf("Merged container.HostConfig ==> %+v", hostConfig) logger.Debugf("Merged container.HostConfig ==> %+v", hostConfig)
return config, hostConfig, nil return config, hostConfig, nil
} }
func (cr *containerReference) create(capAdd []string, capDrop []string) common.Executor { func (cr *containerReference) create(capAdd, capDrop []string) common.Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
if cr.id != "" { if cr.id != "" {
return nil return nil
} }
logger := common.Logger(ctx) logger := common.Logger(ctx)
isTerminal := containerAllocateTerminal isTerminal := term.IsTerminal(int(os.Stdout.Fd()))
input := cr.input input := cr.input
config := &container.Config{ config := &container.Config{
@@ -423,7 +448,8 @@ func (cr *containerReference) create(capAdd []string, capDrop []string) common.E
ExposedPorts: input.ExposedPorts, ExposedPorts: input.ExposedPorts,
Tty: isTerminal, Tty: isTerminal,
} }
logger.Debugf("Common container.Config ==> %+v", config) // For Gitea, reduce log noise
// logger.Debugf("Common container.Config ==> %+v", config)
if len(input.Cmd) != 0 { if len(input.Cmd) != 0 {
config.Cmd = input.Cmd config.Cmd = input.Cmd
@@ -465,16 +491,22 @@ func (cr *containerReference) create(capAdd []string, capDrop []string) common.E
Privileged: input.Privileged, Privileged: input.Privileged,
UsernsMode: container.UsernsMode(input.UsernsMode), UsernsMode: container.UsernsMode(input.UsernsMode),
PortBindings: input.PortBindings, PortBindings: input.PortBindings,
AutoRemove: input.AutoRemove,
} }
logger.Debugf("Common container.HostConfig ==> %+v", hostConfig) // For Gitea, reduce log noise
// logger.Debugf("Common container.HostConfig ==> %+v", hostConfig)
config, hostConfig, err := cr.mergeContainerConfigs(ctx, config, hostConfig) config, hostConfig, err := cr.mergeContainerConfigs(ctx, config, hostConfig)
if err != nil { if err != nil {
return err return err
} }
// For Gitea
config, hostConfig = cr.sanitizeConfig(ctx, config, hostConfig)
var networkingConfig *network.NetworkingConfig var networkingConfig *network.NetworkingConfig
logger.Debugf("input.NetworkAliases ==> %v", input.NetworkAliases) // For Gitea, reduce log noise
// logger.Debugf("input.NetworkAliases ==> %v", input.NetworkAliases)
n := hostConfig.NetworkMode n := hostConfig.NetworkMode
// IsUserDefined and IsHost are broken on windows // IsUserDefined and IsHost are broken on windows
if n.IsUserDefined() && n != "host" && len(input.NetworkAliases) > 0 { if n.IsUserDefined() && n != "host" && len(input.NetworkAliases) > 0 {
@@ -506,7 +538,7 @@ func (cr *containerReference) extractFromImageEnv(env *map[string]string) common
return func(ctx context.Context) error { return func(ctx context.Context) error {
logger := common.Logger(ctx) logger := common.Logger(ctx)
inspect, err := cr.cli.ImageInspect(ctx, cr.input.Image) inspect, _, err := cr.cli.ImageInspectWithRaw(ctx, cr.input.Image)
if err != nil { if err != nil {
logger.Error(err) logger.Error(err)
return fmt.Errorf("inspect image: %w", err) return fmt.Errorf("inspect image: %w", err)
@@ -539,7 +571,8 @@ func (cr *containerReference) extractFromImageEnv(env *map[string]string) common
} }
} }
func (cr *containerReference) exec(ctx context.Context, cmd []string, env map[string]string, user, workdir string) error { func (cr *containerReference) exec(cmd []string, env map[string]string, user, workdir string) common.Executor {
return func(ctx context.Context) error {
logger := common.Logger(ctx) logger := common.Logger(ctx)
// Fix slashes when running on Windows // Fix slashes when running on Windows
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
@@ -551,7 +584,7 @@ func (cr *containerReference) exec(ctx context.Context, cmd []string, env map[st
} }
logger.Debugf("Exec command '%s'", cmd) logger.Debugf("Exec command '%s'", cmd)
isTerminal := containerAllocateTerminal isTerminal := term.IsTerminal(int(os.Stdout.Fd()))
envList := make([]string, 0) envList := make([]string, 0)
for k, v := range env { for k, v := range env {
envList = append(envList, fmt.Sprintf("%s=%s", k, v)) envList = append(envList, fmt.Sprintf("%s=%s", k, v))
@@ -569,7 +602,7 @@ func (cr *containerReference) exec(ctx context.Context, cmd []string, env map[st
} }
logger.Debugf("Working directory '%s'", wd) logger.Debugf("Working directory '%s'", wd)
idResp, err := cr.cli.ContainerExecCreate(ctx, cr.id, container.ExecOptions{ idResp, err := cr.cli.ContainerExecCreate(ctx, cr.id, types.ExecConfig{
User: user, User: user,
Cmd: cmd, Cmd: cmd,
WorkingDir: wd, WorkingDir: wd,
@@ -582,7 +615,7 @@ func (cr *containerReference) exec(ctx context.Context, cmd []string, env map[st
return fmt.Errorf("failed to create exec: %w", err) return fmt.Errorf("failed to create exec: %w", err)
} }
resp, err := cr.cli.ContainerExecAttach(ctx, idResp.ID, container.ExecStartOptions{ resp, err := cr.cli.ContainerExecAttach(ctx, idResp.ID, types.ExecStartCheck{
Tty: isTerminal, Tty: isTerminal,
}) })
if err != nil { if err != nil {
@@ -590,7 +623,7 @@ func (cr *containerReference) exec(ctx context.Context, cmd []string, env map[st
} }
defer resp.Close() defer resp.Close()
err = cr.waitForCommand(ctx, isTerminal, resp) err = cr.waitForCommand(ctx, isTerminal, resp, idResp, user, workdir)
if err != nil { if err != nil {
return err return err
} }
@@ -609,38 +642,11 @@ func (cr *containerReference) exec(ctx context.Context, cmd []string, env map[st
return fmt.Errorf("exitcode '%d': failure", inspectResp.ExitCode) return fmt.Errorf("exitcode '%d': failure", inspectResp.ExitCode)
} }
} }
//nolint:contextcheck
func (cr *containerReference) execExt(cmd []string, env map[string]string, user, workdir string) common.Executor {
return func(ctx context.Context) error {
logger := common.Logger(ctx)
done := make(chan error)
go func() {
defer func() {
done <- errors.New("invalid Operation")
}()
done <- cr.exec(ctx, cmd, env, user, workdir)
}()
select {
case <-ctx.Done():
timed, cancelTimed := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancelTimed()
err := cr.cli.ContainerKill(timed, cr.id, "kill")
if err != nil {
logger.Error(err)
}
_ = cr.start()(timed)
logger.Info("This step was cancelled")
return fmt.Errorf("this step was cancelled: %w", ctx.Err())
case ret := <-done:
return ret
}
}
} }
func (cr *containerReference) tryReadID(opt string, cbk func(id int)) common.Executor { func (cr *containerReference) tryReadID(opt string, cbk func(id int)) common.Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
idResp, err := cr.cli.ContainerExecCreate(ctx, cr.id, container.ExecOptions{ idResp, err := cr.cli.ContainerExecCreate(ctx, cr.id, types.ExecConfig{
Cmd: []string{"id", opt}, Cmd: []string{"id", opt},
AttachStdout: true, AttachStdout: true,
AttachStderr: true, AttachStderr: true,
@@ -649,7 +655,7 @@ func (cr *containerReference) tryReadID(opt string, cbk func(id int)) common.Exe
return nil return nil
} }
resp, err := cr.cli.ContainerExecAttach(ctx, idResp.ID, container.ExecStartOptions{}) resp, err := cr.cli.ContainerExecAttach(ctx, idResp.ID, types.ExecStartCheck{})
if err != nil { if err != nil {
return nil return nil
} }
@@ -679,7 +685,7 @@ func (cr *containerReference) tryReadGID() common.Executor {
return cr.tryReadID("-g", func(id int) { cr.GID = id }) return cr.tryReadID("-g", func(id int) { cr.GID = id })
} }
func (cr *containerReference) waitForCommand(ctx context.Context, isTerminal bool, resp types.HijackedResponse) error { func (cr *containerReference) waitForCommand(ctx context.Context, isTerminal bool, resp types.HijackedResponse, _ types.IDResponse, _, _ string) error {
logger := common.Logger(ctx) logger := common.Logger(ctx)
cmdResponse := make(chan error) cmdResponse := make(chan error)
@@ -725,9 +731,6 @@ func (cr *containerReference) waitForCommand(ctx context.Context, isTerminal boo
} }
func (cr *containerReference) CopyTarStream(ctx context.Context, destPath string, tarStream io.Reader) error { func (cr *containerReference) CopyTarStream(ctx context.Context, destPath string, tarStream io.Reader) error {
if common.Dryrun(ctx) {
return nil
}
// Mkdir // Mkdir
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
tw := tar.NewWriter(buf) tw := tar.NewWriter(buf)
@@ -737,12 +740,12 @@ func (cr *containerReference) CopyTarStream(ctx context.Context, destPath string
Typeflag: tar.TypeDir, Typeflag: tar.TypeDir,
}) })
tw.Close() tw.Close()
err := cr.cli.CopyToContainer(ctx, cr.id, "/", buf, container.CopyToContainerOptions{}) err := cr.cli.CopyToContainer(ctx, cr.id, "/", buf, types.CopyToContainerOptions{})
if err != nil { if err != nil {
return fmt.Errorf("failed to mkdir to copy content to container: %w", err) return fmt.Errorf("failed to mkdir to copy content to container: %w", err)
} }
// Copy Content // Copy Content
err = cr.cli.CopyToContainer(ctx, cr.id, destPath, tarStream, container.CopyToContainerOptions{}) err = cr.cli.CopyToContainer(ctx, cr.id, destPath, tarStream, types.CopyToContainerOptions{})
if err != nil { if err != nil {
return fmt.Errorf("failed to copy content to container: %w", err) return fmt.Errorf("failed to copy content to container: %w", err)
} }
@@ -753,7 +756,7 @@ func (cr *containerReference) CopyTarStream(ctx context.Context, destPath string
return nil return nil
} }
func (cr *containerReference) copyDir(dstPath string, srcPath string, useGitIgnore bool) common.Executor { func (cr *containerReference) copyDir(dstPath, srcPath string, useGitIgnore bool) common.Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
logger := common.Logger(ctx) logger := common.Logger(ctx)
tarFile, err := os.CreateTemp("", "act") tarFile, err := os.CreateTemp("", "act")
@@ -816,7 +819,7 @@ func (cr *containerReference) copyDir(dstPath string, srcPath string, useGitIgno
if err != nil { if err != nil {
return fmt.Errorf("failed to seek tar archive: %w", err) return fmt.Errorf("failed to seek tar archive: %w", err)
} }
err = cr.cli.CopyToContainer(ctx, cr.id, "/", tarFile, container.CopyToContainerOptions{}) err = cr.cli.CopyToContainer(ctx, cr.id, "/", tarFile, types.CopyToContainerOptions{})
if err != nil { if err != nil {
return fmt.Errorf("failed to copy content to container: %w", err) return fmt.Errorf("failed to copy content to container: %w", err)
} }
@@ -833,7 +836,7 @@ func (cr *containerReference) copyContent(dstPath string, files ...*FileEntry) c
logger.Debugf("Writing entry to tarball %s len:%d", file.Name, len(file.Body)) logger.Debugf("Writing entry to tarball %s len:%d", file.Name, len(file.Body))
hdr := &tar.Header{ hdr := &tar.Header{
Name: file.Name, Name: file.Name,
Mode: int64(file.Mode), Mode: file.Mode,
Size: int64(len(file.Body)), Size: int64(len(file.Body)),
Uid: cr.UID, Uid: cr.UID,
Gid: cr.GID, Gid: cr.GID,
@@ -850,7 +853,7 @@ func (cr *containerReference) copyContent(dstPath string, files ...*FileEntry) c
} }
logger.Debugf("Extracting content to '%s'", dstPath) logger.Debugf("Extracting content to '%s'", dstPath)
err := cr.cli.CopyToContainer(ctx, cr.id, dstPath, &buf, container.CopyToContainerOptions{}) err := cr.cli.CopyToContainer(ctx, cr.id, dstPath, &buf, types.CopyToContainerOptions{})
if err != nil { if err != nil {
return fmt.Errorf("failed to copy content to container: %w", err) return fmt.Errorf("failed to copy content to container: %w", err)
} }
@@ -860,7 +863,7 @@ func (cr *containerReference) copyContent(dstPath string, files ...*FileEntry) c
func (cr *containerReference) attach() common.Executor { func (cr *containerReference) attach() common.Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
out, err := cr.cli.ContainerAttach(ctx, cr.id, container.AttachOptions{ out, err := cr.cli.ContainerAttach(ctx, cr.id, types.ContainerAttachOptions{ //nolint:staticcheck // pre-existing issue from nektos/act
Stream: true, Stream: true,
Stdout: true, Stdout: true,
Stderr: true, Stderr: true,
@@ -868,7 +871,7 @@ func (cr *containerReference) attach() common.Executor {
if err != nil { if err != nil {
return fmt.Errorf("failed to attach to container: %w", err) return fmt.Errorf("failed to attach to container: %w", err)
} }
isTerminal := containerAllocateTerminal isTerminal := term.IsTerminal(int(os.Stdout.Fd()))
var outWriter io.Writer var outWriter io.Writer
outWriter = cr.input.Stdout outWriter = cr.input.Stdout
@@ -898,7 +901,7 @@ func (cr *containerReference) start() common.Executor {
logger := common.Logger(ctx) logger := common.Logger(ctx)
logger.Debugf("Starting container: %v", cr.id) logger.Debugf("Starting container: %v", cr.id)
if err := cr.cli.ContainerStart(ctx, cr.id, container.StartOptions{}); err != nil { if err := cr.cli.ContainerStart(ctx, cr.id, types.ContainerStartOptions{}); err != nil { //nolint:staticcheck // pre-existing issue from nektos/act
return fmt.Errorf("failed to start container: %w", err) return fmt.Errorf("failed to start container: %w", err)
} }
@@ -930,3 +933,63 @@ func (cr *containerReference) wait() common.Executor {
return fmt.Errorf("exit with `FAILURE`: %v", statusCode) return fmt.Errorf("exit with `FAILURE`: %v", statusCode)
} }
} }
// For Gitea
// sanitizeConfig remove the invalid configurations from `config` and `hostConfig`
func (cr *containerReference) sanitizeConfig(ctx context.Context, config *container.Config, hostConfig *container.HostConfig) (*container.Config, *container.HostConfig) {
logger := common.Logger(ctx)
if len(cr.input.ValidVolumes) > 0 {
globs := make([]glob.Glob, 0, len(cr.input.ValidVolumes))
for _, v := range cr.input.ValidVolumes {
if g, err := glob.Compile(v); err != nil {
logger.Errorf("create glob from %s error: %v", v, err)
} else {
globs = append(globs, g)
}
}
isValid := func(v string) bool {
for _, g := range globs {
if g.Match(v) {
return true
}
}
return false
}
// sanitize binds
sanitizedBinds := make([]string, 0, len(hostConfig.Binds))
for _, bind := range hostConfig.Binds {
parsed, err := loader.ParseVolume(bind)
if err != nil {
logger.Warnf("parse volume [%s] error: %v", bind, err)
continue
}
if parsed.Source == "" {
// anonymous volume
sanitizedBinds = append(sanitizedBinds, bind)
continue
}
if isValid(parsed.Source) {
sanitizedBinds = append(sanitizedBinds, bind)
} else {
logger.Warnf("[%s] is not a valid volume, will be ignored", parsed.Source)
}
}
hostConfig.Binds = sanitizedBinds
// sanitize mounts
sanitizedMounts := make([]mount.Mount, 0, len(hostConfig.Mounts))
for _, mt := range hostConfig.Mounts {
if isValid(mt.Source) {
sanitizedMounts = append(sanitizedMounts, mt)
} else {
logger.Warnf("[%s] is not a valid volume, will be ignored", mt.Source)
}
}
hostConfig.Mounts = sanitizedMounts
} else {
hostConfig.Binds = []string{}
hostConfig.Mounts = []mount.Mount{}
}
return config, hostConfig
}

View File

@@ -1,19 +1,26 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Copyright 2020 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package container package container
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"context" "context"
"fmt" "errors"
"io" "io"
"net" "net"
"strings" "strings"
"testing" "testing"
"time" "time"
"gitea.com/gitea/act_runner/act/common"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/sirupsen/logrus/hooks/test"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
) )
@@ -21,7 +28,7 @@ import (
func TestDocker(t *testing.T) { func TestDocker(t *testing.T) {
ctx := context.Background() ctx := context.Background()
client, err := GetDockerClient(ctx) client, err := GetDockerClient(ctx)
assert.NoError(t, err) assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
defer client.Close() defer client.Close()
dockerBuild := NewDockerBuildExecutor(NewDockerBuildExecutorInput{ dockerBuild := NewDockerBuildExecutor(NewDockerBuildExecutorInput{
@@ -30,7 +37,7 @@ func TestDocker(t *testing.T) {
}) })
err = dockerBuild(ctx) err = dockerBuild(ctx)
assert.NoError(t, err) assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
cr := &containerReference{ cr := &containerReference{
cli: client, cli: client,
@@ -47,7 +54,7 @@ func TestDocker(t *testing.T) {
envExecutor := cr.extractFromImageEnv(&env) envExecutor := cr.extractFromImageEnv(&env)
err = envExecutor(ctx) err = envExecutor(ctx)
assert.NoError(t, err) assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, map[string]string{ assert.Equal(t, map[string]string{
"PATH": "/usr/local/bin:/usr/bin:/usr/sbin:/bin:/sbin:/this/path/does/not/exists/anywhere:/this/either", "PATH": "/usr/local/bin:/usr/bin:/usr/sbin:/bin:/sbin:/this/path/does/not/exists/anywhere:/this/either",
"RANDOM_VAR": "WITH_VALUE", "RANDOM_VAR": "WITH_VALUE",
@@ -63,42 +70,31 @@ type mockDockerClient struct {
mock.Mock mock.Mock
} }
func (m *mockDockerClient) ContainerExecCreate(ctx context.Context, id string, opts container.ExecOptions) (container.ExecCreateResponse, error) { func (m *mockDockerClient) ContainerExecCreate(ctx context.Context, id string, opts types.ExecConfig) (types.IDResponse, error) {
args := m.Called(ctx, id, opts) args := m.Called(ctx, id, opts)
return args.Get(0).(container.ExecCreateResponse), args.Error(1) return args.Get(0).(types.IDResponse), args.Error(1)
} }
func (m *mockDockerClient) ContainerExecAttach(ctx context.Context, id string, opts container.ExecStartOptions) (types.HijackedResponse, error) { func (m *mockDockerClient) ContainerExecAttach(ctx context.Context, id string, opts types.ExecStartCheck) (types.HijackedResponse, error) {
args := m.Called(ctx, id, opts) args := m.Called(ctx, id, opts)
return args.Get(0).(types.HijackedResponse), args.Error(1) return args.Get(0).(types.HijackedResponse), args.Error(1)
} }
func (m *mockDockerClient) ContainerExecInspect(ctx context.Context, execID string) (container.ExecInspect, error) { func (m *mockDockerClient) ContainerExecInspect(ctx context.Context, execID string) (types.ContainerExecInspect, error) {
args := m.Called(ctx, execID) args := m.Called(ctx, execID)
return args.Get(0).(container.ExecInspect), args.Error(1) return args.Get(0).(types.ContainerExecInspect), args.Error(1)
} }
func (m *mockDockerClient) CopyToContainer(ctx context.Context, id string, path string, content io.Reader, options container.CopyToContainerOptions) error { func (m *mockDockerClient) CopyToContainer(ctx context.Context, id, path string, content io.Reader, options types.CopyToContainerOptions) error {
args := m.Called(ctx, id, path, content, options) args := m.Called(ctx, id, path, content, options)
return args.Error(0) return args.Error(0)
} }
func (m *mockDockerClient) ContainerKill(ctx context.Context, containerID string, signal string) error {
args := m.Called(ctx, containerID, signal)
return args.Error(0)
}
func (m *mockDockerClient) ContainerStart(ctx context.Context, containerID string, options container.StartOptions) error {
args := m.Called(ctx, containerID, options)
return args.Error(0)
}
type endlessReader struct { type endlessReader struct {
io.Reader io.Reader
} }
func (r endlessReader) Read(_ []byte) (n int, err error) { func (r endlessReader) Read(_ []byte) (n int, err error) {
time.Sleep(100 * time.Millisecond)
return 1, nil return 1, nil
} }
@@ -123,18 +119,11 @@ func TestDockerExecAbort(t *testing.T) {
conn.On("Write", mock.AnythingOfType("[]uint8")).Return(1, nil) conn.On("Write", mock.AnythingOfType("[]uint8")).Return(1, nil)
client := &mockDockerClient{} client := &mockDockerClient{}
client.On("ContainerExecCreate", ctx, "123", mock.AnythingOfType("container.ExecOptions")).Return(container.ExecCreateResponse{ID: "id"}, nil) client.On("ContainerExecCreate", ctx, "123", mock.AnythingOfType("types.ExecConfig")).Return(types.IDResponse{ID: "id"}, nil)
attached := make(chan struct{}) client.On("ContainerExecAttach", ctx, "id", mock.AnythingOfType("types.ExecStartCheck")).Return(types.HijackedResponse{
client.On("ContainerExecAttach", ctx, "id", mock.AnythingOfType("container.ExecStartOptions")).Run(func(_ mock.Arguments) {
close(attached)
}).Return(types.HijackedResponse{
Conn: conn, Conn: conn,
Reader: bufio.NewReader(endlessReader{}), Reader: bufio.NewReader(endlessReader{}),
}, nil) }, nil)
client.On("ContainerKill", mock.Anything, "123", "kill").Run(func(_ mock.Arguments) {
<-attached
}).Return(nil)
client.On("ContainerStart", mock.Anything, "123", mock.AnythingOfType("container.StartOptions")).Return(nil)
cr := &containerReference{ cr := &containerReference{
id: "123", id: "123",
@@ -147,13 +136,15 @@ func TestDockerExecAbort(t *testing.T) {
channel := make(chan error) channel := make(chan error)
go func() { go func() {
channel <- cr.execExt([]string{""}, map[string]string{}, "user", "workdir")(ctx) channel <- cr.exec([]string{""}, map[string]string{}, "user", "workdir")(ctx)
}() }()
time.Sleep(500 * time.Millisecond)
cancel() cancel()
err := <-channel err := <-channel
assert.ErrorIs(t, err, context.Canceled) assert.ErrorIs(t, err, context.Canceled) //nolint:testifylint // pre-existing issue from nektos/act
conn.AssertExpectations(t) conn.AssertExpectations(t)
client.AssertExpectations(t) client.AssertExpectations(t)
@@ -165,12 +156,12 @@ func TestDockerExecFailure(t *testing.T) {
conn := &mockConn{} conn := &mockConn{}
client := &mockDockerClient{} client := &mockDockerClient{}
client.On("ContainerExecCreate", ctx, "123", mock.AnythingOfType("container.ExecOptions")).Return(container.ExecCreateResponse{ID: "id"}, nil) client.On("ContainerExecCreate", ctx, "123", mock.AnythingOfType("types.ExecConfig")).Return(types.IDResponse{ID: "id"}, nil)
client.On("ContainerExecAttach", ctx, "id", mock.AnythingOfType("container.ExecStartOptions")).Return(types.HijackedResponse{ client.On("ContainerExecAttach", ctx, "id", mock.AnythingOfType("types.ExecStartCheck")).Return(types.HijackedResponse{
Conn: conn, Conn: conn,
Reader: bufio.NewReader(strings.NewReader("output")), Reader: bufio.NewReader(strings.NewReader("output")),
}, nil) }, nil)
client.On("ContainerExecInspect", ctx, "id").Return(container.ExecInspect{ client.On("ContainerExecInspect", ctx, "id").Return(types.ContainerExecInspect{
ExitCode: 1, ExitCode: 1,
}, nil) }, nil)
@@ -182,8 +173,8 @@ func TestDockerExecFailure(t *testing.T) {
}, },
} }
err := cr.execExt([]string{""}, map[string]string{}, "user", "workdir")(ctx) err := cr.exec([]string{""}, map[string]string{}, "user", "workdir")(ctx)
assert.Error(t, err, "exit with `FAILURE`: 1") assert.Error(t, err, "exit with `FAILURE`: 1") //nolint:testifylint // pre-existing issue from nektos/act
conn.AssertExpectations(t) conn.AssertExpectations(t)
client.AssertExpectations(t) client.AssertExpectations(t)
@@ -195,8 +186,8 @@ func TestDockerCopyTarStream(t *testing.T) {
conn := &mockConn{} conn := &mockConn{}
client := &mockDockerClient{} client := &mockDockerClient{}
client.On("CopyToContainer", ctx, "123", "/", mock.Anything, mock.AnythingOfType("container.CopyToContainerOptions")).Return(nil) client.On("CopyToContainer", ctx, "123", "/", mock.Anything, mock.AnythingOfType("types.CopyToContainerOptions")).Return(nil)
client.On("CopyToContainer", ctx, "123", "/var/run/act", mock.Anything, mock.AnythingOfType("container.CopyToContainerOptions")).Return(nil) client.On("CopyToContainer", ctx, "123", "/var/run/act", mock.Anything, mock.AnythingOfType("types.CopyToContainerOptions")).Return(nil)
cr := &containerReference{ cr := &containerReference{
id: "123", id: "123",
cli: client, cli: client,
@@ -216,11 +207,11 @@ func TestDockerCopyTarStreamErrorInCopyFiles(t *testing.T) {
conn := &mockConn{} conn := &mockConn{}
merr := fmt.Errorf("failure") merr := errors.New("Failure")
client := &mockDockerClient{} client := &mockDockerClient{}
client.On("CopyToContainer", ctx, "123", "/", mock.Anything, mock.AnythingOfType("container.CopyToContainerOptions")).Return(merr) client.On("CopyToContainer", ctx, "123", "/", mock.Anything, mock.AnythingOfType("types.CopyToContainerOptions")).Return(merr)
client.On("CopyToContainer", ctx, "123", "/", mock.Anything, mock.AnythingOfType("container.CopyToContainerOptions")).Return(merr) client.On("CopyToContainer", ctx, "123", "/", mock.Anything, mock.AnythingOfType("types.CopyToContainerOptions")).Return(merr)
cr := &containerReference{ cr := &containerReference{
id: "123", id: "123",
cli: client, cli: client,
@@ -230,7 +221,7 @@ func TestDockerCopyTarStreamErrorInCopyFiles(t *testing.T) {
} }
err := cr.CopyTarStream(ctx, "/var/run/act", &bytes.Buffer{}) err := cr.CopyTarStream(ctx, "/var/run/act", &bytes.Buffer{})
assert.ErrorIs(t, err, merr) assert.ErrorIs(t, err, merr) //nolint:testifylint // pre-existing issue from nektos/act
conn.AssertExpectations(t) conn.AssertExpectations(t)
client.AssertExpectations(t) client.AssertExpectations(t)
@@ -241,11 +232,11 @@ func TestDockerCopyTarStreamErrorInMkdir(t *testing.T) {
conn := &mockConn{} conn := &mockConn{}
merr := fmt.Errorf("failure") merr := errors.New("Failure")
client := &mockDockerClient{} client := &mockDockerClient{}
client.On("CopyToContainer", ctx, "123", "/", mock.Anything, mock.AnythingOfType("container.CopyToContainerOptions")).Return(nil) client.On("CopyToContainer", ctx, "123", "/", mock.Anything, mock.AnythingOfType("types.CopyToContainerOptions")).Return(nil)
client.On("CopyToContainer", ctx, "123", "/var/run/act", mock.Anything, mock.AnythingOfType("container.CopyToContainerOptions")).Return(merr) client.On("CopyToContainer", ctx, "123", "/var/run/act", mock.Anything, mock.AnythingOfType("types.CopyToContainerOptions")).Return(merr)
cr := &containerReference{ cr := &containerReference{
id: "123", id: "123",
cli: client, cli: client,
@@ -255,7 +246,7 @@ func TestDockerCopyTarStreamErrorInMkdir(t *testing.T) {
} }
err := cr.CopyTarStream(ctx, "/var/run/act", &bytes.Buffer{}) err := cr.CopyTarStream(ctx, "/var/run/act", &bytes.Buffer{})
assert.ErrorIs(t, err, merr) assert.ErrorIs(t, err, merr) //nolint:testifylint // pre-existing issue from nektos/act
conn.AssertExpectations(t) conn.AssertExpectations(t)
client.AssertExpectations(t) client.AssertExpectations(t)
@@ -263,3 +254,76 @@ func TestDockerCopyTarStreamErrorInMkdir(t *testing.T) {
// Type assert containerReference implements ExecutionsEnvironment // Type assert containerReference implements ExecutionsEnvironment
var _ ExecutionsEnvironment = &containerReference{} var _ ExecutionsEnvironment = &containerReference{}
func TestCheckVolumes(t *testing.T) {
testCases := []struct {
desc string
validVolumes []string
binds []string
expectedBinds []string
}{
{
desc: "match all volumes",
validVolumes: []string{"**"},
binds: []string{
"shared_volume:/shared_volume",
"/home/test/data:/test_data",
"/etc/conf.d/base.json:/config/base.json",
"sql_data:/sql_data",
"/secrets/keys:/keys",
},
expectedBinds: []string{
"shared_volume:/shared_volume",
"/home/test/data:/test_data",
"/etc/conf.d/base.json:/config/base.json",
"sql_data:/sql_data",
"/secrets/keys:/keys",
},
},
{
desc: "no volumes can be matched",
validVolumes: []string{},
binds: []string{
"shared_volume:/shared_volume",
"/home/test/data:/test_data",
"/etc/conf.d/base.json:/config/base.json",
"sql_data:/sql_data",
"/secrets/keys:/keys",
},
expectedBinds: []string{},
},
{
desc: "only allowed volumes can be matched",
validVolumes: []string{
"shared_volume",
"/home/test/data",
"/etc/conf.d/*.json",
},
binds: []string{
"shared_volume:/shared_volume",
"/home/test/data:/test_data",
"/etc/conf.d/base.json:/config/base.json",
"sql_data:/sql_data",
"/secrets/keys:/keys",
},
expectedBinds: []string{
"shared_volume:/shared_volume",
"/home/test/data:/test_data",
"/etc/conf.d/base.json:/config/base.json",
},
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
logger, _ := test.NewNullLogger()
ctx := common.WithLogger(context.Background(), logger)
cr := &containerReference{
input: &NewContainerInput{
ValidVolumes: tc.validVolumes,
},
}
_, hostConf := cr.sanitizeConfig(ctx, &container.Config{}, &container.HostConfig{Binds: tc.binds})
assert.Equal(t, tc.expectedBinds, hostConf.Binds)
})
}
}

View File

@@ -1,3 +1,7 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// Copyright 2024 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package container package container
import ( import (
@@ -43,8 +47,8 @@ func socketLocation() (string, bool) {
// indicating that the `daemonPath` is a Docker host URI. If it doesn't, or if the "://" delimiter // indicating that the `daemonPath` is a Docker host URI. If it doesn't, or if the "://" delimiter
// is not found in the `daemonPath`, the function returns false. // is not found in the `daemonPath`, the function returns false.
func isDockerHostURI(daemonPath string) bool { func isDockerHostURI(daemonPath string) bool {
if protoIndex := strings.Index(daemonPath, "://"); protoIndex != -1 { if before, _, ok := strings.Cut(daemonPath, "://"); ok {
scheme := daemonPath[:protoIndex] scheme := before
if strings.IndexFunc(scheme, func(r rune) bool { if strings.IndexFunc(scheme, func(r rune) bool {
return (r < 'a' || r > 'z') && (r < 'A' || r > 'Z') return (r < 'a' || r > 'z') && (r < 'A' || r > 'Z')
}) == -1 { }) == -1 {
@@ -90,7 +94,7 @@ func GetSocketAndHost(containerSocket string) (SocketAndHost, error) {
if !hasDockerHost && socketHost.Socket != "" && !isDockerHostURI(socketHost.Socket) { if !hasDockerHost && socketHost.Socket != "" && !isDockerHostURI(socketHost.Socket) {
// Cases: 1B, 2B // Cases: 1B, 2B
// Should we early-exit here, since there is no host nor socket to talk to? // Should we early-exit here, since there is no host nor socket to talk to?
return SocketAndHost{}, fmt.Errorf("docker host aka DOCKER_HOST was not set, couldn't be found in the usual locations, and the container daemon socket ('%s') is invalid", socketHost.Socket) return SocketAndHost{}, fmt.Errorf("DOCKER_HOST was not set, couldn't be found in the usual locations, and the container daemon socket ('%s') is invalid", socketHost.Socket)
} }
// Default to DOCKER_HOST if set // Default to DOCKER_HOST if set

View File

@@ -1,3 +1,7 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// Copyright 2024 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package container package container
import ( import (
@@ -19,26 +23,26 @@ func TestGetSocketAndHostWithSocket(t *testing.T) {
CommonSocketLocations = originalCommonSocketLocations CommonSocketLocations = originalCommonSocketLocations
dockerHost := "unix:///my/docker/host.sock" dockerHost := "unix:///my/docker/host.sock"
socketURI := "/path/to/my.socket" socketURI := "/path/to/my.socket"
os.Setenv("DOCKER_HOST", dockerHost) t.Setenv("DOCKER_HOST", dockerHost)
// Act // Act
ret, err := GetSocketAndHost(socketURI) ret, err := GetSocketAndHost(socketURI)
// Assert // Assert
assert.Nil(t, err) assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, SocketAndHost{socketURI, dockerHost}, ret) assert.Equal(t, SocketAndHost{socketURI, dockerHost}, ret)
} }
func TestGetSocketAndHostNoSocket(t *testing.T) { func TestGetSocketAndHostNoSocket(t *testing.T) {
// Arrange // Arrange
dockerHost := "unix:///my/docker/host.sock" dockerHost := "unix:///my/docker/host.sock"
os.Setenv("DOCKER_HOST", dockerHost) t.Setenv("DOCKER_HOST", dockerHost)
// Act // Act
ret, err := GetSocketAndHost("") ret, err := GetSocketAndHost("")
// Assert // Assert
assert.Nil(t, err) assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, SocketAndHost{dockerHost, dockerHost}, ret) assert.Equal(t, SocketAndHost{dockerHost, dockerHost}, ret)
} }
@@ -53,8 +57,8 @@ func TestGetSocketAndHostOnlySocket(t *testing.T) {
ret, err := GetSocketAndHost(socketURI) ret, err := GetSocketAndHost(socketURI)
// Assert // Assert
assert.NoError(t, err, "Expected no error from GetSocketAndHost") 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") 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, socketURI, ret.Socket, "Expected socket to match common location")
assert.Equal(t, defaultSocket, ret.Host, "Expected ret.Host to match default socket location") assert.Equal(t, defaultSocket, ret.Host, "Expected ret.Host to match default socket location")
} }
@@ -63,13 +67,13 @@ func TestGetSocketAndHostDontMount(t *testing.T) {
// Arrange // Arrange
CommonSocketLocations = originalCommonSocketLocations CommonSocketLocations = originalCommonSocketLocations
dockerHost := "unix:///my/docker/host.sock" dockerHost := "unix:///my/docker/host.sock"
os.Setenv("DOCKER_HOST", dockerHost) t.Setenv("DOCKER_HOST", dockerHost)
// Act // Act
ret, err := GetSocketAndHost("-") ret, err := GetSocketAndHost("-")
// Assert // Assert
assert.Nil(t, err) assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, SocketAndHost{"-", dockerHost}, ret) assert.Equal(t, SocketAndHost{"-", dockerHost}, ret)
} }
@@ -83,8 +87,8 @@ func TestGetSocketAndHostNoHostNoSocket(t *testing.T) {
ret, err := GetSocketAndHost("") ret, err := GetSocketAndHost("")
// Assert // Assert
assert.Equal(t, true, found, "Expected a default socket to be found") 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") 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") assert.Equal(t, SocketAndHost{defaultSocket, defaultSocket}, ret, "Expected to match default socket location")
} }
@@ -93,11 +97,11 @@ func TestGetSocketAndHostNoHostNoSocket(t *testing.T) {
// > This happens if neither DOCKER_HOST nor --container-daemon-socket has a value, but socketLocation() returns a URI // > This happens if neither DOCKER_HOST nor --container-daemon-socket has a value, but socketLocation() returns a URI
func TestGetSocketAndHostNoHostNoSocketDefaultLocation(t *testing.T) { func TestGetSocketAndHostNoHostNoSocketDefaultLocation(t *testing.T) {
// Arrange // Arrange
mySocketFile, tmpErr := os.CreateTemp("", "act-*.sock") mySocketFile, tmpErr := os.CreateTemp(t.TempDir(), "act-*.sock")
mySocket := mySocketFile.Name() mySocket := mySocketFile.Name()
unixSocket := "unix://" + mySocket unixSocket := "unix://" + mySocket
defer os.RemoveAll(mySocket) defer os.RemoveAll(mySocket)
assert.NoError(t, tmpErr) assert.NoError(t, tmpErr) //nolint:testifylint // pre-existing issue from nektos/act
os.Unsetenv("DOCKER_HOST") os.Unsetenv("DOCKER_HOST")
CommonSocketLocations = []string{mySocket} CommonSocketLocations = []string{mySocket}
@@ -108,8 +112,8 @@ func TestGetSocketAndHostNoHostNoSocketDefaultLocation(t *testing.T) {
// Assert // Assert
assert.Equal(t, unixSocket, defaultSocket, "Expected default socket to match common socket location") assert.Equal(t, unixSocket, defaultSocket, "Expected default socket to match common socket location")
assert.Equal(t, true, found, "Expected default socket to be found") 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") 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") assert.Equal(t, SocketAndHost{unixSocket, unixSocket}, ret, "Expected to match default socket location")
} }
@@ -124,8 +128,8 @@ func TestGetSocketAndHostNoHostInvalidSocket(t *testing.T) {
ret, err := GetSocketAndHost(mySocket) ret, err := GetSocketAndHost(mySocket)
// Assert // Assert
assert.Equal(t, false, found, "Expected no default socket to be found") 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") assert.Equal(t, "", defaultSocket, "Expected no default socket to be found") //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, SocketAndHost{}, ret, "Expected to match default socket location") assert.Equal(t, SocketAndHost{}, ret, "Expected to match default socket location")
assert.Error(t, err, "Expected an error in invalid state") assert.Error(t, err, "Expected an error in invalid state")
} }
@@ -142,9 +146,9 @@ func TestGetSocketAndHostOnlySocketValidButUnusualLocation(t *testing.T) {
// Assert // Assert
// Default socket locations // Default socket locations
assert.Equal(t, "", defaultSocket, "Expect default socket location to be empty") assert.Equal(t, "", defaultSocket, "Expect default socket location to be empty") //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, false, found, "Expected no default socket to be found") assert.Equal(t, false, found, "Expected no default socket to be found") //nolint:testifylint // pre-existing issue from nektos/act
// Sane default // Sane default
assert.Nil(t, err, "Expect no error from GetSocketAndHost") 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") assert.Equal(t, socketURI, ret.Host, "Expect host to default to unusual socket")
} }

View File

@@ -1,3 +1,7 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// Copyright 2023 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build WITHOUT_DOCKER || !(linux || darwin || windows || netbsd) //go:build WITHOUT_DOCKER || !(linux || darwin || windows || netbsd)
package container package container
@@ -6,20 +10,21 @@ import (
"context" "context"
"runtime" "runtime"
"github.com/docker/docker/api/types/system" "gitea.com/gitea/act_runner/act/common"
"github.com/actions-oss/act-cli/pkg/common"
"github.com/docker/docker/api/types"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
// ImageExistsLocally returns a boolean indicating if an image with the // ImageExistsLocally returns a boolean indicating if an image with the
// requested name, tag and architecture exists in the local docker image store // requested name, tag and architecture exists in the local docker image store
func ImageExistsLocally(ctx context.Context, imageName string, platform string) (bool, error) { func ImageExistsLocally(ctx context.Context, imageName, platform string) (bool, error) {
return false, errors.New("Unsupported Operation") return false, errors.New("Unsupported Operation")
} }
// RemoveImage removes image from local store, the function is used to run different // RemoveImage removes image from local store, the function is used to run different
// container image architectures // container image architectures
func RemoveImage(ctx context.Context, imageName string, force bool, pruneChildren bool) (bool, error) { func RemoveImage(ctx context.Context, imageName string, force, pruneChildren bool) (bool, error) {
return false, errors.New("Unsupported Operation") return false, errors.New("Unsupported Operation")
} }
@@ -46,8 +51,8 @@ func RunnerArch(ctx context.Context) string {
return runtime.GOOS return runtime.GOOS
} }
func GetHostInfo(ctx context.Context) (info system.Info, err error) { func GetHostInfo(ctx context.Context) (info types.Info, err error) {
return system.Info{}, nil return types.Info{}, nil
} }
func NewDockerVolumeRemoveExecutor(volume string, force bool) common.Executor { func NewDockerVolumeRemoveExecutor(volume string, force bool) common.Executor {

View File

@@ -1,3 +1,7 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// Copyright 2020 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows || netbsd)) //go:build !(WITHOUT_DOCKER || !(linux || darwin || windows || netbsd))
package container package container
@@ -5,7 +9,8 @@ package container
import ( import (
"context" "context"
"github.com/actions-oss/act-cli/pkg/common" "gitea.com/gitea/act_runner/act/common"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/volume" "github.com/docker/docker/api/types/volume"
) )

View File

@@ -1,3 +1,7 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Copyright 2022 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package container package container
import "context" import "context"
@@ -9,7 +13,7 @@ type ExecutionsEnvironment interface {
GetPathVariableName() string GetPathVariableName() string
DefaultPathVariable() string DefaultPathVariable() string
JoinPathVariable(...string) string JoinPathVariable(...string) string
GetRunnerContext(ctx context.Context) map[string]interface{} GetRunnerContext(ctx context.Context) map[string]any
// On windows PATH and Path are the same key // On windows PATH and Path are the same key
IsEnvironmentCaseInsensitive() bool IsEnvironmentCaseInsensitive() bool
} }

View File

@@ -1,3 +1,7 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Copyright 2022 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package container package container
import ( import (
@@ -15,14 +19,14 @@ import (
"strings" "strings"
"time" "time"
"gitea.com/gitea/act_runner/act/common"
"gitea.com/gitea/act_runner/act/filecollector"
"gitea.com/gitea/act_runner/act/lookpath"
"github.com/go-git/go-billy/v5/helper/polyfill" "github.com/go-git/go-billy/v5/helper/polyfill"
"github.com/go-git/go-billy/v5/osfs" "github.com/go-git/go-billy/v5/osfs"
"github.com/go-git/go-git/v5/plumbing/format/gitignore" "github.com/go-git/go-git/v5/plumbing/format/gitignore"
"golang.org/x/term" "golang.org/x/term"
"github.com/actions-oss/act-cli/pkg/common"
"github.com/actions-oss/act-cli/pkg/filecollector"
"github.com/actions-oss/act-cli/pkg/lookpath"
) )
type HostEnvironment struct { type HostEnvironment struct {
@@ -35,20 +39,26 @@ type HostEnvironment struct {
StdOut io.Writer StdOut io.Writer
} }
func (e *HostEnvironment) Create(_ []string, _ []string) common.Executor { func (e *HostEnvironment) Create(_, _ []string) common.Executor {
return func(_ context.Context) error { return func(ctx context.Context) error {
return nil
}
}
func (e *HostEnvironment) ConnectToNetwork(name string) common.Executor {
return func(ctx context.Context) error {
return nil return nil
} }
} }
func (e *HostEnvironment) Close() common.Executor { func (e *HostEnvironment) Close() common.Executor {
return func(_ context.Context) error { return func(ctx context.Context) error {
return nil return nil
} }
} }
func (e *HostEnvironment) Copy(destPath string, files ...*FileEntry) common.Executor { func (e *HostEnvironment) Copy(destPath string, files ...*FileEntry) common.Executor {
return func(_ context.Context) error { return func(ctx context.Context) error {
for _, f := range files { for _, f := range files {
if err := os.MkdirAll(filepath.Dir(filepath.Join(destPath, f.Name)), 0o777); err != nil { if err := os.MkdirAll(filepath.Dir(filepath.Join(destPath, f.Name)), 0o777); err != nil {
return err return err
@@ -62,9 +72,6 @@ func (e *HostEnvironment) Copy(destPath string, files ...*FileEntry) common.Exec
} }
func (e *HostEnvironment) CopyTarStream(ctx context.Context, destPath string, tarStream io.Reader) error { func (e *HostEnvironment) CopyTarStream(ctx context.Context, destPath string, tarStream io.Reader) error {
if common.Dryrun(ctx) {
return nil
}
if err := os.RemoveAll(destPath); err != nil { if err := os.RemoveAll(destPath); err != nil {
return err return err
} }
@@ -83,7 +90,7 @@ func (e *HostEnvironment) CopyTarStream(ctx context.Context, destPath string, ta
continue continue
} }
if ctx.Err() != nil { if ctx.Err() != nil {
return fmt.Errorf("copyTarStream has been cancelled") return errors.New("CopyTarStream has been cancelled")
} }
if err := cp.WriteFile(ti.Name, ti.FileInfo(), ti.Linkname, tr); err != nil { if err := cp.WriteFile(ti.Name, ti.FileInfo(), ti.Linkname, tr); err != nil {
return err return err
@@ -91,7 +98,7 @@ func (e *HostEnvironment) CopyTarStream(ctx context.Context, destPath string, ta
} }
} }
func (e *HostEnvironment) CopyDir(destPath string, srcPath string, useGitIgnore bool) common.Executor { func (e *HostEnvironment) CopyDir(destPath, srcPath string, useGitIgnore bool) common.Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
logger := common.Logger(ctx) logger := common.Logger(ctx)
srcPrefix := filepath.Dir(srcPath) srcPrefix := filepath.Dir(srcPath)
@@ -172,13 +179,13 @@ func (e *HostEnvironment) GetContainerArchive(ctx context.Context, srcPath strin
} }
func (e *HostEnvironment) Pull(_ bool) common.Executor { func (e *HostEnvironment) Pull(_ bool) common.Executor {
return func(_ context.Context) error { return func(ctx context.Context) error {
return nil return nil
} }
} }
func (e *HostEnvironment) Start(_ bool) common.Executor { func (e *HostEnvironment) Start(_ bool) common.Executor {
return func(_ context.Context) error { return func(ctx context.Context) error {
return nil return nil
} }
} }
@@ -224,7 +231,7 @@ func (l *localEnv) Getenv(name string) string {
func lookupPathHost(cmd string, env map[string]string, writer io.Writer) (string, error) { func lookupPathHost(cmd string, env map[string]string, writer io.Writer) (string, error) {
f, err := lookpath.LookPath2(cmd, &localEnv{env: env}) f, err := lookpath.LookPath2(cmd, &localEnv{env: env})
if err != nil { if err != nil {
err := "Cannot find: " + fmt.Sprint(cmd) + " in PATH" err := "Cannot find: " + cmd + " in PATH"
if _, _err := writer.Write([]byte(err + "\n")); _err != nil { if _, _err := writer.Write([]byte(err + "\n")); _err != nil {
return "", fmt.Errorf("%v: %w", err, _err) return "", fmt.Errorf("%v: %w", err, _err)
} }
@@ -272,7 +279,7 @@ func copyPtyOutput(writer io.Writer, ppty io.Reader, finishLog context.CancelFun
} }
func (e *HostEnvironment) UpdateFromImageEnv(_ *map[string]string) common.Executor { func (e *HostEnvironment) UpdateFromImageEnv(_ *map[string]string) common.Executor {
return func(_ context.Context) error { return func(ctx context.Context) error {
return nil return nil
} }
} }
@@ -320,7 +327,7 @@ func (e *HostEnvironment) exec(ctx context.Context, command []string, cmdline st
tty.Close() tty.Close()
} }
}() }()
if containerAllocateTerminal /* allocate Terminal */ { if true /* allocate Terminal */ {
var err error var err error
ppty, tty, err = setupPty(cmd, cmdline) ppty, tty, err = setupPty(cmd, cmdline)
if err != nil { if err != nil {
@@ -343,7 +350,7 @@ func (e *HostEnvironment) exec(ctx context.Context, command []string, cmdline st
} }
if tty != nil { if tty != nil {
writer.AutoStop = true writer.AutoStop = true
if _, err := tty.Write([]byte("\x04")); err != nil { if _, err := tty.WriteString("\x04"); err != nil {
common.Logger(ctx).Debug("Failed to write EOT") common.Logger(ctx).Debug("Failed to write EOT")
} }
} }
@@ -379,7 +386,7 @@ func (e *HostEnvironment) UpdateFromEnv(srcPath string, env *map[string]string)
} }
func (e *HostEnvironment) Remove() common.Executor { func (e *HostEnvironment) Remove() common.Executor {
return func(_ context.Context) error { return func(ctx context.Context) error {
if e.CleanUp != nil { if e.CleanUp != nil {
e.CleanUp() e.CleanUp()
} }
@@ -410,9 +417,8 @@ func (*HostEnvironment) GetPathVariableName() string {
return "path" return "path"
case "windows": case "windows":
return "Path" // Actually we need a case insensitive map return "Path" // Actually we need a case insensitive map
default:
return "PATH"
} }
return "PATH"
} }
func (e *HostEnvironment) DefaultPathVariable() string { func (e *HostEnvironment) DefaultPathVariable() string {
@@ -428,7 +434,6 @@ func (*HostEnvironment) JoinPathVariable(paths ...string) string {
// https://docs.github.com/en/actions/learn-github-actions/contexts#runner-context // https://docs.github.com/en/actions/learn-github-actions/contexts#runner-context
func goArchToActionArch(arch string) string { func goArchToActionArch(arch string) string {
archMapper := map[string]string{ archMapper := map[string]string{
"amd64": "X64",
"x86_64": "X64", "x86_64": "X64",
"386": "X86", "386": "X86",
"aarch64": "ARM64", "aarch64": "ARM64",
@@ -441,8 +446,6 @@ func goArchToActionArch(arch string) string {
func goOsToActionOs(os string) string { func goOsToActionOs(os string) string {
osMapper := map[string]string{ osMapper := map[string]string{
"linux": "Linux",
"windows": "Windows",
"darwin": "macOS", "darwin": "macOS",
} }
if os, ok := osMapper[os]; ok { if os, ok := osMapper[os]; ok {
@@ -451,8 +454,8 @@ func goOsToActionOs(os string) string {
return os return os
} }
func (e *HostEnvironment) GetRunnerContext(_ context.Context) map[string]interface{} { func (e *HostEnvironment) GetRunnerContext(_ context.Context) map[string]any {
return map[string]interface{}{ return map[string]any{
"os": goOsToActionOs(runtime.GOOS), "os": goOsToActionOs(runtime.GOOS),
"arch": goArchToActionArch(runtime.GOARCH), "arch": goArchToActionArch(runtime.GOARCH),
"temp": e.TmpDir, "temp": e.TmpDir,
@@ -460,11 +463,7 @@ func (e *HostEnvironment) GetRunnerContext(_ context.Context) map[string]interfa
} }
} }
func (e *HostEnvironment) GetHealth(_ context.Context) Health { func (e *HostEnvironment) ReplaceLogWriter(stdout, _ io.Writer) (io.Writer, io.Writer) {
return HealthHealthy
}
func (e *HostEnvironment) ReplaceLogWriter(stdout io.Writer, _ io.Writer) (io.Writer, io.Writer) {
org := e.StdOut org := e.StdOut
e.StdOut = stdout e.StdOut = stdout
return org, org return org, org

View File

@@ -1,3 +1,7 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Copyright 2022 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package container package container
import ( import (
@@ -16,9 +20,7 @@ import (
var _ ExecutionsEnvironment = &HostEnvironment{} var _ ExecutionsEnvironment = &HostEnvironment{}
func TestCopyDir(t *testing.T) { func TestCopyDir(t *testing.T) {
dir, err := os.MkdirTemp("", "test-host-env-*") dir := t.TempDir()
assert.NoError(t, err)
defer os.RemoveAll(dir)
ctx := context.Background() ctx := context.Background()
e := &HostEnvironment{ e := &HostEnvironment{
Path: filepath.Join(dir, "path"), Path: filepath.Join(dir, "path"),
@@ -28,18 +30,16 @@ func TestCopyDir(t *testing.T) {
StdOut: os.Stdout, StdOut: os.Stdout,
Workdir: path.Join("testdata", "scratch"), Workdir: path.Join("testdata", "scratch"),
} }
_ = os.MkdirAll(e.Path, 0700) _ = os.MkdirAll(e.Path, 0o700)
_ = os.MkdirAll(e.TmpDir, 0700) _ = os.MkdirAll(e.TmpDir, 0o700)
_ = os.MkdirAll(e.ToolCache, 0700) _ = os.MkdirAll(e.ToolCache, 0o700)
_ = os.MkdirAll(e.ActPath, 0700) _ = os.MkdirAll(e.ActPath, 0o700)
err = e.CopyDir(e.Workdir, e.Path, true)(ctx) err := e.CopyDir(e.Workdir, e.Path, true)(ctx)
assert.NoError(t, err) assert.NoError(t, err)
} }
func TestGetContainerArchive(t *testing.T) { func TestGetContainerArchive(t *testing.T) {
dir, err := os.MkdirTemp("", "test-host-env-*") dir := t.TempDir()
assert.NoError(t, err)
defer os.RemoveAll(dir)
ctx := context.Background() ctx := context.Background()
e := &HostEnvironment{ e := &HostEnvironment{
Path: filepath.Join(dir, "path"), Path: filepath.Join(dir, "path"),
@@ -49,22 +49,22 @@ func TestGetContainerArchive(t *testing.T) {
StdOut: os.Stdout, StdOut: os.Stdout,
Workdir: path.Join("testdata", "scratch"), Workdir: path.Join("testdata", "scratch"),
} }
_ = os.MkdirAll(e.Path, 0700) _ = os.MkdirAll(e.Path, 0o700)
_ = os.MkdirAll(e.TmpDir, 0700) _ = os.MkdirAll(e.TmpDir, 0o700)
_ = os.MkdirAll(e.ToolCache, 0700) _ = os.MkdirAll(e.ToolCache, 0o700)
_ = os.MkdirAll(e.ActPath, 0700) _ = os.MkdirAll(e.ActPath, 0o700)
expectedContent := []byte("sdde/7sh") expectedContent := []byte("sdde/7sh")
err = os.WriteFile(filepath.Join(e.Path, "action.yml"), expectedContent, 0600) err := os.WriteFile(filepath.Join(e.Path, "action.yml"), expectedContent, 0o600)
assert.NoError(t, err) assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
archive, err := e.GetContainerArchive(ctx, e.Path) archive, err := e.GetContainerArchive(ctx, e.Path)
assert.NoError(t, err) assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
defer archive.Close() defer archive.Close()
reader := tar.NewReader(archive) reader := tar.NewReader(archive)
h, err := reader.Next() h, err := reader.Next()
assert.NoError(t, err) assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, "action.yml", h.Name) assert.Equal(t, "action.yml", h.Name)
content, err := io.ReadAll(reader) content, err := io.ReadAll(reader)
assert.NoError(t, err) assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, expectedContent, content) assert.Equal(t, expectedContent, content)
_, err = reader.Next() _, err = reader.Next()
assert.ErrorIs(t, err, io.EOF) assert.ErrorIs(t, err, io.EOF)

View File

@@ -1,3 +1,7 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Copyright 2022 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package container package container
import ( import (
@@ -10,8 +14,7 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
type LinuxContainerEnvironmentExtensions struct { type LinuxContainerEnvironmentExtensions struct{}
}
// Resolves the equivalent host path inside the container // Resolves the equivalent host path inside the container
// This is required for windows and WSL 2 to translate things like C:\Users\Myproject to /mnt/users/Myproject // This is required for windows and WSL 2 to translate things like C:\Users\Myproject to /mnt/users/Myproject
@@ -63,8 +66,8 @@ func (*LinuxContainerEnvironmentExtensions) JoinPathVariable(paths ...string) st
return strings.Join(paths, ":") return strings.Join(paths, ":")
} }
func (*LinuxContainerEnvironmentExtensions) GetRunnerContext(ctx context.Context) map[string]interface{} { func (*LinuxContainerEnvironmentExtensions) GetRunnerContext(ctx context.Context) map[string]any {
return map[string]interface{}{ return map[string]any{
"os": "Linux", "os": "Linux",
"arch": RunnerArch(ctx), "arch": RunnerArch(ctx),
"temp": "/tmp", "temp": "/tmp",

View File

@@ -1,3 +1,7 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Copyright 2022 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package container package container
import ( import (
@@ -31,22 +35,17 @@ func TestContainerPath(t *testing.T) {
for _, v := range []containerPathJob{ for _, v := range []containerPathJob{
{"/mnt/c/Users/act/go/src/github.com/nektos/act", "C:\\Users\\act\\go\\src\\github.com\\nektos\\act\\", ""}, {"/mnt/c/Users/act/go/src/github.com/nektos/act", "C:\\Users\\act\\go\\src\\github.com\\nektos\\act\\", ""},
{"/mnt/f/work/dir", `F:\work\dir`, ""}, {"/mnt/f/work/dir", `F:\work\dir`, ""},
{"/mnt/c/windows/to/unix", "windows\\to\\unix", fmt.Sprintf("%s\\", rootDrive)}, {"/mnt/c/windows/to/unix", "windows\\to\\unix", rootDrive + "\\"},
{fmt.Sprintf("/mnt/%v/act", rootDriveLetter), "act", fmt.Sprintf("%s\\", rootDrive)}, {fmt.Sprintf("/mnt/%v/act", rootDriveLetter), "act", rootDrive + "\\"},
} { } {
if v.workDir != "" { if v.workDir != "" {
if err := os.Chdir(v.workDir); err != nil { t.Chdir(v.workDir)
log.Error(err)
t.Fail()
}
} }
assert.Equal(t, v.destinationPath, linuxcontainerext.ToContainerPath(v.sourcePath)) assert.Equal(t, v.destinationPath, linuxcontainerext.ToContainerPath(v.sourcePath))
} }
if err := os.Chdir(cwd); err != nil { t.Chdir(cwd)
log.Error(err)
}
} else { } else {
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {

View File

@@ -1,3 +1,7 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Copyright 2022 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package container package container
import ( import (
@@ -8,7 +12,7 @@ import (
"io" "io"
"strings" "strings"
"github.com/actions-oss/act-cli/pkg/common" "gitea.com/gitea/act_runner/act/common"
) )
func parseEnvFile(e Container, srcPath string, env *map[string]string) common.Executor { func parseEnvFile(e Container, srcPath string, env *map[string]string) common.Executor {
@@ -25,17 +29,8 @@ func parseEnvFile(e Container, srcPath string, env *map[string]string) common.Ex
return err return err
} }
s := bufio.NewScanner(reader) s := bufio.NewScanner(reader)
s.Buffer(nil, 1024*1024*1024) // increase buffer to 1GB to avoid scanner buffer overflow
firstLine := true
for s.Scan() { for s.Scan() {
line := s.Text() line := s.Text()
if firstLine {
firstLine = false
// skip utf8 bom, powershell 5 legacy uses it for utf8
if len(line) >= 3 && line[0] == 239 && line[1] == 187 && line[2] == 191 {
line = line[3:]
}
}
singleLineEnv := strings.Index(line, "=") singleLineEnv := strings.Index(line, "=")
multiLineEnv := strings.Index(line, "<<") multiLineEnv := strings.Index(line, "<<")
if singleLineEnv != -1 && (multiLineEnv == -1 || singleLineEnv < multiLineEnv) { if singleLineEnv != -1 && (multiLineEnv == -1 || singleLineEnv < multiLineEnv) {
@@ -64,6 +59,6 @@ func parseEnvFile(e Container, srcPath string, env *map[string]string) common.Ex
} }
} }
env = &localEnv env = &localEnv
return s.Err() return nil
} }
} }

View File

@@ -1,3 +1,7 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Copyright 2022 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build (!windows && !plan9 && !openbsd) || (!windows && !plan9 && !mips64) //go:build (!windows && !plan9 && !openbsd) || (!windows && !plan9 && !mips64)
package container package container

Some files were not shown because too many files have changed in this diff Show More