mirror of
https://github.com/go-gitea/gitea.git
synced 2025-02-20 11:43:57 +08:00
Artifacts download api for artifact actions v4 (#33510)
* download endpoint has to use 302 redirect * fake blob download used if direct download not possible * downloading v3 artifacts not possible New repo apis based on GitHub Rest V3 - GET /runs/{run}/artifacts (Cannot use run index of url due to not being unique) - GET /artifacts - GET + DELETE /artifacts/{artifact_id} - GET /artifacts/{artifact_id}/zip - (GET /artifacts/{artifact_id}/zip/raw this is a workaround for a http 302 assertion in actions/toolkit) - api docs removed this is protected by a signed url like the internal artifacts api and no longer usable with any token or swagger - returns http 401 if the signature is invalid - or change the artifact id - or expired after 1 hour Closes #33353 Closes #32124 --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
parent
01bf8da02e
commit
2b8cfb557d
@ -196,7 +196,7 @@ func migrateActionsLog(ctx context.Context, dstStorage storage.ObjectStorage) er
|
|||||||
|
|
||||||
func migrateActionsArtifacts(ctx context.Context, dstStorage storage.ObjectStorage) error {
|
func migrateActionsArtifacts(ctx context.Context, dstStorage storage.ObjectStorage) error {
|
||||||
return db.Iterate(ctx, nil, func(ctx context.Context, artifact *actions_model.ActionArtifact) error {
|
return db.Iterate(ctx, nil, func(ctx context.Context, artifact *actions_model.ActionArtifact) error {
|
||||||
if artifact.Status == int64(actions_model.ArtifactStatusExpired) {
|
if artifact.Status == actions_model.ArtifactStatusExpired {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ type ActionArtifact struct {
|
|||||||
ContentEncoding string // The content encoding of the artifact
|
ContentEncoding string // The content encoding of the artifact
|
||||||
ArtifactPath string `xorm:"index unique(runid_name_path)"` // The path to the artifact when runner uploads it
|
ArtifactPath string `xorm:"index unique(runid_name_path)"` // The path to the artifact when runner uploads it
|
||||||
ArtifactName string `xorm:"index unique(runid_name_path)"` // The name of the artifact when runner uploads it
|
ArtifactName string `xorm:"index unique(runid_name_path)"` // The name of the artifact when runner uploads it
|
||||||
Status int64 `xorm:"index"` // The status of the artifact, uploading, expired or need-delete
|
Status ArtifactStatus `xorm:"index"` // The status of the artifact, uploading, expired or need-delete
|
||||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||||
UpdatedUnix timeutil.TimeStamp `xorm:"updated index"`
|
UpdatedUnix timeutil.TimeStamp `xorm:"updated index"`
|
||||||
ExpiredUnix timeutil.TimeStamp `xorm:"index"` // The time when the artifact will be expired
|
ExpiredUnix timeutil.TimeStamp `xorm:"index"` // The time when the artifact will be expired
|
||||||
@ -68,7 +68,7 @@ func CreateArtifact(ctx context.Context, t *ActionTask, artifactName, artifactPa
|
|||||||
RepoID: t.RepoID,
|
RepoID: t.RepoID,
|
||||||
OwnerID: t.OwnerID,
|
OwnerID: t.OwnerID,
|
||||||
CommitSHA: t.CommitSHA,
|
CommitSHA: t.CommitSHA,
|
||||||
Status: int64(ArtifactStatusUploadPending),
|
Status: ArtifactStatusUploadPending,
|
||||||
ExpiredUnix: timeutil.TimeStamp(time.Now().Unix() + timeutil.Day*expiredDays),
|
ExpiredUnix: timeutil.TimeStamp(time.Now().Unix() + timeutil.Day*expiredDays),
|
||||||
}
|
}
|
||||||
if _, err := db.GetEngine(ctx).Insert(artifact); err != nil {
|
if _, err := db.GetEngine(ctx).Insert(artifact); err != nil {
|
||||||
@ -108,10 +108,11 @@ func UpdateArtifactByID(ctx context.Context, id int64, art *ActionArtifact) erro
|
|||||||
|
|
||||||
type FindArtifactsOptions struct {
|
type FindArtifactsOptions struct {
|
||||||
db.ListOptions
|
db.ListOptions
|
||||||
RepoID int64
|
RepoID int64
|
||||||
RunID int64
|
RunID int64
|
||||||
ArtifactName string
|
ArtifactName string
|
||||||
Status int
|
Status int
|
||||||
|
FinalizedArtifactsV4 bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opts FindArtifactsOptions) ToOrders() string {
|
func (opts FindArtifactsOptions) ToOrders() string {
|
||||||
@ -134,6 +135,10 @@ func (opts FindArtifactsOptions) ToConds() builder.Cond {
|
|||||||
if opts.Status > 0 {
|
if opts.Status > 0 {
|
||||||
cond = cond.And(builder.Eq{"status": opts.Status})
|
cond = cond.And(builder.Eq{"status": opts.Status})
|
||||||
}
|
}
|
||||||
|
if opts.FinalizedArtifactsV4 {
|
||||||
|
cond = cond.And(builder.Eq{"status": ArtifactStatusUploadConfirmed}.Or(builder.Eq{"status": ArtifactStatusExpired}))
|
||||||
|
cond = cond.And(builder.Eq{"content_encoding": "application/zip"})
|
||||||
|
}
|
||||||
|
|
||||||
return cond
|
return cond
|
||||||
}
|
}
|
||||||
@ -172,18 +177,18 @@ func ListPendingDeleteArtifacts(ctx context.Context, limit int) ([]*ActionArtifa
|
|||||||
|
|
||||||
// SetArtifactExpired sets an artifact to expired
|
// SetArtifactExpired sets an artifact to expired
|
||||||
func SetArtifactExpired(ctx context.Context, artifactID int64) error {
|
func SetArtifactExpired(ctx context.Context, artifactID int64) error {
|
||||||
_, err := db.GetEngine(ctx).Where("id=? AND status = ?", artifactID, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: int64(ArtifactStatusExpired)})
|
_, err := db.GetEngine(ctx).Where("id=? AND status = ?", artifactID, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: ArtifactStatusExpired})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetArtifactNeedDelete sets an artifact to need-delete, cron job will delete it
|
// SetArtifactNeedDelete sets an artifact to need-delete, cron job will delete it
|
||||||
func SetArtifactNeedDelete(ctx context.Context, runID int64, name string) error {
|
func SetArtifactNeedDelete(ctx context.Context, runID int64, name string) error {
|
||||||
_, err := db.GetEngine(ctx).Where("run_id=? AND artifact_name=? AND status = ?", runID, name, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: int64(ArtifactStatusPendingDeletion)})
|
_, err := db.GetEngine(ctx).Where("run_id=? AND artifact_name=? AND status = ?", runID, name, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: ArtifactStatusPendingDeletion})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetArtifactDeleted sets an artifact to deleted
|
// SetArtifactDeleted sets an artifact to deleted
|
||||||
func SetArtifactDeleted(ctx context.Context, artifactID int64) error {
|
func SetArtifactDeleted(ctx context.Context, artifactID int64) error {
|
||||||
_, err := db.GetEngine(ctx).ID(artifactID).Cols("status").Update(&ActionArtifact{Status: int64(ArtifactStatusDeleted)})
|
_, err := db.GetEngine(ctx).ID(artifactID).Cols("status").Update(&ActionArtifact{Status: ArtifactStatusDeleted})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -69,3 +69,21 @@
|
|||||||
created_unix: 1730330775
|
created_unix: 1730330775
|
||||||
updated_unix: 1730330775
|
updated_unix: 1730330775
|
||||||
expired_unix: 1738106775
|
expired_unix: 1738106775
|
||||||
|
|
||||||
|
-
|
||||||
|
id: 23
|
||||||
|
run_id: 793
|
||||||
|
runner_id: 1
|
||||||
|
repo_id: 2
|
||||||
|
owner_id: 2
|
||||||
|
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
|
||||||
|
storage_path: "27/5/1730330775594233150.chunk"
|
||||||
|
file_size: 1024
|
||||||
|
file_compressed_size: 1024
|
||||||
|
content_encoding: "application/zip"
|
||||||
|
artifact_path: "artifact-v4-download.zip"
|
||||||
|
artifact_name: "artifact-v4-download"
|
||||||
|
status: 2
|
||||||
|
created_unix: 1730330775
|
||||||
|
updated_unix: 1730330775
|
||||||
|
expired_unix: 1738106775
|
||||||
|
48
modules/actions/artifacts.go
Normal file
48
modules/actions/artifacts.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
actions_model "code.gitea.io/gitea/models/actions"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/storage"
|
||||||
|
"code.gitea.io/gitea/services/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Artifacts using the v4 backend are stored as a single combined zip file per artifact on the backend
|
||||||
|
// The v4 backend ensures ContentEncoding is set to "application/zip", which is not the case for the old backend
|
||||||
|
func IsArtifactV4(art *actions_model.ActionArtifact) bool {
|
||||||
|
return art.ArtifactName+".zip" == art.ArtifactPath && art.ContentEncoding == "application/zip"
|
||||||
|
}
|
||||||
|
|
||||||
|
func DownloadArtifactV4ServeDirectOnly(ctx *context.Base, art *actions_model.ActionArtifact) (bool, error) {
|
||||||
|
if setting.Actions.ArtifactStorage.ServeDirect() {
|
||||||
|
u, err := storage.ActionsArtifacts.URL(art.StoragePath, art.ArtifactPath, nil)
|
||||||
|
if u != nil && err == nil {
|
||||||
|
ctx.Redirect(u.String(), http.StatusFound)
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DownloadArtifactV4Fallback(ctx *context.Base, art *actions_model.ActionArtifact) error {
|
||||||
|
f, err := storage.ActionsArtifacts.Open(art.StoragePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
http.ServeContent(ctx.Resp, ctx.Req, art.ArtifactName+".zip", art.CreatedUnix.AsLocalTime(), f)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DownloadArtifactV4(ctx *context.Base, art *actions_model.ActionArtifact) error {
|
||||||
|
ok, err := DownloadArtifactV4ServeDirectOnly(ctx, art)
|
||||||
|
if ok || err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return DownloadArtifactV4Fallback(ctx, art)
|
||||||
|
}
|
@ -65,3 +65,34 @@ type ActionWorkflowResponse struct {
|
|||||||
Workflows []*ActionWorkflow `json:"workflows"`
|
Workflows []*ActionWorkflow `json:"workflows"`
|
||||||
TotalCount int64 `json:"total_count"`
|
TotalCount int64 `json:"total_count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ActionArtifact represents a ActionArtifact
|
||||||
|
type ActionArtifact struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
SizeInBytes int64 `json:"size_in_bytes"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
ArchiveDownloadURL string `json:"archive_download_url"`
|
||||||
|
Expired bool `json:"expired"`
|
||||||
|
WorkflowRun *ActionWorkflowRun `json:"workflow_run"`
|
||||||
|
|
||||||
|
// swagger:strfmt date-time
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
// swagger:strfmt date-time
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
// swagger:strfmt date-time
|
||||||
|
ExpiresAt time.Time `json:"expires_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActionWorkflowRun represents a WorkflowRun
|
||||||
|
type ActionWorkflowRun struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
RepositoryID int64 `json:"repository_id"`
|
||||||
|
HeadSha string `json:"head_sha"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActionArtifactsResponse returns ActionArtifacts
|
||||||
|
type ActionArtifactsResponse struct {
|
||||||
|
Entries []*ActionArtifact `json:"artifacts"`
|
||||||
|
TotalCount int64 `json:"total_count"`
|
||||||
|
}
|
||||||
|
@ -292,7 +292,7 @@ func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st st
|
|||||||
}
|
}
|
||||||
|
|
||||||
artifact.StoragePath = storagePath
|
artifact.StoragePath = storagePath
|
||||||
artifact.Status = int64(actions.ArtifactStatusUploadConfirmed)
|
artifact.Status = actions.ArtifactStatusUploadConfirmed
|
||||||
if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil {
|
if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil {
|
||||||
return fmt.Errorf("update artifact error: %v", err)
|
return fmt.Errorf("update artifact error: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@ package actions
|
|||||||
// 1.3. Continue Upload Zip Content to Blobstorage (unauthenticated request), repeat until everything is uploaded
|
// 1.3. Continue Upload Zip Content to Blobstorage (unauthenticated request), repeat until everything is uploaded
|
||||||
// PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=appendBlock
|
// PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=appendBlock
|
||||||
// 1.4. BlockList xml payload to Blobstorage (unauthenticated request)
|
// 1.4. BlockList xml payload to Blobstorage (unauthenticated request)
|
||||||
// Files of about 800MB are parallel in parallel and / or out of order, this file is needed to enshure the correct order
|
// Files of about 800MB are parallel in parallel and / or out of order, this file is needed to ensure the correct order
|
||||||
// PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=blockList
|
// PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=blockList
|
||||||
// Request
|
// Request
|
||||||
// <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
// <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
@ -1241,6 +1241,13 @@ func Routes() *web.Router {
|
|||||||
}, reqToken(), reqAdmin())
|
}, reqToken(), reqAdmin())
|
||||||
m.Group("/actions", func() {
|
m.Group("/actions", func() {
|
||||||
m.Get("/tasks", repo.ListActionTasks)
|
m.Get("/tasks", repo.ListActionTasks)
|
||||||
|
m.Get("/runs/{run}/artifacts", repo.GetArtifactsOfRun)
|
||||||
|
m.Get("/artifacts", repo.GetArtifacts)
|
||||||
|
m.Group("/artifacts/{artifact_id}", func() {
|
||||||
|
m.Get("", repo.GetArtifact)
|
||||||
|
m.Delete("", reqRepoWriter(unit.TypeActions), repo.DeleteArtifact)
|
||||||
|
})
|
||||||
|
m.Get("/artifacts/{artifact_id}/zip", repo.DownloadArtifact)
|
||||||
}, reqRepoReader(unit.TypeActions), context.ReferencesGitRepo(true))
|
}, reqRepoReader(unit.TypeActions), context.ReferencesGitRepo(true))
|
||||||
m.Group("/keys", func() {
|
m.Group("/keys", func() {
|
||||||
m.Combo("").Get(repo.ListDeployKeys).
|
m.Combo("").Get(repo.ListDeployKeys).
|
||||||
@ -1401,6 +1408,10 @@ func Routes() *web.Router {
|
|||||||
}, repoAssignment(), checkTokenPublicOnly())
|
}, repoAssignment(), checkTokenPublicOnly())
|
||||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))
|
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))
|
||||||
|
|
||||||
|
// Artifacts direct download endpoint authenticates via signed url
|
||||||
|
// it is protected by the "sig" parameter (to help to access private repo), so no need to use other middlewares
|
||||||
|
m.Get("/repos/{username}/{reponame}/actions/artifacts/{artifact_id}/zip/raw", repo.DownloadArtifactRaw)
|
||||||
|
|
||||||
// Notifications (requires notifications scope)
|
// Notifications (requires notifications scope)
|
||||||
m.Group("/repos", func() {
|
m.Group("/repos", func() {
|
||||||
m.Group("/{username}/{reponame}", func() {
|
m.Group("/{username}/{reponame}", func() {
|
||||||
|
@ -4,13 +4,25 @@
|
|||||||
package repo
|
package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
go_context "context"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
actions_model "code.gitea.io/gitea/models/actions"
|
actions_model "code.gitea.io/gitea/models/actions"
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
secret_model "code.gitea.io/gitea/models/secret"
|
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"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
@ -855,3 +867,382 @@ func ActionsEnableWorkflow(ctx *context.APIContext) {
|
|||||||
|
|
||||||
ctx.Status(http.StatusNoContent)
|
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
|
||||||
|
}
|
||||||
|
@ -443,6 +443,20 @@ type swaggerRepoTasksList struct {
|
|||||||
Body api.ActionTaskResponse `json:"body"`
|
Body api.ActionTaskResponse `json:"body"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ArtifactsList
|
||||||
|
// swagger:response ArtifactsList
|
||||||
|
type swaggerRepoArtifactsList struct {
|
||||||
|
// in:body
|
||||||
|
Body api.ActionArtifactsResponse `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Artifact
|
||||||
|
// swagger:response Artifact
|
||||||
|
type swaggerRepoArtifact struct {
|
||||||
|
// in:body
|
||||||
|
Body api.ActionArtifact `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
// swagger:response Compare
|
// swagger:response Compare
|
||||||
type swaggerCompare struct {
|
type swaggerCompare struct {
|
||||||
// in:body
|
// in:body
|
||||||
|
@ -26,7 +26,6 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/base"
|
"code.gitea.io/gitea/modules/base"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
|
||||||
"code.gitea.io/gitea/modules/storage"
|
"code.gitea.io/gitea/modules/storage"
|
||||||
"code.gitea.io/gitea/modules/templates"
|
"code.gitea.io/gitea/modules/templates"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
@ -669,7 +668,7 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
|
|||||||
|
|
||||||
// if artifacts status is not uploaded-confirmed, treat it as not found
|
// if artifacts status is not uploaded-confirmed, treat it as not found
|
||||||
for _, art := range artifacts {
|
for _, art := range artifacts {
|
||||||
if art.Status != int64(actions_model.ArtifactStatusUploadConfirmed) {
|
if art.Status != actions_model.ArtifactStatusUploadConfirmed {
|
||||||
ctx.Error(http.StatusNotFound, "artifact not found")
|
ctx.Error(http.StatusNotFound, "artifact not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -677,23 +676,12 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
|
|||||||
|
|
||||||
ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(artifactName), artifactName))
|
ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(artifactName), artifactName))
|
||||||
|
|
||||||
// Artifacts using the v4 backend are stored as a single combined zip file per artifact on the backend
|
if len(artifacts) == 1 && actions.IsArtifactV4(artifacts[0]) {
|
||||||
// The v4 backend enshures ContentEncoding is set to "application/zip", which is not the case for the old backend
|
err := actions.DownloadArtifactV4(ctx.Base, artifacts[0])
|
||||||
if len(artifacts) == 1 && artifacts[0].ArtifactName+".zip" == artifacts[0].ArtifactPath && artifacts[0].ContentEncoding == "application/zip" {
|
|
||||||
art := artifacts[0]
|
|
||||||
if setting.Actions.ArtifactStorage.ServeDirect() {
|
|
||||||
u, err := storage.ActionsArtifacts.URL(art.StoragePath, art.ArtifactPath, nil)
|
|
||||||
if u != nil && err == nil {
|
|
||||||
ctx.Redirect(u.String())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
f, err := storage.ActionsArtifacts.Open(art.StoragePath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, _ = io.Copy(ctx.Resp, f)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,6 +229,28 @@ func ToActionTask(ctx context.Context, t *actions_model.ActionTask) (*api.Action
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToActionArtifact convert a actions_model.ActionArtifact to an api.ActionArtifact
|
||||||
|
func ToActionArtifact(repo *repo_model.Repository, art *actions_model.ActionArtifact) (*api.ActionArtifact, error) {
|
||||||
|
url := fmt.Sprintf("%s/actions/artifacts/%d", repo.APIURL(), art.ID)
|
||||||
|
|
||||||
|
return &api.ActionArtifact{
|
||||||
|
ID: art.ID,
|
||||||
|
Name: art.ArtifactName,
|
||||||
|
SizeInBytes: art.FileSize,
|
||||||
|
Expired: art.Status == actions_model.ArtifactStatusExpired,
|
||||||
|
URL: url,
|
||||||
|
ArchiveDownloadURL: url + "/zip",
|
||||||
|
CreatedAt: art.CreatedUnix.AsLocalTime(),
|
||||||
|
UpdatedAt: art.UpdatedUnix.AsLocalTime(),
|
||||||
|
ExpiresAt: art.ExpiredUnix.AsLocalTime(),
|
||||||
|
WorkflowRun: &api.ActionWorkflowRun{
|
||||||
|
ID: art.RunID,
|
||||||
|
RepositoryID: art.RepoID,
|
||||||
|
HeadSha: art.CommitSHA,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ToVerification convert a git.Commit.Signature to an api.PayloadCommitVerification
|
// ToVerification convert a git.Commit.Signature to an api.PayloadCommitVerification
|
||||||
func ToVerification(ctx context.Context, c *git.Commit) *api.PayloadCommitVerification {
|
func ToVerification(ctx context.Context, c *git.Commit) *api.PayloadCommitVerification {
|
||||||
verif := asymkey_model.ParseCommitWithSignature(ctx, c)
|
verif := asymkey_model.ParseCommitWithSignature(ctx, c)
|
||||||
|
336
templates/swagger/v1_json.tmpl
generated
336
templates/swagger/v1_json.tmpl
generated
@ -3919,6 +3919,187 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/repos/{owner}/{repo}/actions/artifacts": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"repository"
|
||||||
|
],
|
||||||
|
"summary": "Lists all artifacts for a repository",
|
||||||
|
"operationId": "getArtifacts",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "name of the owner",
|
||||||
|
"name": "owner",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "name of the repository",
|
||||||
|
"name": "repo",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "name of the artifact",
|
||||||
|
"name": "name",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/responses/ArtifactsList"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$ref": "#/responses/error"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/responses/notFound"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/repos/{owner}/{repo}/actions/artifacts/{artifact_id}": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"repository"
|
||||||
|
],
|
||||||
|
"summary": "Gets a specific artifact for a workflow run",
|
||||||
|
"operationId": "getArtifact",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "name of the owner",
|
||||||
|
"name": "owner",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "name of the repository",
|
||||||
|
"name": "repo",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "id of the artifact",
|
||||||
|
"name": "artifact_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/responses/Artifact"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$ref": "#/responses/error"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/responses/notFound"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"repository"
|
||||||
|
],
|
||||||
|
"summary": "Deletes a specific artifact for a workflow run",
|
||||||
|
"operationId": "deleteArtifact",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "name of the owner",
|
||||||
|
"name": "owner",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "name of the repository",
|
||||||
|
"name": "repo",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "id of the artifact",
|
||||||
|
"name": "artifact_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "No Content"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$ref": "#/responses/error"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/responses/notFound"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/repos/{owner}/{repo}/actions/artifacts/{artifact_id}/zip": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"repository"
|
||||||
|
],
|
||||||
|
"summary": "Downloads a specific artifact for a workflow run redirects to blob url",
|
||||||
|
"operationId": "downloadArtifact",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "name of the owner",
|
||||||
|
"name": "owner",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "name of the repository",
|
||||||
|
"name": "repo",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "id of the artifact",
|
||||||
|
"name": "artifact_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"302": {
|
||||||
|
"description": "redirect to the blob download"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$ref": "#/responses/error"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/responses/notFound"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/repos/{owner}/{repo}/actions/runners/registration-token": {
|
"/repos/{owner}/{repo}/actions/runners/registration-token": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"produces": [
|
||||||
@ -3952,6 +4133,58 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/repos/{owner}/{repo}/actions/runs/{run}/artifacts": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"repository"
|
||||||
|
],
|
||||||
|
"summary": "Lists all artifacts for a repository run",
|
||||||
|
"operationId": "getArtifactsOfRun",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "name of the owner",
|
||||||
|
"name": "owner",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "name of the repository",
|
||||||
|
"name": "repo",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "runid of the workflow run",
|
||||||
|
"name": "run",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "name of the artifact",
|
||||||
|
"name": "name",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/responses/ArtifactsList"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$ref": "#/responses/error"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/responses/notFound"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/repos/{owner}/{repo}/actions/secrets": {
|
"/repos/{owner}/{repo}/actions/secrets": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"produces": [
|
||||||
@ -18837,6 +19070,76 @@
|
|||||||
},
|
},
|
||||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||||
},
|
},
|
||||||
|
"ActionArtifact": {
|
||||||
|
"description": "ActionArtifact represents a ActionArtifact",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"archive_download_url": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "ArchiveDownloadURL"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"x-go-name": "CreatedAt"
|
||||||
|
},
|
||||||
|
"expired": {
|
||||||
|
"type": "boolean",
|
||||||
|
"x-go-name": "Expired"
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"x-go-name": "ExpiresAt"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"x-go-name": "ID"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "Name"
|
||||||
|
},
|
||||||
|
"size_in_bytes": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"x-go-name": "SizeInBytes"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"x-go-name": "UpdatedAt"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "URL"
|
||||||
|
},
|
||||||
|
"workflow_run": {
|
||||||
|
"$ref": "#/definitions/ActionWorkflowRun"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||||
|
},
|
||||||
|
"ActionArtifactsResponse": {
|
||||||
|
"description": "ActionArtifactsResponse returns ActionArtifacts",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"artifacts": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/ActionArtifact"
|
||||||
|
},
|
||||||
|
"x-go-name": "Entries"
|
||||||
|
},
|
||||||
|
"total_count": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"x-go-name": "TotalCount"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||||
|
},
|
||||||
"ActionTask": {
|
"ActionTask": {
|
||||||
"description": "ActionTask represents a ActionTask",
|
"description": "ActionTask represents a ActionTask",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@ -18999,6 +19302,27 @@
|
|||||||
},
|
},
|
||||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||||
},
|
},
|
||||||
|
"ActionWorkflowRun": {
|
||||||
|
"description": "ActionWorkflowRun represents a WorkflowRun",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"head_sha": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "HeadSha"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"x-go-name": "ID"
|
||||||
|
},
|
||||||
|
"repository_id": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"x-go-name": "RepositoryID"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||||
|
},
|
||||||
"Activity": {
|
"Activity": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -26064,6 +26388,18 @@
|
|||||||
"$ref": "#/definitions/AnnotatedTag"
|
"$ref": "#/definitions/AnnotatedTag"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Artifact": {
|
||||||
|
"description": "Artifact",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/ActionArtifact"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ArtifactsList": {
|
||||||
|
"description": "ArtifactsList",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/ActionArtifactsResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
"Attachment": {
|
"Attachment": {
|
||||||
"description": "Attachment",
|
"description": "Attachment",
|
||||||
"schema": {
|
"schema": {
|
||||||
|
@ -8,13 +8,20 @@ import (
|
|||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/json"
|
||||||
"code.gitea.io/gitea/modules/storage"
|
"code.gitea.io/gitea/modules/storage"
|
||||||
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/routers/api/actions"
|
"code.gitea.io/gitea/routers/api/actions"
|
||||||
actions_service "code.gitea.io/gitea/services/actions"
|
actions_service "code.gitea.io/gitea/services/actions"
|
||||||
|
|
||||||
@ -334,6 +341,206 @@ func TestActionsArtifactV4DownloadSingle(t *testing.T) {
|
|||||||
assert.Equal(t, body, resp.Body.String())
|
assert.Equal(t, body, resp.Body.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestActionsArtifactV4RunDownloadSinglePublicApi(t *testing.T) {
|
||||||
|
defer prepareTestEnvActionsArtifacts(t)()
|
||||||
|
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||||
|
session := loginUser(t, user.Name)
|
||||||
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||||
|
|
||||||
|
// confirm artifact can be listed and found by name
|
||||||
|
req := NewRequestWithBody(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/792/artifacts?name=artifact-v4-download", repo.FullName()), nil).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
var listResp api.ActionArtifactsResponse
|
||||||
|
err := json.Unmarshal(resp.Body.Bytes(), &listResp)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, listResp.Entries[0].ArchiveDownloadURL)
|
||||||
|
assert.Equal(t, "artifact-v4-download", listResp.Entries[0].Name)
|
||||||
|
|
||||||
|
// confirm artifact blob storage url can be retrieved
|
||||||
|
req = NewRequestWithBody(t, "GET", listResp.Entries[0].ArchiveDownloadURL, nil).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
|
||||||
|
resp = MakeRequest(t, req, http.StatusFound)
|
||||||
|
|
||||||
|
// confirm artifact can be downloaded and has expected content
|
||||||
|
req = NewRequestWithBody(t, "GET", resp.Header().Get("Location"), nil).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
body := strings.Repeat("D", 1024)
|
||||||
|
assert.Equal(t, body, resp.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActionsArtifactV4DownloadSinglePublicApi(t *testing.T) {
|
||||||
|
defer prepareTestEnvActionsArtifacts(t)()
|
||||||
|
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||||
|
session := loginUser(t, user.Name)
|
||||||
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||||
|
|
||||||
|
// confirm artifact can be listed and found by name
|
||||||
|
req := NewRequestWithBody(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/artifacts?name=artifact-v4-download", repo.FullName()), nil).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
var listResp api.ActionArtifactsResponse
|
||||||
|
err := json.Unmarshal(resp.Body.Bytes(), &listResp)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, listResp.Entries[0].ArchiveDownloadURL)
|
||||||
|
assert.Equal(t, "artifact-v4-download", listResp.Entries[0].Name)
|
||||||
|
|
||||||
|
// confirm artifact blob storage url can be retrieved
|
||||||
|
req = NewRequestWithBody(t, "GET", listResp.Entries[0].ArchiveDownloadURL, nil).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
|
||||||
|
resp = MakeRequest(t, req, http.StatusFound)
|
||||||
|
|
||||||
|
blobLocation := resp.Header().Get("Location")
|
||||||
|
|
||||||
|
// confirm artifact can be downloaded without token and has expected content
|
||||||
|
req = NewRequestWithBody(t, "GET", blobLocation, nil)
|
||||||
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
|
body := strings.Repeat("D", 1024)
|
||||||
|
assert.Equal(t, body, resp.Body.String())
|
||||||
|
|
||||||
|
// confirm artifact can not be downloaded without query
|
||||||
|
req = NewRequestWithBody(t, "GET", blobLocation, nil)
|
||||||
|
req.URL.RawQuery = ""
|
||||||
|
_ = MakeRequest(t, req, http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActionsArtifactV4DownloadSinglePublicApiPrivateRepo(t *testing.T) {
|
||||||
|
defer prepareTestEnvActionsArtifacts(t)()
|
||||||
|
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||||
|
session := loginUser(t, user.Name)
|
||||||
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||||
|
|
||||||
|
// confirm artifact can be listed and found by name
|
||||||
|
req := NewRequestWithBody(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/artifacts?name=artifact-v4-download", repo.FullName()), nil).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
var listResp api.ActionArtifactsResponse
|
||||||
|
err := json.Unmarshal(resp.Body.Bytes(), &listResp)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(23), listResp.Entries[0].ID)
|
||||||
|
assert.NotEmpty(t, listResp.Entries[0].ArchiveDownloadURL)
|
||||||
|
assert.Equal(t, "artifact-v4-download", listResp.Entries[0].Name)
|
||||||
|
|
||||||
|
// confirm artifact blob storage url can be retrieved
|
||||||
|
req = NewRequestWithBody(t, "GET", listResp.Entries[0].ArchiveDownloadURL, nil).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
|
||||||
|
resp = MakeRequest(t, req, http.StatusFound)
|
||||||
|
|
||||||
|
blobLocation := resp.Header().Get("Location")
|
||||||
|
// confirm artifact can be downloaded without token and has expected content
|
||||||
|
req = NewRequestWithBody(t, "GET", blobLocation, nil)
|
||||||
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
|
body := strings.Repeat("D", 1024)
|
||||||
|
assert.Equal(t, body, resp.Body.String())
|
||||||
|
|
||||||
|
// confirm artifact can not be downloaded without query
|
||||||
|
req = NewRequestWithBody(t, "GET", blobLocation, nil)
|
||||||
|
req.URL.RawQuery = ""
|
||||||
|
_ = MakeRequest(t, req, http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActionsArtifactV4ListAndGetPublicApi(t *testing.T) {
|
||||||
|
defer prepareTestEnvActionsArtifacts(t)()
|
||||||
|
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||||
|
session := loginUser(t, user.Name)
|
||||||
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||||
|
|
||||||
|
// confirm artifact can be listed
|
||||||
|
req := NewRequestWithBody(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/artifacts", repo.FullName()), nil).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
var listResp api.ActionArtifactsResponse
|
||||||
|
err := json.Unmarshal(resp.Body.Bytes(), &listResp)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
for _, artifact := range listResp.Entries {
|
||||||
|
assert.Contains(t, artifact.URL, fmt.Sprintf("/api/v1/repos/%s/actions/artifacts/%d", repo.FullName(), artifact.ID))
|
||||||
|
assert.Contains(t, artifact.ArchiveDownloadURL, fmt.Sprintf("/api/v1/repos/%s/actions/artifacts/%d/zip", repo.FullName(), artifact.ID))
|
||||||
|
req = NewRequestWithBody(t, "GET", listResp.Entries[0].URL, nil).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
|
||||||
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
|
var artifactResp api.ActionArtifact
|
||||||
|
err := json.Unmarshal(resp.Body.Bytes(), &artifactResp)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, artifact.ID, artifactResp.ID)
|
||||||
|
assert.Equal(t, artifact.Name, artifactResp.Name)
|
||||||
|
assert.Equal(t, artifact.SizeInBytes, artifactResp.SizeInBytes)
|
||||||
|
assert.Equal(t, artifact.URL, artifactResp.URL)
|
||||||
|
assert.Equal(t, artifact.ArchiveDownloadURL, artifactResp.ArchiveDownloadURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActionsArtifactV4GetArtifactMismatchedRepoNotFound(t *testing.T) {
|
||||||
|
defer prepareTestEnvActionsArtifacts(t)()
|
||||||
|
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||||
|
session := loginUser(t, user.Name)
|
||||||
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||||
|
|
||||||
|
// confirm artifacts of wrong repo is not visible
|
||||||
|
req := NewRequestWithBody(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/artifacts/%d", repo.FullName(), 22), nil).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActionsArtifactV4DownloadArtifactMismatchedRepoNotFound(t *testing.T) {
|
||||||
|
defer prepareTestEnvActionsArtifacts(t)()
|
||||||
|
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||||
|
session := loginUser(t, user.Name)
|
||||||
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||||
|
|
||||||
|
// confirm artifacts of wrong repo is not visible
|
||||||
|
req := NewRequestWithBody(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/artifacts/%d/zip", repo.FullName(), 22), nil).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActionsArtifactV4DownloadArtifactCorrectRepoFound(t *testing.T) {
|
||||||
|
defer prepareTestEnvActionsArtifacts(t)()
|
||||||
|
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||||
|
session := loginUser(t, user.Name)
|
||||||
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||||
|
|
||||||
|
// confirm artifacts of correct repo is visible
|
||||||
|
req := NewRequestWithBody(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/artifacts/%d/zip", repo.FullName(), 22), nil).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActionsArtifactV4DownloadRawArtifactCorrectRepoMissingSignatureUnauthorized(t *testing.T) {
|
||||||
|
defer prepareTestEnvActionsArtifacts(t)()
|
||||||
|
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||||
|
session := loginUser(t, user.Name)
|
||||||
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||||
|
|
||||||
|
// confirm cannot use the raw artifact endpoint even with a correct access token
|
||||||
|
req := NewRequestWithBody(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/artifacts/%d/zip/raw", repo.FullName(), 22), nil).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
func TestActionsArtifactV4Delete(t *testing.T) {
|
func TestActionsArtifactV4Delete(t *testing.T) {
|
||||||
defer prepareTestEnvActionsArtifacts(t)()
|
defer prepareTestEnvActionsArtifacts(t)()
|
||||||
|
|
||||||
@ -351,3 +558,51 @@ func TestActionsArtifactV4Delete(t *testing.T) {
|
|||||||
protojson.Unmarshal(resp.Body.Bytes(), &deleteResp)
|
protojson.Unmarshal(resp.Body.Bytes(), &deleteResp)
|
||||||
assert.True(t, deleteResp.Ok)
|
assert.True(t, deleteResp.Ok)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestActionsArtifactV4DeletePublicApi(t *testing.T) {
|
||||||
|
defer prepareTestEnvActionsArtifacts(t)()
|
||||||
|
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||||
|
session := loginUser(t, user.Name)
|
||||||
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||||
|
|
||||||
|
// confirm artifacts exists
|
||||||
|
req := NewRequestWithBody(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/artifacts/%d", repo.FullName(), 22), nil).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
// delete artifact by id
|
||||||
|
req = NewRequestWithBody(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/actions/artifacts/%d", repo.FullName(), 22), nil).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusNoContent)
|
||||||
|
|
||||||
|
// confirm artifacts has been deleted
|
||||||
|
req = NewRequestWithBody(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/artifacts/%d", repo.FullName(), 22), nil).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActionsArtifactV4DeletePublicApiNotAllowedReadScope(t *testing.T) {
|
||||||
|
defer prepareTestEnvActionsArtifacts(t)()
|
||||||
|
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||||
|
session := loginUser(t, user.Name)
|
||||||
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
|
||||||
|
|
||||||
|
// confirm artifacts exists
|
||||||
|
req := NewRequestWithBody(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/artifacts/%d", repo.FullName(), 22), nil).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
// try delete artifact by id
|
||||||
|
req = NewRequestWithBody(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/actions/artifacts/%d", repo.FullName(), 22), nil).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusForbidden)
|
||||||
|
|
||||||
|
// confirm artifacts has not been deleted
|
||||||
|
req = NewRequestWithBody(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/artifacts/%d", repo.FullName(), 22), nil).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
MakeRequest(t, req, http.StatusOK)
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user