Compare commits

..
47 Commits
Author SHA1 Message Date
Renovate Bot 7b5ebe9618 fix(deps): update module connectrpc.com/connect to v1.20.0 (#985)
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.2` → `v1.20.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/connectrpc.com%2fconnect/v1.20.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/connectrpc.com%2fconnect/v1.19.2/v1.20.0?slim=true) |

---

### Release Notes

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

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

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

#### What's Changed

##### Other changes

- Bump minimum supported Go version to 1.25 by [@&#8203;jonbodner-buf](https://github.com/jonbodner-buf) in [#&#8203;922](https://github.com/connectrpc/connect-go/issues/922)
- Update Unary-Get query parameter order to match spec recommendation by [@&#8203;oliversun9](https://github.com/oliversun9) in [#&#8203;926](https://github.com/connectrpc/connect-go/issues/926)

#### New Contributors

- [@&#8203;jonbodner-buf](https://github.com/jonbodner-buf) made their first contribution in [#&#8203;922](https://github.com/connectrpc/connect-go/issues/922)

**Full Changelog**: <https://github.com/connectrpc/connect-go/compare/v1.19.2...v1.20.0>

</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:eyJjcmVhdGVkSW5WZXIiOiI0My4xODYuMSIsInVwZGF0ZWRJblZlciI6IjQzLjE4Ni4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: https://gitea.com/gitea/runner/pulls/985
Reviewed-by: silverwind <[email protected]>
Co-authored-by: Renovate Bot <[email protected]>
Co-committed-by: Renovate Bot <[email protected]>
2026-05-21 00:32:31 +00:00
Nicolas 4317662a38 update docker cli to v29.5.2 (#984)
Fixes #981

Reviewed-on: https://gitea.com/gitea/runner/pulls/984
Reviewed-by: Zettat123 <[email protected]>
Reviewed-by: Lunny Xiao <[email protected]>
Co-authored-by: Nicolas <[email protected]>
Co-committed-by: Nicolas <[email protected]>
2026-05-20 20:25:44 +00:00
2208e7ec63 feat: add cache.offline_mode to reuse cached actions (#966)
Co-authored-by: silverwind <[email protected]>
Co-authored-by: silverwind <[email protected]>
Co-authored-by: TKaxv_7S <[email protected]>
Co-authored-by: techknowlogick <[email protected]>
Co-authored-by: TKaxv_7S <[email protected]>
Co-authored-by: TKaxv_7S <[email protected]>
Reviewed-on: https://gitea.com/gitea/runner/pulls/966
Reviewed-by: Nicolas <[email protected]>
Reviewed-by: silverwind <[email protected]>
Co-authored-by: Vi <[email protected]>
Co-committed-by: Vi <[email protected]>
2026-05-20 14:09:39 +00:00
silverwindandNicolas fab9714f9a Remove stale Gitea 1.20 compatibility shims (#978)
The runner already enforces Gitea v1.21+ (see the `connect.CodeUnimplemented` check in `daemon.go`), so several shims kept for v1.20 compatibility have been dead since 2023:

- `compatibleWithOldEnvs` — the `GITEA_DEBUG`, `GITEA_TRACE`, `GITEA_RUNNER_CAPACITY`, `GITEA_RUNNER_FILE`, `GITEA_RUNNER_ENVIRON`, `GITEA_RUNNER_ENV_FILE` env vars (superseded by the config file)
- `VersionHeader` (`x-runner-version`) and the `version` param of `client.New`
- `AgentLabels` field in `RegisterRequest` (replaced by `Labels`)

Also replaces a verbose `strings.TrimRightFunc` closure with `strings.TrimRight(s, "\r\n")` in the log row parser.

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

---------

Co-authored-by: Nicolas <[email protected]>
Reviewed-on: https://gitea.com/gitea/runner/pulls/978
Reviewed-by: Nicolas <[email protected]>
Co-authored-by: silverwind <[email protected]>
Co-committed-by: silverwind <[email protected]>
2026-05-19 16:49:15 +00:00
Renovate Bot 10475db58a fix(deps): update module github.com/docker/cli to v29.5.1+incompatible (#979)
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) | `v29.5.0+incompatible` → `v29.5.1+incompatible` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fdocker%2fcli/v29.5.1+incompatible?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fdocker%2fcli/v29.5.0+incompatible/v29.5.1+incompatible?slim=true) |

---

### Release Notes

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

### [`v29.5.1+incompatible`](https://github.com/docker/cli/compare/v29.5.0...v29.5.1)

[Compare Source](https://github.com/docker/cli/compare/v29.5.0...v29.5.1)

</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:eyJjcmVhdGVkSW5WZXIiOiI0My4xODIuMSIsInVwZGF0ZWRJblZlciI6IjQzLjE4Mi4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: https://gitea.com/gitea/runner/pulls/979
Reviewed-by: silverwind <[email protected]>
Co-authored-by: Renovate Bot <[email protected]>
Co-committed-by: Renovate Bot <[email protected]>
2026-05-19 16:27:04 +00:00
Renovate Bot 9e738c203c fix(deps): update module github.com/go-git/go-git/v5 to v5.19.1 (#980)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [github.com/go-git/go-git/v5](https://github.com/go-git/go-git) | `v5.19.0` → `v5.19.1` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fgo-git%2fgo-git%2fv5/v5.19.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fgo-git%2fgo-git%2fv5/v5.19.0/v5.19.1?slim=true) |

---

### Release Notes

<details>
<summary>go-git/go-git (github.com/go-git/go-git/v5)</summary>

### [`v5.19.1`](https://github.com/go-git/go-git/releases/tag/v5.19.1)

[Compare Source](https://github.com/go-git/go-git/compare/v5.19.0...v5.19.1)

#### What's Changed

- v5: plumbing: transport/ssh, Shell-quote path by [@&#8203;hiddeco](https://github.com/hiddeco) in [#&#8203;2068](https://github.com/go-git/go-git/pull/2068)
- v5: git: submodule, Fix relative URL resolution by [@&#8203;hiddeco](https://github.com/hiddeco) in [#&#8203;2070](https://github.com/go-git/go-git/pull/2070)
- v5: git: submodule, canonical remote for relative URLs by [@&#8203;hiddeco](https://github.com/hiddeco) in [#&#8203;2074](https://github.com/go-git/go-git/pull/2074)
- v5: git: submodule, error on remote without URLs by [@&#8203;hiddeco](https://github.com/hiddeco) in [#&#8203;2078](https://github.com/go-git/go-git/pull/2078)
- v5: plumbing: format/idxfile, Validate offset64 indices by [@&#8203;hiddeco](https://github.com/hiddeco) in [#&#8203;2084](https://github.com/go-git/go-git/pull/2084)
- v5: \*: Reject malformed variable-length integers by [@&#8203;hiddeco](https://github.com/hiddeco) in [#&#8203;2092](https://github.com/go-git/go-git/pull/2092)
- v5: plumbing: format/packfile, Tighten delta validation by [@&#8203;hiddeco](https://github.com/hiddeco) in [#&#8203;2091](https://github.com/go-git/go-git/pull/2091)
- v5: Add `worktreeFilesystem` wrapper for worktree and hardening by [@&#8203;hiddeco](https://github.com/hiddeco) in [#&#8203;2100](https://github.com/go-git/go-git/pull/2100)
- v5: config: validate submodule names by [@&#8203;hiddeco](https://github.com/hiddeco) in [#&#8203;2082](https://github.com/go-git/go-git/pull/2082)
- build: Update module github.com/go-git/go-git/v5 to v5.19.0 \[SECURITY] (releases/v5.x) by [@&#8203;go-git-renovate](https://github.com/go-git-renovate)\[bot] in [#&#8203;2111](https://github.com/go-git/go-git/pull/2111)
- v5: git: Allow MkdirAll on worktree-root paths by [@&#8203;hiddeco](https://github.com/hiddeco) in [#&#8203;2117](https://github.com/go-git/go-git/pull/2117)
- v5: git: Stop validating symlink target paths by [@&#8203;pjbgf](https://github.com/pjbgf) in [#&#8203;2116](https://github.com/go-git/go-git/pull/2116)
- v5: plumbing: format decoder input bounds and contracts by [@&#8203;hiddeco](https://github.com/hiddeco) in [#&#8203;2125](https://github.com/go-git/go-git/pull/2125)
- plumbing: format/packfile, cap delta chain depth in parser by [@&#8203;pjbgf](https://github.com/pjbgf) in [#&#8203;2137](https://github.com/go-git/go-git/pull/2137)

**Full Changelog**: <https://github.com/go-git/go-git/compare/v5.19.0...v5.19.1>

</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:eyJjcmVhdGVkSW5WZXIiOiI0My4xODIuMSIsInVwZGF0ZWRJblZlciI6IjQzLjE4Mi4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: https://gitea.com/gitea/runner/pulls/980
Reviewed-by: Lunny Xiao <[email protected]>
Co-authored-by: Renovate Bot <[email protected]>
Co-committed-by: Renovate Bot <[email protected]>
2026-05-19 16:18:34 +00:00
6023928876 Fix token use with schemaless Gitea instance (#977)
Fixes #973

## Summary
- Normalize schemaless `--gitea-instance` values before comparing clone URL hosts
- Add regression tests for `GITEA_TOKEN` use with private action/reusable workflow clones on the same instance

---------

Co-authored-by: silverwind <[email protected]>
Co-authored-by: silverwind <[email protected]>
Reviewed-on: https://gitea.com/gitea/runner/pulls/977
Reviewed-by: Lunny Xiao <[email protected]>
Reviewed-by: silverwind <[email protected]>
Co-authored-by: Nicolas <[email protected]>
Co-committed-by: Nicolas <[email protected]>
2026-05-18 02:22:04 +00:00
silverwind 014ce438c1 Add OCI source and version labels to images (#975)
Adds `org.opencontainers.image.source` and `org.opencontainers.image.version` labels to all three image variants (`basic`, `dind`, `dind-rootless`).

- `source` lets tools like renovate retrieve release notes from the source repo.
- `version` exposes the build version on the image itself.

Both `release-tag` and `release-nightly` workflows pass `VERSION` as a build arg so the label reflects the actual git tag (or `git describe` output for nightly).

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

---------

Reviewed-on: https://gitea.com/gitea/runner/pulls/975
Reviewed-by: Nicolas <[email protected]>
Co-authored-by: silverwind <[email protected]>
Co-committed-by: silverwind <[email protected]>
2026-05-17 18:15:19 +00:00
cf7e29c10d fix(parse_env_file): support env-file lines larger than 64 KiB (#974)
My builds kept flaking out with errors like `invalid format delimiter 'ghadelimiter_...' not found before end of file` or just strange failures in the complete job. After some digging I found an issue in `parseEnvFile` and have tested this fix against the test case presented.

  - `parseEnvFile` reads `$GITHUB_ENV` / `$GITHUB_OUTPUT` with a `bufio.Scanner` using the default 64 KiB token size, and never checks `s.Err()`.
  - Any action that writes a multi-line value with a single line >64 KiB silently aborts the scan with `bufio.ErrTooLong`, which surfaces as the misleading `"invalid format delimiter
  'ghadelimiter_…' not found before end of file"`.
  - Real-world trigger: `docker/build-push-action`'s `metadata` output embeds the full `GITHUB_EVENT_PATH` payload via buildx provenance; a long PR description (e.g. a Renovate dependency
  table) puts the body field on one JSON-escaped line well past 64 KiB.
  - Raise the scanner buffer to 1 MiB so realistic outputs parse.

### Reproduction
Test this in an action. This removes the `docker/build-push-action` aspect and reproduces it directly.
```yaml
  jobs:
    repro:
      runs-on: ubuntu-latest
      steps:
        - id: big
          run: |
            {
              echo 'value<<EOF'
              head -c 70000 /dev/urandom | base64 -w0
              echo
              echo 'EOF'
            } >> "$GITHUB_OUTPUT"
```

---------

Co-authored-by: Nicolas <[email protected]>
Co-authored-by: silverwind <[email protected]>
Reviewed-on: https://gitea.com/gitea/runner/pulls/974
Reviewed-by: Lunny Xiao <[email protected]>
Reviewed-by: silverwind <[email protected]>
Co-authored-by: Jacob Alberty <[email protected]>
Co-committed-by: Jacob Alberty <[email protected]>
2026-05-17 13:00:17 +00:00
Nicolasandsilverwind 8a99506fed Fix host cleanup, volume allowlist, cache upload, and action host edge cases (#970)
## Summary
- prevent host-mode execution from deleting caller-owned workdirs
- harden `valid_volumes` checks against `..` and symlink escapes
- return immediately after artifact cache upload write failures
- default implicit remote action clone hosts to `GitHubInstance`/`github.com`

Authored with assistance from OpenAI Codex GPT-5.

---------

Co-authored-by: silverwind <[email protected]>
Reviewed-on: https://gitea.com/gitea/runner/pulls/970
Reviewed-by: silverwind <[email protected]>
2026-05-17 12:53:04 +00:00
silverwindandNicolas 5873b8b054 Remove dead code from act/ (#971)
Removes code that whole-program reachability analysis (`deadcode` from `golang.org/x/tools`) confirmed unreachable, plus the `act/workflowpattern` package which no file outside its own directory imports.

- `act/common/draw.go` — CLI box-drawing helpers left over from nektos/act's dropped CLI
- `act/common/file.go` — `CopyFile`/`CopyDir` package-level helpers (container types have their own `CopyDir` methods, kept)
- `act/common/executor.go` — `Warning` type and `Warningf`. The `case Warning:` arm in `(Executor).Then`'s type switch was dead too (no code ever constructed a `Warning`); the switch is replaced with `if err != nil { return err }`
- `act/lookpath/env.go` — `LookPath` no-arg wrapper and `defaultEnv` struct. Only `LookPath2(file, env)` was used externally; the `Env` interface is kept
- `act/runner/action_cache_offline_mode.go` — `GoGitActionCacheOfflineMode` wrapper, never instantiated
- `act/workflowpattern/` — entire package, never imported

Net `-943` lines.

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

---------

Co-authored-by: Nicolas <[email protected]>
Reviewed-on: https://gitea.com/gitea/runner/pulls/971
Reviewed-by: Nicolas <[email protected]>
Co-authored-by: silverwind <[email protected]>
Co-committed-by: silverwind <[email protected]>
2026-05-17 03:53:28 +00:00
Lunny Xiao 5464d33eef fix: Return if executors length is zero in ParallelExecutor (#960)
It displayed an unused log and start an unused go routine. We should check the executors number before continue.

```
INFO[2026-05-12T21:01:04-07:00] Running job with maxParallel=1 for 1 matrix combinations
INFO[2026-05-12T21:01:04-07:00] NewParallelExecutor: Creating 1 workers for 1 executors
INFO[2026-05-12T21:01:04-07:00] NewParallelExecutor: Creating 1 workers for 0 executors
INFO[2026-05-12T21:01:04-07:00] NewParallelExecutor: Creating 1 workers for 0 executors
```

Reviewed-on: https://gitea.com/gitea/runner/pulls/960
Reviewed-by: silverwind <[email protected]>
Reviewed-by: Nicolas <[email protected]>
Co-authored-by: Lunny Xiao <[email protected]>
Co-committed-by: Lunny Xiao <[email protected]>
2026-05-15 22:14:13 +00:00
3c5f03ff8f feat: make pseudo-TTY allocation opt-in (#961)
Fixes #956.

Pseudo-TTY allocation is now an explicit, runner-wide opt-in via `runner.allocate_pty`, applied to both host and docker backends. Default is off, matching GitHub `actions/runner`.

```yaml
runner:
  allocate_pty: false  # default
```

**Before:** the host backend hardcoded `if true /* allocate Terminal */` and the docker backend used `term.IsTerminal(os.Stdout.Fd())`. As a result, `docker build` (and other TTY-aware tools) saw a TTY and emitted cursor-control redraw frames that flooded captured logs with thousands of duplicate-looking progress lines — only on host-mode runners in production, and on docker-mode runners when the daemon happened to be launched from a shell rather than a service.

**After:** both backends consult `Config.AllocatePTY`. The `term.IsTerminal` heuristic is gone, so behavior no longer depends on whether the daemon has a controlling terminal.

**Reproduction:** running `docker build` through `HostEnvironment.Exec` with output captured to a buffer:

| | Before (`if true`) | After (`AllocatePTY=false`) |
|---|---:|---:|
| bytes captured | 18,167 | 1,048 |
| ANSI CSI sequences | 556 | 0 |
| cursor-up `\e[1A` | 181 | 0 |

**Side fix:** `ptyWriter.AutoStop` is now `atomic.Bool`. The field is written from the exec goroutine after `cmd.Wait()` and read from the `copyPtyOutput` goroutine via `ptyWriter.Write`; existing tests never tripped the race detector because their commands produced no output before exit. The new host-mode test does.

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

---------

Co-authored-by: Lunny Xiao <[email protected]>
Co-authored-by: Nicolas <[email protected]>
Reviewed-on: https://gitea.com/gitea/runner/pulls/961
Reviewed-by: Nicolas <[email protected]>
Co-authored-by: silverwind <[email protected]>
Co-committed-by: silverwind <[email protected]>
2026-05-15 18:11:39 +00:00
Renovate Bot 880e9755d9 chore(deps): update workflow dependencies (major) (#968)
Reviewed-on: https://gitea.com/gitea/runner/pulls/968
Reviewed-by: silverwind <[email protected]>
Co-authored-by: Renovate Bot <[email protected]>
Co-committed-by: Renovate Bot <[email protected]>
2026-05-15 14:08:07 +00:00
Renovate Bot 8d7cf48a6f fix(deps): update module github.com/docker/cli to v29.5.0+incompatible (#969)
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) | `v29.4.3+incompatible` → `v29.5.0+incompatible` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fdocker%2fcli/v29.5.0+incompatible?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fdocker%2fcli/v29.4.3+incompatible/v29.5.0+incompatible?slim=true) |

---

### Release Notes

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

### [`v29.5.0+incompatible`](https://github.com/docker/cli/compare/v29.4.3...v29.5.0)

[Compare Source](https://github.com/docker/cli/compare/v29.4.3...v29.5.0)

</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:eyJjcmVhdGVkSW5WZXIiOiI0My4xNzkuNCIsInVwZGF0ZWRJblZlciI6IjQzLjE3OS40IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: https://gitea.com/gitea/runner/pulls/969
Reviewed-by: silverwind <[email protected]>
Co-authored-by: Renovate Bot <[email protected]>
Co-committed-by: Renovate Bot <[email protected]>
2026-05-15 14:05:52 +00:00
Renovate Bot f23605c614 chore(deps): update workflow dependencies (major) (#967)
Reviewed-on: https://gitea.com/gitea/runner/pulls/967
Reviewed-by: techknowlogick <[email protected]>
Co-authored-by: Renovate Bot <[email protected]>
Co-committed-by: Renovate Bot <[email protected]>
2026-05-14 16:54:48 +00:00
thisisqasimandsilverwind 00b7fec80f Simplify kubernetes dind example allowing for default docker config in workflows (#709)
With this docker clients in workflows can connect on the default socket without needing to change DOCKER_HOST. Startup probe also removes the need for custom shell command.

Co-authored-by: silverwind <[email protected]>
Reviewed-on: https://gitea.com/gitea/runner/pulls/709
Co-authored-by: thisisqasim <[email protected]>
Co-committed-by: thisisqasim <[email protected]>
2026-05-14 05:52:41 +00:00
silverwind dda5841af8 chore(deps): bump retry-go, golangci-lint, govulncheck (#965)
Bumps `github.com/avast/retry-go` v4.7.0 -> v5.0.0, `golangci-lint` v2.11.4 -> v2.12.2 (aligns with gitea/gitea), and pins `govulncheck` to v1.3.0.

- `retry-go` v5 replaces the package-level `retry.Do(fn, opts...)` with a builder API `retry.New(opts...).Do(fn)`. The single call site in `internal/pkg/report/reporter.go` was migrated.
- `golangci-lint` v2.12.2 surfaces three new findings in `act/` (modernize/slicesbackward, govet/inline): one backward loop now uses `slices.Backward`, and the deprecated `reflect.Ptr` alias is replaced with `reflect.Pointer`.
- `go.mod`: the two direct-`require` blocks are merged into one, and a stray `gopkg.in/yaml.v3 // indirect` is moved into the indirect block. Purely cosmetic; `go.sum` is unchanged.

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

Reviewed-on: https://gitea.com/gitea/runner/pulls/965
Reviewed-by: Lunny Xiao <[email protected]>
Co-authored-by: silverwind <[email protected]>
Co-committed-by: silverwind <[email protected]>
2026-05-14 05:39:28 +00:00
silverwind 32bed52686 fix(deps): bump docker deps, switch to moby/moby (#943)
Fixes: https://gitea.com/gitea/runner/issues/859

Migration approach mirrors [actions-oss/act-cli#154](https://github.com/actions-oss/act-cli/pull/154).

### Dependency changes

- `github.com/docker/docker` v25.0.15 → **removed** (v29 doesn't exist as docker/docker; the project moved to moby/moby)
- `github.com/docker/cli` v25.0.7 → v29.4.3
- `github.com/docker/go-connections` v0.6.0 → v0.7.0
- `github.com/docker/docker-credential-helpers` v0.9.5 → v0.9.6
- `github.com/moby/go-archive` added at v0.2.0
- `github.com/moby/moby/api` added at v1.54.2
- `github.com/moby/moby/client` added at v0.4.1
- `github.com/moby/buildkit` removed (only used `dockerignore.ReadAll`, swapped for `moby/patternmatcher/ignorefile.ReadAll` directly)
- `github.com/containerd/errdefs` v0.3.0 → v1.0.0

### Migration

- v28: type aliases moved to their subpackages (`types.{Container,Image,Network,Exec}*` → `container/image/network/...`); deprecated APIs replaced (`ImageInspectWithRaw`, `client.IsErrNotFound`, `archive.CanonicalTarNameForPath`, `opts.ValidateMACAddress`, `ListOpts.GetAll`)
- v29: structural client redesign — every `cli.X(ctx, ...)` call switched to options-everywhere/Result-typed signatures, `ContainerExec*` → `Exec*`, `ContainerWait` returns a struct with `Result`/`Error` channels, `Tty`→`TTY`, `Copy*Container` takes options struct, `client.NewClientWithOpts` → `client.New`. `pkg/stdcopy` moved to `moby/moby/api/pkg/stdcopy`. The vendored copy of `cli/command/container/opts.go` was refreshed from cli v29 (now uses `netip.Addr` for IPs, port-set conversion helpers). A small local `parsePlatform` helper centralises the `os/arch[/variant]` parsing previously inlined into multiple call sites.

### Behaviour preservation

The migration introduced several behavioural shifts vs the v25 client; all were caught in review and reverted/fixed in follow-up commits:

- `GetDockerClient`: cli v29's `Ping(NegotiateAPIVersion: true)` returns errors that the old `NegotiateAPIVersion` silently swallowed. Restored best-effort behaviour (warn-log + continue) so daemons with blocked `_ping` or API < 1.40 keep working. The SSH-helper `client.New` call no longer inherits `client.FromEnv`, matching the old `NewClientWithOpts(WithHost, WithDialContext)` so `DOCKER_API_VERSION`/`DOCKER_TLS_VERIFY` don't leak into the SSH-tunneled client
- `parsePlatform`: malformed input now returns an explicit error instead of silently dropping to "no platform constraint" and pulling the host-default architecture. Single-segment (`"linux"`), 4+-segment (`"linux/arm/v7/extra"`), and trailing-slash (`"linux/arm/"`) inputs are all rejected
- `LoadDockerAuthConfig`/`LoadDockerAuthConfigs`: `config.LoadDefaultConfigFile(nil)` panics on a malformed config file (it does `fmt.Fprintln` on the nil `io.Writer`). Switched to `config.Load(config.Dir())` so load errors reach the logger and the panic path is gone. Restored the old behaviour of returning `config.Load` and `GetAuthConfig` errors to the caller (the v29 refactor had silently downgraded them to warn-only). A `reference.ParseNormalizedNamed` failure on the image string falls through to the `docker.io` default rather than aborting, since the old string-based hostname extraction was infallible

Test assertions also updated for two upstream error-message string shifts (`go-connections` port-range parser; `cli/opts` envfile BOM check). Added unit-test coverage for the new `parsePlatform` helper, locking in the intentional limits (single-segment, 4+-segment, and trailing-slash platforms rejected).

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

Reviewed-on: https://gitea.com/gitea/runner/pulls/943
Reviewed-by: Lunny Xiao <[email protected]>
Co-authored-by: silverwind <[email protected]>
Co-committed-by: silverwind <[email protected]>
2026-05-14 02:29:05 +00:00
Lunny Xiao a7e972d8de fix: respect proxy env vars in runner client (#962)
Fixes #957.

## Why
The runner builds a custom `http.Transport` for its RPC client. From first principles, once we stop using the default transport we also stop inheriting its default proxy resolution behavior, so `HTTP_PROXY`/`HTTPS_PROXY` are ignored unless we wire that behavior back explicitly.

## What
- set `Proxy: http.ProxyFromEnvironment` on the custom transport
- add a regression test that verifies `getHTTPClient` honors proxy environment variables

Reviewed-on: https://gitea.com/gitea/runner/pulls/962
Reviewed-by: ChristopherHX <[email protected]>
2026-05-13 19:10:52 +00:00
Lunny XiaoandNicolas 763b38ece3 fix: isolate per-task runner envs (#959)
## Summary
- clone the runner environment map for each task before injecting runtime and OIDC tokens
- keep the shared base environment immutable so concurrent jobs cannot hit `concurrent map writes`
- add a unit test covering task-local env cloning

Fixes #958

---------

Co-authored-by: Nicolas <[email protected]>
Reviewed-on: https://gitea.com/gitea/runner/pulls/959
Reviewed-by: Nicolas <[email protected]>
2026-05-12 18:50:25 +00:00
Renovate Bot a1f13cb970 fix(deps): update module github.com/opencontainers/selinux to v1.14.1 (#955)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [github.com/opencontainers/selinux](https://github.com/opencontainers/selinux) | `v1.14.0` → `v1.14.1` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fopencontainers%2fselinux/v1.14.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fopencontainers%2fselinux/v1.14.0/v1.14.1?slim=true) |

---

> ⚠️ **Warning**
>
> Some dependencies could not be looked up. Check the [Dependency Dashboard](issues/856) for more information.

---

### 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:eyJjcmVhdGVkSW5WZXIiOiI0My4xNzAuMjAiLCJ1cGRhdGVkSW5WZXIiOiI0My4xNzAuMjAiLCJ0YXJnZXRCcmFuY2giOiJtYWluIiwibGFiZWxzIjpbXX0=-->

Reviewed-on: https://gitea.com/gitea/runner/pulls/955
Reviewed-by: silverwind <[email protected]>
Co-authored-by: Renovate Bot <[email protected]>
Co-committed-by: Renovate Bot <[email protected]>
2026-05-12 13:34:52 +00:00
silverwind 1e3ab0c40a fix(deps): update mergo to v1.0.2 (now dario.cat/mergo) (#954)
At v1.0.0 the `github.com/imdario/mergo` module was relocated to `dario.cat/mergo`, so a plain version bump (as in https://gitea.com/gitea/runner/pulls/951) leaves the import path pointing at the old, unmaintained location. This PR updates the import in `act/container/docker_run.go` and adjusts `go.mod` accordingly. The public API (`mergo.Merge`, `mergo.WithOverride`, `mergo.WithAppendSlice`) is unchanged.

Supersedes https://gitea.com/gitea/runner/pulls/951.

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

Reviewed-on: https://gitea.com/gitea/runner/pulls/954
Reviewed-by: Lunny Xiao <[email protected]>
Co-authored-by: silverwind <[email protected]>
Co-committed-by: silverwind <[email protected]>
2026-05-10 22:05:05 +00:00
silverwind 295eecb9af fix: ensure dbfs_data is cleaned up after task completion (#952)
Fixes #950.

After #819, the daemon flushes logs eagerly on the job-result entry (via the `stateNotify` path), so `Close()` typically runs `ReportLog(true)` with an empty buffer. Gitea's `UpdateLog` handler short-circuits on `len(Rows)==0` before honoring `NoMore`, so the final request never runs `TransferLogs` and `dbfs_data` rows leak. The server-side short-circuit is latent since the original Actions implementation in 2023; #819 made it deterministically reachable.

Workaround: inject a sentinel row in `Close()` after the daemon has exited so the final `UpdateLog` always carries at least one row. Done after the daemon waits so the sentinel can't be flushed before `ReportLog(true)` reads it.

https://github.com/go-gitea/gitea/pull/37631 drops the empty-rows short-circuit when `NoMore=true`; that would work with or without this PR.

Reviewed-on: https://gitea.com/gitea/runner/pulls/952
Reviewed-by: Nicolas <[email protected]>
Reviewed-by: Zettat123 <[email protected]>
Co-authored-by: silverwind <[email protected]>
Co-committed-by: silverwind <[email protected]>
2026-05-10 20:55:57 +00:00
Zettat123andNicolas ef6ca957b5 fix(artifactcache): preserve cache key case to stop redundant uploads (#947)
## Summary

`artifactcache.Handler` was lowercasing cache keys before storing and returning them. This caused actions like `actions/setup-go` to treat every restore as a partial hit and re-upload the cache on every job run.

Similar issue: [act#2497](https://github.com/nektos/act/issues/2497)

## Root Cause

These actions build cache keys that include `RUNNER_OS` (e.g. `setup-go-Linux-x64-...` See [setup-go/cache-restore.ts](https://github.com/actions/setup-go/blob/78961f6f84d799cd858575bb931c3e51d3b13290/src/cache-restore.ts#L11-L51) ). In `gitea/runner`,  `RUNNER_OS` is always `Linux` by default (See https://gitea.com/gitea/runner/search?q=RUNNER_OS).

These actions decide whether to save the cache data in their post step using **strict** `===` comparison between the primary key and the key returned from the runner. See [setup-go cache-save.ts](https://github.com/actions/setup-go/blob/78961f6f84d799cd858575bb931c3e51d3b13290/src/cache-save.ts#L44-L86) .

|State | Value|
|--- | ---|
|CachePrimaryKey | `setup-go-Linux-x64-ubuntu-22.04-go-1.24.9-abc123` |
|CacheMatchedKey | `setup-go-linux-x64-ubuntu-22.04-go-1.24.9-abc123` |

Because the runner's cache server lowercased the stored key, the response carried `setup-go-linux-...` while the action's primary key was `setup-go-Linux-...`. Strict equality failed, then the actions updated same data again. This repeated on every run, wasting disk and bandwidth. The duplicate blobs accumulate until GC .

https://gitea.com/gitea/runner/actions/runs/462560/jobs/737401#jobstep-2-15
![image.png](/attachments/d3487457-1d09-44b5-9937-a0b8cab1bcc5)

https://gitea.com/gitea/runner/actions/runs/462560/jobs/737401#jobstep-6-22
![image.png](/attachments/9217dc71-cb0c-456b-a516-0017458123c7)

## Fix
Drop the `strings.ToLower` calls in `find` and `reserve` so the original key case is preserved end-to-end. This fix will invalidate existing "case insensitive" keys.

## Notes

The [original act review](https://github.com/nektos/act/pull/1770/changes/BASE..d44b8d15649d9d09d1d891130b8f3962097a81f3#r1177624608) suggested making cache keys case-insensitive because `isExactKeyMatch` compares cache key ignoring case. However, the actions (`setup-go` / `setup-node` / `setup-ruby`) compare with strict `===` rather than `isExactKeyMatch`.

---------

Co-authored-by: Nicolas <[email protected]>
Reviewed-on: https://gitea.com/gitea/runner/pulls/947
Reviewed-by: silverwind <[email protected]>
Reviewed-by: Lunny Xiao <[email protected]>
Reviewed-by: Nicolas <[email protected]>
Co-authored-by: Zettat123 <[email protected]>
Co-committed-by: Zettat123 <[email protected]>
2026-05-09 12:27:52 +00:00
Renovate Bot 8088df52b9 fix(deps): update module golang.org/x/term to v0.43.0 (#948)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) | [`v0.42.0` → `v0.43.0`](https://cs.opensource.google/go/x/term/+/refs/tags/v0.42.0...refs/tags/v0.43.0) | ![age](https://developer.mend.io/api/mc/badges/age/go/golang.org%2fx%2fterm/v0.43.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/golang.org%2fx%2fterm/v0.42.0/v0.43.0?slim=true) |

---

> ⚠️ **Warning**
>
> Some dependencies could not be looked up. Check the [Dependency Dashboard](issues/856) for more information.

---

### 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:eyJjcmVhdGVkSW5WZXIiOiI0My4xNjkuNCIsInVwZGF0ZWRJblZlciI6IjQzLjE2OS40IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: https://gitea.com/gitea/runner/pulls/948
Reviewed-by: silverwind <[email protected]>
Co-authored-by: Renovate Bot <[email protected]>
Co-committed-by: Renovate Bot <[email protected]>
2026-05-09 05:28:44 +00:00
3ea7d39690 fix: overwrite read-only files when copying action directories (#942)
## Summary

  - `CopyCollector.WriteFile` now removes any existing destination file
    before writing, handling read-only modes (e.g. git pack files at
    `0444`) that cause `EACCES`/`ERROR_ACCESS_DENIED` on macOS and Windows.
  - Added `O_TRUNC` to the `OpenFile` flags as a safety net.

  ## Root cause

  When a composite action with a post step runs on a host runner,
  `runPostStep` calls `maybeCopyToActionDir`, which re-copies the action
  into `miscpath/act/actions/<name>/`. The first copy (main step) writes
  `.git/objects/pack/*.idx` at the destination with mode `0444` (as set
  by go-git). The second copy (post step) calls
  `os.OpenFile(dest, O_CREATE|O_WRONLY, …)` on that existing `0444` file,
  which fails immediately:

  - macOS: `open <path>: permission denied`
  - Windows: `open <path>: Access is denied`

Fixes: https://gitea.com/gitea/runner/issues/941
Fixes: https://gitea.com/gitea/runner/issues/876

---------

Co-authored-by: silverwind <[email protected]>
Co-authored-by: silverwind <[email protected]>
Reviewed-on: https://gitea.com/gitea/runner/pulls/942
Reviewed-by: silverwind <[email protected]>
Co-authored-by: Nicolas <[email protected]>
Co-committed-by: Nicolas <[email protected]>
2026-05-08 04:11:42 +00:00
861d351845 add apparmor=rootlesskit in security_opt (#937)
paste depends_on chain from socket-runner-setup  to runner-dind-setup
add apparmor=rootlesscit in security_opt
add explanations for elevated privileges

---------

Co-authored-by: silverwind <[email protected]>
Co-authored-by: silverwind <[email protected]>
Reviewed-on: https://gitea.com/gitea/runner/pulls/937
Reviewed-by: silverwind <[email protected]>
Co-authored-by: Schallbert <[email protected]>
Co-committed-by: Schallbert <[email protected]>
2026-05-07 21:20:33 +00:00
silverwindandNicolas cce8543d06 fix: serialize action-cache reads to prevent worktree race (#938)
`NewGitCloneExecutor` holds a per-directory mutex while it `git checkout --force`s a remote action into the shared `<ActionCacheDir>/<UsesHash>`, but four read sites ran unlocked:

- `maybeCopyToActionDir`'s tar walk via `JobContainer.CopyDir`
- `prepareActionExecutor`'s `readAction` parse of `action.yml`
- `newReusableWorkflowExecutor`'s `model.NewWorkflowPlanner` after `cloneRemoteReusableWorkflow` released its lock
- `execAsDocker` when `ActionCache == nil`: `docker build` walks `contextDir` for the daemon-side build context

When two matrix jobs share a `uses:`, a read interleaved with a peer's checkout produces partial state — observed as `Cannot find module .../dist/index.js` and `setup-uv` failing on a half-written `action.yml`.

Exports `acquireCloneLock` as `AcquireCloneLock` and takes it at all four sites. `container.ImageExistsLocally` / `NewDockerBuildExecutor` and `model.NewWorkflowPlanner` are indirected through package-level vars so the docker-action build path and the reusable-workflow read site are testable without a real daemon, mirroring `ContainerNewContainer`. Three regression tests cover the higher-risk sites (`maybeCopyToActionDir`, `execAsDocker`, `newReusableWorkflowExecutor`); each fails if its `AcquireCloneLock` is removed.

Subsumed by https://gitea.com/gitea/runner/pulls/814 once that lands. Related: https://gitea.com/gitea/runner/pulls/930

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

---------

Co-authored-by: Nicolas <[email protected]>
Reviewed-on: https://gitea.com/gitea/runner/pulls/938
Reviewed-by: Nicolas <[email protected]>
Reviewed-by: Zettat123 <[email protected]>
Co-authored-by: silverwind <[email protected]>
Co-committed-by: silverwind <[email protected]>
2026-05-07 19:57:04 +00:00
silverwind 75643645f0 feat: remove emojis from runner logging, add Starting job container group (#940)
Aligns runner log output more closely with `actions/runner`:

- Strip the whale, rocket, cloud, construction, chequered-flag, and exclamation-mark glyphs from log lines and drop the now-unused `logPrefix` constant.
- Reword `no outputs used step '%s'` → `No outputs registered for step '%s'` (the original was ungrammatical and inaccurate — it fires when `set-output` references an unknown step ID).
- Wrap the docker pull/network/create/start phase of job container startup in a `::group::Starting job container` / `::endgroup::` collapsible section, mirroring `actions/runner`. Since act drives Docker through the SDK rather than the CLI, we can't echo `##[command]/usr/bin/docker create ...` lines verbatim — instead the helper emits a summary inside the group:

  ```
  ::group::Starting job container
  image: <image>
  name: <container-name>
  network: <network-name>
  ::endgroup::
  ```

- Extracted the emit into a `printStartJobContainerGroup` helper (parallel to `printRunActionHeader` in `step_run.go`) and added a golden-style test `TestPrintStartJobContainerGroupGolden`.
- Drive-by: replace two remaining literal `"raw_output"` strings in `run_context.go` with the existing `rawOutputField` constant.

Closes #935

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

Reviewed-on: https://gitea.com/gitea/runner/pulls/940
Reviewed-by: Lunny Xiao <[email protected]>
Co-authored-by: silverwind <[email protected]>
Co-committed-by: silverwind <[email protected]>
2026-05-07 19:32:53 +00:00
Renovate Bot dff63b3ecc fix(deps): update module github.com/go-git/go-git/v5 to v5.19.0 (#934)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [github.com/go-git/go-git/v5](https://github.com/go-git/go-git) | `v5.18.0` → `v5.19.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fgo-git%2fgo-git%2fv5/v5.19.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fgo-git%2fgo-git%2fv5/v5.18.0/v5.19.0?slim=true) |

---

> ⚠️ **Warning**
>
> Some dependencies could not be looked up. Check the [Dependency Dashboard](issues/856) for more information.

---

### 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:eyJjcmVhdGVkSW5WZXIiOiI0My4xNjUuMSIsInVwZGF0ZWRJblZlciI6IjQzLjE2NS4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: https://gitea.com/gitea/runner/pulls/934
Reviewed-by: Lunny Xiao <[email protected]>
Co-authored-by: Renovate Bot <[email protected]>
Co-committed-by: Renovate Bot <[email protected]>
2026-05-07 01:27:20 +00:00
Renovate Bot a5d9fe9651 fix(deps): update module github.com/opencontainers/selinux to v1.14.0 (#928)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [github.com/opencontainers/selinux](https://github.com/opencontainers/selinux) | `v1.13.1` → `v1.14.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fopencontainers%2fselinux/v1.14.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fopencontainers%2fselinux/v1.13.1/v1.14.0?slim=true) |

---

> ⚠️ **Warning**
>
> Some dependencies could not be looked up. Check the [Dependency Dashboard](issues/856) for more information.

---

### 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:eyJjcmVhdGVkSW5WZXIiOiI0My4xNjMuMyIsInVwZGF0ZWRJblZlciI6IjQzLjE2My4zIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: https://gitea.com/gitea/runner/pulls/928
Reviewed-by: silverwind <[email protected]>
Co-authored-by: Renovate Bot <[email protected]>
Co-committed-by: Renovate Bot <[email protected]>
2026-05-07 01:27:06 +00:00
silverwind d607f3b342 test: clean up dead/stale fixtures and bump test container images (#932)
Audit-driven cleanup of `act/` test fixtures. Three commits:

**1. Remove dead fixtures** — 12 fixture directories that no Go test references: `dir with spaces`, `environment-variables`, `issue-104`, `issue-122`, `issue-141`, `localdockerimagetest_`, `node`, `parallel`, `python`, `uses-composite-with-inputs`, `uses-composite-with-pre-and-post-steps`, `shells/custom` (under `act/runner/testdata/`), plus `act/artifactcache/testdata/example`.

**2. Collapse `actions/node{12,16,20}` to a single `actions/node24` fixture** — the trio dispatched through identical `IsNode()` code paths and exercised the container's node binary, not the `using:` string. Bumps bundled deps to current (`@actions/core@^3`, `@actions/github@^9`, `@vercel/ncc@^0.38.4`) — both runtime packages are now ESM-only, so `index.js` is rewritten to ESM and `"type": "module"` added. Drops committed `node_modules/` and `package-lock.json` (now gitignored locally; `dist/` continues to be ignored by the repo-root `.gitignore` as before). Reduces `local-action-js/push.yml` to a single `test-node24` job and bumps four other stale `using: node12/16` references in fixtures.

**3. Bump test container base images** to `node:24-bookworm-slim` / `node:24-bookworm` / `ubuntu:24.04`. Replaces `node:16-buster-slim`, `node:16-buster`, `node:12.20.1-buster-slim`, and the EOL `node:12-buster-slim` / `node:16-buster-slim` / `ubuntu:18.04` base images in `actions/{docker-local,docker-local-noargs,action1}/Dockerfile`.

The runner's model still accepts `using: node12/16/20` for third-party actions in the wild — those constants are untouched.

Fixes: https://gitea.com/gitea/runner/issues/931

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

Reviewed-on: https://gitea.com/gitea/runner/pulls/932
Reviewed-by: Lunny Xiao <[email protected]>
Co-authored-by: silverwind <[email protected]>
Co-committed-by: silverwind <[email protected]>
2026-05-07 00:11:56 +00:00
silverwind 5e59402fb2 fix: re-fetch cached reusable workflow on every run (#930)
`cloneIfRequired` only ran the underlying clone executor when the target directory was missing, so a reusable workflow referenced by a moving ref (`uses: org/repo/.gitea/workflows/wf.yml@master`) was cached forever after the first invocation — edits to the source file never propagated.

Always invoke `git.NewGitCloneExecutor`. It handles existing repositories via fetch + pull + hard-reset, so branch and tag refs are brought up to date on each run, matching GitHub Actions semantics.

Drops the global `executorLock` too: `NewGitCloneExecutor` already takes a per-directory lock via `acquireCloneLock`, so the outer mutex only added unnecessary serialization across unrelated reusable-workflow clones — worse now that every invocation runs the full fetch.

Includes a regression test that drives the wrapper against a local bare repo, pushes a new commit on `master` between two invocations, and asserts the cached workflow file reflects the new tip.

Fixes: https://github.com/go-gitea/gitea/issues/37483
Fixes: https://gitea.com/gitea/runner/issues/726
Related: https://github.com/go-gitea/gitea/issues/30543

Would be subsumed by https://gitea.com/gitea/runner/pulls/814 ("WIP: Introduce new action cache") once that lands.

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

Reviewed-on: https://gitea.com/gitea/runner/pulls/930
Reviewed-by: Zettat123 <[email protected]>
Co-authored-by: silverwind <[email protected]>
Co-committed-by: silverwind <[email protected]>
2026-05-06 16:10:27 +00:00
Renovate BotandLunny Xiao dfeb463904 chore(deps): update docker docker tag to v29 (#924)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| docker | stage | major | `28-dind-rootless` → `29-dind-rootless` |
| docker | stage | major | `28-dind` → `29-dind` |

---

> ⚠️ **Warning**
>
> Some dependencies could not be looked up. Check the [Dependency Dashboard](issues/856) for more information.

---

### 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 these updates 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:eyJjcmVhdGVkSW5WZXIiOiI0My4xNjAuNiIsInVwZGF0ZWRJblZlciI6IjQzLjE2MC42IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

---------

Co-authored-by: Lunny Xiao <[email protected]>
Reviewed-on: https://gitea.com/gitea/runner/pulls/924
Reviewed-by: silverwind <[email protected]>
Co-authored-by: Renovate Bot <[email protected]>
Co-committed-by: Renovate Bot <[email protected]>
2026-05-05 23:14:36 +00:00
silverwindandNicolas 594c9ade7c Align step failure log output with GitHub Actions (#927)
Fixes #926.

Before:

<img src="/attachments/a5ae9221-eee2-410a-964e-6103ce126df4" alt="image.png" width="400">

After:

<img width="400" alt="image.png" src="attachments/2f2d67c4-6080-4ec3-9ae5-df33e6479920">

Also gets rid of a bunch of emojis in the logging and the obsolete link to `nektos/act` and align some other error messages.

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

---------

Co-authored-by: Nicolas <[email protected]>
Reviewed-on: https://gitea.com/gitea/runner/pulls/927
Reviewed-by: Nicolas <[email protected]>
Co-authored-by: silverwind <[email protected]>
Co-committed-by: silverwind <[email protected]>
2026-05-05 20:17:32 +00:00
Nicolasandsilverwind 2a4d56c650 feat: add startup janitor for stale bind-workdir task workspaces (#870)
- Add idle-time cleanup for stale bind-workdir task directories instead of cleaning them on the task execution path.
- Make cleanup behavior configurable with `runner.startup_cleanup_age` as the stale-age threshold (default: `24h`) and `runner.idle_cleanup_interval` as the idle cleanup cadence (default: `10m`).
- Restrict cleanup scope to numeric task directory names only, to avoid touching operator-managed folders.
- Document the cleanup settings in `config.example.yaml` and `README.md`.
- Add tests for stale-directory cleanup, idle cleanup throttling, and config default/override parsing.

## Why

When a runner or host crashes, normal per-task cleanup may not run, leaving stale task directories under the bind-workdir root. Running this cleanup only while the runner is idle recovers that disk space without adding overhead to active job execution.

If you want, I can also tighten the wording around `startup_cleanup_age`, since the key name now reads a bit misleadingly relative to the actual behavior.

---------

Co-authored-by: silverwind <[email protected]>
Reviewed-on: https://gitea.com/gitea/runner/pulls/870
Reviewed-by: silverwind <[email protected]>
2026-05-05 20:11:44 +00:00
a22119cf88 fix(host): correct host workspace cleanup on Windows (#883)
## Summary
- Fix host-mode cleanup to remove the job **workspace** directory after a run (instead of leaving checkouts behind).
- On Windows, track step process PIDs and terminate remaining process trees during teardown before attempting workspace deletion (prevents file-lock failures).
- Skip workspace deletion when `bind_workdir` is enabled to avoid conflicting with runner-level task directory cleanup.

## Implementation details
- `HostEnvironment` now records PIDs for started commands and best-effort terminates them on Windows during `Remove()`.
- Workspace removal uses a small retry loop on Windows to handle transient locks.
- `BindWorkdir` is propagated into `HostEnvironment` so cleanup behavior matches runner configuration.

---------

Co-authored-by: silverwind <[email protected]>
Co-authored-by: silverwind <[email protected]>
Reviewed-on: https://gitea.com/gitea/runner/pulls/883
Reviewed-by: silverwind <[email protected]>
2026-05-05 18:28:12 +00:00
Renovate Bot b68ecf2580 chore(deps): update crazy-max/ghaction-import-gpg action to v7 (#923)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [crazy-max/ghaction-import-gpg](https://github.com/crazy-max/ghaction-import-gpg) | action | major | `v6` → `v7` |

---

### Release Notes

<details>
<summary>crazy-max/ghaction-import-gpg (crazy-max/ghaction-import-gpg)</summary>

### [`v7`](https://github.com/crazy-max/ghaction-import-gpg/compare/v7.0.0...v7.0.0)

[Compare Source](https://github.com/crazy-max/ghaction-import-gpg/compare/v7.0.0...v7.0.0)

### [`v7.0.0`](https://github.com/crazy-max/ghaction-import-gpg/releases/tag/v7.0.0)

[Compare Source](https://github.com/crazy-max/ghaction-import-gpg/compare/v6.3.0...v7.0.0)

- Node 24 as default runtime (requires [Actions Runner v2.327.1](https://github.com/actions/runner/releases/tag/v2.327.1) or later) by [@&#8203;crazy-max](https://github.com/crazy-max) in [#&#8203;241](https://github.com/crazy-max/ghaction-import-gpg/pull/241)
- Switch to ESM and update config/test wiring by [@&#8203;crazy-max](https://github.com/crazy-max) in [#&#8203;239](https://github.com/crazy-max/ghaction-import-gpg/pull/239)
- Bump [@&#8203;actions/core](https://github.com/actions/core) from 1.11.1 to 3.0.0 in [#&#8203;232](https://github.com/crazy-max/ghaction-import-gpg/pull/232)
- Bump [@&#8203;actions/exec](https://github.com/actions/exec) from 1.1.1 to 3.0.0 in [#&#8203;242](https://github.com/crazy-max/ghaction-import-gpg/pull/242)
- Bump brace-expansion from 1.1.11 to 1.1.12 in [#&#8203;221](https://github.com/crazy-max/ghaction-import-gpg/pull/221)
- Bump minimatch from 3.1.2 to 3.1.5 in [#&#8203;240](https://github.com/crazy-max/ghaction-import-gpg/pull/240)
- Bump openpgp from 6.1.0 to 6.3.0 in [#&#8203;233](https://github.com/crazy-max/ghaction-import-gpg/pull/233)

**Full Changelog**: <https://github.com/crazy-max/ghaction-import-gpg/compare/v6.3.0...v7.0.0>

### [`v6.3.0`](https://github.com/crazy-max/ghaction-import-gpg/releases/tag/v6.3.0)

[Compare Source](https://github.com/crazy-max/ghaction-import-gpg/compare/v6.2.0...v6.3.0)

- Bump openpgp from 5.11.2 to 6.1.0 in [#&#8203;215](https://github.com/crazy-max/ghaction-import-gpg/pull/215)
- Bump cross-spawn from 7.0.3 to 7.0.6 in [#&#8203;212](https://github.com/crazy-max/ghaction-import-gpg/pull/212)

**Full Changelog**: <https://github.com/crazy-max/ghaction-import-gpg/compare/v6.2.0...v6.3.0>

### [`v6.2.0`](https://github.com/crazy-max/ghaction-import-gpg/releases/tag/v6.2.0)

[Compare Source](https://github.com/crazy-max/ghaction-import-gpg/compare/v6.1.0...v6.2.0)

- Bump [@&#8203;actions/core](https://github.com/actions/core) from 1.10.1 to 1.11.1 in [#&#8203;209](https://github.com/crazy-max/ghaction-import-gpg/pull/209)
- Bump braces from 3.0.2 to 3.0.3 in [#&#8203;203](https://github.com/crazy-max/ghaction-import-gpg/pull/203)
- Bump ip from 2.0.0 to 2.0.1 in [#&#8203;196](https://github.com/crazy-max/ghaction-import-gpg/pull/196)
- Bump micromatch from 4.0.4 to 4.0.8 in [#&#8203;207](https://github.com/crazy-max/ghaction-import-gpg/pull/207)
- Bump openpgp from 5.11.0 to 5.11.2 in [#&#8203;205](https://github.com/crazy-max/ghaction-import-gpg/pull/205)
- Bump tar from 6.1.14 to 6.2.1 in [#&#8203;198](https://github.com/crazy-max/ghaction-import-gpg/pull/198)

**Full Changelog**: <https://github.com/crazy-max/ghaction-import-gpg/compare/v6.1.0...v6.2.0>

### [`v6.1.0`](https://github.com/crazy-max/ghaction-import-gpg/releases/tag/v6.1.0)

[Compare Source](https://github.com/crazy-max/ghaction-import-gpg/compare/v6...v6.1.0)

- Bump [@&#8203;actions/core](https://github.com/actions/core) from 1.10.0 to 1.10.1 in [#&#8203;186](https://github.com/crazy-max/ghaction-import-gpg/pull/186)
- Bump [@&#8203;babel/traverse](https://github.com/babel/traverse) from 7.17.3 to 7.23.2 in [#&#8203;191](https://github.com/crazy-max/ghaction-import-gpg/pull/191)
- Bump debug from 4.1.1 to 4.3.4 in [#&#8203;190](https://github.com/crazy-max/ghaction-import-gpg/pull/190)
- Bump openpgp from 5.10.1 to 5.11.0 in [#&#8203;192](https://github.com/crazy-max/ghaction-import-gpg/pull/192)

**Full Changelog**: <https://github.com/crazy-max/ghaction-import-gpg/compare/v6.0.0...v6.1.0>

</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:eyJjcmVhdGVkSW5WZXIiOiI0My4xNjAuNiIsInVwZGF0ZWRJblZlciI6IjQzLjE2MC42IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: https://gitea.com/gitea/runner/pulls/923
Reviewed-by: silverwind <[email protected]>
Co-authored-by: Renovate Bot <[email protected]>
Co-committed-by: Renovate Bot <[email protected]>
2026-05-04 12:44:49 +00:00
Renovate Bot d1434237c2 fix(deps): update module golang.org/x/term to v0.42.0 (#920)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) | [`v0.41.0` → `v0.42.0`](https://cs.opensource.google/go/x/term/+/refs/tags/v0.41.0...refs/tags/v0.42.0) | ![age](https://developer.mend.io/api/mc/badges/age/go/golang.org%2fx%2fterm/v0.42.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/golang.org%2fx%2fterm/v0.41.0/v0.42.0?slim=true) |

---

### 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.

---

 - [x] <!-- 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:eyJjcmVhdGVkSW5WZXIiOiI0My4xNjAuNCIsInVwZGF0ZWRJblZlciI6IjQzLjE2MC40IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: https://gitea.com/gitea/runner/pulls/920
Reviewed-by: Nicolas <[email protected]>
Co-authored-by: Renovate Bot <[email protected]>
Co-committed-by: Renovate Bot <[email protected]>
2026-05-03 11:36:03 +00:00
Renovate Bot 35c65e2b14 chore(deps): update actions/hello-world-docker-action action to v2 (#921)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [actions/hello-world-docker-action](https://github.com/actions/hello-world-docker-action) | action | major | `v1` → `v2` |

---

### Release Notes

<details>
<summary>actions/hello-world-docker-action (actions/hello-world-docker-action)</summary>

### [`v2`](https://github.com/actions/hello-world-docker-action/releases/tag/v2): Version v2

[Compare Source](https://github.com/actions/hello-world-docker-action/compare/v1...v2)

Update action to use the new environment file method for setting outputs.

</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:eyJjcmVhdGVkSW5WZXIiOiI0My4xNjAuNCIsInVwZGF0ZWRJblZlciI6IjQzLjE2MC40IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: https://gitea.com/gitea/runner/pulls/921
Reviewed-by: Nicolas <[email protected]>
Co-authored-by: Renovate Bot <[email protected]>
Co-committed-by: Renovate Bot <[email protected]>
2026-05-03 04:44:33 +00:00
Nicolas c45a4e6d32 ci: Fix triggers (#882)
Currently on a branch a workflow got triggered 2x one time on push and one time as its a PR
Now it only gets triggered on PR and on push onto main

Reviewed-on: https://gitea.com/gitea/runner/pulls/882
Reviewed-by: Lunny Xiao <[email protected]>
Co-authored-by: Nicolas <[email protected]>
Co-committed-by: Nicolas <[email protected]>
2026-05-01 16:37:37 +00:00
Renovate BotandNicolas 68d9fc45c9 chore(deps): update dependency @vercel/ncc to ^0.38.0 (#881)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [@vercel/ncc](https://github.com/vercel/ncc) | [`^0.24.1` → `^0.38.0`](https://renovatebot.com/diffs/npm/@vercel%2fncc/0.24.1/0.38.4) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@vercel%2fncc/0.38.4?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@vercel%2fncc/0.24.1/0.38.4?slim=true) |

---

### Release Notes

<details>
<summary>vercel/ncc (@&#8203;vercel/ncc)</summary>

### [`v0.38.4`](https://github.com/vercel/ncc/releases/tag/0.38.4)

[Compare Source](https://github.com/vercel/ncc/compare/0.38.3...0.38.4)

##### Bug Fixes

- **cjs-build:** enable evaluating import.meta in cjs build ([#&#8203;1236](https://github.com/vercel/ncc/issues/1236)) ([e72d34d](https://github.com/vercel/ncc/commit/e72d34d97e7cb2348af19993b6cdb6fec5374ac9)), closes [/github.com/vercel/ncc/pull/897#discussion\_r836916315](https://github.com//github.com/vercel/ncc/pull/897/issues/discussion_r836916315) [#&#8203;1019](https://github.com/vercel/ncc/issues/1019)

### [`v0.38.3`](https://github.com/vercel/ncc/releases/tag/0.38.3)

[Compare Source](https://github.com/vercel/ncc/compare/0.38.2...0.38.3)

##### Bug Fixes

- add missing `--asset-builds` to cli help message ([#&#8203;1228](https://github.com/vercel/ncc/issues/1228)) ([84f8c52](https://github.com/vercel/ncc/commit/84f8c52872621be2fe45d7d837f1e4cc06e8a490))

### [`v0.38.2`](https://github.com/vercel/ncc/releases/tag/0.38.2)

[Compare Source](https://github.com/vercel/ncc/compare/0.38.1...0.38.2)

##### Bug Fixes

- **deps:** update webpack to v5.94.0, terser to v5.33.0 ([#&#8203;1213](https://github.com/vercel/ncc/issues/1213)) ([158a1fd](https://github.com/vercel/ncc/commit/158a1fdcbc32d198a9a0a09d477c9559e7219ed0)), closes [#&#8203;1193](https://github.com/vercel/ncc/issues/1193) [#&#8203;1194](https://github.com/vercel/ncc/issues/1194) [#&#8203;1177](https://github.com/vercel/ncc/issues/1177) [#&#8203;1204](https://github.com/vercel/ncc/issues/1204) [#&#8203;1195](https://github.com/vercel/ncc/issues/1195)

Huge thanks to [@&#8203;theoludwig](https://github.com/theoludwig) 🎉

### [`v0.38.1`](https://github.com/vercel/ncc/releases/tag/0.38.1)

[Compare Source](https://github.com/vercel/ncc/compare/0.38.0...0.38.1)

##### Bug Fixes

- sourcemap sources removes webpack path ([#&#8203;1122](https://github.com/vercel/ncc/issues/1122)) ([ce5984e](https://github.com/vercel/ncc/commit/ce5984e4b07103b4e1123539ebb9fb82daf1344d)), closes [#&#8203;1011](https://github.com/vercel/ncc/issues/1011) [#&#8203;1121](https://github.com/vercel/ncc/issues/1121)

### [`v0.38.0`](https://github.com/vercel/ncc/releases/tag/0.38.0)

[Compare Source](https://github.com/vercel/ncc/compare/0.37.0...0.38.0)

##### Features

- Log minification error when `--debug` ([#&#8203;1102](https://github.com/vercel/ncc/issues/1102)) ([e2779f4](https://github.com/vercel/ncc/commit/e2779f42031569f9b4b47bb8a302f48ef852405b))

### [`v0.37.0`](https://github.com/vercel/ncc/releases/tag/0.37.0)

[Compare Source](https://github.com/vercel/ncc/compare/0.36.1...0.37.0)

##### Features

- add support for TypeScript 5.0's array extends in tsconfig ([#&#8203;1105](https://github.com/vercel/ncc/issues/1105)) ([f898f8e](https://github.com/vercel/ncc/commit/f898f8ea85f940208925dfd2e2162b9de8d4843e))

### [`v0.36.1`](https://github.com/vercel/ncc/releases/tag/0.36.1)

[Compare Source](https://github.com/vercel/ncc/compare/0.36.0...0.36.1)

##### Bug Fixes

- add missing pr title lint action ([#&#8203;1032](https://github.com/vercel/ncc/issues/1032)) ([c2d03cf](https://github.com/vercel/ncc/commit/c2d03cf6dbf24e1f06f9e3f4448b76718a7c44e4))
- add `ncc --version` and `ncc --help` ([#&#8203;1030](https://github.com/vercel/ncc/issues/1030)) ([d38b619](https://github.com/vercel/ncc/commit/d38b61955476b960242a540f26c24a994f241185))

### [`v0.36.0`](https://github.com/vercel/ncc/releases/tag/0.36.0)

[Compare Source](https://github.com/vercel/ncc/compare/0.34.0...0.36.0)

##### Bug Fixes

- gitignore should include release.config.js ([#&#8203;1016](https://github.com/vercel/ncc/issues/1016)) ([44e2eac](https://github.com/vercel/ncc/commit/44e2eac6c966f2f5a2404fcf33a575f2a918ab24))
- node 18 by update source-map used by Terser to 0.7.4 ([#&#8203;999](https://github.com/vercel/ncc/issues/999)) ([2f69f83](https://github.com/vercel/ncc/commit/2f69f838aa7387018dc8760b79ae99e613c45874))

##### Features

- add semantic-release to autopublish ([#&#8203;1015](https://github.com/vercel/ncc/issues/1015)) ([be3405d](https://github.com/vercel/ncc/commit/be3405dbc36c862d2e79f43478b56bc4dd1df200)), closes [#&#8203;1000](https://github.com/vercel/ncc/issues/1000)

### [`v0.34.0`](https://github.com/vercel/ncc/releases/tag/0.34.0)

[Compare Source](https://github.com/vercel/ncc/compare/0.33.4...0.34.0)

##### Changes

Add support for TS 4.7

- Chore(deps-dev): bump ts-loader from 8.3.0 to 9.3.0: [#&#8203;921](https://github.com/vercel/ncc/issues/921)
- Chore(deps-dev): bump express from 4.17.1 to 4.18.1: [#&#8203;917](https://github.com/vercel/ncc/issues/917)
- Chore: add `memory-fs` to the devDependencies: [#&#8203;927](https://github.com/vercel/ncc/issues/927)

##### Credits

Huge thanks to [@&#8203;stscoundrel](https://github.com/stscoundrel) and [@&#8203;shogo82148](https://github.com/shogo82148) for helping!

### [`v0.33.4`](https://github.com/vercel/ncc/releases/tag/0.33.4)

[Compare Source](https://github.com/vercel/ncc/compare/0.33.3...0.33.4)

##### Changes

- Fix: Add missing variable declaration: [#&#8203;773](https://github.com/vercel/ncc/issues/773)
- Chore: add windows to CI: [#&#8203;896](https://github.com/vercel/ncc/issues/896)
- Chore: bump webpack-asset-relocator-loader to 1.7.2: [#&#8203;912](https://github.com/vercel/ncc/issues/912)
- Chore(deps-dev): bump vm2 from 3.9.4 to 3.9.6: [#&#8203;872](https://github.com/vercel/ncc/issues/872)
- Chore(deps): bump url-parse from 1.5.3 to 1.5.7: [#&#8203;875](https://github.com/vercel/ncc/issues/875)
- Chore(deps): bump url-parse from 1.5.7 to 1.5.10: [#&#8203;879](https://github.com/vercel/ncc/issues/879)
- Chore(deps-dev): bump stripe from 8.167.0 to 8.205.0: [#&#8203;882](https://github.com/vercel/ncc/issues/882)
- Chore(deps-dev): bump typescript from 4.4.2 to 4.6.2: [#&#8203;881](https://github.com/vercel/ncc/issues/881)
- Chore(deps-dev): bump twilio from 3.66.1 to 3.75.0: [#&#8203;884](https://github.com/vercel/ncc/issues/884)
- Chore(deps): bump actions/setup-node from 2 to 3: [#&#8203;880](https://github.com/vercel/ncc/issues/880)
- Chore(deps-dev): bump graphql from 15.5.1 to 15.8.0: [#&#8203;885](https://github.com/vercel/ncc/issues/885)
- Chore: replace deprecated String.prototype.substr(): [#&#8203;894](https://github.com/vercel/ncc/issues/894)
- Chore(deps): bump actions/checkout from 2 to 3: [#&#8203;902](https://github.com/vercel/ncc/issues/902)
- Chore(deps-dev): bump [@&#8203;azure/cosmos](https://github.com/azure/cosmos) from 3.12.3 to 3.15.1: [#&#8203;905](https://github.com/vercel/ncc/issues/905)
- Chore(deps-dev): bump stripe from 8.205.0 to 8.214.0: [#&#8203;906](https://github.com/vercel/ncc/issues/906)
- Chore(deps-dev): bump [@&#8203;google-cloud/bigquery](https://github.com/google-cloud/bigquery) from 5.7.0 to 5.12.0: [#&#8203;903](https://github.com/vercel/ncc/issues/903)
- Chore(deps-dev): bump tsconfig-paths from 3.10.1 to 3.14.1: [#&#8203;904](https://github.com/vercel/ncc/issues/904)

##### Credits

Huge thanks to [@&#8203;CommanderRoot](https://github.com/CommanderRoot) for helping!

### [`v0.33.3`](https://github.com/vercel/ncc/releases/tag/0.33.3)

[Compare Source](https://github.com/vercel/ncc/compare/0.33.2...0.33.3)

##### Patches

- Fix: bump license-webpack-plugin: [#&#8203;871](https://github.com/vercel/ncc/issues/871)
- Chore(deps): bump follow-redirects from 1.14.7 to 1.14.8: [#&#8203;870](https://github.com/vercel/ncc/issues/870)

### [`v0.33.2`](https://github.com/vercel/ncc/releases/tag/0.33.2)

[Compare Source](https://github.com/vercel/ncc/compare/0.33.1...0.33.2)

##### Patches

- Fix: use `sha256` instead of deprecated `md5` for hash algorithm: [#&#8203;868](https://github.com/vercel/ncc/issues/868)
- Fix: typo in build script: [#&#8203;835](https://github.com/vercel/ncc/issues/835)
- Chore(test) Add Node.js 16 to CI: [#&#8203;801](https://github.com/vercel/ncc/issues/801)
- Chore(deps): bump nodemailer from 6.5.0 to 6.7.2: [#&#8203;833](https://github.com/vercel/ncc/issues/833)
- Chore(deps-dev): bump terser from 5.7.1 to 5.10.0: [#&#8203;840](https://github.com/vercel/ncc/issues/840)
- Chore(deps-dev): bump passport from 0.4.1 to 0.5.2: [#&#8203;839](https://github.com/vercel/ncc/issues/839)
- Chore(deps-dev): bump sequelize from 6.6.5 to 6.12.4: [#&#8203;843](https://github.com/vercel/ncc/issues/843)
- Chore(deps-dev): bump analytics-node from 5.0.0 to 6.0.0: [#&#8203;838](https://github.com/vercel/ncc/issues/838)
- Chore(deps): bump follow-redirects from 1.14.5 to 1.14.7: [#&#8203;846](https://github.com/vercel/ncc/issues/846)
- Chore(deps): bump cached-path-relative from 1.0.2 to 1.1.0: [#&#8203;854](https://github.com/vercel/ncc/issues/854)
- Chore(deps-dev): bump license-webpack-plugin from 2.3.20 to 4.0.1: [#&#8203;859](https://github.com/vercel/ncc/issues/859)
- Chore(deps): bump simple-get from 3.1.0 to 3.1.1: [#&#8203;864](https://github.com/vercel/ncc/issues/864)
- Chore(deps-dev): bump aws-sdk from 2.1024.0 to 2.1068.0: [#&#8203;867](https://github.com/vercel/ncc/issues/867)

##### Credits

Huge thanks to [@&#8203;shakefu](https://github.com/shakefu) for helping!

### [`v0.33.1`](https://github.com/vercel/ncc/releases/tag/0.33.1)

[Compare Source](https://github.com/vercel/ncc/compare/0.33.0...0.33.1)

##### Patches

- Allow configuring mainFields for nccing browser modules: [#&#8203;832](https://github.com/vercel/ncc/issues/832)

### [`v0.33.0`](https://github.com/vercel/ncc/releases/tag/0.33.0)

[Compare Source](https://github.com/vercel/ncc/compare/0.32.0...0.33.0)

##### Minor Changes

- Chore(deps-dev): bump [@&#8203;vercel/webpack-asset-relocator-loader](https://github.com/vercel/webpack-asset-relocator-loader): [#&#8203;826](https://github.com/vercel/ncc/issues/826)
- Fix: Fix source maps: [#&#8203;818](https://github.com/vercel/ncc/issues/818)
- Feat: Allow using matches from externals for regex matching: [#&#8203;825](https://github.com/vercel/ncc/issues/825)

##### Patches

- Chore(deps-dev): bump koa from 2.13.1 to 2.13.4: [#&#8203;822](https://github.com/vercel/ncc/issues/822)
- Chore(deps-dev): bump mariadb from 2.5.4 to 2.5.5: [#&#8203;823](https://github.com/vercel/ncc/issues/823)

##### Credits

Huge thanks to [@&#8203;fenix20113](https://github.com/fenix20113) for helping!

### [`v0.32.0`](https://github.com/vercel/ncc/releases/tag/0.32.0)

[Compare Source](https://github.com/vercel/ncc/compare/0.31.1...0.32.0)

##### Changes

- Feat: bump to webpack\@&#8203;5.61.0: [#&#8203;809](https://github.com/vercel/ncc/issues/809)
- Docs: add debug command description: [#&#8203;800](https://github.com/vercel/ncc/issues/800)
- Chore(deps): bump object-path from 0.11.7 to 0.11.8: [#&#8203;778](https://github.com/vercel/ncc/issues/778)
- Chore(deps): bump tmpl from 1.0.4 to 1.0.5: [#&#8203;779](https://github.com/vercel/ncc/issues/779)
- Chore(deps-dev): bump vm2 from 3.9.3 to 3.9.4: [#&#8203;795](https://github.com/vercel/ncc/issues/795)
- Chore(deps-dev): bump axios from 0.21.1 to 0.21.2: [#&#8203;810](https://github.com/vercel/ncc/issues/810)
- Chore(deps-dev): bump aws-sdk from 2.958.0 to 2.1024.0: [#&#8203;812](https://github.com/vercel/ncc/issues/812)
- Chore(deps-dev): bump webpack from 5.61.0 to 5.62.1: [#&#8203;813](https://github.com/vercel/ncc/issues/813)
- Chore(deps): bump passport-oauth2 from 1.5.0 to 1.6.1: [#&#8203;811](https://github.com/vercel/ncc/issues/811)
- Chore(deps): bump url-parse from 1.5.1 to 1.5.3: [#&#8203;815](https://github.com/vercel/ncc/issues/815)

##### Credits

Huge thanks to [@&#8203;fireairforce](https://github.com/fireairforce) and [@&#8203;jesec](https://github.com/jesec) for helping!

### [`v0.31.1`](https://github.com/vercel/ncc/releases/tag/0.31.1)

[Compare Source](https://github.com/vercel/ncc/compare/0.31.0...0.31.1)

##### Patches

- Fix `tsconfig.json` detection: [#&#8203;770](https://github.com/vercel/ncc/issues/770)

### [`v0.31.0`](https://github.com/vercel/ncc/releases/tag/0.31.0)

[Compare Source](https://github.com/vercel/ncc/compare/0.30.0...0.31.0)

##### Changes

- Fix `compilerOptions` from `tsconfig.json`: [#&#8203;766](https://github.com/vercel/ncc/issues/766)
- Bump typescript to 4.4.2: [#&#8203;767](https://github.com/vercel/ncc/issues/767)
- Chore(deps-dev): bump graceful-fs from 4.2.6 to 4.2.8: [#&#8203;761](https://github.com/vercel/ncc/issues/761)
- Chore(deps): bump tar from 4.4.15 to 4.4.19: [#&#8203;763](https://github.com/vercel/ncc/issues/763)
- Chore(deps): bump object-path from 0.11.5 to 0.11.7: [#&#8203;764](https://github.com/vercel/ncc/issues/764)

### [`v0.30.0`](https://github.com/vercel/ncc/releases/tag/0.30.0)

[Compare Source](https://github.com/vercel/ncc/compare/0.29.2...0.30.0)

##### Changes

- Major: Change asset builds to opt-in with new option `--asset-builds`: [#&#8203;756](https://github.com/vercel/ncc/issues/756)
- Chore: bump typescript from 3.9.9 to 4.3.5: [#&#8203;739](https://github.com/vercel/ncc/issues/739)
- Chore: bump codecov to 3.8.3: [#&#8203;752](https://github.com/vercel/ncc/issues/752)

##### Description

Previous, `fs.readFile('./asset.js')` would compile `asset.js` instead of including as an asset.

With this release, the default behavior has been changed to include `asset.js` as an asset only.

If you want the old behavior, you can use the `--asset-builds` option.

##### Credits

Huge thanks to [@&#8203;guybedford](https://github.com/guybedford) for helping!

### [`v0.29.2`](https://github.com/vercel/ncc/releases/tag/0.29.2)

[Compare Source](https://github.com/vercel/ncc/compare/0.29.1...0.29.2)

##### Patches

- Fix: ensure nested builds of `__nccwpck_require__`: [#&#8203;751](https://github.com/vercel/ncc/issues/751)

##### Credits

Huge thanks to [@&#8203;guybedford](https://github.com/guybedford) for helping!

### [`v0.29.1`](https://github.com/vercel/ncc/releases/tag/0.29.1)

[Compare Source](https://github.com/vercel/ncc/compare/0.29.0...0.29.1)

##### Patches

- Fix: add stringify-loader: [#&#8203;742](https://github.com/vercel/ncc/issues/742)
- Fix: package.json asset type module setting: [#&#8203;733](https://github.com/vercel/ncc/issues/733)
- Chore(deps): update dependencies: [#&#8203;736](https://github.com/vercel/ncc/issues/736)
- Chore(deps): bump tar from 4.4.13 to 4.4.15: [#&#8203;743](https://github.com/vercel/ncc/issues/743)
- Chore(deps): bump path-parse from 1.0.6 to 1.0.7: [#&#8203;745](https://github.com/vercel/ncc/issues/745)
- Chore(deps-dev): bump pdfkit from 0.12.1 to 0.12.3: [#&#8203;740](https://github.com/vercel/ncc/issues/740)

##### Credits

Huge thanks to [@&#8203;guybedford](https://github.com/guybedford), [@&#8203;mmorel-35](https://github.com/mmorel-35), and [@&#8203;jpcloureiro](https://github.com/jpcloureiro) for helping!

### [`v0.29.0`](https://github.com/vercel/ncc/releases/tag/0.29.0)

[Compare Source](https://github.com/vercel/ncc/compare/0.28.6...0.29.0)

##### Changes

- Major: output ESM for `.mjs` or `type=module` builds: [#&#8203;720](https://github.com/vercel/ncc/issues/720)
- Feat: update to webpack-asset-reloactor-loader\@&#8203;1.5.0: [#&#8203;718](https://github.com/vercel/ncc/issues/718)
- Feat: update to webpack\@&#8203;5.42.0: [#&#8203;723](https://github.com/vercel/ncc/issues/723)
- Feat: update to webpack\@&#8203;5.43.0: [#&#8203;724](https://github.com/vercel/ncc/issues/724)
- Chore: bump set-getter from 0.1.0 to 0.1.1: [#&#8203;719](https://github.com/vercel/ncc/issues/719)
- Fix: typo in readme: [#&#8203;712](https://github.com/vercel/ncc/issues/712)

##### Credits

Huge thanks to [@&#8203;rethab](https://github.com/rethab) and [@&#8203;guybedford](https://github.com/guybedford) for helping!

### [`v0.28.6`](https://github.com/vercel/ncc/releases/tag/0.28.6)

[Compare Source](https://github.com/vercel/ncc/compare/0.28.5...0.28.6)

##### Patches

- Fix: Update to webpack\@&#8203;5.36.2: [#&#8203;707](https://github.com/vercel/ncc/issues/707)
- Fix: Update ts-loader to fix webpack warning: [#&#8203;710](https://github.com/vercel/ncc/issues/710)
- Deps: Bump hosted-git-info from 2.8.8 to 2.8.9: [#&#8203;708](https://github.com/vercel/ncc/issues/708)

##### Credits

Huge thanks to [@&#8203;adriencohen](https://github.com/adriencohen) and [@&#8203;huozhi](https://github.com/huozhi) for helping!

### [`v0.28.5`](https://github.com/vercel/ncc/releases/tag/0.28.5)

[Compare Source](https://github.com/vercel/ncc/compare/0.28.4...0.28.5)

##### Patches

- Fix: handle terser error: [#&#8203;703](https://github.com/vercel/ncc/issues/703)
- Fix: treat compilation.errors as a set: [#&#8203;705](https://github.com/vercel/ncc/issues/705)
- Fix: unify `target` arg description, add `transpile-only` arg to readme: [#&#8203;702](https://github.com/vercel/ncc/issues/702)

##### Credits

Huge thanks to [@&#8203;guybedford](https://github.com/guybedford) and [@&#8203;Simek](https://github.com/Simek) for helping!

### [`v0.28.4`](https://github.com/vercel/ncc/releases/tag/0.28.4)

[Compare Source](https://github.com/vercel/ncc/compare/0.28.3...0.28.4)

##### Patches

- Fix: Adjust caching to use hashes: [#&#8203;698](https://github.com/vercel/ncc/issues/698)
- Fix: support top-level await: [#&#8203;700](https://github.com/vercel/ncc/issues/700)
- Fix: publish should build without cache: [#&#8203;701](https://github.com/vercel/ncc/issues/701)
- Chore:  redis from 2.8.0 to 3.1.1: [#&#8203;699](https://github.com/vercel/ncc/issues/699)
- Chore: Bump ssri from 6.0.1 to 6.0.2: [#&#8203;695](https://github.com/vercel/ncc/issues/695)
- Chore: rename master to main: [#&#8203;694](https://github.com/vercel/ncc/issues/694)

##### Credits

Huge thanks to [@&#8203;guybedford](https://github.com/guybedford) for helping!

### [`v0.28.3`](https://github.com/vercel/ncc/releases/tag/0.28.3)

[Compare Source](https://github.com/vercel/ncc/compare/0.28.2...0.28.3)

##### Patches

- Fix: lock license plugin version: [#&#8203;692](https://github.com/vercel/ncc/issues/692)

##### Credits

Huge thanks to [@&#8203;huozhi](https://github.com/huozhi) for helping!

### [`v0.28.2`](https://github.com/vercel/ncc/releases/tag/0.28.2)

[Compare Source](https://github.com/vercel/ncc/compare/0.28.1...0.28.2)

##### Patches

- Fix: unknown compiler option `incremental`: [#&#8203;685](https://github.com/vercel/ncc/issues/685)
- Fix: replace .npmignore with "files" prop: [#&#8203;688](https://github.com/vercel/ncc/issues/688)

##### Credits

Huge thanks to [@&#8203;Songkeys](https://github.com/Songkeys) for helping!

### [`v0.28.1`](https://github.com/vercel/ncc/releases/tag/0.28.1)

[Compare Source](https://github.com/vercel/ncc/compare/0.28.0...0.28.1)

##### Patches

- Fix: Rebuild bundle to fix [#&#8203;684](https://github.com/vercel/ncc/issues/684)
- Deps: Bump codecov to 3.8.1: [#&#8203;683](https://github.com/vercel/ncc/issues/683)

### [`v0.28.0`](https://github.com/vercel/ncc/releases/tag/0.28.0)

[Compare Source](https://github.com/vercel/ncc/compare/0.27.0...0.28.0)

##### Minor Changes

- Feat: `exports` conditions semantics: [#&#8203;665](https://github.com/vercel/ncc/issues/665)
- Feat: `imports` support, webpack upgrade: [#&#8203;672](https://github.com/vercel/ncc/issues/672)
- Feat: Support Regexp externals: [#&#8203;654](https://github.com/vercel/ncc/issues/654)
- Fix: TS interop test and fix: [#&#8203;671](https://github.com/vercel/ncc/issues/671)
- Fix: Upgrade local TS: [#&#8203;674](https://github.com/vercel/ncc/issues/674)
- Chore: Interop test case: [#&#8203;667](https://github.com/vercel/ncc/issues/667)
- Chore: add console output when minifying fails: [#&#8203;648](https://github.com/vercel/ncc/issues/648)
- Chore: Add Node.js 14 to CI: [#&#8203;659](https://github.com/vercel/ncc/issues/659)
- Docs: Fix out \[file] → out \[dir]: [#&#8203;675](https://github.com/vercel/ncc/issues/675)
- Deps: Update to webpack\@&#8203;5.30.0: [#&#8203;681](https://github.com/vercel/ncc/issues/681)
- Deps: Update to webpack\@&#8203;5.26.3: [#&#8203;664](https://github.com/vercel/ncc/issues/664)
- Deps: Update to webpack\@&#8203;5.24.4: [#&#8203;658](https://github.com/vercel/ncc/issues/658)
- Deps: Update to webpack-asset-relocator-loader\@&#8203;1.3.0: [#&#8203;682](https://github.com/vercel/ncc/issues/682)
- Deps: Upgrade to webpack-asset-relocator-loader\@&#8203;1.2.4: [#&#8203;673](https://github.com/vercel/ncc/issues/673)
- Deps: Update to webpack-asset-relocator\@&#8203;1.2.3: [#&#8203;662](https://github.com/vercel/ncc/issues/662)
- Deps: Upgrade to terser\@&#8203;5.6.1: [#&#8203;669](https://github.com/vercel/ncc/issues/669)
- Deps: Bump socket.io from 2.2.0 to 2.4.0: [#&#8203;645](https://github.com/vercel/ncc/issues/645)
- Deps: Bump pug from 2.0.3 to 3.0.1: [#&#8203;656](https://github.com/vercel/ncc/issues/656)
- Deps: Bump elliptic from 6.5.3 to 6.5.4: [#&#8203;657](https://github.com/vercel/ncc/issues/657)
- Deps: Bump msgpack5 from 4.2.1 to 4.5.1: [#&#8203;660](https://github.com/vercel/ncc/issues/660)
- Deps: Bump y18n from 3.2.1 to 3.2.2: [#&#8203;678](https://github.com/vercel/ncc/issues/678)

##### Credits

Huge thanks to [@&#8203;guybedford](https://github.com/guybedford), [@&#8203;Songkeys](https://github.com/Songkeys), [@&#8203;adriencohen](https://github.com/adriencohen), and [@&#8203;huozhi](https://github.com/huozhi) for helping!

### [`v0.27.0`](https://github.com/vercel/ncc/releases/tag/0.27.0)

[Compare Source](https://github.com/vercel/ncc/compare/0.26.2...0.27.0)

##### Changes

- Feat: support customEmit ncc option: [#&#8203;634](https://github.com/vercel/ncc/issues/634)
- Fix: correct declaration output dir: [#&#8203;627](https://github.com/vercel/ncc/issues/627)
- Update to webpack-asset-relocator\@&#8203;1.2.0: [#&#8203;640](https://github.com/vercel/ncc/issues/640)

##### Credits

Huge thanks to [@&#8203;guybedford](https://github.com/guybedford) and [@&#8203;zeroooooooo](https://github.com/zeroooooooo) for helping!

### [`v0.26.2`](https://github.com/vercel/ncc/releases/tag/0.26.2)

[Compare Source](https://github.com/vercel/ncc/compare/0.26.1...0.26.2)

##### Patches

- Enable minification for sourcemap-register.js: [#&#8203;631](https://github.com/vercel/ncc/issues/631)
- Avoid **webpack\_require** conflicts: [#&#8203;633](https://github.com/vercel/ncc/issues/633)
- Bump axios from 0.18.1 to 0.21.1: [#&#8203;636](https://github.com/vercel/ncc/issues/636)
- Fix: skip typechecking on sub-builds: [#&#8203;637](https://github.com/vercel/ncc/issues/637)

##### Credits

Huge thanks to [@&#8203;xom9ikk](https://github.com/xom9ikk) and [@&#8203;guybedford](https://github.com/guybedford) for helping!

### [`v0.26.1`](https://github.com/vercel/ncc/releases/tag/0.26.1)

[Compare Source](https://github.com/vercel/ncc/compare/0.26.0...0.26.1)

##### Patches

- Ensure separate asset compilation states in subbuilds: [#&#8203;630](https://github.com/vercel/ncc/issues/630)

##### Credits

Huge thanks to [@&#8203;guybedford](https://github.com/guybedford) for helping!

### [`v0.26.0`](https://github.com/vercel/ncc/releases/tag/0.26.0)

[Compare Source](https://github.com/vercel/ncc/compare/0.25.1...0.26.0)

##### Changes

- Asset subbundle builds: [#&#8203;625](https://github.com/vercel/ncc/issues/625)
- Bump ini from 1.3.5 to 1.3.7: [#&#8203;624](https://github.com/vercel/ncc/issues/624)
- Update example with missing TypeScript dependency: [#&#8203;623](https://github.com/vercel/ncc/issues/623)
- Update readme with missing TS and ES options: [#&#8203;615](https://github.com/vercel/ncc/issues/615)

##### Credits

Huge thanks to [@&#8203;restuwahyu13](https://github.com/restuwahyu13) and [@&#8203;guybedford](https://github.com/guybedford) for helping!

### [`v0.25.1`](https://github.com/vercel/ncc/releases/tag/0.25.1)

[Compare Source](https://github.com/vercel/ncc/compare/0.25.0...0.25.1)

##### Changes

- Allow passing `target`: [#&#8203;614](https://github.com/vercel/ncc/issues/614)

##### Credits

Huge thanks to [@&#8203;ijjk](https://github.com/ijjk) for helping!

### [`v0.25.0`](https://github.com/vercel/ncc/releases/tag/0.25.0)

[Compare Source](https://github.com/vercel/ncc/compare/0.24.1...0.25.0)

##### Changes

- Bump `webpack` from `5.0.0-beta.28` to `5.2.0`: [#&#8203;602](https://github.com/vercel/ncc/issues/602)
- Bump `npm-user-validate` from `1.0.0` to `1.0.1`: [#&#8203;604](https://github.com/vercel/ncc/issues/604)
- Bump `object-path` from `0.11.4` to `0.11.5`: [#&#8203;607](https://github.com/vercel/ncc/issues/607)
- Fix `--quiet` flag on ts build: [#&#8203;605](https://github.com/vercel/ncc/issues/605)
- Add integration test for `@slack/web-api`: [#&#8203;591](https://github.com/vercel/ncc/issues/591)

##### Credits

Huge thanks to [@&#8203;huozhi](https://github.com/huozhi) and [@&#8203;ataylorme](https://github.com/ataylorme) for helping!

</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:eyJjcmVhdGVkSW5WZXIiOiI0My4xNTAuMCIsInVwZGF0ZWRJblZlciI6IjQzLjE1MC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

---------

Co-authored-by: Nicolas <[email protected]>
Reviewed-on: https://gitea.com/gitea/runner/pulls/881
Reviewed-by: Nicolas <[email protected]>
Co-authored-by: Renovate Bot <[email protected]>
Co-committed-by: Renovate Bot <[email protected]>
2026-05-01 09:00:34 +00:00
Renovate Bot b1c873a66b chore(deps): update dependency @actions/core to v1.11.1 (#880)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [@actions/core](https://github.com/actions/toolkit/tree/main/packages/core) ([source](https://github.com/actions/toolkit/tree/HEAD/packages/core)) | [`1.10.0` → `1.11.1`](https://renovatebot.com/diffs/npm/@actions%2fcore/1.10.0/1.11.1) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@actions%2fcore/1.11.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@actions%2fcore/1.10.0/1.11.1?slim=true) |

---

### Release Notes

<details>
<summary>actions/toolkit (@&#8203;actions/core)</summary>

### [`v1.11.1`](https://github.com/actions/toolkit/blob/HEAD/packages/core/RELEASES.md#1111)

- Fix uses of `crypto.randomUUID` on Node 18 and earlier [#&#8203;1842](https://github.com/actions/toolkit/pull/1842)

##### 1.11.0

- Add platform info utilities [#&#8203;1551](https://github.com/actions/toolkit/pull/1551)
- Remove dependency on `uuid` package [#&#8203;1824](https://github.com/actions/toolkit/pull/1824)

##### 1.10.1

- Fix error message reference in oidc utils [#&#8203;1511](https://github.com/actions/toolkit/pull/1511)

##### 1.10.0

- `saveState` and `setOutput` now use environment files if available [#&#8203;1178](https://github.com/actions/toolkit/pull/1178)
- `getMultilineInput` now correctly trims whitespace by default [#&#8203;1185](https://github.com/actions/toolkit/pull/1185)

##### 1.9.1

- Randomize delimiter when calling `core.exportVariable`

##### 1.9.0

- Added `toPosixPath`, `toWin32Path` and `toPlatformPath` utilities [#&#8203;1102](https://github.com/actions/toolkit/pull/1102)

##### 1.8.2

- Update to v2.0.1 of `@actions/http-client` [#&#8203;1087](https://github.com/actions/toolkit/pull/1087)

##### 1.8.1

- Update to v2.0.0 of `@actions/http-client`

##### 1.8.0

- Deprecate `markdownSummary` extension export in favor of `summary`
  - [#&#8203;1072](https://github.com/actions/toolkit/pull/1072)
  - [#&#8203;1073](https://github.com/actions/toolkit/pull/1073)

##### 1.7.0

- [Added `markdownSummary` extension](https://github.com/actions/toolkit/pull/1014)

##### 1.6.0

- [Added OIDC Client function `getIDToken`](https://github.com/actions/toolkit/pull/919)
- [Added `file` parameter to `AnnotationProperties`](https://github.com/actions/toolkit/pull/896)

##### 1.5.0

- [Added support for notice annotations and more annotation fields](https://github.com/actions/toolkit/pull/855)

##### 1.4.0

- [Added the `getMultilineInput` function](https://github.com/actions/toolkit/pull/829)

##### 1.3.0

- [Added the trimWhitespace option to getInput](https://github.com/actions/toolkit/pull/802)
- [Added the getBooleanInput function](https://github.com/actions/toolkit/pull/725)

##### 1.2.7

- [Prepend newline for set-output](https://github.com/actions/toolkit/pull/772)

##### 1.2.6

- [Update `exportVariable` and `addPath` to use environment files](https://github.com/actions/toolkit/pull/571)

##### 1.2.5

- [Correctly bundle License File with package](https://github.com/actions/toolkit/pull/548)

##### 1.2.4

- [Be more lenient in accepting non-string command inputs](https://github.com/actions/toolkit/pull/405)
- [Add Echo commands](https://github.com/actions/toolkit/pull/411)

##### 1.2.3

- [IsDebug logging](README.md#logging)

##### 1.2.2

- [Fix escaping for runner commands](https://github.com/actions/toolkit/pull/302)

##### 1.2.1

- [Remove trailing comma from commands](https://github.com/actions/toolkit/pull/263)
- [Add "types" to package.json](https://github.com/actions/toolkit/pull/221)

##### 1.2.0

- saveState and getState functions for wrapper tasks (on finally entry points that run post job)

##### 1.1.3

- setSecret added to register a secret with the runner to be masked from the logs
- exportSecret which was not implemented and never worked was removed after clarification from product.

##### 1.1.1

- Add support for action input variables with multiple spaces [#&#8203;127](https://github.com/actions/toolkit/issues/127)
- Switched ## commands to :: commands (should have no noticeable impact) \[[#&#8203;110](https://github.com/actions/toolkit/issues/110))([#&#8203;110](https://github.com/actions/toolkit/pull/110))

##### 1.1.0

- Added helpers for `group` and `endgroup` [#&#8203;98](https://github.com/actions/toolkit/pull/98)

##### 1.0.0

- Initial release

### [`v1.11.0`](https://github.com/actions/toolkit/blob/HEAD/packages/core/RELEASES.md#1110)

- Add platform info utilities [#&#8203;1551](https://github.com/actions/toolkit/pull/1551)
- Remove dependency on `uuid` package [#&#8203;1824](https://github.com/actions/toolkit/pull/1824)

### [`v1.10.1`](https://github.com/actions/toolkit/blob/HEAD/packages/core/RELEASES.md#1101)

- Fix error message reference in oidc utils [#&#8203;1511](https://github.com/actions/toolkit/pull/1511)

</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:eyJjcmVhdGVkSW5WZXIiOiI0My4xNTAuMCIsInVwZGF0ZWRJblZlciI6IjQzLjE1MC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: https://gitea.com/gitea/runner/pulls/880
Reviewed-by: Nicolas <[email protected]>
Co-authored-by: Renovate Bot <[email protected]>
Co-committed-by: Renovate Bot <[email protected]>
2026-05-01 08:48:49 +00:00
Renovate BotandLunny Xiao 1d6e7879c8 fix(deps): update module github.com/rhysd/actionlint to v1.7.12 (#873)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [github.com/rhysd/actionlint](https://github.com/rhysd/actionlint) | `v1.7.11` → `v1.7.12` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2frhysd%2factionlint/v1.7.12?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2frhysd%2factionlint/v1.7.11/v1.7.12?slim=true) |

---

### Release Notes

<details>
<summary>rhysd/actionlint (github.com/rhysd/actionlint)</summary>

### [`v1.7.12`](https://github.com/rhysd/actionlint/blob/HEAD/CHANGELOG.md#v1712---2026-03-30)

[Compare Source](https://github.com/rhysd/actionlint/compare/v1.7.11...v1.7.12)

- Support the [`timezone` configuration in `on.schedule`](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#onschedule) with checks for IANA timezone string. See the [documentation](https://github.com/rhysd/actionlint/blob/main/docs/checks.md#check-cron-syntax-and-timezone) for more details. Note that actionlint starts to embed the timezone database in the executables from this version so the binary sizes slightly increase. ([#&#8203;641](https://github.com/rhysd/actionlint/issues/641), thanks [@&#8203;martincostello](https://github.com/martincostello))
  ```yaml
  on:
    schedule:
      # ERROR: The timezone is not a valid IANA timezone string
      - cron: '*/5 * * * *'
        timezone: 'Asia/Somewhere'
  ```
- Support the [`jobs.<job_name>.environment.deployment` configuration](https://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/control-deployments#using-environments-without-deployments). ([#&#8203;639](https://github.com/rhysd/actionlint/issues/639), thanks [@&#8203;springmeyer](https://github.com/springmeyer))
- Support the [`macos-26-intel` runner label](https://github.blog/changelog/2026-02-26-macos-26-is-now-generally-available-for-github-hosted-runners/). ([#&#8203;629](https://github.com/rhysd/actionlint/issues/629), thanks [@&#8203;hugovk](https://github.com/hugovk))
- Fix the [table of webhook activity types](https://github.com/rhysd/actionlint/blob/main/all_webhooks.go) are outdated by rebuilding the [script to scrape the table](https://github.com/rhysd/actionlint/tree/main/scripts/generate-webhook-events) from scratch.
- Support Go 1.26 and drop the support for Go 1.24. Now supported versions are 1.25 and 1.26.
- Tests are run on arm64 Windows in CI.
- Update the popular actions data set to the latest.

\[Changes]\[v1.7.12]

<a id="v1.7.11"></a>

</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:eyJjcmVhdGVkSW5WZXIiOiI0My4xNTAuMCIsInVwZGF0ZWRJblZlciI6IjQzLjE1MC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

---------

Co-authored-by: Lunny Xiao <[email protected]>
Reviewed-on: https://gitea.com/gitea/runner/pulls/873
Reviewed-by: Lunny Xiao <[email protected]>
Co-authored-by: Renovate Bot <[email protected]>
Co-committed-by: Renovate Bot <[email protected]>
2026-04-30 21:03:33 +00:00
Lunny Xiaoandsilverwind 13dc9386fe Rename act_runner to runner (#850)
## Consumer-facing breaking changes

- **Go module path**: `gitea.com/gitea/act_runner` → `gitea.com/gitea/runner`. Anything importing `act/...` or `internal/...` packages (notably Gitea itself) must update imports.
- **Binary name**: `act_runner` → `gitea-runner`. Wrapper scripts, systemd units, init scripts, and documentation referencing the binary by `act_runner` will break.
- **Docker image**: `gitea/act_runner` → `gitea/runner` (incl. `*-dind-rootless` variants). Users pulling `gitea/act_runner:nightly` etc. will get stale images. Note: the image name is `gitea/runner`, not `gitea/gitea-runner`.
- **Release artifact paths**: S3 directory `act_runner/{{.Version}}` → `gitea-runner/{{.Version}}`, and artifact filenames change with the new project name. Existing download URLs break.
- **Metrics namespace**: changed from `act_runner` to `gitea_runner` (e.g. `act_runner_jobs_total` → `gitea_runner_jobs_total`); existing monitors/dashboards must be updated.
- **ldflags version path**: `gitea.com/gitea/act_runner/internal/pkg/ver.version` → `gitea.com/gitea/runner/internal/pkg/ver.version`. Affects anyone building with custom ldflags.
- **Kubernetes example resource names**: `act-runner` / `act-runner-vol` → `runner` / `runner-vol`. Users who copied the manifests verbatim will see resource churn on apply.
- **s6 service name**: `scripts/s6/act_runner/` → `scripts/s6/gitea-runner/` (image-internal; only matters for downstream image overrides).

Unchanged: YAML config field names, env vars (`GITEA_*`), CLI flags/subcommands, registration file format.
---------

Co-authored-by: silverwind <[email protected]>
Reviewed-on: https://gitea.com/gitea/runner/pulls/850
Reviewed-by: Zettat123 <[email protected]>
Reviewed-by: silverwind <[email protected]>
Reviewed-by: Nicolas <[email protected]>
Co-authored-by: Lunny Xiao <[email protected]>
Co-committed-by: Lunny Xiao <[email protected]>
2026-04-30 20:12:51 +00:00
NicolasandLunny Xiao 8e6b3be96a docs: Update docs (#872)
Co-authored-by: Lunny Xiao <[email protected]>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/872
Reviewed-by: Lunny Xiao <[email protected]>
Co-authored-by: Nicolas <[email protected]>
Co-committed-by: Nicolas <[email protected]>
2026-04-30 18:29:59 +00:00
1190 changed files with 3461 additions and 261811 deletions

No files matched your search

+1 -1
View File
@@ -40,7 +40,7 @@ cpu.out
*.db *.db
*.log *.log
/act_runner /gitea-runner
/debug /debug
/bin /bin
+14 -7
View File
@@ -24,7 +24,7 @@ jobs:
with: with:
go-version-file: "go.mod" go-version-file: "go.mod"
- name: goreleaser - name: goreleaser
uses: goreleaser/goreleaser-action@v6 uses: goreleaser/goreleaser-action@v7
with: with:
distribution: goreleaser-pro distribution: goreleaser-pro
args: release --nightly args: release --nightly
@@ -57,22 +57,27 @@ jobs:
fetch-depth: 0 # all history for all branches and tags fetch-depth: 0 # all history for all branches and tags
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v4
- name: Set up Docker BuildX - name: Set up Docker BuildX
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v4
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Echo the tag - name: Echo the tag
run: echo "${{ env.DOCKER_ORG }}/act_runner:nightly${{ matrix.variant.tag_suffix }}" run: echo "${{ env.DOCKER_ORG }}/runner:nightly${{ matrix.variant.tag_suffix }}"
- name: Get Meta
id: meta
run: |
echo REPO_VERSION=$(git describe --tags --always | sed 's/-/+/' | sed 's/^v//') >> $GITHUB_OUTPUT
- name: Build and push - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v7
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
@@ -82,4 +87,6 @@ jobs:
linux/arm64 linux/arm64
push: true push: true
tags: | tags: |
${{ env.DOCKER_ORG }}/act_runner:nightly${{ matrix.variant.tag_suffix }} ${{ env.DOCKER_ORG }}/runner:nightly${{ matrix.variant.tag_suffix }}
build-args: |
VERSION=${{ steps.meta.outputs.REPO_VERSION }}
+10 -13
View File
@@ -17,13 +17,13 @@ jobs:
go-version-file: "go.mod" go-version-file: "go.mod"
- name: Import GPG key - name: Import GPG key
id: import_gpg id: import_gpg
uses: crazy-max/ghaction-import-gpg@v6 uses: crazy-max/ghaction-import-gpg@v7
with: with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.PASSPHRASE }} passphrase: ${{ secrets.PASSPHRASE }}
fingerprint: CC64B1DB67ABBEECAB24B6455FC346329753F4B0 fingerprint: CC64B1DB67ABBEECAB24B6455FC346329753F4B0
- name: goreleaser - name: goreleaser
uses: goreleaser/goreleaser-action@v6 uses: goreleaser/goreleaser-action@v7
with: with:
distribution: goreleaser-pro distribution: goreleaser-pro
args: release args: release
@@ -60,28 +60,23 @@ jobs:
fetch-depth: 0 # all history for all branches and tags fetch-depth: 0 # all history for all branches and tags
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v4
- name: Set up Docker BuildX - name: Set up Docker BuildX
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v4
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Repo Meta
id: repo_meta
run: |
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
- name: "Docker meta" - name: "Docker meta"
id: docker_meta id: docker_meta
uses: https://github.com/docker/metadata-action@v5 uses: docker/metadata-action@v6
with: with:
images: | images: |
${{ env.DOCKER_ORG }}/${{ steps.repo_meta.outputs.REPO_NAME }} ${{ env.DOCKER_ORG }}/runner
tags: | tags: |
type=semver,pattern={{major}}.{{minor}}.{{patch}} type=semver,pattern={{major}}.{{minor}}.{{patch}}
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
@@ -91,7 +86,7 @@ jobs:
suffix=${{ matrix.variant.tag_suffix }},onlatest=true suffix=${{ matrix.variant.tag_suffix }},onlatest=true
- name: Build and push - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v7
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
@@ -101,3 +96,5 @@ jobs:
linux/arm64 linux/arm64
push: true push: true
tags: ${{ steps.docker_meta.outputs.tags }} tags: ${{ steps.docker_meta.outputs.tags }}
build-args: |
VERSION=${{ steps.docker_meta.outputs.version }}
+4 -2
View File
@@ -1,7 +1,9 @@
name: checks name: checks
on: on:
- push push:
- pull_request branches:
- main
pull_request:
jobs: jobs:
lint: lint:
+1 -1
View File
@@ -1,4 +1,4 @@
/act_runner /gitea-runner
.env .env
.runner .runner
coverage.txt coverage.txt
+1 -1
View File
@@ -114,7 +114,7 @@ formatters:
custom-order: true custom-order: true
sections: sections:
- standard - standard
- prefix(gitea.com/gitea/act_runner) - prefix(gitea.com/gitea/runner)
- blank - blank
- default - default
gofumpt: gofumpt:
+4 -2
View File
@@ -1,5 +1,7 @@
version: 2 version: 2
project_name: gitea-runner
before: before:
hooks: hooks:
- go mod tidy - go mod tidy
@@ -63,7 +65,7 @@ builds:
flags: flags:
- -trimpath - -trimpath
ldflags: ldflags:
- -s -w -X gitea.com/gitea/act_runner/internal/pkg/ver.version={{ .Summary }} - -s -w -X gitea.com/gitea/runner/internal/pkg/ver.version={{ .Summary }}
binary: >- binary: >-
{{ .ProjectName }}- {{ .ProjectName }}-
{{- .Version }}- {{- .Version }}-
@@ -86,7 +88,7 @@ blobs:
provider: s3 provider: s3
bucket: "{{ .Env.S3_BUCKET }}" bucket: "{{ .Env.S3_BUCKET }}"
region: "{{ .Env.S3_REGION }}" region: "{{ .Env.S3_REGION }}"
directory: "act_runner/{{.Version}}" directory: "gitea-runner/{{.Version}}"
extra_files: extra_files:
- glob: ./**.xz - glob: ./**.xz
- glob: ./**.sha256 - glob: ./**.sha256
+23 -7
View File
@@ -9,19 +9,24 @@ RUN apk add --no-cache make git
ARG GOPROXY ARG GOPROXY
ENV GOPROXY=${GOPROXY:-} ENV GOPROXY=${GOPROXY:-}
COPY . /opt/src/act_runner COPY . /opt/src/runner
WORKDIR /opt/src/act_runner WORKDIR /opt/src/runner
RUN make clean && make build RUN make clean && make build
### DIND VARIANT ### DIND VARIANT
# #
# #
FROM docker:28-dind AS dind FROM docker:29-dind AS dind
ARG VERSION=dev
LABEL org.opencontainers.image.source="https://gitea.com/gitea/runner"
LABEL org.opencontainers.image.version="${VERSION}"
RUN apk add --no-cache s6 bash git tzdata RUN apk add --no-cache s6 bash git tzdata
COPY --from=builder /opt/src/act_runner/act_runner /usr/local/bin/act_runner COPY --from=builder /opt/src/runner/gitea-runner /usr/local/bin/gitea-runner
COPY scripts/run.sh /usr/local/bin/run.sh COPY scripts/run.sh /usr/local/bin/run.sh
COPY scripts/s6 /etc/s6 COPY scripts/s6 /etc/s6
@@ -32,12 +37,17 @@ ENTRYPOINT ["s6-svscan","/etc/s6"]
### DIND-ROOTLESS VARIANT ### DIND-ROOTLESS VARIANT
# #
# #
FROM docker:28-dind-rootless AS dind-rootless FROM docker:29-dind-rootless AS dind-rootless
ARG VERSION=dev
LABEL org.opencontainers.image.source="https://gitea.com/gitea/runner"
LABEL org.opencontainers.image.version="${VERSION}"
USER root USER root
RUN apk add --no-cache s6 bash git tzdata RUN apk add --no-cache s6 bash git tzdata
COPY --from=builder /opt/src/act_runner/act_runner /usr/local/bin/act_runner COPY --from=builder /opt/src/runner/gitea-runner /usr/local/bin/gitea-runner
COPY scripts/run.sh /usr/local/bin/run.sh COPY scripts/run.sh /usr/local/bin/run.sh
COPY scripts/s6 /etc/s6 COPY scripts/s6 /etc/s6
@@ -54,9 +64,15 @@ ENTRYPOINT ["s6-svscan","/etc/s6"]
# #
# #
FROM alpine AS basic FROM alpine AS basic
ARG VERSION=dev
LABEL org.opencontainers.image.source="https://gitea.com/gitea/runner"
LABEL org.opencontainers.image.version="${VERSION}"
RUN apk add --no-cache tini bash git tzdata RUN apk add --no-cache tini bash git tzdata
COPY --from=builder /opt/src/act_runner/act_runner /usr/local/bin/act_runner COPY --from=builder /opt/src/runner/gitea-runner /usr/local/bin/gitea-runner
COPY scripts/run.sh /usr/local/bin/run.sh COPY scripts/run.sh /usr/local/bin/run.sh
VOLUME /data VOLUME /data
+8 -8
View File
@@ -1,5 +1,5 @@
DIST := dist DIST := dist
EXECUTABLE := act_runner EXECUTABLE := gitea-runner
DIST_DIRS := $(DIST)/binaries $(DIST)/release DIST_DIRS := $(DIST)/binaries $(DIST)/release
GO ?= go GO ?= go
SHASUM ?= shasum -a 256 SHASUM ?= shasum -a 256
@@ -13,13 +13,13 @@ DARWIN_ARCHS ?= darwin-12/amd64,darwin-12/arm64
WINDOWS_ARCHS ?= windows/amd64 WINDOWS_ARCHS ?= windows/amd64
GOFILES := $(shell find . -type f -name "*.go" -o -name "go.mod" ! -name "generated.*") GOFILES := $(shell find . -type f -name "*.go" -o -name "go.mod" ! -name "generated.*")
DOCKER_IMAGE ?= gitea/act_runner DOCKER_IMAGE ?= gitea/runner
DOCKER_TAG ?= nightly DOCKER_TAG ?= nightly
DOCKER_REF := $(DOCKER_IMAGE):$(DOCKER_TAG) DOCKER_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)
DOCKER_ROOTLESS_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)-dind-rootless DOCKER_ROOTLESS_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)-dind-rootless
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/[email protected]1.4 GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/[email protected]2.2
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.3.0
STATIC ?= STATIC ?=
EXTLDFLAGS ?= EXTLDFLAGS ?=
@@ -67,7 +67,7 @@ else
endif endif
TAGS ?= TAGS ?=
LDFLAGS ?= -X "gitea.com/gitea/act_runner/internal/pkg/ver.version=v$(RELASE_VERSION)" LDFLAGS ?= -X "gitea.com/gitea/runner/internal/pkg/ver.version=v$(RELASE_VERSION)"
.PHONY: all .PHONY: all
all: build all: build
@@ -86,7 +86,7 @@ go-check:
$(eval MIN_GO_VERSION := $(shell printf "%03d%03d" $(shell echo '$(MIN_GO_VERSION_STR)' | tr '.' ' '))) $(eval MIN_GO_VERSION := $(shell printf "%03d%03d" $(shell echo '$(MIN_GO_VERSION_STR)' | tr '.' ' ')))
$(eval GO_VERSION := $(shell printf "%03d%03d" $(shell $(GO) version | grep -Eo '[0-9]+\.[0-9]+' | tr '.' ' ');)) $(eval GO_VERSION := $(shell printf "%03d%03d" $(shell $(GO) version | grep -Eo '[0-9]+\.[0-9]+' | tr '.' ' ');))
@if [ "$(GO_VERSION)" -lt "$(MIN_GO_VERSION)" ]; then \ @if [ "$(GO_VERSION)" -lt "$(MIN_GO_VERSION)" ]; then \
echo "Act Runner requires Go $(MIN_GO_VERSION_STR) or greater to build. You can get it at https://go.dev/dl/"; \ echo "Gitea Runner requires Go $(MIN_GO_VERSION_STR) or greater to build. You can get it at https://go.dev/dl/"; \
exit 1; \ exit 1; \
fi fi
@@ -140,11 +140,11 @@ test: fmt-check security-check ## test everything
@$(GO) test -race -short -v -cover -coverprofile coverage.txt ./... && echo "\n==>\033[32m Ok\033[m\n" || exit 1 @$(GO) test -race -short -v -cover -coverprofile coverage.txt ./... && echo "\n==>\033[32m Ok\033[m\n" || exit 1
.PHONY: install .PHONY: install
install: $(GOFILES) ## install the act_runner binary via `go install` install: $(GOFILES) ## install the runner binary via `go install`
$(GO) install -v -tags '$(TAGS)' -ldflags '-s -w $(EXTLDFLAGS) $(LDFLAGS)' $(GO) install -v -tags '$(TAGS)' -ldflags '-s -w $(EXTLDFLAGS) $(LDFLAGS)'
.PHONY: build .PHONY: build
build: go-check $(EXECUTABLE) ## build the act_runner binary build: go-check $(EXECUTABLE) ## build the runner binary
$(EXECUTABLE): $(GOFILES) $(EXECUTABLE): $(GOFILES)
$(GO) build -v -tags '$(TAGS)' -ldflags '-s -w $(EXTLDFLAGS) $(LDFLAGS)' -o $@ $(GO) build -v -tags '$(TAGS)' -ldflags '-s -w $(EXTLDFLAGS) $(LDFLAGS)' -o $@
+50 -15
View File
@@ -1,6 +1,4 @@
# act runner # Gitea Runner
Act runner is a runner for Gitea.
## Installation ## Installation
@@ -10,7 +8,7 @@ Docker Engine Community version is required for docker mode. To install Docker C
### Download pre-built binary ### Download pre-built binary
Visit [here](https://dl.gitea.com/act_runner/) and download the right version for your platform. Visit [here](https://dl.gitea.com/gitea-runner/) and download the right version for your platform.
### Build from source ### Build from source
@@ -36,7 +34,7 @@ ENABLED=true
### Register ### Register
```bash ```bash
./act_runner register ./gitea-runner register
``` ```
And you will be asked to input: And you will be asked to input:
@@ -68,7 +66,7 @@ INFO Runner registered successfully.
You can also register with command line arguments. You can also register with command line arguments.
```bash ```bash
./act_runner register --instance http://192.168.8.8:3000 --token <my_runner_token> --no-interactive ./gitea-runner register --instance http://192.168.8.8:3000 --token <my_runner_token> --no-interactive
``` ```
If the registry succeed, it will run immediately. Next time, you could run the runner directly. If the registry succeed, it will run immediately. Next time, you could run the runner directly.
@@ -76,32 +74,69 @@ If the registry succeed, it will run immediately. Next time, you could run the r
### Run ### Run
```bash ```bash
./act_runner daemon ./gitea-runner daemon
``` ```
### Run with docker ### Run with docker
```bash ```bash
docker run -e GITEA_INSTANCE_URL=https://your_gitea.com -e GITEA_RUNNER_REGISTRATION_TOKEN=<your_token> -v /var/run/docker.sock:/var/run/docker.sock --name my_runner gitea/act_runner:nightly docker run -e GITEA_INSTANCE_URL=https://your_gitea.com -e GITEA_RUNNER_REGISTRATION_TOKEN=<your_token> -v /var/run/docker.sock:/var/run/docker.sock --name my_runner gitea/runner:nightly
``` ```
Mount a volume on `/data` if you want the registration file and optional config to survive container recreation (see [scripts/run.sh](scripts/run.sh)).
### Configuration ### Configuration
You can also configure the runner with a configuration file. The runner is configured with a YAML file. Generate a starting point (this matches what ships in the tree):
The configuration file is a YAML file, you can generate a sample configuration file with `./act_runner generate-config`.
```bash ```bash
./act_runner generate-config > config.yaml ./gitea-runner generate-config > config.yaml
``` ```
You can specify the configuration file path with `-c`/`--config` argument. Pass it with `-c` / `--config` on any command that loads configuration (`register`, `daemon`, `cache-server`):
```bash ```bash
./act_runner -c config.yaml register # register with config file ./gitea-runner -c config.yaml register
./act_runner -c config.yaml daemon # run with config file ./gitea-runner -c config.yaml daemon
./gitea-runner -c config.yaml cache-server
``` ```
You can read the latest version of the configuration file online at [config.example.yaml](internal/pkg/config/config.example.yaml). Every option is described in [config.example.yaml](internal/pkg/config/config.example.yaml) (the same content `generate-config` prints).
#### Without a config file
If you omit `-c`, built-in defaults apply (same as an empty YAML document). A small set of **deprecated** environment variables can still override parts of that default config, but **only when no `-c` path was given**; they are ignored if you use a config file:
| Variable | Effect |
| --- | --- |
| `GITEA_DEBUG` | If true, sets log level to `debug` |
| `GITEA_TRACE` | If true, sets log level to `trace` |
| `GITEA_RUNNER_CAPACITY` | Concurrent jobs (integer) |
| `GITEA_RUNNER_FILE` | Registration state file path (default `.runner`) |
| `GITEA_RUNNER_ENVIRON` | Extra job env vars as comma-separated `KEY:VALUE` pairs |
| `GITEA_RUNNER_ENV_FILE` | Path to an env file merged into job env (same idea as `runner.env_file` in YAML) |
Prefer a YAML file for all settings.
#### Registration vs config labels
If `runner.labels` is set in the YAML file, those labels are used during `register` and the `--labels` CLI flag is ignored.
#### External cache (`actions/cache`)
If `cache.external_server` is set, you must set `cache.external_secret` to the same value on this runner and on the standalone cache server. Run the server with `gitea-runner cache-server` using a config that defines `cache.external_secret` (and matching `cache.dir` / host / port as needed). Flags `--dir`, `--host`, and `--port` on `cache-server` override the file.
#### Official Docker image
Besides `GITEA_INSTANCE_URL` and `GITEA_RUNNER_REGISTRATION_TOKEN`, the image entrypoint supports optional variables such as `CONFIG_FILE` (passed through as `-c`), `GITEA_RUNNER_LABELS`, `GITEA_RUNNER_EPHEMERAL`, `GITEA_RUNNER_ONCE`, `GITEA_RUNNER_NAME`, `GITEA_MAX_REG_ATTEMPTS`, `RUNNER_STATE_FILE`, and `GITEA_RUNNER_REGISTRATION_TOKEN_FILE`. See [scripts/run.sh](scripts/run.sh) for exact behavior.
For a fuller container-oriented walkthrough, see [examples/docker](examples/docker/README.md).
When `container.bind_workdir` is enabled, stale task workspace directories can be cleaned while the runner is idle:
- directories older than `runner.workdir_cleanup_age` are removed (default: `24h`; set `0` to disable)
- cleanup runs every `runner.idle_cleanup_interval` (default: `10m`; set `0` to disable)
- only purely numeric subdirectories under `container.workdir_parent` are treated as task workspaces and may be removed
- cleanup assumes `container.workdir_parent` is not shared across multiple runners
### Example Deployments ### Example Deployments
+2 -7
View File
@@ -26,7 +26,7 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"gitea.com/gitea/act_runner/act/common" "gitea.com/gitea/runner/act/common"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@@ -325,10 +325,6 @@ func (h *Handler) openDB() (*bolthold.Store, error) {
func (h *Handler) find(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { func (h *Handler) find(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
cred := credFromContext(r.Context()) cred := credFromContext(r.Context())
keys := strings.Split(r.URL.Query().Get("keys"), ",") keys := strings.Split(r.URL.Query().Get("keys"), ",")
// cache keys are case insensitive
for i, key := range keys {
keys[i] = strings.ToLower(key)
}
version := r.URL.Query().Get("version") version := r.URL.Query().Get("version")
db, err := h.openDB() db, err := h.openDB()
@@ -371,8 +367,6 @@ func (h *Handler) reserve(w http.ResponseWriter, r *http.Request, _ httprouter.P
h.responseJSON(w, r, 400, err) h.responseJSON(w, r, 400, err)
return return
} }
// cache keys are case insensitive
api.Key = strings.ToLower(api.Key)
cache := api.ToCache() cache := api.ToCache()
cache.Repo = cred.Repo cache.Repo = cred.Repo
@@ -437,6 +431,7 @@ func (h *Handler) upload(w http.ResponseWriter, r *http.Request, params httprout
} }
if err := h.storage.Write(cache.ID, start, r.Body); err != nil { if err := h.storage.Write(cache.ID, start, r.Body); err != nil {
h.responseJSON(w, r, 500, err) h.responseJSON(w, r, 500, err)
return
} }
h.useCache(id) h.useCache(id)
h.responseJSON(w, r, 200) h.responseJSON(w, r, 200)
+61 -8
View File
@@ -11,6 +11,7 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"os"
"path/filepath" "path/filepath"
"strings" "strings"
"testing" "testing"
@@ -338,6 +339,54 @@ func TestHandler(t *testing.T) {
} }
}) })
t.Run("upload write failure returns only error", func(t *testing.T) {
key := strings.ToLower(t.Name())
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
var id uint64
{
body, err := json.Marshal(&Request{
Key: key,
Version: version,
Size: 100,
})
require.NoError(t, err)
resp, err := testClient.Post(base+"/caches", "application/json", bytes.NewReader(body))
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, 200, resp.StatusCode)
got := struct {
CacheID uint64 `json:"cacheId"`
}{}
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
id = got.CacheID
}
storageFile := filepath.Join(dir, "not-a-directory")
require.NoError(t, os.WriteFile(storageFile, []byte("blocked"), 0o600))
originalStorage := handler.storage
handler.storage = &Storage{rootDir: storageFile}
defer func() {
handler.storage = originalStorage
}()
req, err := http.NewRequest(http.MethodPatch,
fmt.Sprintf("%s/caches/%d", base, id), bytes.NewReader(make([]byte, 100)))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("Content-Range", "bytes 0-99/*")
resp, err := testClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, 500, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
var got map[string]string
require.NoError(t, json.Unmarshal(body, &got))
assert.NotEmpty(t, got["error"])
})
t.Run("commit early", func(t *testing.T) { t.Run("commit early", func(t *testing.T) {
key := strings.ToLower(t.Name()) key := strings.ToLower(t.Name())
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20" version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
@@ -459,17 +508,20 @@ func TestHandler(t *testing.T) {
assert.Equal(t, contents[except], content) assert.Equal(t, contents[except], content)
}) })
t.Run("case insensitive", func(t *testing.T) { t.Run("case preserved", func(t *testing.T) {
// Some actions (e.g. actions/setup-go, actions/setup-node) build cache keys that contain mixed-case fragments such as RUNNER_OS=Linux,
// then compare the cacheKey returned by the cache server to their original key with case-sensitive equality to decide whether the
// cache was a complete hit. The server must therefore preserve the original key case.
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20" version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
key := strings.ToLower(t.Name()) key := strings.ToLower(t.Name()) + "_ABC"
content := make([]byte, 100) content := make([]byte, 100)
_, err := rand.Read(content) _, err := rand.Read(content)
require.NoError(t, err) require.NoError(t, err)
uploadCacheNormally(t, base, key+"_ABC", version, content) uploadCacheNormally(t, base, key, version, content)
{ {
reqKey := key + "_aBc" resp, err := testClient.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, key, version))
resp, err := testClient.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKey, version))
require.NoError(t, err) require.NoError(t, err)
defer resp.Body.Close() defer resp.Body.Close()
require.Equal(t, 200, resp.StatusCode) require.Equal(t, 200, resp.StatusCode)
@@ -480,7 +532,8 @@ 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, "hit", got.Result) assert.Equal(t, "hit", got.Result)
assert.Equal(t, key+"_abc", got.CacheKey) assert.Equal(t, key, got.CacheKey)
assert.NotEqual(t, strings.ToLower(key), got.CacheKey)
} }
}) })
@@ -643,7 +696,7 @@ func uploadCacheNormally(t *testing.T, base, key, version string, content []byte
}{} }{}
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got)) require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
assert.Equal(t, "hit", got.Result) assert.Equal(t, "hit", got.Result)
assert.Equal(t, strings.ToLower(key), got.CacheKey) assert.Equal(t, key, got.CacheKey)
archiveLocation = got.ArchiveLocation archiveLocation = got.ArchiveLocation
} }
{ {
@@ -969,7 +1022,7 @@ func TestHandler_ArtifactSignature(t *testing.T) {
} }
// TestHandler_SecretPersistsAcrossRestarts is the property that lets // TestHandler_SecretPersistsAcrossRestarts is the property that lets
// act_runner cache-server be pointed at via cfg.Cache.ExternalServer: a // gitea-runner cache-server be pointed at via cfg.Cache.ExternalServer: a
// restart must not invalidate signed URLs the handler has already issued // restart must not invalidate signed URLs the handler has already issued
// (within their expiry window). // (within their expiry window).
func TestHandler_SecretPersistsAcrossRestarts(t *testing.T) { func TestHandler_SecretPersistsAcrossRestarts(t *testing.T) {
-30
View File
@@ -1,30 +0,0 @@
# Copied from https://github.com/actions/cache#example-cache-workflow
name: Caching Primes
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: env
- uses: actions/checkout@v3
- name: Cache Primes
id: cache-primes
uses: actions/cache@v3
with:
path: prime-numbers
key: ${{ runner.os }}-primes-${{ github.run_id }}
restore-keys: |
${{ runner.os }}-primes
${{ runner.os }}
- name: Generate Prime Numbers
if: steps.cache-primes.outputs.cache-hit != 'true'
run: cat /proc/sys/kernel/random/uuid > prime-numbers
- name: Use Prime Numbers
run: cat prime-numbers
+1 -1
View File
@@ -17,7 +17,7 @@ import (
"strings" "strings"
"time" "time"
"gitea.com/gitea/act_runner/act/common" "gitea.com/gitea/runner/act/common"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
) )
+3 -3
View File
@@ -17,8 +17,8 @@ import (
"testing" "testing"
"testing/fstest" "testing/fstest"
"gitea.com/gitea/act_runner/act/model" "gitea.com/gitea/runner/act/model"
"gitea.com/gitea/act_runner/act/runner" "gitea.com/gitea/runner/act/runner"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@@ -260,7 +260,7 @@ func TestArtifactFlow(t *testing.T) {
defer cancel() defer cancel()
platforms := map[string]string{ platforms := map[string]string{
"ubuntu-latest": "node:16-buster", // Don't use node:16-buster-slim because it doesn't have curl command, which is used in the tests "ubuntu-latest": "node:24-bookworm", // Don't use node:24-bookworm-slim because it doesn't have curl command, which is used in the tests
} }
tables := []TestJobFileInfo{ tables := []TestJobFileInfo{
+3 -1
View File
@@ -4,6 +4,8 @@
package common package common
import "slices"
// 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][]any) []map[string]any { func CartesianProduct(mapOfLists map[string][]any) []map[string]any {
listNames := make([]string, 0) listNames := make([]string, 0)
@@ -46,7 +48,7 @@ func cartN(a ...[]any) [][]any {
for j, n := range n { for j, n := range n {
pi[j] = a[j][n] pi[j] = a[j][n]
} }
for j := len(n) - 1; j >= 0; j-- { for j := range slices.Backward(n) {
n[j]++ n[j]++
if n[j] < len(a[j]) { if n[j] < len(a[j]) {
break break
-146
View File
@@ -1,146 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// Copyright 2020 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package common
import (
"fmt"
"io"
"os"
"strings"
)
// Style is a specific style
type Style int
// Styles
const (
StyleDoubleLine = iota
StyleSingleLine
StyleDashedLine
StyleNoLine
)
// NewPen creates a new pen
func NewPen(style Style, color int) *Pen {
bgcolor := 49
if os.Getenv("CLICOLOR") == "0" {
color = 0
bgcolor = 0
}
return &Pen{
style: style,
color: color,
bgcolor: bgcolor,
}
}
type styleDef struct {
cornerTL string
cornerTR string
cornerBL string
cornerBR string
lineH string
lineV string
}
var styleDefs = []styleDef{
{"\u2554", "\u2557", "\u255a", "\u255d", "\u2550", "\u2551"},
{"\u256d", "\u256e", "\u2570", "\u256f", "\u2500", "\u2502"},
{"\u250c", "\u2510", "\u2514", "\u2518", "\u254c", "\u254e"},
{" ", " ", " ", " ", " ", " "},
}
// Pen struct
type Pen struct {
style Style
color int
bgcolor int
}
// Drawing struct
type Drawing struct {
buf *strings.Builder
width int
}
func (p *Pen) drawTopBars(buf io.Writer, labels ...string) {
style := styleDefs[p.style]
for _, label := range labels {
bar := strings.Repeat(style.lineH, len(label)+2)
fmt.Fprintf(buf, " ")
fmt.Fprintf(buf, "\x1b[%d;%dm", p.color, p.bgcolor)
fmt.Fprintf(buf, "%s%s%s", style.cornerTL, bar, style.cornerTR)
fmt.Fprintf(buf, "\x1b[%dm", 0)
}
fmt.Fprintf(buf, "\n")
}
func (p *Pen) drawBottomBars(buf io.Writer, labels ...string) {
style := styleDefs[p.style]
for _, label := range labels {
bar := strings.Repeat(style.lineH, len(label)+2)
fmt.Fprintf(buf, " ")
fmt.Fprintf(buf, "\x1b[%d;%dm", p.color, p.bgcolor)
fmt.Fprintf(buf, "%s%s%s", style.cornerBL, bar, style.cornerBR)
fmt.Fprintf(buf, "\x1b[%dm", 0)
}
fmt.Fprintf(buf, "\n")
}
func (p *Pen) drawLabels(buf io.Writer, labels ...string) {
style := styleDefs[p.style]
for _, label := range labels {
fmt.Fprintf(buf, " ")
fmt.Fprintf(buf, "\x1b[%d;%dm", p.color, p.bgcolor)
fmt.Fprintf(buf, "%s %s %s", style.lineV, label, style.lineV)
fmt.Fprintf(buf, "\x1b[%dm", 0)
}
fmt.Fprintf(buf, "\n")
}
// DrawArrow between boxes
func (p *Pen) DrawArrow() *Drawing {
drawing := &Drawing{
buf: new(strings.Builder),
width: 1,
}
fmt.Fprintf(drawing.buf, "\x1b[%dm", p.color)
fmt.Fprintf(drawing.buf, "\u2b07")
fmt.Fprintf(drawing.buf, "\x1b[%dm", 0)
return drawing
}
// DrawBoxes to draw boxes
func (p *Pen) DrawBoxes(labels ...string) *Drawing {
width := 0
for _, l := range labels {
width += len(l) + 2 + 2 + 1
}
drawing := &Drawing{
buf: new(strings.Builder),
width: width,
}
p.drawTopBars(drawing.buf, labels...)
p.drawLabels(drawing.buf, labels...)
p.drawBottomBars(drawing.buf, labels...)
return drawing
}
// Draw to writer
func (d *Drawing) Draw(writer io.Writer, centerOnWidth int) {
padSize := max((centerOnWidth-d.GetWidth())/2, 0)
for l := range strings.SplitSeq(d.buf.String(), "\n") {
if len(l) > 0 {
padding := strings.Repeat(" ", padSize)
fmt.Fprintf(writer, "%s%s\n", padding, l)
}
}
}
// GetWidth of drawing
func (d *Drawing) GetWidth() int {
return d.width
}
+8 -26
View File
@@ -12,24 +12,6 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
// Warning that implements `error` but safe to ignore
type Warning struct {
Message string
}
// Error the contract for error
func (w Warning) Error() string {
return w.Message
}
// Warningf create a warning
func Warningf(format string, args ...any) Warning {
w := Warning{
Message: fmt.Sprintf(format, args...),
}
return w
}
// Executor define contract for the steps of a workflow // Executor define contract for the steps of a workflow
type Executor func(ctx context.Context) error type Executor func(ctx context.Context) error
@@ -97,6 +79,12 @@ func NewErrorExecutor(err error) Executor {
// NewParallelExecutor creates a new executor from a parallel of other executors // NewParallelExecutor creates a new executor from a parallel of other executors
func NewParallelExecutor(parallel int, executors ...Executor) Executor { func NewParallelExecutor(parallel int, executors ...Executor) Executor {
if len(executors) == 0 {
return func(ctx context.Context) error {
return ctx.Err()
}
}
return func(ctx context.Context) error { return func(ctx context.Context) error {
work := make(chan Executor, len(executors)) work := make(chan Executor, len(executors))
errs := make(chan error, len(executors)) errs := make(chan error, len(executors))
@@ -117,7 +105,7 @@ func NewParallelExecutor(parallel int, executors ...Executor) Executor {
log.Debugf("Worker %d executing task %d", workerID, taskCount) log.Debugf("Worker %d executing task %d", workerID, taskCount)
// Recover from panics in executors to avoid crashing the worker // Recover from panics in executors to avoid crashing the worker
// goroutine which would leave the runner process hung. // goroutine which would leave the runner process hung.
// https://gitea.com/gitea/act_runner/issues/371 // https://gitea.com/gitea/runner/issues/371
errs <- func() (err error) { errs <- func() (err error) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@@ -156,15 +144,9 @@ func NewParallelExecutor(parallel int, executors ...Executor) Executor {
// Then runs another executor if this executor succeeds // Then runs another executor if this executor succeeds
func (e Executor) Then(then Executor) Executor { func (e Executor) Then(then Executor) Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
err := e(ctx) if err := e(ctx); err != nil {
if err != nil {
switch err.(type) {
case Warning:
Logger(ctx).Warning(err.Error())
default:
return err return err
} }
}
if ctx.Err() != nil { if ctx.Err() != nil {
return ctx.Err() return ctx.Err()
} }
+14
View File
@@ -12,6 +12,7 @@ import (
"time" "time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestNewWorkflow(t *testing.T) { func TestNewWorkflow(t *testing.T) {
@@ -119,6 +120,19 @@ func TestNewParallelExecutor(t *testing.T) {
assert.NoError(errSingle) assert.NoError(errSingle)
} }
func TestNewParallelExecutorEmpty(t *testing.T) {
assert := assert.New(t)
ctx := context.Background()
require.NoError(t, NewParallelExecutor(2)(ctx))
canceledCtx, cancel := context.WithCancel(context.Background())
cancel()
err := NewParallelExecutor(2)(canceledCtx)
assert.ErrorIs(err, context.Canceled)
}
func TestNewParallelExecutorFailed(t *testing.T) { func TestNewParallelExecutorFailed(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
-77
View File
@@ -1,77 +0,0 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Copyright 2020 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package common
import (
"fmt"
"io"
"os"
)
// CopyFile copy file
func CopyFile(source, dest string) (err error) {
sourcefile, err := os.Open(source)
if err != nil {
return err
}
defer sourcefile.Close()
destfile, err := os.Create(dest)
if err != nil {
return err
}
defer destfile.Close()
_, err = io.Copy(destfile, sourcefile)
if err == nil {
sourceinfo, err := os.Stat(source)
if err != nil {
_ = os.Chmod(dest, sourceinfo.Mode())
}
}
return err
}
// CopyDir recursive copy of directory
func CopyDir(source, dest string) (err error) {
// get properties of source dir
sourceinfo, err := os.Stat(source)
if err != nil {
return err
}
// create dest dir
err = os.MkdirAll(dest, sourceinfo.Mode())
if err != nil {
return err
}
objects, err := os.ReadDir(source)
for _, obj := range objects {
sourcefilepointer := source + "/" + obj.Name()
destinationfilepointer := dest + "/" + obj.Name()
if obj.IsDir() {
// create sub-directories - recursively
err = CopyDir(sourcefilepointer, destinationfilepointer)
if err != nil {
fmt.Println(err) //nolint:forbidigo // pre-existing issue from nektos/act
}
} else {
// perform copy
err = CopyFile(sourcefilepointer, destinationfilepointer)
if err != nil {
fmt.Println(err) //nolint:forbidigo // pre-existing issue from nektos/act
}
}
}
return err
}
+29 -18
View File
@@ -15,7 +15,7 @@ import (
"strings" "strings"
"sync" "sync"
"gitea.com/gitea/act_runner/act/common" "gitea.com/gitea/runner/act/common"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/config"
@@ -38,9 +38,11 @@ var (
ErrNoRepo = errors.New("unable to find git repo") ErrNoRepo = errors.New("unable to find git repo")
) )
// acquireCloneLock returns an unlock function after locking the per-directory mutex for dir. // AcquireCloneLock returns an unlock function after locking the per-directory mutex for dir.
// Only concurrent operations targeting the same directory are erialized; clones into different directories run in parallel. // Only concurrent operations targeting the same directory are serialized; clones into different directories run in parallel.
func acquireCloneLock(dir string) func() { // Callers reading files inside dir (e.g. tarring a checked-out action into a job container) must hold this lock too,
// otherwise a concurrent NewGitCloneExecutor on the same dir can mutate the worktree mid-read.
func AcquireCloneLock(dir string) func() {
v, _ := cloneLocks.LoadOrStore(dir, &sync.Mutex{}) v, _ := cloneLocks.LoadOrStore(dir, &sync.Mutex{})
mu := v.(*sync.Mutex) mu := v.(*sync.Mutex)
mu.Lock() mu.Lock()
@@ -241,10 +243,14 @@ type NewGitCloneExecutorInput struct {
InsecureSkipTLS bool InsecureSkipTLS bool
} }
// CloneIfRequired ... // CloneIfRequired returns the repository and a boolean indicating whether an existing local clone was reused.
func CloneIfRequired(ctx context.Context, refName plumbing.ReferenceName, input NewGitCloneExecutorInput, logger log.FieldLogger) (*git.Repository, error) { func CloneIfRequired(ctx context.Context, refName plumbing.ReferenceName, input NewGitCloneExecutorInput, logger log.FieldLogger) (*git.Repository, bool, error) {
r, err := git.PlainOpen(input.Dir) r, err := git.PlainOpen(input.Dir)
if err != nil { if err == nil {
// Reuse existing clone
return r, true, nil
}
var progressWriter io.Writer var progressWriter io.Writer
if isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) { if isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) {
if entry, ok := logger.(*log.Entry); ok { if entry, ok := logger.(*log.Entry); ok {
@@ -273,15 +279,14 @@ 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, false, err
} }
if err = os.Chmod(input.Dir, 0o755); err != nil { if err = os.Chmod(input.Dir, 0o755); err != nil {
return nil, err return nil, false, err
}
} }
return r, nil return r, false, nil
} }
func gitOptions(token string) (fetchOptions git.FetchOptions, pullOptions git.PullOptions) { func gitOptions(token string) (fetchOptions git.FetchOptions, pullOptions git.PullOptions) {
@@ -305,13 +310,13 @@ func gitOptions(token string) (fetchOptions git.FetchOptions, pullOptions git.Pu
func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor { func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
logger := common.Logger(ctx) logger := common.Logger(ctx)
logger.Infof(" \u2601 git clone '%s' # ref=%s", input.URL, input.Ref) logger.Infof("git clone '%s' # ref=%s", input.URL, input.Ref)
logger.Debugf(" cloning %s to %s", input.URL, input.Dir) logger.Debugf(" cloning %s to %s", input.URL, input.Dir)
defer acquireCloneLock(input.Dir)() defer AcquireCloneLock(input.Dir)()
refName := plumbing.ReferenceName("refs/heads/" + input.Ref) refName := plumbing.ReferenceName("refs/heads/" + input.Ref)
r, err := CloneIfRequired(ctx, refName, input, logger) r, reused, err := CloneIfRequired(ctx, refName, input, logger)
if err != nil { if err != nil {
return err return err
} }
@@ -336,10 +341,10 @@ func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor {
var hash *plumbing.Hash var hash *plumbing.Hash
rev := plumbing.Revision(input.Ref) rev := plumbing.Revision(input.Ref)
if hash, err = r.ResolveRevision(rev); err != nil { if hash, err = r.ResolveRevision(rev); err != nil {
// ResolveRevision returns a nil hash on error, and a branch ref legitimately fails
// here (no local refs/heads/<ref>); the duck-typing below resolves it.
logger.Errorf("Unable to resolve %s: %v", input.Ref, err) logger.Errorf("Unable to resolve %s: %v", input.Ref, err)
} } else if hash.String() != input.Ref && strings.HasPrefix(hash.String(), input.Ref) {
if hash.String() != input.Ref && strings.HasPrefix(hash.String(), input.Ref) {
return &Error{ return &Error{
err: ErrShortRef, err: ErrShortRef,
commit: hash.String(), commit: hash.String(),
@@ -390,12 +395,18 @@ func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor {
return err return err
} }
} }
reusedMsg := ""
if !isOfflineMode { if !isOfflineMode {
if err = w.Pull(&pullOptions); err != nil && err != git.NoErrAlreadyUpToDate { if err = w.Pull(&pullOptions); err != nil && err != git.NoErrAlreadyUpToDate {
logger.Debugf("Unable to pull %s: %v", refName, err) logger.Debugf("Unable to pull %s: %v", refName, err)
} }
} else if reused {
reusedMsg = " (reused in offline mode)"
} }
logger.Debugf("Cloned %s to %s", input.URL, input.Dir)
logger.Debugf("Cloned %s to %s%s", input.URL, input.Dir, reusedMsg)
if hash.String() != input.Ref && refType == "branch" { if hash.String() != input.Ref && refType == "branch" {
logger.Debugf("Provided ref is not a sha. Updating branch ref after pull") logger.Debugf("Provided ref is not a sha. Updating branch ref after pull")
+52 -4
View File
@@ -279,6 +279,54 @@ func TestGitCloneExecutorNonFastForwardRef(t *testing.T) {
assert.Equal(t, "second", strings.TrimSpace(string(out)), "working tree should be at the latest commit") assert.Equal(t, "second", strings.TrimSpace(string(out)), "working tree should be at the latest commit")
} }
func TestGitCloneExecutorOfflineMode(t *testing.T) {
gitConfig()
// Build a local "remote" with a single commit on main.
remoteDir := t.TempDir()
require.NoError(t, gitCmd("init", "--bare", "--initial-branch=main", remoteDir))
workDir := t.TempDir()
require.NoError(t, gitCmd("clone", remoteDir, workDir))
require.NoError(t, gitCmd("-C", workDir, "checkout", "-b", "main"))
require.NoError(t, gitCmd("-C", workDir, "commit", "--allow-empty", "-m", "initial"))
require.NoError(t, gitCmd("-C", workDir, "push", "-u", "origin", "main"))
// Prime the cache with an online clone of main.
cacheDir := t.TempDir()
require.NoError(t, NewGitCloneExecutor(NewGitCloneExecutorInput{
URL: remoteDir,
Ref: "main",
Dir: cacheDir,
})(context.Background()))
t.Run("cached branch resolves without fetching", func(t *testing.T) {
// Offline reuse of a cached branch must succeed even though ResolveRevision(input.Ref)
// finds no local refs/heads/<ref>.
err := NewGitCloneExecutor(NewGitCloneExecutorInput{
URL: remoteDir,
Ref: "main",
Dir: cacheDir,
OfflineMode: true,
})(context.Background())
require.NoError(t, err)
out, err := exec.Command("git", "-C", cacheDir, "log", "--oneline", "-1", "--format=%s").Output()
require.NoError(t, err)
assert.Equal(t, "initial", strings.TrimSpace(string(out)))
})
t.Run("unresolvable cached ref returns error", func(t *testing.T) {
// The ref was never cached; offline mode cannot resolve it and must return an error.
err := NewGitCloneExecutor(NewGitCloneExecutorInput{
URL: remoteDir,
Ref: "never-fetched",
Dir: cacheDir,
OfflineMode: true,
})(context.Background())
require.Error(t, err)
})
}
func gitConfig() { func gitConfig() {
if os.Getenv("GITHUB_ACTIONS") == "true" { if os.Getenv("GITHUB_ACTIONS") == "true" {
var err error var err error
@@ -310,11 +358,11 @@ func TestAcquireCloneLock(t *testing.T) {
t.Run("same directory serializes", func(t *testing.T) { t.Run("same directory serializes", func(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
unlock1 := acquireCloneLock(dir) unlock1 := AcquireCloneLock(dir)
secondAcquired := make(chan struct{}) secondAcquired := make(chan struct{})
go func() { go func() {
unlock := acquireCloneLock(dir) unlock := AcquireCloneLock(dir)
close(secondAcquired) close(secondAcquired)
unlock() unlock()
}() }()
@@ -338,12 +386,12 @@ func TestAcquireCloneLock(t *testing.T) {
dirA := t.TempDir() dirA := t.TempDir()
dirB := t.TempDir() dirB := t.TempDir()
unlockA := acquireCloneLock(dirA) unlockA := AcquireCloneLock(dirA)
defer unlockA() defer unlockA()
done := make(chan struct{}) done := make(chan struct{})
go func() { go func() {
unlock := acquireCloneLock(dirB) unlock := AcquireCloneLock(dirB)
unlock() unlock()
close(done) close(done)
}() }()
+10 -1
View File
@@ -6,13 +6,21 @@ package container
import ( import (
"context" "context"
"fmt"
"io" "io"
"gitea.com/gitea/act_runner/act/common" "gitea.com/gitea/runner/act/common"
"github.com/docker/go-connections/nat" "github.com/docker/go-connections/nat"
) )
// ExitCodeError reports a non-zero process exit code from a container command.
type ExitCodeError int
func (e ExitCodeError) Error() string {
return fmt.Sprintf("Process completed with exit code %d.", int(e))
}
// NewContainerInput the input for the New function // NewContainerInput the input for the New function
type NewContainerInput struct { type NewContainerInput struct {
Image string Image string
@@ -39,6 +47,7 @@ type NewContainerInput struct {
// Gitea specific // Gitea specific
AutoRemove bool AutoRemove bool
ValidVolumes []string ValidVolumes []string
AllocatePTY bool // allocate a pseudo-TTY for the container's exec processes
} }
// FileEntry is a file to copy to a container // FileEntry is a file to copy to a container
+31 -17
View File
@@ -8,34 +8,38 @@ package container
import ( import (
"context" "context"
"strings"
"gitea.com/gitea/act_runner/act/common" "gitea.com/gitea/runner/act/common"
"github.com/distribution/reference"
"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/moby/moby/api/types/registry"
) )
func LoadDockerAuthConfig(ctx context.Context, image string) (registry.AuthConfig, error) { func LoadDockerAuthConfig(ctx context.Context, image string) (registry.AuthConfig, error) {
logger := common.Logger(ctx) logger := common.Logger(ctx)
config, err := config.Load(config.Dir()) // config.LoadDefaultConfigFile panics on nil io.Writer when the config
// file is malformed; use config.Load to route errors through the logger.
cfg, err := config.Load(config.Dir())
if err != nil { if err != nil {
logger.Warnf("Could not load docker config: %v", err) logger.Warnf("Could not load docker config: %v", err)
return registry.AuthConfig{}, err return registry.AuthConfig{}, err
} }
if !cfg.ContainsAuth() {
if !config.ContainsAuth() { cfg.CredentialsStore = credentials.DetectDefaultStore(cfg.CredentialsStore)
config.CredentialsStore = credentials.DetectDefaultStore(config.CredentialsStore)
} }
hostName := "index.docker.io" registryKey := registryAuthConfigKey("docker.io")
index := strings.IndexRune(image, '/') if image != "" {
if index > -1 && (strings.ContainsAny(image[:index], ".:") || image[:index] == "localhost") { if registryRef, refErr := reference.ParseNormalizedNamed(image); refErr != nil {
hostName = image[:index] logger.Warnf("Could not normalize image reference: %v", refErr)
} else {
registryKey = registryAuthConfigKey(reference.Domain(registryRef))
}
} }
authConfig, err := config.GetAuthConfig(hostName) authConfig, err := cfg.GetAuthConfig(registryKey)
if err != nil { if err != nil {
logger.Warnf("Could not get auth config from docker config: %v", err) logger.Warnf("Could not get auth config from docker config: %v", err)
return registry.AuthConfig{}, err return registry.AuthConfig{}, err
@@ -46,17 +50,20 @@ func LoadDockerAuthConfig(ctx context.Context, image string) (registry.AuthConfi
func LoadDockerAuthConfigs(ctx context.Context) map[string]registry.AuthConfig { func LoadDockerAuthConfigs(ctx context.Context) map[string]registry.AuthConfig {
logger := common.Logger(ctx) logger := common.Logger(ctx)
config, err := config.Load(config.Dir()) cfg, err := config.Load(config.Dir())
if err != nil { if err != nil {
logger.Warnf("Could not load docker config: %v", err) logger.Warnf("Could not load docker config: %v", err)
return nil return nil
} }
if !cfg.ContainsAuth() {
if !config.ContainsAuth() { cfg.CredentialsStore = credentials.DetectDefaultStore(cfg.CredentialsStore)
config.CredentialsStore = credentials.DetectDefaultStore(config.CredentialsStore)
} }
creds, _ := config.GetAllCredentials() creds, err := cfg.GetAllCredentials()
if err != nil {
logger.Warnf("Could not get docker auth configs: %v", err)
return nil
}
authConfigs := make(map[string]registry.AuthConfig, len(creds)) authConfigs := make(map[string]registry.AuthConfig, len(creds))
for k, v := range creds { for k, v := range creds {
authConfigs[k] = registry.AuthConfig(v) authConfigs[k] = registry.AuthConfig(v)
@@ -64,3 +71,10 @@ func LoadDockerAuthConfigs(ctx context.Context) map[string]registry.AuthConfig {
return authConfigs return authConfigs
} }
func registryAuthConfigKey(domainName string) string {
if domainName == "docker.io" || domainName == "index.docker.io" {
return "https://index.docker.io/v1/"
}
return domainName
}
+19 -13
View File
@@ -12,13 +12,14 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"gitea.com/gitea/act_runner/act/common" "gitea.com/gitea/runner/act/common"
"github.com/docker/docker/api/types" "github.com/moby/go-archive"
"github.com/docker/docker/pkg/archive" "github.com/moby/go-archive/compression"
// github.com/docker/docker/builder/dockerignore is deprecated "github.com/moby/moby/client"
"github.com/moby/buildkit/frontend/dockerfile/dockerignore"
"github.com/moby/patternmatcher" "github.com/moby/patternmatcher"
"github.com/moby/patternmatcher/ignorefile"
specs "github.com/opencontainers/image-spec/specs-go/v1"
) )
// NewDockerBuildExecutor function to create a run executor for the container // NewDockerBuildExecutor function to create a run executor for the container
@@ -26,9 +27,9 @@ func NewDockerBuildExecutor(input NewDockerBuildExecutorInput) common.Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
logger := common.Logger(ctx) logger := common.Logger(ctx)
if input.Platform != "" { if input.Platform != "" {
logger.Infof("%sdocker build -t %s --platform %s %s", logPrefix, input.ImageTag, input.Platform, input.ContextDir) logger.Infof("docker build -t %s --platform %s %s", input.ImageTag, input.Platform, input.ContextDir)
} else { } else {
logger.Infof("%sdocker build -t %s %s", logPrefix, input.ImageTag, input.ContextDir) logger.Infof("docker build -t %s %s", input.ImageTag, input.ContextDir)
} }
if common.Dryrun(ctx) { if common.Dryrun(ctx) {
return nil return nil
@@ -43,13 +44,19 @@ 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 := types.ImageBuildOptions{ options := client.ImageBuildOptions{
Tags: tags, Tags: tags,
Remove: true, Remove: true,
Platform: input.Platform,
AuthConfigs: LoadDockerAuthConfigs(ctx), AuthConfigs: LoadDockerAuthConfigs(ctx),
Dockerfile: input.Dockerfile, Dockerfile: input.Dockerfile,
} }
platform, err := parsePlatform(input.Platform)
if err != nil {
return err
}
if platform != nil {
options.Platforms = []specs.Platform{*platform}
}
var buildContext io.ReadCloser var buildContext io.ReadCloser
if input.BuildContext != nil { if input.BuildContext != nil {
buildContext = io.NopCloser(input.BuildContext) buildContext = io.NopCloser(input.BuildContext)
@@ -77,7 +84,7 @@ func createBuildContext(ctx context.Context, contextDir, relDockerfile string) (
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 = archive.CanonicalTarNameForPath(relDockerfile) relDockerfile = filepath.ToSlash(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) {
@@ -87,7 +94,7 @@ func createBuildContext(ctx context.Context, contextDir, relDockerfile string) (
var excludes []string var excludes []string
if err == nil { if err == nil {
excludes, err = dockerignore.ReadAll(f) //nolint:staticcheck // pre-existing issue from nektos/act excludes, err = ignorefile.ReadAll(f)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -107,9 +114,8 @@ func createBuildContext(ctx context.Context, contextDir, relDockerfile string) (
includes = append(includes, ".dockerignore", relDockerfile) includes = append(includes, ".dockerignore", relDockerfile)
} }
compression := archive.Uncompressed
buildCtx, err := archive.TarWithOptions(contextDir, &archive.TarOptions{ buildCtx, err := archive.TarWithOptions(contextDir, &archive.TarOptions{
Compression: compression, Compression: compression.None,
ExcludePatterns: excludes, ExcludePatterns: excludes,
IncludeFiles: includes, IncludeFiles: includes,
}) })
File diff suppressed because it is too large. Load diff
+99 -62
View File
@@ -16,15 +16,18 @@ package container
import ( import (
"fmt" "fmt"
"io" "io"
"net/netip"
"os" "os"
"runtime" "runtime"
"strings" "strings"
"testing" "testing"
"time" "time"
"github.com/docker/docker/api/types/container"
networktypes "github.com/docker/docker/api/types/network"
"github.com/docker/go-connections/nat" "github.com/docker/go-connections/nat"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/moby/moby/api/types/container"
networktypes "github.com/moby/moby/api/types/network"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
@@ -77,21 +80,21 @@ func setupRunFlags() (*pflag.FlagSet, *containerOptions) {
return flags, copts return flags, copts
} }
func mustParse(t *testing.T, args string) (*container.Config, *container.HostConfig) { func mustParse(t *testing.T, args string) (*container.Config, *container.HostConfig, *networktypes.NetworkingConfig) {
t.Helper() t.Helper()
config, hostConfig, _, err := parseRun(append(strings.Split(args, " "), "ubuntu", "bash")) config, hostConfig, networkingConfig, err := parseRun(append(strings.Split(args, " "), "ubuntu", "bash"))
assert.NilError(t, err) assert.NilError(t, err)
return config, hostConfig return config, hostConfig, networkingConfig
} }
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)
} }
} }
@@ -140,7 +143,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)
@@ -194,10 +197,10 @@ func TestParseRunWithInvalidArgs(t *testing.T) {
} }
} }
func TestParseWithVolumes(t *testing.T) { func TestParseWithVolumes(t *testing.T) { //nolint:gocyclo // verbatim copy from docker/cli tests
// 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)
@@ -205,7 +208,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)
@@ -215,13 +218,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)
} }
@@ -230,26 +233,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)
@@ -258,7 +261,7 @@ 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])
} }
} }
@@ -294,6 +297,36 @@ func compareRandomizedStrings(a, b, c, d string) error {
return errors.Errorf("strings don't match") return errors.Errorf("strings don't match")
} }
func mustNetworkPort(t *testing.T, value string) networktypes.Port {
t.Helper()
port, err := networktypes.ParsePort(value)
if err != nil {
t.Fatalf("failed to parse network port %q: %v", value, err)
}
return port
}
func mustAddr(t *testing.T, value string) netip.Addr {
t.Helper()
addr, err := netip.ParseAddr(value)
if err != nil {
t.Fatalf("failed to parse address %q: %v", value, err)
}
return addr
}
func mustAddrs(t *testing.T, values ...string) []netip.Addr {
t.Helper()
addrs := make([]netip.Addr, 0, len(values))
for _, value := range values {
addrs = append(addrs, mustAddr(t, value))
}
return addrs
}
// Simple parse with MacAddress validation // Simple parse with MacAddress validation
func TestParseWithMacAddress(t *testing.T) { func TestParseWithMacAddress(t *testing.T) {
invalidMacAddress := "--mac-address=invalidMacAddress" invalidMacAddress := "--mac-address=invalidMacAddress"
@@ -301,9 +334,10 @@ 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)
} }
if config, _ := mustParse(t, validMacAddress); config.MacAddress != "92:d0:c6:0a:29:33" { //nolint:staticcheck // pre-existing issue from nektos/act _, hostConfig, networkingConfig := mustParse(t, validMacAddress)
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 endpoint := networkingConfig.EndpointsConfig[string(hostConfig.NetworkMode)]
} assert.Check(t, endpoint != nil)
assert.Equal(t, "92:d0:c6:0a:29:33", endpoint.MacAddress.String())
} }
func TestRunFlagsParseWithMemory(t *testing.T) { func TestRunFlagsParseWithMemory(t *testing.T) {
@@ -312,7 +346,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))
} }
@@ -322,10 +356,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))
} }
@@ -340,14 +374,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, "--hostname="+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)
} }
} }
@@ -361,26 +395,28 @@ 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)
} }
} }
func TestParseWithExpose(t *testing.T) { func TestParseWithExpose(t *testing.T) {
invalids := map[string]string{ invalids := []string{
":": "invalid port format for --expose: :", ":",
"8080:9090": "invalid port format for --expose: 8080:9090", "8080:9090",
"NaN/tcp": `invalid range format for --expose: NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`, "/tcp",
"NaN-NaN/tcp": `invalid range format for --expose: NaN-NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`, "/udp",
"8080-NaN/tcp": `invalid range format for --expose: 8080-NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`, "NaN/tcp",
"1234567890-8080/tcp": `invalid range format for --expose: 1234567890-8080/tcp, error: strconv.ParseUint: parsing "1234567890": value out of range`, "NaN-NaN/tcp",
"8080-NaN/tcp",
"1234567890-8080/tcp",
} }
valids := map[string][]nat.Port{ valids := map[string][]nat.Port{
"8080/tcp": {"8080/tcp"}, "8080/tcp": {"8080/tcp"},
@@ -389,9 +425,9 @@ func TestParseWithExpose(t *testing.T) {
"8080-8080/udp": {"8080/udp"}, "8080-8080/udp": {"8080/udp"},
"8080-8082/tcp": {"8080/tcp", "8081/tcp", "8082/tcp"}, "8080-8082/tcp": {"8080/tcp", "8081/tcp", "8082/tcp"},
} }
for expose, expectedError := range invalids { for _, expose := range invalids {
if _, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"}); err == nil || err.Error() != expectedError { if _, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"}); err == nil {
t.Fatalf("Expected error '%v' with '--expose=%v', got '%v'", expectedError, expose, err) t.Fatalf("Expected error with '--expose=%v', got none", expose)
} }
} }
for expose, exposedPorts := range valids { for expose, exposedPorts := range valids {
@@ -403,7 +439,7 @@ func TestParseWithExpose(t *testing.T) {
t.Fatalf("Expected %v exposed port, got %v", len(exposedPorts), len(config.ExposedPorts)) t.Fatalf("Expected %v exposed port, got %v", len(exposedPorts), len(config.ExposedPorts))
} }
for _, port := range exposedPorts { for _, port := range exposedPorts {
if _, ok := config.ExposedPorts[port]; !ok { if _, ok := config.ExposedPorts[mustNetworkPort(t, string(port))]; !ok {
t.Fatalf("Expected %v, got %v", exposedPorts, config.ExposedPorts) t.Fatalf("Expected %v, got %v", exposedPorts, config.ExposedPorts)
} }
} }
@@ -418,7 +454,7 @@ func TestParseWithExpose(t *testing.T) {
} }
ports := []nat.Port{"80/tcp", "81/tcp"} ports := []nat.Port{"80/tcp", "81/tcp"}
for _, port := range ports { for _, port := range ports {
if _, ok := config.ExposedPorts[port]; !ok { if _, ok := config.ExposedPorts[mustNetworkPort(t, string(port))]; !ok {
t.Fatalf("Expected %v, got %v", ports, config.ExposedPorts) t.Fatalf("Expected %v, got %v", ports, config.ExposedPorts)
} }
} }
@@ -498,9 +534,9 @@ func TestParseNetworkConfig(t *testing.T) {
expected: map[string]*networktypes.EndpointSettings{ expected: map[string]*networktypes.EndpointSettings{
"net1": { "net1": {
IPAMConfig: &networktypes.EndpointIPAMConfig{ IPAMConfig: &networktypes.EndpointIPAMConfig{
IPv4Address: "172.20.88.22", IPv4Address: mustAddr(t, "172.20.88.22"),
IPv6Address: "2001:db8::8822", IPv6Address: mustAddr(t, "2001:db8::8822"),
LinkLocalIPs: []string{"169.254.2.2", "fe80::169:254:2:2"}, LinkLocalIPs: mustAddrs(t, "169.254.2.2", "fe80::169:254:2:2"),
}, },
Links: []string{"foo:bar", "bar:baz"}, Links: []string{"foo:bar", "bar:baz"},
Aliases: []string{"web1", "web2"}, Aliases: []string{"web1", "web2"},
@@ -527,9 +563,9 @@ func TestParseNetworkConfig(t *testing.T) {
"net1": { "net1": {
DriverOpts: map[string]string{"field1": "value1"}, DriverOpts: map[string]string{"field1": "value1"},
IPAMConfig: &networktypes.EndpointIPAMConfig{ IPAMConfig: &networktypes.EndpointIPAMConfig{
IPv4Address: "172.20.88.22", IPv4Address: mustAddr(t, "172.20.88.22"),
IPv6Address: "2001:db8::8822", IPv6Address: mustAddr(t, "2001:db8::8822"),
LinkLocalIPs: []string{"169.254.2.2", "fe80::169:254:2:2"}, LinkLocalIPs: mustAddrs(t, "169.254.2.2", "fe80::169:254:2:2"),
}, },
Links: []string{"foo:bar", "bar:baz"}, Links: []string{"foo:bar", "bar:baz"},
Aliases: []string{"web1", "web2"}, Aliases: []string{"web1", "web2"},
@@ -538,8 +574,8 @@ func TestParseNetworkConfig(t *testing.T) {
"net3": { "net3": {
DriverOpts: map[string]string{"field3": "value3"}, DriverOpts: map[string]string{"field3": "value3"},
IPAMConfig: &networktypes.EndpointIPAMConfig{ IPAMConfig: &networktypes.EndpointIPAMConfig{
IPv4Address: "172.20.88.22", IPv4Address: mustAddr(t, "172.20.88.22"),
IPv6Address: "2001:db8::8822", IPv6Address: mustAddr(t, "2001:db8::8822"),
}, },
Aliases: []string{"web3"}, Aliases: []string{"web3"},
}, },
@@ -556,8 +592,8 @@ func TestParseNetworkConfig(t *testing.T) {
"field2": "value2", "field2": "value2",
}, },
IPAMConfig: &networktypes.EndpointIPAMConfig{ IPAMConfig: &networktypes.EndpointIPAMConfig{
IPv4Address: "172.20.88.22", IPv4Address: mustAddr(t, "172.20.88.22"),
IPv6Address: "2001:db8::8822", IPv6Address: mustAddr(t, "2001:db8::8822"),
}, },
Aliases: []string{"web1", "web2"}, Aliases: []string{"web1", "web2"},
}, },
@@ -610,7 +646,9 @@ func TestParseNetworkConfig(t *testing.T) {
assert.NilError(t, err) assert.NilError(t, err)
assert.DeepEqual(t, hConfig.NetworkMode, tc.expectedCfg.NetworkMode) assert.DeepEqual(t, hConfig.NetworkMode, tc.expectedCfg.NetworkMode)
assert.DeepEqual(t, nwConfig.EndpointsConfig, tc.expected) if diff := cmp.Diff(tc.expected, nwConfig.EndpointsConfig, cmpopts.EquateComparable(netip.Addr{})); diff != "" {
t.Fatalf("unexpected endpoints (-want +got):\n%s", diff)
}
}) })
} }
} }
@@ -631,7 +669,7 @@ func TestParseModes(t *testing.T) {
} }
// uts ko // uts ko
_, _, _, err = parseRun([]string{"--uts=container:", "img", "cmd"}) _, _, _, err = parseRun([]string{"--uts=container:", "img", "cmd"}) //nolint:dogsled // verbatim copy from docker/cli tests
assert.ErrorContains(t, err, "--uts: invalid UTS mode") assert.ErrorContains(t, err, "--uts: invalid UTS mode")
// uts ok // uts ok
@@ -691,10 +729,9 @@ func TestParseRestartPolicy(t *testing.T) {
} }
func TestParseRestartPolicyAutoRemove(t *testing.T) { func TestParseRestartPolicyAutoRemove(t *testing.T) {
expected := "Conflicting options: --restart and --rm" _, _, _, err := parseRun([]string{"--rm", "--restart=always", "img", "cmd"}) //nolint:dogsled // verbatim copy from docker/cli tests
_, _, _, err := parseRun([]string{"--rm", "--restart=always", "img", "cmd"}) if err == nil {
if err == nil || err.Error() != expected { t.Fatal("Expected error for conflicting --restart and --rm, but got none")
t.Fatalf("Expected error %v, but got none", expected)
} }
} }
@@ -752,7 +789,7 @@ func TestParseLoggingOpts(t *testing.T) {
} }
} }
func TestParseEnvfileVariables(t *testing.T) { //nolint:dupl // pre-existing issue from nektos/act func TestParseEnvfileVariables(t *testing.T) { //nolint:dupl // verbatim copy from docker/cli tests
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."
@@ -795,7 +832,7 @@ func TestParseEnvfileVariablesWithBOMUnicode(t *testing.T) {
} }
// UTF16 with BOM // UTF16 with BOM
e := "contains invalid utf8 bytes at line" e := "invalid env file"
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)
} }
@@ -805,7 +842,7 @@ func TestParseEnvfileVariablesWithBOMUnicode(t *testing.T) {
} }
} }
func TestParseLabelfileVariables(t *testing.T) { //nolint:dupl // pre-existing issue from nektos/act func TestParseLabelfileVariables(t *testing.T) { //nolint:dupl // verbatim copy from docker/cli tests
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."
+7 -7
View File
@@ -10,8 +10,8 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/docker/docker/api/types" cerrdefs "github.com/containerd/errdefs"
"github.com/docker/docker/client" "github.com/moby/moby/client"
) )
// ImageExistsLocally returns a boolean indicating if an image with the // ImageExistsLocally returns a boolean indicating if an image with the
@@ -23,8 +23,8 @@ func ImageExistsLocally(ctx context.Context, imageName, platform string) (bool,
} }
defer cli.Close() defer cli.Close()
inspectImage, _, err := cli.ImageInspectWithRaw(ctx, imageName) inspectImage, err := cli.ImageInspect(ctx, imageName)
if client.IsErrNotFound(err) { if cerrdefs.IsNotFound(err) {
return false, nil return false, nil
} else if err != nil { } else if err != nil {
return false, err return false, err
@@ -46,14 +46,14 @@ func RemoveImage(ctx context.Context, imageName string, force, pruneChildren boo
} }
defer cli.Close() defer cli.Close()
inspectImage, _, err := cli.ImageInspectWithRaw(ctx, imageName) inspectImage, err := cli.ImageInspect(ctx, imageName)
if client.IsErrNotFound(err) { if cerrdefs.IsNotFound(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, types.ImageRemoveOptions{ if _, err = cli.ImageRemove(ctx, inspectImage.ID, client.ImageRemoveOptions{
Force: force, Force: force,
PruneChildren: pruneChildren, PruneChildren: pruneChildren,
}); err != nil { }); err != nil {
+10 -10
View File
@@ -9,8 +9,8 @@ import (
"io" "io"
"testing" "testing"
"github.com/docker/docker/api/types" "github.com/moby/moby/client"
"github.com/docker/docker/client" specs "github.com/opencontainers/image-spec/specs-go/v1"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -38,34 +38,34 @@ func TestImageExistsLocally(t *testing.T) {
assert.False(t, invalidImagePlatform) assert.False(t, invalidImagePlatform)
// pull an image // pull an image
cli, err := client.NewClientWithOpts(client.FromEnv) cli, err := client.New(client.FromEnv)
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
cli.NegotiateAPIVersion(context.Background()) defer cli.Close()
// 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", types.ImagePullOptions{ readerDefault, err := cli.ImagePull(ctx, "node:24-bookworm-slim", client.ImagePullOptions{
Platform: "linux/amd64", Platforms: []specs.Platform{{OS: "linux", Architecture: "amd64"}},
}) })
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
defer readerDefault.Close() defer readerDefault.Close()
_, err = io.ReadAll(readerDefault) _, err = io.ReadAll(readerDefault)
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
imageDefaultArchExists, err := ImageExistsLocally(ctx, "node:16-buster-slim", "linux/amd64") imageDefaultArchExists, err := ImageExistsLocally(ctx, "node:24-bookworm-slim", "linux/amd64")
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.True(t, imageDefaultArchExists) assert.True(t, imageDefaultArchExists)
// Validate if another architecture platform can be pulled // Validate if another architecture platform can be pulled
readerArm64, err := cli.ImagePull(ctx, "node:16-buster-slim", types.ImagePullOptions{ readerArm64, err := cli.ImagePull(ctx, "node:24-bookworm-slim", client.ImagePullOptions{
Platform: "linux/arm64", Platforms: []specs.Platform{{OS: "linux", Architecture: "arm64"}},
}) })
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
defer readerArm64.Close() defer readerArm64.Close()
_, err = io.ReadAll(readerArm64) _, err = io.ReadAll(readerArm64)
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
imageArm64Exists, err := ImageExistsLocally(ctx, "node:16-buster-slim", "linux/arm64") imageArm64Exists, err := ImageExistsLocally(ctx, "node:24-bookworm-slim", "linux/arm64")
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.True(t, imageArm64Exists) assert.True(t, imageArm64Exists)
} }
-2
View File
@@ -26,8 +26,6 @@ type dockerMessage struct {
Progress string `json:"progress"` Progress string `json:"progress"`
} }
const logPrefix = " \U0001F433 "
func logDockerResponse(logger logrus.FieldLogger, dockerResponse io.ReadCloser, isError bool) error { func logDockerResponse(logger logrus.FieldLogger, dockerResponse io.ReadCloser, isError bool) error {
if dockerResponse == nil { if dockerResponse == nil {
return nil return nil
+13 -13
View File
@@ -9,9 +9,9 @@ package container
import ( import (
"context" "context"
"gitea.com/gitea/act_runner/act/common" "gitea.com/gitea/runner/act/common"
"github.com/docker/docker/api/types" "github.com/moby/moby/client"
) )
func NewDockerNetworkCreateExecutor(name string) common.Executor { func NewDockerNetworkCreateExecutor(name string) common.Executor {
@@ -23,20 +23,20 @@ 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, types.NetworkListOptions{}) networks, err := cli.NetworkList(ctx, client.NetworkListOptions{})
if err != nil { if err != nil {
return err return err
} }
// For Gitea, reduce log noise // For Gitea, reduce log noise
// common.Logger(ctx).Debugf("%v", networks) // common.Logger(ctx).Debugf("%v", networks)
for _, network := range networks { for _, n := range networks.Items {
if network.Name == name { if n.Name == name {
common.Logger(ctx).Debugf("Network %v exists", name) common.Logger(ctx).Debugf("Network %v exists", name)
return nil return nil
} }
} }
_, err = cli.NetworkCreate(ctx, name, types.NetworkCreate{ _, err = cli.NetworkCreate(ctx, name, client.NetworkCreateOptions{
Driver: "bridge", Driver: "bridge",
Scope: "local", Scope: "local",
}) })
@@ -56,23 +56,23 @@ func NewDockerNetworkRemoveExecutor(name string) common.Executor {
} }
defer cli.Close() defer cli.Close()
// Make shure that all network of the specified name are removed // Make sure 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, types.NetworkListOptions{}) networks, err := cli.NetworkList(ctx, client.NetworkListOptions{})
if err != nil { if err != nil {
return err return err
} }
// For Gitea, reduce log noise // For Gitea, reduce log noise
// common.Logger(ctx).Debugf("%v", networks) // common.Logger(ctx).Debugf("%v", networks)
for _, network := range networks { for _, n := range networks.Items {
if network.Name == name { if n.Name == name {
result, err := cli.NetworkInspect(ctx, network.ID, types.NetworkInspectOptions{}) result, err := cli.NetworkInspect(ctx, n.ID, client.NetworkInspectOptions{})
if err != nil { if err != nil {
return err return err
} }
if len(result.Containers) == 0 { if len(result.Network.Containers) == 0 {
if err = cli.NetworkRemove(ctx, network.ID); err != nil { if _, err = cli.NetworkRemove(ctx, n.ID, client.NetworkRemoveOptions{}); err != nil {
common.Logger(ctx).Debugf("%v", err) common.Logger(ctx).Debugf("%v", err)
} }
} else { } else {
+39
View File
@@ -0,0 +1,39 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// Copyright 2025 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows || netbsd))
package container
import (
"fmt"
"strings"
specs "github.com/opencontainers/image-spec/specs-go/v1"
)
// parsePlatform parses an "os/arch[/variant]" string into a Platform. An empty input
// returns (nil, nil), meaning "no platform constraint". A non-empty but malformed
// string is rejected explicitly so it cannot silently fall through to the daemon's
// default architecture.
func parsePlatform(platform string) (*specs.Platform, error) {
if platform == "" {
return nil, nil //nolint:nilnil // no platform constraint requested
}
parts := strings.Split(platform, "/")
if len(parts) < 2 || len(parts) > 3 || parts[0] == "" || parts[1] == "" || (len(parts) == 3 && parts[2] == "") {
return nil, fmt.Errorf("invalid platform %q: expected os/arch[/variant]", platform)
}
spec := &specs.Platform{
OS: strings.ToLower(parts[0]),
Architecture: strings.ToLower(parts[1]),
}
if len(parts) == 3 {
spec.Variant = strings.ToLower(parts[2])
}
return spec, nil
}
+63
View File
@@ -0,0 +1,63 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package container
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParsePlatform(t *testing.T) {
t.Run("empty input returns nil platform without error", func(t *testing.T) {
got, err := parsePlatform("")
require.NoError(t, err)
assert.Nil(t, got)
})
t.Run("os/arch", func(t *testing.T) {
got, err := parsePlatform("linux/amd64")
require.NoError(t, err)
require.NotNil(t, got)
assert.Equal(t, "linux", got.OS)
assert.Equal(t, "amd64", got.Architecture)
assert.Empty(t, got.Variant)
})
t.Run("os/arch/variant", func(t *testing.T) {
got, err := parsePlatform("linux/arm/v7")
require.NoError(t, err)
require.NotNil(t, got)
assert.Equal(t, "linux", got.OS)
assert.Equal(t, "arm", got.Architecture)
assert.Equal(t, "v7", got.Variant)
})
t.Run("input is lowercased", func(t *testing.T) {
got, err := parsePlatform("Linux/AMD64/V8")
require.NoError(t, err)
require.NotNil(t, got)
assert.Equal(t, "linux", got.OS)
assert.Equal(t, "amd64", got.Architecture)
assert.Equal(t, "v8", got.Variant)
})
for _, bad := range []string{
"amd64",
"linux",
"linux/",
"/amd64",
"/",
"//",
"linux/arm/",
"linux/arm/v7/extra",
} {
t.Run("rejects "+bad, func(t *testing.T) {
got, err := parsePlatform(bad)
require.Error(t, err)
assert.Nil(t, got)
})
}
}
+20 -19
View File
@@ -8,23 +8,23 @@ package container
import ( import (
"context" "context"
"encoding/base64"
"encoding/json"
"fmt" "fmt"
"strings" "strings"
"gitea.com/gitea/act_runner/act/common" "gitea.com/gitea/runner/act/common"
"github.com/distribution/reference" "github.com/distribution/reference"
"github.com/docker/docker/api/types" "github.com/moby/moby/api/pkg/authconfig"
"github.com/docker/docker/api/types/registry" "github.com/moby/moby/api/types/registry"
"github.com/moby/moby/client"
specs "github.com/opencontainers/image-spec/specs-go/v1"
) )
// NewDockerPullExecutor function to create a run executor for the container // NewDockerPullExecutor function to create a run executor for the container
func NewDockerPullExecutor(input NewDockerPullExecutorInput) common.Executor { func NewDockerPullExecutor(input NewDockerPullExecutorInput) common.Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
logger := common.Logger(ctx) logger := common.Logger(ctx)
logger.Debugf("%sdocker pull %v", logPrefix, input.Image) logger.Debugf("docker pull %v", input.Image)
if common.Dryrun(ctx) { if common.Dryrun(ctx) {
return nil return nil
@@ -78,26 +78,29 @@ func NewDockerPullExecutor(input NewDockerPullExecutorInput) common.Executor {
} }
} }
func getImagePullOptions(ctx context.Context, input NewDockerPullExecutorInput) (types.ImagePullOptions, error) { func getImagePullOptions(ctx context.Context, input NewDockerPullExecutorInput) (client.ImagePullOptions, error) {
imagePullOptions := types.ImagePullOptions{ imagePullOptions := client.ImagePullOptions{}
Platform: input.Platform, platform, err := parsePlatform(input.Platform)
if err != nil {
return imagePullOptions, err
}
if platform != nil {
imagePullOptions.Platforms = []specs.Platform{*platform}
} }
logger := common.Logger(ctx) logger := common.Logger(ctx)
if input.Username != "" && input.Password != "" { if input.Username != "" && input.Password != "" {
logger.Debugf("using authentication for docker pull") logger.Debugf("using authentication for docker pull")
authConfig := registry.AuthConfig{ encodedAuth, err := authconfig.Encode(registry.AuthConfig{
Username: input.Username, Username: input.Username,
Password: input.Password, Password: input.Password,
} })
encodedJSON, err := json.Marshal(authConfig)
if err != nil { if err != nil {
return imagePullOptions, err return imagePullOptions, err
} }
imagePullOptions.RegistryAuth = base64.URLEncoding.EncodeToString(encodedJSON) imagePullOptions.RegistryAuth = encodedAuth
} else { } else {
authConfig, err := LoadDockerAuthConfig(ctx, input.Image) authConfig, err := LoadDockerAuthConfig(ctx, input.Image)
if err != nil { if err != nil {
@@ -108,19 +111,17 @@ func getImagePullOptions(ctx context.Context, input NewDockerPullExecutorInput)
} }
logger.Info("using DockerAuthConfig authentication for docker pull") logger.Info("using DockerAuthConfig authentication for docker pull")
encodedJSON, err := json.Marshal(authConfig) imagePullOptions.RegistryAuth, err = authconfig.Encode(authConfig)
if err != nil { if err != nil {
return imagePullOptions, err return imagePullOptions, err
} }
imagePullOptions.RegistryAuth = base64.URLEncoding.EncodeToString(encodedJSON)
} }
return imagePullOptions, nil return imagePullOptions, nil
} }
func cleanImage(ctx context.Context, image string) string { func cleanImage(ctx context.Context, imageName string) string {
ref, err := reference.ParseAnyReference(image) ref, err := reference.ParseAnyReference(imageName)
if err != nil { if err != nil {
common.Logger(ctx).Error(err) common.Logger(ctx).Error(err)
return "" return ""
File diff suppressed because it is too large. Load diff
+124 -37
View File
@@ -11,24 +11,31 @@ import (
"errors" "errors"
"io" "io"
"net" "net"
"os"
"path/filepath"
"strings" "strings"
"testing" "testing"
"time" "time"
"gitea.com/gitea/act_runner/act/common" "gitea.com/gitea/runner/act/common"
"github.com/docker/docker/api/types" "github.com/moby/moby/api/types/container"
"github.com/docker/docker/api/types/container" mobyclient "github.com/moby/moby/client"
"github.com/docker/docker/client"
"github.com/sirupsen/logrus/hooks/test" "github.com/sirupsen/logrus/hooks/test"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
) )
func TestDocker(t *testing.T) { func TestDocker(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
ctx := context.Background() ctx := context.Background()
client, err := GetDockerClient(ctx) client, err := GetDockerClient(ctx)
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act if err != nil {
t.Skipf("skipping integration test: %v", err)
}
defer client.Close() defer client.Close()
dockerBuild := NewDockerBuildExecutor(NewDockerBuildExecutorInput{ dockerBuild := NewDockerBuildExecutor(NewDockerBuildExecutorInput{
@@ -66,28 +73,33 @@ func TestDocker(t *testing.T) {
} }
type mockDockerClient struct { type mockDockerClient struct {
client.APIClient mobyclient.APIClient
mock.Mock mock.Mock
} }
func (m *mockDockerClient) ContainerExecCreate(ctx context.Context, id string, opts types.ExecConfig) (types.IDResponse, error) { func (m *mockDockerClient) ExecCreate(ctx context.Context, id string, opts mobyclient.ExecCreateOptions) (mobyclient.ExecCreateResult, error) {
args := m.Called(ctx, id, opts) args := m.Called(ctx, id, opts)
return args.Get(0).(types.IDResponse), args.Error(1) return args.Get(0).(mobyclient.ExecCreateResult), args.Error(1)
} }
func (m *mockDockerClient) ContainerExecAttach(ctx context.Context, id string, opts types.ExecStartCheck) (types.HijackedResponse, error) { func (m *mockDockerClient) ExecAttach(ctx context.Context, id string, opts mobyclient.ExecAttachOptions) (mobyclient.ExecAttachResult, 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).(mobyclient.ExecAttachResult), args.Error(1)
} }
func (m *mockDockerClient) ContainerExecInspect(ctx context.Context, execID string) (types.ContainerExecInspect, error) { func (m *mockDockerClient) ExecInspect(ctx context.Context, execID string, opts mobyclient.ExecInspectOptions) (mobyclient.ExecInspectResult, error) {
args := m.Called(ctx, execID) args := m.Called(ctx, execID, opts)
return args.Get(0).(types.ContainerExecInspect), args.Error(1) return args.Get(0).(mobyclient.ExecInspectResult), args.Error(1)
} }
func (m *mockDockerClient) CopyToContainer(ctx context.Context, id, path string, content io.Reader, options types.CopyToContainerOptions) error { func (m *mockDockerClient) ContainerWait(ctx context.Context, containerID string, opts mobyclient.ContainerWaitOptions) mobyclient.ContainerWaitResult {
args := m.Called(ctx, id, path, content, options) args := m.Called(ctx, containerID, opts)
return args.Error(0) return args.Get(0).(mobyclient.ContainerWaitResult)
}
func (m *mockDockerClient) CopyToContainer(ctx context.Context, id string, options mobyclient.CopyToContainerOptions) (mobyclient.CopyToContainerResult, error) {
args := m.Called(ctx, id, options)
return args.Get(0).(mobyclient.CopyToContainerResult), args.Error(1)
} }
type endlessReader struct { type endlessReader struct {
@@ -119,10 +131,12 @@ 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("types.ExecConfig")).Return(types.IDResponse{ID: "id"}, nil) client.On("ExecCreate", ctx, "123", mock.AnythingOfType("client.ExecCreateOptions")).Return(mobyclient.ExecCreateResult{ID: "id"}, nil)
client.On("ContainerExecAttach", ctx, "id", mock.AnythingOfType("types.ExecStartCheck")).Return(types.HijackedResponse{ client.On("ExecAttach", ctx, "id", mock.AnythingOfType("client.ExecAttachOptions")).Return(mobyclient.ExecAttachResult{
HijackedResponse: mobyclient.HijackedResponse{
Conn: conn, Conn: conn,
Reader: bufio.NewReader(endlessReader{}), Reader: bufio.NewReader(endlessReader{}),
},
}, nil) }, nil)
cr := &containerReference{ cr := &containerReference{
@@ -156,12 +170,14 @@ func TestDockerExecFailure(t *testing.T) {
conn := &mockConn{} conn := &mockConn{}
client := &mockDockerClient{} client := &mockDockerClient{}
client.On("ContainerExecCreate", ctx, "123", mock.AnythingOfType("types.ExecConfig")).Return(types.IDResponse{ID: "id"}, nil) client.On("ExecCreate", ctx, "123", mock.AnythingOfType("client.ExecCreateOptions")).Return(mobyclient.ExecCreateResult{ID: "id"}, nil)
client.On("ContainerExecAttach", ctx, "id", mock.AnythingOfType("types.ExecStartCheck")).Return(types.HijackedResponse{ client.On("ExecAttach", ctx, "id", mock.AnythingOfType("client.ExecAttachOptions")).Return(mobyclient.ExecAttachResult{
HijackedResponse: mobyclient.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(types.ContainerExecInspect{ client.On("ExecInspect", ctx, "id", mobyclient.ExecInspectOptions{}).Return(mobyclient.ExecInspectResult{
ExitCode: 1, ExitCode: 1,
}, nil) }, nil)
@@ -174,20 +190,56 @@ func TestDockerExecFailure(t *testing.T) {
} }
err := cr.exec([]string{""}, map[string]string{}, "user", "workdir")(ctx) err := cr.exec([]string{""}, map[string]string{}, "user", "workdir")(ctx)
assert.Error(t, err, "exit with `FAILURE`: 1") //nolint:testifylint // pre-existing issue from nektos/act var exitErr ExitCodeError
require.ErrorAs(t, err, &exitErr)
assert.Equal(t, ExitCodeError(1), exitErr)
assert.Equal(t, "Process completed with exit code 1.", err.Error())
conn.AssertExpectations(t) conn.AssertExpectations(t)
client.AssertExpectations(t) client.AssertExpectations(t)
} }
func TestDockerWaitFailure(t *testing.T) {
ctx := context.Background()
statusCh := make(chan container.WaitResponse, 1)
statusCh <- container.WaitResponse{StatusCode: 2}
errCh := make(chan error, 1)
client := &mockDockerClient{}
client.On("ContainerWait", ctx, "123", mobyclient.ContainerWaitOptions{Condition: container.WaitConditionNotRunning}).
Return(mobyclient.ContainerWaitResult{
Result: (<-chan container.WaitResponse)(statusCh),
Error: (<-chan error)(errCh),
})
cr := &containerReference{
id: "123",
cli: client,
input: &NewContainerInput{
Image: "image",
},
}
err := cr.wait()(ctx)
var exitErr ExitCodeError
require.ErrorAs(t, err, &exitErr)
assert.Equal(t, ExitCodeError(2), exitErr)
assert.Equal(t, "Process completed with exit code 2.", err.Error())
client.AssertExpectations(t)
}
func TestDockerCopyTarStream(t *testing.T) { func TestDockerCopyTarStream(t *testing.T) {
ctx := context.Background() ctx := context.Background()
conn := &mockConn{}
client := &mockDockerClient{} client := &mockDockerClient{}
client.On("CopyToContainer", ctx, "123", "/", mock.Anything, mock.AnythingOfType("types.CopyToContainerOptions")).Return(nil) client.On("CopyToContainer", ctx, "123", mock.MatchedBy(func(opts mobyclient.CopyToContainerOptions) bool {
client.On("CopyToContainer", ctx, "123", "/var/run/act", mock.Anything, mock.AnythingOfType("types.CopyToContainerOptions")).Return(nil) return opts.DestinationPath == "/" && opts.Content != nil
})).Return(mobyclient.CopyToContainerResult{}, nil)
client.On("CopyToContainer", ctx, "123", mock.MatchedBy(func(opts mobyclient.CopyToContainerOptions) bool {
return opts.DestinationPath == "/var/run/act" && opts.Content != nil
})).Return(mobyclient.CopyToContainerResult{}, nil)
cr := &containerReference{ cr := &containerReference{
id: "123", id: "123",
cli: client, cli: client,
@@ -198,20 +250,18 @@ func TestDockerCopyTarStream(t *testing.T) {
_ = cr.CopyTarStream(ctx, "/var/run/act", &bytes.Buffer{}) _ = cr.CopyTarStream(ctx, "/var/run/act", &bytes.Buffer{})
conn.AssertExpectations(t)
client.AssertExpectations(t) client.AssertExpectations(t)
} }
func TestDockerCopyTarStreamErrorInCopyFiles(t *testing.T) { func TestDockerCopyTarStreamErrorInCopyFiles(t *testing.T) {
ctx := context.Background() ctx := context.Background()
conn := &mockConn{}
merr := errors.New("Failure") merr := errors.New("Failure")
client := &mockDockerClient{} client := &mockDockerClient{}
client.On("CopyToContainer", ctx, "123", "/", mock.Anything, mock.AnythingOfType("types.CopyToContainerOptions")).Return(merr) client.On("CopyToContainer", ctx, "123", mock.MatchedBy(func(opts mobyclient.CopyToContainerOptions) bool {
client.On("CopyToContainer", ctx, "123", "/", mock.Anything, mock.AnythingOfType("types.CopyToContainerOptions")).Return(merr) return opts.DestinationPath == "/" && opts.Content != nil
})).Return(mobyclient.CopyToContainerResult{}, merr)
cr := &containerReference{ cr := &containerReference{
id: "123", id: "123",
cli: client, cli: client,
@@ -223,20 +273,21 @@ 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) //nolint:testifylint // pre-existing issue from nektos/act assert.ErrorIs(t, err, merr) //nolint:testifylint // pre-existing issue from nektos/act
conn.AssertExpectations(t)
client.AssertExpectations(t) client.AssertExpectations(t)
} }
func TestDockerCopyTarStreamErrorInMkdir(t *testing.T) { func TestDockerCopyTarStreamErrorInMkdir(t *testing.T) {
ctx := context.Background() ctx := context.Background()
conn := &mockConn{}
merr := errors.New("Failure") merr := errors.New("Failure")
client := &mockDockerClient{} client := &mockDockerClient{}
client.On("CopyToContainer", ctx, "123", "/", mock.Anything, mock.AnythingOfType("types.CopyToContainerOptions")).Return(nil) client.On("CopyToContainer", ctx, "123", mock.MatchedBy(func(opts mobyclient.CopyToContainerOptions) bool {
client.On("CopyToContainer", ctx, "123", "/var/run/act", mock.Anything, mock.AnythingOfType("types.CopyToContainerOptions")).Return(merr) return opts.DestinationPath == "/" && opts.Content != nil
})).Return(mobyclient.CopyToContainerResult{}, nil)
client.On("CopyToContainer", ctx, "123", mock.MatchedBy(func(opts mobyclient.CopyToContainerOptions) bool {
return opts.DestinationPath == "/var/run/act" && opts.Content != nil
})).Return(mobyclient.CopyToContainerResult{}, merr)
cr := &containerReference{ cr := &containerReference{
id: "123", id: "123",
cli: client, cli: client,
@@ -248,7 +299,6 @@ 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) //nolint:testifylint // pre-existing issue from nektos/act assert.ErrorIs(t, err, merr) //nolint:testifylint // pre-existing issue from nektos/act
conn.AssertExpectations(t)
client.AssertExpectations(t) client.AssertExpectations(t)
} }
@@ -327,3 +377,40 @@ func TestCheckVolumes(t *testing.T) {
}) })
} }
} }
func TestCheckVolumesRejectsEscapingHostPaths(t *testing.T) {
logger, _ := test.NewNullLogger()
ctx := common.WithLogger(context.Background(), logger)
base := t.TempDir()
allowed := filepath.Join(base, "allowed")
denied := filepath.Join(base, "denied")
require.NoError(t, os.MkdirAll(allowed, 0o700))
require.NoError(t, os.MkdirAll(denied, 0o700))
cr := &containerReference{
input: &NewContainerInput{
ValidVolumes: []string{filepath.Join(allowed, "**")},
},
}
escapingPath := allowed + string(filepath.Separator) + ".." + string(filepath.Separator) + "denied"
_, hostConf := cr.sanitizeConfig(ctx, &container.Config{}, &container.HostConfig{
Binds: []string{escapingPath + ":/mnt"},
})
assert.Empty(t, hostConf.Binds)
linkPath := filepath.Join(allowed, "link")
if err := os.Symlink(denied, linkPath); err != nil {
t.Skipf("cannot create symlink: %v", err)
}
_, hostConf = cr.sanitizeConfig(ctx, &container.Config{}, &container.HostConfig{
Binds: []string{linkPath + ":/mnt"},
})
assert.Empty(t, hostConf.Binds)
_, hostConf = cr.sanitizeConfig(ctx, &container.Config{}, &container.HostConfig{
Binds: []string{filepath.Join(linkPath, "missing") + ":/mnt"},
})
assert.Empty(t, hostConf.Binds)
}
+4 -4
View File
@@ -10,9 +10,9 @@ import (
"context" "context"
"runtime" "runtime"
"gitea.com/gitea/act_runner/act/common" "gitea.com/gitea/runner/act/common"
"github.com/docker/docker/api/types" "github.com/moby/moby/api/types/system"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@@ -51,8 +51,8 @@ func RunnerArch(ctx context.Context) string {
return runtime.GOOS return runtime.GOOS
} }
func GetHostInfo(ctx context.Context) (info types.Info, err error) { func GetHostInfo(ctx context.Context) (info system.Info, err error) {
return types.Info{}, nil return system.Info{}, nil
} }
func NewDockerVolumeRemoveExecutor(volume string, force bool) common.Executor { func NewDockerVolumeRemoveExecutor(volume string, force bool) common.Executor {
+7 -7
View File
@@ -9,10 +9,9 @@ package container
import ( import (
"context" "context"
"gitea.com/gitea/act_runner/act/common" "gitea.com/gitea/runner/act/common"
"github.com/docker/docker/api/types/filters" "github.com/moby/moby/client"
"github.com/docker/docker/api/types/volume"
) )
func NewDockerVolumeRemoveExecutor(volumeName string, force bool) common.Executor { func NewDockerVolumeRemoveExecutor(volumeName string, force bool) common.Executor {
@@ -23,12 +22,12 @@ func NewDockerVolumeRemoveExecutor(volumeName string, force bool) common.Executo
} }
defer cli.Close() defer cli.Close()
list, err := cli.VolumeList(ctx, volume.ListOptions{Filters: filters.NewArgs()}) list, err := cli.VolumeList(ctx, client.VolumeListOptions{})
if err != nil { if err != nil {
return err return err
} }
for _, vol := range list.Volumes { for _, vol := range list.Items {
if vol.Name == volumeName { if vol.Name == volumeName {
return removeExecutor(volumeName, force)(ctx) return removeExecutor(volumeName, force)(ctx)
} }
@@ -42,7 +41,7 @@ func NewDockerVolumeRemoveExecutor(volumeName string, force bool) common.Executo
func removeExecutor(volume string, force bool) common.Executor { func removeExecutor(volume string, force bool) common.Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
logger := common.Logger(ctx) logger := common.Logger(ctx)
logger.Debugf("%sdocker volume rm %s", logPrefix, volume) logger.Debugf("docker volume rm %s", volume)
if common.Dryrun(ctx) { if common.Dryrun(ctx) {
return nil return nil
@@ -54,6 +53,7 @@ func removeExecutor(volume string, force bool) common.Executor {
} }
defer cli.Close() defer cli.Close()
return cli.VolumeRemove(ctx, volume, force) _, err = cli.VolumeRemove(ctx, volume, client.VolumeRemoveOptions{Force: force})
return err
} }
} }
+117 -18
View File
@@ -16,12 +16,15 @@ import (
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strconv"
"strings" "strings"
"sync"
"sync/atomic"
"time" "time"
"gitea.com/gitea/act_runner/act/common" "gitea.com/gitea/runner/act/common"
"gitea.com/gitea/act_runner/act/filecollector" "gitea.com/gitea/runner/act/filecollector"
"gitea.com/gitea/act_runner/act/lookpath" "gitea.com/gitea/runner/act/lookpath"
"github.com/go-git/go-billy/v5/helper/polyfill" "github.com/go-git/go-billy/v5/helper/polyfill"
"github.com/go-git/go-billy/v5/osfs" "github.com/go-git/go-billy/v5/osfs"
@@ -34,9 +37,16 @@ type HostEnvironment struct {
TmpDir string TmpDir string
ToolCache string ToolCache string
Workdir string Workdir string
// CleanWorkdir means teardown owns Workdir and may delete it. Leave false
// when Workdir points at a caller-owned checkout (e.g. `act` local mode).
CleanWorkdir bool
ActPath string ActPath string
CleanUp func() CleanUp func()
StdOut io.Writer StdOut io.Writer
AllocatePTY bool // allocate a pseudo-TTY for each step's process
mu sync.Mutex
runningPIDs map[int]struct{}
} }
func (e *HostEnvironment) Create(_, _ []string) common.Executor { func (e *HostEnvironment) Create(_, _ []string) common.Executor {
@@ -192,12 +202,12 @@ func (e *HostEnvironment) Start(_ bool) common.Executor {
type ptyWriter struct { type ptyWriter struct {
Out io.Writer Out io.Writer
AutoStop bool AutoStop atomic.Bool
dirtyLine bool dirtyLine bool
} }
func (w *ptyWriter) Write(buf []byte) (int, error) { func (w *ptyWriter) Write(buf []byte) (int, error) {
if w.AutoStop && len(buf) > 0 && buf[len(buf)-1] == 4 { if w.AutoStop.Load() && len(buf) > 0 && buf[len(buf)-1] == 4 {
n, err := w.Out.Write(buf[:len(buf)-1]) n, err := w.Out.Write(buf[:len(buf)-1])
if err != nil { if err != nil {
return n, err return n, err
@@ -327,36 +337,54 @@ func (e *HostEnvironment) exec(ctx context.Context, command []string, cmdline st
tty.Close() tty.Close()
} }
}() }()
if true /* allocate Terminal */ { if e.AllocatePTY {
var err error var err error
ppty, tty, err = setupPty(cmd, cmdline) ppty, tty, err = setupPty(cmd, cmdline)
if err != nil { if err != nil {
common.Logger(ctx).Debugf("Failed to setup Pty %v\n", err.Error()) common.Logger(ctx).Debugf("Failed to setup Pty %v\n", err.Error())
} }
} }
writer := &ptyWriter{Out: e.StdOut} var writer *ptyWriter
logctx, finishLog := context.WithCancel(context.Background()) var logctx context.Context
if ppty != nil { if ppty != nil {
writer = &ptyWriter{Out: e.StdOut}
var finishLog context.CancelFunc
logctx, finishLog = context.WithCancel(context.Background())
go copyPtyOutput(writer, ppty, finishLog) go copyPtyOutput(writer, ppty, finishLog)
} else {
finishLog()
}
if ppty != nil {
go writeKeepAlive(ppty) go writeKeepAlive(ppty)
} }
err = cmd.Run() // Split Start/Wait so the PID can be registered before the process can exit;
// cmd.Run() would block until exit, by which time the PID may have been reused.
if err := cmd.Start(); err != nil {
return err
}
if cmd.Process != nil {
e.mu.Lock()
if e.runningPIDs == nil {
e.runningPIDs = map[int]struct{}{}
}
e.runningPIDs[cmd.Process.Pid] = struct{}{}
e.mu.Unlock()
defer func(pid int) {
e.mu.Lock()
delete(e.runningPIDs, pid)
e.mu.Unlock()
}(cmd.Process.Pid)
}
err = cmd.Wait()
if err != nil { if err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
return ExitCodeError(exitErr.ExitCode())
}
return err return err
} }
if tty != nil { if tty != nil {
writer.AutoStop = true writer.AutoStop.Store(true)
if _, err := tty.WriteString("\x04"); err != nil { if _, err := tty.WriteString("\x04"); err != nil {
common.Logger(ctx).Debug("Failed to write EOT") common.Logger(ctx).Debug("Failed to write EOT")
} }
}
<-logctx.Done() <-logctx.Done()
if ppty != nil {
ppty.Close() ppty.Close()
ppty = nil ppty = nil
} }
@@ -385,12 +413,83 @@ func (e *HostEnvironment) UpdateFromEnv(srcPath string, env *map[string]string)
return parseEnvFile(e, srcPath, env) return parseEnvFile(e, srcPath, env)
} }
func removePathWithRetry(ctx context.Context, path string) error {
if path == "" {
return nil
}
attempts := 1
delay := time.Duration(0)
if runtime.GOOS == "windows" {
attempts = 5
delay = 200 * time.Millisecond
}
var lastErr error
for i := 0; i < attempts; i++ {
if i > 0 {
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(delay):
}
}
lastErr = os.RemoveAll(path)
if lastErr == nil {
return nil
}
}
return lastErr
}
func (e *HostEnvironment) terminateRunningProcesses(ctx context.Context) {
if runtime.GOOS != "windows" {
return
}
e.mu.Lock()
pids := make([]int, 0, len(e.runningPIDs))
for pid := range e.runningPIDs {
pids = append(pids, pid)
}
e.mu.Unlock()
if len(pids) == 0 {
return
}
logger := common.Logger(ctx)
for _, pid := range pids {
// Best-effort: forcibly terminate process tree to release file handles
// so that workspace cleanup can succeed on Windows.
cmd := exec.CommandContext(ctx, "taskkill", "/PID", strconv.Itoa(pid), "/T", "/F")
out, err := cmd.CombinedOutput()
if err != nil {
logger.Debugf("taskkill failed for pid=%d: %v output=%s", pid, err, strings.TrimSpace(string(out)))
}
}
}
func (e *HostEnvironment) Remove() common.Executor { func (e *HostEnvironment) Remove() common.Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
// Ensure any lingering child processes are ended before attempting
// to remove the workspace (Windows file locks otherwise prevent cleanup).
e.terminateRunningProcesses(ctx)
// Only removes per-job misc state. Must not remove the cache/toolcache root.
if e.CleanUp != nil { if e.CleanUp != nil {
e.CleanUp() e.CleanUp()
} }
return os.RemoveAll(e.Path) logger := common.Logger(ctx)
var errs []error
if err := removePathWithRetry(ctx, e.Path); err != nil {
logger.Warnf("failed to remove host misc state %s: %v", e.Path, err)
errs = append(errs, err)
}
if e.CleanWorkdir {
if err := removePathWithRetry(ctx, e.Workdir); err != nil {
logger.Warnf("failed to remove host workspace %s: %v", e.Workdir, err)
errs = append(errs, err)
}
}
return errors.Join(errs...)
} }
} }
+118
View File
@@ -6,14 +6,21 @@ package container
import ( import (
"archive/tar" "archive/tar"
"bytes"
"context" "context"
"io" "io"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"runtime"
"strings"
"testing" "testing"
"gitea.com/gitea/runner/act/common"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
// Type assert HostEnvironment implements ExecutionsEnvironment // Type assert HostEnvironment implements ExecutionsEnvironment
@@ -69,3 +76,114 @@ func TestGetContainerArchive(t *testing.T) {
_, err = reader.Next() _, err = reader.Next()
assert.ErrorIs(t, err, io.EOF) assert.ErrorIs(t, err, io.EOF)
} }
func TestHostEnvironmentExecExitCode(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("uses POSIX shell")
}
dir := t.TempDir()
ctx := context.Background()
e := &HostEnvironment{
Path: filepath.Join(dir, "path"),
TmpDir: filepath.Join(dir, "tmp"),
ToolCache: filepath.Join(dir, "tool_cache"),
ActPath: filepath.Join(dir, "act_path"),
StdOut: io.Discard,
Workdir: filepath.Join(dir, "path"),
}
for _, p := range []string{e.Path, e.TmpDir, e.ToolCache, e.ActPath} {
assert.NoError(t, os.MkdirAll(p, 0o700)) //nolint:testifylint // test setup
}
err := e.Exec([]string{"sh", "-c", "exit 3"}, map[string]string{"PATH": os.Getenv("PATH")}, "", "")(ctx)
var exitErr ExitCodeError
require.ErrorAs(t, err, &exitErr)
assert.Equal(t, ExitCodeError(3), exitErr)
assert.Equal(t, "Process completed with exit code 3.", err.Error())
}
func TestHostEnvironmentAllocatePTY(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("uses POSIX shell")
}
for _, tc := range []struct {
name string
allocPTY bool
expect string
}{
{name: "off", allocPTY: false, expect: "NOTTY"},
{name: "on", allocPTY: true, expect: "TTY"},
} {
t.Run(tc.name, func(t *testing.T) {
dir := t.TempDir()
buf := &bytes.Buffer{}
e := &HostEnvironment{
Path: filepath.Join(dir, "path"),
TmpDir: filepath.Join(dir, "tmp"),
ToolCache: filepath.Join(dir, "tool_cache"),
ActPath: filepath.Join(dir, "act_path"),
StdOut: buf,
Workdir: filepath.Join(dir, "path"),
AllocatePTY: tc.allocPTY,
}
for _, p := range []string{e.Path, e.TmpDir, e.ToolCache, e.ActPath} {
require.NoError(t, os.MkdirAll(p, 0o700))
}
err := e.Exec(
[]string{"sh", "-c", "[ -t 1 ] && printf TTY || printf NOTTY"},
map[string]string{"PATH": os.Getenv("PATH")}, "", "",
)(context.Background())
require.NoError(t, err)
got := strings.TrimSpace(strings.ReplaceAll(buf.String(), "\r", ""))
assert.Equal(t, tc.expect, got)
})
}
}
func TestHostEnvironmentRemovePreservesWorkdirByDefault(t *testing.T) {
logger := logrus.New()
ctx := common.WithLogger(context.Background(), logrus.NewEntry(logger))
base := t.TempDir()
miscRoot := filepath.Join(base, "misc")
path := filepath.Join(miscRoot, "hostexecutor")
require.NoError(t, os.MkdirAll(path, 0o700))
workdir := filepath.Join(base, "workspace", "owner", "repo")
require.NoError(t, os.MkdirAll(workdir, 0o700))
e := &HostEnvironment{
Path: path,
Workdir: workdir,
CleanUp: func() {
_ = os.RemoveAll(miscRoot)
},
StdOut: os.Stdout,
}
require.NoError(t, e.Remove()(ctx))
_, err := os.Stat(workdir)
require.NoError(t, err)
}
func TestHostEnvironmentRemoveCleansWorkdirWhenOwned(t *testing.T) {
logger := logrus.New()
ctx := common.WithLogger(context.Background(), logrus.NewEntry(logger))
base := t.TempDir()
miscRoot := filepath.Join(base, "misc")
path := filepath.Join(miscRoot, "hostexecutor")
require.NoError(t, os.MkdirAll(path, 0o700))
workdir := filepath.Join(base, "workspace", "123", "owner", "repo")
require.NoError(t, os.MkdirAll(workdir, 0o700))
e := &HostEnvironment{
Path: path,
Workdir: workdir,
CleanWorkdir: true,
CleanUp: func() {
_ = os.RemoveAll(miscRoot)
},
StdOut: os.Stdout,
}
require.NoError(t, e.Remove()(ctx))
_, err := os.Stat(workdir)
assert.ErrorIs(t, err, os.ErrNotExist)
}
+9 -1
View File
@@ -12,7 +12,7 @@ import (
"io" "io"
"strings" "strings"
"gitea.com/gitea/act_runner/act/common" "gitea.com/gitea/runner/act/common"
) )
func parseEnvFile(e Container, srcPath string, env *map[string]string) common.Executor { func parseEnvFile(e Container, srcPath string, env *map[string]string) common.Executor {
@@ -29,6 +29,8 @@ func parseEnvFile(e Container, srcPath string, env *map[string]string) common.Ex
return err return err
} }
s := bufio.NewScanner(reader) s := bufio.NewScanner(reader)
// Default 64 KiB max token size is too small for realistic env-file lines; allow up to 16 MiB.
s.Buffer(make([]byte, 0, 64*1024), 16*1024*1024)
for s.Scan() { for s.Scan() {
line := s.Text() line := s.Text()
singleLineEnv := strings.Index(line, "=") singleLineEnv := strings.Index(line, "=")
@@ -50,6 +52,9 @@ func parseEnvFile(e Container, srcPath string, env *map[string]string) common.Ex
} }
multiLineEnvContent += content multiLineEnvContent += content
} }
if err := s.Err(); err != nil {
return fmt.Errorf("reading env file: %w", err)
}
if !delimiterFound { if !delimiterFound {
return fmt.Errorf("invalid format delimiter '%v' not found before end of file", multiLineEnvDelimiter) return fmt.Errorf("invalid format delimiter '%v' not found before end of file", multiLineEnvDelimiter)
} }
@@ -58,6 +63,9 @@ func parseEnvFile(e Container, srcPath string, env *map[string]string) common.Ex
return fmt.Errorf("invalid format '%v', expected a line with '=' or '<<'", line) return fmt.Errorf("invalid format '%v', expected a line with '=' or '<<'", line)
} }
} }
if err := s.Err(); err != nil {
return fmt.Errorf("reading env file: %w", err)
}
env = &localEnv env = &localEnv
return nil return nil
} }
+75
View File
@@ -0,0 +1,75 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package container
import (
"bufio"
"context"
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func newTestHostEnv(t *testing.T) (*HostEnvironment, string) {
t.Helper()
e := &HostEnvironment{Path: t.TempDir()}
return e, filepath.Join(e.Path, "envfile")
}
func TestParseEnvFileSingleLine(t *testing.T) {
e, envPath := newTestHostEnv(t)
require.NoError(t, os.WriteFile(envPath, []byte("FOO=bar\nBAZ=qux\n"), 0o600))
env := map[string]string{}
require.NoError(t, parseEnvFile(e, envPath, &env)(context.Background()))
assert.Equal(t, "bar", env["FOO"])
assert.Equal(t, "qux", env["BAZ"])
}
func TestParseEnvFileMultiLine(t *testing.T) {
e, envPath := newTestHostEnv(t)
content := "FOO<<EOF\nline1\nline2\nEOF\n"
require.NoError(t, os.WriteFile(envPath, []byte(content), 0o600))
env := map[string]string{}
require.NoError(t, parseEnvFile(e, envPath, &env)(context.Background()))
assert.Equal(t, "line1\nline2", env["FOO"])
}
func TestParseEnvFileLargeValueWithinLimit(t *testing.T) {
e, envPath := newTestHostEnv(t)
big := strings.Repeat("x", 2*1024*1024)
content := "FOO<<EOF\n" + big + "\nEOF\n"
require.NoError(t, os.WriteFile(envPath, []byte(content), 0o600))
env := map[string]string{}
require.NoError(t, parseEnvFile(e, envPath, &env)(context.Background()))
assert.Equal(t, big, env["FOO"])
}
func TestParseEnvFileLineExceedsBufferReportsScannerError(t *testing.T) {
e, envPath := newTestHostEnv(t)
tooBig := strings.Repeat("x", 17*1024*1024) // over the 16 MiB cap
content := "FOO<<EOF\n" + tooBig + "\nEOF\n"
require.NoError(t, os.WriteFile(envPath, []byte(content), 0o600))
env := map[string]string{}
err := parseEnvFile(e, envPath, &env)(context.Background())
require.ErrorIs(t, err, bufio.ErrTooLong)
assert.Contains(t, err.Error(), "reading env file")
}
func TestParseEnvFileMissingDelimiter(t *testing.T) {
e, envPath := newTestHostEnv(t)
require.NoError(t, os.WriteFile(envPath, []byte("FOO<<EOF\nline1\nline2\n"), 0o600))
env := map[string]string{}
err := parseEnvFile(e, envPath, &env)(context.Background())
require.Error(t, err)
assert.Contains(t, err.Error(), "delimiter")
}
+1 -1
View File
@@ -18,7 +18,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"gitea.com/gitea/act_runner/act/model" "gitea.com/gitea/runner/act/model"
"github.com/go-git/go-git/v5/plumbing/format/gitignore" "github.com/go-git/go-git/v5/plumbing/format/gitignore"
"github.com/rhysd/actionlint" "github.com/rhysd/actionlint"
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"gitea.com/gitea/act_runner/act/model" "gitea.com/gitea/runner/act/model"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
+3 -3
View File
@@ -12,7 +12,7 @@ import (
"reflect" "reflect"
"strings" "strings"
"gitea.com/gitea/act_runner/act/model" "gitea.com/gitea/runner/act/model"
"github.com/rhysd/actionlint" "github.com/rhysd/actionlint"
) )
@@ -251,7 +251,7 @@ func (impl *interperterImpl) evaluateArrayDeref(arrayDerefNode *actionlint.Array
func (impl *interperterImpl) getPropertyValue(left reflect.Value, property string) (value any, err error) { func (impl *interperterImpl) getPropertyValue(left reflect.Value, property string) (value any, err error) {
switch left.Kind() { switch left.Kind() {
case reflect.Ptr: case reflect.Pointer:
return impl.getPropertyValue(left.Elem(), property) return impl.getPropertyValue(left.Elem(), property)
case reflect.Struct: case reflect.Struct:
@@ -321,7 +321,7 @@ func (impl *interperterImpl) getPropertyValue(left reflect.Value, property strin
} }
func (impl *interperterImpl) getMapValue(value reflect.Value) (any, error) { func (impl *interperterImpl) getMapValue(value reflect.Value) (any, error) {
if value.Kind() == reflect.Ptr { if value.Kind() == reflect.Pointer {
return impl.getMapValue(value.Elem()) return impl.getMapValue(value.Elem())
} }
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"math" "math"
"testing" "testing"
"gitea.com/gitea/act_runner/act/model" "gitea.com/gitea/runner/act/model"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
+7 -1
View File
@@ -73,10 +73,16 @@ func (cc *CopyCollector) WriteFile(fpath string, fi fs.FileInfo, linkName string
if err := os.MkdirAll(filepath.Dir(fdestpath), 0o777); err != nil { if err := os.MkdirAll(filepath.Dir(fdestpath), 0o777); err != nil {
return err return err
} }
// Remove any existing destination so we can overwrite read-only files
// (e.g. git pack files at mode 0444 trip EACCES on macOS and "Access is
// denied" on Windows when reopened with O_WRONLY) and so os.Symlink does
// not fail with EEXIST. os.Remove clears the Windows read-only attribute
// internally; on Unix unlink only needs write permission on the parent.
_ = os.Remove(fdestpath)
if f == nil { if f == nil {
return os.Symlink(linkName, fdestpath) return os.Symlink(linkName, fdestpath)
} }
df, err := os.OpenFile(fdestpath, os.O_CREATE|os.O_WRONLY, fi.Mode()) df, err := os.OpenFile(fdestpath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, fi.Mode())
if err != nil { if err != nil {
return err return err
} }
+47
View File
@@ -8,7 +8,9 @@ import (
"archive/tar" "archive/tar"
"context" "context"
"io" "io"
"os"
"path/filepath" "path/filepath"
"runtime"
"strings" "strings"
"testing" "testing"
@@ -20,6 +22,7 @@ import (
"github.com/go-git/go-git/v5/plumbing/format/index" "github.com/go-git/go-git/v5/plumbing/format/index"
"github.com/go-git/go-git/v5/storage/filesystem" "github.com/go-git/go-git/v5/storage/filesystem"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
type memoryFs struct { type memoryFs struct {
@@ -174,3 +177,47 @@ func TestSymlinks(t *testing.T) {
assert.Equal(t, ".env", files["test.env"].Linkname) assert.Equal(t, ".env", files["test.env"].Linkname)
assert.ErrorIs(t, err, io.EOF, "tar must be read cleanly to EOF") assert.ErrorIs(t, err, io.EOF, "tar must be read cleanly to EOF")
} }
// Regression for https://gitea.com/gitea/runner/issues/876 and /941:
// re-copying an action directory must overwrite a pre-existing read-only
// file (e.g. a git pack .idx at mode 0444) instead of failing with EACCES
// on macOS or "Access is denied" on Windows.
func TestCopyCollectorWriteFileOverwritesReadOnlyFile(t *testing.T) {
dst := t.TempDir()
target := filepath.Join(dst, "sub", "pack.idx")
require.NoError(t, os.MkdirAll(filepath.Dir(target), 0o755))
require.NoError(t, os.WriteFile(target, []byte("old"), 0o444))
src := filepath.Join(t.TempDir(), "pack.idx")
require.NoError(t, os.WriteFile(src, []byte("new"), 0o444))
fi, err := os.Stat(src)
require.NoError(t, err)
cc := &CopyCollector{DstDir: dst}
require.NoError(t, cc.WriteFile("sub/pack.idx", fi, "", strings.NewReader("new")))
got, err := os.ReadFile(target)
require.NoError(t, err)
assert.Equal(t, "new", string(got))
}
// Without the destination removal, os.Symlink fails with EEXIST when the
// path already holds a regular file from an earlier copy of the action.
func TestCopyCollectorWriteFileOverwritesFileWithSymlink(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("creating symlinks requires elevated privileges on Windows")
}
dst := t.TempDir()
target := filepath.Join(dst, "link")
require.NoError(t, os.WriteFile(target, []byte("stale"), 0o644))
fi, err := os.Lstat(target)
require.NoError(t, err)
cc := &CopyCollector{DstDir: dst}
require.NoError(t, cc.WriteFile("link", fi, "target", nil))
resolved, err := os.Readlink(target)
require.NoError(t, err)
assert.Equal(t, "target", resolved)
}
-12
View File
@@ -4,18 +4,6 @@
package lookpath package lookpath
import "os"
type Env interface { type Env interface {
Getenv(name string) string Getenv(name string) string
} }
type defaultEnv struct{}
func (*defaultEnv) Getenv(name string) string {
return os.Getenv(name)
}
func LookPath(file string) (string, error) {
return LookPath2(file, &defaultEnv{})
}
+2 -2
View File
@@ -9,8 +9,8 @@ import (
"fmt" "fmt"
"strings" "strings"
"gitea.com/gitea/act_runner/act/common" "gitea.com/gitea/runner/act/common"
"gitea.com/gitea/act_runner/act/common/git" "gitea.com/gitea/runner/act/common/git"
) )
type GithubContext struct { type GithubContext struct {
Loaded 50 of 1190 files, more files were not shown because too many files have changed in this diff. Show more