Add AI-friendly enhancements: runner capabilities, release archive, action compatibility
Some checks are pending
Build and Release / Lint and Test (push) Waiting to run
Build and Release / Build Binaries (amd64, darwin) (push) Blocked by required conditions
Build and Release / Build Binaries (amd64, linux) (push) Blocked by required conditions
Build and Release / Build Binaries (amd64, windows) (push) Blocked by required conditions
Build and Release / Build Binaries (arm64, darwin) (push) Blocked by required conditions
Build and Release / Build Binaries (arm64, linux) (push) Blocked by required conditions
Some checks are pending
Build and Release / Lint and Test (push) Waiting to run
Build and Release / Build Binaries (amd64, darwin) (push) Blocked by required conditions
Build and Release / Build Binaries (amd64, linux) (push) Blocked by required conditions
Build and Release / Build Binaries (amd64, windows) (push) Blocked by required conditions
Build and Release / Build Binaries (arm64, darwin) (push) Blocked by required conditions
Build and Release / Build Binaries (arm64, linux) (push) Blocked by required conditions
- Add runner capability discovery API (v2) for AI tools to query before writing workflows - Add release archive feature with filter toggle UI - Add GitHub Actions compatibility layer with action aliasing - Store runner capabilities JSON from act_runner Declare calls - Add migrations for release archive and runner capabilities fields 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
1ad0368230
commit
fbd5da0acb
3
go.mod
3
go.mod
@ -312,6 +312,9 @@ replace github.com/nektos/act => gitea.com/gitea/act v0.261.7-0.20251003180512-a
|
|||||||
|
|
||||||
replace git.sr.ht/~mariusor/go-xsd-duration => gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078
|
replace git.sr.ht/~mariusor/go-xsd-duration => gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078
|
||||||
|
|
||||||
|
// Use GitCaddy fork with capability support
|
||||||
|
replace code.gitea.io/actions-proto-go => git.marketally.com/gitcaddy/actions-proto-go v0.5.0
|
||||||
|
|
||||||
exclude github.com/gofrs/uuid v3.2.0+incompatible
|
exclude github.com/gofrs/uuid v3.2.0+incompatible
|
||||||
|
|
||||||
exclude github.com/gofrs/uuid v4.0.0+incompatible
|
exclude github.com/gofrs/uuid v4.0.0+incompatible
|
||||||
|
|||||||
4
go.sum
4
go.sum
@ -16,8 +16,6 @@ cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2k
|
|||||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||||
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=
|
|
||||||
code.gitea.io/gitea-vet v0.2.3 h1:gdFmm6WOTM65rE8FUBTRzeQZYzXePKSSB1+r574hWwI=
|
code.gitea.io/gitea-vet v0.2.3 h1:gdFmm6WOTM65rE8FUBTRzeQZYzXePKSSB1+r574hWwI=
|
||||||
code.gitea.io/gitea-vet v0.2.3/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE=
|
code.gitea.io/gitea-vet v0.2.3/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE=
|
||||||
code.gitea.io/sdk/gitea v0.22.0 h1:HCKq7bX/HQ85Nw7c/HAhWgRye+vBp5nQOE8Md1+9Ef0=
|
code.gitea.io/sdk/gitea v0.22.0 h1:HCKq7bX/HQ85Nw7c/HAhWgRye+vBp5nQOE8Md1+9Ef0=
|
||||||
@ -31,6 +29,8 @@ dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
|||||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
|
git.marketally.com/gitcaddy/actions-proto-go v0.5.0 h1:D2loMnqTXiaJL6TMfOOUJz4/3Vpv0AnMDSJVuiqMNrM=
|
||||||
|
git.marketally.com/gitcaddy/actions-proto-go v0.5.0/go.mod h1:li5RzZsj1sV8a0SXzXWsGNwv0dYw7Wj829AgloZqF5o=
|
||||||
gitea.com/gitea/act v0.261.7-0.20251003180512-ac6e4b751763 h1:ohdxegvslDEllZmRNDqpKun6L4Oq81jNdEDtGgHEV2c=
|
gitea.com/gitea/act v0.261.7-0.20251003180512-ac6e4b751763 h1:ohdxegvslDEllZmRNDqpKun6L4Oq81jNdEDtGgHEV2c=
|
||||||
gitea.com/gitea/act v0.261.7-0.20251003180512-ac6e4b751763/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok=
|
gitea.com/gitea/act v0.261.7-0.20251003180512-ac6e4b751763/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok=
|
||||||
gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:BAFmdZpRW7zMQZQDClaCWobRj9uL1MR3MzpCVJvc5s4=
|
gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:BAFmdZpRW7zMQZQDClaCWobRj9uL1MR3MzpCVJvc5s4=
|
||||||
|
|||||||
@ -62,6 +62,8 @@ type ActionRunner struct {
|
|||||||
AgentLabels []string `xorm:"TEXT"`
|
AgentLabels []string `xorm:"TEXT"`
|
||||||
// Store if this is a runner that only ever get one single job assigned
|
// Store if this is a runner that only ever get one single job assigned
|
||||||
Ephemeral bool `xorm:"ephemeral NOT NULL DEFAULT false"`
|
Ephemeral bool `xorm:"ephemeral NOT NULL DEFAULT false"`
|
||||||
|
// CapabilitiesJSON stores structured capability information for AI consumption
|
||||||
|
CapabilitiesJSON string `xorm:"TEXT"`
|
||||||
|
|
||||||
Created timeutil.TimeStamp `xorm:"created"`
|
Created timeutil.TimeStamp `xorm:"created"`
|
||||||
Updated timeutil.TimeStamp `xorm:"updated"`
|
Updated timeutil.TimeStamp `xorm:"updated"`
|
||||||
@ -394,3 +396,16 @@ func UpdateWrongRepoLevelRunners(ctx context.Context) (int64, error) {
|
|||||||
}
|
}
|
||||||
return result.RowsAffected()
|
return result.RowsAffected()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetRunnersOfRepo returns all runners available for a repository
|
||||||
|
// This includes repo-level, owner-level, and global runners
|
||||||
|
func GetRunnersOfRepo(ctx context.Context, repoID int64) ([]*ActionRunner, error) {
|
||||||
|
opts := FindRunnerOptions{
|
||||||
|
RepoID: repoID,
|
||||||
|
WithAvailable: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
var runners []*ActionRunner
|
||||||
|
err := db.GetEngine(ctx).Where(opts.ToConds()).OrderBy(opts.ToOrders()).Find(&runners)
|
||||||
|
return runners, err
|
||||||
|
}
|
||||||
|
|||||||
@ -403,6 +403,8 @@ func prepareMigrationTasks() []*migration {
|
|||||||
newMigration(326, "Add organization pinned repos tables", v1_26.AddOrgPinnedTables),
|
newMigration(326, "Add organization pinned repos tables", v1_26.AddOrgPinnedTables),
|
||||||
newMigration(327, "Add Gitea Pages tables", v1_26.AddGiteaPagesTables),
|
newMigration(327, "Add Gitea Pages tables", v1_26.AddGiteaPagesTables),
|
||||||
newMigration(328, "Add wiki index table for search", v1_26.AddWikiIndexTable),
|
newMigration(328, "Add wiki index table for search", v1_26.AddWikiIndexTable),
|
||||||
|
newMigration(329, "Add release archive columns", v1_26.AddReleaseArchiveColumns),
|
||||||
|
newMigration(330, "Add runner capabilities column", v1_26.AddRunnerCapabilitiesColumn),
|
||||||
}
|
}
|
||||||
return preparedMigrations
|
return preparedMigrations
|
||||||
}
|
}
|
||||||
|
|||||||
20
models/migrations/v1_26/v329.go
Normal file
20
models/migrations/v1_26/v329.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package v1_26
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
|
||||||
|
"xorm.io/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddReleaseArchiveColumns adds IsArchived and ArchivedUnix columns to the release table
|
||||||
|
func AddReleaseArchiveColumns(x *xorm.Engine) error {
|
||||||
|
type Release struct {
|
||||||
|
IsArchived bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
|
ArchivedUnix timeutil.TimeStamp `xorm:"INDEX"`
|
||||||
|
}
|
||||||
|
|
||||||
|
return x.Sync(new(Release))
|
||||||
|
}
|
||||||
17
models/migrations/v1_26/v330.go
Normal file
17
models/migrations/v1_26/v330.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package v1_26
|
||||||
|
|
||||||
|
import (
|
||||||
|
"xorm.io/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddRunnerCapabilitiesColumn adds CapabilitiesJSON column to action_runner table
|
||||||
|
func AddRunnerCapabilitiesColumn(x *xorm.Engine) error {
|
||||||
|
type ActionRunner struct {
|
||||||
|
CapabilitiesJSON string `xorm:"TEXT"`
|
||||||
|
}
|
||||||
|
|
||||||
|
return x.Sync(new(ActionRunner))
|
||||||
|
}
|
||||||
@ -85,6 +85,8 @@ type Release struct {
|
|||||||
IsDraft bool `xorm:"NOT NULL DEFAULT false"`
|
IsDraft bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
IsPrerelease bool `xorm:"NOT NULL DEFAULT false"`
|
IsPrerelease bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
IsTag bool `xorm:"NOT NULL DEFAULT false"` // will be true only if the record is a tag and has no related releases
|
IsTag bool `xorm:"NOT NULL DEFAULT false"` // will be true only if the record is a tag and has no related releases
|
||||||
|
IsArchived bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
|
ArchivedUnix timeutil.TimeStamp `xorm:"INDEX"`
|
||||||
Attachments []*Attachment `xorm:"-"`
|
Attachments []*Attachment `xorm:"-"`
|
||||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX"`
|
CreatedUnix timeutil.TimeStamp `xorm:"INDEX"`
|
||||||
}
|
}
|
||||||
@ -233,14 +235,16 @@ func GetReleaseForRepoByID(ctx context.Context, repoID, id int64) (*Release, err
|
|||||||
// FindReleasesOptions describes the conditions to Find releases
|
// FindReleasesOptions describes the conditions to Find releases
|
||||||
type FindReleasesOptions struct {
|
type FindReleasesOptions struct {
|
||||||
db.ListOptions
|
db.ListOptions
|
||||||
RepoID int64
|
RepoID int64
|
||||||
IncludeDrafts bool
|
IncludeDrafts bool
|
||||||
IncludeTags bool
|
IncludeTags bool
|
||||||
IsPreRelease optional.Option[bool]
|
IncludeArchived bool
|
||||||
IsDraft optional.Option[bool]
|
IsPreRelease optional.Option[bool]
|
||||||
TagNames []string
|
IsDraft optional.Option[bool]
|
||||||
HasSha1 optional.Option[bool] // useful to find draft releases which are created with existing tags
|
IsArchived optional.Option[bool]
|
||||||
NamePattern optional.Option[string]
|
TagNames []string
|
||||||
|
HasSha1 optional.Option[bool] // useful to find draft releases which are created with existing tags
|
||||||
|
NamePattern optional.Option[string]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opts FindReleasesOptions) ToConds() builder.Cond {
|
func (opts FindReleasesOptions) ToConds() builder.Cond {
|
||||||
@ -252,6 +256,9 @@ func (opts FindReleasesOptions) ToConds() builder.Cond {
|
|||||||
if !opts.IncludeTags {
|
if !opts.IncludeTags {
|
||||||
cond = cond.And(builder.Eq{"is_tag": false})
|
cond = cond.And(builder.Eq{"is_tag": false})
|
||||||
}
|
}
|
||||||
|
if !opts.IncludeArchived {
|
||||||
|
cond = cond.And(builder.Eq{"is_archived": false})
|
||||||
|
}
|
||||||
if len(opts.TagNames) > 0 {
|
if len(opts.TagNames) > 0 {
|
||||||
cond = cond.And(builder.In("tag_name", opts.TagNames))
|
cond = cond.And(builder.In("tag_name", opts.TagNames))
|
||||||
}
|
}
|
||||||
@ -261,6 +268,9 @@ func (opts FindReleasesOptions) ToConds() builder.Cond {
|
|||||||
if opts.IsDraft.Has() {
|
if opts.IsDraft.Has() {
|
||||||
cond = cond.And(builder.Eq{"is_draft": opts.IsDraft.Value()})
|
cond = cond.And(builder.Eq{"is_draft": opts.IsDraft.Value()})
|
||||||
}
|
}
|
||||||
|
if opts.IsArchived.Has() {
|
||||||
|
cond = cond.And(builder.Eq{"is_archived": opts.IsArchived.Value()})
|
||||||
|
}
|
||||||
if opts.HasSha1.Has() {
|
if opts.HasSha1.Has() {
|
||||||
if opts.HasSha1.Value() {
|
if opts.HasSha1.Value() {
|
||||||
cond = cond.And(builder.Neq{"sha1": ""})
|
cond = cond.And(builder.Neq{"sha1": ""})
|
||||||
|
|||||||
137
modules/actions/compatibility.go
Normal file
137
modules/actions/compatibility.go
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package actions
|
||||||
|
|
||||||
|
// CompatibilityStatus represents the support status of an action
|
||||||
|
type CompatibilityStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
CompatibilityFull CompatibilityStatus = "full"
|
||||||
|
CompatibilityPartial CompatibilityStatus = "partial"
|
||||||
|
CompatibilityNone CompatibilityStatus = "none"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ActionCompatibility represents compatibility information for a GitHub action
|
||||||
|
type ActionCompatibility struct {
|
||||||
|
// Action name (e.g., "actions/checkout")
|
||||||
|
Name string
|
||||||
|
// Supported versions
|
||||||
|
Versions []string
|
||||||
|
// Compatibility status
|
||||||
|
Status CompatibilityStatus
|
||||||
|
// Notes about compatibility
|
||||||
|
Notes string
|
||||||
|
// Suggested alternative if not fully compatible
|
||||||
|
Alternative string
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuiltinCompatibility contains known action compatibility information for Gitea Actions
|
||||||
|
// This is used by the capability discovery API to help AI tools write correct workflows
|
||||||
|
var BuiltinCompatibility = map[string]*ActionCompatibility{
|
||||||
|
"actions/checkout": {
|
||||||
|
Name: "actions/checkout",
|
||||||
|
Versions: []string{"v2", "v3", "v4"},
|
||||||
|
Status: CompatibilityFull,
|
||||||
|
Notes: "Fully compatible with Gitea Actions",
|
||||||
|
},
|
||||||
|
"actions/setup-node": {
|
||||||
|
Name: "actions/setup-node",
|
||||||
|
Versions: []string{"v2", "v3", "v4"},
|
||||||
|
Status: CompatibilityFull,
|
||||||
|
Notes: "Fully compatible with Gitea Actions",
|
||||||
|
},
|
||||||
|
"actions/setup-go": {
|
||||||
|
Name: "actions/setup-go",
|
||||||
|
Versions: []string{"v3", "v4", "v5"},
|
||||||
|
Status: CompatibilityFull,
|
||||||
|
Notes: "Fully compatible with Gitea Actions",
|
||||||
|
},
|
||||||
|
"actions/setup-python": {
|
||||||
|
Name: "actions/setup-python",
|
||||||
|
Versions: []string{"v4", "v5"},
|
||||||
|
Status: CompatibilityFull,
|
||||||
|
Notes: "Fully compatible with Gitea Actions",
|
||||||
|
},
|
||||||
|
"actions/setup-java": {
|
||||||
|
Name: "actions/setup-java",
|
||||||
|
Versions: []string{"v3", "v4"},
|
||||||
|
Status: CompatibilityFull,
|
||||||
|
Notes: "Fully compatible with Gitea Actions",
|
||||||
|
},
|
||||||
|
"actions/setup-dotnet": {
|
||||||
|
Name: "actions/setup-dotnet",
|
||||||
|
Versions: []string{"v3", "v4"},
|
||||||
|
Status: CompatibilityFull,
|
||||||
|
Notes: "Fully compatible with Gitea Actions",
|
||||||
|
},
|
||||||
|
"actions/upload-artifact": {
|
||||||
|
Name: "actions/upload-artifact",
|
||||||
|
Versions: []string{"v2", "v3"},
|
||||||
|
Status: CompatibilityPartial,
|
||||||
|
Notes: "v4 not supported on GHES-compatible runners. Use v3 or Gitea API for artifact upload.",
|
||||||
|
Alternative: "actions/upload-artifact@v3 or direct Gitea API upload",
|
||||||
|
},
|
||||||
|
"actions/download-artifact": {
|
||||||
|
Name: "actions/download-artifact",
|
||||||
|
Versions: []string{"v2", "v3"},
|
||||||
|
Status: CompatibilityPartial,
|
||||||
|
Notes: "v4 not supported on GHES-compatible runners. Use v3.",
|
||||||
|
Alternative: "actions/download-artifact@v3",
|
||||||
|
},
|
||||||
|
"actions/cache": {
|
||||||
|
Name: "actions/cache",
|
||||||
|
Versions: []string{"v3", "v4"},
|
||||||
|
Status: CompatibilityFull,
|
||||||
|
Notes: "Fully compatible with Gitea Actions",
|
||||||
|
},
|
||||||
|
"actions/github-script": {
|
||||||
|
Name: "actions/github-script",
|
||||||
|
Versions: []string{"v6", "v7"},
|
||||||
|
Status: CompatibilityPartial,
|
||||||
|
Notes: "GitHub API calls may not work. Use for basic scripting only.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsupportedFeatures lists features that are not supported in Gitea Actions
|
||||||
|
var UnsupportedFeatures = []string{
|
||||||
|
"GitHub-hosted runners",
|
||||||
|
"Environments with protection rules",
|
||||||
|
"OIDC token authentication",
|
||||||
|
"Required workflows",
|
||||||
|
"Deployment branches",
|
||||||
|
"Reusable workflows from other repositories (limited)",
|
||||||
|
"actions/upload-artifact@v4",
|
||||||
|
"actions/download-artifact@v4",
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncompatibleActions maps action@version to error messages
|
||||||
|
var IncompatibleActions = map[string]string{
|
||||||
|
"actions/upload-artifact@v4": "v4 not supported on Gitea/GHES-compatible runners. Use actions/upload-artifact@v3 or direct Gitea API upload.",
|
||||||
|
"actions/download-artifact@v4": "v4 not supported on Gitea/GHES-compatible runners. Use actions/download-artifact@v3.",
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCompatibility returns compatibility information for an action
|
||||||
|
func GetCompatibility(actionName string) *ActionCompatibility {
|
||||||
|
return BuiltinCompatibility[actionName]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIncompatibilityMessage returns an error message if the action@version is incompatible
|
||||||
|
func GetIncompatibilityMessage(actionWithVersion string) string {
|
||||||
|
return IncompatibleActions[actionWithVersion]
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsActionCompatible checks if an action@version is compatible with Gitea Actions
|
||||||
|
func IsActionCompatible(actionWithVersion string) bool {
|
||||||
|
_, incompatible := IncompatibleActions[actionWithVersion]
|
||||||
|
return !incompatible
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSuggestedAlternative returns a suggested alternative for an incompatible action
|
||||||
|
func GetSuggestedAlternative(actionWithVersion string) string {
|
||||||
|
alternatives := map[string]string{
|
||||||
|
"actions/upload-artifact@v4": "uses: actions/upload-artifact@v3",
|
||||||
|
"actions/download-artifact@v4": "uses: actions/download-artifact@v3",
|
||||||
|
}
|
||||||
|
return alternatives[actionWithVersion]
|
||||||
|
}
|
||||||
93
modules/structs/actions_capabilities.go
Normal file
93
modules/structs/actions_capabilities.go
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package structs
|
||||||
|
|
||||||
|
// RunnerCapability represents the detailed capabilities of a runner
|
||||||
|
type RunnerCapability struct {
|
||||||
|
OS string `json:"os"`
|
||||||
|
Arch string `json:"arch"`
|
||||||
|
Docker bool `json:"docker"`
|
||||||
|
DockerCompose bool `json:"docker_compose"`
|
||||||
|
ContainerRuntime string `json:"container_runtime,omitempty"`
|
||||||
|
Shell []string `json:"shell,omitempty"`
|
||||||
|
Tools map[string][]string `json:"tools,omitempty"`
|
||||||
|
Features *CapabilityFeatures `json:"features,omitempty"`
|
||||||
|
Limitations []string `json:"limitations,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CapabilityFeatures represents feature support flags
|
||||||
|
type CapabilityFeatures struct {
|
||||||
|
ArtifactsV4 bool `json:"artifacts_v4"`
|
||||||
|
Cache bool `json:"cache"`
|
||||||
|
Services bool `json:"services"`
|
||||||
|
CompositeActions bool `json:"composite_actions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActionSupport represents version support for an action
|
||||||
|
type ActionSupport struct {
|
||||||
|
Versions []string `json:"versions"`
|
||||||
|
Notes string `json:"notes,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlatformInfo represents Gitea platform capabilities
|
||||||
|
type PlatformInfo struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
ActionsVersion string `json:"actions_version,omitempty"`
|
||||||
|
DefaultActionsURL string `json:"default_actions_url"`
|
||||||
|
SupportedActions map[string]ActionSupport `json:"supported_actions,omitempty"`
|
||||||
|
UnsupportedFeatures []string `json:"unsupported_features,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WorkflowHints provides hints for AI workflow generation
|
||||||
|
type WorkflowHints struct {
|
||||||
|
PreferredCheckout string `json:"preferred_checkout,omitempty"`
|
||||||
|
ArtifactUploadAlternative string `json:"artifact_upload_alternative,omitempty"`
|
||||||
|
SecretAccess string `json:"secret_access,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunnerWithCapabilities represents a runner with its capabilities for API response
|
||||||
|
type RunnerWithCapabilities struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Labels []string `json:"labels"`
|
||||||
|
Capabilities *RunnerCapability `json:"capabilities,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActionsCapabilitiesResponse is the response for the capabilities endpoint
|
||||||
|
type ActionsCapabilitiesResponse struct {
|
||||||
|
Runners []*RunnerWithCapabilities `json:"runners"`
|
||||||
|
Platform *PlatformInfo `json:"platform"`
|
||||||
|
WorkflowHints *WorkflowHints `json:"workflow_hints,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WorkflowValidationRequest is the request for workflow validation
|
||||||
|
type WorkflowValidationRequest struct {
|
||||||
|
Content string `json:"content" binding:"Required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WorkflowValidationWarning represents a validation warning
|
||||||
|
type WorkflowValidationWarning struct {
|
||||||
|
Line int `json:"line,omitempty"`
|
||||||
|
Action string `json:"action,omitempty"`
|
||||||
|
Severity string `json:"severity"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Suggestion string `json:"suggestion,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunnerMatch represents job-to-runner matching result
|
||||||
|
type RunnerMatch struct {
|
||||||
|
Job string `json:"job"`
|
||||||
|
RunsOn []string `json:"runs_on"`
|
||||||
|
MatchedRunners []string `json:"matched_runners,omitempty"`
|
||||||
|
CapabilitiesMet bool `json:"capabilities_met"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WorkflowValidationResponse is the response for workflow validation
|
||||||
|
type WorkflowValidationResponse struct {
|
||||||
|
Valid bool `json:"valid"`
|
||||||
|
Warnings []*WorkflowValidationWarning `json:"warnings,omitempty"`
|
||||||
|
RunnerMatch []*RunnerMatch `json:"runner_match,omitempty"`
|
||||||
|
}
|
||||||
@ -41,6 +41,10 @@ type Release struct {
|
|||||||
Publisher *User `json:"author"`
|
Publisher *User `json:"author"`
|
||||||
// The files attached to the release
|
// The files attached to the release
|
||||||
Attachments []*Attachment `json:"assets"`
|
Attachments []*Attachment `json:"assets"`
|
||||||
|
// Whether the release is archived
|
||||||
|
IsArchived bool `json:"archived"`
|
||||||
|
// swagger:strfmt date-time
|
||||||
|
ArchivedAt *time.Time `json:"archived_at,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateReleaseOption options when creating a release
|
// CreateReleaseOption options when creating a release
|
||||||
|
|||||||
@ -2613,6 +2613,10 @@
|
|||||||
"repo.release.prerelease": "Pre-Release",
|
"repo.release.prerelease": "Pre-Release",
|
||||||
"repo.release.stable": "Stable",
|
"repo.release.stable": "Stable",
|
||||||
"repo.release.latest": "Latest",
|
"repo.release.latest": "Latest",
|
||||||
|
"repo.release.archived": "Archived",
|
||||||
|
"repo.release.archive": "Archive",
|
||||||
|
"repo.release.unarchive": "Unarchive",
|
||||||
|
"repo.release.show_archived": "Show archived releases",
|
||||||
"repo.release.compare": "Compare",
|
"repo.release.compare": "Compare",
|
||||||
"repo.release.edit": "edit",
|
"repo.release.edit": "edit",
|
||||||
"repo.release.ahead.commits": "<strong>%d</strong> commits",
|
"repo.release.ahead.commits": "<strong>%d</strong> commits",
|
||||||
|
|||||||
@ -117,7 +117,8 @@ func (s *Service) Declare(
|
|||||||
runner := GetRunner(ctx)
|
runner := GetRunner(ctx)
|
||||||
runner.AgentLabels = req.Msg.Labels
|
runner.AgentLabels = req.Msg.Labels
|
||||||
runner.Version = req.Msg.Version
|
runner.Version = req.Msg.Version
|
||||||
if err := actions_model.UpdateRunner(ctx, runner, "agent_labels", "version"); err != nil {
|
runner.CapabilitiesJSON = req.Msg.CapabilitiesJson
|
||||||
|
if err := actions_model.UpdateRunner(ctx, runner, "agent_labels", "version", "capabilities_json"); err != nil {
|
||||||
return nil, status.Errorf(codes.Internal, "update runner: %v", err)
|
return nil, status.Errorf(codes.Internal, "update runner: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1327,6 +1327,9 @@ func Routes() *web.Router {
|
|||||||
m.Combo("").Get(repo.GetRelease).
|
m.Combo("").Get(repo.GetRelease).
|
||||||
Patch(reqToken(), reqRepoWriter(unit.TypeReleases), context.ReferencesGitRepo(), bind(api.EditReleaseOption{}), repo.EditRelease).
|
Patch(reqToken(), reqRepoWriter(unit.TypeReleases), context.ReferencesGitRepo(), bind(api.EditReleaseOption{}), repo.EditRelease).
|
||||||
Delete(reqToken(), reqRepoWriter(unit.TypeReleases), repo.DeleteRelease)
|
Delete(reqToken(), reqRepoWriter(unit.TypeReleases), repo.DeleteRelease)
|
||||||
|
m.Combo("/archive").
|
||||||
|
Post(reqToken(), reqRepoWriter(unit.TypeReleases), repo.ArchiveRelease).
|
||||||
|
Delete(reqToken(), reqRepoWriter(unit.TypeReleases), repo.UnarchiveRelease)
|
||||||
m.Group("/assets", func() {
|
m.Group("/assets", func() {
|
||||||
m.Combo("").Get(repo.ListReleaseAttachments).
|
m.Combo("").Get(repo.ListReleaseAttachments).
|
||||||
Post(reqToken(), reqRepoWriter(unit.TypeReleases), repo.CreateReleaseAttachment)
|
Post(reqToken(), reqRepoWriter(unit.TypeReleases), repo.CreateReleaseAttachment)
|
||||||
|
|||||||
@ -136,6 +136,10 @@ func ListReleases(ctx *context.APIContext) {
|
|||||||
// in: query
|
// in: query
|
||||||
// description: filter (exclude / include) pre-releases
|
// description: filter (exclude / include) pre-releases
|
||||||
// type: boolean
|
// type: boolean
|
||||||
|
// - name: archived
|
||||||
|
// in: query
|
||||||
|
// description: filter archived releases (true=only archived, false=exclude archived, omit=all)
|
||||||
|
// type: boolean
|
||||||
// - name: page
|
// - name: page
|
||||||
// in: query
|
// in: query
|
||||||
// description: page number of results to return (1-based)
|
// description: page number of results to return (1-based)
|
||||||
@ -151,13 +155,21 @@ func ListReleases(ctx *context.APIContext) {
|
|||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
listOptions := utils.GetListOptions(ctx)
|
listOptions := utils.GetListOptions(ctx)
|
||||||
|
|
||||||
|
// By default, exclude archived releases unless explicitly requested
|
||||||
|
includeArchived := false
|
||||||
|
if ctx.FormOptionalBool("archived").Has() {
|
||||||
|
includeArchived = true
|
||||||
|
}
|
||||||
|
|
||||||
opts := repo_model.FindReleasesOptions{
|
opts := repo_model.FindReleasesOptions{
|
||||||
ListOptions: listOptions,
|
ListOptions: listOptions,
|
||||||
IncludeDrafts: ctx.Repo.AccessMode >= perm.AccessModeWrite || ctx.Repo.UnitAccessMode(unit.TypeReleases) >= perm.AccessModeWrite,
|
IncludeDrafts: ctx.Repo.AccessMode >= perm.AccessModeWrite || ctx.Repo.UnitAccessMode(unit.TypeReleases) >= perm.AccessModeWrite,
|
||||||
IncludeTags: false,
|
IncludeTags: false,
|
||||||
IsDraft: ctx.FormOptionalBool("draft"),
|
IncludeArchived: includeArchived,
|
||||||
IsPreRelease: ctx.FormOptionalBool("pre-release"),
|
IsDraft: ctx.FormOptionalBool("draft"),
|
||||||
RepoID: ctx.Repo.Repository.ID,
|
IsPreRelease: ctx.FormOptionalBool("pre-release"),
|
||||||
|
IsArchived: ctx.FormOptionalBool("archived"),
|
||||||
|
RepoID: ctx.Repo.Repository.ID,
|
||||||
}
|
}
|
||||||
|
|
||||||
releases, err := db.Find[repo_model.Release](ctx, opts)
|
releases, err := db.Find[repo_model.Release](ctx, opts)
|
||||||
@ -419,3 +431,109 @@ func DeleteRelease(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
ctx.Status(http.StatusNoContent)
|
ctx.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ArchiveRelease archives a release
|
||||||
|
func ArchiveRelease(ctx *context.APIContext) {
|
||||||
|
// swagger:operation POST /repos/{owner}/{repo}/releases/{id}/archive repository repoArchiveRelease
|
||||||
|
// ---
|
||||||
|
// summary: Archive a release
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: owner
|
||||||
|
// in: path
|
||||||
|
// description: owner of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: repo
|
||||||
|
// in: path
|
||||||
|
// description: name of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: id
|
||||||
|
// in: path
|
||||||
|
// description: id of the release to archive
|
||||||
|
// type: integer
|
||||||
|
// format: int64
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/Release"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
|
id := ctx.PathParamInt64("id")
|
||||||
|
release, err := repo_model.GetReleaseForRepoByID(ctx, ctx.Repo.Repository.ID, id)
|
||||||
|
if err != nil && !repo_model.IsErrReleaseNotExist(err) {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil && repo_model.IsErrReleaseNotExist(err) || release.IsTag {
|
||||||
|
ctx.APIErrorNotFound()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := release_service.ArchiveRelease(ctx, release); err != nil {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := release.LoadAttributes(ctx); err != nil {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.JSON(http.StatusOK, convert.ToAPIRelease(ctx, ctx.Repo.Repository, release))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnarchiveRelease unarchives a release
|
||||||
|
func UnarchiveRelease(ctx *context.APIContext) {
|
||||||
|
// swagger:operation DELETE /repos/{owner}/{repo}/releases/{id}/archive repository repoUnarchiveRelease
|
||||||
|
// ---
|
||||||
|
// summary: Unarchive a release
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: owner
|
||||||
|
// in: path
|
||||||
|
// description: owner of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: repo
|
||||||
|
// in: path
|
||||||
|
// description: name of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: id
|
||||||
|
// in: path
|
||||||
|
// description: id of the release to unarchive
|
||||||
|
// type: integer
|
||||||
|
// format: int64
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/Release"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
|
id := ctx.PathParamInt64("id")
|
||||||
|
release, err := repo_model.GetReleaseForRepoByID(ctx, ctx.Repo.Repository.ID, id)
|
||||||
|
if err != nil && !repo_model.IsErrReleaseNotExist(err) {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil && repo_model.IsErrReleaseNotExist(err) || release.IsTag {
|
||||||
|
ctx.APIErrorNotFound()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := release_service.UnarchiveRelease(ctx, release); err != nil {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := release.LoadAttributes(ctx); err != nil {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.JSON(http.StatusOK, convert.ToAPIRelease(ctx, ctx.Repo.Repository, release))
|
||||||
|
}
|
||||||
|
|||||||
191
routers/api/v2/actions.go
Normal file
191
routers/api/v2/actions.go
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/actions"
|
||||||
|
actions_model "code.gitea.io/gitea/models/actions"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
|
"code.gitea.io/gitea/modules/web"
|
||||||
|
"code.gitea.io/gitea/services/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// getSupportedActions converts the compatibility module data to API format
|
||||||
|
func getSupportedActions() map[string]api.ActionSupport {
|
||||||
|
result := make(map[string]api.ActionSupport)
|
||||||
|
for name, compat := range actions.BuiltinCompatibility {
|
||||||
|
result[name] = api.ActionSupport{
|
||||||
|
Versions: compat.Versions,
|
||||||
|
Notes: compat.Notes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetActionsCapabilities returns structured capability information for AI consumption
|
||||||
|
// @Summary Get runner capabilities for AI workflow generation
|
||||||
|
// @Description Returns detailed runner capabilities, platform info, and action compatibility
|
||||||
|
// @Tags actions
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param owner path string true "owner of the repo"
|
||||||
|
// @Param repo path string true "name of the repo"
|
||||||
|
// @Success 200 {object} api.ActionsCapabilitiesResponse
|
||||||
|
// @Router /repos/{owner}/{repo}/actions/runners/capabilities [get]
|
||||||
|
func GetActionsCapabilities(ctx *context.APIContext) {
|
||||||
|
repo := ctx.Repo.Repository
|
||||||
|
|
||||||
|
// Get runners available for this repository
|
||||||
|
runners, err := actions_model.GetRunnersOfRepo(ctx, repo.ID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build response
|
||||||
|
response := &api.ActionsCapabilitiesResponse{
|
||||||
|
Runners: make([]*api.RunnerWithCapabilities, 0, len(runners)),
|
||||||
|
Platform: &api.PlatformInfo{
|
||||||
|
Type: "gitea",
|
||||||
|
Version: setting.AppVer,
|
||||||
|
DefaultActionsURL: setting.Actions.DefaultActionsURL.URL(),
|
||||||
|
SupportedActions: getSupportedActions(),
|
||||||
|
UnsupportedFeatures: actions.UnsupportedFeatures,
|
||||||
|
},
|
||||||
|
WorkflowHints: &api.WorkflowHints{
|
||||||
|
PreferredCheckout: "actions/checkout@v4",
|
||||||
|
ArtifactUploadAlternative: "Use Gitea API: curl -X POST $GITHUB_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY/releases/{id}/assets",
|
||||||
|
SecretAccess: "Use ${{ secrets.NAME }} syntax",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process each runner
|
||||||
|
for _, runner := range runners {
|
||||||
|
status := "offline"
|
||||||
|
if runner.LastOnline.AsTime().Add(actions_model.RunnerOfflineTime).After(time.Now()) {
|
||||||
|
status = "online"
|
||||||
|
}
|
||||||
|
|
||||||
|
runnerResp := &api.RunnerWithCapabilities{
|
||||||
|
ID: runner.ID,
|
||||||
|
Name: runner.Name,
|
||||||
|
Status: status,
|
||||||
|
Labels: runner.AgentLabels,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse capabilities JSON if available
|
||||||
|
if runner.CapabilitiesJSON != "" {
|
||||||
|
var cap api.RunnerCapability
|
||||||
|
if err := json.Unmarshal([]byte(runner.CapabilitiesJSON), &cap); err == nil {
|
||||||
|
runnerResp.Capabilities = &cap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no capabilities, infer from labels
|
||||||
|
if runnerResp.Capabilities == nil {
|
||||||
|
runnerResp.Capabilities = inferCapabilitiesFromLabels(runner.AgentLabels)
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Runners = append(response.Runners, runnerResp)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(200, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// inferCapabilitiesFromLabels attempts to infer capabilities from runner labels
|
||||||
|
func inferCapabilitiesFromLabels(labels []string) *api.RunnerCapability {
|
||||||
|
cap := &api.RunnerCapability{
|
||||||
|
Limitations: []string{
|
||||||
|
"Capabilities inferred from labels - may not be accurate",
|
||||||
|
"actions/upload-artifact@v4 not supported (use v3 or direct API upload)",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, label := range labels {
|
||||||
|
switch label {
|
||||||
|
case "ubuntu-latest", "ubuntu-22.04", "ubuntu-20.04":
|
||||||
|
cap.OS = "linux"
|
||||||
|
cap.Shell = []string{"bash", "sh"}
|
||||||
|
case "windows-latest", "windows-2022", "windows-2019":
|
||||||
|
cap.OS = "windows"
|
||||||
|
cap.Shell = []string{"pwsh", "powershell", "cmd"}
|
||||||
|
case "macos-latest", "macos-13", "macos-12":
|
||||||
|
cap.OS = "darwin"
|
||||||
|
cap.Shell = []string{"bash", "sh", "zsh"}
|
||||||
|
case "linux":
|
||||||
|
cap.OS = "linux"
|
||||||
|
case "x64", "amd64":
|
||||||
|
cap.Arch = "amd64"
|
||||||
|
case "arm64", "aarch64":
|
||||||
|
cap.Arch = "arm64"
|
||||||
|
case "docker":
|
||||||
|
cap.Docker = true
|
||||||
|
cap.ContainerRuntime = "docker"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cap
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateWorkflow validates a workflow YAML and returns compatibility warnings
|
||||||
|
// @Summary Validate a workflow for compatibility
|
||||||
|
// @Description Parses workflow YAML and returns warnings about unsupported features
|
||||||
|
// @Tags actions
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param owner path string true "owner of the repo"
|
||||||
|
// @Param repo path string true "name of the repo"
|
||||||
|
// @Param body body api.WorkflowValidationRequest true "Workflow content"
|
||||||
|
// @Success 200 {object} api.WorkflowValidationResponse
|
||||||
|
// @Router /repos/{owner}/{repo}/actions/workflows/validate [post]
|
||||||
|
func ValidateWorkflow(ctx *context.APIContext) {
|
||||||
|
form := web.GetForm(ctx).(*api.WorkflowValidationRequest)
|
||||||
|
|
||||||
|
response := &api.WorkflowValidationResponse{
|
||||||
|
Valid: true,
|
||||||
|
Warnings: make([]*api.WorkflowValidationWarning, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for known incompatible actions using the compatibility module
|
||||||
|
for action, message := range actions.IncompatibleActions {
|
||||||
|
if containsAction(form.Content, action) {
|
||||||
|
response.Warnings = append(response.Warnings, &api.WorkflowValidationWarning{
|
||||||
|
Action: action,
|
||||||
|
Severity: "error",
|
||||||
|
Message: message,
|
||||||
|
Suggestion: actions.GetSuggestedAlternative(action),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(response.Warnings) > 0 {
|
||||||
|
response.Valid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(200, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// containsAction checks if workflow content contains a specific action
|
||||||
|
func containsAction(content, action string) bool {
|
||||||
|
// Simple string search - could be enhanced with YAML parsing
|
||||||
|
return len(content) > 0 && len(action) > 0 &&
|
||||||
|
(contains(content, "uses: "+action) || contains(content, "uses: \""+action+"\""))
|
||||||
|
}
|
||||||
|
|
||||||
|
func contains(s, substr string) bool {
|
||||||
|
return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsSubstr(s, substr))
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsSubstr(s, substr string) bool {
|
||||||
|
for i := 0; i <= len(s)-len(substr); i++ {
|
||||||
|
if s[i:i+len(substr)] == substr {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@ -22,8 +22,12 @@ package v2
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
access_model "code.gitea.io/gitea/models/perm/access"
|
||||||
auth_model "code.gitea.io/gitea/models/auth"
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
apierrors "code.gitea.io/gitea/modules/errors"
|
apierrors "code.gitea.io/gitea/modules/errors"
|
||||||
"code.gitea.io/gitea/modules/graceful"
|
"code.gitea.io/gitea/modules/graceful"
|
||||||
"code.gitea.io/gitea/modules/idempotency"
|
"code.gitea.io/gitea/modules/idempotency"
|
||||||
@ -132,6 +136,12 @@ func Routes() *web.Router {
|
|||||||
m.Delete("/pages/{pageName}", DeleteWikiPageV2)
|
m.Delete("/pages/{pageName}", DeleteWikiPageV2)
|
||||||
}, reqToken())
|
}, reqToken())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Actions v2 API - AI-friendly runner capability discovery
|
||||||
|
m.Group("/repos/{owner}/{repo}/actions", func() {
|
||||||
|
m.Get("/runners/capabilities", repoAssignment(), GetActionsCapabilities)
|
||||||
|
m.Post("/workflows/validate", repoAssignment(), reqToken(), web.Bind(api.WorkflowValidationRequest{}), ValidateWorkflow)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return m
|
return m
|
||||||
@ -188,3 +198,59 @@ func reqToken() func(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// repoAssignment loads the repository from path parameters
|
||||||
|
func repoAssignment() func(ctx *context.APIContext) {
|
||||||
|
return func(ctx *context.APIContext) {
|
||||||
|
ownerName := ctx.PathParam("owner")
|
||||||
|
repoName := ctx.PathParam("repo")
|
||||||
|
|
||||||
|
var (
|
||||||
|
owner *user_model.User
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// Check if the user is the same as the repository owner
|
||||||
|
if ctx.IsSigned && strings.EqualFold(ctx.Doer.LowerName, ownerName) {
|
||||||
|
owner = ctx.Doer
|
||||||
|
} else {
|
||||||
|
owner, err = user_model.GetUserByName(ctx, ownerName)
|
||||||
|
if err != nil {
|
||||||
|
if user_model.IsErrUserNotExist(err) {
|
||||||
|
ctx.APIErrorNotFound("GetUserByName", err)
|
||||||
|
} else {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.Repo.Owner = owner
|
||||||
|
ctx.ContextUser = owner
|
||||||
|
|
||||||
|
// Get repository
|
||||||
|
repo, err := repo_model.GetRepositoryByName(ctx, owner.ID, repoName)
|
||||||
|
if err != nil {
|
||||||
|
if repo_model.IsErrRepoNotExist(err) {
|
||||||
|
ctx.APIErrorNotFound("GetRepositoryByName", err)
|
||||||
|
} else {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
repo.Owner = owner
|
||||||
|
ctx.Repo.Repository = repo
|
||||||
|
|
||||||
|
// Get permissions
|
||||||
|
ctx.Repo.Permission, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
|
||||||
|
if err != nil {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ctx.Repo.Permission.HasAnyUnitAccessOrPublicAccess() {
|
||||||
|
ctx.APIErrorNotFound("HasAnyUnitAccessOrPublicAccess")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -168,12 +168,22 @@ func Releases(ctx *context.Context) {
|
|||||||
writeAccess := ctx.Repo.CanWrite(unit.TypeReleases)
|
writeAccess := ctx.Repo.CanWrite(unit.TypeReleases)
|
||||||
ctx.Data["CanCreateRelease"] = writeAccess && !ctx.Repo.Repository.IsArchived
|
ctx.Data["CanCreateRelease"] = writeAccess && !ctx.Repo.Repository.IsArchived
|
||||||
|
|
||||||
releases, err := getReleaseInfos(ctx, &repo_model.FindReleasesOptions{
|
// Handle archived filter
|
||||||
|
showArchived := ctx.FormString("archived") == "true"
|
||||||
|
ctx.Data["ShowArchived"] = showArchived
|
||||||
|
|
||||||
|
findOpts := &repo_model.FindReleasesOptions{
|
||||||
ListOptions: listOptions,
|
ListOptions: listOptions,
|
||||||
// only show draft releases for users who can write, read-only users shouldn't see draft releases.
|
// only show draft releases for users who can write, read-only users shouldn't see draft releases.
|
||||||
IncludeDrafts: writeAccess,
|
IncludeDrafts: writeAccess,
|
||||||
RepoID: ctx.Repo.Repository.ID,
|
RepoID: ctx.Repo.Repository.ID,
|
||||||
})
|
IncludeArchived: showArchived,
|
||||||
|
}
|
||||||
|
if !showArchived {
|
||||||
|
findOpts.IsArchived = optional.Some(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
releases, err := getReleaseInfos(ctx, findOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("getReleaseInfos", err)
|
ctx.ServerError("getReleaseInfos", err)
|
||||||
return
|
return
|
||||||
@ -701,3 +711,47 @@ func deleteReleaseOrTag(ctx *context.Context, isDelTag bool) {
|
|||||||
|
|
||||||
redirect()
|
redirect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ArchiveReleasePost archives a release
|
||||||
|
func ArchiveReleasePost(ctx *context.Context) {
|
||||||
|
releaseID := ctx.PathParamInt64("id")
|
||||||
|
rel, err := repo_model.GetReleaseForRepoByID(ctx, ctx.Repo.Repository.ID, releaseID)
|
||||||
|
if err != nil {
|
||||||
|
if repo_model.IsErrReleaseNotExist(err) {
|
||||||
|
ctx.NotFound(err)
|
||||||
|
} else {
|
||||||
|
ctx.ServerError("GetReleaseForRepoByID", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := release_service.ArchiveRelease(ctx, rel); err != nil {
|
||||||
|
ctx.ServerError("ArchiveRelease", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Flash.Success(string(ctx.Tr("repo.release.archive")) + ": " + rel.Title)
|
||||||
|
ctx.Redirect(ctx.Repo.RepoLink + "/releases")
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnarchiveReleasePost unarchives a release
|
||||||
|
func UnarchiveReleasePost(ctx *context.Context) {
|
||||||
|
releaseID := ctx.PathParamInt64("id")
|
||||||
|
rel, err := repo_model.GetReleaseForRepoByID(ctx, ctx.Repo.Repository.ID, releaseID)
|
||||||
|
if err != nil {
|
||||||
|
if repo_model.IsErrReleaseNotExist(err) {
|
||||||
|
ctx.NotFound(err)
|
||||||
|
} else {
|
||||||
|
ctx.ServerError("GetReleaseForRepoByID", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := release_service.UnarchiveRelease(ctx, rel); err != nil {
|
||||||
|
ctx.ServerError("UnarchiveRelease", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Flash.Success(string(ctx.Tr("repo.release.unarchive")) + ": " + rel.Title)
|
||||||
|
ctx.Redirect(ctx.Repo.RepoLink + "/releases?archived=true")
|
||||||
|
}
|
||||||
|
|||||||
@ -1416,6 +1416,8 @@ func registerWebRoutes(m *web.Router) {
|
|||||||
m.Post("/delete", repo.DeleteRelease)
|
m.Post("/delete", repo.DeleteRelease)
|
||||||
m.Post("/attachments", repo.UploadReleaseAttachment)
|
m.Post("/attachments", repo.UploadReleaseAttachment)
|
||||||
m.Post("/attachments/remove", repo.DeleteAttachment)
|
m.Post("/attachments/remove", repo.DeleteAttachment)
|
||||||
|
m.Post("/{id}/archive", repo.ArchiveReleasePost)
|
||||||
|
m.Post("/{id}/unarchive", repo.UnarchiveReleasePost)
|
||||||
}, reqSignIn, context.RepoMustNotBeArchived(), reqRepoReleaseWriter)
|
}, reqSignIn, context.RepoMustNotBeArchived(), reqRepoReleaseWriter)
|
||||||
m.Group("/releases", func() {
|
m.Group("/releases", func() {
|
||||||
m.Get("/edit/*", repo.EditRelease)
|
m.Get("/edit/*", repo.EditRelease)
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import (
|
|||||||
|
|
||||||
// ToAPIRelease convert a repo_model.Release to api.Release
|
// ToAPIRelease convert a repo_model.Release to api.Release
|
||||||
func ToAPIRelease(ctx context.Context, repo *repo_model.Repository, r *repo_model.Release) *api.Release {
|
func ToAPIRelease(ctx context.Context, repo *repo_model.Repository, r *repo_model.Release) *api.Release {
|
||||||
return &api.Release{
|
rel := &api.Release{
|
||||||
ID: r.ID,
|
ID: r.ID,
|
||||||
TagName: r.TagName,
|
TagName: r.TagName,
|
||||||
Target: r.Target,
|
Target: r.Target,
|
||||||
@ -29,5 +29,11 @@ func ToAPIRelease(ctx context.Context, repo *repo_model.Repository, r *repo_mode
|
|||||||
PublishedAt: r.CreatedUnix.AsTime(),
|
PublishedAt: r.CreatedUnix.AsTime(),
|
||||||
Publisher: ToUser(ctx, r.Publisher, nil),
|
Publisher: ToUser(ctx, r.Publisher, nil),
|
||||||
Attachments: ToAPIAttachments(repo, r.Attachments),
|
Attachments: ToAPIAttachments(repo, r.Attachments),
|
||||||
|
IsArchived: r.IsArchived,
|
||||||
}
|
}
|
||||||
|
if r.IsArchived && r.ArchivedUnix > 0 {
|
||||||
|
archivedAt := r.ArchivedUnix.AsTime()
|
||||||
|
rel.ArchivedAt = &archivedAt
|
||||||
|
}
|
||||||
|
return rel
|
||||||
}
|
}
|
||||||
|
|||||||
@ -422,6 +422,30 @@ func DeleteReleaseByID(ctx context.Context, repo *repo_model.Repository, rel *re
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ArchiveRelease archives a release by setting IsArchived to true
|
||||||
|
func ArchiveRelease(ctx context.Context, rel *repo_model.Release) error {
|
||||||
|
if rel.IsArchived {
|
||||||
|
return nil // Already archived
|
||||||
|
}
|
||||||
|
|
||||||
|
rel.IsArchived = true
|
||||||
|
rel.ArchivedUnix = timeutil.TimeStampNow()
|
||||||
|
|
||||||
|
return repo_model.UpdateRelease(ctx, rel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnarchiveRelease unarchives a release by setting IsArchived to false
|
||||||
|
func UnarchiveRelease(ctx context.Context, rel *repo_model.Release) error {
|
||||||
|
if !rel.IsArchived {
|
||||||
|
return nil // Not archived
|
||||||
|
}
|
||||||
|
|
||||||
|
rel.IsArchived = false
|
||||||
|
rel.ArchivedUnix = 0
|
||||||
|
|
||||||
|
return repo_model.UpdateRelease(ctx, rel)
|
||||||
|
}
|
||||||
|
|
||||||
// Init start release service
|
// Init start release service
|
||||||
func Init() error {
|
func Init() error {
|
||||||
return initTagSyncQueue(graceful.GetManager().ShutdownContext())
|
return initTagSyncQueue(graceful.GetManager().ShutdownContext())
|
||||||
|
|||||||
@ -2,6 +2,9 @@
|
|||||||
* Release: the release
|
* Release: the release
|
||||||
* IsLatest: boolean indicating whether this is the latest release, optional
|
* IsLatest: boolean indicating whether this is the latest release, optional
|
||||||
*/}}
|
*/}}
|
||||||
|
{{if .Release.IsArchived}}
|
||||||
|
<span class="ui grey label">{{ctx.Locale.Tr "repo.release.archived"}}</span>
|
||||||
|
{{end}}
|
||||||
{{if .IsLatest}}
|
{{if .IsLatest}}
|
||||||
<span class="ui green label">{{ctx.Locale.Tr "repo.release.latest"}}</span>
|
<span class="ui green label">{{ctx.Locale.Tr "repo.release.latest"}}</span>
|
||||||
{{else if .Release.IsDraft}}
|
{{else if .Release.IsDraft}}
|
||||||
|
|||||||
@ -36,8 +36,23 @@
|
|||||||
{{template "repo/commit_statuses" dict "Status" $info.CommitStatus "Statuses" $info.CommitStatuses "AdditionalClasses" "tw-flex"}}
|
{{template "repo/commit_statuses" dict "Status" $info.CommitStatus "Statuses" $info.CommitStatuses "AdditionalClasses" "tw-flex"}}
|
||||||
{{template "repo/release/label" (dict "Release" $release)}}
|
{{template "repo/release/label" (dict "Release" $release)}}
|
||||||
</h4>
|
</h4>
|
||||||
<div>
|
<div class="tw-flex tw-gap-2">
|
||||||
{{if and $.CanCreateRelease (not $.PageIsSingleTag)}}
|
{{if and $.CanCreateRelease (not $.PageIsSingleTag)}}
|
||||||
|
{{if $release.IsArchived}}
|
||||||
|
<form action="{{$.RepoLink}}/releases/{{$release.ID}}/unarchive" method="post">
|
||||||
|
{{$.CsrfTokenHtml}}
|
||||||
|
<button class="muted tw-border-0 tw-bg-transparent tw-cursor-pointer" type="submit" data-tooltip-content="{{ctx.Locale.Tr "repo.release.unarchive"}}">
|
||||||
|
{{svg "octicon-archive" 16}}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{{else}}
|
||||||
|
<form action="{{$.RepoLink}}/releases/{{$release.ID}}/archive" method="post">
|
||||||
|
{{$.CsrfTokenHtml}}
|
||||||
|
<button class="muted tw-border-0 tw-bg-transparent tw-cursor-pointer" type="submit" data-tooltip-content="{{ctx.Locale.Tr "repo.release.archive"}}">
|
||||||
|
{{svg "octicon-archive" 16}}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{{end}}
|
||||||
<a class="muted" data-tooltip-content="{{ctx.Locale.Tr "repo.release.edit"}}" href="{{$.RepoLink}}/releases/edit/{{$release.TagName | PathEscapeSegments}}" rel="nofollow">
|
<a class="muted" data-tooltip-content="{{ctx.Locale.Tr "repo.release.edit"}}" href="{{$.RepoLink}}/releases/edit/{{$release.TagName | PathEscapeSegments}}" rel="nofollow">
|
||||||
{{svg "octicon-pencil"}}
|
{{svg "octicon-pencil"}}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@ -22,6 +22,14 @@
|
|||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
{{if and .PageIsReleaseList (not .PageIsSingleTag)}}
|
||||||
|
<div class="tw-flex tw-items-center tw-mb-2">
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<input type="checkbox" id="show-archived" {{if .ShowArchived}}checked{{end}} onchange="window.location.href='{{.RepoLink}}/releases?archived=' + (this.checked ? 'true' : 'false')">
|
||||||
|
<label for="show-archived">{{ctx.Locale.Tr "repo.release.show_archived"}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
{{else if $canReadCode}}
|
{{else if $canReadCode}}
|
||||||
{{/* if the "repo.releases" unit is disabled, only show the "commits / branches / tags" sub menu */}}
|
{{/* if the "repo.releases" unit is disabled, only show the "commits / branches / tags" sub menu */}}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user