diff --git a/README.md b/README.md index de48348..2686b90 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,9 @@ It will save that information to `~/.actrc`, please refer to [Configuration](#co ```none -a, --actor string user that triggered the event (default "nektos/act") + --artifact-server-path string Defines the path where the artifact server stores uploads and retrieves downloads from. + If not specified the artifact server will not start. + --artifact-server-port string Defines the port where the artifact server listens (will only bind to localhost). (default "34567") -b, --bind bind working directory to container, rather than copy --container-architecture string Architecture which should be used to run containers, e.g.: linux/amd64. If not specified, will use host default architecture. Requires Docker server API Version 1.41+. Ignored on earlier Docker server platforms. --container-daemon-socket string Path to Docker daemon socket which will be mounted to containers (default "/var/run/docker.sock") diff --git a/cmd/input.go b/cmd/input.go index 164d848..7e52884 100644 --- a/cmd/input.go +++ b/cmd/input.go @@ -35,6 +35,8 @@ type Input struct { containerCapAdd []string containerCapDrop []string autoRemove bool + artifactServerPath string + artifactServerPort string } func (i *Input) resolve(path string) string { diff --git a/cmd/root.go b/cmd/root.go index 07dce50..31686ef 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -17,6 +17,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "github.com/nektos/act/pkg/artifacts" "github.com/nektos/act/pkg/common" "github.com/nektos/act/pkg/model" "github.com/nektos/act/pkg/runner" @@ -66,6 +67,8 @@ func Execute(ctx context.Context, version string) { rootCmd.PersistentFlags().StringVarP(&input.containerArchitecture, "container-architecture", "", "", "Architecture which should be used to run containers, e.g.: linux/amd64. If not specified, will use host default architecture. Requires Docker server API Version 1.41+. Ignored on earlier Docker server platforms.") rootCmd.PersistentFlags().StringVarP(&input.containerDaemonSocket, "container-daemon-socket", "", "/var/run/docker.sock", "Path to Docker daemon socket which will be mounted to containers") rootCmd.PersistentFlags().StringVarP(&input.githubInstance, "github-instance", "", "github.com", "GitHub instance to use. Don't use this if you are not using GitHub Enterprise Server.") + rootCmd.PersistentFlags().StringVarP(&input.artifactServerPath, "artifact-server-path", "", "", "Defines the path where the artifact server stores uploads and retrieves downloads from. If not specified the artifact server will not start.") + rootCmd.PersistentFlags().StringVarP(&input.artifactServerPort, "artifact-server-port", "", "34567", "Defines the port where the artifact server listens (will only bind to localhost).") rootCmd.SetArgs(args()) if err := rootCmd.Execute(); err != nil { @@ -274,12 +277,16 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str ContainerCapAdd: input.containerCapAdd, ContainerCapDrop: input.containerCapDrop, AutoRemove: input.autoRemove, + ArtifactServerPath: input.artifactServerPath, + ArtifactServerPort: input.artifactServerPort, } r, err := runner.New(config) if err != nil { return err } + cancel := artifacts.Serve(ctx, input.artifactServerPath, input.artifactServerPort) + ctx = common.WithDryrun(ctx, input.dryrun) if watch, err := cmd.Flags().GetBool("watch"); err != nil { return err @@ -287,7 +294,11 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str return watchAndRun(ctx, r.NewPlanExecutor(plan)) } - return r.NewPlanExecutor(plan)(ctx) + executor := r.NewPlanExecutor(plan).Finally(func(ctx context.Context) error { + cancel() + return nil + }) + return executor(ctx) } } diff --git a/go.mod b/go.mod index b81f9ad..ecd04e7 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/joho/godotenv v1.4.0 + github.com/julienschmidt/httprouter v1.3.0 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/kevinburke/ssh_config v1.1.0 // indirect github.com/mattn/go-colorable v0.1.9 // indirect diff --git a/go.sum b/go.sum index a5d545e..930a3e2 100644 --- a/go.sum +++ b/go.sum @@ -766,6 +766,8 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= diff --git a/pkg/artifacts/server.go b/pkg/artifacts/server.go new file mode 100644 index 0000000..2d8fe9b --- /dev/null +++ b/pkg/artifacts/server.go @@ -0,0 +1,278 @@ +package artifacts + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "io/fs" + "net/http" + "os" + "path" + "path/filepath" + "strings" + + "github.com/julienschmidt/httprouter" + "github.com/nektos/act/pkg/common" + log "github.com/sirupsen/logrus" +) + +type FileContainerResourceURL struct { + FileContainerResourceURL string `json:"fileContainerResourceUrl"` +} + +type NamedFileContainerResourceURL struct { + Name string `json:"name"` + FileContainerResourceURL string `json:"fileContainerResourceUrl"` +} + +type NamedFileContainerResourceURLResponse struct { + Count int `json:"count"` + Value []NamedFileContainerResourceURL `json:"value"` +} + +type ContainerItem struct { + Path string `json:"path"` + ItemType string `json:"itemType"` + ContentLocation string `json:"contentLocation"` +} + +type ContainerItemResponse struct { + Value []ContainerItem `json:"value"` +} + +type ResponseMessage struct { + Message string `json:"message"` +} + +type MkdirFS interface { + fs.FS + MkdirAll(path string, perm fs.FileMode) error + Open(name string) (fs.File, error) +} + +type MkdirFsImpl struct { + dir string + fs.FS +} + +func (fsys MkdirFsImpl) MkdirAll(path string, perm fs.FileMode) error { + return os.MkdirAll(fsys.dir+"/"+path, perm) +} + +func (fsys MkdirFsImpl) Open(name string) (fs.File, error) { + return os.OpenFile(fsys.dir+"/"+name, os.O_CREATE|os.O_RDWR, 0644) +} + +var gzipExtension = ".gz__" + +func uploads(router *httprouter.Router, fsys MkdirFS) { + router.POST("/_apis/pipelines/workflows/:runId/artifacts", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { + runID := params.ByName("runId") + + json, err := json.Marshal(FileContainerResourceURL{ + FileContainerResourceURL: fmt.Sprintf("http://%s/upload/%s", req.Host, runID), + }) + if err != nil { + panic(err) + } + + _, err = w.Write(json) + if err != nil { + panic(err) + } + }) + + router.PUT("/upload/:runId", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { + itemPath := req.URL.Query().Get("itemPath") + runID := params.ByName("runId") + + if req.Header.Get("Content-Encoding") == "gzip" { + itemPath += gzipExtension + } + + filePath := fmt.Sprintf("%s/%s", runID, itemPath) + + err := fsys.MkdirAll(path.Dir(filePath), os.ModePerm) + if err != nil { + panic(err) + } + + file, err := fsys.Open(filePath) + if err != nil { + panic(err) + } + defer file.Close() + + writer, ok := file.(io.Writer) + if !ok { + panic(errors.New("File is not writable")) + } + + if req.Body == nil { + panic(errors.New("No body given")) + } + + _, err = io.Copy(writer, req.Body) + if err != nil { + panic(err) + } + + json, err := json.Marshal(ResponseMessage{ + Message: "success", + }) + if err != nil { + panic(err) + } + + _, err = w.Write(json) + if err != nil { + panic(err) + } + }) + + router.PATCH("/_apis/pipelines/workflows/:runId/artifacts", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { + json, err := json.Marshal(ResponseMessage{ + Message: "success", + }) + if err != nil { + panic(err) + } + + _, err = w.Write(json) + if err != nil { + panic(err) + } + }) +} + +func downloads(router *httprouter.Router, fsys fs.FS) { + router.GET("/_apis/pipelines/workflows/:runId/artifacts", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { + runID := params.ByName("runId") + + entries, err := fs.ReadDir(fsys, runID) + if err != nil { + panic(err) + } + + var list []NamedFileContainerResourceURL + for _, entry := range entries { + list = append(list, NamedFileContainerResourceURL{ + Name: entry.Name(), + FileContainerResourceURL: fmt.Sprintf("http://%s/download/%s", req.Host, runID), + }) + } + + json, err := json.Marshal(NamedFileContainerResourceURLResponse{ + Count: len(list), + Value: list, + }) + if err != nil { + panic(err) + } + + _, err = w.Write(json) + if err != nil { + panic(err) + } + }) + + router.GET("/download/:container", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { + container := params.ByName("container") + itemPath := req.URL.Query().Get("itemPath") + dirPath := fmt.Sprintf("%s/%s", container, itemPath) + + var files []ContainerItem + err := fs.WalkDir(fsys, dirPath, func(path string, entry fs.DirEntry, err error) error { + if !entry.IsDir() { + rel, err := filepath.Rel(dirPath, path) + if err != nil { + panic(err) + } + + // if it was upload as gzip + rel = strings.TrimSuffix(rel, gzipExtension) + + files = append(files, ContainerItem{ + Path: fmt.Sprintf("%s/%s", itemPath, rel), + ItemType: "file", + ContentLocation: fmt.Sprintf("http://%s/artifact/%s/%s/%s", req.Host, container, itemPath, rel), + }) + } + return nil + }) + if err != nil { + panic(err) + } + + json, err := json.Marshal(ContainerItemResponse{ + Value: files, + }) + if err != nil { + panic(err) + } + + _, err = w.Write(json) + if err != nil { + panic(err) + } + }) + + router.GET("/artifact/*path", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { + path := params.ByName("path")[1:] + + file, err := fsys.Open(path) + if err != nil { + // try gzip file + file, err = fsys.Open(path + gzipExtension) + if err != nil { + panic(err) + } + w.Header().Add("Content-Encoding", "gzip") + } + + _, err = io.Copy(w, file) + if err != nil { + panic(err) + } + }) +} + +func Serve(ctx context.Context, artifactPath string, port string) context.CancelFunc { + serverContext, cancel := context.WithCancel(ctx) + + if artifactPath == "" { + return cancel + } + + router := httprouter.New() + + log.Debugf("Artifacts base path '%s'", artifactPath) + fs := os.DirFS(artifactPath) + uploads(router, MkdirFsImpl{artifactPath, fs}) + downloads(router, fs) + ip := common.GetOutboundIP().String() + + server := &http.Server{Addr: fmt.Sprintf("%s:%s", ip, port), Handler: router} + + // run server + go func() { + log.Infof("Start server on http://%s:%s", ip, port) + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatal(err) + } + }() + + // wait for cancel to gracefully shutdown server + go func() { + <-serverContext.Done() + + if err := server.Shutdown(ctx); err != nil { + log.Errorf("Failed shutdown gracefully - force shutdown: %v", err) + server.Close() + } + }() + + return cancel +} diff --git a/pkg/artifacts/server_test.go b/pkg/artifacts/server_test.go new file mode 100644 index 0000000..76834af --- /dev/null +++ b/pkg/artifacts/server_test.go @@ -0,0 +1,296 @@ +package artifacts + +import ( + "context" + "encoding/json" + "fmt" + "io/fs" + "net/http" + "net/http/httptest" + "os" + "path" + "path/filepath" + "strings" + "testing" + "testing/fstest" + + "github.com/julienschmidt/httprouter" + "github.com/nektos/act/pkg/model" + "github.com/nektos/act/pkg/runner" + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" +) + +type MapFsImpl struct { + fstest.MapFS +} + +func (fsys MapFsImpl) MkdirAll(path string, perm fs.FileMode) error { + // mocked no-op + return nil +} + +type WritableFile struct { + fs.File + fsys fstest.MapFS + path string +} + +func (file WritableFile) Write(data []byte) (int, error) { + file.fsys[file.path].Data = data + return len(data), nil +} + +func (fsys MapFsImpl) Open(path string) (fs.File, error) { + var file = fstest.MapFile{ + Data: []byte("content2"), + } + fsys.MapFS[path] = &file + + result, err := fsys.MapFS.Open(path) + return WritableFile{result, fsys.MapFS, path}, err +} + +func TestNewArtifactUploadPrepare(t *testing.T) { + assert := assert.New(t) + + var memfs = fstest.MapFS(map[string]*fstest.MapFile{}) + + router := httprouter.New() + uploads(router, MapFsImpl{memfs}) + + req, _ := http.NewRequest("POST", "http://localhost/_apis/pipelines/workflows/1/artifacts", nil) + rr := httptest.NewRecorder() + + router.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusOK { + assert.Fail("Wrong status") + } + + response := FileContainerResourceURL{} + err := json.Unmarshal(rr.Body.Bytes(), &response) + if err != nil { + panic(err) + } + + assert.Equal("http://localhost/upload/1", response.FileContainerResourceURL) +} + +func TestArtifactUploadBlob(t *testing.T) { + assert := assert.New(t) + + var memfs = fstest.MapFS(map[string]*fstest.MapFile{}) + + router := httprouter.New() + uploads(router, MapFsImpl{memfs}) + + req, _ := http.NewRequest("PUT", "http://localhost/upload/1?itemPath=some/file", strings.NewReader("content")) + rr := httptest.NewRecorder() + + router.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusOK { + assert.Fail("Wrong status") + } + + response := ResponseMessage{} + err := json.Unmarshal(rr.Body.Bytes(), &response) + if err != nil { + panic(err) + } + + assert.Equal("success", response.Message) + assert.Equal("content", string(memfs["1/some/file"].Data)) +} + +func TestFinalizeArtifactUpload(t *testing.T) { + assert := assert.New(t) + + var memfs = fstest.MapFS(map[string]*fstest.MapFile{}) + + router := httprouter.New() + uploads(router, MapFsImpl{memfs}) + + req, _ := http.NewRequest("PATCH", "http://localhost/_apis/pipelines/workflows/1/artifacts", nil) + rr := httptest.NewRecorder() + + router.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusOK { + assert.Fail("Wrong status") + } + + response := ResponseMessage{} + err := json.Unmarshal(rr.Body.Bytes(), &response) + if err != nil { + panic(err) + } + + assert.Equal("success", response.Message) +} + +func TestListArtifacts(t *testing.T) { + assert := assert.New(t) + + var memfs = fstest.MapFS(map[string]*fstest.MapFile{ + "1/file.txt": { + Data: []byte(""), + }, + }) + + router := httprouter.New() + downloads(router, memfs) + + req, _ := http.NewRequest("GET", "http://localhost/_apis/pipelines/workflows/1/artifacts", nil) + rr := httptest.NewRecorder() + + router.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusOK { + assert.FailNow(fmt.Sprintf("Wrong status: %d", status)) + } + + response := NamedFileContainerResourceURLResponse{} + err := json.Unmarshal(rr.Body.Bytes(), &response) + if err != nil { + panic(err) + } + + assert.Equal(1, response.Count) + assert.Equal("file.txt", response.Value[0].Name) + assert.Equal("http://localhost/download/1", response.Value[0].FileContainerResourceURL) +} + +func TestListArtifactContainer(t *testing.T) { + assert := assert.New(t) + + var memfs = fstest.MapFS(map[string]*fstest.MapFile{ + "1/some/file": { + Data: []byte(""), + }, + }) + + router := httprouter.New() + downloads(router, memfs) + + req, _ := http.NewRequest("GET", "http://localhost/download/1?itemPath=some/file", nil) + rr := httptest.NewRecorder() + + router.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusOK { + assert.FailNow(fmt.Sprintf("Wrong status: %d", status)) + } + + response := ContainerItemResponse{} + err := json.Unmarshal(rr.Body.Bytes(), &response) + if err != nil { + panic(err) + } + + assert.Equal(1, len(response.Value)) + assert.Equal("some/file/.", response.Value[0].Path) + assert.Equal("file", response.Value[0].ItemType) + assert.Equal("http://localhost/artifact/1/some/file/.", response.Value[0].ContentLocation) +} + +func TestDownloadArtifactFile(t *testing.T) { + assert := assert.New(t) + + var memfs = fstest.MapFS(map[string]*fstest.MapFile{ + "1/some/file": { + Data: []byte("content"), + }, + }) + + router := httprouter.New() + downloads(router, memfs) + + req, _ := http.NewRequest("GET", "http://localhost/artifact/1/some/file", nil) + rr := httptest.NewRecorder() + + router.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusOK { + assert.FailNow(fmt.Sprintf("Wrong status: %d", status)) + } + + data := rr.Body.Bytes() + + assert.Equal("content", string(data)) +} + +type TestJobFileInfo struct { + workdir string + workflowPath string + eventName string + errorMessage string + platforms map[string]string + containerArchitecture string +} + +var aritfactsPath = path.Join(os.TempDir(), "test-artifacts") +var artifactsPort = "12345" + +func TestArtifactFlow(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + ctx := context.Background() + + cancel := Serve(ctx, aritfactsPath, artifactsPort) + defer cancel() + + platforms := map[string]string{ + "ubuntu-latest": "node:12.20.1-buster-slim", + } + + tables := []TestJobFileInfo{ + {"testdata", "upload-and-download", "push", "", platforms, ""}, + } + log.SetLevel(log.DebugLevel) + + for _, table := range tables { + runTestJobFile(ctx, t, table) + } +} + +func runTestJobFile(ctx context.Context, t *testing.T, tjfi TestJobFileInfo) { + t.Run(tjfi.workflowPath, func(t *testing.T) { + if err := os.RemoveAll(aritfactsPath); err != nil { + panic(err) + } + + workdir, err := filepath.Abs(tjfi.workdir) + assert.Nil(t, err, workdir) + fullWorkflowPath := filepath.Join(workdir, tjfi.workflowPath) + runnerConfig := &runner.Config{ + Workdir: workdir, + BindWorkdir: false, + EventName: tjfi.eventName, + Platforms: tjfi.platforms, + ReuseContainers: false, + ContainerArchitecture: tjfi.containerArchitecture, + GitHubInstance: "github.com", + ArtifactServerPath: aritfactsPath, + ArtifactServerPort: artifactsPort, + } + + runner, err := runner.New(runnerConfig) + assert.Nil(t, err, tjfi.workflowPath) + + planner, err := model.NewWorkflowPlanner(fullWorkflowPath, true) + assert.Nil(t, err, fullWorkflowPath) + + plan := planner.PlanEvent(tjfi.eventName) + + err = runner.NewPlanExecutor(plan)(ctx) + if tjfi.errorMessage == "" { + assert.Nil(t, err, fullWorkflowPath) + } else { + assert.Error(t, err, tjfi.errorMessage) + } + }) +} diff --git a/pkg/artifacts/testdata/upload-and-download/artifacts.yml b/pkg/artifacts/testdata/upload-and-download/artifacts.yml new file mode 100644 index 0000000..cfebc70 --- /dev/null +++ b/pkg/artifacts/testdata/upload-and-download/artifacts.yml @@ -0,0 +1,140 @@ + +name: "Test that artifact uploads and downloads succeed" +on: push + +jobs: + test-artifacts: + runs-on: ubuntu-latest + steps: + - run: mkdir -p path/to/artifact + - run: echo hello > path/to/artifact/world.txt + - uses: actions/upload-artifact@v2 + with: + name: my-artifact + path: path/to/artifact/world.txt + + - run: rm -rf path + + - uses: actions/download-artifact@v2 + with: + name: my-artifact + - name: Display structure of downloaded files + run: ls -la + + # Test end-to-end by uploading two artifacts and then downloading them + - name: Create artifact files + run: | + mkdir -p path/to/dir-1 + mkdir -p path/to/dir-2 + mkdir -p path/to/dir-3 + echo "Lorem ipsum dolor sit amet" > path/to/dir-1/file1.txt + echo "Hello world from file #2" > path/to/dir-2/file2.txt + echo "This is a going to be a test for a large enough file that should get compressed with GZip. The @actions/artifact package uses GZip to upload files. This text should have a compression ratio greater than 100% so it should get uploaded using GZip" > path/to/dir-3/gzip.txt + # Upload a single file artifact + - name: 'Upload artifact #1' + uses: actions/upload-artifact@v2 + with: + name: 'Artifact-A' + path: path/to/dir-1/file1.txt + + # Upload using a wildcard pattern, name should default to 'artifact' if not provided + - name: 'Upload artifact #2' + uses: actions/upload-artifact@v2 + with: + path: path/**/dir*/ + + # Upload a directory that contains a file that will be uploaded with GZip + - name: 'Upload artifact #3' + uses: actions/upload-artifact@v2 + with: + name: 'GZip-Artifact' + path: path/to/dir-3/ + + # Upload a directory that contains a file that will be uploaded with GZip + - name: 'Upload artifact #4' + uses: actions/upload-artifact@v2 + with: + name: 'Multi-Path-Artifact' + path: | + path/to/dir-1/* + path/to/dir-[23]/* + !path/to/dir-3/*.txt + # Verify artifacts. Switch to download-artifact@v2 once it's out of preview + + # Download Artifact #1 and verify the correctness of the content + - name: 'Download artifact #1' + uses: actions/download-artifact@v2 + with: + name: 'Artifact-A' + path: some/new/path + + - name: 'Verify Artifact #1' + run: | + file="some/new/path/file1.txt" + if [ ! -f $file ] ; then + echo "Expected file does not exist" + exit 1 + fi + if [ "$(cat $file)" != "Lorem ipsum dolor sit amet" ] ; then + echo "File contents of downloaded artifact are incorrect" + exit 1 + fi + + # Download Artifact #2 and verify the correctness of the content + - name: 'Download artifact #2' + uses: actions/download-artifact@v2 + with: + name: 'artifact' + path: some/other/path + + - name: 'Verify Artifact #2' + run: | + file1="some/other/path/to/dir-1/file1.txt" + file2="some/other/path/to/dir-2/file2.txt" + if [ ! -f $file1 -o ! -f $file2 ] ; then + echo "Expected files do not exist" + exit 1 + fi + if [ "$(cat $file1)" != "Lorem ipsum dolor sit amet" -o "$(cat $file2)" != "Hello world from file #2" ] ; then + echo "File contents of downloaded artifacts are incorrect" + exit 1 + fi + + # Download Artifact #3 and verify the correctness of the content + - name: 'Download artifact #3' + uses: actions/download-artifact@v2 + with: + name: 'GZip-Artifact' + path: gzip/artifact/path + + # Because a directory was used as input during the upload the parent directories, path/to/dir-3/, should not be included in the uploaded artifact + - name: 'Verify Artifact #3' + run: | + gzipFile="gzip/artifact/path/gzip.txt" + if [ ! -f $gzipFile ] ; then + echo "Expected file do not exist" + exit 1 + fi + if [ "$(cat $gzipFile)" != "This is a going to be a test for a large enough file that should get compressed with GZip. The @actions/artifact package uses GZip to upload files. This text should have a compression ratio greater than 100% so it should get uploaded using GZip" ] ; then + echo "File contents of downloaded artifact is incorrect" + exit 1 + fi + + - name: 'Download artifact #4' + uses: actions/download-artifact@v2 + with: + name: 'Multi-Path-Artifact' + path: multi/artifact + + - name: 'Verify Artifact #4' + run: | + file1="multi/artifact/dir-1/file1.txt" + file2="multi/artifact/dir-2/file2.txt" + if [ ! -f $file1 -o ! -f $file2 ] ; then + echo "Expected files do not exist" + exit 1 + fi + if [ "$(cat $file1)" != "Lorem ipsum dolor sit amet" -o "$(cat $file2)" != "Hello world from file #2" ] ; then + echo "File contents of downloaded artifacts are incorrect" + exit 1 + fi diff --git a/pkg/common/outbound_ip.go b/pkg/common/outbound_ip.go new file mode 100644 index 0000000..eaa4cce --- /dev/null +++ b/pkg/common/outbound_ip.go @@ -0,0 +1,21 @@ +package common + +import ( + "net" + + log "github.com/sirupsen/logrus" +) + +// https://stackoverflow.com/a/37382208 +// Get preferred outbound ip of this machine +func GetOutboundIP() net.IP { + conn, err := net.Dial("udp", "8.8.8.8:80") + if err != nil { + log.Fatal(err) + } + defer conn.Close() + + localAddr := conn.LocalAddr().(*net.UDPAddr) + + return localAddr.IP +} diff --git a/pkg/runner/run_context.go b/pkg/runner/run_context.go index 8494e26..d7d071e 100755 --- a/pkg/runner/run_context.go +++ b/pkg/runner/run_context.go @@ -733,6 +733,10 @@ func (rc *RunContext) withGithubEnv(env map[string]string) map[string]string { env["GITHUB_GRAPHQL_URL"] = fmt.Sprintf("https://%s/api/graphql", rc.Config.GitHubInstance) } + if rc.Config.ArtifactServerPath != "" { + setActionRuntimeVars(rc, env) + } + job := rc.Run.Job() if job.RunsOn() != nil { for _, runnerLabel := range job.RunsOn() { @@ -752,6 +756,20 @@ func (rc *RunContext) withGithubEnv(env map[string]string) map[string]string { return env } +func setActionRuntimeVars(rc *RunContext, env map[string]string) { + actionsRuntimeURL := os.Getenv("ACTIONS_RUNTIME_URL") + if actionsRuntimeURL == "" { + actionsRuntimeURL = fmt.Sprintf("http://%s:%s/", common.GetOutboundIP().String(), rc.Config.ArtifactServerPort) + } + env["ACTIONS_RUNTIME_URL"] = actionsRuntimeURL + + actionsRuntimeToken := os.Getenv("ACTIONS_RUNTIME_TOKEN") + if actionsRuntimeToken == "" { + actionsRuntimeToken = "token" + } + env["ACTIONS_RUNTIME_TOKEN"] = actionsRuntimeToken +} + func (rc *RunContext) localCheckoutPath() (string, bool) { ghContext := rc.getGithubContext() for _, step := range rc.Run.Job().Steps { diff --git a/pkg/runner/runner.go b/pkg/runner/runner.go index 343a4b7..2525e5e 100644 --- a/pkg/runner/runner.go +++ b/pkg/runner/runner.go @@ -43,6 +43,8 @@ type Config struct { ContainerCapAdd []string // list of kernel capabilities to add to the containers ContainerCapDrop []string // list of kernel capabilities to remove from the containers AutoRemove bool // controls if the container is automatically removed upon workflow completion + ArtifactServerPath string // the path where the artifact server stores uploads + ArtifactServerPort string // the port the artifact server binds to } // Resolves the equivalent host path inside the container diff --git a/pkg/runner/testdata/actions/node12/.gitignore b/pkg/runner/testdata/actions/node12/.gitignore deleted file mode 100644 index 096746c..0000000 --- a/pkg/runner/testdata/actions/node12/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/node_modules/ \ No newline at end of file