enhance(actions): improve reusable workflow uses handling and cancellation (#37991)

Follow up #37478

## Changes

1. #37478 doesn't support absolute URL in `uses`. This PR provides
partial support for URL-style reusable workflow references. A reusable
workflow can now be referenced by an absolute URL, as long as it points
to the local Gitea instance:

```yaml
jobs:
  call:
    uses: https://your-gitea.example.com/OWNER/REPO/.gitea/workflows/ci.yaml@v1
```

2. Show an error message in the UI for invalid `uses`.

<img width="1600" alt="image"
src="https://github.com/user-attachments/assets/21b34e61-bf10-4af1-b9fd-4ee4e9fde049"
/>

3. Fix reusable caller cancellation issue. A reusable caller's status is
aggregated from its children, so cancellation should processes a
caller's descendants deepest-first.

---------

Signed-off-by: Zettat123 <zettat123@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: bircni <bircni@icloud.com>
Co-authored-by: Giteabot <teabot@gitea.io>
This commit is contained in:
Zettat123
2026-06-08 00:39:06 -06:00
committed by GitHub
parent 1e9ea9c8f5
commit 60f66a9bfd
6 changed files with 179 additions and 24 deletions
@@ -10,6 +10,9 @@ import (
actions_model "gitea.dev/models/actions"
"gitea.dev/models/db"
"gitea.dev/models/unittest"
"gitea.dev/modules/actions/jobparser"
"gitea.dev/modules/setting"
"gitea.dev/modules/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -132,3 +135,44 @@ func buildCallerChain(t *testing.T, callerUses ...string) []*actions_model.Actio
}
return jobs
}
func TestResolveUses(t *testing.T) {
defer test.MockVariableValue(&setting.AppURL, "https://gitea.example.com/sub/")()
defer test.MockVariableValue(&setting.AppSubURL, "/sub")()
ctx := t.Context()
t.Run("LocalForms", func(t *testing.T) {
// Same-repo and cross-repo forms are not URLs and are parsed as-is.
ref, err := ResolveUses(ctx, "./.gitea/workflows/build.yml")
require.NoError(t, err)
assert.Equal(t, jobparser.UsesRef{Kind: jobparser.UsesKindLocalSameRepo, Path: ".gitea/workflows/build.yml"}, *ref)
ref, err = ResolveUses(ctx, "owner/repo/.gitea/workflows/build.yml@v1")
require.NoError(t, err)
assert.Equal(t, jobparser.UsesRef{Kind: jobparser.UsesKindLocalCrossRepo, Owner: "owner", Repo: "repo", Path: ".gitea/workflows/build.yml", Ref: "v1"}, *ref)
})
t.Run("LocalInstanceURL", func(t *testing.T) {
// An absolute URL on this instance (incl. AppSubURL) resolves to the equivalent cross-repo ref.
ref, err := ResolveUses(ctx, "https://gitea.example.com/sub/owner/repo/.gitea/workflows/ci.yml@refs/heads/main")
require.NoError(t, err)
assert.Equal(t, jobparser.UsesRef{Kind: jobparser.UsesKindLocalCrossRepo, Owner: "owner", Repo: "repo", Path: ".gitea/workflows/ci.yml", Ref: "refs/heads/main"}, *ref)
})
t.Run("InvalidSyntax", func(t *testing.T) {
for _, in := range []string{
"owner/.gitea/workflows/foo.yml", // missing repo segment
"owner/repo/.gitea/workflows/foo.yml", // missing @ref
"https://gitea.example.com/sub/repo/.gitea/workflows/ci.yml@refs/heads/main", // local absolute URL but missing owner
"not a valid uses at all",
} {
_, err := ResolveUses(ctx, in)
require.Error(t, err, "in = %s", in)
}
})
t.Run("ForeignURL", func(t *testing.T) {
_, err := ResolveUses(ctx, "https://other.gitea-example.com/owner/repo/.gitea/workflows/ci.yaml@v1")
assert.ErrorContains(t, err, "must point to this Gitea instance")
})
}