forgejo/models/unit/unit.go
Jason Song 4011821c94
Implement actions (#21937)
Close #13539.

Co-authored by: @lunny @appleboy @fuxiaohei and others.

Related projects:
- https://gitea.com/gitea/actions-proto-def
- https://gitea.com/gitea/actions-proto-go
- https://gitea.com/gitea/act
- https://gitea.com/gitea/act_runner

### Summary

The target of this PR is to bring a basic implementation of "Actions",
an internal CI/CD system of Gitea. That means even though it has been
merged, the state of the feature is **EXPERIMENTAL**, and please note
that:

- It is disabled by default;
- It shouldn't be used in a production environment currently;
- It shouldn't be used in a public Gitea instance currently;
- Breaking changes may be made before it's stable.

**Please comment on #13539 if you have any different product design
ideas**, all decisions reached there will be adopted here. But in this
PR, we don't talk about **naming, feature-creep or alternatives**.

### ⚠️ Breaking

`gitea-actions` will become a reserved user name. If a user with the
name already exists in the database, it is recommended to rename it.

### Some important reviews

- What is `DEFAULT_ACTIONS_URL` in `app.ini` for?
  - https://github.com/go-gitea/gitea/pull/21937#discussion_r1055954954
- Why the api for runners is not under the normal `/api/v1` prefix?
  - https://github.com/go-gitea/gitea/pull/21937#discussion_r1061173592
- Why DBFS?
  - https://github.com/go-gitea/gitea/pull/21937#discussion_r1061301178
- Why ignore events triggered by `gitea-actions` bot?
  - https://github.com/go-gitea/gitea/pull/21937#discussion_r1063254103
- Why there's no permission control for actions?
  - https://github.com/go-gitea/gitea/pull/21937#discussion_r1090229868

### What it looks like

<details>

#### Manage runners

<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205870657-c72f590e-2e08-4cd4-be7f-2e0abb299bbf.png">

#### List runs

<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205872794-50fde990-2b45-48c1-a178-908e4ec5b627.png">


#### View logs

<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205872501-9b7b9000-9542-4991-8f55-18ccdada77c3.png">



</details>

### How to try it

<details>

#### 1. Start Gitea

Clone this branch and [install from
source](https://docs.gitea.io/en-us/install-from-source).

Add additional configurations in `app.ini` to enable Actions:

```ini
[actions]
ENABLED = true
```

Start it.

If all is well, you'll see the management page of runners:

<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205877365-8e30a780-9b10-4154-b3e8-ee6c3cb35a59.png">


#### 2. Start runner

Clone the [act_runner](https://gitea.com/gitea/act_runner), and follow
the
[README](https://gitea.com/gitea/act_runner/src/branch/main/README.md)
to start it.

If all is well, you'll see a new runner has been added:

<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205878000-216f5937-e696-470d-b66c-8473987d91c3.png">

#### 3. Enable actions for a repo

Create a new repo or open an existing one, check the `Actions` checkbox
in settings and submit.

<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205879705-53e09208-73c0-4b3e-a123-2dcf9aba4b9c.png">
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205879383-23f3d08f-1a85-41dd-a8b3-54e2ee6453e8.png">

If all is well, you'll see a new tab "Actions":

<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205881648-a8072d8c-5803-4d76-b8a8-9b2fb49516c1.png">

#### 4. Upload workflow files

Upload some workflow files to `.gitea/workflows/xxx.yaml`, you can
follow the [quickstart](https://docs.github.com/en/actions/quickstart)
of GitHub Actions. Yes, Gitea Actions is compatible with GitHub Actions
in most cases, you can use the same demo:

```yaml
name: GitHub Actions Demo
run-name: ${{ github.actor }} is testing out GitHub Actions 🚀
on: [push]
jobs:
  Explore-GitHub-Actions:
    runs-on: ubuntu-latest
    steps:
      - run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event."
      - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!"
      - run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}."
      - name: Check out repository code
        uses: actions/checkout@v3
      - run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
      - run: echo "🖥️ The workflow is now ready to test your code on the runner."
      - name: List files in the repository
        run: |
          ls ${{ github.workspace }}
      - run: echo "🍏 This job's status is ${{ job.status }}."
```

If all is well, you'll see a new run in `Actions` tab:

<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205884473-79a874bc-171b-4aaf-acd5-0241a45c3b53.png">

#### 5. Check the logs of jobs

Click a run and you'll see the logs:

<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205884800-994b0374-67f7-48ff-be9a-4c53f3141547.png">

#### 6. Go on

You can try more examples in [the
documents](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions)
of GitHub Actions, then you might find a lot of bugs.

Come on, PRs are welcome.

</details>

See also: [Feature Preview: Gitea
Actions](https://blog.gitea.io/2022/12/feature-preview-gitea-actions/)

---------

Co-authored-by: a1012112796 <1012112796@qq.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: ChristopherHX <christopher.homberger@web.de>
Co-authored-by: John Olheiser <john.olheiser@gmail.com>
2023-01-31 09:45:19 +08:00

371 lines
8.1 KiB
Go

// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package unit
import (
"fmt"
"strings"
"code.gitea.io/gitea/models/perm"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
)
// Type is Unit's Type
type Type int
// Enumerate all the unit types
const (
TypeInvalid Type = iota // 0 invalid
TypeCode // 1 code
TypeIssues // 2 issues
TypePullRequests // 3 PRs
TypeReleases // 4 Releases
TypeWiki // 5 Wiki
TypeExternalWiki // 6 ExternalWiki
TypeExternalTracker // 7 ExternalTracker
TypeProjects // 8 Kanban board
TypePackages // 9 Packages
TypeActions // 10 Actions
)
// Value returns integer value for unit type
func (u Type) Value() int {
return int(u)
}
func (u Type) String() string {
switch u {
case TypeCode:
return "TypeCode"
case TypeIssues:
return "TypeIssues"
case TypePullRequests:
return "TypePullRequests"
case TypeReleases:
return "TypeReleases"
case TypeWiki:
return "TypeWiki"
case TypeExternalWiki:
return "TypeExternalWiki"
case TypeExternalTracker:
return "TypeExternalTracker"
case TypeProjects:
return "TypeProjects"
case TypePackages:
return "TypePackages"
case TypeActions:
return "TypeActions"
}
return fmt.Sprintf("Unknown Type %d", u)
}
// ColorFormat provides a ColorFormatted version of this Type
func (u Type) ColorFormat(s fmt.State) {
log.ColorFprintf(s, "%d:%s",
log.NewColoredIDValue(u),
u)
}
var (
// AllRepoUnitTypes contains all the unit types
AllRepoUnitTypes = []Type{
TypeCode,
TypeIssues,
TypePullRequests,
TypeReleases,
TypeWiki,
TypeExternalWiki,
TypeExternalTracker,
TypeProjects,
TypePackages,
TypeActions,
}
// DefaultRepoUnits contains the default unit types
DefaultRepoUnits = []Type{
TypeCode,
TypeIssues,
TypePullRequests,
TypeReleases,
TypeWiki,
TypeProjects,
TypePackages,
}
// NotAllowedDefaultRepoUnits contains units that can't be default
NotAllowedDefaultRepoUnits = []Type{
TypeExternalWiki,
TypeExternalTracker,
}
// MustRepoUnits contains the units could not be disabled currently
MustRepoUnits = []Type{
TypeCode,
TypeReleases,
}
// DisabledRepoUnits contains the units that have been globally disabled
DisabledRepoUnits = []Type{}
)
// LoadUnitConfig load units from settings
func LoadUnitConfig() {
setDefaultRepoUnits := FindUnitTypes(setting.Repository.DefaultRepoUnits...)
// Default repo units set if setting is not empty
if len(setDefaultRepoUnits) > 0 {
// MustRepoUnits required as default
DefaultRepoUnits = make([]Type, len(MustRepoUnits))
copy(DefaultRepoUnits, MustRepoUnits)
for _, defaultU := range setDefaultRepoUnits {
if !defaultU.CanBeDefault() {
log.Warn("Not allowed as default unit: %s", defaultU.String())
continue
}
// MustRepoUnits already added
if defaultU.CanDisable() {
DefaultRepoUnits = append(DefaultRepoUnits, defaultU)
}
}
}
DisabledRepoUnits = FindUnitTypes(setting.Repository.DisabledRepoUnits...)
// Check that must units are not disabled
for i, disabledU := range DisabledRepoUnits {
if !disabledU.CanDisable() {
log.Warn("Not allowed to global disable unit %s", disabledU.String())
DisabledRepoUnits = append(DisabledRepoUnits[:i], DisabledRepoUnits[i+1:]...)
}
}
// Remove disabled units from default units
for _, disabledU := range DisabledRepoUnits {
for i, defaultU := range DefaultRepoUnits {
if defaultU == disabledU {
DefaultRepoUnits = append(DefaultRepoUnits[:i], DefaultRepoUnits[i+1:]...)
}
}
}
}
// UnitGlobalDisabled checks if unit type is global disabled
func (u Type) UnitGlobalDisabled() bool {
for _, ud := range DisabledRepoUnits {
if u == ud {
return true
}
}
return false
}
// CanDisable checks if this unit type can be disabled.
func (u *Type) CanDisable() bool {
for _, mu := range MustRepoUnits {
if *u == mu {
return false
}
}
return true
}
// CanBeDefault checks if the unit type can be a default repo unit
func (u *Type) CanBeDefault() bool {
for _, nadU := range NotAllowedDefaultRepoUnits {
if *u == nadU {
return false
}
}
return true
}
// Unit is a section of one repository
type Unit struct {
Type Type
NameKey string
URI string
DescKey string
Idx int
MaxAccessMode perm.AccessMode // The max access mode of the unit. i.e. Read means this unit can only be read.
}
// CanDisable returns if this unit could be disabled.
func (u *Unit) CanDisable() bool {
return u.Type.CanDisable()
}
// IsLessThan compares order of two units
func (u Unit) IsLessThan(unit Unit) bool {
if (u.Type == TypeExternalTracker || u.Type == TypeExternalWiki) && unit.Type != TypeExternalTracker && unit.Type != TypeExternalWiki {
return false
}
return u.Idx < unit.Idx
}
// MaxPerm returns the max perms of this unit
func (u Unit) MaxPerm() perm.AccessMode {
if u.Type == TypeExternalTracker || u.Type == TypeExternalWiki {
return perm.AccessModeRead
}
return perm.AccessModeAdmin
}
// Enumerate all the units
var (
UnitCode = Unit{
TypeCode,
"repo.code",
"/",
"repo.code.desc",
0,
perm.AccessModeOwner,
}
UnitIssues = Unit{
TypeIssues,
"repo.issues",
"/issues",
"repo.issues.desc",
1,
perm.AccessModeOwner,
}
UnitExternalTracker = Unit{
TypeExternalTracker,
"repo.ext_issues",
"/issues",
"repo.ext_issues.desc",
1,
perm.AccessModeRead,
}
UnitPullRequests = Unit{
TypePullRequests,
"repo.pulls",
"/pulls",
"repo.pulls.desc",
2,
perm.AccessModeOwner,
}
UnitReleases = Unit{
TypeReleases,
"repo.releases",
"/releases",
"repo.releases.desc",
3,
perm.AccessModeOwner,
}
UnitWiki = Unit{
TypeWiki,
"repo.wiki",
"/wiki",
"repo.wiki.desc",
4,
perm.AccessModeOwner,
}
UnitExternalWiki = Unit{
TypeExternalWiki,
"repo.ext_wiki",
"/wiki",
"repo.ext_wiki.desc",
4,
perm.AccessModeRead,
}
UnitProjects = Unit{
TypeProjects,
"repo.projects",
"/projects",
"repo.projects.desc",
5,
perm.AccessModeOwner,
}
UnitPackages = Unit{
TypePackages,
"repo.packages",
"/packages",
"packages.desc",
6,
perm.AccessModeRead,
}
UnitActions = Unit{
TypeActions,
"actions.actions",
"/actions",
"actions.unit.desc",
7,
perm.AccessModeOwner,
}
// Units contains all the units
Units = map[Type]Unit{
TypeCode: UnitCode,
TypeIssues: UnitIssues,
TypeExternalTracker: UnitExternalTracker,
TypePullRequests: UnitPullRequests,
TypeReleases: UnitReleases,
TypeWiki: UnitWiki,
TypeExternalWiki: UnitExternalWiki,
TypeProjects: UnitProjects,
TypePackages: UnitPackages,
TypeActions: UnitActions,
}
)
// FindUnitTypes give the unit key names and return unit
func FindUnitTypes(nameKeys ...string) (res []Type) {
for _, key := range nameKeys {
var found bool
for t, u := range Units {
if strings.EqualFold(key, u.NameKey) {
res = append(res, t)
found = true
break
}
}
if !found {
res = append(res, TypeInvalid)
}
}
return res
}
// TypeFromKey give the unit key name and return unit
func TypeFromKey(nameKey string) Type {
for t, u := range Units {
if strings.EqualFold(nameKey, u.NameKey) {
return t
}
}
return TypeInvalid
}
// AllUnitKeyNames returns all unit key names
func AllUnitKeyNames() []string {
res := make([]string, 0, len(Units))
for _, u := range Units {
res = append(res, u.NameKey)
}
return res
}
// MinUnitAccessMode returns the minial permission of the permission map
func MinUnitAccessMode(unitsMap map[Type]perm.AccessMode) perm.AccessMode {
res := perm.AccessModeNone
for t, mode := range unitsMap {
// Don't allow `TypeExternal{Tracker,Wiki}` to influence this as they can only be set to READ perms.
if t == TypeExternalTracker || t == TypeExternalWiki {
continue
}
// get the minial permission great than AccessModeNone except all are AccessModeNone
if mode > perm.AccessModeNone && (res == perm.AccessModeNone || mode < res) {
res = mode
}
}
return res
}