mirror of
https://codeberg.org/forgejo/forgejo
synced 2025-10-21 15:50:38 +02:00
When making a `POST` to start a workflow dispatch, and not specifying the option `"return_run_info": true`, the API handler attempts to render a `nil` as a JSON body to a `204 No Content` response. This results in an error being logged to the console, as this status code does not permit a body. ``` Render JSON failed: http: request method or response status code does not allow body ``` There is no functional impact except for a false error log, as `ctx.JSON` just logs a `Render JSON failed` error if it fails. I could not find any existing code which allows integration tests to intercept or inspect log output, which would be required for an automated test verifying this is fixed. If anyone could advise an existing test that performs log interception, or any hints on how such a mechanism would be created, I don't mind adding it... but it may not be warranted for such a tiny bug either. ## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). ### Tests - I added test coverage for Go changes... - [ ] in their respective `*_test.go` for unit tests. - [ ] in the `tests/integration` directory if it involves interactions with a live Forgejo server. - I added test coverage for JavaScript changes... - [ ] in `web_src/js/*.test.js` if it can be unit tested. - [ ] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)). ### Documentation - [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - [x] I did not document these changes and I do not expect someone else to do it. ### Release notes - [ ] I do not want this change to show in the release notes. - [x] I want the title to show in the release notes with a link to this pull request. - [ ] I want the content of the `release-notes/<pull request number>.md` to be be used for the release notes instead of the title. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/9675 Reviewed-by: Gusted <gusted@noreply.codeberg.org> Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net> Co-committed-by: Mathieu Fenniak <mathieu@fenniak.net>
970 lines
26 KiB
Go
970 lines
26 KiB
Go
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package repo
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
actions_model "forgejo.org/models/actions"
|
|
"forgejo.org/models/db"
|
|
secret_model "forgejo.org/models/secret"
|
|
api "forgejo.org/modules/structs"
|
|
"forgejo.org/modules/util"
|
|
"forgejo.org/modules/web"
|
|
"forgejo.org/routers/api/v1/shared"
|
|
"forgejo.org/routers/api/v1/utils"
|
|
actions_service "forgejo.org/services/actions"
|
|
"forgejo.org/services/context"
|
|
"forgejo.org/services/convert"
|
|
secret_service "forgejo.org/services/secrets"
|
|
)
|
|
|
|
// ListActionsSecrets list an repo's actions secrets
|
|
func (Action) ListActionsSecrets(ctx *context.APIContext) {
|
|
// swagger:operation GET /repos/{owner}/{repo}/actions/secrets repository repoListActionsSecrets
|
|
// ---
|
|
// summary: List an repo's actions secrets
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - name: owner
|
|
// in: path
|
|
// description: owner of the repository
|
|
// type: string
|
|
// required: true
|
|
// - name: repo
|
|
// in: path
|
|
// description: name of the repository
|
|
// type: string
|
|
// required: true
|
|
// - name: page
|
|
// in: query
|
|
// description: page number of results to return (1-based)
|
|
// type: integer
|
|
// - name: limit
|
|
// in: query
|
|
// description: page size of results
|
|
// type: integer
|
|
// responses:
|
|
// "200":
|
|
// "$ref": "#/responses/SecretList"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
repo := ctx.Repo.Repository
|
|
|
|
opts := &secret_model.FindSecretsOptions{
|
|
RepoID: repo.ID,
|
|
ListOptions: utils.GetListOptions(ctx),
|
|
}
|
|
|
|
secrets, count, err := db.FindAndCount[secret_model.Secret](ctx, opts)
|
|
if err != nil {
|
|
ctx.InternalServerError(err)
|
|
return
|
|
}
|
|
|
|
apiSecrets := make([]*api.Secret, len(secrets))
|
|
for k, v := range secrets {
|
|
apiSecrets[k] = &api.Secret{
|
|
Name: v.Name,
|
|
Created: v.CreatedUnix.AsTime(),
|
|
}
|
|
}
|
|
|
|
ctx.SetTotalCountHeader(count)
|
|
ctx.JSON(http.StatusOK, apiSecrets)
|
|
}
|
|
|
|
// create or update one secret of the repository
|
|
func (Action) CreateOrUpdateSecret(ctx *context.APIContext) {
|
|
// swagger:operation PUT /repos/{owner}/{repo}/actions/secrets/{secretname} repository updateRepoSecret
|
|
// ---
|
|
// summary: Create or Update a secret value in a repository
|
|
// consumes:
|
|
// - application/json
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - name: owner
|
|
// in: path
|
|
// description: owner of the repository
|
|
// type: string
|
|
// required: true
|
|
// - name: repo
|
|
// in: path
|
|
// description: name of the repository
|
|
// type: string
|
|
// required: true
|
|
// - name: secretname
|
|
// in: path
|
|
// description: name of the secret
|
|
// type: string
|
|
// required: true
|
|
// - name: body
|
|
// in: body
|
|
// schema:
|
|
// "$ref": "#/definitions/CreateOrUpdateSecretOption"
|
|
// responses:
|
|
// "201":
|
|
// description: response when creating a secret
|
|
// "204":
|
|
// description: response when updating a secret
|
|
// "400":
|
|
// "$ref": "#/responses/error"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
repo := ctx.Repo.Repository
|
|
|
|
opt := web.GetForm(ctx).(*api.CreateOrUpdateSecretOption)
|
|
|
|
_, created, err := secret_service.CreateOrUpdateSecret(ctx, 0, repo.ID, ctx.Params("secretname"), opt.Data)
|
|
if err != nil {
|
|
if errors.Is(err, util.ErrInvalidArgument) {
|
|
ctx.Error(http.StatusBadRequest, "CreateOrUpdateSecret", err)
|
|
} else if errors.Is(err, util.ErrNotExist) {
|
|
ctx.Error(http.StatusNotFound, "CreateOrUpdateSecret", err)
|
|
} else {
|
|
ctx.Error(http.StatusInternalServerError, "CreateOrUpdateSecret", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
if created {
|
|
ctx.Status(http.StatusCreated)
|
|
} else {
|
|
ctx.Status(http.StatusNoContent)
|
|
}
|
|
}
|
|
|
|
// DeleteSecret delete one secret of the repository
|
|
func (Action) DeleteSecret(ctx *context.APIContext) {
|
|
// swagger:operation DELETE /repos/{owner}/{repo}/actions/secrets/{secretname} repository deleteRepoSecret
|
|
// ---
|
|
// summary: Delete a secret in a repository
|
|
// consumes:
|
|
// - application/json
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - name: owner
|
|
// in: path
|
|
// description: owner of the repository
|
|
// type: string
|
|
// required: true
|
|
// - name: repo
|
|
// in: path
|
|
// description: name of the repository
|
|
// type: string
|
|
// required: true
|
|
// - name: secretname
|
|
// in: path
|
|
// description: name of the secret
|
|
// type: string
|
|
// required: true
|
|
// responses:
|
|
// "204":
|
|
// description: delete one secret of the organization
|
|
// "400":
|
|
// "$ref": "#/responses/error"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
repo := ctx.Repo.Repository
|
|
|
|
err := secret_service.DeleteSecretByName(ctx, 0, repo.ID, ctx.Params("secretname"))
|
|
if err != nil {
|
|
if errors.Is(err, util.ErrInvalidArgument) {
|
|
ctx.Error(http.StatusBadRequest, "DeleteSecret", err)
|
|
} else if errors.Is(err, util.ErrNotExist) {
|
|
ctx.Error(http.StatusNotFound, "DeleteSecret", err)
|
|
} else {
|
|
ctx.Error(http.StatusInternalServerError, "DeleteSecret", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
ctx.Status(http.StatusNoContent)
|
|
}
|
|
|
|
// GetVariable get a repo-level variable
|
|
func (Action) GetVariable(ctx *context.APIContext) {
|
|
// swagger:operation GET /repos/{owner}/{repo}/actions/variables/{variablename} repository getRepoVariable
|
|
// ---
|
|
// summary: Get a repo-level variable
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - name: owner
|
|
// in: path
|
|
// description: name of the owner
|
|
// type: string
|
|
// required: true
|
|
// - name: repo
|
|
// in: path
|
|
// description: name of the repository
|
|
// type: string
|
|
// required: true
|
|
// - name: variablename
|
|
// in: path
|
|
// description: name of the variable
|
|
// type: string
|
|
// required: true
|
|
// responses:
|
|
// "200":
|
|
// "$ref": "#/responses/ActionVariable"
|
|
// "400":
|
|
// "$ref": "#/responses/error"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
|
|
RepoID: ctx.Repo.Repository.ID,
|
|
Name: ctx.Params("variablename"),
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, util.ErrNotExist) {
|
|
ctx.Error(http.StatusNotFound, "GetVariable", err)
|
|
} else {
|
|
ctx.Error(http.StatusInternalServerError, "GetVariable", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
variable := &api.ActionVariable{
|
|
OwnerID: v.OwnerID,
|
|
RepoID: v.RepoID,
|
|
Name: v.Name,
|
|
Data: v.Data,
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, variable)
|
|
}
|
|
|
|
// DeleteVariable delete a repo-level variable
|
|
func (Action) DeleteVariable(ctx *context.APIContext) {
|
|
// swagger:operation DELETE /repos/{owner}/{repo}/actions/variables/{variablename} repository deleteRepoVariable
|
|
// ---
|
|
// summary: Delete a repo-level variable
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - name: owner
|
|
// in: path
|
|
// description: name of the owner
|
|
// type: string
|
|
// required: true
|
|
// - name: repo
|
|
// in: path
|
|
// description: name of the repository
|
|
// type: string
|
|
// required: true
|
|
// - name: variablename
|
|
// in: path
|
|
// description: name of the variable
|
|
// type: string
|
|
// required: true
|
|
// responses:
|
|
// "204":
|
|
// description: response when deleting a variable
|
|
// "400":
|
|
// "$ref": "#/responses/error"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
if err := actions_service.DeleteVariableByName(ctx, 0, ctx.Repo.Repository.ID, ctx.Params("variablename")); err != nil {
|
|
if errors.Is(err, util.ErrInvalidArgument) {
|
|
ctx.Error(http.StatusBadRequest, "DeleteVariableByName", err)
|
|
} else if errors.Is(err, util.ErrNotExist) {
|
|
ctx.Error(http.StatusNotFound, "DeleteVariableByName", err)
|
|
} else {
|
|
ctx.Error(http.StatusInternalServerError, "DeleteVariableByName", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
ctx.Status(http.StatusNoContent)
|
|
}
|
|
|
|
// CreateVariable create a repo-level variable
|
|
func (Action) CreateVariable(ctx *context.APIContext) {
|
|
// swagger:operation POST /repos/{owner}/{repo}/actions/variables/{variablename} repository createRepoVariable
|
|
// ---
|
|
// summary: Create a repo-level variable
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - name: owner
|
|
// in: path
|
|
// description: name of the owner
|
|
// type: string
|
|
// required: true
|
|
// - name: repo
|
|
// in: path
|
|
// description: name of the repository
|
|
// type: string
|
|
// required: true
|
|
// - name: variablename
|
|
// in: path
|
|
// description: name of the variable
|
|
// type: string
|
|
// required: true
|
|
// - name: body
|
|
// in: body
|
|
// schema:
|
|
// "$ref": "#/definitions/CreateVariableOption"
|
|
// responses:
|
|
// "201":
|
|
// description: response when creating a repo-level variable
|
|
// "204":
|
|
// description: response when creating a repo-level variable
|
|
// "400":
|
|
// "$ref": "#/responses/error"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
opt := web.GetForm(ctx).(*api.CreateVariableOption)
|
|
|
|
repoID := ctx.Repo.Repository.ID
|
|
variableName := ctx.Params("variablename")
|
|
|
|
v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
|
|
RepoID: repoID,
|
|
Name: variableName,
|
|
})
|
|
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
|
ctx.Error(http.StatusInternalServerError, "GetVariable", err)
|
|
return
|
|
}
|
|
if v != nil && v.ID > 0 {
|
|
ctx.Error(http.StatusConflict, "VariableNameAlreadyExists", util.NewAlreadyExistErrorf("variable name %s already exists", variableName))
|
|
return
|
|
}
|
|
|
|
if _, err := actions_service.CreateVariable(ctx, 0, repoID, variableName, opt.Value); err != nil {
|
|
if errors.Is(err, util.ErrInvalidArgument) {
|
|
ctx.Error(http.StatusBadRequest, "CreateVariable", err)
|
|
} else {
|
|
ctx.Error(http.StatusInternalServerError, "CreateVariable", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
ctx.Status(http.StatusNoContent)
|
|
}
|
|
|
|
// UpdateVariable update a repo-level variable
|
|
func (Action) UpdateVariable(ctx *context.APIContext) {
|
|
// swagger:operation PUT /repos/{owner}/{repo}/actions/variables/{variablename} repository updateRepoVariable
|
|
// ---
|
|
// summary: Update a repo-level variable
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - name: owner
|
|
// in: path
|
|
// description: name of the owner
|
|
// type: string
|
|
// required: true
|
|
// - name: repo
|
|
// in: path
|
|
// description: name of the repository
|
|
// type: string
|
|
// required: true
|
|
// - name: variablename
|
|
// in: path
|
|
// description: name of the variable
|
|
// type: string
|
|
// required: true
|
|
// - name: body
|
|
// in: body
|
|
// schema:
|
|
// "$ref": "#/definitions/UpdateVariableOption"
|
|
// responses:
|
|
// "201":
|
|
// description: response when updating a repo-level variable
|
|
// "204":
|
|
// description: response when updating a repo-level variable
|
|
// "400":
|
|
// "$ref": "#/responses/error"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
opt := web.GetForm(ctx).(*api.UpdateVariableOption)
|
|
|
|
v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
|
|
RepoID: ctx.Repo.Repository.ID,
|
|
Name: ctx.Params("variablename"),
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, util.ErrNotExist) {
|
|
ctx.Error(http.StatusNotFound, "GetVariable", err)
|
|
} else {
|
|
ctx.Error(http.StatusInternalServerError, "GetVariable", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
if opt.Name == "" {
|
|
opt.Name = ctx.Params("variablename")
|
|
}
|
|
if _, err := actions_service.UpdateVariable(ctx, v.ID, 0, ctx.Repo.Repository.ID, opt.Name, opt.Value); err != nil {
|
|
if errors.Is(err, util.ErrInvalidArgument) {
|
|
ctx.Error(http.StatusBadRequest, "UpdateVariable", err)
|
|
} else {
|
|
ctx.Error(http.StatusInternalServerError, "UpdateVariable", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
ctx.Status(http.StatusNoContent)
|
|
}
|
|
|
|
// ListVariables list repo-level variables
|
|
func (Action) ListVariables(ctx *context.APIContext) {
|
|
// swagger:operation GET /repos/{owner}/{repo}/actions/variables repository getRepoVariablesList
|
|
// ---
|
|
// summary: Get repo-level variables list
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - name: owner
|
|
// in: path
|
|
// description: name of the owner
|
|
// type: string
|
|
// required: true
|
|
// - name: repo
|
|
// in: path
|
|
// description: name of the repository
|
|
// type: string
|
|
// required: true
|
|
// - name: page
|
|
// in: query
|
|
// description: page number of results to return (1-based)
|
|
// type: integer
|
|
// - name: limit
|
|
// in: query
|
|
// description: page size of results
|
|
// type: integer
|
|
// responses:
|
|
// "200":
|
|
// "$ref": "#/responses/VariableList"
|
|
// "400":
|
|
// "$ref": "#/responses/error"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
vars, count, err := db.FindAndCount[actions_model.ActionVariable](ctx, &actions_model.FindVariablesOpts{
|
|
RepoID: ctx.Repo.Repository.ID,
|
|
ListOptions: utils.GetListOptions(ctx),
|
|
})
|
|
if err != nil {
|
|
ctx.Error(http.StatusInternalServerError, "FindVariables", err)
|
|
return
|
|
}
|
|
|
|
variables := make([]*api.ActionVariable, len(vars))
|
|
for i, v := range vars {
|
|
variables[i] = &api.ActionVariable{
|
|
OwnerID: v.OwnerID,
|
|
RepoID: v.RepoID,
|
|
Name: v.Name,
|
|
}
|
|
}
|
|
|
|
ctx.SetTotalCountHeader(count)
|
|
ctx.JSON(http.StatusOK, variables)
|
|
}
|
|
|
|
// GetRegistrationToken returns the token to register repo runners
|
|
func (Action) GetRegistrationToken(ctx *context.APIContext) {
|
|
// swagger:operation GET /repos/{owner}/{repo}/actions/runners/registration-token repository repoGetRunnerRegistrationToken
|
|
// ---
|
|
// summary: Get a repository's actions runner registration token
|
|
// 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
|
|
// responses:
|
|
// "200":
|
|
// "$ref": "#/responses/RegistrationToken"
|
|
|
|
shared.GetRegistrationToken(ctx, 0, ctx.Repo.Repository.ID)
|
|
}
|
|
|
|
// CreateRegistrationToken returns the token to register repo runners
|
|
func (Action) CreateRegistrationToken(ctx *context.APIContext) {
|
|
// swagger:operation POST /repos/{owner}/{repo}/actions/runners/registration-token repository repoCreateRunnerRegistrationToken
|
|
// ---
|
|
// summary: Get a repository's actions runner registration token
|
|
// 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
|
|
// responses:
|
|
// "200":
|
|
// "$ref": "#/responses/RegistrationToken"
|
|
|
|
shared.GetRegistrationToken(ctx, 0, ctx.Repo.Repository.ID)
|
|
}
|
|
|
|
// ListRunners get repo-level runners
|
|
func (Action) ListRunners(ctx *context.APIContext) {
|
|
// swagger:operation GET /repos/{owner}/{repo}/actions/runners repository getRepoRunners
|
|
// ---
|
|
// summary: Get repo-level runners
|
|
// 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
|
|
// responses:
|
|
// "200":
|
|
// "$ref": "#/definitions/ActionRunnersResponse"
|
|
// "400":
|
|
// "$ref": "#/responses/error"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
shared.ListRunners(ctx, 0, ctx.Repo.Repository.ID)
|
|
}
|
|
|
|
// GetRunner get a repo-level runner
|
|
func (Action) GetRunner(ctx *context.APIContext) {
|
|
// swagger:operation GET /repos/{owner}/{repo}/actions/runners/{runner_id} repository getRepoRunner
|
|
// ---
|
|
// summary: Get a repo-level runner
|
|
// 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: runner_id
|
|
// in: path
|
|
// description: id of the runner
|
|
// type: string
|
|
// required: true
|
|
// responses:
|
|
// "200":
|
|
// "$ref": "#/definitions/ActionRunner"
|
|
// "400":
|
|
// "$ref": "#/responses/error"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
shared.GetRunner(ctx, 0, ctx.Repo.Repository.ID, ctx.ParamsInt64("runner_id"))
|
|
}
|
|
|
|
// DeleteRunner delete a repo-level runner
|
|
func (Action) DeleteRunner(ctx *context.APIContext) {
|
|
// swagger:operation DELETE /repos/{owner}/{repo}/actions/runners/{runner_id} repository deleteRepoRunner
|
|
// ---
|
|
// summary: Delete a repo-level runner
|
|
// 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: runner_id
|
|
// in: path
|
|
// description: id of the runner
|
|
// type: string
|
|
// required: true
|
|
// responses:
|
|
// "204":
|
|
// description: runner has been deleted
|
|
// "400":
|
|
// "$ref": "#/responses/error"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
shared.DeleteRunner(ctx, 0, ctx.Repo.Repository.ID, ctx.ParamsInt64("runner_id"))
|
|
}
|
|
|
|
// SearchActionRunJobs return a list of actions jobs filtered by the provided parameters
|
|
func (Action) SearchActionRunJobs(ctx *context.APIContext) {
|
|
// swagger:operation GET /repos/{owner}/{repo}/actions/runners/jobs repository repoSearchRunJobs
|
|
// ---
|
|
// summary: Search for repository's action jobs according filter conditions
|
|
// 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: labels
|
|
// in: query
|
|
// description: a comma separated list of run job labels to search for
|
|
// type: string
|
|
// responses:
|
|
// "200":
|
|
// "$ref": "#/responses/RunJobList"
|
|
// "403":
|
|
// "$ref": "#/responses/forbidden"
|
|
shared.GetActionRunJobs(ctx, 0, ctx.Repo.Repository.ID)
|
|
}
|
|
|
|
var _ actions_service.API = new(Action)
|
|
|
|
// Action implements actions_service.API
|
|
type Action struct{}
|
|
|
|
// NewAction creates a new Action service
|
|
func NewAction() actions_service.API {
|
|
return Action{}
|
|
}
|
|
|
|
// ListActionTasks list all the actions of a repository
|
|
func ListActionTasks(ctx *context.APIContext) {
|
|
// swagger:operation GET /repos/{owner}/{repo}/actions/tasks repository ListActionTasks
|
|
// ---
|
|
// summary: List a repository's action tasks
|
|
// 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: page
|
|
// in: query
|
|
// description: page number of results to return (1-based)
|
|
// type: integer
|
|
// - name: limit
|
|
// in: query
|
|
// description: page size of results, default maximum page size is 50
|
|
// type: integer
|
|
// responses:
|
|
// "200":
|
|
// "$ref": "#/responses/TasksList"
|
|
// "400":
|
|
// "$ref": "#/responses/error"
|
|
// "403":
|
|
// "$ref": "#/responses/forbidden"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
// "409":
|
|
// "$ref": "#/responses/conflict"
|
|
// "422":
|
|
// "$ref": "#/responses/validationError"
|
|
|
|
tasks, total, err := db.FindAndCount[actions_model.ActionTask](ctx, &actions_model.FindTaskOptions{
|
|
ListOptions: utils.GetListOptions(ctx),
|
|
RepoID: ctx.Repo.Repository.ID,
|
|
})
|
|
if err != nil {
|
|
ctx.Error(http.StatusInternalServerError, "ListActionTasks", err)
|
|
return
|
|
}
|
|
|
|
res := new(api.ActionTaskResponse)
|
|
res.TotalCount = total
|
|
|
|
res.Entries = make([]*api.ActionTask, len(tasks))
|
|
for i := range tasks {
|
|
convertedTask, err := convert.ToActionTask(ctx, tasks[i])
|
|
if err != nil {
|
|
ctx.Error(http.StatusInternalServerError, "ToActionTask", err)
|
|
return
|
|
}
|
|
res.Entries[i] = convertedTask
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, &res)
|
|
}
|
|
|
|
// DispatchWorkflow dispatches a workflow
|
|
func DispatchWorkflow(ctx *context.APIContext) {
|
|
// swagger:operation POST /repos/{owner}/{repo}/actions/workflows/{workflowfilename}/dispatches repository DispatchWorkflow
|
|
// ---
|
|
// summary: Dispatches a workflow
|
|
// consumes:
|
|
// - 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: workflowfilename
|
|
// in: path
|
|
// description: name of the workflow
|
|
// type: string
|
|
// required: true
|
|
// - name: body
|
|
// in: body
|
|
// schema:
|
|
// "$ref": "#/definitions/DispatchWorkflowOption"
|
|
// responses:
|
|
// "201":
|
|
// "$ref": "#/responses/DispatchWorkflowRun"
|
|
// "204":
|
|
// "$ref": "#/responses/empty"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
opt := web.GetForm(ctx).(*api.DispatchWorkflowOption)
|
|
name := ctx.Params("workflowfilename")
|
|
|
|
if len(opt.Ref) == 0 {
|
|
ctx.Error(http.StatusBadRequest, "ref", "ref is empty")
|
|
return
|
|
} else if len(name) == 0 {
|
|
ctx.Error(http.StatusBadRequest, "workflowfilename", "workflow file name is empty")
|
|
return
|
|
}
|
|
|
|
workflow, err := actions_service.GetWorkflowFromCommit(ctx.Repo.GitRepo, opt.Ref, name)
|
|
if err != nil {
|
|
if errors.Is(err, util.ErrNotExist) {
|
|
ctx.Error(http.StatusNotFound, "GetWorkflowFromCommit", err)
|
|
} else {
|
|
ctx.Error(http.StatusInternalServerError, "GetWorkflowFromCommit", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
inputGetter := func(key string) string {
|
|
return opt.Inputs[key]
|
|
}
|
|
|
|
run, jobs, err := workflow.Dispatch(ctx, inputGetter, ctx.Repo.Repository, ctx.Doer)
|
|
if err != nil {
|
|
if actions_service.IsInputRequiredErr(err) {
|
|
ctx.Error(http.StatusBadRequest, "workflow.Dispatch", err)
|
|
} else {
|
|
ctx.Error(http.StatusInternalServerError, "workflow.Dispatch", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
workflowRun := &api.DispatchWorkflowRun{
|
|
ID: run.ID,
|
|
RunNumber: run.Index,
|
|
Jobs: jobs,
|
|
}
|
|
|
|
if opt.ReturnRunInfo {
|
|
ctx.JSON(http.StatusCreated, workflowRun)
|
|
} else {
|
|
ctx.Status(http.StatusNoContent)
|
|
}
|
|
}
|
|
|
|
// ListActionRuns return a filtered list of ActionRun
|
|
func ListActionRuns(ctx *context.APIContext) {
|
|
// swagger:operation GET /repos/{owner}/{repo}/actions/runs repository ListActionRuns
|
|
// ---
|
|
// summary: List a repository's action runs
|
|
// 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: page
|
|
// in: query
|
|
// description: page number of results to return (1-based)
|
|
// type: integer
|
|
// - name: limit
|
|
// in: query
|
|
// description: page size of results, default maximum page size is 50
|
|
// type: integer
|
|
// - name: event
|
|
// in: query
|
|
// description: Returns workflow run triggered by the specified events. For example, `push`, `pull_request` or `workflow_dispatch`.
|
|
// type: array
|
|
// items:
|
|
// type: string
|
|
// - name: status
|
|
// in: query
|
|
// description: |
|
|
// Returns workflow runs with the check run status or conclusion that is specified. For example, a conclusion can be success or a status can be in_progress. Only Forgejo Actions can set a status of waiting, pending, or requested.
|
|
// type: array
|
|
// items:
|
|
// type: string
|
|
// enum: [unknown, waiting, running, success, failure, cancelled, skipped, blocked]
|
|
// - name: run_number
|
|
// in: query
|
|
// description: |
|
|
// Returns the workflow run associated with the run number.
|
|
// type: integer
|
|
// format: int64
|
|
// - name: head_sha
|
|
// in: query
|
|
// description: Only returns workflow runs that are associated with the specified head_sha.
|
|
// type: string
|
|
// responses:
|
|
// "200":
|
|
// "$ref": "#/responses/ActionRunList"
|
|
// "400":
|
|
// "$ref": "#/responses/error"
|
|
// "403":
|
|
// "$ref": "#/responses/forbidden"
|
|
|
|
statusStrs := ctx.FormStrings("status")
|
|
statuses := make([]actions_model.Status, len(statusStrs))
|
|
for i, s := range statusStrs {
|
|
if status, exists := actions_model.StatusFromString(s); exists {
|
|
statuses[i] = status
|
|
} else {
|
|
ctx.Error(http.StatusBadRequest, "StatusFromString", fmt.Sprintf("unknown status: %s", s))
|
|
return
|
|
}
|
|
}
|
|
|
|
runs, total, err := db.FindAndCount[actions_model.ActionRun](ctx, &actions_model.FindRunJobOptions{
|
|
ListOptions: utils.GetListOptions(ctx),
|
|
OwnerID: ctx.Repo.Owner.ID,
|
|
RepoID: ctx.Repo.Repository.ID,
|
|
Events: ctx.FormStrings("event"),
|
|
Statuses: statuses,
|
|
RunNumber: ctx.FormInt64("run_number"),
|
|
CommitSHA: ctx.FormString("head_sha"),
|
|
})
|
|
if err != nil {
|
|
ctx.Error(http.StatusInternalServerError, "ListActionRuns", err)
|
|
return
|
|
}
|
|
|
|
res := new(api.ListActionRunResponse)
|
|
res.TotalCount = total
|
|
|
|
res.Entries = make([]*api.ActionRun, len(runs))
|
|
for i, r := range runs {
|
|
if err := r.LoadAttributes(ctx); err != nil {
|
|
ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
|
|
return
|
|
}
|
|
cr := convert.ToActionRun(ctx, r, ctx.Doer)
|
|
res.Entries[i] = cr
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, &res)
|
|
}
|
|
|
|
// GetActionRun get one action instance
|
|
func GetActionRun(ctx *context.APIContext) {
|
|
// swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run_id} repository ActionRun
|
|
// ---
|
|
// summary: Get an action run
|
|
// 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: run_id
|
|
// in: path
|
|
// description: id of the action run
|
|
// type: integer
|
|
// format: int64
|
|
// required: true
|
|
// responses:
|
|
// "200":
|
|
// "$ref": "#/responses/ActionRun"
|
|
// "400":
|
|
// "$ref": "#/responses/error"
|
|
// "403":
|
|
// "$ref": "#/responses/forbidden"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
run, err := actions_model.GetRunByID(ctx, ctx.ParamsInt64(":run_id"))
|
|
if err != nil {
|
|
if errors.Is(err, util.ErrNotExist) {
|
|
ctx.Error(http.StatusNotFound, "GetRunById", err)
|
|
} else {
|
|
ctx.Error(http.StatusInternalServerError, "GetRunByID", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Action runs lives in its own table, therefore we check that the
|
|
// run with the requested ID is owned by the repository
|
|
if ctx.Repo.Repository.ID != run.RepoID {
|
|
ctx.Error(http.StatusNotFound, "GetRunById", util.ErrNotExist)
|
|
return
|
|
}
|
|
|
|
if err := run.LoadAttributes(ctx); err != nil {
|
|
ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
|
|
return
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, convert.ToActionRun(ctx, run, ctx.Doer))
|
|
}
|