-
Notifications
You must be signed in to change notification settings - Fork 10
/
Copy pathgoaction.go
271 lines (226 loc) · 10 KB
/
goaction.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
/*
Package goaction enables writing Github Actions in Go.
The idea is: write a standard Go script, one that works with `go run`, and use it as Github action.
The script's inputs - flags and environment variables, are set though the Github action API. This
project will generate all the required files for the script (This generation can be done
automattically with Github action integration). The library also exposes neat API to get workflow
information.
Required Steps
- [x] Write a Go script.
- [x] Add `goaction` configuration in `.github/workflows/goaction.yml`.
- [x] Push the project to Github.
See simplest example for a Goaction script: (posener/goaction-example) https://github.com/posener/goaction-example,
or an example that demonstrait using Github APIs: (posener/goaction-issues-example) https://github.com/posener/goaction-issues-example.
Writing a Goaction Script
Write Github Action by writing Go code! Just start a Go module with a main package, and execute it
as a Github action using Goaction, or from the command line using `go run`.
A go executable can get inputs from the command line flag and from environment variables. Github
actions should have a `action.yml` file that defines this API. Goaction bridges the gap by parsing
the Go code and creating this file automatically for you.
The main package inputs should be defined with the standard `flag` package for command line
arguments, or by `os.Getenv` for environment variables. These inputs define the API of the program
and `goaction` automatically detect them and creates the `action.yml` file from them.
Additionally, goaction also provides a library that exposes all Github action environment in an
easy-to-use API. See the documentation for more information.
Code segments which should run only in Github action (called "CI mode"), and not when the main
package runs as a command line tool, should be protected by a `if goaction.CI { ... }` block.
Goaction Configuration
In order to convert the repository to a Github action, goaction command line should run on the
**"main file"** (described above). This command can run manually (by ./cmd/goaction) but luckily
`goaction` also comes as a Github action :-)
Goaction Github action keeps the Github action file updated according to the main Go file
automatically. When a PR is made, goaction will post a review explaining what changes to expect.
When a new commit is pushed, Goaction makes sure that the Github action files are updated if needed.
Add the following content to `.github/workflows/goaction.yml`
on:
pull_request:
branches: [main]
push:
branches: [main]
permissions:
# Goaction needs permissions to update pull requests comments and update contents.
pull-requests: write
contents: write
jobs:
goaction:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v2
- name: Update action files
uses: posener/goaction@v1
with:
# Optional: required only for commenting on PRs.
github-token: '${{ secrets.GITHUB_TOKEN }}'
# Optional: now that the script is a Github action, it is possible to run it in the
# workflow.
- name: Example
uses: ./
Goaction Artifacts
./action.yml: A "metadata" file for Github actions. If this file exists, the repository is
considered as Github action, and the file contains information that instructs how to invoke this
action. See (metadata syntax) https://help.github.com/en/actions/building-actions/metadata-syntax-for-github-actions.
for more info.
./Dockerfile: A file that contains instructions how to build a container, that is used for Github
actions. Github action uses this file in order to create a container image to the action. The
container can also be built and tested manually:
$ docker build -t my-action .
$ docker run --rm my-action
Annotations
Goaction parses Go script file and looks for annotations that extends the information that exists in
the function calls. Goaction annotations are a comments that start with `//goaction:` (no space
after slashes). They can only be set on a `var` definition. The following annotations are available:
* `//goaction:required` - sets an input definition to be "required".
* `//goaction:skip` - skips an input out output definition.
* `//goaction:description <description>` - add description for `os.Getenv`.
* `//goaction:default <value>` - add default value for `os.Getenv`.
Using Goaction
A list of projects which are using Goaction (please send a PR if your project uses goaction and does
not appear her).
* (posener/goreadme) http://github.com/posener/goreadme
*/
package goaction
import (
"fmt"
"log"
"os"
"strconv"
"strings"
)
// Github actions default environment variables.
// See https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables#default-environment-variables
var (
// CI is set to true when running under github action
//
// This variable can be used to protect code segments which should only run in Github action
// mode and not in command line mode:
//
// if goaction.CI {
// // Code that should run only in Github action mode.
// }
CI = os.Getenv("CI") == "true"
// The path to the GitHub home directory used to store user data. For example, /github/home.
Home = os.Getenv("HOME")
// The name of the workflow
Workflow = os.Getenv("GITHUB_WORKFLOW")
// A unique number for each run within a repository. This number does not change if you re-run
// the workflow run.
RunID = os.Getenv("GITHUB_RUN_ID")
// A unique number for each run of a particular workflow in a repository. This number begins at
// 1 for the workflow's first run, and increments with each new run. This number does not change
// if you re-run the workflow run.
RunNum = os.Getenv("GITHUB_RUN_NUMBER")
// The unique identifier (id) of the action.
ActionID = os.Getenv("GITHUB_ACTION")
// The name of the person or app that initiated the workflow. For example, octocat.
Actor = os.Getenv("GITHUB_ACTOR")
// The owner and repository name. For example, octocat/Hello-World.
Repository = os.Getenv("GITHUB_REPOSITORY")
// The name of the webhook event that triggered the workflow.
Event = EventType(os.Getenv("GITHUB_EVENT_NAME"))
// The GitHub workspace directory path. The workspace directory contains a subdirectory with a
// copy of your repository if your workflow uses the actions/checkout action. If you don't use
// the actions/checkout action, the directory will be empty. For example,
// /home/runner/work/my-repo-name/my-repo-name.
Workspace = os.Getenv("GITHUB_WORKSPACE")
// The commit SHA that triggered the workflow. For example,
// ffac537e6cbbf934b08745a378932722df287a53.
SHA = os.Getenv("GITHUB_SHA")
// The branch or tag ref that triggered the workflow. For example, refs/heads/feature-branch-1.
// If neither a branch or tag is available for the event type, the variable will not exist.
Ref = os.Getenv("GITHUB_REF")
// Only set for forked repositories. The branch of the head repository.
ForkedHeadRef = os.Getenv("GITHUB_HEAD_REF")
// Only set for forked repositories. The branch of the base repository.
ForkedBaseRef = os.Getenv("GITHUB_BASE_REF")
eventPath = os.Getenv("GITHUB_EVENT_PATH")
envPath = os.Getenv("GITHUB_ENV")
repoParts = strings.Split(Repository, "/")
)
func init() {
if CI {
// Set the default logging to stdout since Github actions treats stderr as error level logs.
log.SetOutput(os.Stdout)
}
}
// Setenv sets an environment variable that will only be visible for all following Github actions in
// the current workflow, but not in the current action.
// See https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#environment-files.
func Setenv(name string, value string) error {
if !CI {
return nil
}
// Store in the given environment variable name such that programs that expect this environment
// variable (not through goaction) can get it.
f, err := os.OpenFile(envPath, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0)
if err != nil {
return fmt.Errorf("can't open env file %s: %s", envPath, err)
}
defer f.Close()
_, err = fmt.Fprintf(f, "%s=%s\n", name, value)
if err != nil {
return fmt.Errorf("failed writing to env file: %s", err)
}
return nil
}
// Export sets an environment variable that will also be visible for all following Github actions in
// the current workflow.
func Export(name string, value string) error {
err := os.Setenv(name, value)
if err != nil {
return err
}
return Setenv(name, value)
}
// Output sets Github action output.
// See https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-output-parameter.
func Output(name string, value string, desc string) {
if !CI {
return
}
fmt.Printf("::set-output name=%s::%s\n", name, value)
}
// AddPath prepends a directory to the system PATH variable for all subsequent actions in the
// current job. The currently running action cannot access the new path variable.
// See https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#adding-a-system-path.
func AddPath(path string) {
if !CI {
return
}
fmt.Printf("::add-path::%s\n", path)
}
// Owner returns the name of the owner of the Github repository.
func Owner() string {
if len(repoParts) < 2 {
return ""
}
return repoParts[0]
}
// Project returns the name of the project of the Github repository.
func Project() string {
if len(repoParts) < 2 {
return ""
}
return repoParts[1]
}
// Branch returns the push branch for push flow or empty string for other flows.
func Branch() string {
return strings.Split(Ref, "/")[2]
}
// PrNum returns pull request number for PR flow or -1 in other flows.
func PrNum() int {
if Event == EventPullRequest {
// Ref is in the form: "refs/pull/:prNumber/merge"
// See https://help.github.com/en/actions/reference/events-that-trigger-workflows#pull-request-event-pull_request
num, err := strconv.Atoi(strings.Split(Ref, "/")[2])
if err != nil {
panic(err) // Should not happen
}
return num
}
return -1
}
// IsForked return true if the action is running on a forked repository.
func IsForked() bool {
return ForkedBaseRef != ""
}