├── Dockerfile ├── README.md ├── action.yml ├── assets └── release.png └── entrypoint.go /Dockerfile: -------------------------------------------------------------------------------- 1 | # Debian GNU/Linux 10 (1.13.10-buster) 2 | FROM golang:1.16-buster 3 | 4 | # copy entrypoint file 5 | COPY entrypoint.go /usr/bin/entrypoint.go 6 | 7 | # change mode of the entrypoint file 8 | RUN chmod +x /usr/bin/entrypoint.go 9 | 10 | # set entrypoint command 11 | ENTRYPOINT [ "go", "run", "/usr/bin/entrypoint.go" ] 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitHub Action: go-build-action 2 | This actions generates cross-platform executable files from a Go module. 3 | 4 | ![release](/assets/release.png) 5 | > Automatic release management of the [**tree**](https://github.com/thatisuday/tree/releases) CLI tool using **go-build-action** action. 6 | 7 | 8 | ## Workflow setup 9 | 10 | ```yaml 11 | # workflow name 12 | name: Generate release-artifacts 13 | 14 | # on events 15 | on: 16 | release: 17 | types: 18 | - created 19 | 20 | # workflow tasks 21 | jobs: 22 | generate: 23 | name: Generate cross-platform builds 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout the repository 27 | uses: actions/checkout@v2 28 | - name: Generate build files 29 | uses: thatisuday/go-cross-build@v1 30 | with: 31 | platforms: 'linux/amd64, darwin/amd64, windows/amd64' 32 | package: 'demo' 33 | name: 'program' 34 | compress: 'true' 35 | dest: 'dist' 36 | ``` 37 | 38 | #### ☉ option: **platforms** 39 | The `platforms` option specifies comma-separated platform names to create binary-executable files for. To see the list of supported platforms, use `go tool dist list` command. 40 | 41 | #### ☉ option: **package** 42 | If the module (_repository_) itself is a Go package, then `package` option value should be an empty string (''). If the repository contains a package directory, then `package` value should be the directory name. 43 | 44 | #### ☉ option: **compress** 45 | The `compress` option if set to `'true'` will generate **compressed-tar** archive files for the each platform-build file. The resulting archive file also contains `README.md` and `LICENSE` file if they exist inside the root of the repository. In this mode, the binary executable file name is taken from the `name` option value. 46 | 47 | #### ☉ option: **name** 48 | The `name` option sets a prefix for the build filenames. In compression mode, this prefix is applied to archive files and binary executable filename is set to this value. 49 | 50 | #### ☉ option: **dest** 51 | The `dest` option sets the output directory for the build files. This should be a relative directory without leading `./`. 52 | 53 | 54 | ## Build Artifacts 55 | This action produces following build-artifacts. 56 | 57 | #### In non-compression mode 58 | ``` 59 | .// 60 | ├── -darwin-amd64 61 | ├── -linux-amd64 62 | ├── ... 63 | └── -windows-amd64.exe 64 | ``` 65 | 66 | #### In compression mode 67 | ``` 68 | .// 69 | ├── -darwin-amd64.tar.gz 70 | | ├── 71 | | ├── LICENSE 72 | | └── README.md 73 | ├── -linux-amd64.tar.gz 74 | | ├── 75 | | ├── LICENSE 76 | | └── README.md 77 | ├── ... 78 | └── -windows-amd64.tar.gz 79 | ├── .exe 80 | ├── LICENSE 81 | └── README.md 82 | ``` 83 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | # action name 2 | name: 'go-cross-build' 3 | 4 | # action author 5 | author: 'Uday Hiwarale ' 6 | 7 | # action description 8 | description: 'Generates cross-platform executable files from a Go module.' 9 | 10 | # action input values 11 | inputs: 12 | platforms: 13 | description: 'Comma-separated list of "/" combinations.' 14 | default: 'linux/386,linux/amd64' 15 | required: false 16 | package: 17 | description: 'Package (directory) in the module to build. By default, builds the module directory.' 18 | default: '' 19 | required: false 20 | compress: 21 | description: 'Compress each build file inside a ".tar.gz" archive.' 22 | default: 'false' 23 | required: false 24 | name: 25 | description: 'Binary executable filename and filenames prefix for the build files.' 26 | default: 'program' 27 | required: false 28 | dest: 29 | description: 'Destination directory inside workspace to output build-artifacts.' 30 | default: 'build' 31 | required: false 32 | ldflags: 33 | description: 'Flags to pass to the Go linker.' 34 | default: '' 35 | required: false 36 | 37 | # action runner (golang:latest image) 38 | runs: 39 | using: 'docker' 40 | image: 'Dockerfile' 41 | env: 42 | GO111MODULE: 'on' 43 | 44 | # branding 45 | branding: 46 | icon: terminal 47 | color: green 48 | -------------------------------------------------------------------------------- /assets/release.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thatisuday/go-cross-build/20b61c9d20cc8536ab75030b19d42c46cacb4d1d/assets/release.png -------------------------------------------------------------------------------- /entrypoint.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "path/filepath" 8 | "strings" 9 | ) 10 | 11 | /*************************************/ 12 | 13 | // check if file exists (relative to the current directory) 14 | func fileExists(path string) bool { 15 | 16 | // get current directory 17 | cwd, _ := os.Getwd() 18 | 19 | // get an absolute path of the file 20 | absPath := filepath.Join(cwd, path) 21 | 22 | // access file information 23 | if _, err := os.Stat(absPath); err != nil { 24 | return !os.IsNotExist(err) // return `false` if doesn't exist 25 | } 26 | 27 | // file exists 28 | return true 29 | } 30 | 31 | // copy file using `cp` command 32 | func copyFile(src, dest string) { 33 | if err := exec.Command("cp", src, dest).Run(); err != nil { 34 | fmt.Println("An error occurred during copy operation:", src, "=>", dest) 35 | os.Exit(1) 36 | } 37 | } 38 | 39 | /*************************************/ 40 | 41 | // build the package for a platform 42 | func build(packageName, destDir string, platform map[string]string, ldflags string, compress bool) { 43 | 44 | // platform config 45 | platformKernel := platform["kernel"] 46 | platformArch := platform["arch"] 47 | 48 | // binary executable file path 49 | inputName := os.Getenv("INPUT_NAME") 50 | 51 | // build file name (same as the `inputName` if compression is enabled) 52 | buildFileName := fmt.Sprintf("%s-%s-%s", inputName, platformKernel, platformArch) 53 | if compress { 54 | buildFileName = inputName 55 | } 56 | 57 | // append `.exe` file-extension for windows 58 | if platformKernel == "windows" { 59 | buildFileName += ".exe" 60 | } 61 | 62 | // workspace directory 63 | workspaceDir := os.Getenv("GITHUB_WORKSPACE") 64 | 65 | // destination directory path 66 | destDirPath := filepath.Join(workspaceDir, destDir) 67 | 68 | // join destination path 69 | buildFilePath := filepath.Join(destDirPath, buildFileName) 70 | 71 | // package directory local path 72 | var packagePath string 73 | if packageName == "" { 74 | packagePath = "." 75 | } else { 76 | packagePath = "./" + packageName 77 | } 78 | 79 | /*------------*/ 80 | 81 | // command-line options for the `go build` command 82 | buildOptions := []string{"build", "-buildmode", "exe", "-ldflags", ldflags, "-o", buildFilePath, packagePath} 83 | 84 | // generate `go build` command 85 | buildCmd := exec.Command("go", buildOptions...) 86 | 87 | // set environment variables 88 | buildCmd.Env = append(os.Environ(), []string{ 89 | fmt.Sprintf("GOOS=%s", platformKernel), 90 | fmt.Sprintf("GOARCH=%s", platformArch), 91 | }...) 92 | 93 | // execute `go build` command 94 | fmt.Println("Creating a build using :", buildCmd.String()) 95 | if output, err := buildCmd.Output(); err != nil { 96 | fmt.Println("An error occurred during build:", err) 97 | os.Exit(1) 98 | } else { 99 | fmt.Printf("%s\n", output) 100 | } 101 | 102 | /*------------------------------*/ 103 | 104 | // create a compressed `.tar.gz` file 105 | if compress { 106 | 107 | // compressed gzip file name 108 | gzFileName := fmt.Sprintf("%s-%s-%s.tar.gz", inputName, platformKernel, platformArch) 109 | 110 | /*------------*/ 111 | 112 | // file to compress (default: build file) 113 | includeFiles := []string{buildFileName} 114 | 115 | // copy "README.md" file inside destination directory 116 | if fileExists("README.md") { 117 | copyFile("README.md", filepath.Join(destDirPath, "README.md")) 118 | includeFiles = append(includeFiles, "README.md") 119 | } 120 | 121 | // copy "LICENSE" file inside destination directory 122 | if fileExists("LICENSE") { 123 | copyFile("LICENSE", filepath.Join(destDirPath, "LICENSE")) 124 | includeFiles = append(includeFiles, "LICENSE") 125 | } 126 | 127 | /*------------*/ 128 | 129 | // command-line options for the `tar` command 130 | tarOptions := append([]string{"-cvzf", gzFileName}, includeFiles...) 131 | 132 | // generate `tar` command 133 | tarCmd := exec.Command("tar", tarOptions...) 134 | 135 | // set working directory for the command 136 | tarCmd.Dir = destDirPath 137 | 138 | // execute `tar` command 139 | fmt.Println("Compressing build file using:", tarCmd.String()) 140 | if err := tarCmd.Run(); err != nil { 141 | fmt.Println("An error occurred during compression:", err) 142 | os.Exit(1) 143 | } 144 | 145 | /*------------*/ 146 | 147 | // generate cleanup command 148 | cleanCmd := exec.Command("rm", append([]string{"-f"}, includeFiles...)...) 149 | 150 | // set working directory for the command 151 | cleanCmd.Dir = destDirPath 152 | 153 | // start cleanup process 154 | fmt.Println("Performing cleanup operation using:", cleanCmd.String()) 155 | if err := cleanCmd.Run(); err != nil { 156 | fmt.Println("An error occurred during cleaup:", err) 157 | os.Exit(1) 158 | } 159 | 160 | } 161 | } 162 | 163 | /*************************************/ 164 | 165 | func main() { 166 | 167 | // get input variables from action 168 | inputPlatforms := os.Getenv("INPUT_PLATFORMS") 169 | inputPackage := os.Getenv("INPUT_PACKAGE") 170 | inputCompress := os.Getenv("INPUT_COMPRESS") 171 | inputDest := os.Getenv("INPUT_DEST") 172 | inputLdflags := os.Getenv("INPUT_LDFLAGS") 173 | 174 | // package name to build 175 | packageName := strings.ReplaceAll(inputPackage, " ", "") 176 | 177 | // destination directory 178 | destDir := strings.ReplaceAll(inputDest, " ", "") 179 | 180 | // split platform names by comma (`,`) 181 | platforms := strings.Split(inputPlatforms, ",") 182 | 183 | // should compress build file 184 | compress := false 185 | if strings.ToLower(inputCompress) == "true" { 186 | compress = true 187 | } 188 | 189 | // for each platform, execute `build` function 190 | for _, platform := range platforms { 191 | 192 | // split platform by `/` (and clean all whitespaces) 193 | platformSpec := strings.Split(strings.ReplaceAll(platform, " ", ""), "/") 194 | 195 | // create a `map` of `kernel` and `arch` 196 | platformMap := map[string]string{ 197 | "kernel": platformSpec[0], 198 | "arch": platformSpec[1], 199 | } 200 | 201 | // execute `build` function 202 | build(packageName, destDir, platformMap, inputLdflags, compress) 203 | } 204 | 205 | /*------------*/ 206 | 207 | // list files inside destination directory 208 | if output, err := exec.Command("ls", "-alh", destDir).Output(); err != nil { 209 | fmt.Println("An error occurred during ls operation:", err) 210 | os.Exit(1) 211 | } else { 212 | fmt.Println("--- BUILD FILES ---") 213 | fmt.Printf("%s\n", output) 214 | } 215 | 216 | } 217 | --------------------------------------------------------------------------------