## Summary
This PR improves reusable workflow support for Gitea Actions. The
parsing of the called workflow now happens on Gitea side, not on the
runner. When the caller becomes ready, Gitea fetches the called workflow
source, parses it, and inserts each child job into the database as a
`ActionRunJob` linked to the caller via `ParentCallJobID`. As a result,
every callee job is dispatched as its own task and its logs surface as
an independent job entry in the UI, rather than being inlined into the
caller's "Set up job" step.
This PR supports two kinds of `uses` :
- same-repo call: `uses: ./.gitea/workflows/foo.yaml`
- cross-repo call: `uses: OWNER/REPO/.gitea/workflows/foo.yaml@REF`
## **⚠️ BREAKING ⚠️**
External reusable workflows (`uses:
https://other-gitea-instance/OWNER/REPO/.gitea/workflows/test.yaml@REF`)
are no longer supported. To keep using them, clone the repositories to
the local instance.
## Main changes
### Execution model
- Each caller job carries `IsReusableCaller=true` and won't be fetched
by runners.
- `ParentCallJobID` can link a called job to its caller.
- Caller status is derived from its direct children.
### Workflow syntax
- `jobparser` now supports parsing `on: workflow_call` trigger with
`inputs:`, `outputs:`, and `secrets:` declarations.
- **Max nesting depth**: capped at `MaxReusableCallLevels = 9`, which
means a top-level caller may have at most 9 nested callers below it.
- **Cycle prevention**: at expansion time, `checkCallerChain` walks the
caller's ancestor chain via `ParentCallJobID` and rejects if the same
`uses:` string appears anywhere upstream (`reusable workflow call cycle
detected`). This catches both direct (`A -> A`) and indirect (`A -> B ->
A`) cycles.
### Cross-repo access
- To share reusable workflows from private repos, use `Collaborative
Owners` introduced by #32562
### Rerun semantics
- `expandRerunJobIDs` partitions the latest attempt's jobs into:
- a **rerun set**: jobs being rerun + downstream siblings within the
same scope.
- an **ancestor set**: reusable callers whose only *some* descendants
are being rerun (the caller itself is not).
- Cloning behavior for callers in `execRerunPlan`:
- **Caller is fully rerun** (caller's `AttemptJobID` in `rerunSet`):
none of its descendants are cloned. The caller is cloned with
`IsCallerExpanded=false`, and re-expansion (which reinserts the children
fresh) happens later when the resolver brings the caller to `Waiting`
again.
- **Caller is in ancestor set** (only some descendants rerun): the
caller is pass-through (`Status` will be updated by its fresh children).
Its non-rerun descendants are also pass-through clones (point
`SourceTaskID` at the original task). Their `ParentCallJobID` is
remapped to the new attempt's caller row.
### UI
- Job list in `RepoActionView.vue` is now tree-shaped: callers indent
their children. Callers default to collapsed.
- New caller detail page using `WorkflowGraph` to show direct children
only; the run summary's `WorkflowGraph` shows top-level callers and
their immediate descendants.
### Known trade-offs
- **Caller expansion runs inside the enclosing write transaction.**
`expandReusableWorkflowCaller` performs a git read of the called
workflow while holding the row locks that update the caller and insert
its children. This is intentional: the caller-row update and child-row
inserts must commit atomically. None of the call sites is hot (each
caller is expanded once per attempt), so the trade-off is acceptable.
- **A malformed `if:` expression on a job leaves it `Blocked`
silently.** `evaluateJobIf` now runs server-side as part of resolver
passes; deterministic expression errors (typos, undefined context
fields) are logged but do not surface in the UI. This is the same
behavior the resolver already had for concurrency-expression errors.
Distinguishing transient DB errors from user-authored expression errors
and writing the latter back as `StatusFailure` is a follow-up.
#### Screenshots
<img width="1600" alt="image"
src="https://github.com/user-attachments/assets/bfaa9b7a-07e9-4127-8de9-a81f86e82828"
/>
<img width="1600" alt="image"
src="https://github.com/user-attachments/assets/8af109b3-ef28-4b53-aaad-d4632b923224"
/>
## References
-
https://docs.github.com/en/actions/how-tos/reuse-automations/reuse-workflows
-
https://docs.github.com/en/actions/reference/workflows-and-actions/reusing-workflow-configurations
---
Replace #36388
---------
Signed-off-by: Zettat123 <zettat123@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
A matrix built from a dependent job's runtime output (fromJson(needs.*.outputs.*))
has no inherent size bound, so a large or buggy upstream output could insert an
arbitrary number of jobs in one transaction. The per-attempt AttemptJobIDs also
back the job-index URLs, which must stay below MaxJobNumPerRun. Skip the
expansion with a clear reason when the resulting jobs would exceed that limit.
Also rename a test variable that tripped the misspell linter and clarify the
deferred-placeholder commit-status comment.
Co-Authored-By: Claude (Opus 4.8) <noreply@anthropic.com>
Expand a needs-dependent matrix through the new jobparser.ExpandMatrixWithNeeds,
which evaluates the raw matrix against the needs context via NewInterpeter the
same way EvaluateConcurrency does. This replaces synthesizing a workflow with
stub jobs and re-parsing it once the dependencies complete.
Parse now emits a single placeholder per needs-dependent matrix and the whole
matrix expands exactly once when the needs finish, instead of being split per
static value at planning time and re-expanded per placeholder. This removes
ExtractRawStrategies, HasMatrixWithNeeds and constructWorkflowWithNeeds.
Side effects of deferring the whole matrix: a static+dynamic matrix no longer
duplicates N times, needs-output detection no longer relies on string matching
(so bracket notation is handled), and a deferred placeholder gets no commit
status until it expands, avoiding an orphaned status from the name change.
Co-Authored-By: Claude (Opus 4.8) <noreply@anthropic.com>
- expanded matrix jobs inherit RunAttemptID, Attempt, AttemptJobID and
TokenPermissions, so they stay in the attempt and downstream needs/status
aggregation account for them
- reuse the placeholder as the first combination instead of leaving a skipped
phantom job that poisoned downstream `needs`
- jobparser only clones + pre-evaluates a matrix when it contains a `${{ }}`
expression; static matrices keep the original path
- handle the expanded-job marshal error, drop the unused mergeNeedsIntoVars,
gate and attempt-scope the post-resolve job reload, trim resolver logging
- migration: add IgnoreConstrains for mssql; revert the unrelated go.mod
golang.org/x/net downgrade
Co-Authored-By: Claude (Opus 4.8) <noreply@anthropic.com>
Gitea now only allows `workflow_dispatch.inputs`. If a workflow contains
`workflow_call.inputs`, the workflow cannot be triggered, even though
the `on:` section contains other trigger events.
428ee9fcce/modules/actions/jobparser/model.go (L402-L405)
For example, this workflow cannot be triggered due to
`workflow_call.inputs`:
```yaml
on:
push:
pull_request:
workflow_call:
inputs:
name:
type: string
```
---
This PR is extracted from #37478 for backport
---------
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.8) <noreply@anthropic.com>
Co-authored-by: Giteabot <teabot@gitea.io>
- Expand NewInterpeter ownership comment to explicitly confirm that
run.Workflow is fresh per call and job is only read (not written),
so concurrent calls for different matrix combinations are race-free
- Downgrade the two remaining log.Info lines (matrix re-evaluation
complete / succeeded) to log.Debug per reviewer request
- Add unit tests for constructWorkflowWithNeeds (happy path, invalid
payload, missing jobs section), mergeNeedsIntoVars, and a
multi-dimension dynamic matrix case in TestHasMatrixWithNeeds
Co-Authored-By: Claude <claude-sonnet-4-5@anthropic.com>
This PR hardens artifact URL signing by encoding signature inputs in an
unambiguous binary payload before computing the HMAC.
What it changes:
- replace direct concatenation-style signing inputs with explicit
payload builders
- encode string fields with a length prefix before appending their bytes
- encode integer fields as fixed-width binary values instead of decimal
text
- apply the same hardening to both:
- Actions Artifact V4 signing in `routers/api/actions/artifactsv4.go`
- artifact download signing in `routers/api/v1/repo/action.go`
- add regression tests that verify distinct field combinations produce
distinct payloads and signatures
Why:
The previous signing logic built HMAC inputs by appending multiple
fields without a strongly structured representation. That kind of
construction can create ambiguity at field boundaries, where different
parameter combinations may serialize into the same byte stream for
signing.
This change removes that ambiguity by constructing a deterministic
payload format with explicit boundaries between fields.
---------
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Nicolas <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
`act/workflowpattern` in runner is only consumed by Gitea and dead code
there. Move it to this repo. Use `modules/glob` for glob pattern match.
---------
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Action runs, jobs and steps have 8 statuses but the UI only showed 5
(from the commit status api) for the latter two. Align all 8 to GitHub
as closely as possible:
- waiting — `octicon-circle` (hollow circle), gray
- blocked — `octicon-blocked` (slashed circle), yellow
- running — `gitea-running` (rotating spinner), yellow
- cancelled — `octicon-stop` (gray), was `octicon-x` (red)
Descriptions also aligned with GitHub:
- "Has started running" → "In progress"
- "Has been cancelled" → "Cancelled after {dur}"
- "Has been skipped" → "Skipped"
Fixes: https://github.com/go-gitea/gitea/issues/32228
---------
Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Signed-off-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.6) <noreply@anthropic.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Nicolas <bircni@icloud.com>
## The issue
Closes#37568. Basically due to empty fields being present in the
actions file, the jobs would be produced as `nil` inside `jobparser.go`
. Because of this when we call `Parse` on the `jobparser` module.
```go
Needs: job.Needs(),
```
would propagate the `nil` job down the chain.
## The fix
For now i decide to fix it by guarding with an `if job == nil` check.
---------
Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Giteabot <teabot@gitea.io>
Migrate to https://gitea.com/gitea/runner/releases/tag/v1.0.0 which
includes the `act` package directory previously referenced by
`nektos/act`.
Signed-off-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Ran [`deadcode`](https://pkg.go.dev/golang.org/x/tools/cmd/deadcode)
(`-test ./...`) to find functions, methods and error types unreachable
from any call path (including tests), and removed the truly-dead ones.
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
`model.ReadWorkflow` succeeds for YAML that is syntactically valid but
fails deeper parsing in `jobparser.Parse` (e.g. blank lines inside `run:
|` blocks cause a SetJob round-trip error). Add
`ValidateWorkflowContent` which runs the full `jobparser.Parse` to catch
these cases, and use it in the file view, the actions workflow list, and
the workflow detection loop so users see the error instead of silently
getting a 500 or a dropped workflow.
Fixes#37115
Signed-off-by: Nicolas <bircni@icloud.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-authored-by: Giteabot <teabot@gitea.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Changes:
- Make `GetActionWorkflow` only convert the target workflow
- In `getActionWorkflowEntry`, use `branchName` instead of resolving the
default branch name from `commit.GetBranchName()`
- Add `ref` to `workflow_run` notify input to avoid the empty `ref`
warning
---------
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Update golangci-lint from v2.11.2 to v2.11.4 and fix new `modernize`
lint warnings:
- Use `strings.Builder` instead of string concatenation in loop
(`evaluator.go`)
- Use `atomic.Int64` instead of `int64` with atomic free functions
(`logchecker.go`, `timer_test.go`, `integration_test.go`)
---
This PR was written with the help of Claude Opus 4.6
Co-authored-by: Claude (Opus 4.6) <noreply@anthropic.com>
- content_encoding contains a slash => v4 artifact
- updated proto files to support mime_type and no longer return errors for upload-artifact v7
- json and txt files are now previewed in browser
- normalized content-disposition header creation
- azure blob storage uploads directly in servedirect mode (no proxying data)
- normalize content-disposition headers based on go mime package
- getting both filename and filename* encoding is done via custom code
Closes#36829
-----
Signed-off-by: ChristopherHX <christopher.homberger@web.de>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
I was testing typos-cli and fixed some misspelled wording here.
All changes are internal — no public API fields, database columns,
locale keys, or migration names are affected.
The jobparser sub package in act is only used by Gitea. Move it to Gitea
to make it more easier to maintain.
---------
Co-authored-by: Christopher Homberger <christopher.homberger@web.de>
Fixes: https://github.com/go-gitea/gitea/issues/36612
This new setting controls which workflow directories are searched. The
default value matches the previous hardcoded behaviour.
This allows users for example to exclude `.github/workflows` from being
picked up by Actions in mirrored repositories by setting `WORKFLOW_DIRS
= .gitea/workflows`.
Signed-off-by: silverwind <me@silverwind.io>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This pull request updates the handling of issue label events in
workflows to distinguish between label additions and deletions,
introduces corresponding test cases, and extends the `IssuePayload`
structure to support this functionality.
### Enhancements to issue label event handling:
* Updated `matchIssuesEvent` in `modules/actions/workflows.go` to
differentiate between "labeled" and "unlabeled" events based on whether
labels were added or removed.
* Added a new field, `RemovedLabels`, to the `IssuePayload` struct in
`modules/structs/hook.go` to track labels that were removed during an
issue event.
### Testing improvements:
* Added `TestMatchIssuesEvent` in `modules/actions/workflows_test.go` to
cover scenarios such as label addition, label deletion, and label
clearing, ensuring the correct event type is triggered.
---------
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
## Summary
Fix skipping of `paths` condition in workflows triggered by tag push
events.
## Details
- Ensure workflows triggered by tag pushes bypass the `paths` filter
check.
- Prevent incorrect skipping of workflows due to `paths` conditions on
tag pushes.
- Added and updated unit tests to verify correct behavior.
This PR adds "View workflow file" to Actions list page, and replaces the
redundant link.
Related #34530
---------
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
* download endpoint has to use 302 redirect
* fake blob download used if direct download not possible
* downloading v3 artifacts not possible
New repo apis based on GitHub Rest V3
- GET /runs/{run}/artifacts (Cannot use run index of url due to not
being unique)
- GET /artifacts
- GET + DELETE /artifacts/{artifact_id}
- GET /artifacts/{artifact_id}/zip
- (GET /artifacts/{artifact_id}/zip/raw this is a workaround for a http
302 assertion in actions/toolkit)
- api docs removed this is protected by a signed url like the internal
artifacts api and no longer usable with any token or swagger
- returns http 401 if the signature is invalid
- or change the artifact id
- or expired after 1 hour
Closes#33353Closes#32124
---------
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Support compression for Actions logs to save storage space and
bandwidth. Inspired by
https://github.com/go-gitea/gitea/issues/24256#issuecomment-1521153015
The biggest challenge is that the compression format should support
[seekable](https://github.com/facebook/zstd/blob/dev/contrib/seekable_format/zstd_seekable_compression_format.md).
So when users are viewing a part of the log lines, Gitea doesn't need to
download the whole compressed file and decompress it.
That means gzip cannot help here. And I did research, there aren't too
many choices, like bgzip and xz, but I think zstd is the most popular
one. It has an implementation in Golang with
[zstd](https://github.com/klauspost/compress/tree/master/zstd) and
[zstd-seekable-format-go](https://github.com/SaveTheRbtz/zstd-seekable-format-go),
and what is better is that it has good compatibility: a seekable format
zstd file can be read by a regular zstd reader.
This PR introduces a new package `zstd` to combine and wrap the two
packages, to provide a unified and easy-to-use API.
And a new setting `LOG_COMPRESSION` is added to the config, although I
don't see any reason why not to use compression, I think's it's a good
idea to keep the default with `none` to be consistent with old versions.
`LOG_COMPRESSION` takes effect for only new log files, it adds `.zst` as
an extension to the file name, so Gitea can determine if it needs
decompression according to the file name when reading. Old files will
keep the format since it's not worth converting them, as they will be
cleared after #31735.
<img width="541" alt="image"
src="https://github.com/user-attachments/assets/e9598764-a4e0-4b68-8c2b-f769265183c9">