// Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package repo import ( go_context "context" "crypto/hmac" "crypto/sha256" "encoding/base64" "errors" "fmt" "net/http" "net/url" "strconv" "strings" "time" actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" secret_model "code.gitea.io/gitea/models/secret" "code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/v1/shared" "code.gitea.io/gitea/routers/api/v1/utils" actions_service "code.gitea.io/gitea/services/actions" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" secret_service "code.gitea.io/gitea/services/secrets" "github.com/nektos/act/pkg/model" ) // 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.PathParam("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.PathParam("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.PathParam("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: // "200": // "$ref": "#/responses/ActionVariable" // "201": // description: response when deleting a variable // "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.PathParam("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.PathParam("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.PathParam("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.PathParam("variablename") } v.Name = opt.Name v.Data = opt.Value if _, err := actions_service.UpdateVariableNameData(ctx, v); 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) } 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) } func ActionsListRepositoryWorkflows(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/actions/workflows repository ActionsListRepositoryWorkflows // --- // summary: List repository workflows // 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/ActionWorkflowList" // "400": // "$ref": "#/responses/error" // "403": // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" // "422": // "$ref": "#/responses/validationError" // "500": // "$ref": "#/responses/error" workflows, err := actions_service.ListActionWorkflows(ctx) if err != nil { ctx.Error(http.StatusInternalServerError, "ListActionWorkflows", err) return } ctx.JSON(http.StatusOK, &api.ActionWorkflowResponse{Workflows: workflows, TotalCount: int64(len(workflows))}) } func ActionsGetWorkflow(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/actions/workflows/{workflow_id} repository ActionsGetWorkflow // --- // summary: Get a workflow // 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: workflow_id // in: path // description: id of the workflow // type: string // required: true // responses: // "200": // "$ref": "#/responses/ActionWorkflow" // "400": // "$ref": "#/responses/error" // "403": // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" // "422": // "$ref": "#/responses/validationError" // "500": // "$ref": "#/responses/error" workflowID := ctx.PathParam("workflow_id") workflow, err := actions_service.GetActionWorkflow(ctx, workflowID) if err != nil { if errors.Is(err, util.ErrNotExist) { ctx.Error(http.StatusNotFound, "GetActionWorkflow", err) } else { ctx.Error(http.StatusInternalServerError, "GetActionWorkflow", err) } return } ctx.JSON(http.StatusOK, workflow) } func ActionsDisableWorkflow(ctx *context.APIContext) { // swagger:operation PUT /repos/{owner}/{repo}/actions/workflows/{workflow_id}/disable repository ActionsDisableWorkflow // --- // summary: Disable a workflow // 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: workflow_id // in: path // description: id of the workflow // type: string // required: true // responses: // "204": // description: No Content // "400": // "$ref": "#/responses/error" // "403": // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" // "422": // "$ref": "#/responses/validationError" workflowID := ctx.PathParam("workflow_id") err := actions_service.EnableOrDisableWorkflow(ctx, workflowID, false) if err != nil { if errors.Is(err, util.ErrNotExist) { ctx.Error(http.StatusNotFound, "DisableActionWorkflow", err) } else { ctx.Error(http.StatusInternalServerError, "DisableActionWorkflow", err) } return } ctx.Status(http.StatusNoContent) } func ActionsDispatchWorkflow(ctx *context.APIContext) { // swagger:operation POST /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches repository ActionsDispatchWorkflow // --- // summary: Create a workflow dispatch event // 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: workflow_id // in: path // description: id of the workflow // type: string // required: true // - name: body // in: body // schema: // "$ref": "#/definitions/CreateActionWorkflowDispatch" // responses: // "204": // description: No Content // "400": // "$ref": "#/responses/error" // "403": // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" // "422": // "$ref": "#/responses/validationError" workflowID := ctx.PathParam("workflow_id") opt := web.GetForm(ctx).(*api.CreateActionWorkflowDispatch) if opt.Ref == "" { ctx.Error(http.StatusUnprocessableEntity, "MissingWorkflowParameter", util.NewInvalidArgumentErrorf("ref is required parameter")) return } err := actions_service.DispatchActionWorkflow(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, workflowID, opt.Ref, func(workflowDispatch *model.WorkflowDispatch, inputs map[string]any) error { if strings.Contains(ctx.Req.Header.Get("Content-Type"), "form-urlencoded") { // The chi framework's "Binding" doesn't support to bind the form map values into a map[string]string // So we have to manually read the `inputs[key]` from the form for name, config := range workflowDispatch.Inputs { value := ctx.FormString("inputs["+name+"]", config.Default) inputs[name] = value } } else { for name, config := range workflowDispatch.Inputs { value, ok := opt.Inputs[name] if ok { inputs[name] = value } else { inputs[name] = config.Default } } } return nil }) if err != nil { if errors.Is(err, util.ErrNotExist) { ctx.Error(http.StatusNotFound, "DispatchActionWorkflow", err) } else if errors.Is(err, util.ErrPermissionDenied) { ctx.Error(http.StatusForbidden, "DispatchActionWorkflow", err) } else { ctx.Error(http.StatusInternalServerError, "DispatchActionWorkflow", err) } return } ctx.Status(http.StatusNoContent) } func ActionsEnableWorkflow(ctx *context.APIContext) { // swagger:operation PUT /repos/{owner}/{repo}/actions/workflows/{workflow_id}/enable repository ActionsEnableWorkflow // --- // summary: Enable a workflow // 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: workflow_id // in: path // description: id of the workflow // type: string // required: true // responses: // "204": // description: No Content // "400": // "$ref": "#/responses/error" // "403": // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" // "409": // "$ref": "#/responses/conflict" // "422": // "$ref": "#/responses/validationError" workflowID := ctx.PathParam("workflow_id") err := actions_service.EnableOrDisableWorkflow(ctx, workflowID, true) if err != nil { if errors.Is(err, util.ErrNotExist) { ctx.Error(http.StatusNotFound, "EnableActionWorkflow", err) } else { ctx.Error(http.StatusInternalServerError, "EnableActionWorkflow", err) } return } ctx.Status(http.StatusNoContent) } // GetArtifacts Lists all artifacts for a repository. func GetArtifactsOfRun(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run}/artifacts repository getArtifactsOfRun // --- // summary: Lists all artifacts for a repository run // 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: run // in: path // description: runid of the workflow run // type: integer // required: true // - name: name // in: query // description: name of the artifact // type: string // required: false // responses: // "200": // "$ref": "#/responses/ArtifactsList" // "400": // "$ref": "#/responses/error" // "404": // "$ref": "#/responses/notFound" repoID := ctx.Repo.Repository.ID artifactName := ctx.Req.URL.Query().Get("name") runID := ctx.PathParamInt64("run") artifacts, total, err := db.FindAndCount[actions_model.ActionArtifact](ctx, actions_model.FindArtifactsOptions{ RepoID: repoID, RunID: runID, ArtifactName: artifactName, FinalizedArtifactsV4: true, ListOptions: utils.GetListOptions(ctx), }) if err != nil { ctx.Error(http.StatusInternalServerError, err.Error(), err) return } res := new(api.ActionArtifactsResponse) res.TotalCount = total res.Entries = make([]*api.ActionArtifact, len(artifacts)) for i := range artifacts { convertedArtifact, err := convert.ToActionArtifact(ctx.Repo.Repository, artifacts[i]) if err != nil { ctx.Error(http.StatusInternalServerError, "ToActionArtifact", err) return } res.Entries[i] = convertedArtifact } ctx.JSON(http.StatusOK, &res) } // GetArtifacts Lists all artifacts for a repository. func GetArtifacts(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/actions/artifacts repository getArtifacts // --- // summary: Lists all artifacts for a repository // 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: name // in: query // description: name of the artifact // type: string // required: false // responses: // "200": // "$ref": "#/responses/ArtifactsList" // "400": // "$ref": "#/responses/error" // "404": // "$ref": "#/responses/notFound" repoID := ctx.Repo.Repository.ID artifactName := ctx.Req.URL.Query().Get("name") artifacts, total, err := db.FindAndCount[actions_model.ActionArtifact](ctx, actions_model.FindArtifactsOptions{ RepoID: repoID, ArtifactName: artifactName, FinalizedArtifactsV4: true, ListOptions: utils.GetListOptions(ctx), }) if err != nil { ctx.Error(http.StatusInternalServerError, err.Error(), err) return } res := new(api.ActionArtifactsResponse) res.TotalCount = total res.Entries = make([]*api.ActionArtifact, len(artifacts)) for i := range artifacts { convertedArtifact, err := convert.ToActionArtifact(ctx.Repo.Repository, artifacts[i]) if err != nil { ctx.Error(http.StatusInternalServerError, "ToActionArtifact", err) return } res.Entries[i] = convertedArtifact } ctx.JSON(http.StatusOK, &res) } // GetArtifact Gets a specific artifact for a workflow run. func GetArtifact(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/actions/artifacts/{artifact_id} repository getArtifact // --- // summary: Gets a specific artifact for a workflow run // 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: artifact_id // in: path // description: id of the artifact // type: string // required: true // responses: // "200": // "$ref": "#/responses/Artifact" // "400": // "$ref": "#/responses/error" // "404": // "$ref": "#/responses/notFound" art := getArtifactByPathParam(ctx, ctx.Repo.Repository) if ctx.Written() { return } if actions.IsArtifactV4(art) { convertedArtifact, err := convert.ToActionArtifact(ctx.Repo.Repository, art) if err != nil { ctx.Error(http.StatusInternalServerError, "ToActionArtifact", err) return } ctx.JSON(http.StatusOK, convertedArtifact) return } // v3 not supported due to not having one unique id ctx.Error(http.StatusNotFound, "GetArtifact", "Artifact not found") } // DeleteArtifact Deletes a specific artifact for a workflow run. func DeleteArtifact(ctx *context.APIContext) { // swagger:operation DELETE /repos/{owner}/{repo}/actions/artifacts/{artifact_id} repository deleteArtifact // --- // summary: Deletes a specific artifact for a workflow run // 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: artifact_id // in: path // description: id of the artifact // type: string // required: true // responses: // "204": // description: "No Content" // "400": // "$ref": "#/responses/error" // "404": // "$ref": "#/responses/notFound" art := getArtifactByPathParam(ctx, ctx.Repo.Repository) if ctx.Written() { return } if actions.IsArtifactV4(art) { if err := actions_model.SetArtifactNeedDelete(ctx, art.RunID, art.ArtifactName); err != nil { ctx.Error(http.StatusInternalServerError, "DeleteArtifact", err) return } ctx.Status(http.StatusNoContent) return } // v3 not supported due to not having one unique id ctx.Error(http.StatusNotFound, "DeleteArtifact", "Artifact not found") } func buildSignature(endp string, expires, artifactID int64) []byte { mac := hmac.New(sha256.New, setting.GetGeneralTokenSigningSecret()) mac.Write([]byte(endp)) mac.Write([]byte(fmt.Sprint(expires))) mac.Write([]byte(fmt.Sprint(artifactID))) return mac.Sum(nil) } func buildDownloadRawEndpoint(repo *repo_model.Repository, artifactID int64) string { return fmt.Sprintf("api/v1/repos/%s/%s/actions/artifacts/%d/zip/raw", url.PathEscape(repo.OwnerName), url.PathEscape(repo.Name), artifactID) } func buildSigURL(ctx go_context.Context, endPoint string, artifactID int64) string { // endPoint is a path like "api/v1/repos/owner/repo/actions/artifacts/1/zip/raw" expires := time.Now().Add(60 * time.Minute).Unix() uploadURL := httplib.GuessCurrentAppURL(ctx) + endPoint + "?sig=" + base64.URLEncoding.EncodeToString(buildSignature(endPoint, expires, artifactID)) + "&expires=" + strconv.FormatInt(expires, 10) return uploadURL } // DownloadArtifact Downloads a specific artifact for a workflow run redirects to blob url. func DownloadArtifact(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/actions/artifacts/{artifact_id}/zip repository downloadArtifact // --- // summary: Downloads a specific artifact for a workflow run redirects to blob url // 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: artifact_id // in: path // description: id of the artifact // type: string // required: true // responses: // "302": // description: redirect to the blob download // "400": // "$ref": "#/responses/error" // "404": // "$ref": "#/responses/notFound" art := getArtifactByPathParam(ctx, ctx.Repo.Repository) if ctx.Written() { return } // if artifacts status is not uploaded-confirmed, treat it as not found if art.Status == actions_model.ArtifactStatusExpired { ctx.Error(http.StatusNotFound, "DownloadArtifact", "Artifact has expired") return } ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(art.ArtifactName), art.ArtifactName)) if actions.IsArtifactV4(art) { ok, err := actions.DownloadArtifactV4ServeDirectOnly(ctx.Base, art) if ok { return } if err != nil { ctx.Error(http.StatusInternalServerError, "DownloadArtifactV4ServeDirectOnly", err) return } redirectURL := buildSigURL(ctx, buildDownloadRawEndpoint(ctx.Repo.Repository, art.ID), art.ID) ctx.Redirect(redirectURL, http.StatusFound) return } // v3 not supported due to not having one unique id ctx.Error(http.StatusNotFound, "DownloadArtifact", "Artifact not found") } // DownloadArtifactRaw Downloads a specific artifact for a workflow run directly. func DownloadArtifactRaw(ctx *context.APIContext) { // it doesn't use repoAssignment middleware, so it needs to prepare the repo and check permission (sig) by itself repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, ctx.PathParam("username"), ctx.PathParam("reponame")) if err != nil { if errors.Is(err, util.ErrNotExist) { ctx.NotFound() } else { ctx.InternalServerError(err) } return } art := getArtifactByPathParam(ctx, repo) if ctx.Written() { return } sigStr := ctx.Req.URL.Query().Get("sig") expiresStr := ctx.Req.URL.Query().Get("expires") sigBytes, _ := base64.URLEncoding.DecodeString(sigStr) expires, _ := strconv.ParseInt(expiresStr, 10, 64) expectedSig := buildSignature(buildDownloadRawEndpoint(repo, art.ID), expires, art.ID) if !hmac.Equal(sigBytes, expectedSig) { ctx.Error(http.StatusUnauthorized, "DownloadArtifactRaw", "Error unauthorized") return } t := time.Unix(expires, 0) if t.Before(time.Now()) { ctx.Error(http.StatusUnauthorized, "DownloadArtifactRaw", "Error link expired") return } // if artifacts status is not uploaded-confirmed, treat it as not found if art.Status == actions_model.ArtifactStatusExpired { ctx.Error(http.StatusNotFound, "DownloadArtifactRaw", "Artifact has expired") return } ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(art.ArtifactName), art.ArtifactName)) if actions.IsArtifactV4(art) { err := actions.DownloadArtifactV4(ctx.Base, art) if err != nil { ctx.Error(http.StatusInternalServerError, "DownloadArtifactV4", err) return } return } // v3 not supported due to not having one unique id ctx.Error(http.StatusNotFound, "DownloadArtifactRaw", "artifact not found") } // Try to get the artifact by ID and check access func getArtifactByPathParam(ctx *context.APIContext, repo *repo_model.Repository) *actions_model.ActionArtifact { artifactID := ctx.PathParamInt64("artifact_id") art, ok, err := db.GetByID[actions_model.ActionArtifact](ctx, artifactID) if err != nil { ctx.Error(http.StatusInternalServerError, "getArtifactByPathParam", err) return nil } // if artifacts status is not uploaded-confirmed, treat it as not found // only check RepoID here, because the repository owner may change over the time if !ok || art.RepoID != repo.ID || art.Status != actions_model.ArtifactStatusUploadConfirmed && art.Status != actions_model.ArtifactStatusExpired { ctx.Error(http.StatusNotFound, "getArtifactByPathParam", "artifact not found") return nil } return art }