Skip to content

Commit fa3127b

Browse files
Move to new actions-oss-runtime (#207)
* Move to new actions-oss-runtime
1 parent 18bf9e0 commit fa3127b

12 files changed

Lines changed: 541 additions & 400 deletions

.golangci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
version: "2"
22
run:
3-
go: "1.21"
3+
go: "1.24"
44
linters:
55
enable:
66
- asciicheck

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@ go run . run
131131
- removed openbsd/mips binaries, because this prevents updates to go and dependencies
132132
- go 1.21 now required
133133

134+
# Breaking changes in 0.11.0
135+
- based on [actions-oss/act-cli@v0.3.4](https://github.com/actions-oss/act-cli/tree/v0.3.4)
136+
- go 1.24 now required
137+
134138
# Known Limitations
135139
- ~~This runner ignores pre and post steps of javascript actions~~ Is now working in 0.6.0
136140
- ~~[actions/cache](https://github.com/actions/cache) is incompatible and won't be able to **save your cache**~~
@@ -150,10 +154,10 @@ go run . run
150154
- Job Outputs are sent regardless if they would leak secret data to non secret storage
151155
- You need to provide the `node` program yourself in all containers / host configurations
152156
- You need to manually update the runner
153-
- Most issues of https://github.com/nektos/act/issues applies to this runner as well
157+
- Most issues of https://github.com/nektos/act/issues and https://github.com/actions-oss/act-cli/issues applies to this runner as well
154158

155159
# How does it work?
156-
This runner implements the same protocol as the [actions/runner](https://github.com/actions/runner) in a different way, as such it can be used as a self-hosted runner exactly like the official one. To get this working, I initially built an actions service replacement [ChristopherHX/runner.server](https://github.com/ChristopherHX/runner.server) for the official [actions/runner](https://github.com/actions/runner). My own actions service allowed me to implement the base protocol for this runner and debug how the protocol is serializeing and parsing json messages, while still being incompatible with github. After testing against github, the first thing happend was loosing the ability to run any github action workflows on my test repository. My invalid attempts to register a custom runner caused unrecoverable Internal Server Errors on githubs side, I decided to delete this test repository. After some work everything worked and finally it is safe to register this runner against github. To execute steps this runner translates the github actions job request to be compatible with a modified version of [nektos/act](https://github.com/nektos/act) ( [ChristopherHX/act](https://github.com/ChristopherHX/act) ), which adds a local task runner without the need for docker and increased platform support, also the log output of act gets redirected to github for live logs and storing log files.
160+
This runner implements the same protocol as the [actions/runner](https://github.com/actions/runner) in a different way, as such it can be used as a self-hosted runner exactly like the official one. To get this working, I initially built an actions service replacement [ChristopherHX/runner.server](https://github.com/ChristopherHX/runner.server) for the official [actions/runner](https://github.com/actions/runner). My own actions service allowed me to implement the base protocol for this runner and debug how the protocol is serializeing and parsing json messages, while still being incompatible with github. After testing against github, the first thing happend was loosing the ability to run any github action workflows on my test repository. My invalid attempts to register a custom runner caused unrecoverable Internal Server Errors on githubs side, I decided to delete this test repository. After some work everything worked and finally it is safe to register this runner against github. To execute steps this runner translates the github actions job request to be compatible with a modified version of [nektos/act](https://github.com/nektos/act) ( [actions-oss/act-cli](https://github.com/actions-oss/act-cli) ), which adds a local task runner without the need for docker and increased platform support, also the log output of act gets redirected to github for live logs and storing log files.
157161

158162
# Does this runner work without github?
159163
Yes, you can use this runner together with [ChristopherHX/runner.server](https://github.com/ChristopherHX/runner.server) locally on your PC without depending on compatibility with github. Also CI tests for this runner are using [ChristopherHX/runner.server](https://github.com/ChristopherHX/runner.server), this avoids requiring a PAT for github to run tests and enshures that you are always able to run it locally without github.

actionsdotnetactcompat/act_worker.go

Lines changed: 70 additions & 172 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,28 @@ package actionsdotnetactcompat
22

33
import (
44
"bytes"
5-
"compress/gzip"
65
"context"
76
"encoding/json"
87
"fmt"
9-
"io"
108
"math"
119
"net/http"
1210
"os"
13-
"path/filepath"
1411
"regexp"
1512
"runtime"
1613
"strings"
1714
"time"
1815

16+
"github.com/actions-oss/act-cli/pkg/common"
17+
"github.com/actions-oss/act-cli/pkg/model"
18+
"github.com/actions-oss/act-cli/pkg/runner"
1919
"github.com/google/uuid"
20-
"github.com/nektos/act/pkg/common"
21-
"github.com/nektos/act/pkg/common/git"
22-
"github.com/nektos/act/pkg/filecollector"
23-
"github.com/nektos/act/pkg/model"
24-
"github.com/nektos/act/pkg/runner"
2520
"github.com/rhysd/actionlint"
2621
"github.com/sirupsen/logrus"
2722
"gopkg.in/yaml.v3"
2823

2924
"github.com/ChristopherHX/github-act-runner/actionsrunner"
3025
rcommon "github.com/ChristopherHX/github-act-runner/common"
3126
"github.com/ChristopherHX/github-act-runner/protocol"
32-
"github.com/ChristopherHX/github-act-runner/protocol/launch"
3327
"github.com/ChristopherHX/github-act-runner/protocol/logger"
3428
)
3529

@@ -163,19 +157,20 @@ func (f *ghaFormatter) Format(entry *logrus.Entry) ([]byte, error) {
163157
_ = f.logger.Update() // Ignore logger update errors
164158
}
165159
}
160+
msg := entry.Message
166161
if f.rqt.MaskHints != nil {
167162
for _, v := range f.rqt.MaskHints {
168163
if strings.EqualFold(v.Type, "regex") {
169164
r, _ := regexp.Compile(v.Value)
170-
entry.Message = r.ReplaceAllString(entry.Message, "***")
165+
msg = r.ReplaceAllString(msg, "***")
171166
}
172167
}
173168
}
174169
if f.rqt.Variables != nil {
175170
for _, v := range f.rqt.Variables {
176171
if v.IsSecret && v.Value != "" && !strings.EqualFold(v.Value, "true") &&
177172
!strings.EqualFold(v.Value, "false") && !strings.EqualFold(v.Value, "0") && !strings.EqualFold(v.Value, "1") {
178-
entry.Message = strings.ReplaceAll(entry.Message, v.Value, "***")
173+
msg = strings.ReplaceAll(msg, v.Value, "***")
179174
}
180175
}
181176
}
@@ -201,9 +196,26 @@ func (f *ghaFormatter) Format(entry *logrus.Entry) ([]byte, error) {
201196
case logrus.TraceLevel:
202197
prefix += debugPrefix
203198
}
204-
entry.Message = f.linefeedregex.ReplaceAllString(prefix+strings.Trim(entry.Message, "\r\n"), "\n"+prefix)
205-
206-
b.WriteString(entry.Message)
199+
command, _ := entry.Data["command"].(string)
200+
arg, _ := entry.Data["arg"].(string)
201+
raw, _ := entry.Data["raw"].(string)
202+
switch strings.ToLower(command) {
203+
case "group":
204+
msg = "##[group]" + arg
205+
case "endgroup":
206+
msg = "##[endgroup]" + arg
207+
case "debug":
208+
msg = arg
209+
case "warning":
210+
msg = arg
211+
case "error":
212+
msg = arg
213+
case "ignored":
214+
msg = raw
215+
}
216+
msg = f.linefeedregex.ReplaceAllString(prefix+strings.Trim(msg, "\r\n"), "\n"+prefix)
217+
218+
b.WriteString(msg)
207219
b.WriteByte('\n')
208220
return b.Bytes(), nil
209221
}
@@ -231,7 +243,7 @@ func ExecWorker(rqt *protocol.AgentJobRequestMessage, wc actionsrunner.WorkerCon
231243
ctx: jobExecCtx,
232244
}
233245
actLogger.SetFormatter(formatter)
234-
actLogger.Println("Initialize translating the job request to nektos/act")
246+
actLogger.Println("Initialize translating the job request to actions-oss/act-cli (nektos/act)")
235247
vssConnection, vssConnectionData, _ := rqt.GetConnection("SystemVssConnection")
236248
if jlogger.Connection != nil {
237249
vssConnection.Client = jlogger.Connection.Client
@@ -345,9 +357,9 @@ func ExecWorker(rqt *protocol.AgentJobRequestMessage, wc actionsrunner.WorkerCon
345357
runnerConfig.LogOutput = true
346358
runnerConfig.EventName = githubCtxMap["event_name"].(string)
347359
runnerConfig.GitHubInstance = "github.com"
348-
runnerConfig.GitHubServerUrl = githubCtxMap["server_url"].(string)
349-
runnerConfig.GitHubApiServerUrl = githubCtxMap["api_url"].(string)
350-
runnerConfig.GitHubGraphQlApiServerUrl = githubCtxMap["graphql_url"].(string)
360+
runnerConfig.GitHubServerURL = githubCtxMap["server_url"].(string)
361+
runnerConfig.GitHubAPIServerURL = githubCtxMap["api_url"].(string)
362+
runnerConfig.GitHubGraphQlAPIServerURL = githubCtxMap["graphql_url"].(string)
351363
runnerConfig.NoSkipCheckout = true // Needed to avoid copy the non exiting working dir
352364
runnerConfig.AutoRemove = true // Needed to cleanup always cleanup container
353365
runnerConfig.ForcePull = true
@@ -363,80 +375,6 @@ func ExecWorker(rqt *protocol.AgentJobRequestMessage, wc actionsrunner.WorkerCon
363375
}
364376
return nil
365377
}
366-
runnerConfig.DownloadAction = func(ngcei git.NewGitCloneExecutorInput) common.Executor {
367-
return func(ctx context.Context) error {
368-
actionList := &protocol.ActionReferenceList{}
369-
actionurl := strings.Split(ngcei.URL, "/")
370-
actionurl = actionurl[len(actionurl)-2:]
371-
actionList.Actions = []protocol.ActionReference{
372-
{NameWithOwner: strings.Join(actionurl, "/"), Ref: ngcei.Ref},
373-
}
374-
actionDownloadInfo := &protocol.ActionDownloadInfoCollection{}
375-
if requestErr := vssConnection.RequestWithContext(ctx, "27d7f831-88c1-4719-8ca1-6a061dad90eb", "6.0-preview", "POST", map[string]string{
376-
"scopeIdentifier": rqt.Plan.ScopeIdentifier,
377-
"hubName": rqt.Plan.PlanType,
378-
"planId": rqt.Plan.PlanID,
379-
}, nil, actionList, actionDownloadInfo); requestErr != nil {
380-
return requestErr
381-
}
382-
for _, v := range actionDownloadInfo.Actions {
383-
token := runnerConfig.Token
384-
if v.Authentication != nil && v.Authentication.Token != "" {
385-
token = v.Authentication.Token
386-
}
387-
downloadErr := downloadAndExtractAction(ctx, ngcei.Dir, actionurl[0], actionurl[1],
388-
v.ResolvedSha, v.TarballURL, token, &downloadActionHTTPClient)
389-
if downloadErr != nil {
390-
return downloadErr
391-
}
392-
}
393-
return nil
394-
}
395-
}
396-
if strings.EqualFold(rqt.MessageType, "RunnerJobRequest") {
397-
runnerConfig.DownloadAction = nil
398-
launchEndpoint, hasLaunchEndpoint := rqt.Variables["system.github.launch_endpoint"]
399-
if hasLaunchEndpoint && launchEndpoint.Value != "" {
400-
runnerConfig.DownloadAction = func(ngcei git.NewGitCloneExecutorInput) common.Executor {
401-
return func(ctx context.Context) error {
402-
actionList := &launch.ActionReferenceRequestList{}
403-
actionurl := strings.Split(ngcei.URL, "/")
404-
actionurl = actionurl[len(actionurl)-2:]
405-
actionList.Actions = []launch.ActionReferenceRequest{
406-
{Action: strings.Join(actionurl, "/"), Version: ngcei.Ref},
407-
}
408-
actionDownloadInfo := &launch.ActionDownloadInfoResponseCollection{}
409-
urlBuilder := protocol.VssConnection{TenantURL: launchEndpoint.Value}
410-
url, urlErr := urlBuilder.BuildURL("actions/build/{planId}/jobs/{jobId}/runnerresolve/actions", map[string]string{
411-
"jobId": rqt.JobID,
412-
"planId": rqt.Plan.PlanID,
413-
}, nil)
414-
if urlErr != nil {
415-
return urlErr
416-
}
417-
if requestErr := vssConnection.RequestWithContext2(ctx, "POST", url, "", actionList, actionDownloadInfo); requestErr != nil {
418-
return requestErr
419-
}
420-
for _, v := range actionDownloadInfo.Actions {
421-
token := runnerConfig.Token
422-
if v.Authentication != nil && v.Authentication.Token != "" {
423-
token = v.Authentication.Token
424-
}
425-
downloadErr := downloadAndExtractAction(
426-
ctx, ngcei.Dir, actionurl[0], actionurl[1], v.ResolvedSha, v.TarURL, token, &downloadActionHTTPClient,
427-
)
428-
if downloadErr != nil {
429-
return downloadErr
430-
}
431-
}
432-
return nil
433-
}
434-
}
435-
}
436-
}
437-
if viaGit, hasViaGit := rcommon.LookupEnvBool("GITHUB_ACT_RUNNER_DOWNLOAD_ACTIONS_VIA_GIT"); hasViaGit && viaGit {
438-
runnerConfig.DownloadAction = nil
439-
}
440378
rc := &runner.RunContext{
441379
Name: uuid.New().String(),
442380
Config: runnerConfig,
@@ -463,6 +401,45 @@ func ExecWorker(rqt *protocol.AgentJobRequestMessage, wc actionsrunner.WorkerCon
463401
EventJSON: payload,
464402
ContextData: map[string]interface{}{},
465403
}
404+
cacheBase := ActionCacheBase{
405+
VssConnection: vssConnection,
406+
Plan: rqt.Plan,
407+
GHToken: runnerConfig.Token,
408+
HttpClient: &downloadActionHTTPClient,
409+
CacheDir: rc.ActionCacheDir(),
410+
}
411+
if viaGit, hasViaGit := rcommon.LookupEnvBool("GITHUB_ACT_RUNNER_DOWNLOAD_ACTIONS_VIA_GIT"); hasViaGit && viaGit {
412+
runnerConfig.ActionCache = nil
413+
} else if strings.EqualFold(rqt.MessageType, "RunnerJobRequest") {
414+
launchEndpoint, hasLaunchEndpoint := rqt.Variables["system.github.launch_endpoint"]
415+
if hasLaunchEndpoint && launchEndpoint.Value != "" {
416+
launchCache := &LaunchActionCache{
417+
ActionCacheBase: cacheBase,
418+
LaunchEndpoint: launchEndpoint.Value,
419+
JobID: rqt.JobID,
420+
}
421+
runnerConfig.ActionCache = launchCache
422+
defer func() {
423+
for _, v := range launchCache.delete {
424+
if removeErr := os.Remove(v); removeErr != nil {
425+
actLogger.Warnf("Unable to remove %v: %v", v, removeErr)
426+
}
427+
}
428+
}()
429+
}
430+
} else {
431+
vssCache := &VssActionCache{
432+
ActionCacheBase: cacheBase,
433+
}
434+
runnerConfig.ActionCache = vssCache
435+
defer func() {
436+
for _, v := range vssCache.delete {
437+
if removeErr := os.Remove(v); removeErr != nil {
438+
actLogger.Warnf("Unable to remove %v: %v", v, removeErr)
439+
}
440+
}
441+
}()
442+
}
466443
for k, v := range rqt.ContextData {
467444
rc.ContextData[k] = v.ToRawObject()
468445
}
@@ -527,9 +504,6 @@ func ExecWorker(rqt *protocol.AgentJobRequestMessage, wc actionsrunner.WorkerCon
527504
}
528505
}
529506
rc.ContextData["github"] = githubCtxMap
530-
val, _ := json.Marshal(githubCtx)
531-
sv := string(val)
532-
rc.GHContextData = &sv
533507

534508
ee := rc.NewExpressionEvaluator(jobExecCtx)
535509
rc.ExprEval = ee
@@ -578,7 +552,7 @@ func ExecWorker(rqt *protocol.AgentJobRequestMessage, wc actionsrunner.WorkerCon
578552
actLogger.Warn("github-act-runner is be unable to access \"" + cacheDir + "\". You might want set one of the " +
579553
"following environment variables XDG_CACHE_HOME, HOME to a user read and writeable location. Details: " + mkdirErr.Error())
580554
}
581-
actLogger.Println("Starting nektos/act")
555+
actLogger.Println("Starting actions-oss/act-cli (nektos/act)")
582556
select {
583557
case <-jobExecCtx.Done():
584558
default:
@@ -634,79 +608,3 @@ func ExecWorker(rqt *protocol.AgentJobRequestMessage, wc actionsrunner.WorkerCon
634608
}
635609
finishJob2(jobStatus, outputMap)
636610
}
637-
638-
func downloadAndExtractAction(
639-
ctx context.Context, target, owner, name, resolvedSha, tarURL, token string, httpClient *http.Client,
640-
) (reterr error) {
641-
contextLogger := common.Logger(ctx)
642-
cachedTar := filepath.Join(target, "..", owner+"."+name+"."+resolvedSha+".tar")
643-
defer func() {
644-
if reterr != nil {
645-
_ = os.Remove(cachedTar) // Ignore cleanup errors
646-
}
647-
}()
648-
var tarstream io.Reader
649-
//nolint:gosec // cachedTar is constructed from controlled inputs (owner, name, resolvedSha)
650-
if fr, err := os.Open(cachedTar); err == nil {
651-
tarstream = fr
652-
defer func() { _ = fr.Close() }() // Ignore file close errors
653-
if contextLogger != nil {
654-
contextLogger.Infof("Found cache for action %v/%v (sha:%v) from %v", owner, name, resolvedSha, cachedTar)
655-
}
656-
} else {
657-
if contextLogger != nil {
658-
contextLogger.Infof("Downloading action %v/%v (sha:%v) from %v", owner, name, resolvedSha, tarURL)
659-
}
660-
req, err := http.NewRequestWithContext(ctx, http.MethodGet, tarURL, http.NoBody)
661-
if err != nil {
662-
return err
663-
}
664-
if token != "" {
665-
req.Header.Add("Authorization", "token "+token)
666-
}
667-
req.Header.Add("User-Agent", "github-act-runner/1.0.0")
668-
req.Header.Add("Accept", "*/*")
669-
rsp, err := httpClient.Do(req)
670-
if err != nil {
671-
return err
672-
}
673-
defer func() { _ = rsp.Body.Close() }() // Ignore response body close errors
674-
if rsp.StatusCode != http.StatusOK {
675-
buf := &bytes.Buffer{}
676-
_, _ = io.Copy(buf, rsp.Body) // Ignore error copy for error message
677-
return fmt.Errorf("failed to download action from %v response %v", tarURL, buf.String())
678-
}
679-
if len(resolvedSha) == len("0000000000000000000000000000000000000000") {
680-
//nolint:gosec // cachedTar is constructed from controlled inputs (owner, name, resolvedSha)
681-
fo, err := os.Create(cachedTar)
682-
if err != nil {
683-
return err
684-
}
685-
defer func() { _ = fo.Close() }() // Ignore file close errors
686-
bytesWritten, err := io.Copy(fo, rsp.Body)
687-
if err != nil {
688-
return err
689-
}
690-
if rsp.ContentLength >= 0 && bytesWritten != rsp.ContentLength {
691-
return fmt.Errorf("failed to download tar expected %v, but copied %v", rsp.ContentLength, bytesWritten)
692-
}
693-
tarstream = fo
694-
_, _ = fo.Seek(0, 0) // Ignore seek errors
695-
} else {
696-
tarstream = rsp.Body
697-
}
698-
}
699-
if err := extractTarGz(tarstream, target); err != nil {
700-
return err
701-
}
702-
return nil
703-
}
704-
705-
func extractTarGz(reader io.Reader, dir string) error {
706-
gzr, err := gzip.NewReader(reader)
707-
if err != nil {
708-
return err
709-
}
710-
defer func() { _ = gzr.Close() }() // Ignore gzip reader close errors
711-
return filecollector.ExtractTar(gzr, dir)
712-
}

0 commit comments

Comments
 (0)