wip
All checks were successful
default / test artifact actions (push) Successful in 29s

This commit is contained in:
Louis Seubert 2026-09-13 17:31:36 +02:00
commit abdb555e1a
Signed by: louis9902
GPG key ID: 4B9DB28F826553BD
2 changed files with 724 additions and 608 deletions

133
internal/pull/pull.go Normal file
View file

@ -0,0 +1,133 @@
package pull
import (
"archive/zip"
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"code.geekeey.de/actions/sdk"
"code.geekeey.de/actions/sdk/artifact"
)
type Action struct {
*sdk.Action
}
func New(action *sdk.Action) *Action {
return &Action{Action: action}
}
type Config struct {
Name string
Path string
}
func ParseConfig(get func(string) string) (*Config, error) {
name := get("name")
if len(name) == 0 {
return nil, fmt.Errorf("input 'name': is empty")
}
path := get("path")
if len(path) == 0 {
path = "."
}
return &Config{Name: name, Path: path}, nil
}
func (a *Action) Run(ctx context.Context) error {
cfg, err := ParseConfig(a.GetInput)
if err != nil {
return err
}
run, job, err := artifact.JobInfo()
if err != nil {
return fmt.Errorf("unable to get job info: %v", err)
}
client := artifact.NewClientFromEnv(os.Getenv)
signed, err := client.GetSignedArtifactURL(ctx, artifact.GetSignedArtifactURLRequest{
RunID: run,
JobRunID: job,
Name: cfg.Name,
})
if err != nil {
return fmt.Errorf("cannot get signed artifact URL: %w", err)
}
if signed.SignedUrl == "" {
return fmt.Errorf("no signed URL for artifact %q", cfg.Name)
}
tmp, err := os.CreateTemp("", "artifact-*.zip")
if err != nil {
return fmt.Errorf("cannot create temporary file: %w", err)
}
defer os.Remove(tmp.Name())
defer tmp.Close()
res, err := artifact.PullBlob(ctx, nil, tmp, signed.SignedUrl)
if err != nil {
return fmt.Errorf("cannot download artifact: %w", err)
}
if err := ExtractArchive(tmp.Name(), cfg.Path); err != nil {
return fmt.Errorf("cannot extract artifact: %w", err)
}
a.Noticef("downloaded artifact %s (%d bytes)", cfg.Name, res.Size)
return nil
}
// ExtractArchive extracts the zip file at zipPath into dest. Entries that would
// escape dest are rejected.
func ExtractArchive(zipPath, dest string) error {
r, err := zip.OpenReader(zipPath)
if err != nil {
return err
}
defer r.Close()
for _, f := range r.File {
if err := extractFile(f, dest); err != nil {
return err
}
}
return nil
}
func extractFile(f *zip.File, dest string) error {
cleanDest := filepath.Clean(dest)
target := filepath.Join(cleanDest, f.Name)
if target != cleanDest && !strings.HasPrefix(target, cleanDest+string(os.PathSeparator)) {
return fmt.Errorf("illegal file path %q", f.Name)
}
if f.FileInfo().IsDir() {
return os.MkdirAll(target, 0o755)
}
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
return err
}
rc, err := f.Open()
if err != nil {
return err
}
defer rc.Close()
out, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, rc)
return err
}

117
internal/pull/pull_test.go Normal file
View file

@ -0,0 +1,117 @@
package pull
import (
"archive/zip"
"os"
"path/filepath"
"reflect"
"testing"
)
func TestParseConfig(t *testing.T) {
t.Parallel()
cases := []struct {
name string
env map[string]string
want *Config
wantErr bool
}{
{
name: "default path",
env: map[string]string{"name": "foo"},
want: &Config{Name: "foo", Path: "."},
},
{
name: "explicit path",
env: map[string]string{"name": "foo", "path": "out"},
want: &Config{Name: "foo", Path: "out"},
},
{
name: "missing name",
env: map[string]string{},
wantErr: true,
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got, err := ParseConfig(func(k string) string { return tc.env[k] })
if tc.wantErr {
if err == nil {
t.Fatalf("expected error, got %+v", got)
}
return
}
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("expected %+v, got %+v", tc.want, got)
}
})
}
}
func TestExtractArchive(t *testing.T) {
t.Parallel()
zipPath := filepath.Join(t.TempDir(), "artifact.zip")
writeZip(t, zipPath, map[string]string{
"a.txt": "a",
"sub/b.txt": "b",
})
dest := t.TempDir()
if err := ExtractArchive(zipPath, dest); err != nil {
t.Fatal(err)
}
for name, want := range map[string]string{"a.txt": "a", "sub/b.txt": "b"} {
got, err := os.ReadFile(filepath.Join(dest, name))
if err != nil {
t.Fatal(err)
}
if string(got) != want {
t.Errorf("expected %q to contain %q, got %q", name, want, got)
}
}
}
func TestExtractArchive_PathTraversal(t *testing.T) {
t.Parallel()
zipPath := filepath.Join(t.TempDir(), "artifact.zip")
writeZip(t, zipPath, map[string]string{"../evil.txt": "evil"})
if err := ExtractArchive(zipPath, t.TempDir()); err == nil {
t.Fatal("expected error for path traversal entry")
}
}
func writeZip(t *testing.T, path string, files map[string]string) {
t.Helper()
f, err := os.Create(path)
if err != nil {
t.Fatal(err)
}
defer f.Close()
zw := zip.NewWriter(f)
for name, content := range files {
w, err := zw.Create(name)
if err != nil {
t.Fatal(err)
}
if _, err := w.Write([]byte(content)); err != nil {
t.Fatal(err)
}
}
if err := zw.Close(); err != nil {
t.Fatal(err)
}
}