28 Commits

Author SHA1 Message Date
silverwind
1e71a8f891 Merge branch 'main' into renovate/github.com-docker-go-connections-0.x 2026-05-06 00:06:52 +00:00
Renovate Bot
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 <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/924
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-05-05 23:14:36 +00:00
silverwind
c36198ab55 Merge branch 'main' into renovate/github.com-docker-go-connections-0.x 2026-05-05 23:13:49 +00:00
silverwind
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 <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/927
Reviewed-by: Nicolas <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
2026-05-05 20:17:32 +00:00
Nicolas
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 <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/870
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
2026-05-05 20:11:44 +00:00
Nicolas
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 <me@silverwind.io>
Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/883
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
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 <2021+silverwind@noreply.gitea.com>
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
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 <bircni@icloud.com>
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
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 <bircni@icloud.com>
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-05-03 04:44:33 +00:00
Renovate Bot
0060993e76 fix(deps): update module github.com/docker/go-connections to v0.7.0 2026-05-02 00:16:53 +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 <xiaolunwen@gmail.com>
Co-authored-by: Nicolas <bircni@icloud.com>
Co-committed-by: Nicolas <bircni@icloud.com>
2026-05-01 16:37:37 +00:00
Renovate Bot
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](e72d34d97e)), 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](84f8c52872))

### [`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](158a1fdcbc)), 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](ce5984e4b0)), 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](e2779f4203))

### [`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](f898f8ea85))

