refactor!: remove the builtin actions (#1407)

Removes `builtin:checkout`, `builtin:upload-artifact` and `builtin:download-artifact`, along with the container helpers only they used.

Builtin actions are tied to the runner version, so users cannot pin them, and they diverge from the shareable actions ecosystem that tools like zizmor build on. Workflows should use `actions/checkout`, `actions/upload-artifact` and `actions/download-artifact`, or lightweight third-party actions such as https://github.com/taiki-e/checkout-action.

**Breaking:** steps with `uses: builtin:checkout`, released in v4.0.0, now fail and need to switch to one of the actions above. The artifact actions were never released.

Docs: https://gitea.com/gitea/docs/pulls/643
Fixes: https://gitea.com/gitea/runner/issues/1405
Co-authored-by: silverwind <[email protected]>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1407
Reviewed-by: silverwind <[email protected]>
Co-authored-by: bircni <[email protected]>
This commit is contained in:
bircniandsilverwind authored and silverwind committed 2026-11-10 11:05:38 +00:00
1 parent 85d007c47f
commit c59caa9689
54 files changed
+18 -5751

No files matched your search

-35
View File
@@ -5,7 +5,6 @@
package container
import (
"bytes"
"context"
"errors"
"fmt"
@@ -35,38 +34,6 @@ func (e ExitCodeError) Error() string {
return fmt.Sprintf("Process completed with exit code %d.", int(e))
}
type commandOutput struct {
*io.PipeReader
cancel context.CancelFunc
done chan struct{}
err error
}
func (output *commandOutput) Close() error {
_, _ = io.Copy(io.Discard, output.PipeReader)
output.PipeReader.Close()
output.cancel()
<-output.done
return output.err
}
func streamOutput(ctx context.Context, run func(context.Context, io.Writer, io.Writer) error) io.ReadCloser {
ctx, cancel := context.WithCancel(ctx)
reader, writer := io.Pipe()
output := &commandOutput{PipeReader: reader, cancel: cancel, done: make(chan struct{})}
go func() {
defer close(output.done)
defer cancel()
var stderr bytes.Buffer
output.err = run(ctx, writer, &stderr)
if output.err != nil && strings.TrimSpace(stderr.String()) != "" {
output.err = fmt.Errorf("%w: %s", output.err, strings.TrimSpace(stderr.String()))
}
writer.CloseWithError(output.err)
}()
return output
}
// NewContainerInput the input for the New function
type NewContainerInput struct {
Image string
@@ -167,9 +134,7 @@ type Container interface {
Create(capAdd, capDrop []string) common.Executor
Copy(destPath string, files ...*FileEntry) common.Executor
CopyDir(destPath, srcPath string, useGitIgnore, skipGitDir bool) common.Executor
ExecOutput(ctx context.Context, command []string) (io.ReadCloser, error)
GetContainerArchive(ctx context.Context, srcPath string) (io.ReadCloser, error)
CopyTarStream(ctx context.Context, destPath string, write func(*TarWriter) error) error
Inspect(ctx context.Context) (*Info, error)
DumpLogs(ctx context.Context) error
Pull(forcePull bool) common.Executor
-72
View File
@@ -139,47 +139,6 @@ func (cr *containerReference) CopyDir(destPath, srcPath string, useGitIgnore, sk
).IfNot(common.Dryrun)
}
func (cr *containerReference) ExecOutput(ctx context.Context, command []string) (io.ReadCloser, error) {
if common.Dryrun(ctx) {
return nil, errors.New("DRYRUN is not supported in ExecOutput")
}
if err := cr.connect()(ctx); err != nil {
return nil, err
}
if err := cr.find()(ctx); err != nil {
return nil, err
}
if cr.id == "" {
return nil, cr.missingContainerError("exec %v", command)
}
return streamOutput(ctx, func(ctx context.Context, stdout, stderr io.Writer) error {
created, err := cr.cli.ExecCreate(ctx, cr.id, client.ExecCreateOptions{
Cmd: command, AttachStdout: true, AttachStderr: true,
})
if err != nil {
return err
}
attached, err := cr.cli.ExecAttach(ctx, created.ID, client.ExecAttachOptions{})
if err != nil {
return err
}
defer attached.Close()
stop := context.AfterFunc(ctx, func() { attached.Close() })
defer stop()
if _, err := stdcopy.StdCopy(stdout, stderr, attached.Reader); err != nil {
return err
}
inspected, err := cr.cli.ExecInspect(ctx, created.ID, client.ExecInspectOptions{})
if err != nil {
return err
}
if inspected.ExitCode != 0 {
return ExitCodeError(inspected.ExitCode)
}
return nil
}), nil
}
func (cr *containerReference) GetContainerArchive(ctx context.Context, srcPath string) (io.ReadCloser, error) {
if common.Dryrun(ctx) {
return nil, errors.New("DRYRUN is not supported in GetContainerArchive")
@@ -993,37 +952,6 @@ func (cr *containerReference) extractionRoot(dstPath string) string {
return "/"
}
func (cr *containerReference) CopyTarStream(ctx context.Context, destPath string, write func(*TarWriter) error) error {
if common.Dryrun(ctx) {
return nil
}
if err := cr.connect()(ctx); err != nil {
return err
}
if err := cr.find()(ctx); err != nil {
return err
}
if cr.id == "" {
return cr.missingContainerError("copy to %s", destPath)
}
destPath = path.Clean(destPath)
root := cr.extractionRoot(destPath)
rel := func(fullPath string) string { return strings.TrimPrefix(strings.TrimPrefix(fullPath, root), "/") }
var parents []string // sent as entries so they belong to the job user, the daemon creates missing ones as root
for dir := destPath; dir != root && dir != "/" && dir != "."; dir = path.Dir(dir) {
if _, err := cr.cli.ContainerStatPath(ctx, cr.id, client.ContainerStatPathOptions{Path: dir}); err == nil {
break
} else if !cerrdefs.IsNotFound(err) {
return fmt.Errorf("stat %s: %w", dir, err)
}
parents = append(parents, rel(dir))
}
slices.Reverse(parents)
content := pipeTar(cr.UID, cr.GID, rel(destPath), parents, write)
defer content.Close()
return cr.upload(ctx, root, content)
}
func (cr *containerReference) copyContent(dstPath string, files ...*FileEntry) common.Executor {
return func(ctx context.Context) error {
if cr.id == "" {
-64
View File
@@ -260,28 +260,6 @@ func TestDockerExecFailure(t *testing.T) {
assert.Equal(t, ExitCodeError(1), exitErr)
assert.Equal(t, "Process completed with exit code 1.", err.Error())
client.On("ContainerInspect", ctx, "123", mobyclient.ContainerInspectOptions{}).
Return(mobyclient.ContainerInspectResult{}, nil)
client.On("ExecCreate", mock.Anything, "123", mobyclient.ExecCreateOptions{
Cmd: []string{"printf", `a\b`}, AttachStdout: true, AttachStderr: true,
}).Return(mobyclient.ExecCreateResult{ID: "output"}, nil)
client.On("ExecAttach", mock.Anything, "output", mobyclient.ExecAttachOptions{}).
Return(mobyclient.ExecAttachResult{
Conn: &mockConn{},
Reader: bufio.NewReader(bytes.NewReader(append(stdcopyFrame(stdcopy.Stdout, "a\x00b"), stdcopyFrame(stdcopy.Stderr, " failed \n")...))),
}, nil)
client.On("ExecInspect", mock.Anything, "output", mobyclient.ExecInspectOptions{}).
Return(mobyclient.ExecInspectResult{ExitCode: 3}, nil)
output, err := cr.ExecOutput(ctx, []string{"printf", `a\b`})
require.NoError(t, err)
content, err := io.ReadAll(output)
require.ErrorAs(t, err, &exitErr)
assert.Equal(t, ExitCodeError(3), exitErr)
assert.Equal(t, "Process completed with exit code 3.: failed", err.Error())
assert.Equal(t, "a\x00b", string(content))
require.ErrorAs(t, output.Close(), &exitErr)
assert.Equal(t, ExitCodeError(3), exitErr)
client.AssertExpectations(t)
}
@@ -570,8 +548,6 @@ func TestRejectsMissingContainer(t *testing.T) {
check("exec", cr.exec([]string{"echo"}, nil, "", "")(ctx))
_, err := cr.GetContainerArchive(ctx, "/var/run/act/x")
check("GetContainerArchive", err)
_, err = cr.ExecOutput(ctx, []string{"true"})
check("ExecOutput", err)
_, err = cr.Inspect(ctx)
check("Inspect", err)
@@ -583,46 +559,6 @@ func TestRejectsMissingContainer(t *testing.T) {
check("Inspect after removal", err)
}
func TestCopyTarStreamOwnsMissingParentsAndRejectsEscapes(t *testing.T) {
ctx := t.Context()
client := &mockDockerClient{}
client.On("ContainerInspect", ctx, "job", mobyclient.ContainerInspectOptions{}).
Return(mobyclient.ContainerInspectResult{Container: container.InspectResponse{ID: "job", Name: "/job"}}, nil)
stat := func(p string, err error) {
client.On("ContainerStatPath", ctx, "job", mobyclient.ContainerStatPathOptions{Path: p}).
Return(mobyclient.ContainerStatPathResult{}, err).Once()
}
stat("/w/out/a", cerrdefs.ErrNotFound)
stat("/w/out", cerrdefs.ErrNotFound)
stat("/w", nil)
var entries []string
client.On("CopyToContainer", ctx, "job", mock.Anything).Run(func(args mock.Arguments) {
opts := args.Get(2).(mobyclient.CopyToContainerOptions)
assert.Equal(t, "/", opts.DestinationPath)
reader := tar.NewReader(opts.Content)
for {
header, err := reader.Next()
if errors.Is(err, io.EOF) {
return
}
require.NoError(t, err)
assert.Equal(t, []any{1001, 1002, ""}, []any{header.Uid, header.Gid, header.Uname}, header.Name)
entries = append(entries, header.Name)
}
}).Return(mobyclient.CopyToContainerResult{}, nil).Once()
cr := &containerReference{id: "job", cli: client, input: &NewContainerInput{Name: "job"}, UID: 1001, GID: 1002}
require.NoError(t, cr.CopyTarStream(ctx, "/w/out/a/", func(tw *TarWriter) error {
return tw.WriteHeader(&tar.Header{Typeflag: tar.TypeReg, Name: "dir/f.txt", Mode: 0o644, Uname: "nobody"})
}))
assert.Equal(t, []string{"w/out/", "w/out/a/", "w/out/a/dir/", "w/out/a/dir/f.txt"}, entries)
client.AssertExpectations(t)
require.ErrorContains(t, writeTarStream(io.Discard, 0, 0, "w/out/a", nil, func(tw *TarWriter) error {
return tw.WriteHeader(&tar.Header{Typeflag: tar.TypeReg, Name: "../../../etc/passwd"})
}), "escapes")
}
func TestPublicCopyPipelineHandlesStaleID(t *testing.T) {
source := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(source, "x"), nil, 0o644))
-67
View File
@@ -16,7 +16,6 @@ import (
"os/exec"
"path/filepath"
"runtime"
"slices"
"strings"
"sync"
"sync/atomic"
@@ -116,20 +115,6 @@ func (e *HostEnvironment) Inspect(_ context.Context) (*Info, error) {
return &Info{Health: HealthNone, Ports: map[string]string{}}, nil
}
func (e *HostEnvironment) ExecOutput(ctx context.Context, command []string) (io.ReadCloser, error) {
return streamOutput(ctx, func(ctx context.Context, stdout, stderr io.Writer) error {
cmd := exec.CommandContext(ctx, command[0], command[1:]...)
cmd.Dir, cmd.Stdout, cmd.Stderr = e.Workdir, stdout, stderr
if err := cmd.Run(); err != nil {
if exit, ok := errors.AsType[*exec.ExitError](err); ok {
return ExitCodeError(exit.ExitCode())
}
return err
}
return nil
}), nil
}
func (e *HostEnvironment) GetContainerArchive(ctx context.Context, srcPath string) (io.ReadCloser, error) {
srcPath = filepath.Clean(srcPath)
fi, err := os.Lstat(srcPath)
@@ -177,58 +162,6 @@ func writeHostArchive(ctx context.Context, tc *filecollector.TarCollector, srcPa
return tc.WriteFile(fi.Name(), fi, linkname, f)
}
func (e *HostEnvironment) CopyTarStream(ctx context.Context, destPath string, write func(*TarWriter) error) error {
if err := os.MkdirAll(destPath, 0o777); err != nil {
return err
}
root, err := os.OpenRoot(destPath)
if err != nil {
return err
}
defer root.Close()
content := pipeTar(0, 0, "", nil, write)
defer content.Close()
tr := tar.NewReader(content)
var dirs []*tar.Header
for {
if err := ctx.Err(); err != nil {
return err
}
hdr, err := tr.Next()
if errors.Is(err, io.EOF) {
break
} else if err != nil {
return err
}
hdr.Name = filepath.FromSlash(strings.TrimSuffix(hdr.Name, "/"))
if hdr.Typeflag == tar.TypeDir {
err = root.MkdirAll(hdr.Name, 0o777) // modes apply after the contents, so read-only directories can be filled
dirs = append(dirs, hdr)
} else {
err = writeRootFile(root, hdr.Name, hdr.FileInfo().Mode().Perm(), tr)
}
if err != nil {
return err
}
}
for _, dir := range slices.Backward(dirs) {
if err := root.Chmod(dir.Name, dir.FileInfo().Mode().Perm()); err != nil {
return err
}
}
return nil
}
func writeRootFile(root *os.Root, name string, mode fs.FileMode, body io.Reader) error {
_ = root.Remove(name) // replaces read-only files and applies mode, as the daemon and tar do
file, err := root.OpenFile(name, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode)
if err != nil {
return err
}
_, err = io.Copy(file, body)
return errors.Join(err, file.Close())
}
func (e *HostEnvironment) Pull(_ bool) common.Executor {
return common.NewPipelineExecutor()
}
-59
View File
@@ -8,7 +8,6 @@ import (
"archive/tar"
"bytes"
"context"
"fmt"
"io"
"os"
"path/filepath"
@@ -84,69 +83,11 @@ func TestGetContainerArchive(t *testing.T) {
assert.ErrorIs(t, err, os.ErrNotExist)
}
func TestHostCopyTarStream(t *testing.T) {
dest := filepath.Join(t.TempDir(), "out")
write := func(name string, mode int64, body string) func(*TarWriter) error {
return func(tw *TarWriter) error {
if err := tw.WriteHeader(&tar.Header{Typeflag: tar.TypeReg, Name: name, Mode: mode, Size: int64(len(body))}); err != nil {
return err
}
_, err := tw.Write([]byte(body))
return err
}
}
e := &HostEnvironment{}
require.NoError(t, e.CopyTarStream(t.Context(), dest, write("dir/run.sh", 0o755, "#!/bin/sh\n")))
content, err := os.ReadFile(filepath.Join(dest, "dir", "run.sh"))
require.NoError(t, err)
assert.Equal(t, "#!/bin/sh\n", string(content))
if runtime.GOOS != "windows" {
require.NoError(t, os.Chmod(filepath.Join(dest, "dir", "run.sh"), 0o444))
require.NoError(t, e.CopyTarStream(t.Context(), dest, func(tw *TarWriter) error {
if err := tw.WriteHeader(&tar.Header{Typeflag: tar.TypeDir, Name: "dir/", Mode: 0o555}); err != nil {
return err
}
return write("dir/run.sh", 0o755, "")(tw)
}))
t.Cleanup(func() { require.NoError(t, os.Chmod(filepath.Join(dest, "dir"), 0o755)) })
for name, mode := range map[string]os.FileMode{"dir": 0o555, filepath.Join("dir", "run.sh"): 0o755} {
info, err := os.Stat(filepath.Join(dest, name))
require.NoError(t, err)
assert.Equal(t, mode, info.Mode().Perm(), name)
}
require.NoError(t, os.Symlink(t.TempDir(), filepath.Join(dest, "link")))
require.ErrorContains(t, e.CopyTarStream(t.Context(), dest, write("link/x", 0o644, "x")), "escapes")
}
require.ErrorContains(t, e.CopyTarStream(t.Context(), dest, write("../escape", 0o644, "x")), "escapes")
assert.NoFileExists(t, filepath.Join(filepath.Dir(dest), "escape"))
require.Error(t, e.CopyTarStream(t.Context(), dest, func(_tw *TarWriter) error { return fmt.Errorf("read body: %w", io.EOF) }))
canceled, cancel := context.WithCancel(t.Context())
cancel()
require.ErrorIs(t, e.CopyTarStream(canceled, dest, write("late", 0o644, "x")), context.Canceled)
assert.NoFileExists(t, filepath.Join(dest, "late"))
}
func TestHostEnvironmentExecExitCode(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {
t.Skip("uses POSIX shell")
}
for _, read := range []bool{true, false} {
output, err := (&HostEnvironment{AllocatePTY: true}).ExecOutput(t.Context(), []string{"sh", "-c", `test ! -t 1; printf 'out\000'; printf ' failed \n' >&2; exit 3`})
require.NoError(t, err)
if read {
content, err := io.ReadAll(output)
var exit ExitCodeError
require.ErrorAs(t, err, &exit)
assert.Equal(t, ExitCodeError(3), exit)
assert.Equal(t, "out\x00", string(content))
assert.Equal(t, "Process completed with exit code 3.: failed", err.Error())
}
var exit ExitCodeError
require.ErrorAs(t, output.Close(), &exit)
assert.Equal(t, ExitCodeError(3), exit)
}
for name, allocatePTY := range map[string]bool{"without PTY": false, "with PTY": true} {
t.Run(name, func(t *testing.T) {
t.Parallel()
+10 -14
View File
@@ -553,10 +553,6 @@ func (c *kubernetesContainer) Copy(destPath string, files ...*FileEntry) common.
}
}
func (c *kubernetesContainer) CopyTarStream(ctx context.Context, destPath string, write func(*TarWriter) error) error {
return c.extract(ctx, destPath, true, func(writer io.Writer) error { return writeTarStream(writer, 0, 0, "", nil, write) }) // streams carry large content like artifacts
}
func (c *kubernetesContainer) CopyDir(destPath, srcPath string, useGitIgnore, skipGitDir bool) common.Executor {
return func(ctx context.Context) error {
return c.extract(ctx, destPath, true, func(writer io.Writer) error { // directories are actions and workspaces, megabytes of compressible sources
@@ -565,17 +561,17 @@ func (c *kubernetesContainer) CopyDir(destPath, srcPath string, useGitIgnore, sk
}
}
func (c *kubernetesContainer) ExecOutput(ctx context.Context, command []string) (io.ReadCloser, error) {
return streamOutput(ctx, func(ctx context.Context, stdout, stderr io.Writer) error {
return c.pod.client.exec(ctx, c.pod.name, c.name, command, nil, stdout, stderr)
}), nil
}
// GetContainerArchive streams the archive, checking the path first as tar writes an empty archive for a missing one.
func (c *kubernetesContainer) GetContainerArchive(ctx context.Context, srcPath string) (io.ReadCloser, error) {
reader, err := c.ExecOutput(ctx, []string{"sh", "-c", `test -e "$1" -o -L "$1" && exec tar -cf - -C "$2" "$3"`, "sh", srcPath, path.Dir(srcPath), path.Base(srcPath)})
if err != nil {
return nil, err
}
reader, writer := io.Pipe()
go func() {
var stderr bytes.Buffer
err := c.pod.client.exec(ctx, c.pod.name, c.name, []string{"sh", "-c", `test -e "$1" -o -L "$1" && exec tar -cf - -C "$2" "$3"`, "sh", srcPath, path.Dir(srcPath), path.Base(srcPath)}, nil, writer, &stderr)
if err != nil {
err = fmt.Errorf("archive %s: %w: %s", srcPath, err, strings.TrimSpace(stderr.String()))
}
writer.CloseWithError(err)
}()
return startedArchive(reader)
}
+1 -24
View File
@@ -273,17 +273,6 @@ func TestKubernetesPodRunsJobThroughAPI(t *testing.T) {
assert.Equal(t, "hunter2\nit's\nme\n5\n"+workdir+"\n", jobLog.String())
assert.Equal(t, ExitCodeError(3), job.Exec([]string{"sh", "-c", "echo failing >&2; exit 3"}, nil, "", "")(ctx))
assert.Contains(t, jobLog.String(), "failing\n")
output, err := job.ExecOutput(ctx, []string{"sh", "-c", `printf 'out\000'; printf ' failed \n' >&2; exit 3`})
require.NoError(t, err)
content, err := io.ReadAll(output)
var exit ExitCodeError
require.ErrorAs(t, err, &exit)
assert.Equal(t, ExitCodeError(3), exit)
assert.Contains(t, err.Error(), ": failed")
assert.Equal(t, "out\x00", string(content))
require.ErrorAs(t, output.Close(), &exit)
assert.Equal(t, ExitCodeError(3), exit)
for _, query := range cluster.requests["exec"] {
assert.NotContains(t, query, "hunter2")
}
@@ -296,7 +285,7 @@ func TestKubernetesPodRunsJobThroughAPI(t *testing.T) {
reader := tar.NewReader(archive)
header, err := reader.Next()
require.NoError(t, err)
content, err = io.ReadAll(reader)
content, err := io.ReadAll(reader)
require.NoError(t, err)
assert.Equal(t, "event.json", header.Name)
assert.JSONEq(t, `{"ref":"main"}`, string(content))
@@ -320,18 +309,6 @@ func TestKubernetesPodRunsJobThroughAPI(t *testing.T) {
assert.Contains(t, lastExec(), "command="+url.QueryEscape(workdir+"/act/actions/sha/"))
assert.Contains(t, lastExec(), url.QueryEscape("gzip -dc |"))
require.NoError(t, job.CopyTarStream(ctx, workdir+"/download/new", func(tw *TarWriter) error {
if err := tw.WriteHeader(&tar.Header{Typeflag: tar.TypeReg, Name: "dir/data.txt", Mode: 0o644, Size: 7}); err != nil {
return err
}
_, err := tw.Write([]byte("payload"))
return err
}))
downloaded, err := os.ReadFile(workdir + "/download/new/dir/data.txt")
require.NoError(t, err)
assert.Equal(t, "payload", string(downloaded))
assert.Contains(t, lastExec(), url.QueryEscape("gzip -dc |"))
pathEnv := map[string]string{}
require.NoError(t, job.UpdateFromImageEnv(&pathEnv)(ctx))
assert.Equal(t, path, pathEnv["PATH"])
+1 -70
View File
@@ -8,11 +8,7 @@ import (
"archive/tar"
"bufio"
"context"
"errors"
"fmt"
"io"
"io/fs"
"path"
"path/filepath"
"strings"
@@ -20,73 +16,8 @@ import (
"gitea.com/gitea/runner/act/filecollector"
)
// TarWriter takes directories and files named relative to the destination, owned by the job user.
type TarWriter struct {
writer *tar.Writer
uid, gid int
prefix string
dirs map[string]bool
}
func (w *TarWriter) Write(data []byte) (int, error) {
return w.writer.Write(data)
}
func (w *TarWriter) WriteHeader(hdr *tar.Header) error {
name := strings.TrimSuffix(hdr.Name, "/")
if !fs.ValidPath(name) {
return fmt.Errorf("tar entry %q escapes the destination", hdr.Name)
}
if hdr.Typeflag != tar.TypeDir && hdr.Typeflag != tar.TypeReg {
return fmt.Errorf("tar entry %q: unsupported type %q", hdr.Name, hdr.Typeflag)
}
for index := range len(name) {
if dir := name[:index]; name[index] == '/' && !w.dirs[dir] {
if err := w.write(&tar.Header{Typeflag: tar.TypeDir, Name: path.Join(w.prefix, dir), Mode: 0o755}); err != nil {
return err
}
w.dirs[dir] = true
}
}
if hdr.Typeflag == tar.TypeDir {
w.dirs[name] = true
}
entry := *hdr
entry.Name = path.Join(w.prefix, name)
return w.write(&entry)
}
func (w *TarWriter) write(hdr *tar.Header) error {
hdr.Uid, hdr.Gid, hdr.Uname, hdr.Gname = w.uid, w.gid, "", "" // tar running as root prefers names over ids
if hdr.Typeflag == tar.TypeDir {
hdr.Name += "/"
}
return w.writer.WriteHeader(hdr)
}
func writeTarStream(dst io.Writer, uid, gid int, prefix string, parents []string, write func(*TarWriter) error) error {
tw := &TarWriter{writer: tar.NewWriter(dst), uid: uid, gid: gid, prefix: prefix, dirs: map[string]bool{}}
for _, name := range parents {
if err := tw.write(&tar.Header{Typeflag: tar.TypeDir, Name: name, Mode: 0o755}); err != nil {
return err
}
}
if err := write(tw); errors.Is(err, io.EOF) {
return fmt.Errorf("tar stream: %v", err) // %v, as a wrapped io.EOF reads as the archive's end
} else if err != nil {
return err
}
return tw.writer.Close()
}
func pipeTar(uid, gid int, prefix string, parents []string, write func(*TarWriter) error) *io.PipeReader {
reader, writer := io.Pipe()
go func() { writer.CloseWithError(writeTarStream(writer, uid, gid, prefix, parents, write)) }()
return reader
}
// startedArchive returns reader once its first byte arrives, so failing to start archiving errors here, not on read.
func startedArchive(reader io.ReadCloser) (io.ReadCloser, error) {
func startedArchive(reader *io.PipeReader) (io.ReadCloser, error) {
buffered := bufio.NewReader(reader)
if _, err := buffered.Peek(1); err != nil {
reader.Close()