mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-07-18 07:28:47 +00:00
## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. All work and communication must conform to Forgejo's [AI Agreement](https://codeberg.org/forgejo/governance/src/branch/main/AIAgreement.md). There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). ### Tests for Go changes - I added test coverage for Go changes... - [ ] in their respective `*_test.go` for unit tests. - [x] in the `tests/integration` directory if it involves interactions with a live Forgejo server. - I ran... - [x] `make pr-go` before pushing ### Tests for JavaScript changes (not applicable — Go-only change) ### Documentation - [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - [ ] I did not document these changes and I do not expect someone else to do it. ### Release notes - [x] This change will be noticed by a Forgejo user or admin (feature, bug fix, performance, etc.). I suggest to include a release note for this change. - [ ] This change is not visible to a Forgejo user or admin (refactor, dependency upgrade, etc.). I think there is no need to add a release note for this change. ## Summary Add public REST API endpoints under `/api/v1/` for listing, inspecting, downloading, and deleting Actions artifacts. Previously, artifacts could only be accessed through the web UI or the internal runner API. ### New endpoints | Method | Path | Description | |--------|------|-------------| | `GET` | `/repos/{owner}/{repo}/actions/artifacts` | List all artifacts for a repository | | `GET` | `/repos/{owner}/{repo}/actions/runs/{run_id}/artifacts` | List artifacts for a workflow run | | `GET` | `/repos/{owner}/{repo}/actions/artifacts/{artifact_id}` | Get artifact metadata | | `GET` | `/repos/{owner}/{repo}/actions/artifacts/{artifact_id}/zip` | Download artifact as zip | | `DELETE` | `/repos/{owner}/{repo}/actions/artifacts/{artifact_id}` | Delete an artifact | - List endpoints support `page`, `limit`, and `name` query parameters - Both v1-v3 (multi-file, zip on-the-fly) and v4 (single zip) artifact backends are supported - Expired artifacts are listed with `expired: true` but cannot be downloaded - Delete requires write permission; all other endpoints require read permission Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net> Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/12140 Reviewed-by: Andreas Ahlenstorf <aahlenst@noreply.codeberg.org> Reviewed-by: Mathieu Fenniak <mfenniak@noreply.codeberg.org> Co-authored-by: ShellWen <me@shellwen.com> Co-committed-by: ShellWen <me@shellwen.com>
96 lines
2.7 KiB
Go
96 lines
2.7 KiB
Go
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package actions
|
|
|
|
import (
|
|
"archive/zip"
|
|
"compress/gzip"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/url"
|
|
|
|
actions_model "forgejo.org/models/actions"
|
|
"forgejo.org/modules/httplib"
|
|
"forgejo.org/modules/setting"
|
|
"forgejo.org/modules/storage"
|
|
"forgejo.org/modules/util"
|
|
"forgejo.org/services/context"
|
|
)
|
|
|
|
// ServeArtifact writes an artifact archive to the response. The given rows
|
|
// must all belong to the same logical artifact (same run_id + artifact_name)
|
|
// and be in ArtifactStatusUploadConfirmed; callers are responsible for
|
|
// validating status and repository ownership beforehand.
|
|
//
|
|
// When the artifact was produced by the v4 backend (a single zip already
|
|
// sitting in storage), it is streamed or redirected to directly. Otherwise
|
|
// (v1-v3 backend) the archive is assembled on the fly from the individual
|
|
// file rows, applying gzip decompression where needed.
|
|
func ServeArtifact(base *context.Base, artifacts []*actions_model.ActionArtifact) error {
|
|
if len(artifacts) == 0 {
|
|
return errors.New("no artifacts to serve")
|
|
}
|
|
|
|
if len(artifacts) == 1 && artifacts[0].IsV4() {
|
|
return serveV4Artifact(base, artifacts[0])
|
|
}
|
|
return serveLegacyArtifact(base, artifacts)
|
|
}
|
|
|
|
func serveV4Artifact(base *context.Base, art *actions_model.ActionArtifact) error {
|
|
if setting.Actions.ArtifactStorage.MinioConfig.ServeDirect {
|
|
u, err := storage.ActionsArtifacts.URL(art.StoragePath, art.ArtifactPath, nil)
|
|
if u != nil && err == nil {
|
|
base.Redirect(u.String())
|
|
return nil
|
|
}
|
|
}
|
|
f, err := storage.ActionsArtifacts.Open(art.StoragePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
httplib.ServeContentByReadSeeker(base.Req, base.Resp, art.ArtifactName+".zip", util.ToPointer(art.UpdatedUnix.AsTime()), f)
|
|
return nil
|
|
}
|
|
|
|
func serveLegacyArtifact(base *context.Base, artifacts []*actions_model.ActionArtifact) error {
|
|
name := artifacts[0].ArtifactName
|
|
base.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(name), name))
|
|
|
|
writer := zip.NewWriter(base.Resp)
|
|
defer writer.Close()
|
|
|
|
for _, art := range artifacts {
|
|
if err := writeArtifactFile(writer, art); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func writeArtifactFile(writer *zip.Writer, art *actions_model.ActionArtifact) error {
|
|
f, err := storage.ActionsArtifacts.Open(art.StoragePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
|
|
var r io.Reader = f
|
|
if art.ContentEncoding == "gzip" {
|
|
gz, err := gzip.NewReader(f)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer gz.Close()
|
|
r = gz
|
|
}
|
|
|
|
w, err := writer.Create(art.ArtifactPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = io.Copy(w, r)
|
|
return err
|
|
}
|