### [`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](c2d03cf6db))
- add `ncc --version` and `ncc --help` ([#&#8203;1030](https://github.com/vercel/ncc/issues/1030)) ([d38b619](d38b619554))

### [`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](44e2eac6c9))
- node 18 by update source-map used by Terser to 0.7.4 ([#&#8203;999](https://github.com/vercel/ncc/issues/999)) ([2f69f83](2f69f838aa))

##### Features

- add semantic-release to autopublish ([#&#8203;1015](https://github.com/vercel/ncc/issues/1015)) ([be3405d](be3405dbc3)), 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 <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/881
Reviewed-by: Nicolas <bircni@icloud.com>
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
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 <bircni@icloud.com>
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-05-01 08:48:49 +00:00
Renovate Bot
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 <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/873
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-04-30 21:03:33 +00:00
Lunny Xiao
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 <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/850
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Reviewed-by: Nicolas <bircni@icloud.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-committed-by: Lunny Xiao <xiaolunwen@gmail.com>
2026-04-30 20:12:51 +00:00
Nicolas
8e6b3be96a docs: Update docs (#872)
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/872
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Nicolas <bircni@icloud.com>
Co-committed-by: Nicolas <bircni@icloud.com>
2026-04-30 18:29:59 +00:00
Bo-Yi Wu
e5e53c732e perf(config): lower default fetch_interval_max from 60s to 5s (#875)
## Summary

- Lower the default `fetch_interval_max` from 60s to 5s to reduce job pickup latency for common single-runner deployments
- PR #819 optimized defaults for 200-runner fleets, regressing single-runner pickup time from ~2s to ~65s
- Most deployments use few runners; large fleets can still tune this value higher in their config

## Impact

| `fetch_interval_max` | 1 runner pickup | 200 runners idle |
| -------------------- | --------------- | ---------------- |
| 60s (previous)       | up to **65s**   | 3.3 req/s        |
| **5s (new default)** | up to **5s**    | 40 req/s         |

Closes https://gitea.com/gitea/act_runner/issues/869

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/875
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-committed-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-04-30 17:40:35 +00:00
silverwind
2516573592 chore: clean up nolint directives in act package (#864)
Removes 88 `nolint` directives (386 → 298) via mechanical, zero-regression cleanups:

- **38 `bodyclose`** in `act/artifactcache/handler_test.go`: replaced by `defer resp.Body.Close()` after each HTTP call.
- **21 dead directives** (`gocyclo`, `dogsled`, `contextcheck`): none of these linters are enabled in `.golangci.yml`, so the directives were doing nothing.
- **29 `testifylint`** directives whose underlying issues were addressed by mechanical rewrites:
  - `assert.Nil(t, err)` → `assert.NoError(t, err)`
  - `assert.NotNil(t, err)` → `assert.Error(t, err)`
  - `assert.Equal(t, true/false, x)` → `assert.True/False(t, x)`
  - `assert.Equal(t, 0, len(x))` → `assert.Empty(t, x)`
  - `assert.Equal(t, N, len(x))` → `assert.Len(t, x, N)`
  - `assert.Len(t, x, 0)` → `assert.Empty(t, x)`

Many `testifylint` directives still apply because they flag `require-error` (i.e. testifylint wants `require.NoError` instead of `assert.NoError` for early bail-out). That's a behavior change (fail-fast vs continue) and out of scope for this purely mechanical cleanup — those can be addressed in a follow-up. Same for `expected-actual`, `equal-values`, `error-is-as`, and the remaining `nilnil` / `unparam` / `forbidigo` / `staticcheck` / `goheader` / `dupl` directives.

`golangci-lint run` is clean. Tests pass for all touched packages.

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

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/864
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-committed-by: silverwind <2021+silverwind@noreply.gitea.com>
2026-04-29 18:32:55 +00:00
Renovate Bot
35834bf817 fix(deps): update module github.com/moby/patternmatcher to v0.6.1 (#868)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [github.com/moby/patternmatcher](https://github.com/moby/patternmatcher) | `v0.6.0` → `v0.6.1` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fmoby%2fpatternmatcher/v0.6.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fmoby%2fpatternmatcher/v0.6.0/v0.6.1?slim=true) |

---

### Release Notes

<details>
<summary>moby/patternmatcher (github.com/moby/patternmatcher)</summary>

### [`v0.6.1`](https://github.com/moby/patternmatcher/releases/tag/v0.6.1)

[Compare Source](https://github.com/moby/patternmatcher/compare/v0.6.0...v0.6.1)

#### What's Changed

- fix panic /  nil pointer dereference on invalid patterns [#&#8203;9](https://github.com/moby/patternmatcher/pull/9)
- ci: update actions and test against "oldest", "oldstable" and "stable" [#&#8203;8](https://github.com/moby/patternmatcher/pull/8)

**Full Changelog**: <https://github.com/moby/patternmatcher/compare/v0.6.0...v0.6.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:eyJjcmVhdGVkSW5WZXIiOiI0My4xNDUuMCIsInVwZGF0ZWRJblZlciI6IjQzLjE0NS4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/868
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-04-29 00:47:19 +00:00
Renovate Bot
11a5dc8936 fix(deps): update module github.com/docker/docker to v25.0.15+incompatible (#867)
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/docker](https://github.com/docker/docker) | `v25.0.14+incompatible` → `v25.0.15+incompatible` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fdocker%2fdocker/v25.0.15+incompatible?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fdocker%2fdocker/v25.0.14+incompatible/v25.0.15+incompatible?slim=true) |

---

### Release Notes

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

### [`v25.0.15+incompatible`](https://github.com/docker/docker/compare/v25.0.14...v25.0.15)

[Compare Source](https://github.com/docker/docker/compare/v25.0.14...v25.0.15)

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

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/867
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-04-29 00:46:57 +00:00
Zettat123
f09fafcb0a Clone different git repos in parallel via per-directory locks (#866)
Old `cloneLock` is a package-level `sync.Mutex` that serialized every action clone across all goroutines, regardless of target directory.
This PR replaces it with a `sync.Map` of per-directory mutexes keyed by `input.Dir`. Same-directory operations still serialize; different directories now clone in parallel.

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/866
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
2026-04-28 21:39:41 +00:00
Morgan Peyre
801e5cf4d5 fix: avoid 'filename too long' on matrix jobs by hashing container names (#853)
## Bug fixes

- Fixed "file name too long" errors when running matrix jobs with many or
  long-valued matrix parameters. Docker volume and container names are now
  bounded to a safe length by hashing the full name with SHA-256, keeping
  the result well within the filesystem `NAME_MAX` limit (255 bytes) even
  after Docker appends its own suffixes (`-env`, `-network`, etc.).

## Upgrade notes

This change renames the Docker containers, volumes and networks created by
the runner. Resources created by a previous version will **not** be cleaned
up automatically after upgrade and will become orphans.

After upgrading, you can remove the legacy resources with:

```sh
# volumes
docker volume ls -q | grep -E '^GITEA-ACTIONS-TASK-[0-9]+_' | xargs -r docker volume rm

# networks
docker network ls --format '{{.Name}}' | grep -E '^GITEA-ACTIONS-TASK-[0-9]+_.*-network$' | xargs -r docker network rm
```

> **Note:** If multiple act_runner instances share the same Docker daemon,
> make sure no runner using the old version is running before executing the
> cleanup commands above.

Fixes #686

---------

Co-authored-by: Nicolas <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/853
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: Morgan Peyre <195218+peyremorgan@noreply.gitea.com>
Co-committed-by: Morgan Peyre <195218+peyremorgan@noreply.gitea.com>
2026-04-28 00:30:27 +00:00
Renovate Bot
3f05040438 fix(deps): update module github.com/mattn/go-isatty to v0.0.22 (#863)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [github.com/mattn/go-isatty](https://github.com/mattn/go-isatty) | `v0.0.20` → `v0.0.22` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fmattn%2fgo-isatty/v0.0.22?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fmattn%2fgo-isatty/v0.0.20/v0.0.22?slim=true) |

---

### Release Notes

<details>
<summary>mattn/go-isatty (github.com/mattn/go-isatty)</summary>

### [`v0.0.22`](https://github.com/mattn/go-isatty/compare/v0.0.21...v0.0.22)

[Compare Source](https://github.com/mattn/go-isatty/compare/v0.0.21...v0.0.22)

### [`v0.0.21`](https://github.com/mattn/go-isatty/compare/v0.0.20...v0.0.21)

[Compare Source](https://github.com/mattn/go-isatty/compare/v0.0.20...v0.0.21)

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

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/863
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-04-28 00:15:16 +00:00
Renovate Bot
59d90bff26 fix(deps): update module github.com/docker/docker to v25.0.14+incompatible (#862)
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/docker](https://github.com/docker/docker) | `v25.0.13+incompatible` → `v25.0.14+incompatible` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fdocker%2fdocker/v25.0.14+incompatible?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fdocker%2fdocker/v25.0.13+incompatible/v25.0.14+incompatible?slim=true) |

---

### Release Notes

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

### [`v25.0.14+incompatible`](https://github.com/docker/docker/compare/v25.0.13...v25.0.14)

[Compare Source](https://github.com/docker/docker/compare/v25.0.13...v25.0.14)

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

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/862
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-04-28 00:14:44 +00:00
silverwind
5edc4ba550 Authenticate cache requests via ACTIONS_RUNTIME_TOKEN and scope by repo (#849)
Closes #848. Addresses [GHSA-82g9-637c-2fx2](https://github.com/go-gitea/gitea/security/advisories/GHSA-82g9-637c-2fx2) and the follow-up points raised by @ChristopherHX and @haroutp in that thread.

The change is breaking only for `cache.external_server` which uses auth via a pre-shared secret.

## How auth works now

1. **Runner starts** → opens the embedded cache server on `:port`. Loads / creates a 32-byte HMAC signing key in `<cache-dir>/.secret`.
2. **Runner receives a task** → calls `handler.RegisterJob(ACTIONS_RUNTIME_TOKEN, repository)` before the job runs, defers a revoker that removes the credential on completion. Registrations are reference-counted so a stray re-register cannot revoke a live job.
3. **Job container runs `actions/cache`** → the toolkit sends `Authorization: Bearer $ACTIONS_RUNTIME_TOKEN` on every management call (`reserve`, `upload`, `commit`, `find`, `clean`). The cache server's middleware looks the token up in the registered-jobs map: miss → 401; hit → the job's repository is injected into the request context.
4. **Repository scoping** — every cache entry is stamped with `Repo` on reserve; `find`, `upload`, `commit` all verify the caller's repo matches. A job in repo A cannot see or poison a cache entry owned by repo B, even when both reach the server over the same docker bridge. GC dedup also groups by `(Repo, Key, Version)` so one repo can't age out another.
5. **Archive downloads** — `@actions/cache` does not attach Authorization when downloading `archiveLocation`, so the `find` response is a short-lived HMAC-signed URL: `…/artifacts/:id?exp=<unix>&sig=<hmac>`, 10-minute TTL, signature binds `cacheID:exp`. Tampered, expired, or foreign-secret URLs get 401.
6. **Defence-in-depth** — `ACTIONS_RUNTIME_TOKEN` is added to `task.Secrets` so the runner's log masker scrubs it from step output.

## `cache.external_server` (standalone `act_runner cache-server`)

Operators set `cache.external_secret` to the same value on the runner config and the `act_runner cache-server` config. The `cache-server` then runs with bearer auth on the cache API and exposes a control-plane at `POST /_internal/{register,revoke}` (gated by the shared secret). The runner pre-registers each task's `ACTIONS_RUNTIME_TOKEN` with the remote server before the job runs and revokes it on completion. Same per-job auth + repo scoping as the embedded handler, just over the network.

`cache-server` refuses to start without `cache.external_secret`; runner config load also fails when `cache.external_server` is set without `cache.external_secret`.

## User-facing changes

- **One-time cache miss after upgrade.** Pre-existing entries in `bolt.db` have no `Repo` stamp and won't match any job — they'll be evicted by the normal GC. First job per cache key rebuilds its cache.
- **`cache.external_server` deployments must add `cache.external_secret`.** Breaking change for anyone running a standalone `act_runner cache-server`: set the same `cache.external_secret` in both the runner config and the cache-server config. Without it neither side starts.
- **No config changes required for the default setup.** Runners using the embedded cache server (the common case) keep working without any yaml edits; the auth mechanism is invisible to workflows.

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

---------

Co-authored-by: Nicolas <bircni@icloud.com>
Co-authored-by: Christopher Homberger <christopher.homberger@web.de>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/849
Reviewed-by: ChristopherHX <38043+christopherhx@noreply.gitea.com>
2026-04-27 23:59:20 +00:00
Nicolas
547a0ff297 feat: show run command, shell and env in collapsible group before step output (#847)
## Summary

Mirrors the GitHub Actions runner behaviour where each `run:` step shows a collapsible **"Run \<command\>"** section containing the script, shell command, and environment variables before the actual step output.

### What changes

- **`pkg/runner/step_run.go`**: In `stepRun.main()`, two new executors are added to the pipeline:
  1. `logRunGroupHeader()` — runs after `setupShellCommandExecutor()` (so `sr.cmdline` is already resolved). Emits a `::group::Run <step>` log entry followed by the interpolated script, the full shell command line, and the step's env vars (sorted, internal vars filtered out).
  2. The existing execution function now has `defer rawLogger.Infof("::endgroup::")` so the group is closed after the step finishes, regardless of success or failure.

### Env var filtering

Internal runner vars are hidden (`GITHUB_*`, `GITEA_*`, `RUNNER_*`, `INPUT_*`, `PATH`, `HOME`) — only user-relevant vars are shown, matching what GitHub Actions displays.

### Example output

```
▼ Run cargo build
  cargo build
  shell: bash --noprofile --norc -e -o pipefail {0}
  env:
    CARGO_HOME: /home/runner/.cargo
    CARGO_INCREMENTAL: 0
    CARGO_TERM_COLOR: always
  <actual build output>
```

---------

Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/847
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Reviewed-by: ChristopherHX <38043+christopherhx@noreply.gitea.com>
2026-04-27 16:31:56 +00:00
Mirko Sekulic
f2b4dbf05f run docker step in host mode (#857)
## Problem
In host executor mode, uses: docker://<image> step actions fail because
act/runner/step_docker.go always attaches the step container to the job
container's network namespace, which doesn't exist in host mode.

### Example
Run following job in host runner

```yaml
jobs:
  test:
    runs-on: ubuntu-latest-host
    steps:
      - uses: docker://alpine:3.20
        with:
          args: echo hello
```
```
Error:
  failed to start container: Error response from daemon:
    joining network namespace of container:
    No such container: xxxxxx
```

This pr allows the docker step in the host mode

## Testing
I tested following steps on host runner and it worked

```yaml

 - name: Test azure cli action in host mode
   uses: azure/cli@v2
   env:
     RUNNER_OS: Linux
   with:
     inlineScript: echo "hello from azure cli"
 
 - uses: docker://alpine:3.20
   with:
     args: echo hello

```

---------

Co-authored-by: Nicolas <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/857
Reviewed-by: Nicolas <bircni@icloud.com>
Co-authored-by: Mirko Sekulic <misha.sekulic@gmail.com>
Co-committed-by: Mirko Sekulic <misha.sekulic@gmail.com>
2026-04-27 15:26:31 +00:00
silverwind
bad4239d18 chore(deps): drop unused distribution/reference replace directive (#858)
The `replace github.com/distribution/reference v0.6.0 => v0.5.0` was added defensively in [c4b57fbc](c4b57fbcb2) (#775) against `github.com/docker/distribution v2.8.3`, whose `reference_deprecated.go` calls the now-removed `reference.SplitHostname`. However, nothing in act_runner's build graph imports `github.com/docker/distribution/reference`, so the file is never compiled and the replace has no effect.

Verified locally: removing the directive followed by `go mod tidy` keeps `distribution/reference` at v0.6.0, drops `docker/distribution` from the graph entirely, and `go build ./...` / `go vet ./...` both pass.

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

Reviewed-on: https://gitea.com/gitea/act_runner/pulls/858
Reviewed-by: Nicolas <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
2026-04-27 15:14:36 +00:00
120 changed files with 3188 additions and 641 deletions

View File

@@ -40,7 +40,7 @@ cpu.out
*.db
*.log
/act_runner
/gitea-runner
/debug
/bin

View File

@@ -69,7 +69,7 @@ jobs:
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Echo the tag
run: echo "${{ env.DOCKER_ORG }}/act_runner:nightly${{ matrix.variant.tag_suffix }}"
run: echo "${{ env.DOCKER_ORG }}/runner:nightly${{ matrix.variant.tag_suffix }}"
- name: Build and push
uses: docker/build-push-action@v6
@@ -82,4 +82,4 @@ jobs:
linux/arm64
push: true
tags: |
${{ env.DOCKER_ORG }}/act_runner:nightly${{ matrix.variant.tag_suffix }}
${{ env.DOCKER_ORG }}/runner:nightly${{ matrix.variant.tag_suffix }}

View File

@@ -17,7 +17,7 @@ jobs:
go-version-file: "go.mod"
- name: Import GPG key
id: import_gpg
uses: crazy-max/ghaction-import-gpg@v6
uses: crazy-max/ghaction-import-gpg@v7
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.PASSPHRASE }}
@@ -71,17 +71,12 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Repo Meta
id: repo_meta
run: |
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
- name: "Docker meta"
id: docker_meta
uses: https://github.com/docker/metadata-action@v5
with:
images: |
${{ env.DOCKER_ORG }}/${{ steps.repo_meta.outputs.REPO_NAME }}
${{ env.DOCKER_ORG }}/runner
tags: |
type=semver,pattern={{major}}.{{minor}}.{{patch}}
type=semver,pattern={{major}}.{{minor}}

View File

@@ -1,7 +1,9 @@
name: checks
on:
- push
- pull_request
push:
branches:
- main
pull_request:
jobs:
lint:
@@ -17,4 +19,4 @@ jobs:
- name: build
run: make build
- name: test
run: make test
run: make test

2
.gitignore vendored
View File

@@ -1,4 +1,4 @@
/act_runner
/gitea-runner
.env
.runner
coverage.txt

View File

@@ -114,7 +114,7 @@ formatters:
custom-order: true
sections:
- standard
- prefix(gitea.com/gitea/act_runner)
- prefix(gitea.com/gitea/runner)
- blank
- default
gofumpt:

View File

@@ -1,5 +1,7 @@
version: 2
project_name: gitea-runner
before:
hooks:
- go mod tidy
@@ -63,7 +65,7 @@ builds:
flags:
- -trimpath
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: >-
{{ .ProjectName }}-
{{- .Version }}-
@@ -86,7 +88,7 @@ blobs:
provider: s3
bucket: "{{ .Env.S3_BUCKET }}"
region: "{{ .Env.S3_REGION }}"
directory: "act_runner/{{.Version}}"
directory: "gitea-runner/{{.Version}}"
extra_files:
- glob: ./**.xz
- glob: ./**.sha256

View File

@@ -9,19 +9,19 @@ RUN apk add --no-cache make git
ARG GOPROXY
ENV GOPROXY=${GOPROXY:-}
COPY . /opt/src/act_runner
WORKDIR /opt/src/act_runner
COPY . /opt/src/runner
WORKDIR /opt/src/runner
RUN make clean && make build
### DIND VARIANT
#
#
FROM docker:28-dind AS dind
FROM docker:29-dind AS dind
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/s6 /etc/s6
@@ -32,12 +32,12 @@ ENTRYPOINT ["s6-svscan","/etc/s6"]
### DIND-ROOTLESS VARIANT
#
#
FROM docker:28-dind-rootless AS dind-rootless
FROM docker:29-dind-rootless AS dind-rootless
USER root
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/s6 /etc/s6
@@ -56,7 +56,7 @@ ENTRYPOINT ["s6-svscan","/etc/s6"]
FROM alpine AS basic
RUN apk add --no-cache tini bash git tzdata
COPY --from=builder /opt/src/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
VOLUME /data

View File

@@ -1,5 +1,5 @@
DIST := dist
EXECUTABLE := act_runner
EXECUTABLE := gitea-runner
DIST_DIRS := $(DIST)/binaries $(DIST)/release
GO ?= go
SHASUM ?= shasum -a 256
@@ -13,7 +13,7 @@ DARWIN_ARCHS ?= darwin-12/amd64,darwin-12/arm64
WINDOWS_ARCHS ?= windows/amd64
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_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)
DOCKER_ROOTLESS_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)-dind-rootless
@@ -67,7 +67,7 @@ else
endif
TAGS ?=
LDFLAGS ?= -X "gitea.com/gitea/act_runner/internal/pkg/ver.version=v$(RELASE_VERSION)"
LDFLAGS ?= -X "gitea.com/gitea/runner/internal/pkg/ver.version=v$(RELASE_VERSION)"
.PHONY: all
all: build
@@ -86,7 +86,7 @@ go-check:
$(eval MIN_GO_VERSION := $(shell printf "%03d%03d" $(shell echo '$(MIN_GO_VERSION_STR)' | tr '.' ' ')))
$(eval GO_VERSION := $(shell printf "%03d%03d" $(shell $(GO) version | grep -Eo '[0-9]+\.[0-9]+' | tr '.' ' ');))
@if [ "$(GO_VERSION)" -lt "$(MIN_GO_VERSION)" ]; then \
echo "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; \
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
.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)'
.PHONY: build
build: go-check $(EXECUTABLE) ## build the act_runner binary
build: go-check $(EXECUTABLE) ## build the runner binary
$(EXECUTABLE): $(GOFILES)
$(GO) build -v -tags '$(TAGS)' -ldflags '-s -w $(EXTLDFLAGS) $(LDFLAGS)' -o $@

View File

@@ -1,6 +1,4 @@
# act runner
Act runner is a runner for Gitea.
# Gitea Runner
## Installation
@@ -10,7 +8,7 @@ Docker Engine Community version is required for docker mode. To install Docker C
### 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
@@ -26,8 +24,8 @@ make docker
## Quickstart
Actions are disabled by default, so you need to add the following to the configuration file of your Gitea instance to enable it:
Actions are disabled by default, so you need to add the following to the configuration file of your Gitea instance to enable it:
```ini
[actions]
ENABLED=true
@@ -36,7 +34,7 @@ ENABLED=true
### Register
```bash
./act_runner register
./gitea-runner register
```
And you will be asked to input:
@@ -68,7 +66,7 @@ INFO Runner registered successfully.
You can also register with command line arguments.
```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.
@@ -76,32 +74,69 @@ If the registry succeed, it will run immediately. Next time, you could run the r
### Run
```bash
./act_runner daemon
./gitea-runner daemon
```
### Run with docker
```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
You can also configure the runner with a configuration file.
The configuration file is a YAML file, you can generate a sample configuration file with `./act_runner generate-config`.
The runner is configured with a YAML file. Generate a starting point (this matches what ships in the tree):
```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
./act_runner -c config.yaml register # register with config file
./act_runner -c config.yaml daemon # run with config file
./gitea-runner -c config.yaml register
./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

View File

@@ -5,21 +5,28 @@
package artifactcache
import (
"context"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"gitea.com/gitea/act_runner/act/common"
"gitea.com/gitea/runner/act/common"
"github.com/julienschmidt/httprouter"
"github.com/sirupsen/logrus"
@@ -28,9 +35,36 @@ import (
)
const (
urlBase = "/_apis/artifactcache"
apiPath = "/_apis/artifactcache"
internalPath = "/_internal"
// artifactURLTTL bounds how long a signed artifactLocation URL stays valid.
// Short enough that a leaked URL is near-worthless; long enough to let the
// @actions/cache client download a big blob that was returned from /cache.
artifactURLTTL = 10 * time.Minute
)
type credKey struct{}
// JobCredential ties a per-job bearer token (ACTIONS_RUNTIME_TOKEN) to the
// repository that owns it. Every cache entry is stamped with Repo on
// reserve/commit and checked on read/write so one repo can never observe or
// poison another repo's cache, even from inside a container that reaches the
// cache server over the docker bridge network.
type JobCredential struct {
Repo string
}
// credEntry holds a registered job's credential along with an active
// registration count. RegisterJob is reference-counted so that if two tasks
// briefly share an ACTIONS_RUNTIME_TOKEN — e.g. a runner that retries a task
// after a crash before the old registration is revoked — the first task's
// revoker does not cut the second task's auth out from under it.
type credEntry struct {
cred JobCredential
refs int
}
type Handler struct {
dir string
storage *Storage
@@ -43,10 +77,36 @@ type Handler struct {
gcAt time.Time
outboundIP string
// internalSecret guards /_internal/{register,revoke}. When set, a remote
// runner can use these endpoints to pre-register per-job
// ACTIONS_RUNTIME_TOKENs against this server, enabling the same
// per-job auth and repo scoping as the embedded handler over the
// network. Empty disables the control-plane entirely.
internalSecret string
// secret signs short-lived artifact download URLs. The @actions/cache
// toolkit does not send Authorization on the download request, so blob
// GETs authenticate via a per-URL HMAC signature with expiry rather than
// via the bearer token used for management endpoints.
secret []byte
credMu sync.RWMutex
creds map[string]*credEntry
}
func StartHandler(dir, outboundIP string, port uint16, logger logrus.FieldLogger) (*Handler, error) {
h := &Handler{}
// StartHandler opens the on-disk cache store and starts the HTTP server.
//
// internalSecret, when non-empty, enables a control-plane API at
// /_internal/{register,revoke} that lets a remote runner pre-register the
// per-job ACTIONS_RUNTIME_TOKENs it expects this server to honor. The
// embedded in-process handler leaves it empty and registers tokens via the
// in-process RegisterJob method directly.
func StartHandler(dir, outboundIP string, port uint16, internalSecret string, logger logrus.FieldLogger) (*Handler, error) {
h := &Handler{
creds: make(map[string]*credEntry),
internalSecret: internalSecret,
}
if logger == nil {
discard := logrus.New()
@@ -83,19 +143,37 @@ func StartHandler(dir, outboundIP string, port uint16, logger logrus.FieldLogger
h.outboundIP = ip.String()
}
secret, err := loadOrCreateSecret(dir)
if err != nil {
return nil, err
}
h.secret = secret
router := httprouter.New()
router.GET(urlBase+"/cache", h.middleware(h.find))
router.POST(urlBase+"/caches", h.middleware(h.reserve))
router.PATCH(urlBase+"/caches/:id", h.middleware(h.upload))
router.POST(urlBase+"/caches/:id", h.middleware(h.commit))
router.GET(urlBase+"/artifacts/:id", h.middleware(h.get))
router.POST(urlBase+"/clean", h.middleware(h.clean))
router.GET(apiPath+"/cache", h.bearerAuth(h.find))
router.POST(apiPath+"/caches", h.bearerAuth(h.reserve))
router.PATCH(apiPath+"/caches/:id", h.bearerAuth(h.upload))
router.POST(apiPath+"/caches/:id", h.bearerAuth(h.commit))
router.POST(apiPath+"/clean", h.bearerAuth(h.clean))
// Artifact GET is signed via query-string HMAC because @actions/cache
// does not attach Authorization when downloading archiveLocation.
router.GET(apiPath+"/artifacts/:id", h.signedURLAuth(h.get))
// Control-plane: a remote runner registers/revokes per-job tokens so the
// cache API can authenticate them. Always wired so the routes exist; the
// handlers themselves 401 when internalSecret is unset.
router.POST(internalPath+"/register", h.internalAuth(h.internalRegister))
router.POST(internalPath+"/revoke", h.internalAuth(h.internalRevoke))
h.router = router
h.gcCache()
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) // listen on all interfaces
// Listen on all interfaces. Binding to outboundIP only would give no real
// security benefit (it is the LAN/internet-facing address either way) and
// can break Docker Desktop variants where the host's outbound IP is not
// routable from inside the container network. Authentication is enforced
// by the bearer middleware and per-repo scoping, not by reachability.
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
return nil, err
}
@@ -121,6 +199,91 @@ func (h *Handler) ExternalURL() string {
h.listener.Addr().(*net.TCPAddr).Port)
}
// RegisterJob makes token a valid bearer credential for cache requests from
// the given repository and returns a function that removes it. The runner
// calls this at job start and defers the returned func so that the credential
// is only accepted while the job is running.
//
// Registrations are reference-counted: if a token is already registered, the
// existing repo is kept and the refcount is incremented. The entry is
// removed only when every revoker returned by RegisterJob has been called.
// This keeps a stray re-registration from silently revoking a live job.
func (h *Handler) RegisterJob(token, repo string) func() {
if h == nil || token == "" {
return func() {}
}
h.credMu.Lock()
if existing, ok := h.creds[token]; ok {
existing.refs++
} else {
h.creds[token] = &credEntry{
cred: JobCredential{Repo: repo},
refs: 1,
}
}
h.credMu.Unlock()
return func() {
h.credMu.Lock()
if entry, ok := h.creds[token]; ok {
entry.refs--
if entry.refs <= 0 {
delete(h.creds, token)
}
}
h.credMu.Unlock()
}
}
// RevokeJob explicitly revokes one registration of token, mirroring one call
// of the closure returned by RegisterJob. Used by the control-plane endpoint
// so a remote runner can revoke without holding the closure.
func (h *Handler) RevokeJob(token string) {
if h == nil || token == "" {
return
}
h.credMu.Lock()
if entry, ok := h.creds[token]; ok {
entry.refs--
if entry.refs <= 0 {
delete(h.creds, token)
}
}
h.credMu.Unlock()
}
func (h *Handler) lookupCredential(token string) (JobCredential, bool) {
h.credMu.RLock()
entry, ok := h.creds[token]
h.credMu.RUnlock()
if !ok {
return JobCredential{}, false
}
return entry.cred, true
}
// loadOrCreateSecret returns the 32-byte HMAC signing key for artifact URLs,
// persisted in dir/.secret so signed URLs handed out before a restart stay
// valid across the restart and so the standalone cache-server can be pointed
// at by config.Cache.ExternalServer without the URL rotating.
func loadOrCreateSecret(dir string) ([]byte, error) {
path := filepath.Join(dir, ".secret")
if data, err := os.ReadFile(path); err == nil {
if secret, err := hex.DecodeString(strings.TrimSpace(string(data))); err == nil && len(secret) >= 32 {
return secret, nil
}
} else if !os.IsNotExist(err) {
return nil, fmt.Errorf("read cache secret: %w", err)
}
secret := make([]byte, 32)
if _, err := rand.Read(secret); err != nil {
return nil, fmt.Errorf("generate cache secret: %w", err)
}
if err := os.WriteFile(path, []byte(hex.EncodeToString(secret)), 0o600); err != nil {
return nil, fmt.Errorf("write cache secret: %w", err)
}
return secret, nil
}
func (h *Handler) Close() error {
if h == nil {
return nil
@@ -160,6 +323,7 @@ func (h *Handler) openDB() (*bolthold.Store, error) {
// GET /_apis/artifactcache/cache
func (h *Handler) find(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
cred := credFromContext(r.Context())
keys := strings.Split(r.URL.Query().Get("keys"), ",")
// cache keys are case insensitive
for i, key := range keys {
@@ -174,7 +338,7 @@ func (h *Handler) find(w http.ResponseWriter, r *http.Request, _ httprouter.Para
}
defer db.Close()
cache, err := findCache(db, keys, version)
cache, err := findCache(db, cred.Repo, keys, version)
if err != nil {
h.responseJSON(w, r, 500, err)
return
@@ -194,13 +358,14 @@ func (h *Handler) find(w http.ResponseWriter, r *http.Request, _ httprouter.Para
}
h.responseJSON(w, r, 200, map[string]any{
"result": "hit",
"archiveLocation": fmt.Sprintf("%s%s/artifacts/%d", h.ExternalURL(), urlBase, cache.ID),
"archiveLocation": h.signedArtifactURL(cache.ID, time.Now().Add(artifactURLTTL)),
"cacheKey": cache.Key,
})
}
// POST /_apis/artifactcache/caches
func (h *Handler) reserve(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
cred := credFromContext(r.Context())
api := &Request{}
if err := json.NewDecoder(r.Body).Decode(api); err != nil {
h.responseJSON(w, r, 400, err)
@@ -210,6 +375,7 @@ func (h *Handler) reserve(w http.ResponseWriter, r *http.Request, _ httprouter.P
api.Key = strings.ToLower(api.Key)
cache := api.ToCache()
cache.Repo = cred.Repo
db, err := h.openDB()
if err != nil {
h.responseJSON(w, r, 500, err)
@@ -231,6 +397,7 @@ func (h *Handler) reserve(w http.ResponseWriter, r *http.Request, _ httprouter.P
// PATCH /_apis/artifactcache/caches/:id
func (h *Handler) upload(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
cred := credFromContext(r.Context())
id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
if err != nil {
h.responseJSON(w, r, 400, err)
@@ -253,6 +420,11 @@ func (h *Handler) upload(w http.ResponseWriter, r *http.Request, params httprout
return
}
if cache.Repo != cred.Repo {
h.responseJSON(w, r, 403, fmt.Errorf("cache %d: forbidden", id))
return
}
if cache.Complete {
h.responseJSON(w, r, 400, fmt.Errorf("cache %v %q: already complete", cache.ID, cache.Key))
return
@@ -272,6 +444,7 @@ func (h *Handler) upload(w http.ResponseWriter, r *http.Request, params httprout
// POST /_apis/artifactcache/caches/:id
func (h *Handler) commit(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
cred := credFromContext(r.Context())
id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
if err != nil {
h.responseJSON(w, r, 400, err)
@@ -294,6 +467,11 @@ func (h *Handler) commit(w http.ResponseWriter, r *http.Request, params httprout
return
}
if cache.Repo != cred.Repo {
h.responseJSON(w, r, 403, fmt.Errorf("cache %d: forbidden", id))
return
}
if cache.Complete {
h.responseJSON(w, r, 400, fmt.Errorf("cache %v %q: already complete", cache.ID, cache.Key))
return
@@ -326,6 +504,10 @@ func (h *Handler) commit(w http.ResponseWriter, r *http.Request, params httprout
}
// GET /_apis/artifactcache/artifacts/:id
// Authenticated via signed URL (see signedURLAuth), not bearer, because the
// @actions/cache toolkit downloads archiveLocation without Authorization.
// Repository scoping is already enforced at find() time; the signature binds
// the URL to the specific cache ID and an expiry.
func (h *Handler) get(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
if err != nil {
@@ -344,21 +526,158 @@ func (h *Handler) clean(w http.ResponseWriter, r *http.Request, _ httprouter.Par
h.responseJSON(w, r, 200)
}
func (h *Handler) middleware(handler httprouter.Handle) httprouter.Handle {
// bearerAuth resolves ACTIONS_RUNTIME_TOKEN against the set of currently
// registered jobs. A match attaches the job's JobCredential to the request
// context; a miss returns 401 before the handler body runs.
func (h *Handler) bearerAuth(handler httprouter.Handle) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
h.logger.Debugf("%s %s", r.Method, r.RequestURI)
h.logger.Debugf("%s %s", r.Method, r.URL.Path)
token := bearerToken(r)
if token == "" {
h.responseJSON(w, r, http.StatusUnauthorized, errors.New("missing bearer token"))
return
}
cred, ok := h.lookupCredential(token)
if !ok {
h.responseJSON(w, r, http.StatusUnauthorized, errors.New("unknown bearer token"))
return
}
ctx := context.WithValue(r.Context(), credKey{}, cred)
handler(w, r.WithContext(ctx), params)
go h.gcCache()
}
}
func (h *Handler) signedURLAuth(handler httprouter.Handle) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
h.logger.Debugf("%s %s", r.Method, r.URL.Path)
id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
if err != nil {
h.responseJSON(w, r, 400, err)
return
}
expStr := r.URL.Query().Get("exp")
sig := r.URL.Query().Get("sig")
if expStr == "" || sig == "" {
h.responseJSON(w, r, http.StatusUnauthorized, errors.New("missing signature"))
return
}
exp, err := strconv.ParseInt(expStr, 10, 64)
if err != nil {
h.responseJSON(w, r, http.StatusUnauthorized, errors.New("invalid expiry"))
return
}
if time.Now().Unix() > exp {
h.responseJSON(w, r, http.StatusUnauthorized, errors.New("signature expired"))
return
}
expected := h.computeSignature(id, exp)
if !hmac.Equal([]byte(sig), []byte(expected)) {
h.responseJSON(w, r, http.StatusUnauthorized, errors.New("bad signature"))
return
}
handler(w, r, params)
go h.gcCache()
}
}
// internalAuth gates the control-plane endpoints. The bearer must
// constant-time-equal the configured internalSecret. If the secret is empty,
// the control-plane is disabled and every request gets 404 — which matches
// the upstream nektos/act behavior of "the route does not exist".
func (h *Handler) internalAuth(handler httprouter.Handle) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
if h.internalSecret == "" {
http.NotFound(w, r)
return
}
token := bearerToken(r)
if token == "" || !hmac.Equal([]byte(token), []byte(h.internalSecret)) {
h.responseJSON(w, r, http.StatusUnauthorized, errors.New("internal: bad secret"))
return
}
handler(w, r, params)
}
}
type internalRegisterBody struct {
Token string `json:"token"`
Repo string `json:"repo"`
}
type internalRevokeBody struct {
Token string `json:"token"`
}
// POST /_internal/register
func (h *Handler) internalRegister(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
var body internalRegisterBody
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
h.responseJSON(w, r, http.StatusBadRequest, err)
return
}
if body.Token == "" {
h.responseJSON(w, r, http.StatusBadRequest, errors.New("token is required"))
return
}
h.RegisterJob(body.Token, body.Repo)
h.responseJSON(w, r, http.StatusOK)
}
// POST /_internal/revoke
func (h *Handler) internalRevoke(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
var body internalRevokeBody
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
h.responseJSON(w, r, http.StatusBadRequest, err)
return
}
if body.Token == "" {
h.responseJSON(w, r, http.StatusBadRequest, errors.New("token is required"))
return
}
h.RevokeJob(body.Token)
h.responseJSON(w, r, http.StatusOK)
}
func bearerToken(r *http.Request) string {
auth := r.Header.Get("Authorization")
const prefix = "Bearer "
if len(auth) > len(prefix) && strings.EqualFold(auth[:len(prefix)], prefix) {
return auth[len(prefix):]
}
return ""
}
func credFromContext(ctx context.Context) JobCredential {
if cred, ok := ctx.Value(credKey{}).(JobCredential); ok {
return cred
}
return JobCredential{}
}
func (h *Handler) computeSignature(cacheID, exp int64) string {
mac := hmac.New(sha256.New, h.secret)
fmt.Fprintf(mac, "%d:%d", cacheID, exp)
return hex.EncodeToString(mac.Sum(nil))
}
func (h *Handler) signedArtifactURL(cacheID uint64, exp time.Time) string {
expUnix := exp.Unix()
sig := h.computeSignature(int64(cacheID), expUnix)
q := url.Values{}
q.Set("exp", strconv.FormatInt(expUnix, 10))
q.Set("sig", sig)
return fmt.Sprintf("%s%s/artifacts/%d?%s", h.ExternalURL(), apiPath, cacheID, q.Encode())
}
// if not found, return (nil, nil) instead of an error.
func findCache(db *bolthold.Store, keys []string, version string) (*Cache, error) {
func findCache(db *bolthold.Store, repo string, keys []string, version string) (*Cache, error) {
cache := &Cache{}
for _, prefix := range keys {
// if a key in the list matches exactly, don't return partial matches
if err := db.FindOne(cache,
bolthold.Where("Key").Eq(prefix).
bolthold.Where("Repo").Eq(repo).
And("Key").Eq(prefix).
And("Version").Eq(version).
And("Complete").Eq(true).
SortBy("CreatedAt").Reverse()); err == nil || !errors.Is(err, bolthold.ErrNotFound) {
@@ -373,7 +692,8 @@ func findCache(db *bolthold.Store, keys []string, version string) (*Cache, error
continue
}
if err := db.FindOne(cache,
bolthold.Where("Key").RegExp(re).
bolthold.Where("Repo").Eq(repo).
And("Key").RegExp(re).
And("Version").Eq(version).
And("Complete").Eq(true).
SortBy("CreatedAt").Reverse()); err != nil {
@@ -419,7 +739,6 @@ const (
keepOld = 5 * time.Minute
)
//nolint:gocyclo // function handles many cases
func (h *Handler) gcCache() {
if h.gcing.Load() {
return
@@ -494,12 +813,16 @@ func (h *Handler) gcCache() {
}
}
// Remove the old caches with the same key and version, keep the latest one.
// Remove the old caches with the same key and version within the same
// repository, keep the latest one. Aggregation must include Repo so two
// repos that happen to share a (key, version) do not evict each other —
// otherwise per-repo scoping holds for reads but one repo can age
// another out after keepOld.
// Also keep the olds which have been used recently for a while in case of the cache is still in use.
if results, err := db.FindAggregate(
&Cache{},
bolthold.Where("Complete").Eq(true),
"Key", "Version",
"Repo", "Key", "Version",
); err != nil {
h.logger.Warnf("find aggregate caches: %v", err)
} else {
@@ -533,7 +856,7 @@ func (h *Handler) responseJSON(w http.ResponseWriter, r *http.Request, code int,
if len(v) == 0 || v[0] == nil {
data, _ = json.Marshal(struct{}{})
} else if err, ok := v[0].(error); ok {
h.logger.Errorf("%v %v: %v", r.Method, r.RequestURI, err)
h.logger.Errorf("%v %v: %v", r.Method, r.URL.Path, err)
data, _ = json.Marshal(map[string]any{
"error": err.Error(),
})

View File

@@ -22,12 +22,38 @@ import (
"go.etcd.io/bbolt"
)
// testToken is registered with the cache server in every test that needs to
// make authenticated requests; testClient then attaches it as the
// Authorization: Bearer header. testRepo is the repository scope used when
// registering it; cross-repo isolation is exercised in its own test.
const (
testToken = "test-runtime-token"
testRepo = "owner/repo"
)
type bearerTransport struct{ token string }
func (b *bearerTransport) RoundTrip(r *http.Request) (*http.Response, error) {
r.Header.Set("Authorization", "Bearer "+b.token)
return http.DefaultTransport.RoundTrip(r)
}
var testClient = &http.Client{Transport: &bearerTransport{token: testToken}}
// signArtifactURL builds a signed download URL the same way the server does;
// tests use it to reach the get handler directly without going through a
// find/cache-hit round trip.
func signArtifactURL(h *Handler, id int64) string {
return h.signedArtifactURL(uint64(id), time.Now().Add(artifactURLTTL))
}
func TestHandler(t *testing.T) {
dir := filepath.Join(t.TempDir(), "artifactcache")
handler, err := StartHandler(dir, "", 0, nil)
handler, err := StartHandler(dir, "", 0, "", nil)
require.NoError(t, err)
handler.RegisterJob(testToken, testRepo)
base := fmt.Sprintf("%s%s", handler.ExternalURL(), urlBase)
base := fmt.Sprintf("%s%s", handler.ExternalURL(), apiPath)
defer func() {
t.Run("inpect db", func(t *testing.T) {
@@ -45,7 +71,10 @@ func TestHandler(t *testing.T) {
require.NoError(t, handler.Close())
assert.Nil(t, handler.server)
assert.Nil(t, handler.listener)
_, err := http.Post(fmt.Sprintf("%s/caches/%d", base, 1), "", nil) //nolint:bodyclose // pre-existing issue from nektos/act
resp, err := testClient.Post(fmt.Sprintf("%s/caches/%d", base, 1), "", nil)
if err == nil {
resp.Body.Close()
}
assert.Error(t, err)
})
}()
@@ -53,8 +82,9 @@ func TestHandler(t *testing.T) {
t.Run("get not exist", func(t *testing.T) {
key := strings.ToLower(t.Name())
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, key, version)) //nolint:bodyclose // pre-existing issue from nektos/act
resp, err := testClient.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, key, version))
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, 204, resp.StatusCode)
})
@@ -68,16 +98,18 @@ func TestHandler(t *testing.T) {
})
t.Run("clean", func(t *testing.T) {
resp, err := http.Post(base+"/clean", "", nil) //nolint:bodyclose // pre-existing issue from nektos/act
resp, err := testClient.Post(base+"/clean", "", nil)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
})
t.Run("reserve with bad request", func(t *testing.T) {
body := []byte(`invalid json`)
require.NoError(t, err)
resp, err := http.Post(base+"/caches", "application/json", bytes.NewReader(body)) //nolint:bodyclose // pre-existing issue from nektos/act
resp, err := testClient.Post(base+"/caches", "application/json", bytes.NewReader(body))
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 400, resp.StatusCode)
})
@@ -94,8 +126,9 @@ func TestHandler(t *testing.T) {
Size: 100,
})
require.NoError(t, err)
resp, err := http.Post(base+"/caches", "application/json", bytes.NewReader(body)) //nolint:bodyclose // pre-existing issue from nektos/act
resp, err := testClient.Post(base+"/caches", "application/json", bytes.NewReader(body))
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
require.NoError(t, json.NewDecoder(resp.Body).Decode(&first))
@@ -108,8 +141,9 @@ func TestHandler(t *testing.T) {
Size: 100,
})
require.NoError(t, err)
resp, err := http.Post(base+"/caches", "application/json", bytes.NewReader(body)) //nolint:bodyclose // pre-existing issue from nektos/act
resp, err := testClient.Post(base+"/caches", "application/json", bytes.NewReader(body))
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
require.NoError(t, json.NewDecoder(resp.Body).Decode(&second))
@@ -125,8 +159,9 @@ func TestHandler(t *testing.T) {
require.NoError(t, err)
req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("Content-Range", "bytes 0-99/*")
resp, err := http.DefaultClient.Do(req) //nolint:bodyclose // pre-existing issue from nektos/act
resp, err := testClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 400, resp.StatusCode)
})
@@ -136,8 +171,9 @@ func TestHandler(t *testing.T) {
require.NoError(t, err)
req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("Content-Range", "bytes 0-99/*")
resp, err := http.DefaultClient.Do(req) //nolint:bodyclose // pre-existing issue from nektos/act
resp, err := testClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 400, resp.StatusCode)
})
@@ -155,8 +191,9 @@ func TestHandler(t *testing.T) {
Size: 100,
})
require.NoError(t, err)
resp, err := http.Post(base+"/caches", "application/json", bytes.NewReader(body)) //nolint:bodyclose // pre-existing issue from nektos/act
resp, err := testClient.Post(base+"/caches", "application/json", bytes.NewReader(body))
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
got := struct {
@@ -171,13 +208,15 @@ func TestHandler(t *testing.T) {
require.NoError(t, err)
req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("Content-Range", "bytes 0-99/*")
resp, err := http.DefaultClient.Do(req) //nolint:bodyclose // pre-existing issue from nektos/act
resp, err := testClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
}
{
resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil) //nolint:bodyclose // pre-existing issue from nektos/act
resp, err := testClient.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
}
{
@@ -186,8 +225,9 @@ func TestHandler(t *testing.T) {
require.NoError(t, err)
req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("Content-Range", "bytes 0-99/*")
resp, err := http.DefaultClient.Do(req) //nolint:bodyclose // pre-existing issue from nektos/act
resp, err := testClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 400, resp.StatusCode)
}
})
@@ -206,8 +246,9 @@ func TestHandler(t *testing.T) {
Size: 100,
})
require.NoError(t, err)
resp, err := http.Post(base+"/caches", "application/json", bytes.NewReader(body)) //nolint:bodyclose // pre-existing issue from nektos/act
resp, err := testClient.Post(base+"/caches", "application/json", bytes.NewReader(body))
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
got := struct {
@@ -222,24 +263,27 @@ func TestHandler(t *testing.T) {
require.NoError(t, err)
req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("Content-Range", "bytes xx-99/*")
resp, err := http.DefaultClient.Do(req) //nolint:bodyclose // pre-existing issue from nektos/act
resp, err := testClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 400, resp.StatusCode)
}
})
t.Run("commit with bad id", func(t *testing.T) {
{
resp, err := http.Post(base+"/caches/invalid_id", "", nil) //nolint:bodyclose // pre-existing issue from nektos/act
resp, err := testClient.Post(base+"/caches/invalid_id", "", nil)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 400, resp.StatusCode)
}
})
t.Run("commit with not exist id", func(t *testing.T) {
{
resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, 100), "", nil) //nolint:bodyclose // pre-existing issue from nektos/act
resp, err := testClient.Post(fmt.Sprintf("%s/caches/%d", base, 100), "", nil)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 400, resp.StatusCode)
}
})
@@ -258,8 +302,9 @@ func TestHandler(t *testing.T) {
Size: 100,
})
require.NoError(t, err)
resp, err := http.Post(base+"/caches", "application/json", bytes.NewReader(body)) //nolint:bodyclose // pre-existing issue from nektos/act
resp, err := testClient.Post(base+"/caches", "application/json", bytes.NewReader(body))
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
got := struct {
@@ -274,18 +319,21 @@ func TestHandler(t *testing.T) {
require.NoError(t, err)
req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("Content-Range", "bytes 0-99/*")
resp, err := http.DefaultClient.Do(req) //nolint:bodyclose // pre-existing issue from nektos/act
resp, err := testClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
}
{
resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil) //nolint:bodyclose // pre-existing issue from nektos/act
resp, err := testClient.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
}
{
resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil) //nolint:bodyclose // pre-existing issue from nektos/act
resp, err := testClient.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 400, resp.StatusCode)
}
})
@@ -304,8 +352,9 @@ func TestHandler(t *testing.T) {
Size: 100,
})
require.NoError(t, err)
resp, err := http.Post(base+"/caches", "application/json", bytes.NewReader(body)) //nolint:bodyclose // pre-existing issue from nektos/act
resp, err := testClient.Post(base+"/caches", "application/json", bytes.NewReader(body))
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
got := struct {
@@ -320,32 +369,37 @@ func TestHandler(t *testing.T) {
require.NoError(t, err)
req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("Content-Range", "bytes 0-59/*")
resp, err := http.DefaultClient.Do(req) //nolint:bodyclose // pre-existing issue from nektos/act
resp, err := testClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
}
{
resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil) //nolint:bodyclose // pre-existing issue from nektos/act
resp, err := testClient.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 500, resp.StatusCode)
}
})
t.Run("get with bad id", func(t *testing.T) {
resp, err := http.Get(base + "/artifacts/invalid_id") //nolint:bodyclose // pre-existing issue from nektos/act
resp, err := testClient.Get(base + "/artifacts/invalid_id")
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, 400, resp.StatusCode)
})
t.Run("get with not exist id", func(t *testing.T) {
resp, err := http.Get(fmt.Sprintf("%s/artifacts/%d", base, 100)) //nolint:bodyclose // pre-existing issue from nektos/act
resp, err := testClient.Get(signArtifactURL(handler, 100))
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, 404, resp.StatusCode)
})
t.Run("get with not exist id", func(t *testing.T) {
resp, err := http.Get(fmt.Sprintf("%s/artifacts/%d", base, 100)) //nolint:bodyclose // pre-existing issue from nektos/act
resp, err := testClient.Get(signArtifactURL(handler, 100))
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, 404, resp.StatusCode)
})
@@ -375,8 +429,9 @@ func TestHandler(t *testing.T) {
key + "_a",
}, ",")
resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKeys, version)) //nolint:bodyclose // pre-existing issue from nektos/act
resp, err := testClient.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKeys, version))
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, 200, resp.StatusCode)
/*
@@ -395,8 +450,9 @@ func TestHandler(t *testing.T) {
assert.Equal(t, "hit", got.Result)
assert.Equal(t, keys[except], got.CacheKey)
contentResp, err := http.Get(got.ArchiveLocation) //nolint:bodyclose // pre-existing issue from nektos/act
contentResp, err := testClient.Get(got.ArchiveLocation)
require.NoError(t, err)
defer contentResp.Body.Close()
require.Equal(t, 200, contentResp.StatusCode)
content, err := io.ReadAll(contentResp.Body)
require.NoError(t, err)
@@ -413,8 +469,9 @@ func TestHandler(t *testing.T) {
{
reqKey := key + "_aBc"
resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKey, version)) //nolint:bodyclose // pre-existing issue from nektos/act
resp, err := testClient.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKey, version))
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, 200, resp.StatusCode)
got := struct {
Result string `json:"result"`
@@ -452,8 +509,9 @@ func TestHandler(t *testing.T) {
key + "_a_b",
}, ",")
resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKeys, version)) //nolint:bodyclose // pre-existing issue from nektos/act
resp, err := testClient.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKeys, version))
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, 200, resp.StatusCode)
/*
@@ -470,8 +528,9 @@ func TestHandler(t *testing.T) {
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
assert.Equal(t, keys[expect], got.CacheKey)
contentResp, err := http.Get(got.ArchiveLocation) //nolint:bodyclose // pre-existing issue from nektos/act
contentResp, err := testClient.Get(got.ArchiveLocation)
require.NoError(t, err)
defer contentResp.Body.Close()
require.Equal(t, 200, contentResp.StatusCode)
content, err := io.ReadAll(contentResp.Body)
require.NoError(t, err)
@@ -504,8 +563,9 @@ func TestHandler(t *testing.T) {
key + "_a_b",
}, ",")
resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKeys, version)) //nolint:bodyclose // pre-existing issue from nektos/act
resp, err := testClient.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKeys, version))
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, 200, resp.StatusCode)
/*
@@ -523,8 +583,9 @@ func TestHandler(t *testing.T) {
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
assert.Equal(t, keys[expect], got.CacheKey)
contentResp, err := http.Get(got.ArchiveLocation) //nolint:bodyclose // pre-existing issue from nektos/act
contentResp, err := testClient.Get(got.ArchiveLocation)
require.NoError(t, err)
defer contentResp.Body.Close()
require.Equal(t, 200, contentResp.StatusCode)
content, err := io.ReadAll(contentResp.Body)
require.NoError(t, err)
@@ -541,8 +602,9 @@ func uploadCacheNormally(t *testing.T, base, key, version string, content []byte
Size: int64(len(content)),
})
require.NoError(t, err)
resp, err := http.Post(base+"/caches", "application/json", bytes.NewReader(body)) //nolint:bodyclose // pre-existing issue from nektos/act
resp, err := testClient.Post(base+"/caches", "application/json", bytes.NewReader(body))
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
got := struct {
@@ -557,19 +619,22 @@ func uploadCacheNormally(t *testing.T, base, key, version string, content []byte
require.NoError(t, err)
req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("Content-Range", "bytes 0-99/*")
resp, err := http.DefaultClient.Do(req) //nolint:bodyclose // pre-existing issue from nektos/act
resp, err := testClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
}
{
resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil) //nolint:bodyclose // pre-existing issue from nektos/act
resp, err := testClient.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
}
var archiveLocation string
{
resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, key, version)) //nolint:bodyclose // pre-existing issue from nektos/act
resp, err := testClient.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, key, version))
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, 200, resp.StatusCode)
got := struct {
Result string `json:"result"`
@@ -582,8 +647,9 @@ func uploadCacheNormally(t *testing.T, base, key, version string, content []byte
archiveLocation = got.ArchiveLocation
}
{
resp, err := http.Get(archiveLocation) //nolint:bodyclose // pre-existing issue from nektos/act
resp, err := testClient.Get(archiveLocation)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, 200, resp.StatusCode)
got, err := io.ReadAll(resp.Body)
require.NoError(t, err)
@@ -593,7 +659,7 @@ func uploadCacheNormally(t *testing.T, base, key, version string, content []byte
func TestHandler_gcCache(t *testing.T) {
dir := filepath.Join(t.TempDir(), "artifactcache")
handler, err := StartHandler(dir, "", 0, nil)
handler, err := StartHandler(dir, "", 0, "", nil)
require.NoError(t, err)
defer func() {
@@ -699,3 +765,421 @@ func TestHandler_gcCache(t *testing.T) {
}
require.NoError(t, db.Close())
}
// TestHandler_RejectsMissingBearer covers the advisory's root cause:
// unauthenticated access to management endpoints is now refused with 401.
func TestHandler_RejectsMissingBearer(t *testing.T) {
dir := filepath.Join(t.TempDir(), "artifactcache")
handler, err := StartHandler(dir, "", 0, "", nil)
require.NoError(t, err)
defer handler.Close()
base := handler.ExternalURL() + apiPath
for _, tc := range []struct {
name string
method string
path string
body string
}{
{"find", http.MethodGet, "/cache?keys=x&version=y", ""},
{"reserve", http.MethodPost, "/caches", "{}"},
{"upload", http.MethodPatch, "/caches/1", ""},
{"commit", http.MethodPost, "/caches/1", ""},
{"clean", http.MethodPost, "/clean", ""},
} {
t.Run(tc.name, func(t *testing.T) {
req, err := http.NewRequest(tc.method, base+tc.path, strings.NewReader(tc.body))
require.NoError(t, err)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
resp.Body.Close()
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
})
}
}
// TestHandler_RejectsUnknownBearer verifies that a bearer token is only
// accepted after RegisterJob; stale/forged tokens cannot be replayed.
func TestHandler_RejectsUnknownBearer(t *testing.T) {
dir := filepath.Join(t.TempDir(), "artifactcache")
handler, err := StartHandler(dir, "", 0, "", nil)
require.NoError(t, err)
defer handler.Close()
base := handler.ExternalURL() + apiPath
req, err := http.NewRequest(http.MethodGet, base+"/cache?keys=x&version=y", nil)
require.NoError(t, err)
req.Header.Set("Authorization", "Bearer not-a-registered-token")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
resp.Body.Close()
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
}
// TestHandler_UnregisterRevokes ensures that the function returned by
// RegisterJob invalidates the credential, so a token leaked at job time stops
// working the moment the job ends instead of living for the runner's lifetime.
func TestHandler_UnregisterRevokes(t *testing.T) {
dir := filepath.Join(t.TempDir(), "artifactcache")
handler, err := StartHandler(dir, "", 0, "", nil)
require.NoError(t, err)
defer handler.Close()
unregister := handler.RegisterJob("tmp-token", testRepo)
base := handler.ExternalURL() + apiPath
req, err := http.NewRequest(http.MethodGet, base+"/cache?keys=x&version=y", nil)
require.NoError(t, err)
req.Header.Set("Authorization", "Bearer tmp-token")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
resp.Body.Close()
assert.NotEqual(t, http.StatusUnauthorized, resp.StatusCode)
unregister()
resp, err = http.DefaultClient.Do(req)
require.NoError(t, err)
resp.Body.Close()
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
}
// TestHandler_CrossRepoIsolation addresses the intra-runner poisoning vector
// raised in GHSA-82g9-637c-2fx2: job containers can reach the cache server
// over the docker bridge, so IP allowlisting alone does not stop a malicious
// PR run from another repo. A cache entry created under repoA must be
// invisible to queries scoped to repoB.
func TestHandler_CrossRepoIsolation(t *testing.T) {
dir := filepath.Join(t.TempDir(), "artifactcache")
handler, err := StartHandler(dir, "", 0, "", nil)
require.NoError(t, err)
defer handler.Close()
handler.RegisterJob("token-a", "owner/repoA")
handler.RegisterJob("token-b", "owner/repoB")
base := handler.ExternalURL() + apiPath
key := "shared-key"
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
content := []byte("repoA-payload")
clientA := &http.Client{Transport: &bearerTransport{token: "token-a"}}
clientB := &http.Client{Transport: &bearerTransport{token: "token-b"}}
// repoA reserves + uploads + commits.
reserveBody, err := json.Marshal(&Request{Key: key, Version: version, Size: int64(len(content))})
require.NoError(t, err)
resp, err := clientA.Post(base+"/caches", "application/json", bytes.NewReader(reserveBody))
require.NoError(t, err)
var reserved struct {
CacheID uint64 `json:"cacheId"`
}
require.NoError(t, json.NewDecoder(resp.Body).Decode(&reserved))
resp.Body.Close()
require.NotZero(t, reserved.CacheID)
req, err := http.NewRequest(http.MethodPatch, fmt.Sprintf("%s/caches/%d", base, reserved.CacheID), bytes.NewReader(content))
require.NoError(t, err)
req.Header.Set("Content-Range", fmt.Sprintf("bytes 0-%d/*", len(content)-1))
resp, err = clientA.Do(req)
require.NoError(t, err)
resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode)
resp, err = clientA.Post(fmt.Sprintf("%s/caches/%d", base, reserved.CacheID), "", nil)
require.NoError(t, err)
resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode)
// repoB with a matching key and version must NOT see repoA's cache.
resp, err = clientB.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, key, version))
require.NoError(t, err)
resp.Body.Close()
assert.Equal(t, http.StatusNoContent, resp.StatusCode)
// repoA still sees its own cache.
resp, err = clientA.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, key, version))
require.NoError(t, err)
resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
// repoB cannot upload to repoA's reserved id either (forbidden, not 401).
req, err = http.NewRequest(http.MethodPatch, fmt.Sprintf("%s/caches/%d", base, reserved.CacheID), bytes.NewReader([]byte("poison")))
require.NoError(t, err)
req.Header.Set("Content-Range", "bytes 0-5/*")
resp, err = clientB.Do(req)
require.NoError(t, err)
resp.Body.Close()
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
}
// TestHandler_ArtifactSignature verifies that archive downloads reject
// missing / tampered / expired signatures, so a leaked archiveLocation stops
// working after artifactURLTTL even if the bearer token is still registered.
func TestHandler_ArtifactSignature(t *testing.T) {
dir := filepath.Join(t.TempDir(), "artifactcache")
handler, err := StartHandler(dir, "", 0, "", nil)
require.NoError(t, err)
defer handler.Close()
handler.RegisterJob(testToken, testRepo)
base := handler.ExternalURL() + apiPath
t.Run("missing signature", func(t *testing.T) {
resp, err := testClient.Get(fmt.Sprintf("%s/artifacts/%d", base, 1))
require.NoError(t, err)
resp.Body.Close()
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
})
t.Run("tampered signature", func(t *testing.T) {
good := handler.signedArtifactURL(1, time.Now().Add(artifactURLTTL))
bad := good[:len(good)-4] + "dead"
resp, err := testClient.Get(bad)
require.NoError(t, err)
resp.Body.Close()
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
})
t.Run("expired signature", func(t *testing.T) {
expired := handler.signedArtifactURL(1, time.Now().Add(-time.Second))
resp, err := testClient.Get(expired)
require.NoError(t, err)
resp.Body.Close()
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
})
t.Run("signature from a different server", func(t *testing.T) {
dir2 := filepath.Join(t.TempDir(), "artifactcache2")
other, err := StartHandler(dir2, "", 0, "", nil)
require.NoError(t, err)
defer other.Close()
otherURL := other.signedArtifactURL(1, time.Now().Add(artifactURLTTL))
// Rewrite the host so the request still lands on our handler, but
// the signature was computed with a different secret.
parts := strings.SplitN(otherURL, apiPath, 2)
forged := base + parts[1]
resp, err := testClient.Get(forged)
require.NoError(t, err)
resp.Body.Close()
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
})
}
// TestHandler_SecretPersistsAcrossRestarts is the property that lets
// gitea-runner cache-server be pointed at via cfg.Cache.ExternalServer: a
// restart must not invalidate signed URLs the handler has already issued
// (within their expiry window).
func TestHandler_SecretPersistsAcrossRestarts(t *testing.T) {
dir := filepath.Join(t.TempDir(), "artifactcache")
first, err := StartHandler(dir, "127.0.0.1", 0, "", nil)
require.NoError(t, err)
exp := time.Now().Add(artifactURLTTL).Unix()
sig := first.computeSignature(42, exp)
require.NoError(t, first.Close())
second, err := StartHandler(dir, "127.0.0.1", 0, "", nil)
require.NoError(t, err)
defer second.Close()
assert.Equal(t, sig, second.computeSignature(42, exp))
}
// TestHandler_ArtifactSignatureDownload is a happy-path round trip that
// ensures a real reserve/upload/commit/find/download flow still works after
// the auth refactor.
func TestHandler_ArtifactSignatureDownload(t *testing.T) {
dir := filepath.Join(t.TempDir(), "artifactcache")
handler, err := StartHandler(dir, "", 0, "", nil)
require.NoError(t, err)
defer handler.Close()
handler.RegisterJob(testToken, testRepo)
base := handler.ExternalURL() + apiPath
key := "download-key"
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
content := []byte("hello")
uploadCacheNormally(t, base, key, version, content)
resp, err := testClient.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, key, version))
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
var hit struct {
ArchiveLocation string `json:"archiveLocation"`
}
require.NoError(t, json.NewDecoder(resp.Body).Decode(&hit))
resp.Body.Close()
require.Contains(t, hit.ArchiveLocation, "sig=")
require.Contains(t, hit.ArchiveLocation, "exp=")
// Download without any Authorization header — the signature alone must
// be enough, because @actions/cache downloads archiveLocation unauth'd.
dl, err := http.Get(hit.ArchiveLocation)
require.NoError(t, err)
body, err := io.ReadAll(dl.Body)
dl.Body.Close()
require.NoError(t, err)
assert.Equal(t, http.StatusOK, dl.StatusCode)
assert.Equal(t, content, body)
}
// TestHandler_RegisterJob_RefCounted verifies that a duplicate RegisterJob
// for the same token does not silently revoke the first registration on the
// first revoker call. This matters if a runner ever re-registers a token
// (restart mid-task, retry), which must not kill the live job's auth.
func TestHandler_RegisterJob_RefCounted(t *testing.T) {
dir := filepath.Join(t.TempDir(), "artifactcache")
handler, err := StartHandler(dir, "", 0, "", nil)
require.NoError(t, err)
defer handler.Close()
first := handler.RegisterJob("shared", testRepo)
second := handler.RegisterJob("shared", testRepo)
base := handler.ExternalURL() + apiPath
probe := func() int {
req, err := http.NewRequest(http.MethodGet, base+"/cache?keys=x&version=v", nil)
require.NoError(t, err)
req.Header.Set("Authorization", "Bearer shared")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
resp.Body.Close()
return resp.StatusCode
}
require.NotEqual(t, http.StatusUnauthorized, probe())
first()
assert.NotEqual(t, http.StatusUnauthorized, probe(),
"token must stay valid while another registration holds the refcount")
second()
assert.Equal(t, http.StatusUnauthorized, probe(),
"token is revoked only after every revoker has run")
}
// TestHandler_GC_PerRepoDedup ensures duplicate-pruning does not evict
// another repo's entry. Two repos reserve the same (key, version); after the
// keepOld window, GC must keep the one from each repo.
func TestHandler_GC_PerRepoDedup(t *testing.T) {
dir := filepath.Join(t.TempDir(), "artifactcache")
handler, err := StartHandler(dir, "", 0, "", nil)
require.NoError(t, err)
defer handler.Close()
handler.RegisterJob("tok-a", "owner/repoA")
handler.RegisterJob("tok-b", "owner/repoB")
key := "shared-dedup-key"
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
// Seed one completed cache per repo directly via the DB, bypassing the
// HTTP round trip so we can precisely control UsedAt.
db, err := handler.openDB()
require.NoError(t, err)
now := time.Now().Unix()
stale := time.Now().Add(-keepOld - time.Minute).Unix()
a := &Cache{Repo: "owner/repoA", Key: key, Version: version, Complete: true, CreatedAt: stale, UsedAt: stale, Size: 1}
b := &Cache{Repo: "owner/repoB", Key: key, Version: version, Complete: true, CreatedAt: now, UsedAt: now, Size: 1}
require.NoError(t, insertCache(db, a))
require.NoError(t, insertCache(db, b))
// Write the backing blobs so the dedup deletion has something to remove.
require.NoError(t, handler.storage.Write(a.ID, 0, strings.NewReader("a")))
_, err = handler.storage.Commit(a.ID, 1)
require.NoError(t, err)
require.NoError(t, handler.storage.Write(b.ID, 0, strings.NewReader("b")))
_, err = handler.storage.Commit(b.ID, 1)
require.NoError(t, err)
require.NoError(t, db.Close())
// Force GC to run regardless of the cooldown.
handler.gcAt = time.Time{}
handler.gcCache()
db, err = handler.openDB()
require.NoError(t, err)
defer db.Close()
var after []Cache
require.NoError(t, db.Find(&after, bolthold.Where("Key").Eq(key).And("Version").Eq(version)))
repos := make(map[string]bool)
for _, c := range after {
repos[c.Repo] = true
}
assert.True(t, repos["owner/repoA"], "repoA's cache must survive dedup against repoB")
assert.True(t, repos["owner/repoB"], "repoB's cache must survive dedup against repoA")
}
// TestHandler_InternalAPI_Disabled verifies that without an internalSecret
// the control-plane routes are 404 — operators can't accidentally hit
// register/revoke when the feature is off.
func TestHandler_InternalAPI_Disabled(t *testing.T) {
dir := filepath.Join(t.TempDir(), "artifactcache")
handler, err := StartHandler(dir, "", 0, "", nil)
require.NoError(t, err)
defer handler.Close()
for _, ep := range []string{"/_internal/register", "/_internal/revoke"} {
resp, err := http.Post(handler.ExternalURL()+ep, "application/json", strings.NewReader(`{}`))
require.NoError(t, err)
resp.Body.Close()
assert.Equal(t, http.StatusNotFound, resp.StatusCode, ep)
}
}
// TestHandler_InternalAPI_AuthAndUsage covers the control-plane: bad/missing
// secret → 401, malformed body → 400, happy path round-trips a token through
// register → cache-API accepts it → revoke → cache-API rejects it.
func TestHandler_InternalAPI_AuthAndUsage(t *testing.T) {
dir := filepath.Join(t.TempDir(), "artifactcache")
const secret = "internal-secret"
handler, err := StartHandler(dir, "", 0, secret, nil)
require.NoError(t, err)
defer handler.Close()
base := handler.ExternalURL()
post := func(path, bearer, body string) int {
req, err := http.NewRequest(http.MethodPost, base+path, strings.NewReader(body))
require.NoError(t, err)
if bearer != "" {
req.Header.Set("Authorization", "Bearer "+bearer)
}
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
resp.Body.Close()
return resp.StatusCode
}
t.Run("missing secret 401", func(t *testing.T) {
assert.Equal(t, http.StatusUnauthorized, post("/_internal/register", "", `{"token":"x","repo":"r"}`))
})
t.Run("wrong secret 401", func(t *testing.T) {
assert.Equal(t, http.StatusUnauthorized, post("/_internal/register", "wrong", `{"token":"x","repo":"r"}`))
})
t.Run("malformed body 400", func(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, post("/_internal/register", secret, `not json`))
})
t.Run("missing token 400", func(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, post("/_internal/register", secret, `{"repo":"r"}`))
})
t.Run("register then revoke round-trip", func(t *testing.T) {
probe := func(token string) int {
req, _ := http.NewRequest(http.MethodGet, base+apiPath+"/cache?keys=k&version=v", nil)
req.Header.Set("Authorization", "Bearer "+token)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
resp.Body.Close()
return resp.StatusCode
}
assert.Equal(t, http.StatusUnauthorized, probe("via-internal-api"))
assert.Equal(t, http.StatusOK, post("/_internal/register", secret, `{"token":"via-internal-api","repo":"owner/repo"}`))
assert.NotEqual(t, http.StatusUnauthorized, probe("via-internal-api"))
assert.Equal(t, http.StatusOK, post("/_internal/revoke", secret, `{"token":"via-internal-api"}`))
assert.Equal(t, http.StatusUnauthorized, probe("via-internal-api"))
})
}

View File

@@ -29,6 +29,7 @@ func (c *Request) ToCache() *Cache {
type Cache struct {
ID uint64 `json:"id" boltholdKey:"ID"`
Repo string `json:"repo" boltholdIndex:"Repo"`
Key string `json:"key" boltholdIndex:"Key"`
Version string `json:"version" boltholdIndex:"Version"`
Size int64 `json:"cacheSize"`

View File

@@ -17,7 +17,7 @@ import (
"strings"
"time"
"gitea.com/gitea/act_runner/act/common"
"gitea.com/gitea/runner/act/common"
"github.com/julienschmidt/httprouter"
)

View File

@@ -17,8 +17,8 @@ import (
"testing"
"testing/fstest"
"gitea.com/gitea/act_runner/act/model"
"gitea.com/gitea/act_runner/act/runner"
"gitea.com/gitea/runner/act/model"
"gitea.com/gitea/runner/act/runner"
"github.com/julienschmidt/httprouter"
log "github.com/sirupsen/logrus"
@@ -202,7 +202,7 @@ func TestListArtifactContainer(t *testing.T) {
panic(err)
}
assert.Equal(1, len(response.Value)) //nolint:testifylint // pre-existing issue from nektos/act
assert.Len(response.Value, 1)
assert.Equal("some/file", response.Value[0].Path)
assert.Equal("file", response.Value[0].ItemType)
assert.Equal("http://localhost/artifact/1/some/file/.", response.Value[0].ContentLocation)
@@ -283,7 +283,7 @@ func runTestJobFile(ctx context.Context, t *testing.T, tjfi TestJobFileInfo) {
}
workdir, err := filepath.Abs(tjfi.workdir)
assert.Nil(t, err, workdir) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err, workdir) //nolint:testifylint // pre-existing issue from nektos/act
fullWorkflowPath := filepath.Join(workdir, tjfi.workflowPath)
runnerConfig := &runner.Config{
Workdir: workdir,
@@ -299,16 +299,16 @@ func runTestJobFile(ctx context.Context, t *testing.T, tjfi TestJobFileInfo) {
}
runner, err := runner.New(runnerConfig)
assert.Nil(t, err, tjfi.workflowPath) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err, tjfi.workflowPath) //nolint:testifylint // pre-existing issue from nektos/act
planner, err := model.NewWorkflowPlanner(fullWorkflowPath, true)
assert.Nil(t, err, fullWorkflowPath) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err, fullWorkflowPath) //nolint:testifylint // pre-existing issue from nektos/act
plan, err := planner.PlanEvent(tjfi.eventName)
if err == nil {
err = runner.NewPlanExecutor(plan)(ctx)
if tjfi.errorMessage == "" {
assert.Nil(t, err, fullWorkflowPath) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err, fullWorkflowPath) //nolint:testifylint // pre-existing issue from nektos/act
} else {
assert.Error(t, err, tjfi.errorMessage) //nolint:testifylint // pre-existing issue from nektos/act
}

View File

@@ -35,9 +35,9 @@ func TestCartesianProduct(t *testing.T) {
"baz": {false, true},
}
output = CartesianProduct(input)
assert.Len(output, 0) //nolint:testifylint // pre-existing issue from nektos/act
assert.Empty(output)
input = map[string][]any{}
output = CartesianProduct(input)
assert.Len(output, 0) //nolint:testifylint // pre-existing issue from nektos/act
assert.Empty(output)
}

View File

@@ -117,7 +117,7 @@ func NewParallelExecutor(parallel int, executors ...Executor) Executor {
log.Debugf("Worker %d executing task %d", workerID, taskCount)
// Recover from panics in executors to avoid crashing the worker
// goroutine which would leave the runner process hung.
// https://gitea.com/gitea/act_runner/issues/371
// https://gitea.com/gitea/runner/issues/371
errs <- func() (err error) {
defer func() {
if r := recover(); r != nil {

View File

@@ -21,11 +21,11 @@ func TestNewWorkflow(t *testing.T) {
// empty
emptyWorkflow := NewPipelineExecutor()
assert.Nil(emptyWorkflow(ctx)) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(emptyWorkflow(ctx)) //nolint:testifylint // pre-existing issue from nektos/act
// error case
errorWorkflow := NewErrorExecutor(errors.New("test error"))
assert.NotNil(errorWorkflow(ctx)) //nolint:testifylint // pre-existing issue from nektos/act
assert.Error(errorWorkflow(ctx)) //nolint:testifylint // pre-existing issue from nektos/act
// multiple success case
runcount := 0
@@ -38,7 +38,7 @@ func TestNewWorkflow(t *testing.T) {
runcount++
return nil
})
assert.Nil(successWorkflow(ctx)) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(successWorkflow(ctx)) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(2, runcount)
}
@@ -60,7 +60,7 @@ func TestNewConditionalExecutor(t *testing.T) {
return nil
})(ctx)
assert.Nil(err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(0, trueCount)
assert.Equal(1, falseCount)
@@ -74,7 +74,7 @@ func TestNewConditionalExecutor(t *testing.T) {
return nil
})(ctx)
assert.Nil(err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(1, trueCount)
assert.Equal(1, falseCount)
}
@@ -105,7 +105,7 @@ func TestNewParallelExecutor(t *testing.T) {
assert.Equal(int32(3), count.Load(), "should run all 3 executors")
assert.Equal(int32(2), maxCount.Load(), "should run at most 2 executors in parallel")
assert.Nil(err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
// Reset to test running the executor with 0 parallelism
count.Store(0)
@@ -116,7 +116,7 @@ func TestNewParallelExecutor(t *testing.T) {
assert.Equal(int32(3), count.Load(), "should run all 3 executors")
assert.Equal(int32(1), maxCount.Load(), "should run at most 1 executors in parallel")
assert.Nil(errSingle) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(errSingle)
}
func TestNewParallelExecutorFailed(t *testing.T) {

View File

@@ -15,7 +15,7 @@ import (
"strings"
"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/config"
@@ -32,12 +32,21 @@ var (
githubHTTPRegex = regexp.MustCompile(`^https?://.*github.com.*/(.+)/(.+?)(?:.git)?$`)
githubSSHRegex = regexp.MustCompile(`github.com[:/](.+)/(.+?)(?:.git)?$`)
cloneLock sync.Mutex
cloneLocks sync.Map // key: clone target directory; value: *sync.Mutex
ErrShortRef = errors.New("short SHA references are not supported")
ErrNoRepo = errors.New("unable to find git repo")
)
// acquireCloneLock returns an unlock function after locking the per-directory mutex for dir.
// Only concurrent operations targeting the same directory are erialized; clones into different directories run in parallel.
func acquireCloneLock(dir string) func() {
v, _ := cloneLocks.LoadOrStore(dir, &sync.Mutex{})
mu := v.(*sync.Mutex)
mu.Lock()
return mu.Unlock
}
type Error struct {
err error
commit string
@@ -293,16 +302,13 @@ func gitOptions(token string) (fetchOptions git.FetchOptions, pullOptions git.Pu
}
// NewGitCloneExecutor creates an executor to clone git repos
//
//nolint:gocyclo // function handles many cases
func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor {
return func(ctx context.Context) error {
logger := common.Logger(ctx)
logger.Infof(" \u2601 git clone '%s' # ref=%s", input.URL, input.Ref)
logger.Debugf(" cloning %s to %s", input.URL, input.Dir)
cloneLock.Lock()
defer cloneLock.Unlock()
defer acquireCloneLock(input.Dir)()
refName := plumbing.ReferenceName("refs/heads/" + input.Ref)
r, err := CloneIfRequired(ctx, refName, input, logger)

View File

@@ -11,8 +11,10 @@ import (
"os/exec"
"path/filepath"
"strings"
"sync"
"syscall"
"testing"
"time"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
@@ -303,3 +305,61 @@ func gitCmd(args ...string) error {
}
return nil
}
func TestAcquireCloneLock(t *testing.T) {
t.Run("same directory serializes", func(t *testing.T) {
dir := t.TempDir()
unlock1 := acquireCloneLock(dir)
secondAcquired := make(chan struct{})
go func() {
unlock := acquireCloneLock(dir)
close(secondAcquired)
unlock()
}()
select {
case <-secondAcquired:
t.Fatal("second acquire should block while first holds the lock")
case <-time.After(50 * time.Millisecond):
}
unlock1()
select {
case <-secondAcquired:
case <-time.After(time.Second):
t.Fatal("second acquire should proceed after first releases the lock")
}
})
t.Run("different directories do not block", func(t *testing.T) {
dirA := t.TempDir()
dirB := t.TempDir()
unlockA := acquireCloneLock(dirA)
defer unlockA()
done := make(chan struct{})
go func() {
unlock := acquireCloneLock(dirB)
unlock()
close(done)
}()
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("acquire on a different directory must not block")
}
})
t.Run("same directory reuses the same mutex", func(t *testing.T) {
dir := t.TempDir()
v1, _ := cloneLocks.LoadOrStore(dir, &sync.Mutex{})
v2, _ := cloneLocks.LoadOrStore(dir, &sync.Mutex{})
require.Same(t, v1, v2)
})
}

View File

@@ -6,13 +6,21 @@ package container
import (
"context"
"fmt"
"io"
"gitea.com/gitea/act_runner/act/common"
"gitea.com/gitea/runner/act/common"
"github.com/docker/go-connections/nat"
)
// ExitCodeError reports a non-zero process exit code from a container command.
type ExitCodeError int
func (e ExitCodeError) Error() string {
return fmt.Sprintf("Process completed with exit code %d.", int(e))
}
// NewContainerInput the input for the New function
type NewContainerInput struct {
Image string

View File

@@ -10,7 +10,7 @@ import (
"context"
"strings"
"gitea.com/gitea/act_runner/act/common"
"gitea.com/gitea/runner/act/common"
"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/credentials"

View File

@@ -12,11 +12,10 @@ import (
"os"
"path/filepath"
"gitea.com/gitea/act_runner/act/common"
"gitea.com/gitea/runner/act/common"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/archive"
// github.com/docker/docker/builder/dockerignore is deprecated
"github.com/moby/buildkit/frontend/dockerfile/dockerignore"
"github.com/moby/patternmatcher"
)

View File

@@ -324,8 +324,6 @@ type containerConfig struct {
// parse parses the args for the specified command and generates a Config,
// a HostConfig and returns them with the specified command.
// If the specified args are not valid, it will return an error.
//
//nolint:gocyclo // function handles many cases
func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*containerConfig, error) {
var (
attachStdin = copts.attach.Get("stdin")

View File

@@ -194,7 +194,6 @@ func TestParseRunWithInvalidArgs(t *testing.T) {
}
}
//nolint:gocyclo // function handles many cases
func TestParseWithVolumes(t *testing.T) {
// A single volume
arr, tryit := setupPlatformVolume([]string{`/tmp`}, []string{`c:\tmp`})
@@ -632,7 +631,7 @@ func TestParseModes(t *testing.T) {
}
// uts ko
_, _, _, err = parseRun([]string{"--uts=container:", "img", "cmd"}) //nolint:dogsled // ignoring multiple returns in test helpers
_, _, _, err = parseRun([]string{"--uts=container:", "img", "cmd"})
assert.ErrorContains(t, err, "--uts: invalid UTS mode")
// uts ok
@@ -693,7 +692,7 @@ func TestParseRestartPolicy(t *testing.T) {
func TestParseRestartPolicyAutoRemove(t *testing.T) {
expected := "Conflicting options: --restart and --rm"
_, _, _, err := parseRun([]string{"--rm", "--restart=always", "img", "cmd"}) //nolint:dogsled // ignoring multiple returns in test helpers
_, _, _, err := parseRun([]string{"--rm", "--restart=always", "img", "cmd"})
if err == nil || err.Error() != expected {
t.Fatalf("Expected error %v, but got none", expected)
}

View File

@@ -29,17 +29,17 @@ func TestImageExistsLocally(t *testing.T) {
// Test if image exists with specific tag
invalidImageTag, err := ImageExistsLocally(ctx, "library/alpine:this-random-tag-will-never-exist", "linux/amd64")
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, false, invalidImageTag) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.False(t, invalidImageTag)
// Test if image exists with specific architecture (image platform)
invalidImagePlatform, err := ImageExistsLocally(ctx, "alpine:latest", "windows/amd64")
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, false, invalidImagePlatform) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.False(t, invalidImagePlatform)
// pull an image
cli, err := client.NewClientWithOpts(client.FromEnv)
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
cli.NegotiateAPIVersion(context.Background())
// Chose alpine latest because it's so small
@@ -47,25 +47,25 @@ func TestImageExistsLocally(t *testing.T) {
readerDefault, err := cli.ImagePull(ctx, "node:16-buster-slim", types.ImagePullOptions{
Platform: "linux/amd64",
})
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
defer readerDefault.Close()
_, err = io.ReadAll(readerDefault)
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
imageDefaultArchExists, err := ImageExistsLocally(ctx, "node:16-buster-slim", "linux/amd64")
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, true, imageDefaultArchExists) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.True(t, imageDefaultArchExists)
// Validate if another architecture platform can be pulled
readerArm64, err := cli.ImagePull(ctx, "node:16-buster-slim", types.ImagePullOptions{
Platform: "linux/arm64",
})
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
defer readerArm64.Close()
_, err = io.ReadAll(readerArm64)
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
imageArm64Exists, err := ImageExistsLocally(ctx, "node:16-buster-slim", "linux/arm64")
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, true, imageArm64Exists) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.True(t, imageArm64Exists)
}

View File

@@ -9,7 +9,7 @@ package container
import (
"context"
"gitea.com/gitea/act_runner/act/common"
"gitea.com/gitea/runner/act/common"
"github.com/docker/docker/api/types"
)

View File

@@ -13,7 +13,7 @@ import (
"fmt"
"strings"
"gitea.com/gitea/act_runner/act/common"
"gitea.com/gitea/runner/act/common"
"github.com/distribution/reference"
"github.com/docker/docker/api/types"

View File

@@ -43,7 +43,7 @@ func TestGetImagePullOptions(t *testing.T) {
config.SetDir("/non-existent/docker")
options, err := getImagePullOptions(ctx, NewDockerPullExecutorInput{})
assert.Nil(t, err, "Failed to create ImagePullOptions") //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err, "Failed to create ImagePullOptions") //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, "", options.RegistryAuth, "RegistryAuth should be empty if no username or password is set") //nolint:testifylint // pre-existing issue from nektos/act
options, err = getImagePullOptions(ctx, NewDockerPullExecutorInput{
@@ -51,7 +51,7 @@ func TestGetImagePullOptions(t *testing.T) {
Username: "username",
Password: "password",
})
assert.Nil(t, err, "Failed to create ImagePullOptions") //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err, "Failed to create ImagePullOptions") //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, "eyJ1c2VybmFtZSI6InVzZXJuYW1lIiwicGFzc3dvcmQiOiJwYXNzd29yZCJ9", options.RegistryAuth, "Username and Password should be provided")
config.SetDir("testdata/docker-pull-options")
@@ -59,6 +59,6 @@ func TestGetImagePullOptions(t *testing.T) {
options, err = getImagePullOptions(ctx, NewDockerPullExecutorInput{
Image: "nektos/act",
})
assert.Nil(t, err, "Failed to create ImagePullOptions") //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err, "Failed to create ImagePullOptions") //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, "eyJ1c2VybmFtZSI6InVzZXJuYW1lIiwicGFzc3dvcmQiOiJwYXNzd29yZFxuIiwic2VydmVyYWRkcmVzcyI6Imh0dHBzOi8vaW5kZXguZG9ja2VyLmlvL3YxLyJ9", options.RegistryAuth, "RegistryAuth should be taken from local docker config")
}

View File

@@ -20,8 +20,8 @@ import (
"strconv"
"strings"
"gitea.com/gitea/act_runner/act/common"
"gitea.com/gitea/act_runner/act/filecollector"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/act/filecollector"
"github.com/Masterminds/semver"
"github.com/docker/cli/cli/compose/loader"
@@ -633,14 +633,10 @@ func (cr *containerReference) exec(cmd []string, env map[string]string, user, wo
return fmt.Errorf("failed to inspect exec: %w", err)
}
switch inspectResp.ExitCode {
case 0:
if inspectResp.ExitCode == 0 {
return nil
case 127:
return fmt.Errorf("exitcode '%d': command not found, please refer to https://github.com/nektos/act/issues/107 for more information", inspectResp.ExitCode)
default:
return fmt.Errorf("exitcode '%d': failure", inspectResp.ExitCode)
}
return ExitCodeError(inspectResp.ExitCode)
}
}
@@ -930,7 +926,7 @@ func (cr *containerReference) wait() common.Executor {
return nil
}
return fmt.Errorf("exit with `FAILURE`: %v", statusCode)
return ExitCodeError(statusCode)
}
}

View File

@@ -15,7 +15,7 @@ import (
"testing"
"time"
"gitea.com/gitea/act_runner/act/common"
"gitea.com/gitea/runner/act/common"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
@@ -23,6 +23,7 @@ import (
"github.com/sirupsen/logrus/hooks/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
func TestDocker(t *testing.T) {
@@ -85,6 +86,11 @@ func (m *mockDockerClient) ContainerExecInspect(ctx context.Context, execID stri
return args.Get(0).(types.ContainerExecInspect), args.Error(1)
}
func (m *mockDockerClient) ContainerWait(ctx context.Context, containerID string, condition container.WaitCondition) (<-chan container.WaitResponse, <-chan error) {
args := m.Called(ctx, containerID, condition)
return args.Get(0).(<-chan container.WaitResponse), args.Get(1).(<-chan error)
}
func (m *mockDockerClient) CopyToContainer(ctx context.Context, id, path string, content io.Reader, options types.CopyToContainerOptions) error {
args := m.Called(ctx, id, path, content, options)
return args.Error(0)
@@ -174,12 +180,43 @@ func TestDockerExecFailure(t *testing.T) {
}
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)
client.AssertExpectations(t)
}
func TestDockerWaitFailure(t *testing.T) {
ctx := context.Background()
statusCh := make(chan container.WaitResponse, 1)
statusCh <- container.WaitResponse{StatusCode: 2}
errCh := make(chan error, 1)
client := &mockDockerClient{}
client.On("ContainerWait", ctx, "123", container.WaitConditionNotRunning).
Return((<-chan container.WaitResponse)(statusCh), (<-chan error)(errCh))
cr := &containerReference{
id: "123",
cli: client,
input: &NewContainerInput{
Image: "image",
},
}
err := cr.wait()(ctx)
var exitErr ExitCodeError
require.ErrorAs(t, err, &exitErr)
assert.Equal(t, ExitCodeError(2), exitErr)
assert.Equal(t, "Process completed with exit code 2.", err.Error())
client.AssertExpectations(t)
}
func TestDockerCopyTarStream(t *testing.T) {
ctx := context.Background()

View File

@@ -29,7 +29,7 @@ func TestGetSocketAndHostWithSocket(t *testing.T) {
ret, err := GetSocketAndHost(socketURI)
// Assert
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, SocketAndHost{socketURI, dockerHost}, ret)
}
@@ -42,7 +42,7 @@ func TestGetSocketAndHostNoSocket(t *testing.T) {
ret, err := GetSocketAndHost("")
// Assert
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, SocketAndHost{dockerHost, dockerHost}, ret)
}
@@ -57,8 +57,8 @@ func TestGetSocketAndHostOnlySocket(t *testing.T) {
ret, err := GetSocketAndHost(socketURI)
// Assert
assert.NoError(t, err, "Expected no error from GetSocketAndHost") //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, true, defaultSocketFound, "Expected to find default socket") //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err, "Expected no error from GetSocketAndHost") //nolint:testifylint // pre-existing issue from nektos/act
assert.True(t, defaultSocketFound, "Expected to find default socket")
assert.Equal(t, socketURI, ret.Socket, "Expected socket to match common location")
assert.Equal(t, defaultSocket, ret.Host, "Expected ret.Host to match default socket location")
}
@@ -73,7 +73,7 @@ func TestGetSocketAndHostDontMount(t *testing.T) {
ret, err := GetSocketAndHost("-")
// Assert
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, SocketAndHost{"-", dockerHost}, ret)
}
@@ -87,8 +87,8 @@ func TestGetSocketAndHostNoHostNoSocket(t *testing.T) {
ret, err := GetSocketAndHost("")
// Assert
assert.Equal(t, true, found, "Expected a default socket to be found") //nolint:testifylint // pre-existing issue from nektos/act
assert.Nil(t, err, "Expected no error from GetSocketAndHost") //nolint:testifylint // pre-existing issue from nektos/act
assert.True(t, found, "Expected a default socket to be found")
assert.NoError(t, err, "Expected no error from GetSocketAndHost") //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, SocketAndHost{defaultSocket, defaultSocket}, ret, "Expected to match default socket location")
}
@@ -112,8 +112,8 @@ func TestGetSocketAndHostNoHostNoSocketDefaultLocation(t *testing.T) {
// Assert
assert.Equal(t, unixSocket, defaultSocket, "Expected default socket to match common socket location")
assert.Equal(t, true, found, "Expected default socket to be found") //nolint:testifylint // pre-existing issue from nektos/act
assert.Nil(t, err, "Expected no error from GetSocketAndHost") //nolint:testifylint // pre-existing issue from nektos/act
assert.True(t, found, "Expected default socket to be found")
assert.NoError(t, err, "Expected no error from GetSocketAndHost") //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, SocketAndHost{unixSocket, unixSocket}, ret, "Expected to match default socket location")
}
@@ -128,7 +128,7 @@ func TestGetSocketAndHostNoHostInvalidSocket(t *testing.T) {
ret, err := GetSocketAndHost(mySocket)
// Assert
assert.Equal(t, false, found, "Expected no default socket to be found") //nolint:testifylint // pre-existing issue from nektos/act
assert.False(t, found, "Expected no default socket to be found")
assert.Equal(t, "", defaultSocket, "Expected no default socket to be found") //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, SocketAndHost{}, ret, "Expected to match default socket location")
assert.Error(t, err, "Expected an error in invalid state")
@@ -147,8 +147,8 @@ func TestGetSocketAndHostOnlySocketValidButUnusualLocation(t *testing.T) {
// Assert
// Default socket locations
assert.Equal(t, "", defaultSocket, "Expect default socket location to be empty") //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, false, found, "Expected no default socket to be found") //nolint:testifylint // pre-existing issue from nektos/act
assert.False(t, found, "Expected no default socket to be found")
// Sane default
assert.Nil(t, err, "Expect no error from GetSocketAndHost") //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err, "Expect no error from GetSocketAndHost") //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, socketURI, ret.Host, "Expect host to default to unusual socket")
}

View File

@@ -10,7 +10,7 @@ import (
"context"
"runtime"
"gitea.com/gitea/act_runner/act/common"
"gitea.com/gitea/runner/act/common"
"github.com/docker/docker/api/types"
"github.com/pkg/errors"

View File

@@ -9,7 +9,7 @@ package container
import (
"context"
"gitea.com/gitea/act_runner/act/common"
"gitea.com/gitea/runner/act/common"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/volume"

View File

@@ -16,12 +16,14 @@ import (
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"time"
"gitea.com/gitea/act_runner/act/common"
"gitea.com/gitea/act_runner/act/filecollector"
"gitea.com/gitea/act_runner/act/lookpath"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/act/filecollector"
"gitea.com/gitea/runner/act/lookpath"
"github.com/go-git/go-billy/v5/helper/polyfill"
"github.com/go-git/go-billy/v5/osfs"
@@ -34,9 +36,15 @@ type HostEnvironment struct {
TmpDir string
ToolCache string
Workdir string
ActPath string
CleanUp func()
StdOut io.Writer
// BindWorkdir is true when the app runner mounts the workspace on the host and
// deletes the task directory after the job; host teardown must not remove Workdir.
BindWorkdir bool
ActPath string
CleanUp func()
StdOut io.Writer
mu sync.Mutex
runningPIDs map[int]struct{}
}
func (e *HostEnvironment) Create(_, _ []string) common.Executor {
@@ -344,8 +352,30 @@ func (e *HostEnvironment) exec(ctx context.Context, command []string, cmdline st
if ppty != nil {
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 {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
return ExitCodeError(exitErr.ExitCode())
}
return err
}
if tty != nil {
@@ -385,12 +415,83 @@ func (e *HostEnvironment) UpdateFromEnv(srcPath string, env *map[string]string)
return parseEnvFile(e, srcPath, env)
}
func removePathWithRetry(ctx context.Context, path string) error {
if path == "" {
return nil
}
attempts := 1
delay := time.Duration(0)
if runtime.GOOS == "windows" {
attempts = 5
delay = 200 * time.Millisecond
}
var lastErr error
for i := 0; i < attempts; i++ {
if i > 0 {
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(delay):
}
}
lastErr = os.RemoveAll(path)
if lastErr == nil {
return nil
}
}
return lastErr
}
func (e *HostEnvironment) terminateRunningProcesses(ctx context.Context) {
if runtime.GOOS != "windows" {
return
}
e.mu.Lock()
pids := make([]int, 0, len(e.runningPIDs))
for pid := range e.runningPIDs {
pids = append(pids, pid)
}
e.mu.Unlock()
if len(pids) == 0 {
return
}
logger := common.Logger(ctx)
for _, pid := range pids {
// Best-effort: forcibly terminate process tree to release file handles
// so that workspace cleanup can succeed on Windows.
cmd := exec.CommandContext(ctx, "taskkill", "/PID", strconv.Itoa(pid), "/T", "/F")
out, err := cmd.CombinedOutput()
if err != nil {
logger.Debugf("taskkill failed for pid=%d: %v output=%s", pid, err, strings.TrimSpace(string(out)))
}
}
}
func (e *HostEnvironment) Remove() common.Executor {
return func(ctx context.Context) error {
// Ensure any lingering child processes are ended before attempting
// to remove the workspace (Windows file locks otherwise prevent cleanup).
e.terminateRunningProcesses(ctx)
// Only removes per-job misc state. Must not remove the cache/toolcache root.
if e.CleanUp != nil {
e.CleanUp()
}
return os.RemoveAll(e.Path)
logger := common.Logger(ctx)
var errs []error
if err := removePathWithRetry(ctx, e.Path); err != nil {
logger.Warnf("failed to remove host misc state %s: %v", e.Path, err)
errs = append(errs, err)
}
if !e.BindWorkdir && e.Workdir != "" {
if err := removePathWithRetry(ctx, e.Workdir); err != nil {
logger.Warnf("failed to remove host workspace %s: %v", e.Workdir, err)
errs = append(errs, err)
}
}
return errors.Join(errs...)
}
}

View File

@@ -11,9 +11,14 @@ import (
"os"
"path"
"path/filepath"
"runtime"
"testing"
"gitea.com/gitea/runner/act/common"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Type assert HostEnvironment implements ExecutionsEnvironment
@@ -69,3 +74,76 @@ func TestGetContainerArchive(t *testing.T) {
_, err = reader.Next()
assert.ErrorIs(t, err, io.EOF)
}
func TestHostEnvironmentExecExitCode(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("uses POSIX shell")
}
dir := t.TempDir()
ctx := context.Background()
e := &HostEnvironment{
Path: filepath.Join(dir, "path"),
TmpDir: filepath.Join(dir, "tmp"),
ToolCache: filepath.Join(dir, "tool_cache"),
ActPath: filepath.Join(dir, "act_path"),
StdOut: io.Discard,
Workdir: filepath.Join(dir, "path"),
}
for _, p := range []string{e.Path, e.TmpDir, e.ToolCache, e.ActPath} {
assert.NoError(t, os.MkdirAll(p, 0o700)) //nolint:testifylint // test setup
}
err := e.Exec([]string{"sh", "-c", "exit 3"}, map[string]string{"PATH": os.Getenv("PATH")}, "", "")(ctx)
var exitErr ExitCodeError
require.ErrorAs(t, err, &exitErr)
assert.Equal(t, ExitCodeError(3), exitErr)
assert.Equal(t, "Process completed with exit code 3.", err.Error())
}
func TestHostEnvironmentRemoveCleansWorkdir(t *testing.T) {
logger := logrus.New()
ctx := common.WithLogger(context.Background(), logrus.NewEntry(logger))
base := t.TempDir()
miscRoot := filepath.Join(base, "misc")
path := filepath.Join(miscRoot, "hostexecutor")
require.NoError(t, os.MkdirAll(path, 0o700))
workdir := filepath.Join(base, "workspace", "owner", "repo")
require.NoError(t, os.MkdirAll(workdir, 0o700))
e := &HostEnvironment{
Path: path,
Workdir: workdir,
BindWorkdir: false,
CleanUp: func() {
_ = os.RemoveAll(miscRoot)
},
StdOut: os.Stdout,
}
require.NoError(t, e.Remove()(ctx))
_, err := os.Stat(workdir)
assert.ErrorIs(t, err, os.ErrNotExist)
}
func TestHostEnvironmentRemoveSkipsWorkdirWhenBindWorkdir(t *testing.T) {
logger := logrus.New()
ctx := common.WithLogger(context.Background(), logrus.NewEntry(logger))
base := t.TempDir()
miscRoot := filepath.Join(base, "misc")
path := filepath.Join(miscRoot, "hostexecutor")
require.NoError(t, os.MkdirAll(path, 0o700))
workdir := filepath.Join(base, "workspace", "123", "owner", "repo")
require.NoError(t, os.MkdirAll(workdir, 0o700))
e := &HostEnvironment{
Path: path,
Workdir: workdir,
BindWorkdir: true,
CleanUp: func() {
_ = os.RemoveAll(miscRoot)
},
StdOut: os.Stdout,
}
require.NoError(t, e.Remove()(ctx))
_, err := os.Stat(workdir)
require.NoError(t, err)
}

View File

@@ -12,7 +12,7 @@ import (
"io"
"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 {

View File

@@ -18,7 +18,7 @@ import (
"strconv"
"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/rhysd/actionlint"

View File

@@ -8,7 +8,7 @@ import (
"path/filepath"
"testing"
"gitea.com/gitea/act_runner/act/model"
"gitea.com/gitea/runner/act/model"
"github.com/stretchr/testify/assert"
)
@@ -43,7 +43,7 @@ func TestFunctionContains(t *testing.T) {
for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, tt.expected, output)
})
@@ -72,7 +72,7 @@ func TestFunctionStartsWith(t *testing.T) {
for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, tt.expected, output)
})
@@ -101,7 +101,7 @@ func TestFunctionEndsWith(t *testing.T) {
for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, tt.expected, output)
})
@@ -128,7 +128,7 @@ func TestFunctionJoin(t *testing.T) {
for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, tt.expected, output)
})
@@ -154,7 +154,7 @@ func TestFunctionToJSON(t *testing.T) {
for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, tt.expected, output)
})
@@ -177,7 +177,7 @@ func TestFunctionFromJSON(t *testing.T) {
for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, tt.expected, output)
})
@@ -205,9 +205,9 @@ func TestFunctionHashFiles(t *testing.T) {
for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
workdir, err := filepath.Abs("testdata")
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
output, err := NewInterpeter(env, Config{WorkingDir: workdir}).Evaluate(tt.input, DefaultStatusCheckNone)
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, tt.expected, output)
})
@@ -248,7 +248,7 @@ func TestFunctionFormat(t *testing.T) {
if tt.error != nil {
assert.Equal(t, tt.error, err.Error())
} else {
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, tt.expected, output)
}
})

View File

@@ -12,7 +12,7 @@ import (
"reflect"
"strings"
"gitea.com/gitea/act_runner/act/model"
"gitea.com/gitea/runner/act/model"
"github.com/rhysd/actionlint"
)
@@ -156,7 +156,6 @@ func (impl *interperterImpl) evaluateNode(exprNode actionlint.ExprNode) (any, er
}
}
//nolint:gocyclo // function handles many cases
func (impl *interperterImpl) evaluateVariable(variableNode *actionlint.VariableNode) (any, error) {
switch strings.ToLower(variableNode.Name) {
case "github":
@@ -584,7 +583,6 @@ func (impl *interperterImpl) evaluateLogicalCompare(compareNode *actionlint.Logi
return nil, fmt.Errorf("Unable to compare incompatibles types '%s' and '%s'", leftValue.Kind(), rightValue.Kind())
}
//nolint:gocyclo // function handles many cases
func (impl *interperterImpl) evaluateFuncCall(funcCallNode *actionlint.FuncCallNode) (any, error) {
args := make([]reflect.Value, 0)

View File

@@ -8,7 +8,7 @@ import (
"math"
"testing"
"gitea.com/gitea/act_runner/act/model"
"gitea.com/gitea/runner/act/model"
"github.com/stretchr/testify/assert"
)
@@ -35,7 +35,7 @@ func TestLiterals(t *testing.T) {
for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, tt.expected, output)
})
@@ -105,10 +105,10 @@ func TestOperators(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
if tt.error != "" {
assert.NotNil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Error(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, tt.error, err.Error())
} else {
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
}
assert.Equal(t, tt.expected, output)
@@ -157,7 +157,7 @@ func TestOperatorsCompare(t *testing.T) {
for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, tt.expected, output)
})
@@ -520,7 +520,7 @@ func TestOperatorsBooleanEvaluation(t *testing.T) {
for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
if expected, ok := tt.expected.(float64); ok && math.IsNaN(expected) {
assert.True(t, math.IsNaN(output.(float64)))
@@ -624,7 +624,7 @@ func TestContexts(t *testing.T) {
for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, tt.expected, output)
})

View File

@@ -128,7 +128,6 @@ func (*DefaultFs) Readlink(path string) (string, error) {
return os.Readlink(path)
}
//nolint:gocyclo // function handles many cases
func (fc *FileCollector) CollectFiles(ctx context.Context, submodulePath []string) filepath.WalkFunc {
i, _ := fc.Fs.OpenGitIndex(path.Join(fc.SrcPath, path.Join(submodulePath...)))
return func(file string, fi os.FileInfo, err error) error {

View File

@@ -9,8 +9,8 @@ import (
"fmt"
"strings"
"gitea.com/gitea/act_runner/act/common"
"gitea.com/gitea/act_runner/act/common/git"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/act/common/git"
)
type GithubContext struct {

View File

@@ -61,8 +61,6 @@ type WorkflowFiles struct {
}
// NewWorkflowPlanner will load a specific workflow, all workflows from a directory or all workflows from a directory and its subdirectories
//
//nolint:gocyclo // function handles many cases
func NewWorkflowPlanner(path string, noWorkflowRecurse bool) (WorkflowPlanner, error) {
path, err := filepath.Abs(path)
if err != nil {

View File

@@ -57,11 +57,11 @@ func TestWorkflow(t *testing.T) {
// Check that an invalid job id returns error
result, err := createStages(&workflow, "invalid_job_id")
assert.NotNil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Error(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Nil(t, result)
// Check that an valid job id returns non-error
result, err = createStages(&workflow, "valid_job")
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NotNil(t, result)
}

View File

@@ -15,7 +15,7 @@ import (
"strconv"
"strings"
"gitea.com/gitea/act_runner/act/common"
"gitea.com/gitea/runner/act/common"
log "github.com/sirupsen/logrus"
"go.yaml.in/yaml/v4"
@@ -440,8 +440,6 @@ func (j *Job) Matrix() map[string][]any {
// GetMatrixes returns the matrix cross product
// It skips includes and hard fails excludes for non-existing keys
//
//nolint:gocyclo // function handles many cases
func (j *Job) GetMatrixes() ([]map[string]any, error) {
matrixes := make([]map[string]any, 0)
if j.Strategy != nil {

View File

@@ -56,7 +56,7 @@ jobs:
assert.NoError(t, err, "read workflow should succeed") //nolint:testifylint // pre-existing issue from nektos/act
newSchedules = workflow.OnSchedule()
assert.Len(t, newSchedules, 0) //nolint:testifylint // pre-existing issue from nektos/act
assert.Empty(t, newSchedules)
yaml = `
name: local-action-docker-url
@@ -74,7 +74,7 @@ jobs:
assert.NoError(t, err, "read workflow should succeed") //nolint:testifylint // pre-existing issue from nektos/act
newSchedules = workflow.OnSchedule()
assert.Len(t, newSchedules, 0) //nolint:testifylint // pre-existing issue from nektos/act
assert.Empty(t, newSchedules)
yaml = `
name: local-action-docker-url
@@ -91,7 +91,7 @@ jobs:
assert.NoError(t, err, "read workflow should succeed") //nolint:testifylint // pre-existing issue from nektos/act
newSchedules = workflow.OnSchedule()
assert.Len(t, newSchedules, 0) //nolint:testifylint // pre-existing issue from nektos/act
assert.Empty(t, newSchedules)
}
func TestReadWorkflow_StringEvent(t *testing.T) {
@@ -870,7 +870,7 @@ jobs:
assert.Nil(t, matrix, "matrix should be nil for jobs without strategy")
} else {
assert.NotNil(t, matrix, "matrix should not be nil")
assert.Equal(t, tt.wantLen, len(matrix), "matrix should have expected number of keys") //nolint:testifylint // pre-existing issue from nektos/act
assert.Len(t, matrix, tt.wantLen, "matrix should have expected number of keys")
if tt.checkFn != nil {
tt.checkFn(t, matrix)
}

View File

@@ -18,9 +18,9 @@ import (
"runtime"
"strings"
"gitea.com/gitea/act_runner/act/common"
"gitea.com/gitea/act_runner/act/container"
"gitea.com/gitea/act_runner/act/model"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/act/container"
"gitea.com/gitea/runner/act/model"
"github.com/kballard/go-shellquote"
)
@@ -265,8 +265,6 @@ func removeGitIgnore(ctx context.Context, directory string) error {
}
// TODO: break out parts of function to reduce complexicity
//
//nolint:gocyclo // function handles many cases
func execAsDocker(ctx context.Context, step actionStep, actionName, basedir string, localAction bool) error {
logger := common.Logger(ctx)
rc := step.getRunContext()
@@ -429,7 +427,7 @@ func newStepContainer(ctx context.Context, step step, image string, cmd, entrypo
Image: image,
Username: rc.Config.Secrets["DOCKER_USERNAME"],
Password: rc.Config.Secrets["DOCKER_PASSWORD"],
Name: createSimpleContainerName(rc.jobContainerName(), "STEP-"+stepModel.ID),
Name: createContainerName(rc.jobContainerName(), "STEP-"+stepModel.ID),
Env: envList,
Mounts: mounts,
NetworkMode: networkMode,

View File

@@ -11,8 +11,8 @@ import (
"strconv"
"strings"
"gitea.com/gitea/act_runner/act/common"
"gitea.com/gitea/act_runner/act/model"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/act/model"
)
func evaluateCompositeInputAndEnv(ctx context.Context, parent *RunContext, step actionStep) map[string]string {

View File

@@ -11,7 +11,7 @@ import (
"strings"
"testing"
"gitea.com/gitea/act_runner/act/model"
"gitea.com/gitea/runner/act/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
@@ -137,7 +137,7 @@ runs:
action, err := readActionImpl(context.Background(), tt.step, "actionDir", "actionPath", readFile, writeFile)
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, tt.expected, action)
closerMock.AssertExpectations(t)
@@ -247,7 +247,7 @@ func TestActionRunner(t *testing.T) {
err := runActionImpl(tt.step, "dir", newRemoteAction("org/repo/path@ref"))(ctx)
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
cm.AssertExpectations(t)
})
}

View File

@@ -9,7 +9,7 @@ import (
"regexp"
"strings"
"gitea.com/gitea/act_runner/act/common"
"gitea.com/gitea/runner/act/common"
)
var commandPatternGA *regexp.Regexp

View File

@@ -11,8 +11,8 @@ import (
"os"
"testing"
"gitea.com/gitea/act_runner/act/common"
"gitea.com/gitea/act_runner/act/model"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/act/model"
"github.com/sirupsen/logrus/hooks/test"
"github.com/stretchr/testify/assert"

View File

@@ -8,8 +8,8 @@ import (
"context"
"io"
"gitea.com/gitea/act_runner/act/common"
"gitea.com/gitea/act_runner/act/container"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/act/container"
"github.com/stretchr/testify/mock"
)

View File

@@ -15,10 +15,10 @@ import (
"strings"
"time"
"gitea.com/gitea/act_runner/act/common"
"gitea.com/gitea/act_runner/act/container"
"gitea.com/gitea/act_runner/act/exprparser"
"gitea.com/gitea/act_runner/act/model"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/act/container"
"gitea.com/gitea/runner/act/exprparser"
"gitea.com/gitea/runner/act/model"
_ "embed"
@@ -405,7 +405,6 @@ func escapeFormatString(in string) string {
return strings.ReplaceAll(strings.ReplaceAll(in, "{", "{{"), "}", "}}")
}
//nolint:gocyclo // function handles many cases
func rewriteSubExpression(ctx context.Context, in string, forceFormat bool) (string, error) { //nolint:unparam // pre-existing issue from nektos/act
if !strings.Contains(in, "${{") || !strings.Contains(in, "}}") {
return in, nil
@@ -472,7 +471,6 @@ func rewriteSubExpression(ctx context.Context, in string, forceFormat bool) (str
return out, nil
}
//nolint:gocyclo // function handles many cases
func getEvaluatorInputs(ctx context.Context, rc *RunContext, step step, ghc *model.GithubContext) map[string]any {
inputs := map[string]any{}

View File

@@ -8,8 +8,8 @@ import (
"context"
"testing"
"gitea.com/gitea/act_runner/act/exprparser"
"gitea.com/gitea/act_runner/act/model"
"gitea.com/gitea/runner/act/exprparser"
"gitea.com/gitea/runner/act/model"
assert "github.com/stretchr/testify/assert"
yaml "go.yaml.in/yaml/v4"

View File

@@ -10,8 +10,8 @@ import (
"strconv"
"time"
"gitea.com/gitea/act_runner/act/common"
"gitea.com/gitea/act_runner/act/model"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/act/model"
)
type jobInfo interface {
@@ -24,7 +24,13 @@ type jobInfo interface {
result(result string)
}
//nolint:contextcheck,gocyclo // composes many step executors
// reportStepError emits the GitHub Actions ##[error] annotation and records
// the error against the job so the job is reported as failed.
func reportStepError(ctx context.Context, err error) {
common.Logger(ctx).Errorf("##[error]%v", err)
common.SetJobError(ctx, err)
}
func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executor {
steps := make([]common.Executor, 0)
preSteps := make([]common.Executor, 0)
@@ -33,7 +39,7 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
steps = append(steps, func(ctx context.Context) error {
logger := common.Logger(ctx)
if len(info.matrix()) > 0 {
logger.Infof("\U0001F9EA Matrix: %v", info.matrix())
logger.Infof("Matrix: %v", info.matrix())
}
return nil
})
@@ -76,33 +82,36 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
preExec := step.pre()
preSteps = append(preSteps, useStepLogger(rc, stepModel, stepStagePre, func(ctx context.Context) error {
logger := common.Logger(ctx)
preErr := preExec(ctx)
if preErr != nil {
logger.Errorf("%v", preErr)
common.SetJobError(ctx, preErr)
reportStepError(ctx, preErr)
} else if ctx.Err() != nil {
logger.Errorf("%v", ctx.Err())
common.SetJobError(ctx, ctx.Err())
reportStepError(ctx, ctx.Err())
}
return preErr
}))
stepExec := step.main()
steps = append(steps, useStepLogger(rc, stepModel, stepStageMain, func(ctx context.Context) error {
logger := common.Logger(ctx)
err := stepExec(ctx)
if err != nil {
logger.Errorf("%v", err)
common.SetJobError(ctx, err)
reportStepError(ctx, err)
} else if ctx.Err() != nil {
logger.Errorf("%v", ctx.Err())
common.SetJobError(ctx, ctx.Err())
reportStepError(ctx, ctx.Err())
}
return nil
}))
postExec := useStepLogger(rc, stepModel, stepStagePost, step.post())
postFn := step.post()
postExec := useStepLogger(rc, stepModel, stepStagePost, func(ctx context.Context) error {
err := postFn(ctx)
if err != nil {
reportStepError(ctx, err)
} else if ctx.Err() != nil {
reportStepError(ctx, ctx.Err())
}
return err
})
if postExecutor != nil {
// run the post executor in reverse order
postExecutor = postExec.Finally(postExecutor)
@@ -137,7 +146,7 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
// if !rc.IsHostEnv(ctx) && rc.Config.ContainerNetworkMode == "" {
// // clean network in docker mode only
// // if the value of `ContainerNetworkMode` is empty string,
// // it means that the network to which containers are connecting is created by `act_runner`,
// // it means that the network to which containers are connecting is created by `runner`,
// // so, we should remove the network at last.
// networkName, _ := rc.networkName()
// logger.Infof("Cleaning up network for job %s, and network name is: %s", rc.JobName, networkName)
@@ -157,7 +166,7 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
pipeline = append(pipeline, steps...)
return common.NewPipelineExecutor(info.startContainer(), common.NewPipelineExecutor(pipeline...).
Finally(func(ctx context.Context) error { //nolint:contextcheck // intentionally detaches from canceled parent
Finally(func(ctx context.Context) error {
var cancel context.CancelFunc
if ctx.Err() == context.Canceled {
// in case of an aborted run, we still should execute the
@@ -197,7 +206,7 @@ func setJobResult(ctx context.Context, info jobInfo, rc *RunContext, success boo
jobResultMessage = "failed"
}
logger.WithField("jobResult", jobResult).Infof("\U0001F3C1 Job %s", jobResultMessage)
logger.WithField("jobResult", jobResult).Infof("Job %s", jobResultMessage)
}
func setJobOutputs(ctx context.Context, rc *RunContext) {

View File

@@ -12,9 +12,9 @@ import (
"slices"
"testing"
"gitea.com/gitea/act_runner/act/common"
"gitea.com/gitea/act_runner/act/container"
"gitea.com/gitea/act_runner/act/model"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/act/container"
"gitea.com/gitea/runner/act/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
@@ -331,7 +331,7 @@ func TestNewJobExecutor(t *testing.T) {
executor := newJobExecutor(jim, sfm, rc)
err := executor(ctx)
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, tt.executedSteps, executorOrder)
jim.AssertExpectations(t)

View File

@@ -16,7 +16,7 @@ import (
"path/filepath"
"strings"
"gitea.com/gitea/act_runner/act/filecollector"
"gitea.com/gitea/runner/act/filecollector"
)
type LocalRepositoryCache struct {

View File

@@ -13,7 +13,7 @@ import (
"strings"
"sync"
"gitea.com/gitea/act_runner/act/common"
"gitea.com/gitea/runner/act/common"
"github.com/sirupsen/logrus"
"golang.org/x/term"
@@ -30,6 +30,11 @@ const (
gray = 37
)
const (
rawOutputField = "raw_output"
scriptLineCyanField = "script_line_cyan"
)
var (
colors []int
nextColor int
@@ -161,6 +166,8 @@ func withStepLogger(ctx context.Context, stepNumber int, stepID, stepName, stage
type entryProcessor func(entry *logrus.Entry) *logrus.Entry
// valueMasker applies secrets and ::add-mask:: patterns to every log entry, including
// raw_output (command/stream) lines; there is no bypass by field.
func valueMasker(insecureSecrets bool, secrets map[string]string) entryProcessor {
return func(entry *logrus.Entry) *logrus.Entry {
if insecureSecrets {
@@ -227,8 +234,12 @@ func (f *jobLogFormatter) printColored(b *bytes.Buffer, entry *logrus.Entry) {
debugFlag = "[DEBUG] "
}
if entry.Data["raw_output"] == true {
fmt.Fprintf(b, "\x1b[%dm|\x1b[0m %s", f.color, entry.Message)
if entry.Data[rawOutputField] == true {
if entry.Data[scriptLineCyanField] == true {
fmt.Fprintf(b, "\x1b[%dm|\x1b[0m \x1b[36;1m%s\x1b[0m", f.color, entry.Message)
} else {
fmt.Fprintf(b, "\x1b[%dm|\x1b[0m %s", f.color, entry.Message)
}
} else if entry.Data["dryrun"] == true {
fmt.Fprintf(b, "\x1b[1m\x1b[%dm\x1b[7m*DRYRUN*\x1b[0m \x1b[%dm[%s] \x1b[0m%s%s", gray, f.color, job, debugFlag, entry.Message)
} else {
@@ -251,7 +262,7 @@ func (f *jobLogFormatter) print(b *bytes.Buffer, entry *logrus.Entry) {
debugFlag = "[DEBUG] "
}
if entry.Data["raw_output"] == true {
if entry.Data[rawOutputField] == true {
fmt.Fprintf(b, "[%s] | %s", job, entry.Message)
} else if entry.Data["dryrun"] == true {
fmt.Fprintf(b, "*DRYRUN* [%s] %s%s", job, debugFlag, entry.Message)

View File

@@ -6,7 +6,7 @@ package runner
import (
"testing"
"gitea.com/gitea/act_runner/act/model"
"gitea.com/gitea/runner/act/model"
"github.com/stretchr/testify/assert"
"go.yaml.in/yaml/v4"
@@ -60,7 +60,7 @@ func TestMaxParallelStrategy(t *testing.T) {
matrixes, err := job.GetMatrixes()
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NotNil(t, matrixes)
assert.Equal(t, 5, len(matrixes)) //nolint:testifylint // pre-existing issue from nektos/act
assert.Len(t, matrixes, 5)
assert.Equal(t, tt.expectedMaxParallel, job.Strategy.MaxParallel)
})
}

View File

@@ -17,9 +17,9 @@ import (
"strings"
"sync"
"gitea.com/gitea/act_runner/act/common"
"gitea.com/gitea/act_runner/act/common/git"
"gitea.com/gitea/act_runner/act/model"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/act/common/git"
"gitea.com/gitea/runner/act/model"
)
func newLocalReusableWorkflowExecutor(rc *RunContext) common.Executor {

View File

@@ -23,10 +23,10 @@ import (
"strings"
"time"
"gitea.com/gitea/act_runner/act/common"
"gitea.com/gitea/act_runner/act/container"
"gitea.com/gitea/act_runner/act/exprparser"
"gitea.com/gitea/act_runner/act/model"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/act/container"
"gitea.com/gitea/runner/act/exprparser"
"gitea.com/gitea/runner/act/model"
"github.com/docker/go-connections/nat"
"github.com/opencontainers/selinux/go-selinux"
@@ -101,8 +101,7 @@ func (rc *RunContext) jobContainerName() string {
if rc.caller != nil {
nameParts = append(nameParts, "CALLED-BY-"+rc.caller.runContext.JobName)
}
// return createSimpleContainerName(rc.Config.ContainerNamePrefix, "WORKFLOW-"+rc.Run.Workflow.Name, "JOB-"+rc.Name)
return createSimpleContainerName(nameParts...) // For Gitea
return createContainerName(nameParts...) // For Gitea
}
// networkNameForGitea return the name of the network
@@ -221,11 +220,12 @@ func (rc *RunContext) startHostEnvironment() common.Executor {
}
toolCache := filepath.Join(cacheDir, "tool_cache")
rc.JobContainer = &container.HostEnvironment{
Path: path,
TmpDir: runnerTmp,
ToolCache: toolCache,
Workdir: rc.Config.Workdir,
ActPath: actPath,
Path: path,
TmpDir: runnerTmp,
ToolCache: toolCache,
Workdir: rc.Config.Workdir,
BindWorkdir: rc.Config.BindWorkdir,
ActPath: actPath,
CleanUp: func() {
os.RemoveAll(miscpath)
},
@@ -260,7 +260,6 @@ func (rc *RunContext) startHostEnvironment() common.Executor {
}
}
//nolint:gocyclo // function handles many cases
func (rc *RunContext) startJobContainer() common.Executor {
return func(ctx context.Context) error {
logger := common.Logger(ctx)
@@ -383,7 +382,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
if createAndDeleteNetwork {
// clean network if it has been created by act
// if using service containers
// it means that the network to which containers are connecting is created by `act_runner`,
// it means that the network to which containers are connecting is created by `runner`,
// so, we should remove the network at last.
logger.Infof("Cleaning up network for job %s, and network name is: %s", rc.JobName, networkName)
if err := container.NewDockerNetworkRemoveExecutor(networkName)(ctx); err != nil {
@@ -731,7 +730,7 @@ func (rc *RunContext) isEnabled(ctx context.Context) (bool, error) {
jobType, jobTypeErr := job.Type()
if runJobErr != nil {
return false, fmt.Errorf(" \u274C Error in if-expression: \"if: %s\" (%s)", job.If.Value, runJobErr)
return false, fmt.Errorf("if-expression %q evaluation failed: %s", job.If.Value, runJobErr)
}
if jobType == model.JobTypeInvalid {
@@ -769,7 +768,6 @@ func mergeMaps(maps ...map[string]string) map[string]string {
return rtnMap
}
// Deprecated: use createSimpleContainerName
func createContainerName(parts ...string) string {
name := strings.Join(parts, "-")
pattern := regexp.MustCompile("[^a-zA-Z0-9]")
@@ -783,22 +781,6 @@ func createContainerName(parts ...string) string {
return fmt.Sprintf("%s-%x", trimmedName, hash)
}
func createSimpleContainerName(parts ...string) string {
pattern := regexp.MustCompile("[^a-zA-Z0-9-]")
name := make([]string, 0, len(parts))
for _, v := range parts {
v = pattern.ReplaceAllString(v, "-")
v = strings.Trim(v, "-")
for strings.Contains(v, "--") {
v = strings.ReplaceAll(v, "--", "-")
}
if v != "" {
name = append(name, v)
}
}
return strings.Join(name, "_")
}
func trimToLen(s string, l int) string {
if l < 0 {
l = 0
@@ -826,7 +808,6 @@ func (rc *RunContext) getStepsContext() map[string]*model.StepResult {
return rc.StepResults
}
//nolint:gocyclo // function handles many cases
func (rc *RunContext) getGithubContext(ctx context.Context) *model.GithubContext {
logger := common.Logger(ctx)
ghc := &model.GithubContext{

View File

@@ -12,8 +12,8 @@ import (
"strings"
"testing"
"gitea.com/gitea/act_runner/act/exprparser"
"gitea.com/gitea/act_runner/act/model"
"gitea.com/gitea/runner/act/exprparser"
"gitea.com/gitea/runner/act/model"
log "github.com/sirupsen/logrus"
assert "github.com/stretchr/testify/assert"
@@ -282,7 +282,7 @@ func TestGetGitHubContext(t *testing.T) {
log.SetLevel(log.DebugLevel)
cwd, err := os.Getwd()
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
rc := &RunContext{
Config: &Config{
@@ -622,23 +622,16 @@ func TestRunContextGetEnv(t *testing.T) {
}
}
func Test_createSimpleContainerName(t *testing.T) {
tests := []struct {
parts []string
want string
}{
{
parts: []string{"a--a", "BB正", "c-C"},
want: "a-a_BB_c-C",
},
{
parts: []string{"a-a", "", "-"},
want: "a-a",
},
}
for _, tt := range tests {
t.Run(strings.Join(tt.parts, " "), func(t *testing.T) {
assert.Equalf(t, tt.want, createSimpleContainerName(tt.parts...), "createSimpleContainerName(%v)", tt.parts)
})
}
func TestCreateContainerNameBoundedForLongMatrixInput(t *testing.T) {
longMatrixValue := strings.Repeat("os=ubuntu-latest-go=1.24-node=22-", 20)
name := createContainerName(
"gitea",
"WORKFLOW-super-long-workflow-name",
"JOB-build-matrix-"+longMatrixValue,
)
assert.LessOrEqual(t, len(name), 128)
assert.LessOrEqual(t, len(name+"-env"), 255)
assert.LessOrEqual(t, len(name+"-network"), 255)
assert.LessOrEqual(t, len(name+"-job1234567890"), 255)
}

View File

@@ -13,8 +13,8 @@ import (
"sync"
"time"
"gitea.com/gitea/act_runner/act/common"
"gitea.com/gitea/act_runner/act/model"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/act/model"
docker_container "github.com/docker/docker/api/types/container"
log "github.com/sirupsen/logrus"
@@ -137,8 +137,6 @@ func (runner *runnerImpl) configure() (Runner, error) {
}
// NewPlanExecutor ...
//
//nolint:gocyclo // function handles many cases
func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor {
maxJobNameLen := 0

View File

@@ -16,8 +16,8 @@ import (
"strings"
"testing"
"gitea.com/gitea/act_runner/act/common"
"gitea.com/gitea/act_runner/act/model"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/act/model"
"github.com/joho/godotenv"
log "github.com/sirupsen/logrus"
@@ -87,7 +87,7 @@ func TestGraphMissingEvent(t *testing.T) {
plan, err := planner.PlanEvent("push")
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NotNil(t, plan)
assert.Equal(t, 0, len(plan.Stages)) //nolint:testifylint // pre-existing issue from nektos/act
assert.Empty(t, plan.Stages)
assert.Contains(t, buf.String(), "no events found for workflow: no-event.yml")
log.SetOutput(out)
@@ -100,7 +100,7 @@ func TestGraphMissingFirst(t *testing.T) {
plan, err := planner.PlanEvent("push")
assert.EqualError(t, err, "unable to build dependency graph for no first (no-first.yml)") //nolint:testifylint // pre-existing issue from nektos/act
assert.NotNil(t, plan)
assert.Equal(t, 0, len(plan.Stages)) //nolint:testifylint // pre-existing issue from nektos/act
assert.Empty(t, plan.Stages)
}
func TestGraphWithMissing(t *testing.T) {
@@ -114,7 +114,7 @@ func TestGraphWithMissing(t *testing.T) {
plan, err := planner.PlanEvent("push")
assert.NotNil(t, plan)
assert.Equal(t, 0, len(plan.Stages)) //nolint:testifylint // pre-existing issue from nektos/act
assert.Empty(t, plan.Stages)
assert.EqualError(t, err, "unable to build dependency graph for missing (missing.yml)") //nolint:testifylint // pre-existing issue from nektos/act
assert.Contains(t, buf.String(), "unable to build dependency graph for missing (missing.yml)")
log.SetOutput(out)
@@ -134,7 +134,7 @@ func TestGraphWithSomeMissing(t *testing.T) {
plan, err := planner.PlanAll()
assert.Error(t, err, "unable to build dependency graph for no first (no-first.yml)") //nolint:testifylint // pre-existing issue from nektos/act
assert.NotNil(t, plan)
assert.Equal(t, 1, len(plan.Stages)) //nolint:testifylint // pre-existing issue from nektos/act
assert.Len(t, plan.Stages, 1)
assert.Contains(t, buf.String(), "unable to build dependency graph for missing (missing.yml)")
assert.Contains(t, buf.String(), "unable to build dependency graph for no first (no-first.yml)")
log.SetOutput(out)
@@ -159,7 +159,7 @@ func TestGraphEvent(t *testing.T) {
plan, err = planner.PlanEvent("release")
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NotNil(t, plan)
assert.Equal(t, 0, len(plan.Stages)) //nolint:testifylint // pre-existing issue from nektos/act
assert.Empty(t, plan.Stages)
}
type TestJobFileInfo struct {
@@ -177,7 +177,7 @@ func (j *TestJobFileInfo) runTest(ctx context.Context, t *testing.T, cfg *Config
log.SetLevel(logLevel)
workdir, err := filepath.Abs(j.workdir)
assert.Nil(t, err, workdir) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err, workdir) //nolint:testifylint // pre-existing issue from nektos/act
fullWorkflowPath := filepath.Join(workdir, j.workflowPath)
runnerConfig := &Config{
@@ -197,17 +197,17 @@ func (j *TestJobFileInfo) runTest(ctx context.Context, t *testing.T, cfg *Config
}
runner, err := New(runnerConfig)
assert.Nil(t, err, j.workflowPath) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err, j.workflowPath) //nolint:testifylint // pre-existing issue from nektos/act
planner, err := model.NewWorkflowPlanner(fullWorkflowPath, true)
assert.Nil(t, err, fullWorkflowPath) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err, fullWorkflowPath) //nolint:testifylint // pre-existing issue from nektos/act
plan, err := planner.PlanEvent(j.eventName)
assert.True(t, (err == nil) != (plan == nil), "PlanEvent should return either a plan or an error") //nolint:testifylint // pre-existing issue from nektos/act
if err == nil && plan != nil {
err = runner.NewPlanExecutor(plan)(ctx)
if j.errorMessage == "" {
assert.Nil(t, err, fullWorkflowPath) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err, fullWorkflowPath) //nolint:testifylint // pre-existing issue from nektos/act
} else {
assert.Error(t, err, j.errorMessage) //nolint:testifylint // pre-existing issue from nektos/act
}

View File

@@ -13,10 +13,10 @@ import (
"strings"
"time"
"gitea.com/gitea/act_runner/act/common"
"gitea.com/gitea/act_runner/act/container"
"gitea.com/gitea/act_runner/act/exprparser"
"gitea.com/gitea/act_runner/act/model"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/act/container"
"gitea.com/gitea/runner/act/exprparser"
"gitea.com/gitea/runner/act/model"
)
type step interface {
@@ -107,7 +107,7 @@ func runStepExecutor(step step, stage stepStage, executor common.Executor) commo
if strings.Contains(stepString, "::add-mask::") {
stepString = "add-mask command"
}
logger.Infof("\u2B50 Run %s %s", stage, stepString)
logger.Infof("Run %s %s", stage, stepString)
// Prepare and clean Runner File Commands
actPath := rc.JobContainer.GetActPath()
@@ -158,7 +158,7 @@ func runStepExecutor(step step, stage stepStage, executor common.Executor) commo
err = executor(timeoutctx)
if err == nil {
logger.WithField("stepResult", stepResult.Outcome).Infof(" \u2705 Success - %s %s", stage, stepString)
logger.WithField("stepResult", stepResult.Outcome).Infof("Success - %s %s", stage, stepString)
} else {
stepResult.Outcome = model.StepStatusFailure
@@ -169,6 +169,7 @@ func runStepExecutor(step step, stage stepStage, executor common.Executor) commo
}
if continueOnError {
logger.Errorf("##[error]%v", err)
logger.Infof("Failed but continue next step")
err = nil
stepResult.Conclusion = model.StepStatusSuccess
@@ -176,7 +177,9 @@ func runStepExecutor(step step, stage stepStage, executor common.Executor) commo
stepResult.Conclusion = model.StepStatusFailure
}
logger.WithField("stepResult", stepResult.Outcome).Errorf(" \u274C Failure - %s %s", stage, stepString)
// Infof: Errorf entries are promoted to the user log by the reporter,
// which would duplicate the ##[error] annotation emitted elsewhere.
logger.WithField("stepResult", stepResult.Outcome).Infof("Failure - %s %s", stage, stepString)
}
// Process Runner File Commands
orgerr := err
@@ -268,7 +271,7 @@ func isStepEnabled(ctx context.Context, expr string, step step, stage stepStage)
runStep, err := EvalBool(ctx, rc.NewStepExpressionEvaluator(ctx, step), expr, defaultStatusCheck)
if err != nil {
return false, fmt.Errorf(" \u274C Error in if-expression: \"if: %s\" (%s)", expr, err)
return false, fmt.Errorf("if-expression %q evaluation failed: %s", expr, err)
}
return runStep, nil
@@ -284,7 +287,7 @@ func isContinueOnError(ctx context.Context, expr string, step step, _ stepStage)
continueOnError, err := EvalBool(ctx, rc.NewStepExpressionEvaluator(ctx, step), expr, exprparser.DefaultStatusCheckNone)
if err != nil {
return false, fmt.Errorf(" \u274C Error in continue-on-error-expression: \"continue-on-error: %s\" (%s)", expr, err)
return false, fmt.Errorf("continue-on-error expression %q evaluation failed: %s", expr, err)
}
return continueOnError, nil

View File

@@ -15,8 +15,8 @@ import (
"path"
"path/filepath"
"gitea.com/gitea/act_runner/act/common"
"gitea.com/gitea/act_runner/act/model"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/act/model"
)
type stepActionLocal struct {
@@ -44,6 +44,10 @@ func (sal *stepActionLocal) main() common.Executor {
return nil
}
printRunActionHeader(ctx, sal.Step, sal.env, sal.getRunContext())
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
defer rawLogger.Infof("::endgroup::")
actionDir := filepath.Join(sal.getRunContext().Config.Workdir, sal.Step.Uses)
localReader := func(ctx context.Context) actionYamlReader {

View File

@@ -12,8 +12,8 @@ import (
"strings"
"testing"
"gitea.com/gitea/act_runner/act/common"
"gitea.com/gitea/act_runner/act/model"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/act/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
@@ -97,10 +97,10 @@ func TestStepActionLocalTest(t *testing.T) {
})
err := sal.pre()(ctx)
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
err = sal.main()(ctx)
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
cm.AssertExpectations(t)
salm.AssertExpectations(t)

View File

@@ -16,9 +16,9 @@ import (
"regexp"
"strings"
"gitea.com/gitea/act_runner/act/common"
"gitea.com/gitea/act_runner/act/common/git"
"gitea.com/gitea/act_runner/act/model"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/act/common/git"
"gitea.com/gitea/runner/act/model"
gogit "github.com/go-git/go-git/v5"
)
@@ -39,7 +39,6 @@ type stepActionRemote struct {
var stepActionRemoteNewCloneExecutor = git.NewGitCloneExecutor
//nolint:gocyclo // function handles many cases
func (sar *stepActionRemote) prepareActionExecutor() common.Executor {
return func(ctx context.Context) error {
if sar.remoteAction != nil && sar.action != nil {
@@ -166,6 +165,10 @@ func (sar *stepActionRemote) main() common.Executor {
return common.NewPipelineExecutor(
sar.prepareActionExecutor(),
runStepExecutor(sar, stepStageMain, func(ctx context.Context) error {
printRunActionHeader(ctx, sar.Step, sar.env, sar.RunContext)
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
defer rawLogger.Infof("::endgroup::")
github := sar.getGithubContext(ctx)
if sar.remoteAction.IsCheckout() && isLocalCheckout(github, sar.Step) && !sar.RunContext.Config.NoSkipCheckout {
if sar.RunContext.Config.BindWorkdir {

View File

@@ -14,9 +14,9 @@ import (
"testing"
"time"
"gitea.com/gitea/act_runner/act/common"
"gitea.com/gitea/act_runner/act/common/git"
"gitea.com/gitea/act_runner/act/model"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/act/common/git"
"gitea.com/gitea/runner/act/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
@@ -272,8 +272,8 @@ func TestStepActionRemotePre(t *testing.T) {
err := sar.pre()(ctx)
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, true, clonedAction) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.True(t, clonedAction)
sarm.AssertExpectations(t)
})
@@ -343,8 +343,8 @@ func TestStepActionRemotePreThroughAction(t *testing.T) {
err := sar.pre()(ctx)
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, true, clonedAction) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.True(t, clonedAction)
sarm.AssertExpectations(t)
})
@@ -419,7 +419,7 @@ func TestStepActionRemotePreThroughActionToken(t *testing.T) {
err := sar.pre()(ctx)
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
// Verify that the clone was called (URL should be redirected to github.com)
assert.True(t, actualURL != "", "Expected clone to be called") //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, "https://github.com/org/repo", actualURL, "URL should be redirected to github.com")

View File

@@ -9,9 +9,9 @@ import (
"fmt"
"strings"
"gitea.com/gitea/act_runner/act/common"
"gitea.com/gitea/act_runner/act/container"
"gitea.com/gitea/act_runner/act/model"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/act/container"
"gitea.com/gitea/runner/act/model"
"github.com/kballard/go-shellquote"
)
@@ -116,6 +116,10 @@ func (sd *stepDocker) newStepContainer(ctx context.Context, image string, cmd, e
envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_TEMP", "/tmp"))
binds, mounts := rc.GetBindsAndMounts()
networkMode := "container:" + rc.jobContainerName()
if rc.IsHostEnv(ctx) {
networkMode = "default"
}
stepContainer := ContainerNewContainer(&container.NewContainerInput{
Cmd: cmd,
Entrypoint: entrypoint,
@@ -123,10 +127,10 @@ func (sd *stepDocker) newStepContainer(ctx context.Context, image string, cmd, e
Image: image,
Username: rc.Config.Secrets["DOCKER_USERNAME"],
Password: rc.Config.Secrets["DOCKER_PASSWORD"],
Name: createSimpleContainerName(rc.jobContainerName(), "STEP-"+step.ID),
Name: createContainerName(rc.jobContainerName(), "STEP-"+step.ID),
Env: envList,
Mounts: mounts,
NetworkMode: "container:" + rc.jobContainerName(),
NetworkMode: networkMode,
Binds: binds,
Stdout: logWriter,
Stderr: logWriter,

View File

@@ -8,10 +8,11 @@ import (
"bytes"
"context"
"io"
"strings"
"testing"
"gitea.com/gitea/act_runner/act/container"
"gitea.com/gitea/act_runner/act/model"
"gitea.com/gitea/runner/act/container"
"gitea.com/gitea/runner/act/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
@@ -101,7 +102,7 @@ func TestStepDockerMain(t *testing.T) {
cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/pathcmd.txt").Return(io.NopCloser(&bytes.Buffer{}), nil)
err := sd.main()(ctx)
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, "node:14", input.Image)
@@ -113,8 +114,86 @@ func TestStepDockerPrePost(t *testing.T) {
sd := &stepDocker{}
err := sd.pre()(ctx)
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
err = sd.post()(ctx)
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err)
}
func TestStepDockerNewStepContainerNetworkMode(t *testing.T) {
cases := []struct {
name string
platform string
expectDefault bool
}{
{
name: "docker mode attaches to job container network",
platform: "node:14",
expectDefault: false,
},
{
name: "host mode uses default network",
platform: "-self-hosted",
expectDefault: true,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
cm := &containerMock{}
var captured *container.NewContainerInput
origContainerNewContainer := ContainerNewContainer
ContainerNewContainer = func(input *container.NewContainerInput) container.ExecutionsEnvironment {
captured = input
return cm
}
defer func() {
ContainerNewContainer = origContainerNewContainer
}()
ctx := context.Background()
platform := tc.platform
sd := &stepDocker{
RunContext: &RunContext{
StepResults: map[string]*model.StepResult{},
Config: &Config{
PlatformPicker: func(_ []string) string {
return platform
},
},
Run: &model.Run{
JobID: "1",
Workflow: &model.Workflow{
Jobs: map[string]*model.Job{
"1": {},
},
},
},
JobContainer: cm,
},
Step: &model.Step{
ID: "1",
Uses: "docker://alpine:3.20",
},
}
sd.RunContext.ExprEval = sd.RunContext.NewExpressionEvaluator(ctx)
assert.Equal(t, tc.expectDefault, sd.RunContext.IsHostEnv(ctx),
"IsHostEnv mismatch for platform %q", tc.platform)
_ = sd.newStepContainer(ctx, "alpine:3.20", []string{"echo", "hello"}, nil)
if tc.expectDefault {
assert.Equal(t, "default", captured.NetworkMode,
"host-mode step container must use 'default' network, got %q",
captured.NetworkMode)
} else {
assert.True(t, strings.HasPrefix(captured.NetworkMode, "container:"),
"docker-mode step container must attach to job container network, got %q",
captured.NetworkMode)
}
})
}
}

View File

@@ -7,7 +7,7 @@ package runner
import (
"fmt"
"gitea.com/gitea/act_runner/act/model"
"gitea.com/gitea/runner/act/model"
)
type stepFactory interface {

View File

@@ -7,7 +7,7 @@ package runner
import (
"testing"
"gitea.com/gitea/act_runner/act/model"
"gitea.com/gitea/runner/act/model"
"github.com/stretchr/testify/assert"
)
@@ -67,7 +67,7 @@ func TestStepFactoryNewStep(t *testing.T) {
step, err := sf.newStep(tt.model, &RunContext{})
assert.True(t, tt.check((step)))
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err)
})
}
}

View File

@@ -9,23 +9,27 @@ import (
"fmt"
"maps"
"runtime"
"slices"
"strings"
"gitea.com/gitea/act_runner/act/common"
"gitea.com/gitea/act_runner/act/container"
"gitea.com/gitea/act_runner/act/lookpath"
"gitea.com/gitea/act_runner/act/model"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/act/container"
"gitea.com/gitea/runner/act/lookpath"
"gitea.com/gitea/runner/act/model"
"github.com/kballard/go-shellquote"
yaml "go.yaml.in/yaml/v4"
)
type stepRun struct {
Step *model.Step
RunContext *RunContext
cmd []string
cmdline string
env map[string]string
WorkingDirectory string
Step *model.Step
RunContext *RunContext
cmd []string
cmdline string
env map[string]string
WorkingDirectory string
interpolatedScript string
shellCommand string
}
func (sr *stepRun) pre() common.Executor {
@@ -39,15 +43,154 @@ func (sr *stepRun) main() common.Executor {
return runStepExecutor(sr, stepStageMain, common.NewPipelineExecutor(
sr.setupShellCommandExecutor(),
func(ctx context.Context) error {
sr.getRunContext().ApplyExtraPath(ctx, &sr.env)
if he, ok := sr.getRunContext().JobContainer.(*container.HostEnvironment); ok && he != nil {
rc := sr.getRunContext()
// Apply ::add-path:: effects before printing so PATH is accurate in the env: block.
rc.ApplyExtraPath(ctx, &sr.env)
sr.printRunScriptActionDetails(ctx)
if he, ok := rc.JobContainer.(*container.HostEnvironment); ok && he != nil {
return he.ExecWithCmdLine(sr.cmd, sr.cmdline, sr.env, "", sr.WorkingDirectory)(ctx)
}
return sr.getRunContext().JobContainer.Exec(sr.cmd, sr.env, "", sr.WorkingDirectory)(ctx)
return rc.JobContainer.Exec(sr.cmd, sr.env, "", sr.WorkingDirectory)(ctx)
},
))
}
// printRunScriptActionDetails mirrors actions/runner ScriptHandler.PrintActionDetails
// for script steps.
func (sr *stepRun) printRunScriptActionDetails(ctx context.Context) {
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
scriptLineLogger := rawLogger.WithField(scriptLineCyanField, true)
normalized := strings.TrimRight(strings.ReplaceAll(sr.interpolatedScript, "\r\n", "\n"), "\n")
rawLogger.Infof("::group::Run %s", sr.runScriptGroupTitle(normalized))
if normalized != "" {
for line := range strings.SplitSeq(normalized, "\n") {
scriptLineLogger.Info(line)
}
}
rawLogger.Infof("shell: %s", sr.shellCommand)
printStepEnvBlock(ctx, sr.Step, sr.env, sr.getRunContext())
rawLogger.Infof("::endgroup::")
}
// printRunActionHeader mirrors actions/runner's "Run <action>" header for `uses:` steps,
// including the with: inputs and the step-level env: block. The caller is responsible
// for emitting ::endgroup:: after the action finishes.
func printRunActionHeader(ctx context.Context, step *model.Step, env map[string]string, rc *RunContext) {
if step == nil {
return
}
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
title := step.Uses
if step.Name != "" {
title = step.Name
}
rawLogger.Infof("::group::Run %s", title)
if len(step.With) > 0 {
rawLogger.Infof("with:")
for _, k := range slices.Sorted(maps.Keys(step.With)) {
rawLogger.Infof(" %s: %s", k, step.With[k])
}
}
printStepEnvBlock(ctx, step, env, rc)
}
// printStepEnvBlock emits the declared-env block (YAML order, internal vars filtered)
// shared by the run: and uses: "Run" headers.
func printStepEnvBlock(ctx context.Context, step *model.Step, env map[string]string, rc *RunContext) {
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
caseInsensitive := rc != nil && rc.JobContainer != nil && rc.JobContainer.IsEnvironmentCaseInsensitive()
var visible []string
for _, k := range stepDeclaredEnvKeysInOrder(step) {
if !isInternalEnvKey(k, caseInsensitive) {
visible = append(visible, k)
}
}
if len(visible) == 0 {
return
}
rawLogger.Infof("env:")
envLookup := env
if caseInsensitive {
envLookup = make(map[string]string, len(env))
for k, v := range env {
envLookup[strings.ToUpper(k)] = v
}
}
for _, k := range visible {
lookupKey := k
if caseInsensitive {
lookupKey = strings.ToUpper(k)
}
rawLogger.Infof(" %s: %s", k, envLookup[lookupKey])
}
}
// isInternalEnvKey matches actions/runner's filtered set of vars that are hidden
// from the "Run" header's env: block because they are injected by the runner itself.
func isInternalEnvKey(k string, caseInsensitive bool) bool {
upper := k
if caseInsensitive {
upper = strings.ToUpper(k)
}
switch upper {
case "PATH", "HOME", "CI":
return true
}
return strings.HasPrefix(upper, "GITHUB_") ||
strings.HasPrefix(upper, "GITEA_") ||
strings.HasPrefix(upper, "RUNNER_") ||
strings.HasPrefix(upper, "INPUT_")
}
func (sr *stepRun) runScriptGroupTitle(normalizedScript string) string {
trimmed := strings.TrimLeft(normalizedScript, " \t\r\n")
if idx := strings.IndexAny(trimmed, "\r\n"); idx >= 0 {
trimmed = trimmed[:idx]
}
if trimmed != "" {
return trimmed
}
if sr.Step != nil {
if sr.Step.Name != "" {
return sr.Step.Name
}
return sr.Step.ID
}
return ""
}
// stepDeclaredEnvKeysInOrder walks the raw YAML Env mapping so keys are emitted in
// the order the workflow author wrote them; step.Environment() decodes into a Go map
// and loses ordering.
func stepDeclaredEnvKeysInOrder(step *model.Step) []string {
if step == nil || step.Env.Kind != yaml.MappingNode {
return nil
}
content := step.Env.Content
keys := make([]string, 0, len(content)/2)
seen := make(map[string]struct{}, len(content)/2)
for i := 0; i+1 < len(content); i += 2 {
k := content[i]
if k.Kind != yaml.ScalarNode || k.Tag == "!!merge" || k.Value == "<<" {
continue
}
if _, dup := seen[k.Value]; dup {
continue
}
seen[k.Value] = struct{}{}
keys = append(keys, k.Value)
}
return keys
}
func (sr *stepRun) post() common.Executor {
return func(ctx context.Context) error {
return nil
@@ -111,8 +254,10 @@ func (sr *stepRun) setupShellCommand(ctx context.Context) (name, script string,
step := sr.Step
script = sr.RunContext.NewStepExpressionEvaluator(ctx, sr).Interpolate(ctx, step.Run)
sr.interpolatedScript = script
scCmd := step.ShellCommand()
sr.shellCommand = scCmd
name = getScriptName(sr.RunContext, step)

View File

@@ -0,0 +1,182 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// Copyright 2026 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package runner
import (
"bytes"
"context"
"strings"
"testing"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/act/model"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
yaml "go.yaml.in/yaml/v4"
)
func TestRunScriptGroupTitle(t *testing.T) {
sr := &stepRun{Step: &model.Step{Name: "Build"}}
assert.Equal(t, "make build", sr.runScriptGroupTitle("make build"))
assert.Equal(t, "echo one", sr.runScriptGroupTitle(" \techo one\necho two"))
assert.Equal(t, "Build", sr.runScriptGroupTitle(""))
sr = &stepRun{Step: &model.Step{ID: "s1"}}
assert.Equal(t, "s1", sr.runScriptGroupTitle("\n \n"))
}
func TestStepDeclaredEnvOrderPreservesYAML(t *testing.T) {
raw := `id: s1
run: "echo 1"
env:
GITHUB_TOKEN: tok
PATH: /custom/bin
MY_VAR: hello
`
var step model.Step
require.NoError(t, yaml.Unmarshal([]byte(raw), &step))
assert.Equal(t, []string{"GITHUB_TOKEN", "PATH", "MY_VAR"}, stepDeclaredEnvKeysInOrder(&step))
}
func TestStepDeclaredEnvKeysInOrderEmpty(t *testing.T) {
assert.Nil(t, stepDeclaredEnvKeysInOrder(nil))
assert.Empty(t, stepDeclaredEnvKeysInOrder(&model.Step{}))
}
func TestStepDeclaredEnvKeysIgnoreYAMLMergeKey(t *testing.T) {
doc := `
common: &common
COMMON_A: a
COMMON_B: b
step:
env:
LOCAL_BEFORE: before
<<: *common
COMMON_B: overridden
LOCAL_AFTER: after
`
var root struct {
Step model.Step `yaml:"step"`
}
require.NoError(t, yaml.Unmarshal([]byte(doc), &root))
keys := stepDeclaredEnvKeysInOrder(&root.Step)
assert.Equal(t, []string{"LOCAL_BEFORE", "COMMON_B", "LOCAL_AFTER"}, keys)
}
func TestPrintRunScriptActionDetailsGolden(t *testing.T) {
raw := `id: s1
name: Build
run: |
echo one
echo two
shell: pwsh
env:
PATH_PREFIX: /custom/bin
GITHUB_TOKEN: tok
GREETING: hello
`
var step model.Step
require.NoError(t, yaml.Unmarshal([]byte(raw), &step))
buf := &bytes.Buffer{}
logger := logrus.New()
logger.SetOutput(buf)
logger.SetLevel(logrus.InfoLevel)
logger.SetFormatter(&jobLogFormatter{color: cyan})
entry := logger.WithFields(logrus.Fields{"job": "j1"})
ctx := common.WithLogger(context.Background(), entry)
sr := &stepRun{
Step: &step,
RunContext: &RunContext{},
shellCommand: "pwsh -command . '{0}'",
interpolatedScript: "echo one\necho two\n",
env: map[string]string{
"PATH_PREFIX": "/custom/bin",
"GITHUB_TOKEN": "tok",
"GREETING": "hello",
},
}
sr.printRunScriptActionDetails(ctx)
want := strings.Join([]string{
"[j1] | ::group::Run echo one",
"[j1] | echo one",
"[j1] | echo two",
"[j1] | shell: pwsh -command . '{0}'",
"[j1] | env:",
"[j1] | PATH_PREFIX: /custom/bin",
"[j1] | GREETING: hello",
"[j1] | ::endgroup::",
"",
}, "\n")
assert.Equal(t, want, buf.String())
}
func TestPrintRunActionHeaderGolden(t *testing.T) {
raw := `id: s1
uses: actions/checkout@v4
with:
fetch-depth: "0"
token: secret
env:
CUSTOM: value
GITHUB_TOKEN: tok
`
var step model.Step
require.NoError(t, yaml.Unmarshal([]byte(raw), &step))
buf := &bytes.Buffer{}
logger := logrus.New()
logger.SetOutput(buf)
logger.SetLevel(logrus.InfoLevel)
logger.SetFormatter(&jobLogFormatter{color: cyan})
entry := logger.WithFields(logrus.Fields{"job": "j1"})
ctx := common.WithLogger(context.Background(), entry)
printRunActionHeader(ctx, &step, map[string]string{"CUSTOM": "value", "GITHUB_TOKEN": "tok"}, &RunContext{})
want := strings.Join([]string{
"[j1] | ::group::Run actions/checkout@v4",
"[j1] | with:",
"[j1] | fetch-depth: 0",
"[j1] | token: secret",
"[j1] | env:",
"[j1] | CUSTOM: value",
"",
}, "\n")
assert.Equal(t, want, buf.String())
}
func TestIsInternalEnvKey(t *testing.T) {
for _, k := range []string{"PATH", "HOME", "CI", "GITHUB_TOKEN", "GITEA_ACTIONS", "RUNNER_OS", "INPUT_FOO"} {
assert.True(t, isInternalEnvKey(k, false), k)
}
for _, k := range []string{"PATH_PREFIX", "MY_VAR", "GREETING", "HOMEPAGE"} {
assert.False(t, isInternalEnvKey(k, false), k)
}
assert.True(t, isInternalEnvKey("path", true))
assert.False(t, isInternalEnvKey("path", false))
}
func TestPrintColoredScriptLineCyan(t *testing.T) {
f := &jobLogFormatter{color: cyan}
entry := &logrus.Entry{
Level: logrus.InfoLevel,
Message: "echo one",
Data: logrus.Fields{
"job": "j1",
rawOutputField: true,
scriptLineCyanField: true,
},
}
buf := &bytes.Buffer{}
f.printColored(buf, entry)
assert.Equal(t, "\x1b[36m|\x1b[0m \x1b[36;1mecho one\x1b[0m", buf.String())
}

View File

@@ -10,8 +10,8 @@ import (
"io"
"testing"
"gitea.com/gitea/act_runner/act/container"
"gitea.com/gitea/act_runner/act/model"
"gitea.com/gitea/runner/act/container"
"gitea.com/gitea/runner/act/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
@@ -81,7 +81,7 @@ func TestStepRun(t *testing.T) {
cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/pathcmd.txt").Return(io.NopCloser(&bytes.Buffer{}), nil)
err := sr.main()(ctx)
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
cm.AssertExpectations(t)
}
@@ -91,8 +91,8 @@ func TestStepRunPrePost(t *testing.T) {
sr := &stepRun{}
err := sr.pre()(ctx)
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
err = sr.post()(ctx)
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err)
}

View File

@@ -8,8 +8,8 @@ import (
"context"
"testing"
"gitea.com/gitea/act_runner/act/common"
"gitea.com/gitea/act_runner/act/model"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/act/model"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
@@ -156,7 +156,7 @@ func TestSetupEnv(t *testing.T) {
sm.On("getEnv").Return(&env)
err := setupEnv(context.Background(), sm)
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
// These are commit or system specific
delete((env), "GITHUB_REF")
@@ -318,35 +318,35 @@ func TestIsContinueOnError(t *testing.T) {
step := createTestStep(t, "name: test")
continueOnError, err := isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain)
assertObject.False(continueOnError)
assertObject.Nil(err) //nolint:testifylint // pre-existing issue from nektos/act
assertObject.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
// explcit true
step = createTestStep(t, "continue-on-error: true")
continueOnError, err = isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain)
assertObject.True(continueOnError)
assertObject.Nil(err) //nolint:testifylint // pre-existing issue from nektos/act
assertObject.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
// explicit false
step = createTestStep(t, "continue-on-error: false")
continueOnError, err = isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain)
assertObject.False(continueOnError)
assertObject.Nil(err) //nolint:testifylint // pre-existing issue from nektos/act
assertObject.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
// expression true
step = createTestStep(t, "continue-on-error: ${{ 'test' == 'test' }}")
continueOnError, err = isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain)
assertObject.True(continueOnError)
assertObject.Nil(err) //nolint:testifylint // pre-existing issue from nektos/act
assertObject.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
// expression false
step = createTestStep(t, "continue-on-error: ${{ 'test' != 'test' }}")
continueOnError, err = isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain)
assertObject.False(continueOnError)
assertObject.Nil(err) //nolint:testifylint // pre-existing issue from nektos/act
assertObject.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
// expression parse error
step = createTestStep(t, "continue-on-error: ${{ 'test' != test }}")
continueOnError, err = isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain)
assertObject.False(continueOnError)
assertObject.NotNil(err) //nolint:testifylint // pre-existing issue from nektos/act
assertObject.Error(err)
}

View File

@@ -13,19 +13,29 @@
"@actions/github": "^4.0.0"
},
"devDependencies": {
"@vercel/ncc": "^0.24.1"
"@vercel/ncc": "^0.38.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@actions/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.0.tgz",
"integrity": "sha512-2aZDDa3zrrZbP5ZYg159sNoLRb61nQ7awl5pSvIq5Qpj81vwDzdMRKzkWJGJuwVvWpvZKx7vspJALyvaaIQyug==",
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz",
"integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==",
"license": "MIT",
"dependencies": {
"@actions/http-client": "^2.0.1",
"uuid": "^8.3.2"
"@actions/exec": "^1.1.1",
"@actions/http-client": "^2.0.1"
}
},
"node_modules/@actions/exec": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz",
"integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==",
"license": "MIT",
"dependencies": {
"@actions/io": "^1.0.1"
}
},
"node_modules/@actions/github": {
@@ -55,6 +65,12 @@
"tunnel": "^0.0.6"
}
},
"node_modules/@actions/io": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz",
"integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==",
"license": "MIT"
},
"node_modules/@octokit/auth-token": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz",
@@ -157,10 +173,11 @@
}
},
"node_modules/@vercel/ncc": {
"version": "0.24.1",
"resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.24.1.tgz",
"integrity": "sha512-r9m7brz2hNmq5TF3sxrK4qR/FhXn44XIMglQUir4sT7Sh5GOaYXlMYikHFwJStf8rmQGTlvOoBXt4yHVonRG8A==",
"version": "0.38.4",
"resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.38.4.tgz",
"integrity": "sha512-8LwjnlP39s08C08J5NstzriPvW1SP8Zfpp1BvC2sI35kPeZnHfxVkCwu4/+Wodgnd60UtT1n8K8zw+Mp7J9JmQ==",
"dev": true,
"license": "MIT",
"bin": {
"ncc": "dist/ncc/cli.js"
}
@@ -228,14 +245,6 @@
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
},
"node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",

View File

@@ -15,7 +15,7 @@
"@actions/github": "^4.0.0"
},
"devDependencies": {
"@vercel/ncc": "^0.24.1"
"@vercel/ncc": "^0.38.0"
},
"engines": {
"node": ">=12"

View File

@@ -13,19 +13,29 @@
"@actions/github": "^4.0.0"
},
"devDependencies": {
"@vercel/ncc": "^0.24.1"
"@vercel/ncc": "^0.38.0"
},
"engines": {
"node": ">=16"
}
},
"node_modules/@actions/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.0.tgz",
"integrity": "sha512-2aZDDa3zrrZbP5ZYg159sNoLRb61nQ7awl5pSvIq5Qpj81vwDzdMRKzkWJGJuwVvWpvZKx7vspJALyvaaIQyug==",
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz",
"integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==",
"license": "MIT",
"dependencies": {
"@actions/http-client": "^2.0.1",
"uuid": "^8.3.2"
"@actions/exec": "^1.1.1",
"@actions/http-client": "^2.0.1"
}
},
"node_modules/@actions/exec": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz",
"integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==",
"license": "MIT",
"dependencies": {
"@actions/io": "^1.0.1"
}
},
"node_modules/@actions/github": {
@@ -55,6 +65,12 @@
"tunnel": "^0.0.6"
}
},
"node_modules/@actions/io": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz",
"integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==",
"license": "MIT"
},
"node_modules/@octokit/auth-token": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz",
@@ -157,10 +173,11 @@
}
},
"node_modules/@vercel/ncc": {
"version": "0.24.1",
"resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.24.1.tgz",
"integrity": "sha512-r9m7brz2hNmq5TF3sxrK4qR/FhXn44XIMglQUir4sT7Sh5GOaYXlMYikHFwJStf8rmQGTlvOoBXt4yHVonRG8A==",
"version": "0.38.4",
"resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.38.4.tgz",
"integrity": "sha512-8LwjnlP39s08C08J5NstzriPvW1SP8Zfpp1BvC2sI35kPeZnHfxVkCwu4/+Wodgnd60UtT1n8K8zw+Mp7J9JmQ==",
"dev": true,
"license": "MIT",
"bin": {
"ncc": "dist/ncc/cli.js"
}
@@ -228,14 +245,6 @@
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
},
"node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",

View File

@@ -15,7 +15,7 @@
"@actions/github": "^4.0.0"
},
"devDependencies": {
"@vercel/ncc": "^0.24.1"
"@vercel/ncc": "^0.38.0"
},
"engines": {
"node": ">=16"

View File

@@ -1,11 +1,11 @@
{
"name": "node16",
"name": "node20",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "node16",
"name": "node20",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
@@ -13,19 +13,29 @@
"@actions/github": "^4.0.0"
},
"devDependencies": {
"@vercel/ncc": "^0.24.1"
"@vercel/ncc": "^0.38.0"
},
"engines": {
"node": ">=20"
}
},
"node_modules/@actions/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.0.tgz",
"integrity": "sha512-2aZDDa3zrrZbP5ZYg159sNoLRb61nQ7awl5pSvIq5Qpj81vwDzdMRKzkWJGJuwVvWpvZKx7vspJALyvaaIQyug==",
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz",
"integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==",
"license": "MIT",
"dependencies": {
"@actions/http-client": "^2.0.1",
"uuid": "^8.3.2"
"@actions/exec": "^1.1.1",
"@actions/http-client": "^2.0.1"
}
},
"node_modules/@actions/exec": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz",
"integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==",
"license": "MIT",
"dependencies": {
"@actions/io": "^1.0.1"
}
},
"node_modules/@actions/github": {
@@ -55,6 +65,12 @@
"tunnel": "^0.0.6"
}
},
"node_modules/@actions/io": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz",
"integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==",
"license": "MIT"
},
"node_modules/@octokit/auth-token": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz",
@@ -157,10 +173,11 @@
}
},
"node_modules/@vercel/ncc": {
"version": "0.24.1",
"resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.24.1.tgz",
"integrity": "sha512-r9m7brz2hNmq5TF3sxrK4qR/FhXn44XIMglQUir4sT7Sh5GOaYXlMYikHFwJStf8rmQGTlvOoBXt4yHVonRG8A==",
"version": "0.38.4",
"resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.38.4.tgz",
"integrity": "sha512-8LwjnlP39s08C08J5NstzriPvW1SP8Zfpp1BvC2sI35kPeZnHfxVkCwu4/+Wodgnd60UtT1n8K8zw+Mp7J9JmQ==",
"dev": true,
"license": "MIT",
"bin": {
"ncc": "dist/ncc/cli.js"
}
@@ -228,14 +245,6 @@
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
},
"node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",

View File

@@ -15,7 +15,7 @@
"@actions/github": "^4.0.0"
},
"devDependencies": {
"@vercel/ncc": "^0.24.1"
"@vercel/ncc": "^0.38.0"
},
"engines": {
"node": ">=20"

View File

@@ -32,7 +32,7 @@ runs:
shell: bash
- uses: ./localdockerimagetest_
# Also test a remote docker action here
- uses: actions/hello-world-docker-action@v1
- uses: actions/hello-world-docker-action@v2
with:
who-to-greet: 'Mona the Octocat'
# Test if GITHUB_ACTION_PATH is set correctly after all steps

View File

@@ -38,7 +38,6 @@ func CompilePattern(rawpattern string) (*WorkflowPattern, error) {
}, nil
}
//nolint:gocyclo // function handles many cases
func PatternToRegex(pattern string) (string, error) {
var rpattern strings.Builder
rpattern.WriteString("^")

View File

@@ -1,4 +1,4 @@
# Usage Examples for `act_runner`
# Usage Examples for `gitea-runner`
Welcome to our collection of usage and deployment examples specifically designed for Gitea setups. Whether you're a beginner or an experienced user, you'll find practical resources here that you can directly apply to enhance your Gitea experience. We encourage you to contribute your own insights and knowledge to make this collection even more comprehensive and valuable.

View File

@@ -1,4 +1,4 @@
### Running `act_runner` using `docker-compose`
### Running `gitea-runner` using `docker-compose`
```yml
...
@@ -19,15 +19,15 @@
# - GITEA_RUNNER_REGISTRATION_TOKEN=<user-defined registration token>
runner:
image: gitea/act_runner
image: gitea/runner
restart: always
depends_on:
gitea:
# required so runner can attach to gitea, see "healthcheck"
condition: service_healthy
condition: service_healthy
restart: true
volumes:
- ./data/act_runner:/data
- ./data/runner:/data
- /var/run/docker.sock:/var/run/docker.sock
environment:
- GITEA_INSTANCE_URL=<instance url>
@@ -38,18 +38,18 @@
- GITEA_RUNNER_REGISTRATION_TOKEN=<registration token>
```
### Running `act_runner` using Docker-in-Docker (DIND)
### Running `gitea-runner` using Docker-in-Docker (DIND)
```yml
...
runner:
image: gitea/act_runner:latest-dind-rootless
image: gitea/runner:latest-dind-rootless
restart: always
privileged: true
depends_on:
- gitea
volumes:
- ./data/act_runner:/data
- ./data/runner:/data
environment:
- GITEA_INSTANCE_URL=<instance url>
- DOCKER_HOST=unix:///var/run/user/1000/docker.sock

View File

@@ -1,7 +1,7 @@
### Run `act_runner` in a Docker Container
### Run `gitea-runner` in a Docker Container
```sh
docker run -e GITEA_INSTANCE_URL=http://192.168.8.18:3000 -e GITEA_RUNNER_REGISTRATION_TOKEN=<runner_token> -v /var/run/docker.sock:/var/run/docker.sock -v $PWD/data:/data --name my_runner gitea/act_runner:nightly
docker run -e GITEA_INSTANCE_URL=http://192.168.8.18:3000 -e GITEA_RUNNER_REGISTRATION_TOKEN=<runner_token> -v /var/run/docker.sock:/var/run/docker.sock -v $PWD/data:/data --name my_runner gitea/runner:nightly
```
The `/data` directory inside the docker container contains the runner API keys after registration.

View File

@@ -1,4 +1,4 @@
## Kubernetes Docker in Docker Deployment with `act_runner`
## Kubernetes Docker in Docker Deployment with `gitea-runner`
NOTE: Docker in Docker (dind) requires elevated privileges on Kubernetes. The current way to achieve this is to set the pod `SecurityContext` to `privileged`. Keep in mind that this is a potential security issue that has the potential for a malicious application to break out of the container context.

View File

@@ -1,7 +1,7 @@
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: act-runner-vol
name: runner-vol
spec:
accessModes:
- ReadWriteOnce
@@ -13,7 +13,7 @@ spec:
apiVersion: v1
data:
# The registration token can be obtained from the web UI, API or command-line.
# You can also set a pre-defined global runner registration token for the Gitea instance via
# You can also set a pre-defined global runner registration token for the Gitea instance via
# `GITEA_RUNNER_REGISTRATION_TOKEN`/`GITEA_RUNNER_REGISTRATION_TOKEN_FILE` environment variable.
token: << base64 encoded registration token >>
kind: Secret
@@ -25,19 +25,19 @@ apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: act-runner
name: act-runner
app: runner
name: runner
spec:
replicas: 1
selector:
matchLabels:
app: act-runner
app: runner
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: act-runner
app: runner
spec:
restartPolicy: Always
volumes:
@@ -45,10 +45,10 @@ spec:
emptyDir: {}
- name: runner-data
persistentVolumeClaim:
claimName: act-runner-vol
claimName: runner-vol
containers:
- name: runner
image: gitea/act_runner:nightly
image: gitea/runner:nightly
command: ["sh", "-c", "while ! nc -z localhost 2376 </dev/null; do echo 'waiting for docker daemon...'; sleep 5; done; /sbin/tini -- run.sh"]
env:
- name: DOCKER_HOST

View File

@@ -1,7 +1,7 @@
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: act-runner-vol
name: runner-vol
spec:
accessModes:
- ReadWriteOnce
@@ -13,7 +13,7 @@ spec:
apiVersion: v1
data:
# The registration token can be obtained from the web UI, API or command-line.
# You can also set a pre-defined global runner registration token for the Gitea instance via
# You can also set a pre-defined global runner registration token for the Gitea instance via
# `GITEA_RUNNER_REGISTRATION_TOKEN`/`GITEA_RUNNER_REGISTRATION_TOKEN_FILE` environment variable.
token: << base64 encoded registration token >>
kind: Secret
@@ -25,32 +25,32 @@ apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: act-runner
name: act-runner
app: runner
name: runner
spec:
replicas: 1
selector:
matchLabels:
app: act-runner
app: runner
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: act-runner
app: runner
spec:
restartPolicy: Always
volumes:
- name: runner-data
persistentVolumeClaim:
claimName: act-runner-vol
claimName: runner-vol
securityContext:
fsGroup: 1000
containers:
- name: runner
image: gitea/act_runner:nightly-dind-rootless
image: gitea/runner:nightly-dind-rootless
imagePullPolicy: Always
# command: ["sh", "-c", "while ! nc -z localhost 2376 </dev/null; do echo 'waiting for docker daemon...'; sleep 5; done; /sbin/tini -- /opt/act/run.sh"]
# command: ["sh", "-c", "while ! nc -z localhost 2376 </dev/null; do echo 'waiting for docker daemon...'; sleep 5; done; /sbin/tini -- run.sh"]
env:
- name: DOCKER_HOST
value: tcp://localhost:2376

View File

@@ -1,4 +1,4 @@
## `act_runner` on Virtual or Physical Servers
## `gitea-runner` on Virtual or Physical Servers
Files in this directory:

View File

@@ -1,12 +1,12 @@
## Using Rootless Docker with`act_runner`
## Using Rootless Docker with`gitea-runner`
Here is a simple example of how to set up `act_runner` with rootless Docker. It has been created with Debian, but other Linux should work the same way.
Here is a simple example of how to set up `gitea-runner` with rootless Docker. It has been created with Debian, but other Linux should work the same way.
Note: This procedure needs a real login shell -- using `sudo su` or other method of accessing the account will fail some of the steps below.
As `root`:
- Create a user to run both `docker` and `act_runner`. In this example, we use a non-privileged account called `rootless`.
- Create a user to run both `docker` and `gitea-runner`. In this example, we use a non-privileged account called `rootless`.
```bash
useradd -m rootless
@@ -38,36 +38,36 @@ export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
```
- Reboot. Ensure that the Docker process is working.
- Create a directory for saving `act_runner` data between restarts
- Create a directory for saving `gitea-runner` data between restarts
`mkdir /home/rootless/act_runner`
`mkdir /home/rootless/gitea-runner`
- Register the runner from the data directory
```bash
cd /home/rootless/act_runner
act_runner register
cd /home/rootless/gitea-runner
gitea-runner register
```
- Generate a `act_runner` configuration file in the data directory. Edit the file to adjust for the system.
- Generate a `gitea-runner` configuration file in the data directory. Edit the file to adjust for the system.
```bash
act_runner generate-config >/home/rootless/act_runner/config
gitea-runner generate-config >/home/rootless/gitea-runner/config
```
- Create a new user-level`systemd` unit file as `/home/rootless/.config/systemd/user/act_runner.service` with the following contents:
- Create a new user-level`systemd` unit file as `/home/rootless/.config/systemd/user/gitea-runner.service` with the following contents:
```bash
Description=Gitea Actions runner
Documentation=https://gitea.com/gitea/act_runner
Documentation=https://gitea.com/gitea/runner
After=docker.service
[Service]
Environment=PATH=/home/rootless/bin:/sbin:/usr/sbin:/home/rootless/bin:/home/rootless/bin:/home/rootless/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games
Environment=DOCKER_HOST=unix:///run/user/1001/docker.sock
ExecStart=/usr/bin/act_runner daemon -c /home/rootless/act_runner/config
ExecStart=/usr/bin/gitea-runner daemon -c /home/rootless/gitea-runner/config
ExecReload=/bin/kill -s HUP $MAINPID
WorkingDirectory=/home/rootless/act_runner
WorkingDirectory=/home/rootless/gitea-runner
TimeoutSec=0
RestartSec=2
Restart=always
@@ -88,8 +88,8 @@ export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
- Reboot
After the system restarts, check that the`act_runner` is working and that the runner is connected to Gitea.
After the system restarts, check that the`gitea-runner` is working and that the runner is connected to Gitea.
````bash
systemctl --user status act_runner
journalctl --user -xeu act_runner
systemctl --user status gitea-runner
journalctl --user -xeu gitea-runner

30
go.mod
View File

@@ -1,4 +1,4 @@
module gitea.com/gitea/act_runner
module gitea.com/gitea/runner
go 1.26.0
@@ -6,14 +6,14 @@ require (
code.gitea.io/actions-proto-go v0.4.1
connectrpc.com/connect v1.19.2
github.com/avast/retry-go/v4 v4.7.0
github.com/docker/docker v25.0.13+incompatible
github.com/docker/docker v25.0.15+incompatible
github.com/joho/godotenv v1.5.1
github.com/mattn/go-isatty v0.0.20
github.com/mattn/go-isatty v0.0.22
github.com/sirupsen/logrus v1.9.4
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1
go.yaml.in/yaml/v4 v4.0.0-rc.3
golang.org/x/term v0.40.0
golang.org/x/term v0.42.0
golang.org/x/time v0.14.0 // indirect
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1 // indirect
@@ -25,7 +25,7 @@ require (
github.com/creack/pty v1.1.24
github.com/distribution/reference v0.6.0
github.com/docker/cli v25.0.7+incompatible
github.com/docker/go-connections v0.6.0
github.com/docker/go-connections v0.7.0
github.com/go-git/go-billy/v5 v5.8.0
github.com/go-git/go-git/v5 v5.18.0
github.com/gobwas/glob v0.2.3
@@ -33,12 +33,12 @@ require (
github.com/julienschmidt/httprouter v1.3.0
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/moby/buildkit v0.13.2
github.com/moby/patternmatcher v0.6.0
github.com/moby/patternmatcher v0.6.1
github.com/opencontainers/image-spec v1.1.1
github.com/opencontainers/selinux v1.13.1
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.23.2
github.com/rhysd/actionlint v1.7.11
github.com/rhysd/actionlint v1.7.12
github.com/spf13/pflag v1.0.10
github.com/timshannon/bolthold v0.0.0-20240314194003-30aac6950928
go.etcd.io/bbolt v1.4.3
@@ -62,7 +62,7 @@ require (
github.com/docker/docker-credential-helpers v0.9.5 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fatih/color v1.19.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-logr/logr v1.4.3 // indirect
@@ -77,7 +77,7 @@ require (
github.com/klauspost/compress v1.18.4 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-runewidth v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.21 // indirect
github.com/mattn/go-shellwords v1.0.12 // indirect
github.com/mitchellh/mapstructure v1.1.2 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
@@ -105,16 +105,12 @@ require (
go.opentelemetry.io/otel/metric v1.40.0 // indirect
go.opentelemetry.io/otel/trace v1.40.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.50.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.43.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/grpc v1.67.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
// Remove after github.com/docker/distribution is updated to support distribution/reference v0.6.0
// (pulled in via moby/buildkit, breaks on undefined: reference.SplitHostname)
replace github.com/distribution/reference v0.6.0 => github.com/distribution/reference v0.5.0

38
go.sum
View File

@@ -1,7 +1,5 @@
code.gitea.io/actions-proto-go v0.4.1 h1:l0EYhjsgpUe/1VABo2eK7zcoNX2W44WOnb0MSLrKfls=
code.gitea.io/actions-proto-go v0.4.1/go.mod h1:mn7Wkqz6JbnTOHQpot3yDeHx+O5C9EGhMEE+htvHBas=
connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14=
connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
connectrpc.com/connect v1.19.2 h1:McQ83FGdzL+t60peksi0gXC7MQ/iLKgLduAnThbM0mo=
connectrpc.com/connect v1.19.2/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
cyphar.com/go-pathrs v0.2.3 h1:0pH8gep37wB0BgaXrEaN1OtZhUMeS7VvaejSr6i822o=
@@ -51,18 +49,22 @@ github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/cli v25.0.3+incompatible h1:KLeNs7zws74oFuVhgZQ5ONGZiXUUdgsdy6/EsX/6284=
github.com/docker/cli v25.0.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/cli v25.0.7+incompatible h1:scW/AbGafKmANsonsFckFHTwpz2QypoPA/zpoLnDs/E=
github.com/docker/cli v25.0.7+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker v25.0.13+incompatible h1:YeBrkUd3q0ZoRDNoEzuopwCLU+uD8GZahDHwBdsTnkU=
github.com/docker/docker v25.0.13+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v25.0.14+incompatible h1:+HNue3fKbqiDHYFAriyiMjfS5u25zB0E2/R8f42lOMc=
github.com/docker/docker v25.0.14+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v25.0.15+incompatible h1:JhRD6vZdk0Ms3SEMztefBISJL13NbxudQnGix6l+T5M=
github.com/docker/docker v25.0.15+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=
github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=
github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
@@ -71,6 +73,8 @@ github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
@@ -135,8 +139,12 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
@@ -145,6 +153,8 @@ github.com/moby/buildkit v0.13.2 h1:nXNszM4qD9E7QtG7bFWPnDI1teUQFQglBzon/IU3SzI=
github.com/moby/buildkit v0.13.2/go.mod h1:2cyVOv9NoHM7arphK9ZfHIWKn9YVZRFd1wXB8kKmEzY=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
@@ -181,6 +191,8 @@ github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzM
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/rhysd/actionlint v1.7.11 h1:m+aSuCpCIClS8X02xMG4Z8s87fCHPsAtYkAoWGQZgEE=
github.com/rhysd/actionlint v1.7.11/go.mod h1:8n50YougV9+50niD7oxgDTZ1KbN/ZnKiQ2xpLFeVhsI=
github.com/rhysd/actionlint v1.7.12 h1:vQ4GeJN86C0QH+gTUQcs8McmK62OLT3kmakPMtEWYnY=
github.com/rhysd/actionlint v1.7.12/go.mod h1:krOUhujIsJusovkaYzQ/VNH8PFexjNKqU0q5XI/4w+g=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
@@ -261,6 +273,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -272,11 +286,15 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -291,9 +309,17 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=

View File

@@ -4,12 +4,13 @@
package cmd
import (
"errors"
"fmt"
"os"
"os/signal"
"gitea.com/gitea/act_runner/act/artifactcache"
"gitea.com/gitea/act_runner/internal/pkg/config"
"gitea.com/gitea/runner/act/artifactcache"
"gitea.com/gitea/runner/internal/pkg/config"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
@@ -47,10 +48,15 @@ func runCacheServer(configFile *string, cacheArgs *cacheServerArgs) func(cmd *co
port = cacheArgs.Port
}
secret := cfg.Cache.ExternalSecret
if secret == "" {
return errors.New("cache.external_secret must be set for cache-server; configure the same value on each runner that points at this server via cache.external_server")
}
cacheHandler, err := artifactcache.StartHandler(
dir,
host,
port,
secret,
log.StandardLogger().WithField("module", "cache_request"),
)
if err != nil {

View File

@@ -8,25 +8,24 @@ import (
"fmt"
"os"
"gitea.com/gitea/act_runner/internal/pkg/config"
"gitea.com/gitea/act_runner/internal/pkg/ver"
"gitea.com/gitea/runner/internal/pkg/config"
"gitea.com/gitea/runner/internal/pkg/ver"
"github.com/spf13/cobra"
)
func Execute(ctx context.Context) {
// ./act_runner
// ./gitea-runner
rootCmd := &cobra.Command{
Use: "act_runner [event name to run]\nIf no event name passed, will default to \"on: push\"",
Short: "Run GitHub actions locally by specifying the event name (e.g. `push`) or an action name directly.",
Args: cobra.MaximumNArgs(1),
Use: "gitea-runner",
Short: "Gitea Runner",
Version: ver.Version(),
SilenceUsage: true,
}
configFile := ""
rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "", "Config file path")
// ./act_runner register
// ./gitea-runner register
var regArgs registerArgs
registerCmd := &cobra.Command{
Use: "register",
@@ -42,7 +41,7 @@ func Execute(ctx context.Context) {
registerCmd.Flags().BoolVar(&regArgs.Ephemeral, "ephemeral", false, "Configure the runner to be ephemeral and only ever be able to pick a single job (stricter than --once)")
rootCmd.AddCommand(registerCmd)
// ./act_runner daemon
// ./gitea-runner daemon
var daemArgs daemonArgs
daemonCmd := &cobra.Command{
Use: "daemon",
@@ -53,10 +52,10 @@ func Execute(ctx context.Context) {
daemonCmd.Flags().BoolVar(&daemArgs.Once, "once", false, "Run one job then exit")
rootCmd.AddCommand(daemonCmd)
// ./act_runner exec
// ./gitea-runner exec
rootCmd.AddCommand(loadExecCmd(ctx))
// ./act_runner config
// ./gitea-runner config
rootCmd.AddCommand(&cobra.Command{
Use: "generate-config",
Short: "Generate an example config file",
@@ -66,7 +65,7 @@ func Execute(ctx context.Context) {
},
})
// ./act_runner cache-server
// ./gitea-runner cache-server
var cacheArgs cacheServerArgs
cacheCmd := &cobra.Command{
Use: "cache-server",

View File

@@ -16,14 +16,14 @@ import (
"strings"
"time"
"gitea.com/gitea/act_runner/internal/app/poll"
"gitea.com/gitea/act_runner/internal/app/run"
"gitea.com/gitea/act_runner/internal/pkg/client"
"gitea.com/gitea/act_runner/internal/pkg/config"
"gitea.com/gitea/act_runner/internal/pkg/envcheck"
"gitea.com/gitea/act_runner/internal/pkg/labels"
"gitea.com/gitea/act_runner/internal/pkg/metrics"
"gitea.com/gitea/act_runner/internal/pkg/ver"
"gitea.com/gitea/runner/internal/app/poll"
"gitea.com/gitea/runner/internal/app/run"
"gitea.com/gitea/runner/internal/pkg/client"
"gitea.com/gitea/runner/internal/pkg/config"
"gitea.com/gitea/runner/internal/pkg/envcheck"
"gitea.com/gitea/runner/internal/pkg/labels"
"gitea.com/gitea/runner/internal/pkg/metrics"
"gitea.com/gitea/runner/internal/pkg/ver"
"connectrpc.com/connect"
"github.com/mattn/go-isatty"
@@ -104,7 +104,7 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
}
// if dockerSocketPath passes the check, override DOCKER_HOST with dockerSocketPath
os.Setenv("DOCKER_HOST", dockerSocketPath)
// empty cfg.Container.DockerHost means act_runner need to find an available docker host automatically
// empty cfg.Container.DockerHost means runner need to find an available docker host automatically
// and assign the path to cfg.Container.DockerHost
if cfg.Container.DockerHost == "" {
cfg.Container.DockerHost = dockerSocketPath

View File

@@ -6,6 +6,8 @@ package cmd
import (
"context"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"maps"
@@ -15,11 +17,11 @@ import (
"strings"
"time"
"gitea.com/gitea/act_runner/act/artifactcache"
"gitea.com/gitea/act_runner/act/artifacts"
"gitea.com/gitea/act_runner/act/common"
"gitea.com/gitea/act_runner/act/model"
"gitea.com/gitea/act_runner/act/runner"
"gitea.com/gitea/runner/act/artifactcache"
"gitea.com/gitea/runner/act/artifacts"
"gitea.com/gitea/runner/act/common"
"gitea.com/gitea/runner/act/model"
"gitea.com/gitea/runner/act/runner"
"github.com/docker/docker/api/types/container"
"github.com/joho/godotenv"
@@ -368,7 +370,7 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
}
// init a cache server
handler, err := artifactcache.StartHandler("", "", 0, log.StandardLogger().WithField("module", "cache_request"))
handler, err := artifactcache.StartHandler("", "", 0, "", log.StandardLogger().WithField("module", "cache_request"))
if err != nil {
return err
}
@@ -393,6 +395,25 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
execArgs.artifactServerPath = tempDir
}
// Register ACTIONS_RUNTIME_TOKEN against local cache server
env := execArgs.LoadEnvs()
const actionsRuntimeTokenEnvName = "ACTIONS_RUNTIME_TOKEN"
actionsRuntimeToken := env[actionsRuntimeTokenEnvName]
if actionsRuntimeToken == "" {
actionsRuntimeToken = os.Getenv(actionsRuntimeTokenEnvName)
}
if actionsRuntimeToken == "" {
tmpBranch := make([]byte, 12)
if _, err := rand.Read(tmpBranch); err != nil {
actionsRuntimeToken = "token"
} else {
actionsRuntimeToken = hex.EncodeToString(tmpBranch)
}
env[actionsRuntimeTokenEnvName] = actionsRuntimeToken
os.Setenv(actionsRuntimeTokenEnvName, actionsRuntimeToken)
}
handler.RegisterJob(actionsRuntimeToken, "__local/__exec")
// run the plan
config := &runner.Config{
Workdir: execArgs.Workdir(),
@@ -402,7 +423,7 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
ForceRebuild: execArgs.forceRebuild,
LogOutput: true,
JSONLogger: execArgs.jsonLogger,
Env: execArgs.LoadEnvs(),
Env: env,
Vars: execArgs.LoadVars(),
Secrets: execArgs.LoadSecrets(),
InsecureSecrets: execArgs.insecureSecrets,

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