Skip to content
Clone

refactor(ci): move the actions engine into its own repository

act/ now lives in C:\code\GitDataAi\action (git-data-actions), history included.

- the root Dockerfile no longer builds the engine from source: it takes
  /usr/local/bin/gitdata-actions-engine out of the published gitdata-act image
  (ARG ACTIONS_IMAGE), the same image the runner pool runs
- the actions image tag is decoupled from image.tag: it is pinned per chart
  (0.2.89) instead of riding the main sha-<short> tag
- the ci fixture accepts ACTIONS_ENGINE_BINARY since target/ no longer gets a
  hand-built engine
ZhenYi committed
parent 872e73f commit d58a911 Browse files

120 files changed

+32 −21340

This change is very large, so some diffs are not shown. Clone the repository to see the full change.

File tree 120
Deleted act/charts/gitdata-act/templates/_helpers.tpl
@@ -1,51 +0,0 @@
1{{- define "gitdata-act.name" -}}
2{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
3{{- end -}}
4
5{{- define "gitdata-act.fullname" -}}
6{{- if .Values.fullnameOverride -}}
7{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
8{{- else -}}
9{{- $name := default .Chart.Name .Values.nameOverride -}}
10{{- if contains $name .Release.Name -}}
11{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
12{{- else -}}
13{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
14{{- end -}}
15{{- end -}}
16{{- end -}}
17
18{{- define "gitdata-act.labels" -}}
19helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
20app.kubernetes.io/name: {{ include "gitdata-act.name" . }}
21app.kubernetes.io/instance: {{ .Release.Name }}
22app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
23app.kubernetes.io/managed-by: {{ .Release.Service }}
24{{- end -}}
25
26{{- define "gitdata-act.selectorLabels" -}}
27app.kubernetes.io/name: {{ include "gitdata-act.name" . }}
28app.kubernetes.io/instance: {{ .Release.Name }}
29{{- end -}}
30
31{{- define "gitdata-act.serviceAccountName" -}}
32{{- if .Values.serviceAccount.create -}}
33{{- default (include "gitdata-act.fullname" .) .Values.serviceAccount.name -}}
34{{- else -}}
35{{- default "default" .Values.serviceAccount.name -}}
36{{- end -}}
37{{- end -}}
38
39{{- define "gitdata-act.image" -}}
40{{- $tag := .Values.image.tag | default .Chart.AppVersion -}}
41{{- printf "%s:%s" .Values.image.repository $tag -}}
42{{- end -}}
43
44{{- define "gitdata-act.actionsImage" -}}
45{{- $tag := .Values.actionsImage.tag | default .Values.image.tag | default .Chart.AppVersion -}}
46{{- printf "%s:%s" .Values.actionsImage.repository $tag -}}
47{{- end -}}
48
49{{- define "gitdata-act.tokenSecretName" -}}
50{{- default (printf "%s-token" (include "gitdata-act.fullname" .)) .Values.runner.existingSecret -}}
51{{- end -}}
Deleted act/charts/gitdata-act/templates/deployment.yaml
@@ -1,128 +0,0 @@
1apiVersion: apps/v1
2kind: Deployment
3metadata:
4 name: {{ include "gitdata-act.fullname" . }}
5 labels:
6 {{- include "gitdata-act.labels" . | nindent 4 }}
7spec:
8 {{- if not .Values.autoscaling.enabled }}
9 replicas: {{ .Values.replicaCount }}
10 {{- end }}
11 revisionHistoryLimit: 3
12 selector:
13 matchLabels:
14 {{- include "gitdata-act.selectorLabels" . | nindent 6 }}
15 template:
16 metadata:
17 labels:
18 {{- include "gitdata-act.selectorLabels" . | nindent 8 }}
19 {{- with .Values.podLabels }}
20 {{- toYaml . | nindent 8 }}
21 {{- end }}
22 {{- with .Values.podAnnotations }}
23 annotations:
24 {{- toYaml . | nindent 8 }}
25 {{- end }}
26 spec:
27 serviceAccountName: {{ include "gitdata-act.serviceAccountName" . }}
28 {{- with .Values.imagePullSecrets }}
29 imagePullSecrets:
30 {{- toYaml . | nindent 8 }}
31 {{- end }}
32 terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }}
33 securityContext:
34 {{- toYaml .Values.podSecurityContext | nindent 8 }}
35 initContainers:
36 - name: actions-runtime
37 image: {{ include "gitdata-act.actionsImage" . }}
38 imagePullPolicy: {{ .Values.actionsImage.pullPolicy }}
39 command: ["sh", "-c"]
40 args:
41 - |
42 mkdir -p /bundle/bin /bundle/node
43 cp /usr/local/bin/gitdata-actions-engine /bundle/bin/
44 cp -a /opt/gitdata/node/. /bundle/node/
45 securityContext:
46 {{- toYaml .Values.securityContext | nindent 12 }}
47 volumeMounts:
48 - name: actions-runtime
49 mountPath: /bundle
50 containers:
51 - name: runner
52 image: {{ include "gitdata-act.image" . }}
53 imagePullPolicy: {{ .Values.image.pullPolicy }}
54 args: ["runner"]
55 env:
56 - name: APP_RUNNER__SERVER_URL
57 value: {{ .Values.runner.serverUrl | quote }}
58 - name: APP_RUNNER__REGISTRATION_TOKEN
59 valueFrom:
60 secretKeyRef:
61 name: {{ include "gitdata-act.tokenSecretName" . }}
62 key: {{ .Values.runner.registrationTokenKey }}
63 optional: true
64 {{- if .Values.runner.name }}
65 - name: APP_RUNNER__NAME
66 value: {{ .Values.runner.name | quote }}
67 {{- else }}
68 - name: APP_RUNNER__NAME
69 valueFrom:
70 fieldRef:
71 fieldPath: metadata.name
72 {{- end }}
73 - name: APP_RUNNER__LABELS
74 value: {{ .Values.runner.labels | toJson | quote }}
75 - name: APP_RUNNER__CAPACITY
76 value: {{ .Values.runner.capacity | quote }}
77 - name: APP_RUNNER__WORKSPACE_ROOT
78 value: {{ .Values.runner.workspaceRoot | quote }}
79 - name: APP_RUNNER__CREDENTIAL_PATH
80 value: {{ .Values.runner.credentialPath | quote }}
81 - name: APP_RUNNER__DOCKER_ENABLED
82 value: {{ .Values.runner.dockerEnabled | quote }}
83 - name: APP_RUNNER__ACTIONS_ENGINE_BINARY
84 value: /opt/gitdata/actions/bin/gitdata-actions-engine
85 - name: APP_RUNNER__ACTIONS_CACHE_DIR
86 value: /var/lib/gitdata-runner/actions
87 {{- with .Values.runner.extraEnv }}
88 {{- toYaml . | nindent 12 }}
89 {{- end }}
90 securityContext:
91 {{- toYaml .Values.securityContext | nindent 12 }}
92 resources:
93 {{- toYaml .Values.resources | nindent 12 }}
94 volumeMounts:
95 - name: workspace
96 mountPath: /var/lib/gitdata-runner
97 - name: actions-runtime
98 mountPath: /opt/gitdata/actions
99 readOnly: true
100 - name: actions-runtime
101 mountPath: /opt/gitdata/node
102 readOnly: true
103 {{- if .Values.runner.dockerEnabled }}
104 - name: docker-sock
105 mountPath: /var/run/docker.sock
106 {{- end }}
107 volumes:
108 - name: workspace
109 emptyDir: {}
110 - name: actions-runtime
111 emptyDir: {}
112 {{- if .Values.runner.dockerEnabled }}
113 - name: docker-sock
114 hostPath:
115 path: /var/run/docker.sock
116 {{- end }}
117 {{- with .Values.nodeSelector }}
118 nodeSelector:
119 {{- toYaml . | nindent 8 }}
120 {{- end }}
121 {{- with .Values.tolerations }}
122 tolerations:
123 {{- toYaml . | nindent 8 }}
124 {{- end }}
125 {{- with .Values.affinity }}
126 affinity:
127 {{- toYaml . | nindent 8 }}
128 {{- end }}
Deleted act/charts/gitdata-act/templates/hpa.yaml
@@ -1,34 +0,0 @@
1{{- if .Values.autoscaling.enabled }}
2apiVersion: autoscaling/v2
3kind: HorizontalPodAutoscaler
4metadata:
5 name: {{ include "gitdata-act.fullname" . }}
6 labels:
7 {{- include "gitdata-act.labels" . | nindent 4 }}
8spec:
9 scaleTargetRef:
10 apiVersion: apps/v1
11 kind: Deployment
12 name: {{ include "gitdata-act.fullname" . }}
13 minReplicas: {{ .Values.autoscaling.minReplicas }}
14 maxReplicas: {{ .Values.autoscaling.maxReplicas }}
15 metrics:
16 - type: Resource
17 resource:
18 name: cpu
19 target:
20 type: Utilization
21 averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
22 {{- if gt (int (.Values.autoscaling.targetMemoryUtilizationPercentage | default 0)) 0 }}
23 - type: Resource
24 resource:
25 name: memory
26 target:
27 type: Utilization
28 averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
29 {{- end }}
30 {{- with .Values.autoscaling.behavior }}
31 behavior:
32 {{- toYaml . | nindent 4 }}
33 {{- end }}
34{{- end }}
Deleted act/charts/gitdata-act/templates/pdb.yaml
@@ -1,13 +0,0 @@
1{{- if .Values.podDisruptionBudget.enabled }}
2apiVersion: policy/v1
3kind: PodDisruptionBudget
4metadata:
5 name: {{ include "gitdata-act.fullname" . }}
6 labels:
7 {{- include "gitdata-act.labels" . | nindent 4 }}
8spec:
9 minAvailable: {{ .Values.podDisruptionBudget.minAvailable }}
10 selector:
11 matchLabels:
12 {{- include "gitdata-act.selectorLabels" . | nindent 6 }}
13{{- end }}
Deleted act/charts/gitdata-act/templates/secret.yaml
@@ -1,11 +0,0 @@
1{{- if and .Values.runner.registrationToken (not .Values.runner.existingSecret) }}
2apiVersion: v1
3kind: Secret
4metadata:
5 name: {{ include "gitdata-act.tokenSecretName" . }}
6 labels:
7 {{- include "gitdata-act.labels" . | nindent 4 }}
8type: Opaque
9stringData:
10 {{ .Values.runner.registrationTokenKey }}: {{ .Values.runner.registrationToken | quote }}
11{{- end }}
Deleted act/charts/gitdata-act/templates/serviceaccount.yaml
@@ -1,12 +0,0 @@
1{{- if .Values.serviceAccount.create }}
2apiVersion: v1
3kind: ServiceAccount
4metadata:
5 name: {{ include "gitdata-act.serviceAccountName" . }}
6 labels:
7 {{- include "gitdata-act.labels" . | nindent 4 }}
8 {{- with .Values.serviceAccount.annotations }}
9 annotations:
10 {{- toYaml . | nindent 4 }}
11 {{- end }}
12{{- end }}
Deleted act/charts/gitdata-act/Chart.yaml
@@ -1,6 +0,0 @@
1apiVersion: v2
2name: gitdata-act
3description: GitData act runner workers
4type: application
5version: 0.1.0
6appVersion: "0.2.89"
Deleted act/charts/gitdata-act/values.yaml
@@ -1,72 +0,0 @@
1nameOverride: ""
2fullnameOverride: ""
3
4image:
5 repository: harbor.gitdata.me/app/gitdata
6 tag: ""
7 pullPolicy: IfNotPresent
8
9actionsImage:
10 repository: harbor.gitdata.me/app/gitdata-act
11 tag: ""
12 pullPolicy: IfNotPresent
13
14imagePullSecrets: []
15
16serviceAccount:
17 create: true
18 name: ""
19 annotations: {}
20
21replicaCount: 1
22terminationGracePeriodSeconds: 3600
23
24runner:
25 serverUrl: "http://gitdata-webhook:3005"
26 registrationToken: ""
27 existingSecret: ""
28 registrationTokenKey: APP_RUNNER__REGISTRATION_TOKEN
29 name: ""
30 labels: ["self-hosted", "docker"]
31 capacity: 1
32 dockerEnabled: false
33 workspaceRoot: /var/lib/gitdata-runner
34 credentialPath: /var/lib/gitdata-runner/credential
35 extraEnv: []
36
37podAnnotations: {}
38podLabels: {}
39podSecurityContext:
40 fsGroup: 10001
41securityContext:
42 runAsNonRoot: true
43 runAsUser: 10001
44 allowPrivilegeEscalation: false
45 capabilities:
46 drop: ["ALL"]
47
48resources:
49 requests: { cpu: 500m, memory: 512Mi }
50 limits: { memory: 4Gi }
51
52autoscaling:
53 enabled: true
54 minReplicas: 1
55 maxReplicas: 5
56 targetCPUUtilizationPercentage: 70
57 targetMemoryUtilizationPercentage: 0
58 behavior:
59 scaleDown:
60 stabilizationWindowSeconds: 600
61 policies:
62 - type: Pods
63 value: 1
64 periodSeconds: 120
65
66podDisruptionBudget:
67 enabled: false
68 minAvailable: 1
69
70nodeSelector: {}
71tolerations: []
72affinity: {}
Deleted act/cmd/gitdata-actions-engine/main.go
@@ -1,525 +0,0 @@
1package main
2
3import (
4 "bufio"
5 "context"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "io"
10 "os"
11 "os/signal"
12 "sort"
13 "strconv"
14 "strings"
15 "sync"
16 "syscall"
17 "time"
18
19 "github.com/nektos/act/pkg/common"
20 "github.com/nektos/act/pkg/exprparser"
21 "github.com/nektos/act/pkg/model"
22 "github.com/nektos/act/pkg/runner"
23 "github.com/nektos/act/pkg/workflowpattern"
24 "github.com/robfig/cron/v3"
25 log "github.com/sirupsen/logrus"
26 "gopkg.in/yaml.v3"
27)
28
29type request struct {
30 Patterns []string `json:"patterns"`
31 Paths []string `json:"paths"`
32 TriggerEvent string `json:"trigger_event"`
33 Branch string `json:"branch"`
34 Tag string `json:"tag"`
35 Action string `json:"action"`
36 ID json.RawMessage `json:"id"`
37 Op string `json:"op"`
38 Source string `json:"source"`
39 File string `json:"file"`
40 Job string `json:"job"`
41 Expression string `json:"expression"`
42 Value interface{} `json:"value"`
43 StatusCheck bool `json:"status_check"`
44 Matrix map[string]interface{} `json:"matrix"`
45 Needs map[string]exprparser.Needs `json:"needs"`
46 Jobs map[string]*model.WorkflowCallResult `json:"jobs"`
47 Github map[string]interface{} `json:"github"`
48 Event map[string]interface{} `json:"event"`
49 Inputs map[string]interface{} `json:"inputs"`
50 Env map[string]string `json:"env"`
51 Vars map[string]string `json:"vars"`
52 Secrets map[string]string `json:"secrets"`
53 Workdir string `json:"workdir"`
54 Platforms map[string]string `json:"platforms"`
55 NodeRuntimes map[string]string `json:"node_runtimes"`
56 ActionCacheDir string `json:"action_cache_dir"`
57 ActionServer string `json:"action_server"`
58 ActionToken string `json:"action_token"`
59 DockerHost string `json:"docker_host"`
60 RuntimeURL string `json:"runtime_url"`
61 RuntimeToken string `json:"runtime_token"`
62 Token string `json:"token"`
63 TimeoutSeconds uint64 `json:"timeout_seconds"`
64}
65
66type output struct {
67 ID json.RawMessage `json:"id,omitempty"`
68 Event string `json:"event"`
69 Data interface{} `json:"data,omitempty"`
70 Error string `json:"error,omitempty"`
71}
72
73type emitter struct {
74 mu sync.Mutex
75 encoder *json.Encoder
76 id json.RawMessage
77 masks []string
78 allowSensitiveResult bool
79}
80
81func (e *emitter) send(event string, data interface{}, err error) {
82 e.mu.Lock()
83 defer e.mu.Unlock()
84 item := output{ID: e.id, Event: event, Data: data}
85 if err != nil {
86 item.Error = err.Error()
87 }
88 encoded, marshalErr := json.Marshal(item)
89 if marshalErr != nil {
90 return
91 }
92 text := string(encoded)
93 masks := e.masks
94 if event == "result" && e.allowSensitiveResult {
95 masks = nil
96 }
97 for _, mask := range masks {
98 if mask != "" {
99 escaped, _ := json.Marshal(mask)
100 text = strings.ReplaceAll(text, string(escaped[1:len(escaped)-1]), "***")
101 }
102 }
103 _, _ = fmt.Fprintln(os.Stdout, text)
104}
105
106type eventHook struct{ emit *emitter }
107
108func (h eventHook) Levels() []log.Level { return log.AllLevels }
109func (h eventHook) Fire(entry *log.Entry) error {
110 h.emit.send("log", map[string]interface{}{"text": entry.Message + "\n"}, nil)
111 return nil
112}
113
114func readWorkflow(req request) (*model.Workflow, map[string]interface{}, error) {
115 workflow, err := model.ReadWorkflow(strings.NewReader(req.Source), true)
116 if err != nil {
117 return nil, nil, err
118 }
119 workflow.File = req.File
120 var raw map[string]interface{}
121 if err = yaml.Unmarshal([]byte(req.Source), &raw); err != nil {
122 return nil, nil, err
123 }
124 triggers := map[string]interface{}{}
125 switch value := raw["on"].(type) {
126 case string:
127 triggers[value] = map[string]interface{}{}
128 case []interface{}:
129 for _, name := range value {
130 triggers[fmt.Sprint(name)] = map[string]interface{}{}
131 }
132 case map[string]interface{}:
133 for name, value := range value {
134 if value == nil {
135 value = map[string]interface{}{}
136 }
137 triggers[name] = value
138 }
139 default:
140 return nil, nil, fmt.Errorf("on must declare a trigger")
141 }
142 allowed := map[string]bool{"push": true, "pull_request": true, "workflow_dispatch": true, "schedule": true, "repository_dispatch": true, "workflow_call": true, "workflow_run": true}
143 for event := range triggers {
144 if !allowed[event] {
145 return nil, nil, fmt.Errorf("event %q has no GitData event source", event)
146 }
147 }
148 if len(triggers) == 0 || len(workflow.Jobs) == 0 {
149 return nil, nil, fmt.Errorf("workflow must declare triggers and jobs")
150 }
151 raw["on"] = triggers
152 marks := map[string]int{}
153 var visit func(string) error
154 visit = func(id string) error {
155 if marks[id] == 1 {
156 return fmt.Errorf("dependency cycle at job %q", id)
157 }
158 if marks[id] == 2 {
159 return nil
160 }
161 job, ok := workflow.Jobs[id]
162 if !ok {
163 return fmt.Errorf("unknown job %q", id)
164 }
165 marks[id] = 1
166 if job.Uses == "" && len(job.Steps) == 0 {
167 return fmt.Errorf("job %q must declare steps", id)
168 }
169 for _, dep := range job.Needs() {
170 if err := visit(dep); err != nil {
171 return err
172 }
173 }
174 marks[id] = 2
175 return nil
176 }
177 for id := range workflow.Jobs {
178 if err := visit(id); err != nil {
179 return nil, nil, err
180 }
181 }
182 return workflow, raw, nil
183}
184
185func runContext(req request, wf *model.Workflow, emit *emitter) *runner.RunContext {
186 if req.Env == nil {
187 req.Env = map[string]string{}
188 }
189 if req.Github == nil {
190 req.Github = map[string]interface{}{}
191 }
192 if req.Workdir == "" {
193 req.Workdir = "."
194 }
195 if req.ActionServer == "" {
196 req.ActionServer = "https://github.com"
197 }
198 for name, need := range req.Needs {
199 job := wf.Jobs[name]
200 if job == nil {
201 job = &model.Job{}
202 wf.Jobs[name] = job
203 }
204 job.Outputs = need.Outputs
205 job.Result = need.Result
206 }
207 event, _ := json.Marshal(req.Event)
208 cfg := &runner.Config{Workdir: req.Workdir, ActionCacheDir: req.ActionCacheDir, Env: req.Env, Secrets: req.Secrets, Vars: req.Vars, Token: req.Token,
209 Platforms: req.Platforms, GitHubInstance: "github.com", EventName: fmt.Sprint(req.Github["event_name"]), Actor: fmt.Sprint(req.Github["actor"]),
210 NoSkipCheckout: true, AutoRemove: true, BindWorkdir: false, LogOutput: true, ContainerDaemonSocket: req.DockerHost,
211 GitDataGithub: req.Github, GitDataInputs: req.Inputs, GitDataJobs: req.Jobs, GitDataActionServer: req.ActionServer, GitDataActionToken: req.ActionToken,
212 GitDataRuntimeURL: req.RuntimeURL, GitDataRuntimeToken: req.RuntimeToken, GitDataNodeRuntimes: req.NodeRuntimes}
213 if req.RuntimeURL != "" {
214 cfg.ArtifactServerPath = "gitdata"
215 }
216 if emit != nil {
217 cfg.GitDataObserver = func(event runner.GitDataEvent) { emit.send(event.Type, event, nil) }
218 }
219 rc := &runner.RunContext{Config: cfg, Run: &model.Run{Workflow: wf, JobID: req.Job}, Matrix: req.Matrix, EventJSON: string(event), StepResults: map[string]*model.StepResult{}}
220 ctx := context.Background()
221 rc.ExprEval = rc.NewExpressionEvaluator(ctx)
222 rc.Name = rc.ExprEval.Interpolate(ctx, rc.Run.String())
223 rc.JobName = rc.Name
224 return rc
225}
226
227func evaluateValue(ctx context.Context, rc *runner.RunContext, value interface{}) (interface{}, error) {
228 var node yaml.Node
229 if err := node.Encode(value); err != nil {
230 return nil, err
231 }
232 if err := rc.NewExpressionEvaluator(ctx).EvaluateYamlNode(ctx, &node); err != nil {
233 return nil, err
234 }
235 var result interface{}
236 err := node.Decode(&result)
237 return result, err
238}
239
240func stringValue(value interface{}) string {
241 if value == nil {
242 return ""
243 }
244 return fmt.Sprint(value)
245}
246func boolValue(value interface{}, fallback bool) bool {
247 if value == nil {
248 return fallback
249 }
250 if b, ok := value.(bool); ok {
251 return b
252 }
253 b, err := strconv.ParseBool(fmt.Sprint(value))
254 if err != nil {
255 return fallback
256 }
257 return b
258}
259func object(value interface{}) map[string]interface{} {
260 m, _ := value.(map[string]interface{})
261 if m == nil {
262 return map[string]interface{}{}
263 }
264 return m
265}
266
267func expand(req request, wf *model.Workflow, raw map[string]interface{}, rc *runner.RunContext) (interface{}, error) {
268 ctx := context.Background()
269 job := wf.GetJob(req.Job)
270 enabled, err := runner.EvalBool(ctx, rc.ExprEval, job.If.Value, exprparser.DefaultStatusCheckSuccess)
271 if err != nil {
272 return nil, err
273 }
274 if !enabled {
275 return map[string]interface{}{"skipped": true, "instances": []interface{}{}}, nil
276 }
277 rawJob := object(object(raw["jobs"])[req.Job])
278 strategy := object(rawJob["strategy"])
279 if job.Strategy != nil {
280 if err := rc.ExprEval.EvaluateYamlNode(ctx, &job.Strategy.RawMatrix); err != nil {
281 return nil, err
282 }
283 }
284 matrices, err := job.GetMatrixes()
285 if err != nil {
286 return nil, err
287 }
288 if len(matrices) > 256 {
289 return nil, fmt.Errorf("matrix exceeds 256 instances")
290 }
291 instances := []interface{}{}
292 for index, matrix := range matrices {
293 rc.Matrix = matrix
294 rc.ExprEval = rc.NewExpressionEvaluator(ctx)
295 fields := map[string]interface{}{}
296 for _, name := range []string{"name", "runs-on", "environment", "timeout-minutes", "continue-on-error", "concurrency", "with", "permissions"} {
297 value, e := evaluateValue(ctx, rc, rawJob[name])
298 if e != nil {
299 return nil, fmt.Errorf("job %s %s: %w", req.Job, name, e)
300 }
301 fields[name] = value
302 }
303 labels := []string{}
304 switch label := fields["runs-on"].(type) {
305 case string:
306 labels = append(labels, label)
307 case []interface{}:
308 for _, l := range label {
309 labels = append(labels, stringValue(l))
310 }
311 case map[string]interface{}:
312 switch ls := label["labels"].(type) {
313 case string:
314 labels = append(labels, ls)
315 case []interface{}:
316 for _, l := range ls {
317 labels = append(labels, stringValue(l))
318 }
319 }
320 }
321 if job.Uses == "" && len(labels) == 0 {
322 return nil, fmt.Errorf("job %s runs-on has no labels", req.Job)
323 }
324 environment := fields["environment"]
325 if env, ok := environment.(map[string]interface{}); ok {
326 environment = env["name"]
327 }
328 timeout := uint64(21600)
329 if fields["timeout-minutes"] != nil {
330 minutes, e := strconv.ParseFloat(stringValue(fields["timeout-minutes"]), 64)
331 if e != nil || minutes <= 0 {
332 return nil, fmt.Errorf("invalid timeout-minutes")
333 }
334 timeout = uint64(minutes * 60)
335 }
336 failFast, e := evaluateValue(ctx, rc, strategy["fail-fast"])
337 if e != nil {
338 return nil, e
339 }
340 maxParallel := len(matrices)
341 if strategy["max-parallel"] != nil {
342 v, e := evaluateValue(ctx, rc, strategy["max-parallel"])
343 if e != nil {
344 return nil, e
345 }
346 n, e := strconv.Atoi(stringValue(v))
347 if e != nil || n <= 0 {
348 return nil, fmt.Errorf("invalid max-parallel")
349 }
350 maxParallel = n
351 }
352 name := stringValue(fields["name"])
353 if name == "" {
354 name = req.Job
355 }
356 instances = append(instances, map[string]interface{}{"index": index, "matrix": matrix, "name": name, "runs_on": labels, "environment": stringValue(environment), "timeout_seconds": timeout, "continue_on_error": boolValue(fields["continue-on-error"], false), "fail_fast": boolValue(failFast, true), "max_parallel": maxParallel, "concurrency": fields["concurrency"], "permissions": fields["permissions"], "with": fields["with"], "uses": job.Uses, "group": object(fields["runs-on"])["group"]})
357 }
358 return map[string]interface{}{"skipped": false, "instances": instances}, nil
359}
360
361func execute(req request, rc *runner.RunContext, emit *emitter) (interface{}, error) {
362 if err := os.MkdirAll(req.Workdir, 0750); err != nil {
363 return nil, err
364 }
365 signalCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
366 defer stop()
367 jobCtx := signalCtx
368 cancel := func() {}
369 if req.TimeoutSeconds > 0 {
370 jobCtx, cancel = context.WithTimeout(signalCtx, time.Duration(req.TimeoutSeconds)*time.Second)
371 }
372 defer cancel()
373 logger := log.New()
374 logger.SetOutput(io.Discard)
375 logger.SetLevel(log.InfoLevel)
376 logger.AddHook(eventHook{emit: emit})
377 ctx := common.WithLogger(context.Background(), logger)
378 ctx = common.WithJobCancelContext(ctx, jobCtx)
379 ctx = common.WithJobErrorContainer(ctx)
380 hardCtx, hardCancel := context.WithCancel(ctx)
381 defer hardCancel()
382 go func() {
383 select {
384 case <-jobCtx.Done():
385 timer := time.NewTimer(10 * time.Second)
386 defer timer.Stop()
387 select {
388 case <-timer.C:
389 hardCancel()
390 case <-hardCtx.Done():
391 }
392 case <-hardCtx.Done():
393 }
394 }()
395 ctx = hardCtx
396 executor, err := rc.Executor()
397 if err != nil {
398 return nil, err
399 }
400 err = executor(ctx)
401 result := rc.Run.Job().Result
402 if result == "" {
403 result = "success"
404 }
405 if errors.Is(jobCtx.Err(), context.DeadlineExceeded) {
406 result = "timed_out"
407 } else if jobCtx.Err() != nil {
408 result = "cancelled"
409 } else if err != nil || common.JobError(ctx) != nil {
410 result = "failure"
411 }
412 outputs := rc.Run.Job().Outputs
413 for name, value := range outputs {
414 for _, secret := range append(emit.masks, rc.Masks...) {
415 if secret != "" && strings.Contains(value, secret) {
416 delete(outputs, name)
417 break
418 }
419 }
420 }
421 message := ""
422 if err != nil {
423 message = err.Error()
424 }
425 if jobErr := common.JobError(ctx); jobErr != nil {
426 message = jobErr.Error()
427 }
428 return map[string]interface{}{"result": result, "outputs": outputs, "error": message}, nil
429}
430
431func handle(req request, emit *emitter) (result interface{}, err error) {
432 defer func() {
433 if recovered := recover(); recovered != nil {
434 result = nil
435 err = fmt.Errorf("engine panic: %v", recovered)
436 }
437 }()
438 if req.Op == "version" {
439 return map[string]interface{}{"protocol_version": 2, "upstream": "v0.2.89", "commit": "4f411281417e88660bea1c1a1749aa71ae0bd60f"}, nil
440 }
441 if req.Op == "oidc_sign" || req.Op == "oidc_jwks" {
442 return oidc(req)
443 }
444 if req.Op == "match" {
445 patterns, e := workflowpattern.CompilePatterns(req.Patterns...)
446 if e != nil {
447 return nil, e
448 }
449 return !workflowpattern.Skip(patterns, req.Paths, &workflowpattern.EmptyTraceWriter{}), nil
450 }
451 if req.Op == "cron" {
452 schedule, e := cron.ParseStandard(req.Expression)
453 if e != nil {
454 return nil, e
455 }
456 timestamp, e := strconv.ParseFloat(fmt.Sprint(req.Value), 64)
457 if e != nil {
458 return nil, e
459 }
460 return schedule.Next(time.Unix(int64(timestamp), 0).UTC()).Unix(), nil
461 }
462 wf, raw, err := readWorkflow(req)
463 if err != nil {
464 return nil, err
465 }
466 if req.Op == "parse" {
467 return raw, nil
468 }
469 if req.Op == "trigger" {
470 return triggerMatches(req, raw)
471 }
472 if req.Job == "" {
473 ids := wf.GetJobIDs()
474 sort.Strings(ids)
475 req.Job = ids[0]
476 }
477 if wf.Jobs[req.Job] == nil {
478 return nil, fmt.Errorf("unknown job %q", req.Job)
479 }
480 rc := runContext(req, wf, emit)
481 switch req.Op {
482 case "evaluate":
483 if req.Expression != "" || req.StatusCheck {
484 check := exprparser.DefaultStatusCheckNone
485 if req.StatusCheck {
486 check = exprparser.DefaultStatusCheckSuccess
487 }
488 return rc.GitDataEvaluate(context.Background(), req.Expression, check)
489 }
490 return evaluateValue(context.Background(), rc, req.Value)
491 case "expand":
492 return expand(req, wf, raw, rc)
493 case "execute":
494 return execute(req, rc, emit)
495 default:
496 return nil, fmt.Errorf("unknown operation %q", req.Op)
497 }
498}
499
500func main() {
501 log.SetOutput(os.Stderr)
502 log.SetLevel(log.WarnLevel)
503 scanner := bufio.NewScanner(os.Stdin)
504 scanner.Buffer(make([]byte, 65536), 8*1024*1024)
505 for scanner.Scan() {
506 var req request
507 emit := &emitter{encoder: json.NewEncoder(os.Stdout)}
508 if err := json.Unmarshal(scanner.Bytes(), &req); err != nil {
509 emit.send("result", nil, err)
510 continue
511 }
512 emit.id = req.ID
513 emit.allowSensitiveResult = req.Op != "execute"
514 for _, secret := range req.Secrets {
515 emit.masks = append(emit.masks, secret)
516 }
517 emit.masks = append(emit.masks, req.Token, req.RuntimeToken, req.ActionToken)
518 result, err := handle(req, emit)
519 emit.send("result", result, err)
520 }
521 if err := scanner.Err(); err != nil {
522 _, _ = fmt.Fprintln(os.Stderr, err)
523 os.Exit(1)
524 }
525}
Deleted act/cmd/gitdata-actions-engine/oidc.go
@@ -1,50 +0,0 @@
1package main
2
3import (
4 "crypto"
5 "crypto/rand"
6 "crypto/rsa"
7 "crypto/sha256"
8 "crypto/x509"
9 "encoding/base64"
10 "encoding/json"
11 "encoding/pem"
12 "fmt"
13 "math/big"
14)
15
16func oidc(req request) (interface{}, error) {
17 block, _ := pem.Decode([]byte(req.Token))
18 if block == nil {
19 return nil, fmt.Errorf("OIDC private key must be PEM encoded")
20 }
21 var key *rsa.PrivateKey
22 if parsed, err := x509.ParsePKCS8PrivateKey(block.Bytes); err == nil {
23 key, _ = parsed.(*rsa.PrivateKey)
24 } else {
25 key, _ = x509.ParsePKCS1PrivateKey(block.Bytes)
26 }
27 if key == nil || key.N.BitLen() < 2048 {
28 return nil, fmt.Errorf("OIDC requires an RSA key of at least 2048 bits")
29 }
30 encoding := base64.RawURLEncoding
31 modulus := encoding.EncodeToString(key.N.Bytes())
32 exponent := encoding.EncodeToString(big.NewInt(int64(key.E)).Bytes())
33 thumbprint := sha256.Sum256([]byte(modulus))
34 kid := encoding.EncodeToString(thumbprint[:16])
35 if req.Op == "oidc_jwks" {
36 return map[string]interface{}{"keys": []interface{}{map[string]interface{}{"kty": "RSA", "alg": "RS256", "use": "sig", "kid": kid, "n": modulus, "e": exponent}}}, nil
37 }
38 header, _ := json.Marshal(map[string]string{"alg": "RS256", "typ": "JWT", "kid": kid})
39 claims, err := json.Marshal(req.Value)
40 if err != nil {
41 return nil, err
42 }
43 message := encoding.EncodeToString(header) + "." + encoding.EncodeToString(claims)
44 hash := sha256.Sum256([]byte(message))
45 signature, err := rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA256, hash[:])
46 if err != nil {
47 return nil, err
48 }
49 return message + "." + encoding.EncodeToString(signature), nil
50}
Deleted act/cmd/gitdata-actions-engine/triggers.go
@@ -1,112 +0,0 @@
1package main
2
3import (
4 "fmt"
5 "github.com/nektos/act/pkg/workflowpattern"
6)
7
8func stringList(value interface{}) []string {
9 values := []string{}
10 switch value := value.(type) {
11 case string:
12 values = append(values, value)
13 case []interface{}:
14 for _, v := range value {
15 values = append(values, fmt.Sprint(v))
16 }
17 }
18 return values
19}
20
21func filterMatches(config map[string]interface{}, key string, paths []string) (bool, error) {
22 include, hasInclude := config[key]
23 exclude, hasExclude := config[key+"-ignore"]
24 if hasInclude && hasExclude {
25 return false, fmt.Errorf("%s and %s-ignore are mutually exclusive", key, key)
26 }
27 if hasInclude {
28 patterns, err := workflowpattern.CompilePatterns(stringList(include)...)
29 if err != nil {
30 return false, err
31 }
32 return !workflowpattern.Skip(patterns, paths, &workflowpattern.EmptyTraceWriter{}), nil
33 }
34 if hasExclude {
35 patterns, err := workflowpattern.CompilePatterns(stringList(exclude)...)
36 if err != nil {
37 return false, err
38 }
39 return !workflowpattern.Filter(patterns, paths, &workflowpattern.EmptyTraceWriter{}), nil
40 }
41 return true, nil
42}
43
44func triggerMatches(req request, raw map[string]interface{}) (bool, error) {
45 event, exists := object(raw["on"])[req.TriggerEvent]
46 if !exists {
47 return false, nil
48 }
49 config := object(event)
50 if types := stringList(config["types"]); len(types) > 0 {
51 found := false
52 for _, kind := range types {
53 if kind == req.Action {
54 found = true
55 }
56 }
57 if !found {
58 return false, nil
59 }
60 }
61 if req.TriggerEvent == "pull_request" && config["types"] == nil && req.Action != "opened" && req.Action != "synchronize" && req.Action != "reopened" {
62 return false, nil
63 }
64 if req.TriggerEvent == "workflow_run" {
65 names := stringList(config["workflows"])
66 if len(names) > 0 {
67 found := false
68 for _, name := range names {
69 if name == req.Expression {
70 found = true
71 }
72 }
73 if !found {
74 return false, nil
75 }
76 }
77 }
78 if req.TriggerEvent == "push" && req.Tag != "" {
79 _, branches := config["branches"]
80 _, branchesIgnore := config["branches-ignore"]
81 _, tags := config["tags"]
82 _, tagsIgnore := config["tags-ignore"]
83 if (branches || branchesIgnore) && !(tags || tagsIgnore) {
84 return false, nil
85 }
86 return filterMatches(config, "tags", []string{req.Tag})
87 }
88 if req.TriggerEvent == "push" || req.TriggerEvent == "pull_request" || req.TriggerEvent == "workflow_run" {
89 if req.TriggerEvent == "push" {
90 _, tags := config["tags"]
91 _, tagsIgnore := config["tags-ignore"]
92 _, branches := config["branches"]
93 _, branchesIgnore := config["branches-ignore"]
94 if (tags || tagsIgnore) && !(branches || branchesIgnore) {
95 return false, nil
96 }
97 }
98 matched, err := filterMatches(config, "branches", []string{req.Branch})
99 if err != nil || !matched {
100 return matched, err
101 }
102 }
103 if req.TriggerEvent == "push" || req.TriggerEvent == "pull_request" {
104 if config["paths"] != nil || config["paths-ignore"] != nil {
105 if len(req.Paths) == 0 {
106 return false, nil
107 }
108 return filterMatches(config, "paths", req.Paths)
109 }
110 }
111 return true, nil
112}
Deleted act/cmd/dir.go
@@ -1,27 +0,0 @@
1package cmd
2
3import (
4 "os"
5 "path/filepath"
6
7 log "github.com/sirupsen/logrus"
8)
9
10var (
11 UserHomeDir string
12 CacheHomeDir string
13)
14
15func init() {
16 home, err := os.UserHomeDir()
17 if err != nil {
18 log.Fatal(err)
19 }
20 UserHomeDir = home
21
22 if v := os.Getenv("XDG_CACHE_HOME"); v != "" {
23 CacheHomeDir = v
24 } else {
25 CacheHomeDir = filepath.Join(UserHomeDir, ".cache")
26 }
27}
Deleted act/cmd/graph.go
@@ -1,38 +0,0 @@
1package cmd
2
3import (
4 "os"
5
6 "github.com/nektos/act/pkg/common"
7 "github.com/nektos/act/pkg/model"
8)
9
10func drawGraph(plan *model.Plan) error {
11 drawings := make([]*common.Drawing, 0)
12
13 jobPen := common.NewPen(common.StyleSingleLine, 96)
14 arrowPen := common.NewPen(common.StyleNoLine, 97)
15 for i, stage := range plan.Stages {
16 if i > 0 {
17 drawings = append(drawings, arrowPen.DrawArrow())
18 }
19
20 ids := make([]string, 0)
21 for _, r := range stage.Runs {
22 ids = append(ids, r.String())
23 }
24 drawings = append(drawings, jobPen.DrawBoxes(ids...))
25 }
26
27 maxWidth := 0
28 for _, d := range drawings {
29 if d.GetWidth() > maxWidth {
30 maxWidth = d.GetWidth()
31 }
32 }
33
34 for _, d := range drawings {
35 d.Draw(os.Stdout, maxWidth)
36 }
37 return nil
38}
Deleted act/cmd/input.go
@@ -1,116 +0,0 @@
1package cmd
2
3import (
4 "path/filepath"
5
6 log "github.com/sirupsen/logrus"
7)
8
9// Input contains the input for the root command
10type Input struct {
11 actor string
12 workdir string
13 workflowsPath string
14 autodetectEvent bool
15 eventPath string
16 reuseContainers bool
17 bindWorkdir bool
18 secrets []string
19 vars []string
20 envs []string
21 inputs []string
22 platforms []string
23 dryrun bool
24 forcePull bool
25 forceRebuild bool
26 noOutput bool
27 envfile string
28 inputfile string
29 secretfile string
30 varfile string
31 insecureSecrets bool
32 defaultBranch string
33 privileged bool
34 usernsMode string
35 containerArchitecture string
36 containerDaemonSocket string
37 containerOptions string
38 noWorkflowRecurse bool
39 useGitIgnore bool
40 githubInstance string
41 containerCapAdd []string
42 containerCapDrop []string
43 autoRemove bool
44 artifactServerPath string
45 artifactServerAddr string
46 artifactServerPort string
47 noCacheServer bool
48 cacheServerPath string
49 cacheServerExternalURL string
50 cacheServerAddr string
51 cacheServerPort uint16
52 jsonLogger bool
53 noSkipCheckout bool
54 remoteName string
55 replaceGheActionWithGithubCom []string
56 replaceGheActionTokenWithGithubCom string
57 matrix []string
58 actionCachePath string
59 actionOfflineMode bool
60 logPrefixJobID bool
61 networkName string
62 useNewActionCache bool
63 localRepository []string
64 listOptions bool
65 validate bool
66 strict bool
67 concurrentJobs int
68}
69
70func (i *Input) resolve(path string) string {
71 basedir, err := filepath.Abs(i.workdir)
72 if err != nil {
73 log.Fatal(err)
74 }
75 if path == "" {
76 return path
77 }
78 if !filepath.IsAbs(path) {
79 path = filepath.Join(basedir, path)
80 }
81 return path
82}
83
84// Envfile returns path to .env
85func (i *Input) Envfile() string {
86 return i.resolve(i.envfile)
87}
88
89// Secretfile returns path to secrets
90func (i *Input) Secretfile() string {
91 return i.resolve(i.secretfile)
92}
93
94func (i *Input) Varfile() string {
95 return i.resolve(i.varfile)
96}
97
98// Workdir returns path to workdir
99func (i *Input) Workdir() string {
100 return i.resolve(".")
101}
102
103// WorkflowsPath returns path to workflow file(s)
104func (i *Input) WorkflowsPath() string {
105 return i.resolve(i.workflowsPath)
106}
107
108// EventPath returns the path to events file
109func (i *Input) EventPath() string {
110 return i.resolve(i.eventPath)
111}
112
113// Inputfile returns the path to the input file
114func (i *Input) Inputfile() string {
115 return i.resolve(i.inputfile)
116}
Deleted act/cmd/list.go
@@ -1,107 +0,0 @@
1package cmd
2
3import (
4 "fmt"
5 "strconv"
6 "strings"
7
8 "github.com/nektos/act/pkg/model"
9)
10
11func printList(plan *model.Plan) error {
12 type lineInfoDef struct {
13 jobID string
14 jobName string
15 stage string
16 wfName string
17 wfFile string
18 events string
19 }
20 lineInfos := []lineInfoDef{}
21
22 header := lineInfoDef{
23 jobID: "Job ID",
24 jobName: "Job name",
25 stage: "Stage",
26 wfName: "Workflow name",
27 wfFile: "Workflow file",
28 events: "Events",
29 }
30
31 jobs := map[string]bool{}
32 duplicateJobIDs := false
33
34 jobIDMaxWidth := len(header.jobID)
35 jobNameMaxWidth := len(header.jobName)
36 stageMaxWidth := len(header.stage)
37 wfNameMaxWidth := len(header.wfName)
38 wfFileMaxWidth := len(header.wfFile)
39 eventsMaxWidth := len(header.events)
40
41 for i, stage := range plan.Stages {
42 for _, r := range stage.Runs {
43 jobID := r.JobID
44 line := lineInfoDef{
45 jobID: jobID,
46 jobName: r.String(),
47 stage: strconv.Itoa(i),
48 wfName: r.Workflow.Name,
49 wfFile: r.Workflow.File,
50 events: strings.Join(r.Workflow.On(), `,`),
51 }
52 if _, ok := jobs[jobID]; ok {
53 duplicateJobIDs = true
54 } else {
55 jobs[jobID] = true
56 }
57 lineInfos = append(lineInfos, line)
58 if jobIDMaxWidth < len(line.jobID) {
59 jobIDMaxWidth = len(line.jobID)
60 }
61 if jobNameMaxWidth < len(line.jobName) {
62 jobNameMaxWidth = len(line.jobName)
63 }
64 if stageMaxWidth < len(line.stage) {
65 stageMaxWidth = len(line.stage)
66 }
67 if wfNameMaxWidth < len(line.wfName) {
68 wfNameMaxWidth = len(line.wfName)
69 }
70 if wfFileMaxWidth < len(line.wfFile) {
71 wfFileMaxWidth = len(line.wfFile)
72 }
73 if eventsMaxWidth < len(line.events) {
74 eventsMaxWidth = len(line.events)
75 }
76 }
77 }
78
79 jobIDMaxWidth += 2
80 jobNameMaxWidth += 2
81 stageMaxWidth += 2
82 wfNameMaxWidth += 2
83 wfFileMaxWidth += 2
84
85 fmt.Printf("%*s%*s%*s%*s%*s%*s\n",
86 -stageMaxWidth, header.stage,
87 -jobIDMaxWidth, header.jobID,
88 -jobNameMaxWidth, header.jobName,
89 -wfNameMaxWidth, header.wfName,
90 -wfFileMaxWidth, header.wfFile,
91 -eventsMaxWidth, header.events,
92 )
93 for _, line := range lineInfos {
94 fmt.Printf("%*s%*s%*s%*s%*s%*s\n",
95 -stageMaxWidth, line.stage,
96 -jobIDMaxWidth, line.jobID,
97 -jobNameMaxWidth, line.jobName,
98 -wfNameMaxWidth, line.wfName,
99 -wfFileMaxWidth, line.wfFile,
100 -eventsMaxWidth, line.events,
101 )
102 }
103 if duplicateJobIDs {
104 fmt.Print("\nDetected multiple jobs with the same job name, use `-W` to specify the path to the specific workflow.\n")
105 }
106 return nil
107}
Deleted act/cmd/notices.go
@@ -1,144 +0,0 @@
1package cmd
2
3import (
4 "encoding/json"
5 "fmt"
6 "net/http"
7 "net/url"
8 "os"
9 "path/filepath"
10 "runtime"
11 "strings"
12 "time"
13
14 log "github.com/sirupsen/logrus"
15)
16
17type Notice struct {
18 Level string `json:"level"`
19 Message string `json:"message"`
20}
21
22func displayNotices(input *Input) {
23 // Avoid causing trouble parsing the json
24 if input.listOptions {
25 return
26 }
27 select {
28 case notices := <-noticesLoaded:
29 if len(notices) > 0 {
30 noticeLogger := log.New()
31 if input.jsonLogger {
32 noticeLogger.SetFormatter(&log.JSONFormatter{})
33 } else {
34 noticeLogger.SetFormatter(&log.TextFormatter{
35 DisableQuote: true,
36 DisableTimestamp: true,
37 PadLevelText: true,
38 })
39 }
40
41 fmt.Printf("\n")
42 for _, notice := range notices {
43 level, err := log.ParseLevel(notice.Level)
44 if err != nil {
45 level = log.InfoLevel
46 }
47 noticeLogger.Log(level, notice.Message)
48 }
49 }
50 case <-time.After(time.Second * 1):
51 log.Debugf("Timeout waiting for notices")
52 }
53}
54
55var noticesLoaded = make(chan []Notice)
56
57func loadVersionNotices(version string) {
58 go func() {
59 noticesLoaded <- getVersionNotices(version)
60 }()
61}
62
63const NoticeURL = "https://api.nektosact.com/notices"
64
65func getVersionNotices(version string) []Notice {
66 if os.Getenv("ACT_DISABLE_VERSION_CHECK") == "1" {
67 return nil
68 }
69
70 noticeURL, err := url.Parse(NoticeURL)
71 if err != nil {
72 log.Error(err)
73 return nil
74 }
75 query := noticeURL.Query()
76 query.Add("os", runtime.GOOS)
77 query.Add("arch", runtime.GOARCH)
78 query.Add("version", version)
79
80 noticeURL.RawQuery = query.Encode()
81
82 client := &http.Client{}
83 req, err := http.NewRequest("GET", noticeURL.String(), nil)
84 if err != nil {
85 log.Debug(err)
86 return nil
87 }
88
89 etag := loadNoticesEtag()
90 if etag != "" {
91 log.Debugf("Conditional GET for notices etag=%s", etag)
92 req.Header.Set("If-None-Match", etag)
93 }
94
95 resp, err := client.Do(req)
96 if err != nil {
97 log.Debug(err)
98 return nil
99 }
100
101 newEtag := resp.Header.Get("Etag")
102 if newEtag != "" {
103 log.Debugf("Saving notices etag=%s", newEtag)
104 saveNoticesEtag(newEtag)
105 }
106
107 defer resp.Body.Close()
108 notices := []Notice{}
109 if resp.StatusCode == 304 {
110 log.Debug("No new notices")
111 return nil
112 }
113 if err := json.NewDecoder(resp.Body).Decode(&notices); err != nil {
114 log.Debug(err)
115 return nil
116 }
117
118 return notices
119}
120
121func loadNoticesEtag() string {
122 p := etagPath()
123 content, err := os.ReadFile(p)
124 if err != nil {
125 log.Debugf("Unable to load etag from %s: %e", p, err)
126 }
127 return strings.TrimSuffix(string(content), "\n")
128}
129
130func saveNoticesEtag(etag string) {
131 p := etagPath()
132 err := os.WriteFile(p, []byte(strings.TrimSuffix(etag, "\n")), 0o600)
133 if err != nil {
134 log.Debugf("Unable to save etag to %s: %e", p, err)
135 }
136}
137
138func etagPath() string {
139 dir := filepath.Join(CacheHomeDir, "act")
140 if err := os.MkdirAll(dir, 0o777); err != nil {
141 log.Fatal(err)
142 }
143 return filepath.Join(dir, ".notices.etag")
144}
Deleted act/cmd/platforms.go
@@ -1,22 +0,0 @@
1package cmd
2
3import (
4 "strings"
5)
6
7func (i *Input) newPlatforms() map[string]string {
8 platforms := map[string]string{
9 "ubuntu-latest": "node:16-buster-slim",
10 "ubuntu-22.04": "node:16-bullseye-slim",
11 "ubuntu-20.04": "node:16-buster-slim",
12 "ubuntu-18.04": "node:16-buster-slim",
13 }
14
15 for _, p := range i.platforms {
16 pParts := strings.Split(p, "=")
17 if len(pParts) == 2 {
18 platforms[strings.ToLower(pParts[0])] = pParts[1]
19 }
20 }
21 return platforms
22}
Deleted act/cmd/root.go
@@ -1,806 +0,0 @@
1package cmd
2
3import (
4 "bufio"
5 "bytes"
6 "context"
7 "encoding/json"
8 "fmt"
9 "os"
10 "path/filepath"
11 "regexp"
12 "runtime"
13 "runtime/debug"
14 "strings"
15
16 "github.com/AlecAivazis/survey/v2"
17 "github.com/adrg/xdg"
18 "github.com/andreaskoch/go-fswatch"
19 "github.com/joho/godotenv"
20 docker_container "github.com/moby/moby/api/types/container"
21 gitignore "github.com/sabhiram/go-gitignore"
22 log "github.com/sirupsen/logrus"
23 "github.com/spf13/cobra"
24 "github.com/spf13/cobra/doc"
25 "github.com/spf13/pflag"
26 "gopkg.in/yaml.v3"
27
28 "github.com/nektos/act/pkg/artifactcache"
29 "github.com/nektos/act/pkg/artifacts"
30 "github.com/nektos/act/pkg/common"
31 "github.com/nektos/act/pkg/container"
32 "github.com/nektos/act/pkg/gh"
33 "github.com/nektos/act/pkg/model"
34 "github.com/nektos/act/pkg/runner"
35)
36
37type Flag struct {
38 Name string `json:"name"`
39 Default string `json:"default"`
40 Type string `json:"type"`
41 Description string `json:"description"`
42}
43
44var exitFunc = os.Exit
45
46// Execute is the entry point to running the CLI
47func Execute(ctx context.Context, version string) {
48 input := new(Input)
49 rootCmd := createRootCommand(ctx, input, version)
50
51 if err := rootCmd.Execute(); err != nil {
52 exitFunc(1)
53 }
54}
55
56func createRootCommand(ctx context.Context, input *Input, version string) *cobra.Command {
57 rootCmd := &cobra.Command{
58 Use: "act [event name to run] [flags]\n\nIf no event name passed, will default to \"on: push\"\nIf actions handles only one event it will be used as default instead of \"on: push\"",
59 Short: "Run GitHub actions locally by specifying the event name (e.g. `push`) or an action name directly.",
60 Args: cobra.MaximumNArgs(1),
61 RunE: newRunCommand(ctx, input),
62 PersistentPreRun: setup(input),
63 PersistentPostRun: cleanup(input),
64 Version: version,
65 SilenceUsage: true,
66 }
67
68 rootCmd.Flags().BoolP("watch", "w", false, "watch the contents of the local repo and run when files change")
69 rootCmd.Flags().BoolVar(&input.validate, "validate", false, "validate workflows")
70 rootCmd.Flags().BoolVar(&input.strict, "strict", false, "use strict workflow schema")
71 rootCmd.Flags().BoolP("list", "l", false, "list workflows")
72 rootCmd.Flags().BoolP("graph", "g", false, "draw workflows")
73 rootCmd.Flags().StringP("job", "j", "", "run a specific job ID")
74 rootCmd.Flags().BoolP("bug-report", "", false, "Display system information for bug report")
75 rootCmd.Flags().BoolP("man-page", "", false, "Print a generated manual page to stdout")
76
77 rootCmd.Flags().StringVar(&input.remoteName, "remote-name", "origin", "git remote name that will be used to retrieve url of git repo")
78 rootCmd.Flags().StringArrayVarP(&input.secrets, "secret", "s", []string{}, "secret to make available to actions with optional value (e.g. -s mysecret=foo or -s mysecret)")
79 rootCmd.Flags().StringArrayVar(&input.vars, "var", []string{}, "variable to make available to actions with optional value (e.g. --var myvar=foo or --var myvar)")
80 rootCmd.Flags().StringArrayVarP(&input.envs, "env", "", []string{}, "env to make available to actions with optional value (e.g. --env myenv=foo or --env myenv)")
81 rootCmd.Flags().StringArrayVarP(&input.inputs, "input", "", []string{}, "action input to make available to actions (e.g. --input myinput=foo)")
82 rootCmd.Flags().StringArrayVarP(&input.platforms, "platform", "P", []string{}, "custom image to use per platform (e.g. -P ubuntu-18.04=nektos/act-environments-ubuntu:18.04)")
83 rootCmd.Flags().BoolVarP(&input.reuseContainers, "reuse", "r", false, "don't remove container(s) on successfully completed workflow(s) to maintain state between runs")
84 rootCmd.Flags().BoolVarP(&input.bindWorkdir, "bind", "b", false, "bind working directory to container, rather than copy")
85 rootCmd.Flags().BoolVarP(&input.forcePull, "pull", "p", true, "pull docker image(s) even if already present")
86 rootCmd.Flags().BoolVarP(&input.forceRebuild, "rebuild", "", true, "rebuild local action docker image(s) even if already present")
87 rootCmd.Flags().BoolVarP(&input.autodetectEvent, "detect-event", "", false, "Use first event type from workflow as event that triggered the workflow")
88 rootCmd.Flags().StringVarP(&input.eventPath, "eventpath", "e", "", "path to event JSON file")
89 rootCmd.Flags().StringVar(&input.defaultBranch, "defaultbranch", "", "the name of the main branch")
90 rootCmd.Flags().BoolVar(&input.privileged, "privileged", false, "use privileged mode")
91 rootCmd.Flags().StringVar(&input.usernsMode, "userns", "", "user namespace to use")
92 rootCmd.Flags().BoolVar(&input.useGitIgnore, "use-gitignore", true, "Controls whether paths specified in .gitignore should be copied into container")
93 rootCmd.Flags().StringArrayVarP(&input.containerCapAdd, "container-cap-add", "", []string{}, "kernel capabilities to add to the workflow containers (e.g. --container-cap-add SYS_PTRACE)")
94 rootCmd.Flags().StringArrayVarP(&input.containerCapDrop, "container-cap-drop", "", []string{}, "kernel capabilities to remove from the workflow containers (e.g. --container-cap-drop SYS_PTRACE)")
95 rootCmd.Flags().BoolVar(&input.autoRemove, "rm", false, "automatically remove container(s)/volume(s) after a workflow(s) failure")
96 rootCmd.Flags().StringArrayVarP(&input.replaceGheActionWithGithubCom, "replace-ghe-action-with-github-com", "", []string{}, "If you are using GitHub Enterprise Server and allow specified actions from GitHub (github.com), you can set actions on this. (e.g. --replace-ghe-action-with-github-com =github/super-linter)")
97 rootCmd.Flags().StringVar(&input.replaceGheActionTokenWithGithubCom, "replace-ghe-action-token-with-github-com", "", "If you are using replace-ghe-action-with-github-com and you want to use private actions on GitHub, you have to set personal access token")
98 rootCmd.Flags().StringArrayVarP(&input.matrix, "matrix", "", []string{}, "specify which matrix configuration to include (e.g. --matrix java:13")
99 rootCmd.PersistentFlags().StringVarP(&input.actor, "actor", "a", "nektos/act", "user that triggered the event")
100 rootCmd.PersistentFlags().StringVarP(&input.workflowsPath, "workflows", "W", "./.github/workflows/", "path to workflow file(s)")
101 rootCmd.PersistentFlags().BoolVarP(&input.noWorkflowRecurse, "no-recurse", "", false, "Flag to disable running workflows from subdirectories of specified path in '--workflows'/'-W' flag")
102 rootCmd.PersistentFlags().StringVarP(&input.workdir, "directory", "C", ".", "working directory")
103 rootCmd.PersistentFlags().BoolP("verbose", "v", false, "verbose output")
104 rootCmd.PersistentFlags().BoolVar(&input.jsonLogger, "json", false, "Output logs in json format")
105 rootCmd.PersistentFlags().BoolVar(&input.logPrefixJobID, "log-prefix-job-id", false, "Output the job id within non-json logs instead of the entire name")
106 rootCmd.PersistentFlags().BoolVarP(&input.noOutput, "quiet", "q", false, "disable logging of output from steps")
107 rootCmd.PersistentFlags().BoolVarP(&input.dryrun, "dryrun", "n", false, "disable container creation, validates only workflow correctness")
108 rootCmd.PersistentFlags().StringVarP(&input.secretfile, "secret-file", "", ".secrets", "file with list of secrets to read from (e.g. --secret-file .secrets)")
109 rootCmd.PersistentFlags().StringVarP(&input.varfile, "var-file", "", ".vars", "file with list of vars to read from (e.g. --var-file .vars)")
110 rootCmd.PersistentFlags().BoolVarP(&input.insecureSecrets, "insecure-secrets", "", false, "NOT RECOMMENDED! Doesn't hide secrets while printing logs.")
111 rootCmd.PersistentFlags().StringVarP(&input.envfile, "env-file", "", ".env", "environment file to read and use as env in the containers")
112 rootCmd.PersistentFlags().StringVarP(&input.inputfile, "input-file", "", ".input", "input file to read and use as action input")
113 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.")
114 rootCmd.PersistentFlags().StringVarP(&input.containerDaemonSocket, "container-daemon-socket", "", "", "URI to Docker Engine socket (e.g.: unix://~/.docker/run/docker.sock or - to disable bind mounting the socket)")
115 rootCmd.PersistentFlags().StringVarP(&input.containerOptions, "container-options", "", "", "Custom docker container options for the job container without an options property in the job definition")
116 rootCmd.PersistentFlags().StringVarP(&input.githubInstance, "github-instance", "", "github.com", "GitHub instance to use. Only use this when using GitHub Enterprise Server.")
117 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.")
118 rootCmd.PersistentFlags().StringVarP(&input.artifactServerAddr, "artifact-server-addr", "", common.GetOutboundIP().String(), "Defines the address to which the artifact server binds.")
119 rootCmd.PersistentFlags().StringVarP(&input.artifactServerPort, "artifact-server-port", "", "34567", "Defines the port where the artifact server listens.")
120 rootCmd.PersistentFlags().BoolVarP(&input.noSkipCheckout, "no-skip-checkout", "", false, "Use actions/checkout instead of copying local files into container")
121 rootCmd.PersistentFlags().BoolVarP(&input.noCacheServer, "no-cache-server", "", false, "Disable cache server")
122 rootCmd.PersistentFlags().StringVarP(&input.cacheServerPath, "cache-server-path", "", filepath.Join(CacheHomeDir, "actcache"), "Defines the path where the cache server stores caches.")
123 rootCmd.PersistentFlags().StringVarP(&input.cacheServerExternalURL, "cache-server-external-url", "", "", "Defines the external URL for if the cache server is behind a proxy. e.g.: https://act-cache-server.example.com. Be careful that there is no trailing slash.")
124 rootCmd.PersistentFlags().StringVarP(&input.cacheServerAddr, "cache-server-addr", "", common.GetOutboundIP().String(), "Defines the address to which the cache server binds.")
125 rootCmd.PersistentFlags().Uint16VarP(&input.cacheServerPort, "cache-server-port", "", 0, "Defines the port where the artifact server listens. 0 means a randomly available port.")
126 rootCmd.PersistentFlags().StringVarP(&input.actionCachePath, "action-cache-path", "", filepath.Join(CacheHomeDir, "act"), "Defines the path where the actions get cached and host workspaces created.")
127 rootCmd.PersistentFlags().BoolVarP(&input.actionOfflineMode, "action-offline-mode", "", false, "If action contents exists, it will not be fetch and pull again. If turn on this, will turn off force pull")
128 rootCmd.PersistentFlags().StringVarP(&input.networkName, "network", "", "host", "Sets a docker network name. Defaults to host.")
129 rootCmd.PersistentFlags().BoolVarP(&input.useNewActionCache, "use-new-action-cache", "", false, "Enable using the new Action Cache for storing Actions locally")
130 rootCmd.PersistentFlags().StringArrayVarP(&input.localRepository, "local-repository", "", []string{}, "Replaces the specified repository and ref with a local folder (e.g. https://github.com/test/test@v0=/home/act/test or test/test@v0=/home/act/test, the latter matches any hosts or protocols)")
131 rootCmd.PersistentFlags().BoolVar(&input.listOptions, "list-options", false, "Print a json structure of compatible options")
132 rootCmd.PersistentFlags().IntVar(&input.concurrentJobs, "concurrent-jobs", 0, "Maximum number of concurrent jobs to run. Default is the number of CPUs available.")
133 rootCmd.SetArgs(args())
134 return rootCmd
135}
136
137// Return locations where Act's config can be found in order: XDG spec, .actrc in HOME directory, .actrc in invocation directory
138func configLocations() []string {
139 configFileName := ".actrc"
140
141 homePath := filepath.Join(UserHomeDir, configFileName)
142 invocationPath := filepath.Join(".", configFileName)
143
144 // Though named xdg, adrg's lib support macOS and Windows config paths as well
145 // It also takes cares of creating the parent folder so we don't need to bother later
146 specPath, err := xdg.ConfigFile("act/actrc")
147 if err != nil {
148 specPath = homePath
149 }
150
151 // This order should be enforced since the survey part relies on it
152 return []string{specPath, homePath, invocationPath}
153}
154
155func args() []string {
156 actrc := configLocations()
157
158 args := make([]string, 0)
159 for _, f := range actrc {
160 args = append(args, readArgsFile(f, true)...)
161 }
162
163 args = append(args, os.Args[1:]...)
164 return args
165}
166
167func bugReport(ctx context.Context, version string) error {
168 sprintf := func(key, val string) string {
169 return fmt.Sprintf("%-24s%s\n", key, val)
170 }
171
172 report := sprintf("act version:", version)
173 report += sprintf("GOOS:", runtime.GOOS)
174 report += sprintf("GOARCH:", runtime.GOARCH)
175 report += sprintf("NumCPU:", fmt.Sprint(runtime.NumCPU()))
176
177 var dockerHost string
178 var exists bool
179 if dockerHost, exists = os.LookupEnv("DOCKER_HOST"); !exists {
180 dockerHost = "DOCKER_HOST environment variable is not set"
181 } else if dockerHost == "" {
182 dockerHost = "DOCKER_HOST environment variable is empty."
183 }
184
185 report += sprintf("Docker host:", dockerHost)
186 report += fmt.Sprintln("Sockets found:")
187 for _, p := range container.CommonSocketLocations {
188 if _, err := os.Lstat(os.ExpandEnv(p)); err != nil {
189 continue
190 } else if _, err := os.Stat(os.ExpandEnv(p)); err != nil {
191 report += fmt.Sprintf("\t%s(broken)\n", p)
192 } else {
193 report += fmt.Sprintf("\t%s\n", p)
194 }
195 }
196
197 report += sprintf("Config files:", "")
198 for _, c := range configLocations() {
199 args := readArgsFile(c, false)
200 if len(args) > 0 {
201 report += fmt.Sprintf("\t%s:\n", c)
202 for _, l := range args {
203 report += fmt.Sprintf("\t\t%s\n", l)
204 }
205 }
206 }
207
208 vcs, ok := debug.ReadBuildInfo()
209 if ok && vcs != nil {
210 report += fmt.Sprintln("Build info:")
211 vcs := *vcs
212 report += sprintf("\tGo version:", vcs.GoVersion)
213 report += sprintf("\tModule path:", vcs.Path)
214 report += sprintf("\tMain version:", vcs.Main.Version)
215 report += sprintf("\tMain path:", vcs.Main.Path)
216 report += sprintf("\tMain checksum:", vcs.Main.Sum)
217
218 report += fmt.Sprintln("\tBuild settings:")
219 for _, set := range vcs.Settings {
220 report += sprintf(fmt.Sprintf("\t\t%s:", set.Key), set.Value)
221 }
222 }
223
224 info, err := container.GetHostInfo(ctx)
225 if err != nil {
226 fmt.Println(report)
227 return err
228 }
229
230 report += fmt.Sprintln("Docker Engine:")
231
232 report += sprintf("\tEngine version:", info.ServerVersion)
233 report += sprintf("\tEngine runtime:", info.DefaultRuntime)
234 report += sprintf("\tCgroup version:", info.CgroupVersion)
235 report += sprintf("\tCgroup driver:", info.CgroupDriver)
236 report += sprintf("\tStorage driver:", info.Driver)
237 report += sprintf("\tRegistry URI:", info.IndexServerAddress)
238
239 report += sprintf("\tOS:", info.OperatingSystem)
240 report += sprintf("\tOS type:", info.OSType)
241 report += sprintf("\tOS version:", info.OSVersion)
242 report += sprintf("\tOS arch:", info.Architecture)
243 report += sprintf("\tOS kernel:", info.KernelVersion)
244 report += sprintf("\tOS CPU:", fmt.Sprint(info.NCPU))
245 report += sprintf("\tOS memory:", fmt.Sprintf("%d MB", info.MemTotal/1024/1024))
246
247 report += fmt.Sprintln("\tSecurity options:")
248 for _, secopt := range info.SecurityOptions {
249 report += fmt.Sprintf("\t\t%s\n", secopt)
250 }
251
252 fmt.Println(report)
253 return nil
254}
255
256func generateManPage(cmd *cobra.Command) error {
257 header := &doc.GenManHeader{
258 Title: "act",
259 Section: "1",
260 Source: fmt.Sprintf("act %s", cmd.Version),
261 }
262 buf := new(bytes.Buffer)
263 cobra.CheckErr(doc.GenMan(cmd, header, buf))
264 fmt.Print(buf.String())
265 return nil
266}
267
268func listOptions(cmd *cobra.Command) error {
269 flags := []Flag{}
270 cmd.LocalFlags().VisitAll(func(f *pflag.Flag) {
271 flags = append(flags, Flag{Name: f.Name, Default: f.DefValue, Description: f.Usage, Type: f.Value.Type()})
272 })
273 a, err := json.Marshal(flags)
274 fmt.Println(string(a))
275 return err
276}
277
278func readArgsFile(file string, split bool) []string {
279 args := make([]string, 0)
280 f, err := os.Open(file)
281 if err != nil {
282 return args
283 }
284 defer func() {
285 err := f.Close()
286 if err != nil {
287 log.Errorf("Failed to close args file: %v", err)
288 }
289 }()
290 scanner := bufio.NewScanner(f)
291 scanner.Buffer(nil, 1024*1024*1024) // increase buffer to 1GB to avoid scanner buffer overflow
292 for scanner.Scan() {
293 arg := os.ExpandEnv(strings.TrimSpace(scanner.Text()))
294
295 if strings.HasPrefix(arg, "-") && split {
296 args = append(args, regexp.MustCompile(`\s`).Split(arg, 2)...)
297 } else if !split {
298 args = append(args, arg)
299 }
300 }
301 return args
302}
303
304func setup(_ *Input) func(*cobra.Command, []string) {
305 return func(cmd *cobra.Command, _ []string) {
306 verbose, _ := cmd.Flags().GetBool("verbose")
307 if verbose {
308 log.SetLevel(log.DebugLevel)
309 }
310 loadVersionNotices(cmd.Version)
311 }
312}
313
314func cleanup(inputs *Input) func(*cobra.Command, []string) {
315 return func(_ *cobra.Command, _ []string) {
316 displayNotices(inputs)
317 }
318}
319
320func parseEnvs(env []string) map[string]string {
321 envs := make(map[string]string, len(env))
322 for _, envVar := range env {
323 e := strings.SplitN(envVar, `=`, 2)
324 if len(e) == 2 {
325 envs[e[0]] = e[1]
326 } else {
327 envs[e[0]] = ""
328 }
329 }
330 return envs
331}
332
333func readYamlFile(file string) (map[string]string, error) {
334 content, err := os.ReadFile(file)
335 if err != nil {
336 return nil, err
337 }
338 ret := map[string]string{}
339 if err = yaml.Unmarshal(content, &ret); err != nil {
340 return nil, err
341 }
342 return ret, nil
343}
344
345func readEnvs(path string, envs map[string]string) bool {
346 return readEnvsEx(path, envs, false)
347}
348
349func readEnvsEx(path string, envs map[string]string, caseInsensitive bool) bool {
350 if _, err := os.Stat(path); err == nil {
351 var env map[string]string
352 if ext := filepath.Ext(path); ext == ".yml" || ext == ".yaml" {
353 env, err = readYamlFile(path)
354 } else {
355 env, err = godotenv.Read(path)
356 }
357 if err != nil {
358 log.Fatalf("Error loading from %s: %v", path, err)
359 }
360 for k, v := range env {
361 if caseInsensitive {
362 k = strings.ToUpper(k)
363 }
364 if _, ok := envs[k]; !ok {
365 envs[k] = v
366 }
367 }
368 return true
369 }
370 return false
371}
372
373func parseMatrix(matrix []string) map[string]map[string]bool {
374 // each matrix entry should be of the form - string:string
375 r := regexp.MustCompile(":")
376 matrixes := make(map[string]map[string]bool)
377 for _, m := range matrix {
378 matrix := r.Split(m, 2)
379 if len(matrix) < 2 {
380 log.Fatalf("Invalid matrix format. Failed to parse %s", m)
381 }
382 if _, ok := matrixes[matrix[0]]; !ok {
383 matrixes[matrix[0]] = make(map[string]bool)
384 }
385 matrixes[matrix[0]][matrix[1]] = true
386 }
387 return matrixes
388}
389
390//nolint:gocyclo
391func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []string) error {
392 return func(cmd *cobra.Command, args []string) error {
393 if input.jsonLogger {
394 log.SetFormatter(&log.JSONFormatter{})
395 }
396
397 if ok, _ := cmd.Flags().GetBool("bug-report"); ok {
398 ctx, cancel := common.EarlyCancelContext(ctx)
399 defer cancel()
400 return bugReport(ctx, cmd.Version)
401 }
402 if ok, _ := cmd.Flags().GetBool("man-page"); ok {
403 return generateManPage(cmd)
404 }
405 if input.listOptions {
406 return listOptions(cmd)
407 }
408
409 if ret, err := container.GetSocketAndHost(input.containerDaemonSocket); err != nil {
410 log.Warnf("Couldn't get a valid docker connection: %+v", err)
411 } else {
412 os.Setenv("DOCKER_HOST", ret.Host)
413 input.containerDaemonSocket = ret.Socket
414 log.Infof("Using docker host '%s', and daemon socket '%s'", ret.Host, ret.Socket)
415 }
416
417 if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" && input.containerArchitecture == "" {
418 l := log.New()
419 l.SetFormatter(&log.TextFormatter{
420 DisableQuote: true,
421 DisableTimestamp: true,
422 })
423 l.Warnf(" \U000026A0 You are using Apple M-series chip and you have not specified container architecture, you might encounter issues while running act. If so, try running it with '--container-architecture linux/amd64'. \U000026A0 \n")
424 }
425
426 log.Debugf("Loading environment from %s", input.Envfile())
427 envs := parseEnvs(input.envs)
428 _ = readEnvs(input.Envfile(), envs)
429
430 log.Debugf("Loading action inputs from %s", input.Inputfile())
431 inputs := parseEnvs(input.inputs)
432 _ = readEnvs(input.Inputfile(), inputs)
433
434 log.Debugf("Loading secrets from %s", input.Secretfile())
435 secrets := newSecrets(input.secrets)
436 _ = readEnvsEx(input.Secretfile(), secrets, true)
437
438 if _, hasGitHubToken := secrets["GITHUB_TOKEN"]; !hasGitHubToken {
439 ctx, cancel := common.EarlyCancelContext(ctx)
440 defer cancel()
441 secrets["GITHUB_TOKEN"], _ = gh.GetToken(ctx, "")
442 }
443
444 log.Debugf("Loading vars from %s", input.Varfile())
445 vars := newSecrets(input.vars)
446 _ = readEnvs(input.Varfile(), vars)
447
448 matrixes := parseMatrix(input.matrix)
449 log.Debugf("Evaluated matrix inclusions: %v", matrixes)
450
451 planner, err := model.NewWorkflowPlanner(input.WorkflowsPath(), input.noWorkflowRecurse, input.strict)
452 if err != nil {
453 return err
454 }
455
456 jobID, err := cmd.Flags().GetString("job")
457 if err != nil {
458 return err
459 }
460
461 // check if we should just list the workflows
462 list, err := cmd.Flags().GetBool("list")
463 if err != nil {
464 return err
465 }
466
467 // check if we should just validate the workflows
468 if input.validate {
469 return err
470 }
471
472 // check if we should just draw the graph
473 graph, err := cmd.Flags().GetBool("graph")
474 if err != nil {
475 return err
476 }
477
478 // collect all events from loaded workflows
479 events := planner.GetEvents()
480
481 // plan with filtered jobs - to be used for filtering only
482 var filterPlan *model.Plan
483
484 // Determine the event name to be filtered
485 var filterEventName string
486
487 if len(args) > 0 {
488 log.Debugf("Using first passed in arguments event for filtering: %s", args[0])
489 filterEventName = args[0]
490 } else if input.autodetectEvent && len(events) > 0 && len(events[0]) > 0 {
491 // set default event type to first event from many available
492 // this way user dont have to specify the event.
493 log.Debugf("Using first detected workflow event for filtering: %s", events[0])
494 filterEventName = events[0]
495 }
496
497 var plannerErr error
498 if jobID != "" {
499 log.Debugf("Preparing plan with a job: %s", jobID)
500 filterPlan, plannerErr = planner.PlanJob(jobID)
501 } else if filterEventName != "" {
502 log.Debugf("Preparing plan for a event: %s", filterEventName)
503 filterPlan, plannerErr = planner.PlanEvent(filterEventName)
504 } else {
505 log.Debugf("Preparing plan with all jobs")
506 filterPlan, plannerErr = planner.PlanAll()
507 }
508 if filterPlan == nil && plannerErr != nil {
509 return plannerErr
510 }
511
512 if list {
513 err = printList(filterPlan)
514 if err != nil {
515 return err
516 }
517 return plannerErr
518 }
519
520 if graph {
521 err = drawGraph(filterPlan)
522 if err != nil {
523 return err
524 }
525 return plannerErr
526 }
527
528 // plan with triggered jobs
529 var plan *model.Plan
530
531 // Determine the event name to be triggered
532 var eventName string
533
534 if len(args) > 0 {
535 log.Debugf("Using first passed in arguments event: %s", args[0])
536 eventName = args[0]
537 } else if len(events) == 1 && len(events[0]) > 0 {
538 log.Debugf("Using the only detected workflow event: %s", events[0])
539 eventName = events[0]
540 } else if input.autodetectEvent && len(events) > 0 && len(events[0]) > 0 {
541 // set default event type to first event from many available
542 // this way user dont have to specify the event.
543 log.Debugf("Using first detected workflow event: %s", events[0])
544 eventName = events[0]
545 } else {
546 log.Debugf("Using default workflow event: push")
547 eventName = "push"
548 }
549
550 // build the plan for this run
551 if jobID != "" {
552 log.Debugf("Planning job: %s", jobID)
553 plan, plannerErr = planner.PlanJob(jobID)
554 } else {
555 log.Debugf("Planning jobs for event: %s", eventName)
556 plan, plannerErr = planner.PlanEvent(eventName)
557 }
558 if plan != nil {
559 if len(plan.Stages) == 0 {
560 plannerErr = fmt.Errorf("Could not find any stages to run. View the valid jobs with `act --list`. Use `act --help` to find how to filter by Job ID/Workflow/Event Name")
561 }
562 }
563 if plan == nil && plannerErr != nil {
564 return plannerErr
565 }
566
567 // check to see if the main branch was defined
568 defaultbranch, err := cmd.Flags().GetString("defaultbranch")
569 if err != nil {
570 return err
571 }
572
573 // Check if platforms flag is set, if not, run default image survey
574 if len(input.platforms) == 0 {
575 cfgFound := false
576 cfgLocations := configLocations()
577 for _, v := range cfgLocations {
578 _, err := os.Stat(v)
579 if os.IsExist(err) {
580 cfgFound = true
581 }
582 }
583 if !cfgFound && len(cfgLocations) > 0 {
584 // The first config location refers to the global config folder one
585 if err := defaultImageSurvey(cfgLocations[0]); err != nil {
586 log.Fatal(err)
587 }
588 input.platforms = readArgsFile(cfgLocations[0], true)
589 }
590 }
591 deprecationWarning := "--%s is deprecated and will be removed soon, please switch to cli: `--container-options \"%[2]s\"` or `.actrc`: `--container-options %[2]s`."
592 if input.privileged {
593 log.Warnf(deprecationWarning, "privileged", "--privileged")
594 }
595 if len(input.usernsMode) > 0 {
596 log.Warnf(deprecationWarning, "userns", fmt.Sprintf("--userns=%s", input.usernsMode))
597 }
598 if len(input.containerCapAdd) > 0 {
599 log.Warnf(deprecationWarning, "container-cap-add", fmt.Sprintf("--cap-add=%s", input.containerCapAdd))
600 }
601 if len(input.containerCapDrop) > 0 {
602 log.Warnf(deprecationWarning, "container-cap-drop", fmt.Sprintf("--cap-drop=%s", input.containerCapDrop))
603 }
604
605 // run the plan
606 config := &runner.Config{
607 Actor: input.actor,
608 EventName: eventName,
609 EventPath: input.EventPath(),
610 DefaultBranch: defaultbranch,
611 ForcePull: !input.actionOfflineMode && input.forcePull,
612 ForceRebuild: input.forceRebuild,
613 ReuseContainers: input.reuseContainers,
614 Workdir: input.Workdir(),
615 ActionCacheDir: input.actionCachePath,
616 ActionOfflineMode: input.actionOfflineMode,
617 BindWorkdir: input.bindWorkdir,
618 LogOutput: !input.noOutput,
619 JSONLogger: input.jsonLogger,
620 LogPrefixJobID: input.logPrefixJobID,
621 Env: envs,
622 Secrets: secrets,
623 Vars: vars,
624 Inputs: inputs,
625 Token: secrets["GITHUB_TOKEN"],
626 InsecureSecrets: input.insecureSecrets,
627 Platforms: input.newPlatforms(),
628 Privileged: input.privileged,
629 UsernsMode: input.usernsMode,
630 ContainerArchitecture: input.containerArchitecture,
631 ContainerDaemonSocket: input.containerDaemonSocket,
632 ContainerOptions: input.containerOptions,
633 UseGitIgnore: input.useGitIgnore,
634 GitHubInstance: input.githubInstance,
635 ContainerCapAdd: input.containerCapAdd,
636 ContainerCapDrop: input.containerCapDrop,
637 AutoRemove: input.autoRemove,
638 ArtifactServerPath: input.artifactServerPath,
639 ArtifactServerAddr: input.artifactServerAddr,
640 ArtifactServerPort: input.artifactServerPort,
641 NoSkipCheckout: input.noSkipCheckout,
642 RemoteName: input.remoteName,
643 ReplaceGheActionWithGithubCom: input.replaceGheActionWithGithubCom,
644 ReplaceGheActionTokenWithGithubCom: input.replaceGheActionTokenWithGithubCom,
645 Matrix: matrixes,
646 ContainerNetworkMode: docker_container.NetworkMode(input.networkName),
647 ConcurrentJobs: input.concurrentJobs,
648 }
649 if input.useNewActionCache || len(input.localRepository) > 0 {
650 if input.actionOfflineMode {
651 config.ActionCache = &runner.GoGitActionCacheOfflineMode{
652 Parent: runner.GoGitActionCache{
653 Path: config.ActionCacheDir,
654 },
655 }
656 } else {
657 config.ActionCache = &runner.GoGitActionCache{
658 Path: config.ActionCacheDir,
659 }
660 }
661 if len(input.localRepository) > 0 {
662 localRepositories := map[string]string{}
663 for _, l := range input.localRepository {
664 k, v, _ := strings.Cut(l, "=")
665 localRepositories[k] = v
666 }
667 config.ActionCache = &runner.LocalRepositoryCache{
668 Parent: config.ActionCache,
669 LocalRepositories: localRepositories,
670 CacheDirCache: map[string]string{},
671 }
672 }
673 }
674 r, err := runner.New(config)
675 if err != nil {
676 return err
677 }
678
679 cancel := artifacts.Serve(ctx, input.artifactServerPath, input.artifactServerAddr, input.artifactServerPort)
680
681 const cacheURLKey = "ACTIONS_CACHE_URL"
682 var cacheHandler *artifactcache.Handler
683 if !input.noCacheServer && envs[cacheURLKey] == "" {
684 var err error
685 cacheHandler, err = artifactcache.StartHandler(input.cacheServerPath, input.cacheServerExternalURL, input.cacheServerAddr, input.cacheServerPort, common.Logger(ctx))
686 if err != nil {
687 return err
688 }
689 envs[cacheURLKey] = cacheHandler.ExternalURL() + "/"
690 }
691
692 ctx = common.WithDryrun(ctx, input.dryrun)
693 if watch, err := cmd.Flags().GetBool("watch"); err != nil {
694 return err
695 } else if watch {
696 err = watchAndRun(ctx, r.NewPlanExecutor(plan))
697 if err != nil {
698 return err
699 }
700 return plannerErr
701 }
702
703 executor := r.NewPlanExecutor(plan).Finally(func(_ context.Context) error {
704 cancel()
705 _ = cacheHandler.Close()
706 return nil
707 })
708 err = executor(ctx)
709 if err != nil {
710 return err
711 }
712 return plannerErr
713 }
714}
715
716func defaultImageSurvey(actrc string) error {
717 var answer string
718 confirmation := &survey.Select{
719 Message: "Please choose the default image you want to use with act:\n - Large size image: ca. 17GB download + 53.1GB storage, you will need 75GB of free disk space, snapshots of GitHub Hosted Runners without snap and pulled docker images\n - Medium size image: ~500MB, includes only necessary tools to bootstrap actions and aims to be compatible with most actions\n - Micro size image: <200MB, contains only NodeJS required to bootstrap actions, doesn't work with all actions\n\nDefault image and other options can be changed manually in " + configLocations()[0] + " (please refer to https://nektosact.com/usage/index.html?highlight=configur#configuration-file for additional information about file structure)",
720 Help: "If you want to know why act asks you that, please go to https://github.com/nektos/act/issues/107",
721 Default: "Medium",
722 Options: []string{"Large", "Medium", "Micro"},
723 }
724
725 err := survey.AskOne(confirmation, &answer)
726 if err != nil {
727 return err
728 }
729
730 var option string
731 switch answer {
732 case "Large":
733 option = "-P ubuntu-latest=catthehacker/ubuntu:full-latest\n-P ubuntu-22.04=catthehacker/ubuntu:full-22.04\n-P ubuntu-20.04=catthehacker/ubuntu:full-20.04\n-P ubuntu-18.04=catthehacker/ubuntu:full-18.04\n"
734 case "Medium":
735 option = "-P ubuntu-latest=catthehacker/ubuntu:act-latest\n-P ubuntu-22.04=catthehacker/ubuntu:act-22.04\n-P ubuntu-20.04=catthehacker/ubuntu:act-20.04\n-P ubuntu-18.04=catthehacker/ubuntu:act-18.04\n"
736 case "Micro":
737 option = "-P ubuntu-latest=node:16-buster-slim\n-P ubuntu-22.04=node:16-bullseye-slim\n-P ubuntu-20.04=node:16-buster-slim\n-P ubuntu-18.04=node:16-buster-slim\n"
738 }
739
740 f, err := os.Create(actrc)
741 if err != nil {
742 return err
743 }
744
745 _, err = f.WriteString(option)
746 if err != nil {
747 _ = f.Close()
748 return err
749 }
750
751 err = f.Close()
752 if err != nil {
753 return err
754 }
755
756 return nil
757}
758
759func watchAndRun(ctx context.Context, fn common.Executor) error {
760 dir, err := os.Getwd()
761 if err != nil {
762 return err
763 }
764
765 ignoreFile := filepath.Join(dir, ".gitignore")
766 ignore := &gitignore.GitIgnore{}
767 if info, err := os.Stat(ignoreFile); err == nil && !info.IsDir() {
768 ignore, err = gitignore.CompileIgnoreFile(ignoreFile)
769 if err != nil {
770 return fmt.Errorf("compile %q: %w", ignoreFile, err)
771 }
772 }
773
774 folderWatcher := fswatch.NewFolderWatcher(
775 dir,
776 true,
777 ignore.MatchesPath,
778 2, // 2 seconds
779 )
780
781 folderWatcher.Start()
782 defer folderWatcher.Stop()
783
784 // run once before watching
785 if err := fn(ctx); err != nil {
786 return err
787 }
788
789 earlyCancelCtx, cancel := common.EarlyCancelContext(ctx)
790 defer cancel()
791
792 for folderWatcher.IsRunning() {
793 log.Debugf("Watching %s for changes", dir)
794 select {
795 case <-earlyCancelCtx.Done():
796 return nil
797 case changes := <-folderWatcher.ChangeDetails():
798 log.Debugf("%s", changes.String())
799 if err := fn(ctx); err != nil {
800 return err
801 }
802 }
803 }
804
805 return nil
806}
Deleted act/cmd/secrets.go
@@ -1,42 +0,0 @@
1package cmd
2
3import (
4 "fmt"
5 "os"
6 "strings"
7
8 log "github.com/sirupsen/logrus"
9 "golang.org/x/term"
10)
11
12type secrets map[string]string
13
14func newSecrets(secretList []string) secrets {
15 s := make(map[string]string)
16 for _, secretPair := range secretList {
17 secretPairParts := strings.SplitN(secretPair, "=", 2)
18 secretPairParts[0] = strings.ToUpper(secretPairParts[0])
19 if strings.ToUpper(s[secretPairParts[0]]) == secretPairParts[0] {
20 log.Errorf("Secret %s is already defined (secrets are case insensitive)", secretPairParts[0])
21 }
22 if len(secretPairParts) == 2 {
23 s[secretPairParts[0]] = secretPairParts[1]
24 } else if env, ok := os.LookupEnv(secretPairParts[0]); ok && env != "" {
25 s[secretPairParts[0]] = env
26 } else {
27 fmt.Printf("Provide value for '%s': ", secretPairParts[0])
28 val, err := term.ReadPassword(int(os.Stdin.Fd()))
29 fmt.Println()
30 if err != nil {
31 log.Errorf("failed to read input: %v", err)
32 os.Exit(1)
33 }
34 s[secretPairParts[0]] = string(val)
35 }
36 }
37 return s
38}
39
40func (s secrets) AsMap() map[string]string {
41 return s
42}
Deleted act/pkg/artifactcache/doc.go
@@ -1,7 +0,0 @@
1// Package artifactcache provides a cache handler for the runner.
2//
3// Inspired by https://github.com/sp-ricard-valverde/github-act-cache-server
4//
5// TODO: Restrictions for accessing a cache, see https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#restrictions-for-accessing-a-cache
6// TODO: Force deleting cache entries, see https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries
7package artifactcache
Deleted act/pkg/artifactcache/handler.go
@@ -1,576 +0,0 @@
1package artifactcache
2
3import (
4 "crypto/rand"
5 "encoding/hex"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "io"
10 "net"
11 "net/http"
12 "os"
13 "path/filepath"
14 "regexp"
15 "strconv"
16 "strings"
17 "sync/atomic"
18 "time"
19
20 "github.com/julienschmidt/httprouter"
21 "github.com/sirupsen/logrus"
22 "github.com/timshannon/bolthold"
23 "go.etcd.io/bbolt"
24
25 "github.com/nektos/act/pkg/common"
26)
27
28const (
29 apiPath = "/_apis/artifactcache"
30)
31
32type Handler struct {
33 dir string
34 storage *Storage
35 router *httprouter.Router
36 listener net.Listener
37 server *http.Server
38 logger logrus.FieldLogger
39
40 gcing atomic.Bool
41 gcAt time.Time
42
43 outboundIP string
44 customExternalURL string
45 token string
46}
47
48func StartHandler(dir, customExternalURL string, outboundIP string, port uint16, logger logrus.FieldLogger) (*Handler, error) {
49 h := &Handler{}
50
51 if logger == nil {
52 discard := logrus.New()
53 discard.Out = io.Discard
54 logger = discard
55 }
56 logger = logger.WithField("module", "artifactcache")
57 h.logger = logger
58
59 if dir == "" {
60 home, err := os.UserHomeDir()
61 if err != nil {
62 return nil, err
63 }
64 dir = filepath.Join(home, ".cache", "actcache")
65 }
66 if err := os.MkdirAll(dir, 0o755); err != nil {
67 return nil, err
68 }
69
70 h.dir = dir
71
72 storage, err := NewStorage(filepath.Join(dir, "cache"))
73 if err != nil {
74 return nil, err
75 }
76 h.storage = storage
77
78 if customExternalURL != "" {
79 h.customExternalURL = customExternalURL
80 }
81
82 if outboundIP != "" {
83 h.outboundIP = outboundIP
84 } else if ip := common.GetOutboundIP(); ip == nil {
85 return nil, fmt.Errorf("unable to determine outbound IP address")
86 } else {
87 h.outboundIP = ip.String()
88 }
89
90 tokenBytes := make([]byte, 16)
91 if _, err := rand.Read(tokenBytes); err != nil {
92 return nil, fmt.Errorf("generate auth token: %w", err)
93 }
94 h.token = hex.EncodeToString(tokenBytes)
95
96 router := httprouter.New()
97 base := "/" + h.token + apiPath
98 router.GET(base+"/cache", h.middleware(h.find))
99 router.POST(base+"/caches", h.middleware(h.reserve))
100 router.PATCH(base+"/caches/:id", h.middleware(h.upload))
101 router.POST(base+"/caches/:id", h.middleware(h.commit))
102 router.GET(base+"/artifacts/:id", h.middleware(h.get))
103 router.POST(base+"/clean", h.middleware(h.clean))
104
105 h.router = router
106
107 h.gcCache()
108
109 listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", h.outboundIP, port))
110 if err != nil {
111 return nil, err
112 }
113
114 server := &http.Server{
115 ReadHeaderTimeout: 2 * time.Second,
116 Handler: router,
117 }
118 go func() {
119 if err := server.Serve(listener); err != nil && errors.Is(err, net.ErrClosed) {
120 logger.Errorf("http serve: %v", err)
121 }
122 }()
123 h.listener = listener
124 h.server = server
125
126 return h, nil
127}
128
129func (h *Handler) GetActualPort() int {
130 return h.listener.Addr().(*net.TCPAddr).Port
131}
132
133func (h *Handler) ExternalURL() string {
134 if h.customExternalURL != "" {
135 return h.customExternalURL + "/" + h.token
136 }
137 return fmt.Sprintf("http://%s:%d/%s", h.outboundIP, h.GetActualPort(), h.token)
138}
139
140func (h *Handler) Close() error {
141 if h == nil {
142 return nil
143 }
144 var retErr error
145 if h.server != nil {
146 err := h.server.Close()
147 if err != nil {
148 retErr = err
149 }
150 h.server = nil
151 }
152 if h.listener != nil {
153 err := h.listener.Close()
154 if errors.Is(err, net.ErrClosed) {
155 err = nil
156 }
157 if err != nil {
158 retErr = err
159 }
160 h.listener = nil
161 }
162 return retErr
163}
164
165func (h *Handler) openDB() (*bolthold.Store, error) {
166 return bolthold.Open(filepath.Join(h.dir, "bolt.db"), 0o644, &bolthold.Options{
167 Encoder: json.Marshal,
168 Decoder: json.Unmarshal,
169 Options: &bbolt.Options{
170 Timeout: 5 * time.Second,
171 NoGrowSync: bbolt.DefaultOptions.NoGrowSync,
172 FreelistType: bbolt.DefaultOptions.FreelistType,
173 },
174 })
175}
176
177// GET /_apis/artifactcache/cache
178func (h *Handler) find(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
179 keys := strings.Split(r.URL.Query().Get("keys"), ",")
180 // cache keys are case insensitive
181 for i, key := range keys {
182 keys[i] = strings.ToLower(key)
183 }
184 version := r.URL.Query().Get("version")
185
186 db, err := h.openDB()
187 if err != nil {
188 h.responseJSON(w, r, 500, err)
189 return
190 }
191 defer db.Close()
192
193 cache, err := findCache(db, keys, version)
194 if err != nil {
195 h.responseJSON(w, r, 500, err)
196 return
197 }
198 if cache == nil {
199 h.responseJSON(w, r, 204)
200 return
201 }
202
203 if ok, err := h.storage.Exist(cache.ID); err != nil {
204 h.responseJSON(w, r, 500, err)
205 return
206 } else if !ok {
207 _ = db.Delete(cache.ID, cache)
208 h.responseJSON(w, r, 204)
209 return
210 }
211 h.responseJSON(w, r, 200, map[string]any{
212 "result": "hit",
213 "archiveLocation": fmt.Sprintf("%s%s/artifacts/%d", h.ExternalURL(), apiPath, cache.ID),
214 "cacheKey": cache.Key,
215 })
216}
217
218// POST /_apis/artifactcache/caches
219func (h *Handler) reserve(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
220 api := &Request{}
221 if err := json.NewDecoder(r.Body).Decode(api); err != nil {
222 h.responseJSON(w, r, 400, err)
223 return
224 }
225 // cache keys are case insensitive
226 api.Key = strings.ToLower(api.Key)
227
228 cache := api.ToCache()
229 db, err := h.openDB()
230 if err != nil {
231 h.responseJSON(w, r, 500, err)
232 return
233 }
234 defer db.Close()
235
236 now := time.Now().Unix()
237 cache.CreatedAt = now
238 cache.UsedAt = now
239 if err := insertCache(db, cache); err != nil {
240 h.responseJSON(w, r, 500, err)
241 return
242 }
243 h.responseJSON(w, r, 200, map[string]any{
244 "cacheId": cache.ID,
245 })
246}
247
248// PATCH /_apis/artifactcache/caches/:id
249func (h *Handler) upload(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
250 id, err := strconv.ParseUint(params.ByName("id"), 10, 64)
251 if err != nil {
252 h.responseJSON(w, r, 400, err)
253 return
254 }
255
256 cache := &Cache{}
257 db, err := h.openDB()
258 if err != nil {
259 h.responseJSON(w, r, 500, err)
260 return
261 }
262 defer db.Close()
263 if err := db.Get(id, cache); err != nil {
264 if errors.Is(err, bolthold.ErrNotFound) {
265 h.responseJSON(w, r, 400, fmt.Errorf("cache %d: not reserved", id))
266 return
267 }
268 h.responseJSON(w, r, 500, err)
269 return
270 }
271
272 if cache.Complete {
273 h.responseJSON(w, r, 400, fmt.Errorf("cache %v %q: already complete", cache.ID, cache.Key))
274 return
275 }
276 db.Close()
277 start, _, err := parseContentRange(r.Header.Get("Content-Range"))
278 if err != nil {
279 h.responseJSON(w, r, 400, err)
280 return
281 }
282 if err := h.storage.Write(cache.ID, start, r.Body); err != nil {
283 h.responseJSON(w, r, 500, err)
284 }
285 h.useCache(id)
286 h.responseJSON(w, r, 200)
287}
288
289// POST /_apis/artifactcache/caches/:id
290func (h *Handler) commit(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
291 id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
292 if err != nil {
293 h.responseJSON(w, r, 400, err)
294 return
295 }
296
297 cache := &Cache{}
298 db, err := h.openDB()
299 if err != nil {
300 h.responseJSON(w, r, 500, err)
301 return
302 }
303 defer db.Close()
304 if err := db.Get(id, cache); err != nil {
305 if errors.Is(err, bolthold.ErrNotFound) {
306 h.responseJSON(w, r, 400, fmt.Errorf("cache %d: not reserved", id))
307 return
308 }
309 h.responseJSON(w, r, 500, err)
310 return
311 }
312
313 if cache.Complete {
314 h.responseJSON(w, r, 400, fmt.Errorf("cache %v %q: already complete", cache.ID, cache.Key))
315 return
316 }
317
318 db.Close()
319
320 size, err := h.storage.Commit(cache.ID, cache.Size)
321 if err != nil {
322 h.responseJSON(w, r, 500, err)
323 return
324 }
325 // write real size back to cache, it may be different from the current value when the request doesn't specify it.
326 cache.Size = size
327
328 db, err = h.openDB()
329 if err != nil {
330 h.responseJSON(w, r, 500, err)
331 return
332 }
333 defer db.Close()
334
335 cache.Complete = true
336 if err := db.Update(cache.ID, cache); err != nil {
337 h.responseJSON(w, r, 500, err)
338 return
339 }
340
341 h.responseJSON(w, r, 200)
342}
343
344// GET /_apis/artifactcache/artifacts/:id
345func (h *Handler) get(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
346 id, err := strconv.ParseUint(params.ByName("id"), 10, 64)
347 if err != nil {
348 h.responseJSON(w, r, 400, err)
349 return
350 }
351 h.useCache(id)
352 h.storage.Serve(w, r, id)
353}
354
355// POST /_apis/artifactcache/clean
356func (h *Handler) clean(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
357 // TODO: don't support force deleting cache entries
358 // see: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries
359
360 h.responseJSON(w, r, 200)
361}
362
363func (h *Handler) middleware(handler httprouter.Handle) httprouter.Handle {
364 return func(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
365 h.logger.Debugf("%s %s", r.Method, r.RequestURI)
366 handler(w, r, params)
367 go h.gcCache()
368 }
369}
370
371// if not found, return (nil, nil) instead of an error.
372func findCache(db *bolthold.Store, keys []string, version string) (*Cache, error) {
373 cache := &Cache{}
374 for _, prefix := range keys {
375 // if a key in the list matches exactly, don't return partial matches
376 if err := db.FindOne(cache,
377 bolthold.Where("Key").Eq(prefix).
378 And("Version").Eq(version).
379 And("Complete").Eq(true).
380 SortBy("CreatedAt").Reverse()); err == nil || !errors.Is(err, bolthold.ErrNotFound) {
381 if err != nil {
382 return nil, fmt.Errorf("find cache: %w", err)
383 }
384 return cache, nil
385 }
386 prefixPattern := fmt.Sprintf("^%s", regexp.QuoteMeta(prefix))
387 re, err := regexp.Compile(prefixPattern)
388 if err != nil {
389 continue
390 }
391 if err := db.FindOne(cache,
392 bolthold.Where("Key").RegExp(re).
393 And("Version").Eq(version).
394 And("Complete").Eq(true).
395 SortBy("CreatedAt").Reverse()); err != nil {
396 if errors.Is(err, bolthold.ErrNotFound) {
397 continue
398 }
399 return nil, fmt.Errorf("find cache: %w", err)
400 }
401 return cache, nil
402 }
403 return nil, nil
404}
405
406func insertCache(db *bolthold.Store, cache *Cache) error {
407 if err := db.Insert(bolthold.NextSequence(), cache); err != nil {
408 return fmt.Errorf("insert cache: %w", err)
409 }
410 // write back id to db
411 if err := db.Update(cache.ID, cache); err != nil {
412 return fmt.Errorf("write back id to db: %w", err)
413 }
414 return nil
415}
416
417func (h *Handler) useCache(id uint64) {
418 db, err := h.openDB()
419 if err != nil {
420 return
421 }
422 defer db.Close()
423 cache := &Cache{}
424 if err := db.Get(id, cache); err != nil {
425 return
426 }
427 cache.UsedAt = time.Now().Unix()
428 _ = db.Update(cache.ID, cache)
429}
430
431const (
432 keepUsed = 30 * 24 * time.Hour
433 keepUnused = 7 * 24 * time.Hour
434 keepTemp = 5 * time.Minute
435 keepOld = 5 * time.Minute
436)
437
438func (h *Handler) gcCache() {
439 if h.gcing.Load() {
440 return
441 }
442 if !h.gcing.CompareAndSwap(false, true) {
443 return
444 }
445 defer h.gcing.Store(false)
446
447 if time.Since(h.gcAt) < time.Hour {
448 h.logger.Debugf("skip gc: %v", h.gcAt.String())
449 return
450 }
451 h.gcAt = time.Now()
452 h.logger.Debugf("gc: %v", h.gcAt.String())
453
454 db, err := h.openDB()
455 if err != nil {
456 return
457 }
458 defer db.Close()
459
460 // Remove the caches which are not completed for a while, they are most likely to be broken.
461 var caches []*Cache
462 if err := db.Find(&caches, bolthold.
463 Where("UsedAt").Lt(time.Now().Add(-keepTemp).Unix()).
464 And("Complete").Eq(false),
465 ); err != nil {
466 h.logger.Warnf("find caches: %v", err)
467 } else {
468 for _, cache := range caches {
469 h.storage.Remove(cache.ID)
470 if err := db.Delete(cache.ID, cache); err != nil {
471 h.logger.Warnf("delete cache: %v", err)
472 continue
473 }
474 h.logger.Infof("deleted cache: %+v", cache)
475 }
476 }
477
478 // Remove the old caches which have not been used recently.
479 caches = caches[:0]
480 if err := db.Find(&caches, bolthold.
481 Where("UsedAt").Lt(time.Now().Add(-keepUnused).Unix()),
482 ); err != nil {
483 h.logger.Warnf("find caches: %v", err)
484 } else {
485 for _, cache := range caches {
486 h.storage.Remove(cache.ID)
487 if err := db.Delete(cache.ID, cache); err != nil {
488 h.logger.Warnf("delete cache: %v", err)
489 continue
490 }
491 h.logger.Infof("deleted cache: %+v", cache)
492 }
493 }
494
495 // Remove the old caches which are too old.
496 caches = caches[:0]
497 if err := db.Find(&caches, bolthold.
498 Where("CreatedAt").Lt(time.Now().Add(-keepUsed).Unix()),
499 ); err != nil {
500 h.logger.Warnf("find caches: %v", err)
501 } else {
502 for _, cache := range caches {
503 h.storage.Remove(cache.ID)
504 if err := db.Delete(cache.ID, cache); err != nil {
505 h.logger.Warnf("delete cache: %v", err)
506 continue
507 }
508 h.logger.Infof("deleted cache: %+v", cache)
509 }
510 }
511
512 // Remove the old caches with the same key and version, keep the latest one.
513 // Also keep the olds which have been used recently for a while in case of the cache is still in use.
514 if results, err := db.FindAggregate(
515 &Cache{},
516 bolthold.Where("Complete").Eq(true),
517 "Key", "Version",
518 ); err != nil {
519 h.logger.Warnf("find aggregate caches: %v", err)
520 } else {
521 for _, result := range results {
522 if result.Count() <= 1 {
523 continue
524 }
525 result.Sort("CreatedAt")
526 caches = caches[:0]
527 result.Reduction(&caches)
528 for _, cache := range caches[:len(caches)-1] {
529 if time.Since(time.Unix(cache.UsedAt, 0)) < keepOld {
530 // Keep it since it has been used recently, even if it's old.
531 // Or it could break downloading in process.
532 continue
533 }
534 h.storage.Remove(cache.ID)
535 if err := db.Delete(cache.ID, cache); err != nil {
536 h.logger.Warnf("delete cache: %v", err)
537 continue
538 }
539 h.logger.Infof("deleted cache: %+v", cache)
540 }
541 }
542 }
543}
544
545func (h *Handler) responseJSON(w http.ResponseWriter, r *http.Request, code int, v ...any) {
546 w.Header().Set("Content-Type", "application/json; charset=utf-8")
547 var data []byte
548 if len(v) == 0 || v[0] == nil {
549 data, _ = json.Marshal(struct{}{})
550 } else if err, ok := v[0].(error); ok {
551 h.logger.Errorf("%v %v: %v", r.Method, r.RequestURI, err)
552 data, _ = json.Marshal(map[string]any{
553 "error": err.Error(),
554 })
555 } else {
556 data, _ = json.Marshal(v[0])
557 }
558 w.WriteHeader(code)
559 _, _ = w.Write(data)
560}
561
562func parseContentRange(s string) (int64, int64, error) {
563 // support the format like "bytes 11-22/*" only
564 s, _, _ = strings.Cut(strings.TrimPrefix(s, "bytes "), "/")
565 s1, s2, _ := strings.Cut(s, "-")
566
567 start, err := strconv.ParseInt(s1, 10, 64)
568 if err != nil {
569 return 0, 0, fmt.Errorf("parse %q: %w", s, err)
570 }
571 stop, err := strconv.ParseInt(s2, 10, 64)
572 if err != nil {
573 return 0, 0, fmt.Errorf("parse %q: %w", s, err)
574 }
575 return start, stop, nil
576}
Deleted act/pkg/artifactcache/model.go
@@ -1,34 +0,0 @@
1package artifactcache
2
3type Request struct {
4 Key string `json:"key" `
5 Version string `json:"version"`
6 Size int64 `json:"cacheSize"`
7}
8
9func (c *Request) ToCache() *Cache {
10 if c == nil {
11 return nil
12 }
13 ret := &Cache{
14 Key: c.Key,
15 Version: c.Version,
16 Size: c.Size,
17 }
18 if c.Size == 0 {
19 // So the request comes from old versions of actions, like `actions/cache@v2`.
20 // It doesn't send cache size. Set it to -1 to indicate that.
21 ret.Size = -1
22 }
23 return ret
24}
25
26type Cache struct {
27 ID uint64 `json:"id" boltholdKey:"ID"`
28 Key string `json:"key" boltholdIndex:"Key"`
29 Version string `json:"version" boltholdIndex:"Version"`
30 Size int64 `json:"cacheSize"`
31 Complete bool `json:"complete" boltholdIndex:"Complete"`
32 UsedAt int64 `json:"usedAt" boltholdIndex:"UsedAt"`
33 CreatedAt int64 `json:"createdAt" boltholdIndex:"CreatedAt"`
34}
Deleted act/pkg/artifactcache/storage.go
@@ -1,130 +0,0 @@
1package artifactcache
2
3import (
4 "fmt"
5 "io"
6 "net/http"
7 "os"
8 "path/filepath"
9)
10
11type Storage struct {
12 rootDir string
13}
14
15func NewStorage(rootDir string) (*Storage, error) {
16 if err := os.MkdirAll(rootDir, 0o755); err != nil {
17 return nil, err
18 }
19 return &Storage{
20 rootDir: rootDir,
21 }, nil
22}
23
24func (s *Storage) Exist(id uint64) (bool, error) {
25 name := s.filename(id)
26 if _, err := os.Stat(name); os.IsNotExist(err) {
27 return false, nil
28 } else if err != nil {
29 return false, err
30 }
31 return true, nil
32}
33
34func (s *Storage) Write(id uint64, offset int64, reader io.Reader) error {
35 name := s.tempName(id, offset)
36 if err := os.MkdirAll(filepath.Dir(name), 0o755); err != nil {
37 return err
38 }
39 file, err := os.Create(name)
40 if err != nil {
41 return err
42 }
43 defer file.Close()
44
45 _, err = io.Copy(file, reader)
46 return err
47}
48
49func (s *Storage) Commit(id uint64, size int64) (int64, error) {
50 defer func() {
51 _ = os.RemoveAll(s.tempDir(id))
52 }()
53
54 name := s.filename(id)
55 tempNames, err := s.tempNames(id)
56 if err != nil {
57 return 0, err
58 }
59
60 if err := os.MkdirAll(filepath.Dir(name), 0o755); err != nil {
61 return 0, err
62 }
63 file, err := os.Create(name)
64 if err != nil {
65 return 0, err
66 }
67 defer file.Close()
68
69 var written int64
70 for _, v := range tempNames {
71 f, err := os.Open(v)
72 if err != nil {
73 return 0, err
74 }
75 n, err := io.Copy(file, f)
76 _ = f.Close()
77 if err != nil {
78 return 0, err
79 }
80 written += n
81 }
82
83 // If size is less than 0, it means the size is unknown.
84 // We can't check the size of the file, just skip the check.
85 // It happens when the request comes from old versions of actions, like `actions/cache@v2`.
86 if size >= 0 && written != size {
87 _ = file.Close()
88 _ = os.Remove(name)
89 return 0, fmt.Errorf("broken file: %v != %v", written, size)
90 }
91
92 return written, nil
93}
94
95func (s *Storage) Serve(w http.ResponseWriter, r *http.Request, id uint64) {
96 name := s.filename(id)
97 http.ServeFile(w, r, name)
98}
99
100func (s *Storage) Remove(id uint64) {
101 _ = os.Remove(s.filename(id))
102 _ = os.RemoveAll(s.tempDir(id))
103}
104
105func (s *Storage) filename(id uint64) string {
106 return filepath.Join(s.rootDir, fmt.Sprintf("%02x", id%0xff), fmt.Sprint(id))
107}
108
109func (s *Storage) tempDir(id uint64) string {
110 return filepath.Join(s.rootDir, "tmp", fmt.Sprint(id))
111}
112
113func (s *Storage) tempName(id uint64, offset int64) string {
114 return filepath.Join(s.tempDir(id), fmt.Sprintf("%016x", offset))
115}
116
117func (s *Storage) tempNames(id uint64) ([]string, error) {
118 dir := s.tempDir(id)
119 files, err := os.ReadDir(dir)
120 if err != nil {
121 return nil, err
122 }
123 var names []string
124 for _, v := range files {
125 if !v.IsDir() {
126 names = append(names, filepath.Join(dir, v.Name()))
127 }
128 }
129 return names, nil
130}
Deleted act/pkg/artifacts/artifact.pb.go
@@ -1,1058 +0,0 @@
1// Copyright 2024 The Gitea Authors. All rights reserved.
2// SPDX-License-Identifier: MIT
3
4// Code generated by protoc-gen-go. DO NOT EDIT.
5// versions:
6// protoc-gen-go v1.32.0
7// protoc v4.25.2
8// source: artifact.proto
9
10package artifacts
11
12import (
13 reflect "reflect"
14 sync "sync"
15
16 protoreflect "google.golang.org/protobuf/reflect/protoreflect"
17 protoimpl "google.golang.org/protobuf/runtime/protoimpl"
18 timestamppb "google.golang.org/protobuf/types/known/timestamppb"
19 wrapperspb "google.golang.org/protobuf/types/known/wrapperspb"
20)
21
22const (
23 // Verify that this generated code is sufficiently up-to-date.
24 _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
25 // Verify that runtime/protoimpl is sufficiently up-to-date.
26 _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
27)
28
29type CreateArtifactRequest struct {
30 state protoimpl.MessageState
31 sizeCache protoimpl.SizeCache
32 unknownFields protoimpl.UnknownFields
33
34 WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"`
35 WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"`
36 Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
37 ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"`
38 Version int32 `protobuf:"varint,5,opt,name=version,proto3" json:"version,omitempty"`
39}
40
41func (x *CreateArtifactRequest) Reset() {
42 *x = CreateArtifactRequest{}
43 if protoimpl.UnsafeEnabled {
44 mi := &file_artifact_proto_msgTypes[0]
45 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
46 ms.StoreMessageInfo(mi)
47 }
48}
49
50func (x *CreateArtifactRequest) String() string {
51 return protoimpl.X.MessageStringOf(x)
52}
53
54func (*CreateArtifactRequest) ProtoMessage() {}
55
56func (x *CreateArtifactRequest) ProtoReflect() protoreflect.Message {
57 mi := &file_artifact_proto_msgTypes[0]
58 if protoimpl.UnsafeEnabled && x != nil {
59 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
60 if ms.LoadMessageInfo() == nil {
61 ms.StoreMessageInfo(mi)
62 }
63 return ms
64 }
65 return mi.MessageOf(x)
66}
67
68// Deprecated: Use CreateArtifactRequest.ProtoReflect.Descriptor instead.
69func (*CreateArtifactRequest) Descriptor() ([]byte, []int) {
70 return file_artifact_proto_rawDescGZIP(), []int{0}
71}
72
73func (x *CreateArtifactRequest) GetWorkflowRunBackendId() string {
74 if x != nil {
75 return x.WorkflowRunBackendId
76 }
77 return ""
78}
79
80func (x *CreateArtifactRequest) GetWorkflowJobRunBackendId() string {
81 if x != nil {
82 return x.WorkflowJobRunBackendId
83 }
84 return ""
85}
86
87func (x *CreateArtifactRequest) GetName() string {
88 if x != nil {
89 return x.Name
90 }
91 return ""
92}
93
94func (x *CreateArtifactRequest) GetExpiresAt() *timestamppb.Timestamp {
95 if x != nil {
96 return x.ExpiresAt
97 }
98 return nil
99}
100
101func (x *CreateArtifactRequest) GetVersion() int32 {
102 if x != nil {
103 return x.Version
104 }
105 return 0
106}
107
108type CreateArtifactResponse struct {
109 state protoimpl.MessageState
110 sizeCache protoimpl.SizeCache
111 unknownFields protoimpl.UnknownFields
112
113 Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"`
114 SignedUploadUrl string `protobuf:"bytes,2,opt,name=signed_upload_url,json=signedUploadUrl,proto3" json:"signed_upload_url,omitempty"`
115}
116
117func (x *CreateArtifactResponse) Reset() {
118 *x = CreateArtifactResponse{}
119 if protoimpl.UnsafeEnabled {
120 mi := &file_artifact_proto_msgTypes[1]
121 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
122 ms.StoreMessageInfo(mi)
123 }
124}
125
126func (x *CreateArtifactResponse) String() string {
127 return protoimpl.X.MessageStringOf(x)
128}
129
130func (*CreateArtifactResponse) ProtoMessage() {}
131
132func (x *CreateArtifactResponse) ProtoReflect() protoreflect.Message {
133 mi := &file_artifact_proto_msgTypes[1]
134 if protoimpl.UnsafeEnabled && x != nil {
135 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
136 if ms.LoadMessageInfo() == nil {
137 ms.StoreMessageInfo(mi)
138 }
139 return ms
140 }
141 return mi.MessageOf(x)
142}
143
144// Deprecated: Use CreateArtifactResponse.ProtoReflect.Descriptor instead.
145func (*CreateArtifactResponse) Descriptor() ([]byte, []int) {
146 return file_artifact_proto_rawDescGZIP(), []int{1}
147}
148
149func (x *CreateArtifactResponse) GetOk() bool {
150 if x != nil {
151 return x.Ok
152 }
153 return false
154}
155
156func (x *CreateArtifactResponse) GetSignedUploadUrl() string {
157 if x != nil {
158 return x.SignedUploadUrl
159 }
160 return ""
161}
162
163type FinalizeArtifactRequest struct {
164 state protoimpl.MessageState
165 sizeCache protoimpl.SizeCache
166 unknownFields protoimpl.UnknownFields
167
168 WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"`
169 WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"`
170 Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
171 Size int64 `protobuf:"varint,4,opt,name=size,proto3" json:"size,omitempty"`
172 Hash *wrapperspb.StringValue `protobuf:"bytes,5,opt,name=hash,proto3" json:"hash,omitempty"`
173}
174
175func (x *FinalizeArtifactRequest) Reset() {
176 *x = FinalizeArtifactRequest{}
177 if protoimpl.UnsafeEnabled {
178 mi := &file_artifact_proto_msgTypes[2]
179 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
180 ms.StoreMessageInfo(mi)
181 }
182}
183
184func (x *FinalizeArtifactRequest) String() string {
185 return protoimpl.X.MessageStringOf(x)
186}
187
188func (*FinalizeArtifactRequest) ProtoMessage() {}
189
190func (x *FinalizeArtifactRequest) ProtoReflect() protoreflect.Message {
191 mi := &file_artifact_proto_msgTypes[2]
192 if protoimpl.UnsafeEnabled && x != nil {
193 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
194 if ms.LoadMessageInfo() == nil {
195 ms.StoreMessageInfo(mi)
196 }
197 return ms
198 }
199 return mi.MessageOf(x)
200}
201
202// Deprecated: Use FinalizeArtifactRequest.ProtoReflect.Descriptor instead.
203func (*FinalizeArtifactRequest) Descriptor() ([]byte, []int) {
204 return file_artifact_proto_rawDescGZIP(), []int{2}
205}
206
207func (x *FinalizeArtifactRequest) GetWorkflowRunBackendId() string {
208 if x != nil {
209 return x.WorkflowRunBackendId
210 }
211 return ""
212}
213
214func (x *FinalizeArtifactRequest) GetWorkflowJobRunBackendId() string {
215 if x != nil {
216 return x.WorkflowJobRunBackendId
217 }
218 return ""
219}
220
221func (x *FinalizeArtifactRequest) GetName() string {
222 if x != nil {
223 return x.Name
224 }
225 return ""
226}
227
228func (x *FinalizeArtifactRequest) GetSize() int64 {
229 if x != nil {
230 return x.Size
231 }
232 return 0
233}
234
235func (x *FinalizeArtifactRequest) GetHash() *wrapperspb.StringValue {
236 if x != nil {
237 return x.Hash
238 }
239 return nil
240}
241
242type FinalizeArtifactResponse struct {
243 state protoimpl.MessageState
244 sizeCache protoimpl.SizeCache
245 unknownFields protoimpl.UnknownFields
246
247 Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"`
248 ArtifactId int64 `protobuf:"varint,2,opt,name=artifact_id,json=artifactId,proto3" json:"artifact_id,omitempty"`
249}
250
251func (x *FinalizeArtifactResponse) Reset() {
252 *x = FinalizeArtifactResponse{}
253 if protoimpl.UnsafeEnabled {
254 mi := &file_artifact_proto_msgTypes[3]
255 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
256 ms.StoreMessageInfo(mi)
257 }
258}
259
260func (x *FinalizeArtifactResponse) String() string {
261 return protoimpl.X.MessageStringOf(x)
262}
263
264func (*FinalizeArtifactResponse) ProtoMessage() {}
265
266func (x *FinalizeArtifactResponse) ProtoReflect() protoreflect.Message {
267 mi := &file_artifact_proto_msgTypes[3]
268 if protoimpl.UnsafeEnabled && x != nil {
269 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
270 if ms.LoadMessageInfo() == nil {
271 ms.StoreMessageInfo(mi)
272 }
273 return ms
274 }
275 return mi.MessageOf(x)
276}
277
278// Deprecated: Use FinalizeArtifactResponse.ProtoReflect.Descriptor instead.
279func (*FinalizeArtifactResponse) Descriptor() ([]byte, []int) {
280 return file_artifact_proto_rawDescGZIP(), []int{3}
281}
282
283func (x *FinalizeArtifactResponse) GetOk() bool {
284 if x != nil {
285 return x.Ok
286 }
287 return false
288}
289
290func (x *FinalizeArtifactResponse) GetArtifactId() int64 {
291 if x != nil {
292 return x.ArtifactId
293 }
294 return 0
295}
296
297type ListArtifactsRequest struct {
298 state protoimpl.MessageState
299 sizeCache protoimpl.SizeCache
300 unknownFields protoimpl.UnknownFields
301
302 WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"`
303 WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"`
304 NameFilter *wrapperspb.StringValue `protobuf:"bytes,3,opt,name=name_filter,json=nameFilter,proto3" json:"name_filter,omitempty"`
305 IdFilter *wrapperspb.Int64Value `protobuf:"bytes,4,opt,name=id_filter,json=idFilter,proto3" json:"id_filter,omitempty"`
306}
307
308func (x *ListArtifactsRequest) Reset() {
309 *x = ListArtifactsRequest{}
310 if protoimpl.UnsafeEnabled {
311 mi := &file_artifact_proto_msgTypes[4]
312 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
313 ms.StoreMessageInfo(mi)
314 }
315}
316
317func (x *ListArtifactsRequest) String() string {
318 return protoimpl.X.MessageStringOf(x)
319}
320
321func (*ListArtifactsRequest) ProtoMessage() {}
322
323func (x *ListArtifactsRequest) ProtoReflect() protoreflect.Message {
324 mi := &file_artifact_proto_msgTypes[4]
325 if protoimpl.UnsafeEnabled && x != nil {
326 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
327 if ms.LoadMessageInfo() == nil {
328 ms.StoreMessageInfo(mi)
329 }
330 return ms
331 }
332 return mi.MessageOf(x)
333}
334
335// Deprecated: Use ListArtifactsRequest.ProtoReflect.Descriptor instead.
336func (*ListArtifactsRequest) Descriptor() ([]byte, []int) {
337 return file_artifact_proto_rawDescGZIP(), []int{4}
338}
339
340func (x *ListArtifactsRequest) GetWorkflowRunBackendId() string {
341 if x != nil {
342 return x.WorkflowRunBackendId
343 }
344 return ""
345}
346
347func (x *ListArtifactsRequest) GetWorkflowJobRunBackendId() string {
348 if x != nil {
349 return x.WorkflowJobRunBackendId
350 }
351 return ""
352}
353
354func (x *ListArtifactsRequest) GetNameFilter() *wrapperspb.StringValue {
355 if x != nil {
356 return x.NameFilter
357 }
358 return nil
359}
360
361func (x *ListArtifactsRequest) GetIdFilter() *wrapperspb.Int64Value {
362 if x != nil {
363 return x.IdFilter
364 }
365 return nil
366}
367
368type ListArtifactsResponse struct {
369 state protoimpl.MessageState
370 sizeCache protoimpl.SizeCache
371 unknownFields protoimpl.UnknownFields
372
373 Artifacts []*ListArtifactsResponse_MonolithArtifact `protobuf:"bytes,1,rep,name=artifacts,proto3" json:"artifacts,omitempty"`
374}
375
376func (x *ListArtifactsResponse) Reset() {
377 *x = ListArtifactsResponse{}
378 if protoimpl.UnsafeEnabled {
379 mi := &file_artifact_proto_msgTypes[5]
380 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
381 ms.StoreMessageInfo(mi)
382 }
383}
384
385func (x *ListArtifactsResponse) String() string {
386 return protoimpl.X.MessageStringOf(x)
387}
388
389func (*ListArtifactsResponse) ProtoMessage() {}
390
391func (x *ListArtifactsResponse) ProtoReflect() protoreflect.Message {
392 mi := &file_artifact_proto_msgTypes[5]
393 if protoimpl.UnsafeEnabled && x != nil {
394 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
395 if ms.LoadMessageInfo() == nil {
396 ms.StoreMessageInfo(mi)
397 }
398 return ms
399 }
400 return mi.MessageOf(x)
401}
402
403// Deprecated: Use ListArtifactsResponse.ProtoReflect.Descriptor instead.
404func (*ListArtifactsResponse) Descriptor() ([]byte, []int) {
405 return file_artifact_proto_rawDescGZIP(), []int{5}
406}
407
408func (x *ListArtifactsResponse) GetArtifacts() []*ListArtifactsResponse_MonolithArtifact {
409 if x != nil {
410 return x.Artifacts
411 }
412 return nil
413}
414
415type ListArtifactsResponse_MonolithArtifact struct {
416 state protoimpl.MessageState
417 sizeCache protoimpl.SizeCache
418 unknownFields protoimpl.UnknownFields
419
420 WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"`
421 WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"`
422 DatabaseId int64 `protobuf:"varint,3,opt,name=database_id,json=databaseId,proto3" json:"database_id,omitempty"`
423 Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"`
424 Size int64 `protobuf:"varint,5,opt,name=size,proto3" json:"size,omitempty"`
425 CreatedAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
426}
427
428func (x *ListArtifactsResponse_MonolithArtifact) Reset() {
429 *x = ListArtifactsResponse_MonolithArtifact{}
430 if protoimpl.UnsafeEnabled {
431 mi := &file_artifact_proto_msgTypes[6]
432 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
433 ms.StoreMessageInfo(mi)
434 }
435}
436
437func (x *ListArtifactsResponse_MonolithArtifact) String() string {
438 return protoimpl.X.MessageStringOf(x)
439}
440
441func (*ListArtifactsResponse_MonolithArtifact) ProtoMessage() {}
442
443func (x *ListArtifactsResponse_MonolithArtifact) ProtoReflect() protoreflect.Message {
444 mi := &file_artifact_proto_msgTypes[6]
445 if protoimpl.UnsafeEnabled && x != nil {
446 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
447 if ms.LoadMessageInfo() == nil {
448 ms.StoreMessageInfo(mi)
449 }
450 return ms
451 }
452 return mi.MessageOf(x)
453}
454
455// Deprecated: Use ListArtifactsResponse_MonolithArtifact.ProtoReflect.Descriptor instead.
456func (*ListArtifactsResponse_MonolithArtifact) Descriptor() ([]byte, []int) {
457 return file_artifact_proto_rawDescGZIP(), []int{6}
458}
459
460func (x *ListArtifactsResponse_MonolithArtifact) GetWorkflowRunBackendId() string {
461 if x != nil {
462 return x.WorkflowRunBackendId
463 }
464 return ""
465}
466
467func (x *ListArtifactsResponse_MonolithArtifact) GetWorkflowJobRunBackendId() string {
468 if x != nil {
469 return x.WorkflowJobRunBackendId
470 }
471 return ""
472}
473
474func (x *ListArtifactsResponse_MonolithArtifact) GetDatabaseId() int64 {
475 if x != nil {
476 return x.DatabaseId
477 }
478 return 0
479}
480
481func (x *ListArtifactsResponse_MonolithArtifact) GetName() string {
482 if x != nil {
483 return x.Name
484 }
485 return ""
486}
487
488func (x *ListArtifactsResponse_MonolithArtifact) GetSize() int64 {
489 if x != nil {
490 return x.Size
491 }
492 return 0
493}
494
495func (x *ListArtifactsResponse_MonolithArtifact) GetCreatedAt() *timestamppb.Timestamp {
496 if x != nil {
497 return x.CreatedAt
498 }
499 return nil
500}
501
502type GetSignedArtifactURLRequest struct {
503 state protoimpl.MessageState
504 sizeCache protoimpl.SizeCache
505 unknownFields protoimpl.UnknownFields
506
507 WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"`
508 WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"`
509 Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
510}
511
512func (x *GetSignedArtifactURLRequest) Reset() {
513 *x = GetSignedArtifactURLRequest{}
514 if protoimpl.UnsafeEnabled {
515 mi := &file_artifact_proto_msgTypes[7]
516 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
517 ms.StoreMessageInfo(mi)
518 }
519}
520
521func (x *GetSignedArtifactURLRequest) String() string {
522 return protoimpl.X.MessageStringOf(x)
523}
524
525func (*GetSignedArtifactURLRequest) ProtoMessage() {}
526
527func (x *GetSignedArtifactURLRequest) ProtoReflect() protoreflect.Message {
528 mi := &file_artifact_proto_msgTypes[7]
529 if protoimpl.UnsafeEnabled && x != nil {
530 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
531 if ms.LoadMessageInfo() == nil {
532 ms.StoreMessageInfo(mi)
533 }
534 return ms
535 }
536 return mi.MessageOf(x)
537}
538
539// Deprecated: Use GetSignedArtifactURLRequest.ProtoReflect.Descriptor instead.
540func (*GetSignedArtifactURLRequest) Descriptor() ([]byte, []int) {
541 return file_artifact_proto_rawDescGZIP(), []int{7}
542}
543
544func (x *GetSignedArtifactURLRequest) GetWorkflowRunBackendId() string {
545 if x != nil {
546 return x.WorkflowRunBackendId
547 }
548 return ""
549}
550
551func (x *GetSignedArtifactURLRequest) GetWorkflowJobRunBackendId() string {
552 if x != nil {
553 return x.WorkflowJobRunBackendId
554 }
555 return ""
556}
557
558func (x *GetSignedArtifactURLRequest) GetName() string {
559 if x != nil {
560 return x.Name
561 }
562 return ""
563}
564
565type GetSignedArtifactURLResponse struct {
566 state protoimpl.MessageState
567 sizeCache protoimpl.SizeCache
568 unknownFields protoimpl.UnknownFields
569
570 SignedUrl string `protobuf:"bytes,1,opt,name=signed_url,json=signedUrl,proto3" json:"signed_url,omitempty"`
571}
572
573func (x *GetSignedArtifactURLResponse) Reset() {
574 *x = GetSignedArtifactURLResponse{}
575 if protoimpl.UnsafeEnabled {
576 mi := &file_artifact_proto_msgTypes[8]
577 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
578 ms.StoreMessageInfo(mi)
579 }
580}
581
582func (x *GetSignedArtifactURLResponse) String() string {
583 return protoimpl.X.MessageStringOf(x)
584}
585
586func (*GetSignedArtifactURLResponse) ProtoMessage() {}
587
588func (x *GetSignedArtifactURLResponse) ProtoReflect() protoreflect.Message {
589 mi := &file_artifact_proto_msgTypes[8]
590 if protoimpl.UnsafeEnabled && x != nil {
591 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
592 if ms.LoadMessageInfo() == nil {
593 ms.StoreMessageInfo(mi)
594 }
595 return ms
596 }
597 return mi.MessageOf(x)
598}
599
600// Deprecated: Use GetSignedArtifactURLResponse.ProtoReflect.Descriptor instead.
601func (*GetSignedArtifactURLResponse) Descriptor() ([]byte, []int) {
602 return file_artifact_proto_rawDescGZIP(), []int{8}
603}
604
605func (x *GetSignedArtifactURLResponse) GetSignedUrl() string {
606 if x != nil {
607 return x.SignedUrl
608 }
609 return ""
610}
611
612type DeleteArtifactRequest struct {
613 state protoimpl.MessageState
614 sizeCache protoimpl.SizeCache
615 unknownFields protoimpl.UnknownFields
616
617 WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"`
618 WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"`
619 Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
620}
621
622func (x *DeleteArtifactRequest) Reset() {
623 *x = DeleteArtifactRequest{}
624 if protoimpl.UnsafeEnabled {
625 mi := &file_artifact_proto_msgTypes[9]
626 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
627 ms.StoreMessageInfo(mi)
628 }
629}
630
631func (x *DeleteArtifactRequest) String() string {
632 return protoimpl.X.MessageStringOf(x)
633}
634
635func (*DeleteArtifactRequest) ProtoMessage() {}
636
637func (x *DeleteArtifactRequest) ProtoReflect() protoreflect.Message {
638 mi := &file_artifact_proto_msgTypes[9]
639 if protoimpl.UnsafeEnabled && x != nil {
640 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
641 if ms.LoadMessageInfo() == nil {
642 ms.StoreMessageInfo(mi)
643 }
644 return ms
645 }
646 return mi.MessageOf(x)
647}
648
649// Deprecated: Use DeleteArtifactRequest.ProtoReflect.Descriptor instead.
650func (*DeleteArtifactRequest) Descriptor() ([]byte, []int) {
651 return file_artifact_proto_rawDescGZIP(), []int{9}
652}
653
654func (x *DeleteArtifactRequest) GetWorkflowRunBackendId() string {
655 if x != nil {
656 return x.WorkflowRunBackendId
657 }
658 return ""
659}
660
661func (x *DeleteArtifactRequest) GetWorkflowJobRunBackendId() string {
662 if x != nil {
663 return x.WorkflowJobRunBackendId
664 }
665 return ""
666}
667
668func (x *DeleteArtifactRequest) GetName() string {
669 if x != nil {
670 return x.Name
671 }
672 return ""
673}
674
675type DeleteArtifactResponse struct {
676 state protoimpl.MessageState
677 sizeCache protoimpl.SizeCache
678 unknownFields protoimpl.UnknownFields
679
680 Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"`
681 ArtifactId int64 `protobuf:"varint,2,opt,name=artifact_id,json=artifactId,proto3" json:"artifact_id,omitempty"`
682}
683
684func (x *DeleteArtifactResponse) Reset() {
685 *x = DeleteArtifactResponse{}
686 if protoimpl.UnsafeEnabled {
687 mi := &file_artifact_proto_msgTypes[10]
688 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
689 ms.StoreMessageInfo(mi)
690 }
691}
692
693func (x *DeleteArtifactResponse) String() string {
694 return protoimpl.X.MessageStringOf(x)
695}
696
697func (*DeleteArtifactResponse) ProtoMessage() {}
698
699func (x *DeleteArtifactResponse) ProtoReflect() protoreflect.Message {
700 mi := &file_artifact_proto_msgTypes[10]
701 if protoimpl.UnsafeEnabled && x != nil {
702 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
703 if ms.LoadMessageInfo() == nil {
704 ms.StoreMessageInfo(mi)
705 }
706 return ms
707 }
708 return mi.MessageOf(x)
709}
710
711// Deprecated: Use DeleteArtifactResponse.ProtoReflect.Descriptor instead.
712func (*DeleteArtifactResponse) Descriptor() ([]byte, []int) {
713 return file_artifact_proto_rawDescGZIP(), []int{10}
714}
715
716func (x *DeleteArtifactResponse) GetOk() bool {
717 if x != nil {
718 return x.Ok
719 }
720 return false
721}
722
723func (x *DeleteArtifactResponse) GetArtifactId() int64 {
724 if x != nil {
725 return x.ArtifactId
726 }
727 return 0
728}
729
730var File_artifact_proto protoreflect.FileDescriptor
731
732var file_artifact_proto_rawDesc = []byte{
733 0x0a, 0x0e, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
734 0x12, 0x1d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73,
735 0x2e, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x1a,
736 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
737 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
738 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
739 0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
740 0x22, 0xf5, 0x01, 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66,
741 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f,
742 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65,
743 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72,
744 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49,
745 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f,
746 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64,
747 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77,
748 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12,
749 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e,
750 0x61, 0x6d, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x61,
751 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
752 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74,
753 0x61, 0x6d, 0x70, 0x52, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x12, 0x18,
754 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52,
755 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x54, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61,
756 0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
757 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x02,
758 0x6f, 0x6b, 0x12, 0x2a, 0x0a, 0x11, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x75, 0x70, 0x6c,
759 0x6f, 0x61, 0x64, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73,
760 0x69, 0x67, 0x6e, 0x65, 0x64, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x55, 0x72, 0x6c, 0x22, 0xe8,
761 0x01, 0x0a, 0x17, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66,
762 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f,
763 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65,
764 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72,
765 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49,
766 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f,
767 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64,
768 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77,
769 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12,
770 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e,
771 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28,
772 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x30, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18,
773 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
774 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61,
775 0x6c, 0x75, 0x65, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x22, 0x4b, 0x0a, 0x18, 0x46, 0x69, 0x6e,
776 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x73,
777 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28,
778 0x08, 0x52, 0x02, 0x6f, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63,
779 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x61, 0x72, 0x74, 0x69,
780 0x66, 0x61, 0x63, 0x74, 0x49, 0x64, 0x22, 0x84, 0x02, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x41,
781 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
782 0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f,
783 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
784 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63,
785 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c,
786 0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65,
787 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72,
788 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65,
789 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3d, 0x0a, 0x0b, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x66, 0x69, 0x6c,
790 0x74, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
791 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69,
792 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0a, 0x6e, 0x61, 0x6d, 0x65, 0x46, 0x69, 0x6c,
793 0x74, 0x65, 0x72, 0x12, 0x38, 0x0a, 0x09, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72,
794 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
795 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61,
796 0x6c, 0x75, 0x65, 0x52, 0x08, 0x69, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x7c, 0x0a,
797 0x15, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65,
798 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x63, 0x0a, 0x09, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61,
799 0x63, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x45, 0x2e, 0x67, 0x69, 0x74, 0x68,
800 0x75, 0x62, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x72, 0x65, 0x73, 0x75, 0x6c,
801 0x74, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x72,
802 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f,
803 0x4d, 0x6f, 0x6e, 0x6f, 0x6c, 0x69, 0x74, 0x68, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74,
804 0x52, 0x09, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x22, 0xa1, 0x02, 0x0a, 0x26,
805 0x4c, 0x69, 0x73, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73,
806 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x4d, 0x6f, 0x6e, 0x6f, 0x6c, 0x69, 0x74, 0x68, 0x41, 0x72,
807 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c,
808 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69,
809 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f,
810 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a,
811 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75,
812 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01,
813 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52,
814 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x64,
815 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03,
816 0x52, 0x0a, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04,
817 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65,
818 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04,
819 0x73, 0x69, 0x7a, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f,
820 0x61, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
821 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73,
822 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22,
823 0xa6, 0x01, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x41, 0x72, 0x74,
824 0x69, 0x66, 0x61, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
825 0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f,
826 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
827 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63,
828 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c,
829 0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65,
830 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72,
831 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65,
832 0x6e, 0x64, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01,
833 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x3d, 0x0a, 0x1c, 0x47, 0x65, 0x74, 0x53,
834 0x69, 0x67, 0x6e, 0x65, 0x64, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x55, 0x52, 0x4c,
835 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x69, 0x67, 0x6e,
836 0x65, 0x64, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x69,
837 0x67, 0x6e, 0x65, 0x64, 0x55, 0x72, 0x6c, 0x22, 0xa0, 0x01, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65,
838 0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
839 0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75,
840 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
841 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42,
842 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b,
843 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63,
844 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77,
845 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63,
846 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03,
847 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x49, 0x0a, 0x16, 0x44, 0x65,
848 0x6c, 0x65, 0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70,
849 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08,
850 0x52, 0x02, 0x6f, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74,
851 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x61, 0x72, 0x74, 0x69, 0x66,
852 0x61, 0x63, 0x74, 0x49, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
853}
854
855var (
856 file_artifact_proto_rawDescOnce sync.Once
857 file_artifact_proto_rawDescData = file_artifact_proto_rawDesc
858)
859
860func file_artifact_proto_rawDescGZIP() []byte {
861 file_artifact_proto_rawDescOnce.Do(func() {
862 file_artifact_proto_rawDescData = protoimpl.X.CompressGZIP(file_artifact_proto_rawDescData)
863 })
864 return file_artifact_proto_rawDescData
865}
866
867var (
868 file_artifact_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
869 file_artifact_proto_goTypes = []interface{}{
870 (*CreateArtifactRequest)(nil), // 0: github.actions.results.api.v1.CreateArtifactRequest
871 (*CreateArtifactResponse)(nil), // 1: github.actions.results.api.v1.CreateArtifactResponse
872 (*FinalizeArtifactRequest)(nil), // 2: github.actions.results.api.v1.FinalizeArtifactRequest
873 (*FinalizeArtifactResponse)(nil), // 3: github.actions.results.api.v1.FinalizeArtifactResponse
874 (*ListArtifactsRequest)(nil), // 4: github.actions.results.api.v1.ListArtifactsRequest
875 (*ListArtifactsResponse)(nil), // 5: github.actions.results.api.v1.ListArtifactsResponse
876 (*ListArtifactsResponse_MonolithArtifact)(nil), // 6: github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact
877 (*GetSignedArtifactURLRequest)(nil), // 7: github.actions.results.api.v1.GetSignedArtifactURLRequest
878 (*GetSignedArtifactURLResponse)(nil), // 8: github.actions.results.api.v1.GetSignedArtifactURLResponse
879 (*DeleteArtifactRequest)(nil), // 9: github.actions.results.api.v1.DeleteArtifactRequest
880 (*DeleteArtifactResponse)(nil), // 10: github.actions.results.api.v1.DeleteArtifactResponse
881 (*timestamppb.Timestamp)(nil), // 11: google.protobuf.Timestamp
882 (*wrapperspb.StringValue)(nil), // 12: google.protobuf.StringValue
883 (*wrapperspb.Int64Value)(nil), // 13: google.protobuf.Int64Value
884 }
885)
886
887var file_artifact_proto_depIdxs = []int32{
888 11, // 0: github.actions.results.api.v1.CreateArtifactRequest.expires_at:type_name -> google.protobuf.Timestamp
889 12, // 1: github.actions.results.api.v1.FinalizeArtifactRequest.hash:type_name -> google.protobuf.StringValue
890 12, // 2: github.actions.results.api.v1.ListArtifactsRequest.name_filter:type_name -> google.protobuf.StringValue
891 13, // 3: github.actions.results.api.v1.ListArtifactsRequest.id_filter:type_name -> google.protobuf.Int64Value
892 6, // 4: github.actions.results.api.v1.ListArtifactsResponse.artifacts:type_name -> github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact
893 11, // 5: github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact.created_at:type_name -> google.protobuf.Timestamp
894 6, // [6:6] is the sub-list for method output_type
895 6, // [6:6] is the sub-list for method input_type
896 6, // [6:6] is the sub-list for extension type_name
897 6, // [6:6] is the sub-list for extension extendee
898 0, // [0:6] is the sub-list for field type_name
899}
900
901func init() { file_artifact_proto_init() }
902func file_artifact_proto_init() {
903 if File_artifact_proto != nil {
904 return
905 }
906 if !protoimpl.UnsafeEnabled {
907 file_artifact_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
908 switch v := v.(*CreateArtifactRequest); i {
909 case 0:
910 return &v.state
911 case 1:
912 return &v.sizeCache
913 case 2:
914 return &v.unknownFields
915 default:
916 return nil
917 }
918 }
919 file_artifact_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
920 switch v := v.(*CreateArtifactResponse); i {
921 case 0:
922 return &v.state
923 case 1:
924 return &v.sizeCache
925 case 2:
926 return &v.unknownFields
927 default:
928 return nil
929 }
930 }
931 file_artifact_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
932 switch v := v.(*FinalizeArtifactRequest); i {
933 case 0:
934 return &v.state
935 case 1:
936 return &v.sizeCache
937 case 2:
938 return &v.unknownFields
939 default:
940 return nil
941 }
942 }
943 file_artifact_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
944 switch v := v.(*FinalizeArtifactResponse); i {
945 case 0:
946 return &v.state
947 case 1:
948 return &v.sizeCache
949 case 2:
950 return &v.unknownFields
951 default:
952 return nil
953 }
954 }
955 file_artifact_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
956 switch v := v.(*ListArtifactsRequest); i {
957 case 0:
958 return &v.state
959 case 1:
960 return &v.sizeCache
961 case 2:
962 return &v.unknownFields
963 default:
964 return nil
965 }
966 }
967 file_artifact_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
968 switch v := v.(*ListArtifactsResponse); i {
969 case 0:
970 return &v.state
971 case 1:
972 return &v.sizeCache
973 case 2:
974 return &v.unknownFields
975 default:
976 return nil
977 }
978 }
979 file_artifact_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
980 switch v := v.(*ListArtifactsResponse_MonolithArtifact); i {
981 case 0:
982 return &v.state
983 case 1:
984 return &v.sizeCache
985 case 2:
986 return &v.unknownFields
987 default:
988 return nil
989 }
990 }
991 file_artifact_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
992 switch v := v.(*GetSignedArtifactURLRequest); i {
993 case 0:
994 return &v.state
995 case 1:
996 return &v.sizeCache
997 case 2:
998 return &v.unknownFields
999 default:
1000 return nil
1001 }
1002 }
1003 file_artifact_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} {
1004 switch v := v.(*GetSignedArtifactURLResponse); i {
1005 case 0:
1006 return &v.state
1007 case 1:
1008 return &v.sizeCache
1009 case 2:
1010 return &v.unknownFields
1011 default:
1012 return nil
1013 }
1014 }
1015 file_artifact_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} {
1016 switch v := v.(*DeleteArtifactRequest); i {
1017 case 0:
1018 return &v.state
1019 case 1:
1020 return &v.sizeCache
1021 case 2:
1022 return &v.unknownFields
1023 default:
1024 return nil
1025 }
1026 }
1027 file_artifact_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} {
1028 switch v := v.(*DeleteArtifactResponse); i {
1029 case 0:
1030 return &v.state
1031 case 1:
1032 return &v.sizeCache
1033 case 2:
1034 return &v.unknownFields
1035 default:
1036 return nil
1037 }
1038 }
1039 }
1040 type x struct{}
1041 out := protoimpl.TypeBuilder{
1042 File: protoimpl.DescBuilder{
1043 GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
1044 RawDescriptor: file_artifact_proto_rawDesc,
1045 NumEnums: 0,
1046 NumMessages: 11,
1047 NumExtensions: 0,
1048 NumServices: 0,
1049 },
1050 GoTypes: file_artifact_proto_goTypes,
1051 DependencyIndexes: file_artifact_proto_depIdxs,
1052 MessageInfos: file_artifact_proto_msgTypes,
1053 }.Build()
1054 File_artifact_proto = out.File
1055 file_artifact_proto_rawDesc = nil
1056 file_artifact_proto_goTypes = nil
1057 file_artifact_proto_depIdxs = nil
1058}
Deleted act/pkg/artifacts/artifacts_v4.go
@@ -1,456 +0,0 @@
1// Copyright 2024 The Gitea Authors. All rights reserved.
2// SPDX-License-Identifier: MIT
3
4package artifacts
5
6// GitHub Actions Artifacts V4 API Simple Description
7//
8// 1. Upload artifact
9// 1.1. CreateArtifact
10// Post: /twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact
11// Request:
12// {
13// "workflow_run_backend_id": "21",
14// "workflow_job_run_backend_id": "49",
15// "name": "test",
16// "version": 4
17// }
18// Response:
19// {
20// "ok": true,
21// "signedUploadUrl": "http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75"
22// }
23// 1.2. Upload Zip Content to Blobstorage (unauthenticated request)
24// PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=block
25// 1.3. Continue Upload Zip Content to Blobstorage (unauthenticated request), repeat until everything is uploaded
26// PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=appendBlock
27// 1.4. Unknown xml payload to Blobstorage (unauthenticated request), ignored for now
28// PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=blockList
29// 1.5. FinalizeArtifact
30// Post: /twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact
31// Request
32// {
33// "workflow_run_backend_id": "21",
34// "workflow_job_run_backend_id": "49",
35// "name": "test",
36// "size": "2097",
37// "hash": "sha256:b6325614d5649338b87215d9536b3c0477729b8638994c74cdefacb020a2cad4"
38// }
39// Response
40// {
41// "ok": true,
42// "artifactId": "4"
43// }
44// 2. Download artifact
45// 2.1. ListArtifacts and optionally filter by artifact exact name or id
46// Post: /twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts
47// Request
48// {
49// "workflow_run_backend_id": "21",
50// "workflow_job_run_backend_id": "49",
51// "name_filter": "test"
52// }
53// Response
54// {
55// "artifacts": [
56// {
57// "workflowRunBackendId": "21",
58// "workflowJobRunBackendId": "49",
59// "databaseId": "4",
60// "name": "test",
61// "size": "2093",
62// "createdAt": "2024-01-23T00:13:28Z"
63// }
64// ]
65// }
66// 2.2. GetSignedArtifactURL get the URL to download the artifact zip file of a specific artifact
67// Post: /twirp/github.actions.results.api.v1.ArtifactService/GetSignedArtifactURL
68// Request
69// {
70// "workflow_run_backend_id": "21",
71// "workflow_job_run_backend_id": "49",
72// "name": "test"
73// }
74// Response
75// {
76// "signedUrl": "http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/DownloadArtifact?sig=wHzFOwpF-6220-5CA0CIRmAX9VbiTC2Mji89UOqo1E8=&expires=2024-01-23+21%3A51%3A56.872846295+%2B0100+CET&artifactName=test&taskID=76"
77// }
78// 2.3. Download Zip from Blobstorage (unauthenticated request)
79// GET: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/DownloadArtifact?sig=wHzFOwpF-6220-5CA0CIRmAX9VbiTC2Mji89UOqo1E8=&expires=2024-01-23+21%3A51%3A56.872846295+%2B0100+CET&artifactName=test&taskID=76
80
81import (
82 "crypto/hmac"
83 "crypto/sha256"
84 "encoding/base64"
85 "errors"
86 "fmt"
87 "hash/fnv"
88 "io"
89 "io/fs"
90 "net/http"
91 "net/url"
92 "os"
93 "path"
94 "strconv"
95 "strings"
96 "time"
97
98 "github.com/julienschmidt/httprouter"
99 log "github.com/sirupsen/logrus"
100 "google.golang.org/protobuf/encoding/protojson"
101 protoreflect "google.golang.org/protobuf/reflect/protoreflect"
102 "google.golang.org/protobuf/types/known/timestamppb"
103)
104
105const (
106 ArtifactV4RouteBase = "/twirp/github.actions.results.api.v1.ArtifactService"
107 ArtifactV4ContentEncoding = "application/zip"
108)
109
110type artifactV4Routes struct {
111 prefix string
112 fs WriteFS
113 rfs fs.FS
114 AppURL string
115 baseDir string
116}
117
118type ArtifactContext struct {
119 Req *http.Request
120 Resp http.ResponseWriter
121}
122
123func artifactNameToID(s string) int64 {
124 h := fnv.New32a()
125 h.Write([]byte(s))
126 return int64(h.Sum32())
127}
128
129func (c ArtifactContext) Error(status int, _ ...interface{}) {
130 c.Resp.WriteHeader(status)
131}
132
133func (c ArtifactContext) JSON(status int, _ ...interface{}) {
134 c.Resp.WriteHeader(status)
135}
136
137func validateRunIDV4(ctx *ArtifactContext, rawRunID string) (interface{}, int64, bool) {
138 runID, err := strconv.ParseInt(rawRunID, 10, 64)
139 if err != nil /* || task.Job.RunID != runID*/ {
140 log.Error("Error runID not match")
141 ctx.Error(http.StatusBadRequest, "run-id does not match")
142 return nil, 0, false
143 }
144 return nil, runID, true
145}
146
147func RoutesV4(router *httprouter.Router, baseDir string, fsys WriteFS, rfs fs.FS) {
148 route := &artifactV4Routes{
149 fs: fsys,
150 rfs: rfs,
151 baseDir: baseDir,
152 prefix: ArtifactV4RouteBase,
153 }
154 router.POST(path.Join(ArtifactV4RouteBase, "CreateArtifact"), func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
155 route.AppURL = r.Host
156 route.createArtifact(&ArtifactContext{
157 Req: r,
158 Resp: w,
159 })
160 })
161 router.POST(path.Join(ArtifactV4RouteBase, "FinalizeArtifact"), func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
162 route.finalizeArtifact(&ArtifactContext{
163 Req: r,
164 Resp: w,
165 })
166 })
167 router.POST(path.Join(ArtifactV4RouteBase, "ListArtifacts"), func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
168 route.listArtifacts(&ArtifactContext{
169 Req: r,
170 Resp: w,
171 })
172 })
173 router.POST(path.Join(ArtifactV4RouteBase, "GetSignedArtifactURL"), func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
174 route.AppURL = r.Host
175 route.getSignedArtifactURL(&ArtifactContext{
176 Req: r,
177 Resp: w,
178 })
179 })
180 router.POST(path.Join(ArtifactV4RouteBase, "DeleteArtifact"), func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
181 route.AppURL = r.Host
182 route.deleteArtifact(&ArtifactContext{
183 Req: r,
184 Resp: w,
185 })
186 })
187 router.PUT(path.Join(ArtifactV4RouteBase, "UploadArtifact"), func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
188 route.uploadArtifact(&ArtifactContext{
189 Req: r,
190 Resp: w,
191 })
192 })
193 router.GET(path.Join(ArtifactV4RouteBase, "DownloadArtifact"), func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
194 route.downloadArtifact(&ArtifactContext{
195 Req: r,
196 Resp: w,
197 })
198 })
199}
200
201func (r artifactV4Routes) buildSignature(endp, expires, artifactName string, taskID int64) []byte {
202 mac := hmac.New(sha256.New, []byte{0xba, 0xdb, 0xee, 0xf0})
203 mac.Write([]byte(endp))
204 mac.Write([]byte(expires))
205 mac.Write([]byte(artifactName))
206 mac.Write([]byte(fmt.Sprint(taskID)))
207 return mac.Sum(nil)
208}
209
210func (r artifactV4Routes) buildArtifactURL(endp, artifactName string, taskID int64) string {
211 expires := time.Now().Add(60 * time.Minute).Format("2006-01-02 15:04:05.999999999 -0700 MST")
212 uploadURL := "http://" + strings.TrimSuffix(r.AppURL, "/") + strings.TrimSuffix(r.prefix, "/") +
213 "/" + endp + "?sig=" + base64.URLEncoding.EncodeToString(r.buildSignature(endp, expires, artifactName, taskID)) + "&expires=" + url.QueryEscape(expires) + "&artifactName=" + url.QueryEscape(artifactName) + "&taskID=" + fmt.Sprint(taskID)
214 return uploadURL
215}
216
217func (r artifactV4Routes) verifySignature(ctx *ArtifactContext, endp string) (int64, string, bool) {
218 rawTaskID := ctx.Req.URL.Query().Get("taskID")
219 sig := ctx.Req.URL.Query().Get("sig")
220 expires := ctx.Req.URL.Query().Get("expires")
221 artifactName := ctx.Req.URL.Query().Get("artifactName")
222 dsig, _ := base64.URLEncoding.DecodeString(sig)
223 taskID, _ := strconv.ParseInt(rawTaskID, 10, 64)
224
225 expecedsig := r.buildSignature(endp, expires, artifactName, taskID)
226 if !hmac.Equal(dsig, expecedsig) {
227 log.Error("Error unauthorized")
228 ctx.Error(http.StatusUnauthorized, "Error unauthorized")
229 return -1, "", false
230 }
231 t, err := time.Parse("2006-01-02 15:04:05.999999999 -0700 MST", expires)
232 if err != nil || t.Before(time.Now()) {
233 log.Error("Error link expired")
234 ctx.Error(http.StatusUnauthorized, "Error link expired")
235 return -1, "", false
236 }
237 return taskID, artifactName, true
238}
239
240func (r *artifactV4Routes) parseProtbufBody(ctx *ArtifactContext, req protoreflect.ProtoMessage) bool {
241 body, err := io.ReadAll(ctx.Req.Body)
242 if err != nil {
243 log.Errorf("Error decode request body: %v", err)
244 ctx.Error(http.StatusInternalServerError, "Error decode request body")
245 return false
246 }
247 err = protojson.Unmarshal(body, req)
248 if err != nil {
249 log.Errorf("Error decode request body: %v", err)
250 ctx.Error(http.StatusInternalServerError, "Error decode request body")
251 return false
252 }
253 return true
254}
255
256func (r *artifactV4Routes) sendProtbufBody(ctx *ArtifactContext, req protoreflect.ProtoMessage) {
257 resp, err := protojson.Marshal(req)
258 if err != nil {
259 log.Errorf("Error encode response body: %v", err)
260 ctx.Error(http.StatusInternalServerError, "Error encode response body")
261 return
262 }
263 ctx.Resp.Header().Set("Content-Type", "application/json;charset=utf-8")
264 ctx.Resp.WriteHeader(http.StatusOK)
265 _, _ = ctx.Resp.Write(resp)
266}
267
268func (r *artifactV4Routes) createArtifact(ctx *ArtifactContext) {
269 var req CreateArtifactRequest
270
271 if ok := r.parseProtbufBody(ctx, &req); !ok {
272 return
273 }
274 _, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
275 if !ok {
276 return
277 }
278
279 artifactName := req.Name
280
281 safeRunPath := safeResolve(r.baseDir, fmt.Sprint(runID))
282 safePath := safeResolve(safeRunPath, artifactName)
283 safePath = safeResolve(safePath, artifactName+".zip")
284 file, err := r.fs.OpenWritable(safePath)
285
286 if err != nil {
287 panic(err)
288 }
289 file.Close()
290
291 respData := CreateArtifactResponse{
292 Ok: true,
293 SignedUploadUrl: r.buildArtifactURL("UploadArtifact", artifactName, runID),
294 }
295 r.sendProtbufBody(ctx, &respData)
296}
297
298func (r *artifactV4Routes) uploadArtifact(ctx *ArtifactContext) {
299 task, artifactName, ok := r.verifySignature(ctx, "UploadArtifact")
300 if !ok {
301 return
302 }
303
304 comp := ctx.Req.URL.Query().Get("comp")
305 switch comp {
306 case "block", "appendBlock":
307
308 safeRunPath := safeResolve(r.baseDir, fmt.Sprint(task))
309 safePath := safeResolve(safeRunPath, artifactName)
310 safePath = safeResolve(safePath, artifactName+".zip")
311
312 file, err := r.fs.OpenAppendable(safePath)
313
314 if err != nil {
315 panic(err)
316 }
317 defer file.Close()
318
319 writer, ok := file.(io.Writer)
320 if !ok {
321 panic(errors.New("File is not writable"))
322 }
323
324 if ctx.Req.Body == nil {
325 panic(errors.New("No body given"))
326 }
327
328 _, err = io.Copy(writer, ctx.Req.Body)
329 if err != nil {
330 panic(err)
331 }
332 file.Close()
333 ctx.JSON(http.StatusCreated, "appended")
334 case "blocklist":
335 ctx.JSON(http.StatusCreated, "created")
336 }
337}
338
339func (r *artifactV4Routes) finalizeArtifact(ctx *ArtifactContext) {
340 var req FinalizeArtifactRequest
341
342 if ok := r.parseProtbufBody(ctx, &req); !ok {
343 return
344 }
345 _, _, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
346 if !ok {
347 return
348 }
349
350 respData := FinalizeArtifactResponse{
351 Ok: true,
352 ArtifactId: artifactNameToID(req.Name),
353 }
354 r.sendProtbufBody(ctx, &respData)
355}
356
357func (r *artifactV4Routes) listArtifacts(ctx *ArtifactContext) {
358 var req ListArtifactsRequest
359
360 if ok := r.parseProtbufBody(ctx, &req); !ok {
361 return
362 }
363 _, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
364 if !ok {
365 return
366 }
367
368 safePath := safeResolve(r.baseDir, fmt.Sprint(runID))
369
370 entries, err := fs.ReadDir(r.rfs, safePath)
371 if err != nil {
372 panic(err)
373 }
374
375 list := []*ListArtifactsResponse_MonolithArtifact{}
376
377 for _, entry := range entries {
378 id := artifactNameToID(entry.Name())
379 if (req.NameFilter == nil || req.NameFilter.Value == entry.Name()) && (req.IdFilter == nil || req.IdFilter.Value == id) {
380 data := &ListArtifactsResponse_MonolithArtifact{
381 Name: entry.Name(),
382 CreatedAt: timestamppb.Now(),
383 DatabaseId: id,
384 WorkflowRunBackendId: req.WorkflowRunBackendId,
385 WorkflowJobRunBackendId: req.WorkflowJobRunBackendId,
386 Size: 0,
387 }
388 if info, err := entry.Info(); err == nil {
389 data.Size = info.Size()
390 data.CreatedAt = timestamppb.New(info.ModTime())
391 }
392 list = append(list, data)
393 }
394 }
395
396 respData := ListArtifactsResponse{
397 Artifacts: list,
398 }
399 r.sendProtbufBody(ctx, &respData)
400}
401
402func (r *artifactV4Routes) getSignedArtifactURL(ctx *ArtifactContext) {
403 var req GetSignedArtifactURLRequest
404
405 if ok := r.parseProtbufBody(ctx, &req); !ok {
406 return
407 }
408 _, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
409 if !ok {
410 return
411 }
412
413 artifactName := req.Name
414
415 respData := GetSignedArtifactURLResponse{}
416
417 respData.SignedUrl = r.buildArtifactURL("DownloadArtifact", artifactName, runID)
418 r.sendProtbufBody(ctx, &respData)
419}
420
421func (r *artifactV4Routes) downloadArtifact(ctx *ArtifactContext) {
422 task, artifactName, ok := r.verifySignature(ctx, "DownloadArtifact")
423 if !ok {
424 return
425 }
426
427 safeRunPath := safeResolve(r.baseDir, fmt.Sprint(task))
428 safePath := safeResolve(safeRunPath, artifactName)
429 safePath = safeResolve(safePath, artifactName+".zip")
430
431 file, _ := r.rfs.Open(safePath)
432
433 _, _ = io.Copy(ctx.Resp, file)
434}
435
436func (r *artifactV4Routes) deleteArtifact(ctx *ArtifactContext) {
437 var req DeleteArtifactRequest
438
439 if ok := r.parseProtbufBody(ctx, &req); !ok {
440 return
441 }
442 _, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
443 if !ok {
444 return
445 }
446 safeRunPath := safeResolve(r.baseDir, fmt.Sprint(runID))
447 safePath := safeResolve(safeRunPath, req.Name)
448
449 _ = os.RemoveAll(safePath)
450
451 respData := DeleteArtifactResponse{
452 Ok: true,
453 ArtifactId: artifactNameToID(req.Name),
454 }
455 r.sendProtbufBody(ctx, &respData)
456}
Deleted act/pkg/artifacts/server.go
@@ -1,319 +0,0 @@
1package artifacts
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "io"
9 "io/fs"
10 "net/http"
11 "os"
12 "path/filepath"
13 "strings"
14 "time"
15
16 "github.com/julienschmidt/httprouter"
17
18 "github.com/nektos/act/pkg/common"
19)
20
21type FileContainerResourceURL struct {
22 FileContainerResourceURL string `json:"fileContainerResourceUrl"`
23}
24
25type NamedFileContainerResourceURL struct {
26 Name string `json:"name"`
27 FileContainerResourceURL string `json:"fileContainerResourceUrl"`
28}
29
30type NamedFileContainerResourceURLResponse struct {
31 Count int `json:"count"`
32 Value []NamedFileContainerResourceURL `json:"value"`
33}
34
35type ContainerItem struct {
36 Path string `json:"path"`
37 ItemType string `json:"itemType"`
38 ContentLocation string `json:"contentLocation"`
39}
40
41type ContainerItemResponse struct {
42 Value []ContainerItem `json:"value"`
43}
44
45type ResponseMessage struct {
46 Message string `json:"message"`
47}
48
49type WritableFile interface {
50 io.WriteCloser
51}
52
53type WriteFS interface {
54 OpenWritable(name string) (WritableFile, error)
55 OpenAppendable(name string) (WritableFile, error)
56}
57
58type readWriteFSImpl struct {
59}
60
61func (fwfs readWriteFSImpl) Open(name string) (fs.File, error) {
62 return os.Open(name)
63}
64
65func (fwfs readWriteFSImpl) OpenWritable(name string) (WritableFile, error) {
66 if err := os.MkdirAll(filepath.Dir(name), os.ModePerm); err != nil {
67 return nil, err
68 }
69 return os.OpenFile(name, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0o644)
70}
71
72func (fwfs readWriteFSImpl) OpenAppendable(name string) (WritableFile, error) {
73 if err := os.MkdirAll(filepath.Dir(name), os.ModePerm); err != nil {
74 return nil, err
75 }
76 file, err := os.OpenFile(name, os.O_CREATE|os.O_RDWR, 0o644)
77
78 if err != nil {
79 return nil, err
80 }
81
82 _, err = file.Seek(0, io.SeekEnd)
83 if err != nil {
84 return nil, err
85 }
86 return file, nil
87}
88
89var gzipExtension = ".gz__"
90
91func safeResolve(baseDir string, relPath string) string {
92 return filepath.Join(baseDir, filepath.Clean(filepath.Join(string(os.PathSeparator), relPath)))
93}
94
95func uploads(router *httprouter.Router, baseDir string, fsys WriteFS) {
96 router.POST("/_apis/pipelines/workflows/:runId/artifacts", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
97 runID := params.ByName("runId")
98
99 json, err := json.Marshal(FileContainerResourceURL{
100 FileContainerResourceURL: fmt.Sprintf("http://%s/upload/%s", req.Host, runID),
101 })
102 if err != nil {
103 panic(err)
104 }
105
106 _, err = w.Write(json)
107 if err != nil {
108 panic(err)
109 }
110 })
111
112 router.PUT("/upload/:runId", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
113 itemPath := req.URL.Query().Get("itemPath")
114 runID := params.ByName("runId")
115
116 if req.Header.Get("Content-Encoding") == "gzip" {
117 itemPath += gzipExtension
118 }
119
120 safeRunPath := safeResolve(baseDir, runID)
121 safePath := safeResolve(safeRunPath, itemPath)
122
123 file, err := func() (WritableFile, error) {
124 contentRange := req.Header.Get("Content-Range")
125 if contentRange != "" && !strings.HasPrefix(contentRange, "bytes 0-") {
126 return fsys.OpenAppendable(safePath)
127 }
128 return fsys.OpenWritable(safePath)
129 }()
130
131 if err != nil {
132 panic(err)
133 }
134 defer file.Close()
135
136 writer, ok := file.(io.Writer)
137 if !ok {
138 panic(errors.New("File is not writable"))
139 }
140
141 if req.Body == nil {
142 panic(errors.New("No body given"))
143 }
144
145 _, err = io.Copy(writer, req.Body)
146 if err != nil {
147 panic(err)
148 }
149
150 json, err := json.Marshal(ResponseMessage{
151 Message: "success",
152 })
153 if err != nil {
154 panic(err)
155 }
156
157 _, err = w.Write(json)
158 if err != nil {
159 panic(err)
160 }
161 })
162
163 router.PATCH("/_apis/pipelines/workflows/:runId/artifacts", func(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
164 json, err := json.Marshal(ResponseMessage{
165 Message: "success",
166 })
167 if err != nil {
168 panic(err)
169 }
170
171 _, err = w.Write(json)
172 if err != nil {
173 panic(err)
174 }
175 })
176}
177
178func downloads(router *httprouter.Router, baseDir string, fsys fs.FS) {
179 router.GET("/_apis/pipelines/workflows/:runId/artifacts", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
180 runID := params.ByName("runId")
181
182 safePath := safeResolve(baseDir, runID)
183
184 entries, err := fs.ReadDir(fsys, safePath)
185 if err != nil {
186 panic(err)
187 }
188
189 var list []NamedFileContainerResourceURL
190 for _, entry := range entries {
191 list = append(list, NamedFileContainerResourceURL{
192 Name: entry.Name(),
193 FileContainerResourceURL: fmt.Sprintf("http://%s/download/%s", req.Host, runID),
194 })
195 }
196
197 json, err := json.Marshal(NamedFileContainerResourceURLResponse{
198 Count: len(list),
199 Value: list,
200 })
201 if err != nil {
202 panic(err)
203 }
204
205 _, err = w.Write(json)
206 if err != nil {
207 panic(err)
208 }
209 })
210
211 router.GET("/download/:container", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
212 container := params.ByName("container")
213 itemPath := req.URL.Query().Get("itemPath")
214 safePath := safeResolve(baseDir, filepath.Join(container, itemPath))
215
216 var files []ContainerItem
217 err := fs.WalkDir(fsys, safePath, func(path string, entry fs.DirEntry, _ error) error {
218 if !entry.IsDir() {
219 rel, err := filepath.Rel(safePath, path)
220 if err != nil {
221 panic(err)
222 }
223
224 // if it was upload as gzip
225 rel = strings.TrimSuffix(rel, gzipExtension)
226 path := filepath.Join(itemPath, rel)
227
228 rel = filepath.ToSlash(rel)
229 path = filepath.ToSlash(path)
230
231 files = append(files, ContainerItem{
232 Path: path,
233 ItemType: "file",
234 ContentLocation: fmt.Sprintf("http://%s/artifact/%s/%s/%s", req.Host, container, itemPath, rel),
235 })
236 }
237 return nil
238 })
239 if err != nil {
240 panic(err)
241 }
242
243 json, err := json.Marshal(ContainerItemResponse{
244 Value: files,
245 })
246 if err != nil {
247 panic(err)
248 }
249
250 _, err = w.Write(json)
251 if err != nil {
252 panic(err)
253 }
254 })
255
256 router.GET("/artifact/*path", func(w http.ResponseWriter, _ *http.Request, params httprouter.Params) {
257 path := params.ByName("path")[1:]
258
259 safePath := safeResolve(baseDir, path)
260
261 file, err := fsys.Open(safePath)
262 if err != nil {
263 // try gzip file
264 file, err = fsys.Open(safePath + gzipExtension)
265 if err != nil {
266 panic(err)
267 }
268 w.Header().Add("Content-Encoding", "gzip")
269 }
270
271 _, err = io.Copy(w, file)
272 if err != nil {
273 panic(err)
274 }
275 })
276}
277
278func Serve(ctx context.Context, artifactPath string, addr string, port string) context.CancelFunc {
279 serverContext, cancel := context.WithCancel(ctx)
280 logger := common.Logger(serverContext)
281
282 if artifactPath == "" {
283 return cancel
284 }
285
286 router := httprouter.New()
287
288 logger.Debugf("Artifacts base path '%s'", artifactPath)
289 fsys := readWriteFSImpl{}
290 uploads(router, artifactPath, fsys)
291 downloads(router, artifactPath, fsys)
292 RoutesV4(router, artifactPath, fsys, fsys)
293
294 server := &http.Server{
295 Addr: fmt.Sprintf("%s:%s", addr, port),
296 ReadHeaderTimeout: 2 * time.Second,
297 Handler: router,
298 }
299
300 // run server
301 go func() {
302 logger.Infof("Start server on http://%s:%s", addr, port)
303 if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
304 logger.Fatal(err)
305 }
306 }()
307
308 // wait for cancel to gracefully shutdown server
309 go func() {
310 <-serverContext.Done()
311
312 if err := server.Shutdown(ctx); err != nil {
313 logger.Errorf("Failed shutdown gracefully - force shutdown: %v", err)
314 server.Close()
315 }
316 }()
317
318 return cancel
319}
Deleted act/pkg/common/git/git.go
@@ -1,417 +0,0 @@
1package git
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "io"
8 "os"
9 "path"
10 "regexp"
11 "strings"
12 "sync"
13
14 "github.com/go-git/go-git/v5"
15 "github.com/go-git/go-git/v5/config"
16 "github.com/go-git/go-git/v5/plumbing"
17 "github.com/go-git/go-git/v5/plumbing/storer"
18 "github.com/go-git/go-git/v5/plumbing/transport/http"
19 "github.com/mattn/go-isatty"
20 log "github.com/sirupsen/logrus"
21
22 "github.com/nektos/act/pkg/common"
23)
24
25var (
26 codeCommitHTTPRegex = regexp.MustCompile(`^https?://git-codecommit\.(.+)\.amazonaws.com/v1/repos/(.+)$`)
27 codeCommitSSHRegex = regexp.MustCompile(`ssh://git-codecommit\.(.+)\.amazonaws.com/v1/repos/(.+)$`)
28 githubHTTPRegex = regexp.MustCompile(`^https?://.*github.com.*/(.+)/(.+?)(?:.git)?$`)
29 githubSSHRegex = regexp.MustCompile(`github.com[:/](.+)/(.+?)(?:.git)?$`)
30
31 cloneLock sync.Mutex
32
33 ErrShortRef = errors.New("short SHA references are not supported")
34 ErrNoRepo = errors.New("unable to find git repo")
35)
36
37type Error struct {
38 err error
39 commit string
40}
41
42func (e *Error) Error() string {
43 return e.err.Error()
44}
45
46func (e *Error) Unwrap() error {
47 return e.err
48}
49
50func (e *Error) Commit() string {
51 return e.commit
52}
53
54// FindGitRevision get the current git revision
55func FindGitRevision(ctx context.Context, file string) (shortSha string, sha string, err error) {
56 logger := common.Logger(ctx)
57
58 gitDir, err := git.PlainOpenWithOptions(
59 file,
60 &git.PlainOpenOptions{
61 DetectDotGit: true,
62 EnableDotGitCommonDir: true,
63 },
64 )
65
66 if err != nil {
67 logger.WithError(err).Error("path", file, "not located inside a git repository")
68 return "", "", err
69 }
70
71 head, err := gitDir.Reference(plumbing.HEAD, true)
72 if err != nil {
73 return "", "", err
74 }
75
76 if head.Hash().IsZero() {
77 return "", "", fmt.Errorf("HEAD sha1 could not be resolved")
78 }
79
80 hash := head.Hash().String()
81
82 logger.Debugf("Found revision: %s", hash)
83 return hash[:7], strings.TrimSpace(hash), nil
84}
85
86// FindGitRef get the current git ref
87func FindGitRef(ctx context.Context, file string) (string, error) {
88 logger := common.Logger(ctx)
89
90 logger.Debugf("Loading revision from git directory")
91 _, ref, err := FindGitRevision(ctx, file)
92 if err != nil {
93 return "", err
94 }
95
96 logger.Debugf("HEAD points to '%s'", ref)
97
98 // Prefer the git library to iterate over the references and find a matching tag or branch.
99 var refTag = ""
100 var refBranch = ""
101 repo, err := git.PlainOpenWithOptions(
102 file,
103 &git.PlainOpenOptions{
104 DetectDotGit: true,
105 EnableDotGitCommonDir: true,
106 },
107 )
108
109 if err != nil {
110 return "", err
111 }
112
113 iter, err := repo.References()
114 if err != nil {
115 return "", err
116 }
117
118 // find the reference that matches the revision's has
119 err = iter.ForEach(func(r *plumbing.Reference) error {
120 /* tags and branches will have the same hash
121 * when a user checks out a tag, it is not mentioned explicitly
122 * in the go-git package, we must identify the revision
123 * then check if any tag matches that revision,
124 * if so then we checked out a tag
125 * else we look for branches and if matches,
126 * it means we checked out a branch
127 *
128 * If a branches matches first we must continue and check all tags (all references)
129 * in case we match with a tag later in the iteration
130 */
131 if r.Hash().String() == ref {
132 if r.Name().IsTag() {
133 refTag = r.Name().String()
134 }
135 if r.Name().IsBranch() {
136 refBranch = r.Name().String()
137 }
138 }
139
140 // we found what we where looking for
141 if refTag != "" && refBranch != "" {
142 return storer.ErrStop
143 }
144
145 return nil
146 })
147
148 if err != nil {
149 return "", err
150 }
151
152 // order matters here see above comment.
153 if refTag != "" {
154 return refTag, nil
155 }
156 if refBranch != "" {
157 return refBranch, nil
158 }
159
160 return "", fmt.Errorf("failed to identify reference (tag/branch) for the checked-out revision '%s'", ref)
161}
162
163// FindGithubRepo get the repo
164func FindGithubRepo(ctx context.Context, file, githubInstance, remoteName string) (string, error) {
165 if remoteName == "" {
166 remoteName = "origin"
167 }
168
169 url, err := findGitRemoteURL(ctx, file, remoteName)
170 if err != nil {
171 return "", err
172 }
173 _, slug, err := findGitSlug(url, githubInstance)
174 return slug, err
175}
176
177func findGitRemoteURL(_ context.Context, file, remoteName string) (string, error) {
178 repo, err := git.PlainOpenWithOptions(
179 file,
180 &git.PlainOpenOptions{
181 DetectDotGit: true,
182 EnableDotGitCommonDir: true,
183 },
184 )
185 if err != nil {
186 return "", err
187 }
188
189 remote, err := repo.Remote(remoteName)
190 if err != nil {
191 return "", err
192 }
193
194 if len(remote.Config().URLs) < 1 {
195 return "", fmt.Errorf("remote '%s' exists but has no URL", remoteName)
196 }
197
198 return remote.Config().URLs[0], nil
199}
200
201func findGitSlug(url string, githubInstance string) (string, string, error) {
202 if matches := codeCommitHTTPRegex.FindStringSubmatch(url); matches != nil {
203 return "CodeCommit", matches[2], nil
204 } else if matches := codeCommitSSHRegex.FindStringSubmatch(url); matches != nil {
205 return "CodeCommit", matches[2], nil
206 } else if matches := githubHTTPRegex.FindStringSubmatch(url); matches != nil {
207 return "GitHub", fmt.Sprintf("%s/%s", matches[1], matches[2]), nil
208 } else if matches := githubSSHRegex.FindStringSubmatch(url); matches != nil {
209 return "GitHub", fmt.Sprintf("%s/%s", matches[1], matches[2]), nil
210 } else if githubInstance != "github.com" {
211 gheHTTPRegex := regexp.MustCompile(fmt.Sprintf(`^https?://%s/(.+)/(.+?)(?:.git)?$`, githubInstance))
212 gheSSHRegex := regexp.MustCompile(fmt.Sprintf(`%s[:/](.+)/(.+?)(?:.git)?$`, githubInstance))
213 if matches := gheHTTPRegex.FindStringSubmatch(url); matches != nil {
214 return "GitHubEnterprise", fmt.Sprintf("%s/%s", matches[1], matches[2]), nil
215 } else if matches := gheSSHRegex.FindStringSubmatch(url); matches != nil {
216 return "GitHubEnterprise", fmt.Sprintf("%s/%s", matches[1], matches[2]), nil
217 }
218 }
219 return "", url, nil
220}
221
222// NewGitCloneExecutorInput the input for the NewGitCloneExecutor
223type NewGitCloneExecutorInput struct {
224 URL string
225 Ref string
226 Dir string
227 Token string
228 OfflineMode bool
229}
230
231// CloneIfRequired ...
232func CloneIfRequired(ctx context.Context, refName plumbing.ReferenceName, input NewGitCloneExecutorInput, logger log.FieldLogger) (*git.Repository, error) {
233 // If the remote URL has changed, remove the directory and clone again.
234 if r, err := git.PlainOpen(input.Dir); err == nil {
235 if remote, err := r.Remote("origin"); err == nil {
236 if len(remote.Config().URLs) > 0 && remote.Config().URLs[0] != input.URL {
237 _ = os.RemoveAll(input.Dir)
238 }
239 }
240 }
241
242 r, err := git.PlainOpen(input.Dir)
243 if err != nil {
244 var progressWriter io.Writer
245 if isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) {
246 if entry, ok := logger.(*log.Entry); ok {
247 progressWriter = entry.WriterLevel(log.DebugLevel)
248 } else if lgr, ok := logger.(*log.Logger); ok {
249 progressWriter = lgr.WriterLevel(log.DebugLevel)
250 } else {
251 log.Errorf("Unable to get writer from logger (type=%T)", logger)
252 progressWriter = os.Stdout
253 }
254 }
255
256 cloneOptions := git.CloneOptions{
257 URL: input.URL,
258 Progress: progressWriter,
259 }
260 if input.Token != "" {
261 cloneOptions.Auth = &http.BasicAuth{
262 Username: "token",
263 Password: input.Token,
264 }
265 }
266
267 r, err = git.PlainCloneContext(ctx, input.Dir, false, &cloneOptions)
268 if err != nil {
269 logger.Errorf("Unable to clone %v %s: %v", input.URL, refName, err)
270 return nil, err
271 }
272
273 if err = os.Chmod(input.Dir, 0o755); err != nil {
274 return nil, err
275 }
276 }
277
278 return r, nil
279}
280
281func gitOptions(token string) (fetchOptions git.FetchOptions, pullOptions git.PullOptions) {
282 fetchOptions.RefSpecs = []config.RefSpec{"refs/*:refs/*", "HEAD:refs/heads/HEAD"}
283 pullOptions.Force = true
284
285 if token != "" {
286 auth := &http.BasicAuth{
287 Username: "token",
288 Password: token,
289 }
290 fetchOptions.Auth = auth
291 pullOptions.Auth = auth
292 }
293
294 return fetchOptions, pullOptions
295}
296
297// NewGitCloneExecutor creates an executor to clone git repos
298//
299//nolint:gocyclo
300func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor {
301 return func(ctx context.Context) error {
302 logger := common.Logger(ctx)
303 logger.Infof(" \u2601 git clone '%s' # ref=%s", input.URL, input.Ref)
304 logger.Debugf(" cloning %s to %s", input.URL, input.Dir)
305
306 cloneLock.Lock()
307 defer cloneLock.Unlock()
308
309 refName := plumbing.ReferenceName(fmt.Sprintf("refs/heads/%s", input.Ref))
310 r, err := CloneIfRequired(ctx, refName, input, logger)
311 if err != nil {
312 return err
313 }
314
315 isOfflineMode := input.OfflineMode
316
317 // fetch latest changes
318 fetchOptions, pullOptions := gitOptions(input.Token)
319
320 if !isOfflineMode {
321 err = r.Fetch(&fetchOptions)
322 if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) {
323 return err
324 }
325 }
326
327 var hash *plumbing.Hash
328 rev := plumbing.Revision(input.Ref)
329 if hash, err = r.ResolveRevision(rev); err != nil {
330 logger.Errorf("Unable to resolve %s: %v", input.Ref, err)
331 }
332
333 if hash.String() != input.Ref && len(input.Ref) >= 4 && strings.HasPrefix(hash.String(), input.Ref) {
334 return &Error{
335 err: ErrShortRef,
336 commit: hash.String(),
337 }
338 }
339
340 // At this point we need to know if it's a tag or a branch
341 // And the easiest way to do it is duck typing
342 //
343 // If err is nil, it's a tag so let's proceed with that hash like we would if
344 // it was a sha
345 refType := "tag"
346 rev = plumbing.Revision(path.Join("refs", "tags", input.Ref))
347 if _, err := r.Tag(input.Ref); errors.Is(err, git.ErrTagNotFound) {
348 rName := plumbing.ReferenceName(path.Join("refs", "remotes", "origin", input.Ref))
349 if _, err := r.Reference(rName, false); errors.Is(err, plumbing.ErrReferenceNotFound) {
350 refType = "sha"
351 rev = plumbing.Revision(input.Ref)
352 } else {
353 refType = "branch"
354 rev = plumbing.Revision(rName)
355 }
356 }
357
358 if hash, err = r.ResolveRevision(rev); err != nil {
359 logger.Errorf("Unable to resolve %s: %v", input.Ref, err)
360 return err
361 }
362
363 var w *git.Worktree
364 if w, err = r.Worktree(); err != nil {
365 return err
366 }
367
368 // If the hash resolved doesn't match the ref provided in a workflow then we're
369 // using a branch or tag ref, not a sha
370 //
371 // Repos on disk point to commit hashes, and need to checkout input.Ref before
372 // we try and pull down any changes
373 if hash.String() != input.Ref && refType == "branch" {
374 logger.Debugf("Provided ref is not a sha. Checking out branch before pulling changes")
375 sourceRef := plumbing.ReferenceName(path.Join("refs", "remotes", "origin", input.Ref))
376 if err = w.Checkout(&git.CheckoutOptions{
377 Branch: sourceRef,
378 Force: true,
379 }); err != nil {
380 logger.Errorf("Unable to checkout %s: %v", sourceRef, err)
381 return err
382 }
383 }
384 if !isOfflineMode {
385 if err = w.Pull(&pullOptions); err != nil && err != git.NoErrAlreadyUpToDate {
386 logger.Debugf("Unable to pull %s: %v", refName, err)
387 }
388 }
389 logger.Debugf("Cloned %s to %s", input.URL, input.Dir)
390
391 if hash.String() != input.Ref && refType == "branch" {
392 logger.Debugf("Provided ref is not a sha. Updating branch ref after pull")
393 if hash, err = r.ResolveRevision(rev); err != nil {
394 logger.Errorf("Unable to resolve %s: %v", input.Ref, err)
395 return err
396 }
397 }
398 if err = w.Checkout(&git.CheckoutOptions{
399 Hash: *hash,
400 Force: true,
401 }); err != nil {
402 logger.Errorf("Unable to checkout %s: %v", *hash, err)
403 return err
404 }
405
406 if err = w.Reset(&git.ResetOptions{
407 Mode: git.HardReset,
408 Commit: *hash,
409 }); err != nil {
410 logger.Errorf("Unable to reset to %s: %v", hash.String(), err)
411 return err
412 }
413
414 logger.Debugf("Checked out %s", input.Ref)
415 return nil
416 }
417}
Deleted act/pkg/common/auth.go
@@ -1,100 +0,0 @@
1// Copyright 2024 The Gitea Authors. All rights reserved.
2// SPDX-License-Identifier: MIT
3
4package common
5
6import (
7 "encoding/json"
8 "fmt"
9 "net/http"
10 "strings"
11 "time"
12
13 "github.com/golang-jwt/jwt/v5"
14 log "github.com/sirupsen/logrus"
15)
16
17type actionsClaims struct {
18 jwt.RegisteredClaims
19 Scp string `json:"scp"`
20 TaskID int64
21 RunID int64
22 JobID int64
23 Ac string `json:"ac"`
24}
25
26type actionsCacheScope struct {
27 Scope string
28 Permission actionsCachePermission
29}
30
31type actionsCachePermission int
32
33const (
34 actionsCachePermissionRead = 1 << iota
35 actionsCachePermissionWrite
36)
37
38func CreateAuthorizationToken(taskID, runID, jobID int64) (string, error) {
39 now := time.Now()
40
41 ac, err := json.Marshal(&[]actionsCacheScope{
42 {
43 Scope: "",
44 Permission: actionsCachePermissionWrite,
45 },
46 })
47 if err != nil {
48 return "", err
49 }
50
51 claims := actionsClaims{
52 RegisteredClaims: jwt.RegisteredClaims{
53 ExpiresAt: jwt.NewNumericDate(now.Add(24 * time.Hour)),
54 NotBefore: jwt.NewNumericDate(now),
55 },
56 Scp: fmt.Sprintf("Actions.Results:%d:%d", runID, jobID),
57 TaskID: taskID,
58 RunID: runID,
59 JobID: jobID,
60 Ac: string(ac),
61 }
62 token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
63
64 tokenString, err := token.SignedString([]byte{})
65 if err != nil {
66 return "", err
67 }
68
69 return tokenString, nil
70}
71
72func ParseAuthorizationToken(req *http.Request) (int64, error) {
73 h := req.Header.Get("Authorization")
74 if h == "" {
75 return 0, nil
76 }
77
78 parts := strings.SplitN(h, " ", 2)
79 if len(parts) != 2 {
80 log.Errorf("split token failed: %s", h)
81 return 0, fmt.Errorf("split token failed")
82 }
83
84 token, err := jwt.ParseWithClaims(parts[1], &actionsClaims{}, func(t *jwt.Token) (any, error) {
85 if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
86 return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
87 }
88 return []byte{}, nil
89 })
90 if err != nil {
91 return 0, err
92 }
93
94 c, ok := token.Claims.(*actionsClaims)
95 if !token.Valid || !ok {
96 return 0, fmt.Errorf("invalid token claim")
97 }
98
99 return c.TaskID, nil
100}
Deleted act/pkg/common/cartesian.go
@@ -1,54 +0,0 @@
1package common
2
3// CartesianProduct takes map of lists and returns list of unique tuples
4func CartesianProduct(mapOfLists map[string][]interface{}) []map[string]interface{} {
5 listNames := make([]string, 0)
6 lists := make([][]interface{}, 0)
7 for k, v := range mapOfLists {
8 listNames = append(listNames, k)
9 lists = append(lists, v)
10 }
11
12 listCart := cartN(lists...)
13
14 rtn := make([]map[string]interface{}, 0)
15 for _, list := range listCart {
16 vMap := make(map[string]interface{})
17 for i, v := range list {
18 vMap[listNames[i]] = v
19 }
20 rtn = append(rtn, vMap)
21 }
22 return rtn
23}
24
25func cartN(a ...[]interface{}) [][]interface{} {
26 c := 1
27 for _, a := range a {
28 c *= len(a)
29 }
30 if c == 0 || len(a) == 0 {
31 return nil
32 }
33 p := make([][]interface{}, c)
34 b := make([]interface{}, c*len(a))
35 n := make([]int, len(a))
36 s := 0
37 for i := range p {
38 e := s + len(a)
39 pi := b[s:e]
40 p[i] = pi
41 s = e
42 for j, n := range n {
43 pi[j] = a[j][n]
44 }
45 for j := len(n) - 1; j >= 0; j-- {
46 n[j]++
47 if n[j] < len(a[j]) {
48 break
49 }
50 n[j] = 0
51 }
52 }
53 return p
54}
Deleted act/pkg/common/context.go
@@ -1,45 +0,0 @@
1package common
2
3import (
4 "context"
5 "os"
6 "os/signal"
7 "syscall"
8)
9
10func createGracefulJobCancellationContext() (context.Context, func(), chan os.Signal) {
11 ctx := context.Background()
12 ctx, forceCancel := context.WithCancel(ctx)
13 cancelCtx, cancel := context.WithCancel(ctx)
14 ctx = WithJobCancelContext(ctx, cancelCtx)
15
16 // trap Ctrl+C and call cancel on the context
17 c := make(chan os.Signal, 1)
18 signal.Notify(c, os.Interrupt, syscall.SIGTERM)
19 go func() {
20 select {
21 case sig := <-c:
22 if sig == os.Interrupt {
23 cancel()
24 select {
25 case <-c:
26 forceCancel()
27 case <-ctx.Done():
28 }
29 } else {
30 forceCancel()
31 }
32 case <-ctx.Done():
33 }
34 }()
35 return ctx, func() {
36 signal.Stop(c)
37 forceCancel()
38 cancel()
39 }, c
40}
41
42func CreateGracefulJobCancellationContext() (context.Context, func()) {
43 ctx, cancel, _ := createGracefulJobCancellationContext()
44 return ctx, cancel
45}
Deleted act/pkg/common/draw.go
@@ -1,143 +0,0 @@
1package common
2
3import (
4 "fmt"
5 "io"
6 "os"
7 "strings"
8)
9
10// Style is a specific style
11type Style int
12
13// Styles
14const (
15 StyleDoubleLine = iota
16 StyleSingleLine
17 StyleDashedLine
18 StyleNoLine
19)
20
21// NewPen creates a new pen
22func NewPen(style Style, color int) *Pen {
23 bgcolor := 49
24 if os.Getenv("CLICOLOR") == "0" {
25 color = 0
26 bgcolor = 0
27 }
28 return &Pen{
29 style: style,
30 color: color,
31 bgcolor: bgcolor,
32 }
33}
34
35type styleDef struct {
36 cornerTL string
37 cornerTR string
38 cornerBL string
39 cornerBR string
40 lineH string
41 lineV string
42}
43
44var styleDefs = []styleDef{
45 {"\u2554", "\u2557", "\u255a", "\u255d", "\u2550", "\u2551"},
46 {"\u256d", "\u256e", "\u2570", "\u256f", "\u2500", "\u2502"},
47 {"\u250c", "\u2510", "\u2514", "\u2518", "\u254c", "\u254e"},
48 {" ", " ", " ", " ", " ", " "},
49}
50
51// Pen struct
52type Pen struct {
53 style Style
54 color int
55 bgcolor int
56}
57
58// Drawing struct
59type Drawing struct {
60 buf *strings.Builder
61 width int
62}
63
64func (p *Pen) drawTopBars(buf io.Writer, labels ...string) {
65 style := styleDefs[p.style]
66 for _, label := range labels {
67 bar := strings.Repeat(style.lineH, len(label)+2)
68 fmt.Fprintf(buf, " ")
69 fmt.Fprintf(buf, "\x1b[%d;%dm", p.color, p.bgcolor)
70 fmt.Fprintf(buf, "%s%s%s", style.cornerTL, bar, style.cornerTR)
71 fmt.Fprintf(buf, "\x1b[%dm", 0)
72 }
73 fmt.Fprintf(buf, "\n")
74}
75func (p *Pen) drawBottomBars(buf io.Writer, labels ...string) {
76 style := styleDefs[p.style]
77 for _, label := range labels {
78 bar := strings.Repeat(style.lineH, len(label)+2)
79 fmt.Fprintf(buf, " ")
80 fmt.Fprintf(buf, "\x1b[%d;%dm", p.color, p.bgcolor)
81 fmt.Fprintf(buf, "%s%s%s", style.cornerBL, bar, style.cornerBR)
82 fmt.Fprintf(buf, "\x1b[%dm", 0)
83 }
84 fmt.Fprintf(buf, "\n")
85}
86func (p *Pen) drawLabels(buf io.Writer, labels ...string) {
87 style := styleDefs[p.style]
88 for _, label := range labels {
89 fmt.Fprintf(buf, " ")
90 fmt.Fprintf(buf, "\x1b[%d;%dm", p.color, p.bgcolor)
91 fmt.Fprintf(buf, "%s %s %s", style.lineV, label, style.lineV)
92 fmt.Fprintf(buf, "\x1b[%dm", 0)
93 }
94 fmt.Fprintf(buf, "\n")
95}
96
97// DrawArrow between boxes
98func (p *Pen) DrawArrow() *Drawing {
99 drawing := &Drawing{
100 buf: new(strings.Builder),
101 width: 1,
102 }
103 fmt.Fprintf(drawing.buf, "\x1b[%dm", p.color)
104 fmt.Fprintf(drawing.buf, "\u2b07")
105 fmt.Fprintf(drawing.buf, "\x1b[%dm", 0)
106 return drawing
107}
108
109// DrawBoxes to draw boxes
110func (p *Pen) DrawBoxes(labels ...string) *Drawing {
111 width := 0
112 for _, l := range labels {
113 width += len(l) + 2 + 2 + 1
114 }
115 drawing := &Drawing{
116 buf: new(strings.Builder),
117 width: width,
118 }
119 p.drawTopBars(drawing.buf, labels...)
120 p.drawLabels(drawing.buf, labels...)
121 p.drawBottomBars(drawing.buf, labels...)
122
123 return drawing
124}
125
126// Draw to writer
127func (d *Drawing) Draw(writer io.Writer, centerOnWidth int) {
128 padSize := (centerOnWidth - d.GetWidth()) / 2
129 if padSize < 0 {
130 padSize = 0
131 }
132 for _, l := range strings.Split(d.buf.String(), "\n") {
133 if len(l) > 0 {
134 padding := strings.Repeat(" ", padSize)
135 fmt.Fprintf(writer, "%s%s\n", padding, l)
136 }
137 }
138}
139
140// GetWidth of drawing
141func (d *Drawing) GetWidth() int {
142 return d.width
143}
Deleted act/pkg/common/dryrun.go
@@ -1,25 +0,0 @@
1package common
2
3import (
4 "context"
5)
6
7type dryrunContextKey string
8
9const dryrunContextKeyVal = dryrunContextKey("dryrun")
10
11// Dryrun returns true if the current context is dryrun
12func Dryrun(ctx context.Context) bool {
13 val := ctx.Value(dryrunContextKeyVal)
14 if val != nil {
15 if dryrun, ok := val.(bool); ok {
16 return dryrun
17 }
18 }
19 return false
20}
21
22// WithDryrun adds a value to the context for dryrun
23func WithDryrun(ctx context.Context, dryrun bool) context.Context {
24 return context.WithValue(ctx, dryrunContextKeyVal, dryrun)
25}
Deleted act/pkg/common/executor.go
@@ -1,241 +0,0 @@
1package common
2
3import (
4 "context"
5 "errors"
6 "fmt"
7
8 log "github.com/sirupsen/logrus"
9)
10
11// Warning that implements `error` but safe to ignore
12type Warning struct {
13 Message string
14}
15
16// Error the contract for error
17func (w Warning) Error() string {
18 return w.Message
19}
20
21// Warningf create a warning
22func Warningf(format string, args ...interface{}) Warning {
23 w := Warning{
24 Message: fmt.Sprintf(format, args...),
25 }
26 return w
27}
28
29// Executor define contract for the steps of a workflow
30type Executor func(ctx context.Context) error
31
32// Conditional define contract for the conditional predicate
33type Conditional func(ctx context.Context) bool
34
35// NewInfoExecutor is an executor that logs messages
36func NewInfoExecutor(format string, args ...interface{}) Executor {
37 return func(ctx context.Context) error {
38 logger := Logger(ctx)
39 logger.Infof(format, args...)
40 return nil
41 }
42}
43
44// NewDebugExecutor is an executor that logs messages
45func NewDebugExecutor(format string, args ...interface{}) Executor {
46 return func(ctx context.Context) error {
47 logger := Logger(ctx)
48 logger.Debugf(format, args...)
49 return nil
50 }
51}
52
53// NewPipelineExecutor creates a new executor from a series of other executors
54func NewPipelineExecutor(executors ...Executor) Executor {
55 if len(executors) == 0 {
56 return func(_ context.Context) error {
57 return nil
58 }
59 }
60 var rtn Executor
61 for _, executor := range executors {
62 if rtn == nil {
63 rtn = executor
64 } else {
65 rtn = rtn.Then(executor)
66 }
67 }
68 return rtn
69}
70
71// NewConditionalExecutor creates a new executor based on conditions
72func NewConditionalExecutor(conditional Conditional, trueExecutor Executor, falseExecutor Executor) Executor {
73 return func(ctx context.Context) error {
74 if conditional(ctx) {
75 if trueExecutor != nil {
76 return trueExecutor(ctx)
77 }
78 } else {
79 if falseExecutor != nil {
80 return falseExecutor(ctx)
81 }
82 }
83 return nil
84 }
85}
86
87// NewErrorExecutor creates a new executor that always errors out
88func NewErrorExecutor(err error) Executor {
89 return func(_ context.Context) error {
90 return err
91 }
92}
93
94// NewParallelExecutor creates a new executor from a parallel of other executors
95func NewParallelExecutor(parallel int, executors ...Executor) Executor {
96 return func(ctx context.Context) error {
97 work := make(chan Executor, len(executors))
98 errs := make(chan error, len(executors))
99
100 if 1 > parallel {
101 log.Debugf("Parallel tasks (%d) below minimum, setting to 1", parallel)
102 parallel = 1
103 }
104
105 for i := 0; i < parallel; i++ {
106 go func(work <-chan Executor, errs chan<- error) {
107 for executor := range work {
108 errs <- executor(ctx)
109 }
110 }(work, errs)
111 }
112
113 for i := 0; i < len(executors); i++ {
114 work <- executors[i]
115 }
116 close(work)
117
118 // Executor waits all executors to cleanup these resources.
119 var firstErr error
120 for i := 0; i < len(executors); i++ {
121 err := <-errs
122 if firstErr == nil {
123 firstErr = err
124 }
125 }
126
127 if err := ctx.Err(); err != nil {
128 return err
129 }
130 return firstErr
131 }
132}
133
134func NewFieldExecutor(name string, value interface{}, exec Executor) Executor {
135 return func(ctx context.Context) error {
136 return exec(WithLogger(ctx, Logger(ctx).WithField(name, value)))
137 }
138}
139
140// Then runs another executor if this executor succeeds
141func (e Executor) ThenError(then func(ctx context.Context, err error) error) Executor {
142 return func(ctx context.Context) error {
143 err := e(ctx)
144 if err != nil {
145 switch err.(type) {
146 case Warning:
147 Logger(ctx).Warning(err.Error())
148 default:
149 return then(ctx, err)
150 }
151 }
152 if ctx.Err() != nil {
153 return ctx.Err()
154 }
155 return then(ctx, err)
156 }
157}
158
159// Then runs another executor if this executor succeeds
160func (e Executor) Then(then Executor) Executor {
161 return func(ctx context.Context) error {
162 err := e(ctx)
163 if err != nil {
164 switch err.(type) {
165 case Warning:
166 Logger(ctx).Warning(err.Error())
167 default:
168 return err
169 }
170 }
171 if ctx.Err() != nil {
172 return ctx.Err()
173 }
174 return then(ctx)
175 }
176}
177
178// Then runs another executor if this executor succeeds
179func (e Executor) OnError(then Executor) Executor {
180 return func(ctx context.Context) error {
181 err := e(ctx)
182 if err != nil {
183 switch err.(type) {
184 case Warning:
185 Logger(ctx).Warning(err.Error())
186 default:
187 return errors.Join(err, then(ctx))
188 }
189 }
190 if ctx.Err() != nil {
191 return ctx.Err()
192 }
193 return nil
194 }
195}
196
197// If only runs this executor if conditional is true
198func (e Executor) If(conditional Conditional) Executor {
199 return func(ctx context.Context) error {
200 if conditional(ctx) {
201 return e(ctx)
202 }
203 return nil
204 }
205}
206
207// IfNot only runs this executor if conditional is true
208func (e Executor) IfNot(conditional Conditional) Executor {
209 return func(ctx context.Context) error {
210 if !conditional(ctx) {
211 return e(ctx)
212 }
213 return nil
214 }
215}
216
217// IfBool only runs this executor if conditional is true
218func (e Executor) IfBool(conditional bool) Executor {
219 return e.If(func(_ context.Context) bool {
220 return conditional
221 })
222}
223
224// Finally adds an executor to run after other executor
225func (e Executor) Finally(finally Executor) Executor {
226 return func(ctx context.Context) error {
227 err := e(ctx)
228 err2 := finally(ctx)
229 if err2 != nil {
230 return fmt.Errorf("Error occurred running finally: %v (original error: %v)", err2, err)
231 }
232 return err
233 }
234}
235
236// Not return an inverted conditional
237func (c Conditional) Not() Conditional {
238 return func(ctx context.Context) bool {
239 return !c(ctx)
240 }
241}
Deleted act/pkg/common/file.go
@@ -1,73 +0,0 @@
1package common
2
3import (
4 "fmt"
5 "io"
6 "os"
7)
8
9// CopyFile copy file
10func CopyFile(source string, dest string) (err error) {
11 sourcefile, err := os.Open(source)
12 if err != nil {
13 return err
14 }
15
16 defer sourcefile.Close()
17
18 destfile, err := os.Create(dest)
19 if err != nil {
20 return err
21 }
22
23 defer destfile.Close()
24
25 _, err = io.Copy(destfile, sourcefile)
26 if err == nil {
27 sourceinfo, err := os.Stat(source)
28 if err != nil {
29 _ = os.Chmod(dest, sourceinfo.Mode())
30 }
31 }
32
33 return
34}
35
36// CopyDir recursive copy of directory
37func CopyDir(source string, dest string) (err error) {
38 // get properties of source dir
39 sourceinfo, err := os.Stat(source)
40 if err != nil {
41 return err
42 }
43
44 // create dest dir
45
46 err = os.MkdirAll(dest, sourceinfo.Mode())
47 if err != nil {
48 return err
49 }
50
51 objects, err := os.ReadDir(source)
52
53 for _, obj := range objects {
54 sourcefilepointer := source + "/" + obj.Name()
55
56 destinationfilepointer := dest + "/" + obj.Name()
57
58 if obj.IsDir() {
59 // create sub-directories - recursively
60 err = CopyDir(sourcefilepointer, destinationfilepointer)
61 if err != nil {
62 fmt.Println(err)
63 }
64 } else {
65 // perform copy
66 err = CopyFile(sourcefilepointer, destinationfilepointer)
67 if err != nil {
68 fmt.Println(err)
69 }
70 }
71 }
72 return err
73}
Deleted act/pkg/common/job_error.go
@@ -1,66 +0,0 @@
1package common
2
3import (
4 "context"
5)
6
7type jobErrorContextKey string
8
9const jobErrorContextKeyVal = jobErrorContextKey("job.error")
10
11type jobCancelCtx string
12
13const JobCancelCtxVal = jobCancelCtx("job.cancel")
14
15// JobError returns the job error for current context if any
16func JobError(ctx context.Context) error {
17 val := ctx.Value(jobErrorContextKeyVal)
18 if val != nil {
19 if container, ok := val.(map[string]error); ok {
20 return container["error"]
21 }
22 }
23 return nil
24}
25
26func SetJobError(ctx context.Context, err error) {
27 ctx.Value(jobErrorContextKeyVal).(map[string]error)["error"] = err
28}
29
30// WithJobErrorContainer adds a value to the context as a container for an error
31func WithJobErrorContainer(ctx context.Context) context.Context {
32 container := map[string]error{}
33 return context.WithValue(ctx, jobErrorContextKeyVal, container)
34}
35
36func WithJobCancelContext(ctx context.Context, cancelContext context.Context) context.Context {
37 return context.WithValue(ctx, JobCancelCtxVal, cancelContext)
38}
39
40func JobCancelContext(ctx context.Context) context.Context {
41 val := ctx.Value(JobCancelCtxVal)
42 if val != nil {
43 if container, ok := val.(context.Context); ok {
44 return container
45 }
46 }
47 return nil
48}
49
50// EarlyCancelContext returns a new context based on ctx that is canceled when the first of the provided contexts is canceled.
51func EarlyCancelContext(ctx context.Context) (context.Context, context.CancelFunc) {
52 val := JobCancelContext(ctx)
53 if val != nil {
54 context, cancel := context.WithCancel(ctx)
55 go func() {
56 defer cancel()
57 select {
58 case <-context.Done():
59 case <-ctx.Done():
60 case <-val.Done():
61 }
62 }()
63 return context, cancel
64 }
65 return ctx, func() {}
66}
Deleted act/pkg/common/line_writer.go
@@ -1,50 +0,0 @@
1package common
2
3import (
4 "bytes"
5 "io"
6)
7
8// LineHandler is a callback function for handling a line
9type LineHandler func(line string) bool
10
11type lineWriter struct {
12 buffer bytes.Buffer
13 handlers []LineHandler
14}
15
16// NewLineWriter creates a new instance of a line writer
17func NewLineWriter(handlers ...LineHandler) io.Writer {
18 w := new(lineWriter)
19 w.handlers = handlers
20 return w
21}
22
23func (lw *lineWriter) Write(p []byte) (n int, err error) {
24 pBuf := bytes.NewBuffer(p)
25 written := 0
26 for {
27 line, err := pBuf.ReadString('\n')
28 w, _ := lw.buffer.WriteString(line)
29 written += w
30 if err == nil {
31 lw.handleLine(lw.buffer.String())
32 lw.buffer.Reset()
33 } else if err == io.EOF {
34 break
35 } else {
36 return written, err
37 }
38 }
39
40 return written, nil
41}
42
43func (lw *lineWriter) handleLine(line string) {
44 for _, h := range lw.handlers {
45 ok := h(line)
46 if !ok {
47 break
48 }
49 }
50}
Deleted act/pkg/common/logger.go
@@ -1,27 +0,0 @@
1package common
2
3import (
4 "context"
5
6 "github.com/sirupsen/logrus"
7)
8
9type loggerContextKey string
10
11const loggerContextKeyVal = loggerContextKey("logrus.FieldLogger")
12
13// Logger returns the appropriate logger for current context
14func Logger(ctx context.Context) logrus.FieldLogger {
15 val := ctx.Value(loggerContextKeyVal)
16 if val != nil {
17 if logger, ok := val.(logrus.FieldLogger); ok {
18 return logger
19 }
20 }
21 return logrus.StandardLogger()
22}
23
24// WithLogger adds a value to the context for the logger
25func WithLogger(ctx context.Context, logger logrus.FieldLogger) context.Context {
26 return context.WithValue(ctx, loggerContextKeyVal, logger)
27}
Deleted act/pkg/common/outbound_ip.go
@@ -1,75 +0,0 @@
1package common
2
3import (
4 "net"
5 "sort"
6 "strings"
7)
8
9// GetOutboundIP returns an outbound IP address of this machine.
10// It tries to access the internet and returns the local IP address of the connection.
11// If the machine cannot access the internet, it returns a preferred IP address from network interfaces.
12// It returns nil if no IP address is found.
13func GetOutboundIP() net.IP {
14 // See https://stackoverflow.com/a/37382208
15 conn, err := net.Dial("udp", "8.8.8.8:80")
16 if err == nil {
17 defer conn.Close()
18 return conn.LocalAddr().(*net.UDPAddr).IP
19 }
20
21 // So the machine cannot access the internet. Pick an IP address from network interfaces.
22 if ifs, err := net.Interfaces(); err == nil {
23 type IP struct {
24 net.IP
25 net.Interface
26 }
27 var ips []IP
28 for _, i := range ifs {
29 if addrs, err := i.Addrs(); err == nil {
30 for _, addr := range addrs {
31 var ip net.IP
32 switch v := addr.(type) {
33 case *net.IPNet:
34 ip = v.IP
35 case *net.IPAddr:
36 ip = v.IP
37 }
38 if ip.IsGlobalUnicast() {
39 ips = append(ips, IP{ip, i})
40 }
41 }
42 }
43 }
44 if len(ips) > 1 {
45 sort.Slice(ips, func(i, j int) bool {
46 ifi := ips[i].Interface
47 ifj := ips[j].Interface
48
49 // ethernet is preferred
50 if vi, vj := strings.HasPrefix(ifi.Name, "e"), strings.HasPrefix(ifj.Name, "e"); vi != vj {
51 return vi
52 }
53
54 ipi := ips[i].IP
55 ipj := ips[j].IP
56
57 // IPv4 is preferred
58 if vi, vj := ipi.To4() != nil, ipj.To4() != nil; vi != vj {
59 return vi
60 }
61
62 // en0 is preferred to en1
63 if ifi.Name != ifj.Name {
64 return ifi.Name < ifj.Name
65 }
66
67 // fallback
68 return ipi.String() < ipj.String()
69 })
70 return ips[0].IP
71 }
72 }
73
74 return nil
75}
Deleted act/pkg/container/container_types.go
@@ -1,84 +0,0 @@
1package container
2
3import (
4 "context"
5 "io"
6
7 "github.com/docker/go-connections/nat"
8 "github.com/nektos/act/pkg/common"
9)
10
11// NewContainerInput the input for the New function
12type NewContainerInput struct {
13 Image string
14 Username string
15 Password string
16 Entrypoint []string
17 Cmd []string
18 WorkingDir string
19 Env []string
20 Binds []string
21 Mounts map[string]string
22 Name string
23 Stdout io.Writer
24 Stderr io.Writer
25 NetworkMode string
26 Privileged bool
27 UsernsMode string
28 Platform string
29 Options string
30 NetworkAliases []string
31 ExposedPorts nat.PortSet
32 PortBindings nat.PortMap
33}
34
35// FileEntry is a file to copy to a container
36type FileEntry struct {
37 Name string
38 Mode uint32
39 Body string
40}
41
42// Container for managing docker run containers
43type Container interface {
44 Create(capAdd []string, capDrop []string) common.Executor
45 Copy(destPath string, files ...*FileEntry) common.Executor
46 CopyTarStream(ctx context.Context, destPath string, tarStream io.Reader) error
47 CopyDir(destPath string, srcPath string, useGitIgnore bool) common.Executor
48 GetContainerArchive(ctx context.Context, srcPath string) (io.ReadCloser, error)
49 Pull(forcePull bool) common.Executor
50 Start(attach bool) common.Executor
51 Exec(command []string, env map[string]string, user, workdir string) common.Executor
52 UpdateFromEnv(srcPath string, env *map[string]string) common.Executor
53 UpdateFromImageEnv(env *map[string]string) common.Executor
54 Remove() common.Executor
55 Close() common.Executor
56 ReplaceLogWriter(io.Writer, io.Writer) (io.Writer, io.Writer)
57 GetHealth(ctx context.Context) Health
58}
59
60// NewDockerBuildExecutorInput the input for the NewDockerBuildExecutor function
61type NewDockerBuildExecutorInput struct {
62 ContextDir string
63 Dockerfile string
64 BuildContext io.Reader
65 ImageTag string
66 Platform string
67}
68
69// NewDockerPullExecutorInput the input for the NewDockerPullExecutor function
70type NewDockerPullExecutorInput struct {
71 Image string
72 ForcePull bool
73 Platform string
74 Username string
75 Password string
76}
77
78type Health int
79
80const (
81 HealthStarting Health = iota
82 HealthHealthy
83 HealthUnHealthy
84)
Deleted act/pkg/container/docker_auth.go
@@ -1,61 +0,0 @@
1//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows || netbsd))
2
3package container
4
5import (
6 "context"
7 "strings"
8
9 "github.com/docker/cli/cli/config"
10 "github.com/docker/cli/cli/config/credentials"
11 "github.com/moby/moby/api/types/registry"
12 "github.com/nektos/act/pkg/common"
13)
14
15func LoadDockerAuthConfig(ctx context.Context, image string) (registry.AuthConfig, error) {
16 logger := common.Logger(ctx)
17 config, err := config.Load(config.Dir())
18 if err != nil {
19 logger.Warnf("Could not load docker config: %v", err)
20 return registry.AuthConfig{}, err
21 }
22
23 if !config.ContainsAuth() {
24 config.CredentialsStore = credentials.DetectDefaultStore(config.CredentialsStore)
25 }
26
27 hostName := "index.docker.io"
28 index := strings.IndexRune(image, '/')
29 if index > -1 && (strings.ContainsAny(image[:index], ".:") || image[:index] == "localhost") {
30 hostName = image[:index]
31 }
32
33 authConfig, err := config.GetAuthConfig(hostName)
34 if err != nil {
35 logger.Warnf("Could not get auth config from docker config: %v", err)
36 return registry.AuthConfig{}, err
37 }
38
39 return registry.AuthConfig(authConfig), nil
40}
41
42func LoadDockerAuthConfigs(ctx context.Context) map[string]registry.AuthConfig {
43 logger := common.Logger(ctx)
44 config, err := config.Load(config.Dir())
45 if err != nil {
46 logger.Warnf("Could not load docker config: %v", err)
47 return nil
48 }
49
50 if !config.ContainsAuth() {
51 config.CredentialsStore = credentials.DetectDefaultStore(config.CredentialsStore)
52 }
53
54 creds, _ := config.GetAllCredentials()
55 authConfigs := make(map[string]registry.AuthConfig, len(creds))
56 for k, v := range creds {
57 authConfigs[k] = registry.AuthConfig(v)
58 }
59
60 return authConfigs
61}
Deleted act/pkg/container/docker_build.go
@@ -1,123 +0,0 @@
1//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows || netbsd))
2
3package container
4
5import (
6 "context"
7 "io"
8 "os"
9 "path/filepath"
10 "strings"
11
12 "github.com/moby/go-archive"
13 "github.com/moby/moby/client"
14 specs "github.com/opencontainers/image-spec/specs-go/v1"
15
16 "github.com/moby/patternmatcher"
17 "github.com/moby/patternmatcher/ignorefile"
18
19 "github.com/nektos/act/pkg/common"
20)
21
22// NewDockerBuildExecutor function to create a run executor for the container
23func NewDockerBuildExecutor(input NewDockerBuildExecutorInput) common.Executor {
24 return func(ctx context.Context) error {
25 logger := common.Logger(ctx)
26 if input.Platform != "" {
27 logger.Infof("%sdocker build -t %s --platform %s %s", logPrefix, input.ImageTag, input.Platform, input.ContextDir)
28 } else {
29 logger.Infof("%sdocker build -t %s %s", logPrefix, input.ImageTag, input.ContextDir)
30 }
31 if common.Dryrun(ctx) {
32 return nil
33 }
34
35 cli, err := GetDockerClient(ctx)
36 if err != nil {
37 return err
38 }
39 defer cli.Close()
40
41 logger.Debugf("Building image from '%v'", input.ContextDir)
42
43 tags := []string{input.ImageTag}
44 options := client.ImageBuildOptions{
45 Tags: tags,
46 Remove: true,
47 AuthConfigs: LoadDockerAuthConfigs(ctx),
48 Dockerfile: input.Dockerfile,
49 }
50 if input.Platform != "" {
51 parts := strings.SplitN(input.Platform, "/", 2)
52 if len(parts) == 2 {
53 options.Platforms = []specs.Platform{{OS: parts[0], Architecture: parts[1]}}
54 }
55 }
56 var buildContext io.ReadCloser
57 if input.BuildContext != nil {
58 buildContext = io.NopCloser(input.BuildContext)
59 } else {
60 buildContext, err = createBuildContext(ctx, input.ContextDir, input.Dockerfile)
61 }
62 if err != nil {
63 return err
64 }
65
66 defer buildContext.Close()
67
68 logger.Debugf("Creating image from context dir '%s' with tag '%s' and platform '%s'", input.ContextDir, input.ImageTag, input.Platform)
69 resp, err := cli.ImageBuild(ctx, buildContext, options)
70
71 err = logDockerResponse(logger, resp.Body, err != nil)
72 if err != nil {
73 return err
74 }
75 return nil
76 }
77}
78func createBuildContext(ctx context.Context, contextDir string, relDockerfile string) (io.ReadCloser, error) {
79 common.Logger(ctx).Debugf("Creating archive for build context dir '%s' with relative dockerfile '%s'", contextDir, relDockerfile)
80
81 // And canonicalize dockerfile name to a platform-independent one
82 relDockerfile = filepath.ToSlash(relDockerfile)
83
84 f, err := os.Open(filepath.Join(contextDir, ".dockerignore"))
85 if err != nil && !os.IsNotExist(err) {
86 return nil, err
87 }
88 defer f.Close()
89
90 var excludes []string
91 if err == nil {
92 excludes, err = ignorefile.ReadAll(f)
93 if err != nil {
94 return nil, err
95 }
96 }
97
98 // If .dockerignore mentions .dockerignore or the Dockerfile
99 // then make sure we send both files over to the daemon
100 // because Dockerfile is, obviously, needed no matter what, and
101 // .dockerignore is needed to know if either one needs to be
102 // removed. The daemon will remove them for us, if needed, after it
103 // parses the Dockerfile. Ignore errors here, as they will have been
104 // caught by validateContextDirectory above.
105 var includes = []string{"."}
106 keepThem1, _ := patternmatcher.Matches(".dockerignore", excludes)
107 keepThem2, _ := patternmatcher.Matches(relDockerfile, excludes)
108 if keepThem1 || keepThem2 {
109 includes = append(includes, ".dockerignore", relDockerfile)
110 }
111
112 compression := archive.Uncompressed
113 buildCtx, err := archive.TarWithOptions(contextDir, &archive.TarOptions{
114 Compression: compression,
115 ExcludePatterns: excludes,
116 IncludeFiles: includes,
117 })
118 if err != nil {
119 return nil, err
120 }
121
122 return buildCtx, nil
123}
Deleted act/pkg/container/docker_cli.go
@@ -1,1162 +0,0 @@
1//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows || netbsd))
2
3// This file is adapted from https://github.com/docker/cli/blob/v29.3.0/cli/command/container/opts.go
4// with import paths adjusted for the act codebase.
5//
6// docker/cli is licensed under the Apache License, Version 2.0.
7// See DOCKER_LICENSE for the full license text.
8//
9
10//nolint:unparam,errcheck
11package container
12
13import (
14 "bytes"
15 "encoding/json"
16 "errors"
17 "fmt"
18 "net"
19 "net/netip"
20 "os"
21 "path"
22 "path/filepath"
23 "reflect"
24 "regexp"
25 "slices"
26 "strings"
27 "time"
28
29 "github.com/docker/cli/cli/compose/loader"
30 "github.com/docker/cli/opts"
31 "github.com/docker/go-connections/nat"
32 "github.com/moby/moby/api/types/container"
33 "github.com/moby/moby/api/types/mount"
34 "github.com/moby/moby/api/types/network"
35 "github.com/spf13/pflag"
36 cdi "tags.cncf.io/container-device-interface/pkg/parser"
37)
38
39const (
40 // TODO(thaJeztah): define these in the API-types, or query available defaults
41 // from the daemon, or require "local" profiles to be an absolute path or
42 // relative paths starting with "./". The daemon-config has consts for this
43 // but we don't want to import that package:
44 // https://github.com/moby/moby/blob/v23.0.0/daemon/config/config.go#L63-L67
45
46 // seccompProfileDefault is the built-in default seccomp profile.
47 seccompProfileDefault = "builtin"
48 // seccompProfileUnconfined is a special profile name for seccomp to use an
49 // "unconfined" seccomp profile.
50 seccompProfileUnconfined = "unconfined"
51)
52
53var deviceCgroupRuleRegexp = regexp.MustCompile(`^[acb] ([0-9]+|\*):([0-9]+|\*) [rwm]{1,3}$`)
54
55// containerOptions is a data object with all the options for creating a container
56type containerOptions struct {
57 attach opts.ListOpts
58 volumes opts.ListOpts
59 tmpfs opts.ListOpts
60 mounts opts.MountOpt
61 blkioWeightDevice opts.WeightdeviceOpt
62 deviceReadBps opts.ThrottledeviceOpt
63 deviceWriteBps opts.ThrottledeviceOpt
64 links opts.ListOpts
65 aliases opts.ListOpts
66 linkLocalIPs opts.ListOpts // TODO(thaJeztah): we need a flag-type to handle []netip.Addr directly
67 deviceReadIOps opts.ThrottledeviceOpt
68 deviceWriteIOps opts.ThrottledeviceOpt
69 env opts.ListOpts
70 labels opts.ListOpts
71 deviceCgroupRules opts.ListOpts
72 devices opts.ListOpts
73 gpus opts.GpuOpts
74 ulimits *opts.UlimitOpt
75 sysctls *opts.MapOpts
76 publish opts.ListOpts
77 expose opts.ListOpts
78 dns opts.ListOpts // TODO(thaJeztah): we need a flag-type to handle []netip.Addr directly
79 dnsSearch opts.ListOpts
80 dnsOptions opts.ListOpts
81 extraHosts opts.ListOpts
82 volumesFrom opts.ListOpts
83 envFile opts.ListOpts
84 capAdd opts.ListOpts
85 capDrop opts.ListOpts
86 groupAdd opts.ListOpts
87 securityOpt opts.ListOpts
88 storageOpt opts.ListOpts
89 labelsFile opts.ListOpts
90 loggingOpts opts.ListOpts
91 privileged bool
92 pidMode string
93 utsMode string
94 usernsMode string
95 cgroupnsMode string
96 publishAll bool
97 stdin bool
98 tty bool
99 oomKillDisable bool
100 oomScoreAdj int
101 containerIDFile string
102 entrypoint string
103 hostname string
104 domainname string
105 memory opts.MemBytes
106 memoryReservation opts.MemBytes
107 memorySwap opts.MemSwapBytes
108 user string
109 workingDir string
110 cpuCount int64
111 cpuShares int64
112 cpuPercent int64
113 cpuPeriod int64
114 cpuRealtimePeriod int64
115 cpuRealtimeRuntime int64
116 cpuQuota int64
117 cpus opts.NanoCPUs
118 cpusetCpus string
119 cpusetMems string
120 blkioWeight uint16
121 ioMaxBandwidth opts.MemBytes
122 ioMaxIOps uint64
123 swappiness int64
124 netMode opts.NetworkOpt
125 macAddress string
126 ipv4Address net.IP // TODO(thaJeztah): we need a flag-type to handle netip.Addr directly
127 ipv6Address net.IP // TODO(thaJeztah): we need a flag-type to handle netip.Addr directly
128 ipcMode string
129 pidsLimit int64
130 restartPolicy string
131 readonlyRootfs bool
132 loggingDriver string
133 cgroupParent string
134 volumeDriver string
135 stopSignal string
136 stopTimeout int
137 isolation string
138 shmSize opts.MemBytes
139 noHealthcheck bool
140 healthCmd string
141 healthInterval time.Duration
142 healthTimeout time.Duration
143 healthStartPeriod time.Duration
144 healthStartInterval time.Duration
145 healthRetries int
146 runtime string
147 autoRemove bool
148 init bool
149 annotations *opts.MapOpts
150
151 Image string
152 Args []string
153}
154
155// addFlags adds all command line flags that will be used by parse to the FlagSet
156func addFlags(flags *pflag.FlagSet) *containerOptions {
157 copts := &containerOptions{
158 aliases: opts.NewListOpts(nil),
159 attach: opts.NewListOpts(validateAttach),
160 blkioWeightDevice: opts.NewWeightdeviceOpt(opts.ValidateWeightDevice),
161 capAdd: opts.NewListOpts(nil),
162 capDrop: opts.NewListOpts(nil),
163 dns: opts.NewListOpts(opts.ValidateIPAddress),
164 dnsOptions: opts.NewListOpts(nil),
165 dnsSearch: opts.NewListOpts(opts.ValidateDNSSearch),
166 deviceCgroupRules: opts.NewListOpts(validateDeviceCgroupRule),
167 deviceReadBps: opts.NewThrottledeviceOpt(opts.ValidateThrottleBpsDevice),
168 deviceReadIOps: opts.NewThrottledeviceOpt(opts.ValidateThrottleIOpsDevice),
169 deviceWriteBps: opts.NewThrottledeviceOpt(opts.ValidateThrottleBpsDevice),
170 deviceWriteIOps: opts.NewThrottledeviceOpt(opts.ValidateThrottleIOpsDevice),
171 devices: opts.NewListOpts(nil), // devices can only be validated after we know the server OS
172 env: opts.NewListOpts(opts.ValidateEnv),
173 envFile: opts.NewListOpts(nil),
174 expose: opts.NewListOpts(nil),
175 extraHosts: opts.NewListOpts(opts.ValidateExtraHost),
176 groupAdd: opts.NewListOpts(nil),
177 labels: opts.NewListOpts(opts.ValidateLabel),
178 labelsFile: opts.NewListOpts(nil),
179 linkLocalIPs: opts.NewListOpts(nil),
180 links: opts.NewListOpts(opts.ValidateLink),
181 loggingOpts: opts.NewListOpts(nil),
182 publish: opts.NewListOpts(nil),
183 securityOpt: opts.NewListOpts(nil),
184 storageOpt: opts.NewListOpts(nil),
185 sysctls: opts.NewMapOpts(nil, opts.ValidateSysctl),
186 tmpfs: opts.NewListOpts(nil),
187 ulimits: opts.NewUlimitOpt(nil),
188 volumes: opts.NewListOpts(nil),
189 volumesFrom: opts.NewListOpts(nil),
190 annotations: opts.NewMapOpts(nil, nil),
191 }
192
193 // General purpose flags
194 flags.VarP(&copts.attach, "attach", "a", "Attach to STDIN, STDOUT or STDERR")
195 flags.Var(&copts.deviceCgroupRules, "device-cgroup-rule", "Add a rule to the cgroup allowed devices list")
196 flags.Var(&copts.devices, "device", "Add a host device to the container")
197 flags.Var(&copts.gpus, "gpus", "GPU devices to add to the container ('all' to pass all GPUs)")
198 flags.SetAnnotation("gpus", "version", []string{"1.40"})
199 flags.VarP(&copts.env, "env", "e", "Set environment variables")
200 flags.Var(&copts.envFile, "env-file", "Read in a file of environment variables")
201 flags.StringVar(&copts.entrypoint, "entrypoint", "", "Overwrite the default ENTRYPOINT of the image")
202 flags.Var(&copts.groupAdd, "group-add", "Add additional groups to join")
203 flags.StringVarP(&copts.hostname, "hostname", "h", "", "Container host name")
204 flags.StringVar(&copts.domainname, "domainname", "", "Container NIS domain name")
205 flags.BoolVarP(&copts.stdin, "interactive", "i", false, "Keep STDIN open even if not attached")
206 flags.VarP(&copts.labels, "label", "l", "Set meta data on a container")
207 flags.Var(&copts.labelsFile, "label-file", "Read in a line delimited file of labels")
208 flags.BoolVar(&copts.readonlyRootfs, "read-only", false, "Mount the container's root filesystem as read only")
209 flags.StringVar(&copts.restartPolicy, "restart", string(container.RestartPolicyDisabled), "Restart policy to apply when a container exits")
210 flags.StringVar(&copts.stopSignal, "stop-signal", "", "Signal to stop the container")
211 flags.IntVar(&copts.stopTimeout, "stop-timeout", 0, "Timeout (in seconds) to stop a container")
212 flags.SetAnnotation("stop-timeout", "version", []string{"1.25"})
213 flags.Var(copts.sysctls, "sysctl", "Sysctl options")
214 flags.BoolVarP(&copts.tty, "tty", "t", false, "Allocate a pseudo-TTY")
215 flags.Var(copts.ulimits, "ulimit", "Ulimit options")
216 flags.StringVarP(&copts.user, "user", "u", "", "Username or UID (format: <name|uid>[:<group|gid>])")
217 flags.StringVarP(&copts.workingDir, "workdir", "w", "", "Working directory inside the container")
218 flags.BoolVar(&copts.autoRemove, "rm", false, "Automatically remove the container and its associated anonymous volumes when it exits")
219
220 // Security
221 flags.Var(&copts.capAdd, "cap-add", "Add Linux capabilities")
222 flags.Var(&copts.capDrop, "cap-drop", "Drop Linux capabilities")
223 flags.BoolVar(&copts.privileged, "privileged", false, "Give extended privileges to this container")
224 flags.Var(&copts.securityOpt, "security-opt", "Security Options")
225 flags.StringVar(&copts.usernsMode, "userns", "", "User namespace to use")
226 flags.StringVar(&copts.cgroupnsMode, "cgroupns", "", `Cgroup namespace to use (host|private)
227'host': Run the container in the Docker host's cgroup namespace
228'private': Run the container in its own private cgroup namespace
229'': Use the cgroup namespace as configured by the
230 default-cgroupns-mode option on the daemon (default)`)
231 flags.SetAnnotation("cgroupns", "version", []string{"1.41"})
232
233 // Network and port publishing flag
234 flags.Var(&copts.extraHosts, "add-host", "Add a custom host-to-IP mapping (host:ip)")
235 flags.Var(&copts.dns, "dns", "Set custom DNS servers")
236 // We allow for both "--dns-opt" and "--dns-option", although the latter is the recommended way.
237 // This is to be consistent with service create/update
238 flags.Var(&copts.dnsOptions, "dns-opt", "Set DNS options")
239 flags.Var(&copts.dnsOptions, "dns-option", "Set DNS options")
240 flags.MarkHidden("dns-opt")
241 flags.Var(&copts.dnsSearch, "dns-search", "Set custom DNS search domains")
242 flags.Var(&copts.expose, "expose", "Expose a port or a range of ports")
243 flags.IPVar(&copts.ipv4Address, "ip", nil, "IPv4 address (e.g., 172.30.100.104)")
244 flags.IPVar(&copts.ipv6Address, "ip6", nil, "IPv6 address (e.g., 2001:db8::33)")
245 flags.Var(&copts.links, "link", "Add link to another container")
246 flags.Var(&copts.linkLocalIPs, "link-local-ip", "Container IPv4/IPv6 link-local addresses")
247 flags.StringVar(&copts.macAddress, "mac-address", "", "Container MAC address (e.g., 92:d0:c6:0a:29:33)")
248 flags.VarP(&copts.publish, "publish", "p", "Publish a container's port(s) to the host")
249 flags.BoolVarP(&copts.publishAll, "publish-all", "P", false, "Publish all exposed ports to random ports")
250 // We allow for both "--net" and "--network", although the latter is the recommended way.
251 flags.Var(&copts.netMode, "net", "Connect a container to a network")
252 flags.Var(&copts.netMode, "network", "Connect a container to a network")
253 flags.MarkHidden("net")
254 // We allow for both "--net-alias" and "--network-alias", although the latter is the recommended way.
255 flags.Var(&copts.aliases, "net-alias", "Add network-scoped alias for the container")
256 flags.Var(&copts.aliases, "network-alias", "Add network-scoped alias for the container")
257 flags.MarkHidden("net-alias")
258
259 // Logging and storage
260 flags.StringVar(&copts.loggingDriver, "log-driver", "", "Logging driver for the container")
261 flags.StringVar(&copts.volumeDriver, "volume-driver", "", "Optional volume driver for the container")
262 flags.Var(&copts.loggingOpts, "log-opt", "Log driver options")
263 flags.Var(&copts.storageOpt, "storage-opt", "Storage driver options for the container")
264 flags.Var(&copts.tmpfs, "tmpfs", "Mount a tmpfs directory")
265 flags.Var(&copts.volumesFrom, "volumes-from", "Mount volumes from the specified container(s)")
266 flags.VarP(&copts.volumes, "volume", "v", "Bind mount a volume")
267 flags.Var(&copts.mounts, "mount", "Attach a filesystem mount to the container")
268
269 // Health-checking
270 flags.StringVar(&copts.healthCmd, "health-cmd", "", "Command to run to check health")
271 flags.DurationVar(&copts.healthInterval, "health-interval", 0, "Time between running the check (ms|s|m|h) (default 0s)")
272 flags.IntVar(&copts.healthRetries, "health-retries", 0, "Consecutive failures needed to report unhealthy")
273 flags.DurationVar(&copts.healthTimeout, "health-timeout", 0, "Maximum time to allow one check to run (ms|s|m|h) (default 0s)")
274 flags.DurationVar(&copts.healthStartPeriod, "health-start-period", 0, "Start period for the container to initialize before starting health-retries countdown (ms|s|m|h) (default 0s)")
275 flags.SetAnnotation("health-start-period", "version", []string{"1.29"})
276 flags.DurationVar(&copts.healthStartInterval, "health-start-interval", 0, "Time between running the check during the start period (ms|s|m|h) (default 0s)")
277 flags.SetAnnotation("health-start-interval", "version", []string{"1.44"})
278 flags.BoolVar(&copts.noHealthcheck, "no-healthcheck", false, "Disable any container-specified HEALTHCHECK")
279
280 // Resource management
281 flags.Uint16Var(&copts.blkioWeight, "blkio-weight", 0, "Block IO (relative weight), between 10 and 1000, or 0 to disable (default 0)")
282 flags.Var(&copts.blkioWeightDevice, "blkio-weight-device", "Block IO weight (relative device weight)")
283 flags.StringVar(&copts.containerIDFile, "cidfile", "", "Write the container ID to the file")
284 flags.StringVar(&copts.cpusetCpus, "cpuset-cpus", "", "CPUs in which to allow execution (0-3, 0,1)")
285 flags.StringVar(&copts.cpusetMems, "cpuset-mems", "", "MEMs in which to allow execution (0-3, 0,1)")
286 flags.Int64Var(&copts.cpuCount, "cpu-count", 0, "CPU count (Windows only)")
287 flags.SetAnnotation("cpu-count", "ostype", []string{"windows"})
288 flags.Int64Var(&copts.cpuPercent, "cpu-percent", 0, "CPU percent (Windows only)")
289 flags.SetAnnotation("cpu-percent", "ostype", []string{"windows"})
290 flags.Int64Var(&copts.cpuPeriod, "cpu-period", 0, "Limit CPU CFS (Completely Fair Scheduler) period")
291 flags.Int64Var(&copts.cpuQuota, "cpu-quota", 0, "Limit CPU CFS (Completely Fair Scheduler) quota")
292 flags.Int64Var(&copts.cpuRealtimePeriod, "cpu-rt-period", 0, "Limit CPU real-time period in microseconds")
293 flags.SetAnnotation("cpu-rt-period", "version", []string{"1.25"})
294 flags.Int64Var(&copts.cpuRealtimeRuntime, "cpu-rt-runtime", 0, "Limit CPU real-time runtime in microseconds")
295 flags.SetAnnotation("cpu-rt-runtime", "version", []string{"1.25"})
296 flags.Int64VarP(&copts.cpuShares, "cpu-shares", "c", 0, "CPU shares (relative weight)")
297 flags.Var(&copts.cpus, "cpus", "Number of CPUs")
298 flags.SetAnnotation("cpus", "version", []string{"1.25"})
299 flags.Var(&copts.deviceReadBps, "device-read-bps", "Limit read rate (bytes per second) from a device")
300 flags.Var(&copts.deviceReadIOps, "device-read-iops", "Limit read rate (IO per second) from a device")
301 flags.Var(&copts.deviceWriteBps, "device-write-bps", "Limit write rate (bytes per second) to a device")
302 flags.Var(&copts.deviceWriteIOps, "device-write-iops", "Limit write rate (IO per second) to a device")
303 flags.Var(&copts.ioMaxBandwidth, "io-maxbandwidth", "Maximum IO bandwidth limit for the system drive (Windows only)")
304 flags.SetAnnotation("io-maxbandwidth", "ostype", []string{"windows"})
305 flags.Uint64Var(&copts.ioMaxIOps, "io-maxiops", 0, "Maximum IOps limit for the system drive (Windows only)")
306 flags.SetAnnotation("io-maxiops", "ostype", []string{"windows"})
307 flags.VarP(&copts.memory, "memory", "m", "Memory limit")
308 flags.Var(&copts.memoryReservation, "memory-reservation", "Memory soft limit")
309 flags.Var(&copts.memorySwap, "memory-swap", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap")
310 flags.Int64Var(&copts.swappiness, "memory-swappiness", -1, "Tune container memory swappiness (0 to 100)")
311 flags.BoolVar(&copts.oomKillDisable, "oom-kill-disable", false, "Disable OOM Killer")
312 flags.IntVar(&copts.oomScoreAdj, "oom-score-adj", 0, "Tune host's OOM preferences (-1000 to 1000)")
313 flags.Int64Var(&copts.pidsLimit, "pids-limit", 0, "Tune container pids limit (set -1 for unlimited)")
314
315 // Low-level execution (cgroups, namespaces, ...)
316 flags.StringVar(&copts.cgroupParent, "cgroup-parent", "", "Optional parent cgroup for the container")
317 flags.StringVar(&copts.ipcMode, "ipc", "", "IPC mode to use")
318 flags.StringVar(&copts.isolation, "isolation", "", "Container isolation technology")
319 flags.StringVar(&copts.pidMode, "pid", "", "PID namespace to use")
320 flags.Var(&copts.shmSize, "shm-size", "Size of /dev/shm")
321 flags.StringVar(&copts.utsMode, "uts", "", "UTS namespace to use")
322 flags.StringVar(&copts.runtime, "runtime", "", "Runtime to use for this container")
323
324 flags.BoolVar(&copts.init, "init", false, "Run an init inside the container that forwards signals and reaps processes")
325 flags.SetAnnotation("init", "version", []string{"1.25"})
326
327 flags.Var(copts.annotations, "annotation", "Add an annotation to the container (passed through to the OCI runtime)")
328 flags.SetAnnotation("annotation", "version", []string{"1.43"})
329
330 // TODO(thaJeztah): remove in next release (v30.0, or v29.x)
331 var stub opts.MemBytes
332 flags.Var(&stub, "kernel-memory", "Kernel memory limit (deprecated)")
333 _ = flags.MarkDeprecated("kernel-memory", "and no longer supported by the kernel")
334
335 return copts
336}
337
338type containerConfig struct {
339 Config *container.Config
340 HostConfig *container.HostConfig
341 NetworkingConfig *network.NetworkingConfig
342}
343
344// parse parses the args for the specified command and generates a Config,
345// a HostConfig and returns them with the specified command.
346// If the specified args are not valid, it will return an error.
347//
348//nolint:gocyclo
349func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*containerConfig, error) {
350 var (
351 attachStdin = copts.attach.Get("stdin")
352 attachStdout = copts.attach.Get("stdout")
353 attachStderr = copts.attach.Get("stderr")
354 )
355
356 // Validate the input mac address
357 if copts.macAddress != "" {
358 if _, err := net.ParseMAC(strings.TrimSpace(copts.macAddress)); err != nil {
359 return nil, fmt.Errorf("%s is not a valid mac address", copts.macAddress)
360 }
361 }
362 if copts.stdin {
363 attachStdin = true
364 }
365 // If -a is not set, attach to stdout and stderr
366 if copts.attach.Len() == 0 {
367 attachStdout = true
368 attachStderr = true
369 }
370
371 var err error
372
373 swappiness := copts.swappiness
374 if swappiness != -1 && (swappiness < 0 || swappiness > 100) {
375 return nil, fmt.Errorf("invalid value: %d. Valid memory swappiness range is 0-100", swappiness)
376 }
377
378 var binds []string
379 volumes := copts.volumes.GetMap()
380 // add any bind targets to the list of container volumes
381 for bind := range copts.volumes.GetMap() {
382 parsed, err := loader.ParseVolume(bind)
383 if err != nil {
384 return nil, err
385 }
386
387 if parsed.Source != "" {
388 toBind := bind
389
390 if parsed.Type == string(mount.TypeBind) {
391 if hostPart, targetPath, ok := strings.Cut(bind, ":"); ok {
392 if !filepath.IsAbs(hostPart) && strings.HasPrefix(hostPart, ".") {
393 if absHostPart, err := filepath.Abs(hostPart); err == nil {
394 hostPart = absHostPart
395 }
396 }
397 toBind = hostPart + ":" + targetPath
398 }
399 }
400
401 // after creating the bind mount we want to delete it from the copts.volumes values because
402 // we do not want bind mounts being committed to image configs
403 binds = append(binds, toBind)
404 // We should delete from the map (`volumes`) here, as deleting from copts.volumes will not work if
405 // there are duplicates entries.
406 delete(volumes, bind)
407 }
408 }
409
410 // Can't evaluate options passed into --tmpfs until we actually mount
411 tmpfs := make(map[string]string)
412 for _, t := range copts.tmpfs.GetSlice() {
413 k, v, _ := strings.Cut(t, ":")
414 tmpfs[k] = v
415 }
416
417 var runCmd, entrypoint []string
418
419 if len(copts.Args) > 0 {
420 runCmd = copts.Args
421 }
422
423 if copts.entrypoint != "" {
424 entrypoint = []string{copts.entrypoint}
425 } else if flags.Changed("entrypoint") {
426 // if `--entrypoint=` is parsed then Entrypoint is reset
427 entrypoint = []string{""}
428 }
429
430 // TODO(thaJeztah): remove uses of go-connections/nat here.
431 convertedOpts, err := convertToStandardNotation(copts.publish.GetSlice())
432 if err != nil {
433 return nil, err
434 }
435
436 // short syntax ([ip:]public:private[/proto])
437 //
438 // TODO(thaJeztah): we need an equivalent that handles the "ip-address" part without depending on the nat package.
439 ports, natPortBindings, err := nat.ParsePortSpecs(convertedOpts)
440 if err != nil {
441 return nil, err
442 }
443 portBindings := network.PortMap{}
444 for port, bindings := range natPortBindings {
445 p, err := network.ParsePort(string(port))
446 if err != nil {
447 return nil, err
448 }
449 portBindings[p] = []network.PortBinding{}
450 for _, b := range bindings {
451 var hostIP netip.Addr
452 if b.HostIP != "" {
453 hostIP, err = netip.ParseAddr(b.HostIP)
454 if err != nil {
455 return nil, err
456 }
457 }
458 portBindings[p] = append(portBindings[p], network.PortBinding{
459 HostIP: hostIP,
460 HostPort: b.HostPort,
461 })
462 }
463 }
464
465 // Add published ports as exposed ports.
466 exposedPorts := network.PortSet{}
467 for port := range ports {
468 p, err := network.ParsePort(string(port))
469 if err != nil {
470 return nil, err
471 }
472 exposedPorts[p] = struct{}{}
473 }
474
475 // Merge in exposed ports to the map of published ports
476 for _, e := range copts.expose.GetSlice() {
477 // support two formats for expose, original format <portnum>/[<proto>]
478 // or <startport-endport>/[<proto>]
479 pr, err := network.ParsePortRange(e)
480 if err != nil {
481 return nil, fmt.Errorf("invalid range format for --expose: %w", err)
482 }
483 // parse the start and end port and create a sequence of ports to expose
484 // if expose a port, the start and end port are the same
485 for p := range pr.All() {
486 exposedPorts[p] = struct{}{}
487 }
488 }
489
490 // validate and parse device mappings. Note we do late validation of the
491 // device path (as opposed to during flag parsing), as at the time we are
492 // parsing flags, we haven't yet sent a _ping to the daemon to determine
493 // what operating system it is.
494 devices := copts.devices.GetSlice()
495 deviceMappings := make([]container.DeviceMapping, 0, len(devices))
496 cdiDeviceNames := make([]string, 0, len(devices))
497 for _, device := range devices {
498 if cdi.IsQualifiedName(device) {
499 cdiDeviceNames = append(cdiDeviceNames, device)
500 continue
501 }
502 validated, err := validateDevice(device, serverOS)
503 if err != nil {
504 return nil, err
505 }
506 deviceMapping, err := parseDevice(validated, serverOS)
507 if err != nil {
508 return nil, err
509 }
510 deviceMappings = append(deviceMappings, deviceMapping)
511 }
512
513 // collect all the environment variables for the container
514 envVariables, err := opts.ReadKVEnvStrings(copts.envFile.GetSlice(), copts.env.GetSlice())
515 if err != nil {
516 return nil, err
517 }
518
519 // collect all the labels for the container
520 labels, err := opts.ReadKVStrings(copts.labelsFile.GetSlice(), copts.labels.GetSlice())
521 if err != nil {
522 return nil, err
523 }
524
525 pidMode := container.PidMode(copts.pidMode)
526 if !pidMode.Valid() {
527 return nil, errors.New("--pid: invalid PID mode")
528 }
529
530 utsMode := container.UTSMode(copts.utsMode)
531 if !utsMode.Valid() {
532 return nil, errors.New("--uts: invalid UTS mode")
533 }
534
535 usernsMode := container.UsernsMode(copts.usernsMode)
536 if !usernsMode.Valid() {
537 return nil, errors.New("--userns: invalid USER mode")
538 }
539
540 cgroupnsMode := container.CgroupnsMode(copts.cgroupnsMode)
541 if !cgroupnsMode.Valid() {
542 return nil, errors.New("--cgroupns: invalid CGROUP mode")
543 }
544
545 restartPolicy, err := opts.ParseRestartPolicy(copts.restartPolicy)
546 if err != nil {
547 return nil, err
548 }
549
550 loggingOpts, err := parseLoggingOpts(copts.loggingDriver, copts.loggingOpts.GetSlice())
551 if err != nil {
552 return nil, err
553 }
554
555 securityOpts, err := parseSecurityOpts(copts.securityOpt.GetSlice())
556 if err != nil {
557 return nil, err
558 }
559
560 securityOpts, maskedPaths, readonlyPaths := parseSystemPaths(securityOpts)
561
562 storageOpts, err := parseStorageOpts(copts.storageOpt.GetSlice())
563 if err != nil {
564 return nil, err
565 }
566
567 // Healthcheck
568 var healthConfig *container.HealthConfig
569 haveHealthSettings := copts.healthCmd != "" ||
570 copts.healthInterval != 0 ||
571 copts.healthTimeout != 0 ||
572 copts.healthStartPeriod != 0 ||
573 copts.healthRetries != 0 ||
574 copts.healthStartInterval != 0
575 if copts.noHealthcheck {
576 if haveHealthSettings {
577 return nil, errors.New("--no-healthcheck conflicts with --health-* options")
578 }
579 healthConfig = &container.HealthConfig{Test: []string{"NONE"}}
580 } else if haveHealthSettings {
581 var probe []string
582 if copts.healthCmd != "" {
583 probe = []string{"CMD-SHELL", copts.healthCmd}
584 }
585 if copts.healthInterval < 0 {
586 return nil, errors.New("--health-interval cannot be negative")
587 }
588 if copts.healthTimeout < 0 {
589 return nil, errors.New("--health-timeout cannot be negative")
590 }
591 if copts.healthRetries < 0 {
592 return nil, errors.New("--health-retries cannot be negative")
593 }
594 if copts.healthStartPeriod < 0 {
595 return nil, errors.New("--health-start-period cannot be negative")
596 }
597 if copts.healthStartInterval < 0 {
598 return nil, errors.New("--health-start-interval cannot be negative")
599 }
600
601 healthConfig = &container.HealthConfig{
602 Test: probe,
603 Interval: copts.healthInterval,
604 Timeout: copts.healthTimeout,
605 StartPeriod: copts.healthStartPeriod,
606 StartInterval: copts.healthStartInterval,
607 Retries: copts.healthRetries,
608 }
609 }
610
611 deviceRequests := copts.gpus.Value()
612 if len(cdiDeviceNames) > 0 {
613 cdiDeviceRequest := container.DeviceRequest{
614 Driver: "cdi",
615 DeviceIDs: cdiDeviceNames,
616 }
617 deviceRequests = append(deviceRequests, cdiDeviceRequest)
618 }
619
620 resources := container.Resources{
621 CgroupParent: copts.cgroupParent,
622 Memory: copts.memory.Value(),
623 MemoryReservation: copts.memoryReservation.Value(),
624 MemorySwap: copts.memorySwap.Value(),
625 MemorySwappiness: &copts.swappiness,
626 OomKillDisable: &copts.oomKillDisable,
627 NanoCPUs: copts.cpus.Value(),
628 CPUCount: copts.cpuCount,
629 CPUPercent: copts.cpuPercent,
630 CPUShares: copts.cpuShares,
631 CPUPeriod: copts.cpuPeriod,
632 CpusetCpus: copts.cpusetCpus,
633 CpusetMems: copts.cpusetMems,
634 CPUQuota: copts.cpuQuota,
635 CPURealtimePeriod: copts.cpuRealtimePeriod,
636 CPURealtimeRuntime: copts.cpuRealtimeRuntime,
637 PidsLimit: &copts.pidsLimit,
638 BlkioWeight: copts.blkioWeight,
639 BlkioWeightDevice: copts.blkioWeightDevice.GetList(),
640 BlkioDeviceReadBps: copts.deviceReadBps.GetList(),
641 BlkioDeviceWriteBps: copts.deviceWriteBps.GetList(),
642 BlkioDeviceReadIOps: copts.deviceReadIOps.GetList(),
643 BlkioDeviceWriteIOps: copts.deviceWriteIOps.GetList(),
644 IOMaximumIOps: copts.ioMaxIOps,
645 IOMaximumBandwidth: uint64(copts.ioMaxBandwidth),
646 Ulimits: copts.ulimits.GetList(),
647 DeviceCgroupRules: copts.deviceCgroupRules.GetSlice(),
648 Devices: deviceMappings,
649 DeviceRequests: deviceRequests,
650 }
651
652 config := &container.Config{
653 Hostname: copts.hostname,
654 Domainname: copts.domainname,
655 ExposedPorts: exposedPorts,
656 User: copts.user,
657 Tty: copts.tty,
658 OpenStdin: copts.stdin,
659 AttachStdin: attachStdin,
660 AttachStdout: attachStdout,
661 AttachStderr: attachStderr,
662 Env: envVariables,
663 Cmd: runCmd,
664 Image: copts.Image,
665 Volumes: volumes,
666 Entrypoint: entrypoint,
667 WorkingDir: copts.workingDir,
668 Labels: opts.ConvertKVStringsToMap(labels),
669 StopSignal: copts.stopSignal,
670 Healthcheck: healthConfig,
671 }
672 if flags.Changed("stop-timeout") {
673 config.StopTimeout = &copts.stopTimeout
674 }
675
676 hostConfig := &container.HostConfig{
677 Binds: binds,
678 ContainerIDFile: copts.containerIDFile,
679 OomScoreAdj: copts.oomScoreAdj,
680 AutoRemove: copts.autoRemove,
681 Privileged: copts.privileged,
682 PortBindings: portBindings,
683 Links: copts.links.GetSlice(),
684 PublishAllPorts: copts.publishAll,
685 // Make sure the dns fields are never nil.
686 // New containers don't ever have those fields nil,
687 // but pre created containers can still have those nil values.
688 // See https://github.com/docker/docker/pull/17779
689 // for a more detailed explanation on why we don't want that.
690 DNS: toNetipAddrSlice(copts.dns.GetAllOrEmpty()),
691 DNSSearch: copts.dnsSearch.GetAllOrEmpty(),
692 DNSOptions: copts.dnsOptions.GetAllOrEmpty(),
693 ExtraHosts: copts.extraHosts.GetSlice(),
694 VolumesFrom: copts.volumesFrom.GetSlice(),
695 IpcMode: container.IpcMode(copts.ipcMode),
696 NetworkMode: container.NetworkMode(copts.netMode.NetworkMode()),
697 PidMode: pidMode,
698 UTSMode: utsMode,
699 UsernsMode: usernsMode,
700 CgroupnsMode: cgroupnsMode,
701 CapAdd: copts.capAdd.GetSlice(),
702 CapDrop: copts.capDrop.GetSlice(),
703 GroupAdd: copts.groupAdd.GetSlice(),
704 RestartPolicy: restartPolicy,
705 SecurityOpt: securityOpts,
706 StorageOpt: storageOpts,
707 ReadonlyRootfs: copts.readonlyRootfs,
708 LogConfig: container.LogConfig{Type: copts.loggingDriver, Config: loggingOpts},
709 VolumeDriver: copts.volumeDriver,
710 Isolation: container.Isolation(copts.isolation),
711 ShmSize: copts.shmSize.Value(),
712 Resources: resources,
713 Tmpfs: tmpfs,
714 Sysctls: copts.sysctls.GetAll(),
715 Runtime: copts.runtime,
716 Mounts: copts.mounts.Value(),
717 MaskedPaths: maskedPaths,
718 ReadonlyPaths: readonlyPaths,
719 Annotations: copts.annotations.GetAll(),
720 }
721
722 if copts.autoRemove && !hostConfig.RestartPolicy.IsNone() {
723 return nil, errors.New("conflicting options: cannot specify both --restart and --rm")
724 }
725
726 // only set this value if the user provided the flag, else it should default to nil
727 if flags.Changed("init") {
728 hostConfig.Init = &copts.init
729 }
730
731 // When allocating stdin in attached mode, close stdin at client disconnect
732 if config.OpenStdin && config.AttachStdin {
733 config.StdinOnce = true
734 }
735
736 epCfg, err := parseNetworkOpts(copts)
737 if err != nil {
738 return nil, err
739 }
740
741 return &containerConfig{
742 Config: config,
743 HostConfig: hostConfig,
744 NetworkingConfig: &network.NetworkingConfig{
745 EndpointsConfig: epCfg,
746 },
747 }, nil
748}
749
750// parseNetworkOpts converts --network advanced options to endpoint-specs, and combines
751// them with the old --network-alias and --links. If returns an error if conflicting options
752// are found.
753//
754// this function may return _multiple_ endpoints, which is not currently supported
755// by the daemon, but may be in future; it's up to the daemon to produce an error
756// in case that is not supported.
757func parseNetworkOpts(copts *containerOptions) (map[string]*network.EndpointSettings, error) {
758 var (
759 endpoints = make(map[string]*network.EndpointSettings, len(copts.netMode.Value()))
760 hasUserDefined, hasNonUserDefined bool
761 )
762
763 if len(copts.netMode.Value()) == 0 {
764 n := opts.NetworkAttachmentOpts{
765 Target: "default",
766 }
767 if err := applyContainerOptions(&n, copts); err != nil {
768 return nil, err
769 }
770 ep, err := parseNetworkAttachmentOpt(n)
771 if err != nil {
772 return nil, err
773 }
774 endpoints["default"] = ep
775 }
776
777 for i, n := range copts.netMode.Value() {
778 if container.NetworkMode(n.Target).IsUserDefined() {
779 hasUserDefined = true
780 } else {
781 hasNonUserDefined = true
782 }
783 if i == 0 {
784 // The first network corresponds with what was previously the "only"
785 // network, and what would be used when using the non-advanced syntax
786 // `--network-alias`, `--link`, `--ip`, `--ip6`, and `--link-local-ip`
787 // are set on this network, to preserve backward compatibility with
788 // the non-advanced notation
789 if err := applyContainerOptions(&n, copts); err != nil {
790 return nil, err
791 }
792 }
793 ep, err := parseNetworkAttachmentOpt(n)
794 if err != nil {
795 return nil, err
796 }
797 if _, ok := endpoints[n.Target]; ok {
798 return nil, fmt.Errorf("network %q is specified multiple times", n.Target)
799 }
800
801 // For backward compatibility: if no custom options are provided for the network,
802 // and only a single network is specified, omit the endpoint-configuration
803 // on the client (the daemon will still create it when creating the container)
804 if i == 0 && len(copts.netMode.Value()) == 1 {
805 if ep == nil || reflect.ValueOf(*ep).IsZero() {
806 continue
807 }
808 }
809 endpoints[n.Target] = ep
810 }
811 if hasUserDefined && hasNonUserDefined {
812 return nil, errors.New("conflicting options: cannot attach both user-defined and non-user-defined network-modes")
813 }
814 return endpoints, nil
815}
816
817func applyContainerOptions(n *opts.NetworkAttachmentOpts, copts *containerOptions) error { //nolint:gocyclo
818 // TODO should we error if _any_ advanced option is used? (i.e. forbid to combine advanced notation with the "old" flags (`--network-alias`, `--link`, `--ip`, `--ip6`)?
819 if len(n.Aliases) > 0 && copts.aliases.Len() > 0 {
820 return errors.New("conflicting options: cannot specify both --network-alias and per-network alias")
821 }
822 if len(n.Links) > 0 && copts.links.Len() > 0 {
823 return errors.New("conflicting options: cannot specify both --link and per-network links")
824 }
825 if n.IPv4Address.IsValid() && copts.ipv4Address != nil {
826 return errors.New("conflicting options: cannot specify both --ip and per-network IPv4 address")
827 }
828 if n.IPv6Address.IsValid() && copts.ipv6Address != nil {
829 return errors.New("conflicting options: cannot specify both --ip6 and per-network IPv6 address")
830 }
831 if n.MacAddress != "" && copts.macAddress != "" {
832 return errors.New("conflicting options: cannot specify both --mac-address and per-network MAC address")
833 }
834 if len(n.LinkLocalIPs) > 0 && copts.linkLocalIPs.Len() > 0 {
835 return errors.New("conflicting options: cannot specify both --link-local-ip and per-network link-local IP addresses")
836 }
837 if copts.aliases.Len() > 0 {
838 n.Aliases = make([]string, copts.aliases.Len())
839 copy(n.Aliases, copts.aliases.GetSlice())
840 }
841 // For a user-defined network, "--link" is an endpoint option, it creates an alias. But,
842 // for the default bridge it defines a legacy-link.
843 if container.NetworkMode(n.Target).IsUserDefined() && copts.links.Len() > 0 {
844 n.Links = make([]string, copts.links.Len())
845 copy(n.Links, copts.links.GetSlice())
846 }
847 if copts.ipv4Address != nil {
848 if ipv4, ok := netip.AddrFromSlice(copts.ipv4Address.To4()); ok {
849 n.IPv4Address = ipv4
850 }
851 }
852 if copts.ipv6Address != nil {
853 if ipv6, ok := netip.AddrFromSlice(copts.ipv6Address.To16()); ok {
854 n.IPv6Address = ipv6
855 }
856 }
857 if copts.macAddress != "" {
858 n.MacAddress = copts.macAddress
859 }
860 if copts.linkLocalIPs.Len() > 0 {
861 n.LinkLocalIPs = toNetipAddrSlice(copts.linkLocalIPs.GetSlice())
862 }
863 return nil
864}
865
866func parseNetworkAttachmentOpt(ep opts.NetworkAttachmentOpts) (*network.EndpointSettings, error) {
867 if strings.TrimSpace(ep.Target) == "" {
868 return nil, errors.New("no name set for network")
869 }
870 if !container.NetworkMode(ep.Target).IsUserDefined() {
871 if len(ep.Aliases) > 0 {
872 return nil, errors.New("network-scoped aliases are only supported for user-defined networks")
873 }
874 if len(ep.Links) > 0 {
875 return nil, errors.New("links are only supported for user-defined networks")
876 }
877 }
878
879 epConfig := &network.EndpointSettings{
880 GwPriority: ep.GwPriority,
881 }
882 epConfig.Aliases = append(epConfig.Aliases, ep.Aliases...)
883 if len(ep.DriverOpts) > 0 {
884 epConfig.DriverOpts = make(map[string]string)
885 epConfig.DriverOpts = ep.DriverOpts
886 }
887 if len(ep.Links) > 0 {
888 epConfig.Links = ep.Links
889 }
890 if ep.IPv4Address.IsValid() || ep.IPv6Address.IsValid() || len(ep.LinkLocalIPs) > 0 {
891 epConfig.IPAMConfig = &network.EndpointIPAMConfig{
892 IPv4Address: ep.IPv4Address,
893 IPv6Address: ep.IPv6Address,
894 LinkLocalIPs: ep.LinkLocalIPs,
895 }
896 }
897 if ep.MacAddress != "" {
898 ma, err := net.ParseMAC(strings.TrimSpace(ep.MacAddress))
899 if err != nil {
900 return nil, fmt.Errorf("%s is not a valid mac address", ep.MacAddress)
901 }
902 epConfig.MacAddress = network.HardwareAddr(ma)
903 }
904 return epConfig, nil
905}
906
907func convertToStandardNotation(ports []string) ([]string, error) {
908 optsList := []string{}
909 for _, publish := range ports {
910 if strings.Contains(publish, "=") {
911 params := map[string]string{"protocol": "tcp"}
912 for param := range strings.SplitSeq(publish, ",") {
913 k, v, ok := strings.Cut(param, "=")
914 if !ok || k == "" {
915 return optsList, fmt.Errorf("invalid publish opts format (should be name=value but got '%s')", param)
916 }
917 params[k] = v
918 }
919 optsList = append(optsList, fmt.Sprintf("%s:%s/%s", params["published"], params["target"], params["protocol"]))
920 } else {
921 optsList = append(optsList, publish)
922 }
923 }
924 return optsList, nil
925}
926
927func parseLoggingOpts(loggingDriver string, loggingOpts []string) (map[string]string, error) {
928 loggingOptsMap := opts.ConvertKVStringsToMap(loggingOpts)
929 if loggingDriver == "none" && len(loggingOpts) > 0 {
930 return map[string]string{}, fmt.Errorf("invalid logging opts for driver %s", loggingDriver)
931 }
932 return loggingOptsMap, nil
933}
934
935// takes a local seccomp daemon, reads the file contents for sending to the daemon
936func parseSecurityOpts(securityOpts []string) ([]string, error) {
937 for key, opt := range securityOpts {
938 k, v, ok := strings.Cut(opt, "=")
939 if !ok && k != "no-new-privileges" {
940 k, v, ok = strings.Cut(opt, ":")
941 }
942 if (!ok || v == "") && k != "no-new-privileges" {
943 // "no-new-privileges" is the only option that does not require a value.
944 return securityOpts, fmt.Errorf("invalid --security-opt: %q", opt)
945 }
946 if k == "seccomp" {
947 switch v {
948 case seccompProfileDefault, seccompProfileUnconfined:
949 // known special names for built-in profiles, nothing to do.
950 default:
951 // value may be a filename, in which case we send the profile's
952 // content if it's valid JSON.
953 f, err := os.ReadFile(v)
954 if err != nil {
955 return securityOpts, fmt.Errorf("opening seccomp profile (%s) failed: %w", v, err)
956 }
957 var b bytes.Buffer
958 if err := json.Compact(&b, f); err != nil {
959 return securityOpts, fmt.Errorf("compacting json for seccomp profile (%s) failed: %w", v, err)
960 }
961 securityOpts[key] = "seccomp=" + b.String()
962 }
963 }
964 }
965
966 return securityOpts, nil
967}
968
969// parseSystemPaths checks if `systempaths=unconfined` security option is set,
970// and returns the `MaskedPaths` and `ReadonlyPaths` accordingly. An updated
971// list of security options is returned with this option removed, because the
972// `unconfined` option is handled client-side, and should not be sent to the
973// daemon.
974func parseSystemPaths(securityOpts []string) (filtered, maskedPaths, readonlyPaths []string) {
975 filtered = securityOpts[:0]
976 for _, opt := range securityOpts {
977 if opt == "systempaths=unconfined" {
978 maskedPaths = []string{}
979 readonlyPaths = []string{}
980 } else {
981 filtered = append(filtered, opt)
982 }
983 }
984
985 return filtered, maskedPaths, readonlyPaths
986}
987
988// parses storage options per container into a map
989func parseStorageOpts(storageOpts []string) (map[string]string, error) {
990 m := make(map[string]string)
991 for _, option := range storageOpts {
992 k, v, ok := strings.Cut(option, "=")
993 if !ok {
994 return nil, errors.New("invalid storage option")
995 }
996 m[k] = v
997 }
998 return m, nil
999}
1000
1001// parseDevice parses a device mapping string to a container.DeviceMapping struct
1002func parseDevice(device, serverOS string) (container.DeviceMapping, error) {
1003 switch serverOS {
1004 case "linux":
1005 return parseLinuxDevice(device)
1006 case "windows":
1007 // Windows doesn't support mapping, so passing the given value as-is.
1008 return container.DeviceMapping{PathOnHost: device}, nil
1009 default:
1010 return container.DeviceMapping{}, fmt.Errorf("unknown server OS: %s", serverOS)
1011 }
1012}
1013
1014// parseLinuxDevice parses a device mapping string to a container.DeviceMapping struct
1015// knowing that the target is a Linux daemon
1016func parseLinuxDevice(device string) (container.DeviceMapping, error) {
1017 var src, dst string
1018 permissions := "rwm"
1019 // We expect 3 parts at maximum; limit to 4 parts to detect invalid options.
1020 arr := strings.SplitN(device, ":", 4)
1021 switch len(arr) {
1022 case 3:
1023 permissions = arr[2]
1024 fallthrough
1025 case 2:
1026 if validDeviceMode(arr[1]) {
1027 permissions = arr[1]
1028 } else {
1029 dst = arr[1]
1030 }
1031 fallthrough
1032 case 1:
1033 src = arr[0]
1034 default:
1035 return container.DeviceMapping{}, fmt.Errorf("invalid device specification: %s", device)
1036 }
1037
1038 if dst == "" {
1039 dst = src
1040 }
1041
1042 return container.DeviceMapping{
1043 PathOnHost: src,
1044 PathInContainer: dst,
1045 CgroupPermissions: permissions,
1046 }, nil
1047}
1048
1049// validateDeviceCgroupRule validates a device cgroup rule string format
1050// It will make sure 'val' is in the form:
1051//
1052// 'type major:minor mode'
1053func validateDeviceCgroupRule(val string) (string, error) {
1054 if deviceCgroupRuleRegexp.MatchString(val) {
1055 return val, nil
1056 }
1057
1058 return val, fmt.Errorf("invalid device cgroup format '%s'", val)
1059}
1060
1061// validDeviceMode checks if the mode for device is valid or not.
1062// Valid mode is a composition of r (read), w (write), and m (mknod).
1063func validDeviceMode(mode string) bool {
1064 legalDeviceMode := map[rune]bool{
1065 'r': true,
1066 'w': true,
1067 'm': true,
1068 }
1069 if mode == "" {
1070 return false
1071 }
1072 for _, c := range mode {
1073 if !legalDeviceMode[c] {
1074 return false
1075 }
1076 legalDeviceMode[c] = false
1077 }
1078 return true
1079}
1080
1081// validateDevice validates a path for devices
1082func validateDevice(val string, serverOS string) (string, error) {
1083 switch serverOS {
1084 case "linux":
1085 return validateLinuxPath(val, validDeviceMode)
1086 case "windows":
1087 // Windows does validation entirely server-side
1088 return val, nil
1089 }
1090 return "", fmt.Errorf("unknown server OS: %s", serverOS)
1091}
1092
1093// validateLinuxPath is the implementation of validateDevice knowing that the
1094// target server operating system is a Linux daemon.
1095// It will make sure 'val' is in the form:
1096//
1097// [host-dir:]container-path[:mode]
1098//
1099// It also validates the device mode.
1100func validateLinuxPath(val string, validator func(string) bool) (string, error) {
1101 var containerPath string
1102 var mode string
1103
1104 if strings.Count(val, ":") > 2 {
1105 return val, fmt.Errorf("bad format for path: %s", val)
1106 }
1107
1108 split := strings.SplitN(val, ":", 3)
1109 if split[0] == "" {
1110 return val, fmt.Errorf("bad format for path: %s", val)
1111 }
1112 switch len(split) {
1113 case 1:
1114 containerPath = split[0]
1115 val = path.Clean(containerPath)
1116 case 2:
1117 if isValid := validator(split[1]); isValid {
1118 containerPath = split[0]
1119 mode = split[1]
1120 val = fmt.Sprintf("%s:%s", path.Clean(containerPath), mode)
1121 } else {
1122 containerPath = split[1]
1123 val = fmt.Sprintf("%s:%s", split[0], path.Clean(containerPath))
1124 }
1125 case 3:
1126 containerPath = split[1]
1127 mode = split[2]
1128 if isValid := validator(split[2]); !isValid {
1129 return val, fmt.Errorf("bad mode specified: %s", mode)
1130 }
1131 val = fmt.Sprintf("%s:%s:%s", split[0], containerPath, mode)
1132 }
1133
1134 if !path.IsAbs(containerPath) {
1135 return val, fmt.Errorf("%s is not an absolute path", containerPath)
1136 }
1137 return val, nil
1138}
1139
1140// validateAttach validates that the specified string is a valid attach option.
1141func validateAttach(val string) (string, error) {
1142 s := strings.ToLower(val)
1143 if slices.Contains([]string{"stdin", "stdout", "stderr"}, s) {
1144 return s, nil
1145 }
1146 return val, errors.New("valid streams are STDIN, STDOUT and STDERR")
1147}
1148
1149func toNetipAddrSlice(ips []string) []netip.Addr {
1150 if len(ips) == 0 {
1151 return nil
1152 }
1153 netIPs := make([]netip.Addr, 0, len(ips))
1154 for _, ip := range ips {
1155 addr, err := netip.ParseAddr(ip)
1156 if err != nil {
1157 continue
1158 }
1159 netIPs = append(netIPs, addr)
1160 }
1161 return netIPs
1162}
Deleted act/pkg/container/docker_images.go
@@ -1,66 +0,0 @@
1//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows || netbsd))
2
3package container
4
5import (
6 "context"
7 "fmt"
8
9 cerrdefs "github.com/containerd/errdefs"
10 "github.com/moby/moby/client"
11 "github.com/nektos/act/pkg/common"
12)
13
14// ImageExistsLocally returns a boolean indicating if an image with the
15// requested name, tag and architecture exists in the local docker image store
16func ImageExistsLocally(ctx context.Context, imageName string, platform string) (bool, error) {
17 cli, err := GetDockerClient(ctx)
18 if err != nil {
19 return false, err
20 }
21 defer cli.Close()
22
23 inspectImage, err := cli.ImageInspect(ctx, imageName)
24 if cerrdefs.IsNotFound(err) {
25 return false, nil
26 } else if err != nil {
27 return false, err
28 }
29
30 imagePlatform := fmt.Sprintf("%s/%s", inspectImage.Os, inspectImage.Architecture)
31
32 if platform == "" || platform == "any" || imagePlatform == platform {
33 return true, nil
34 }
35
36 logger := common.Logger(ctx)
37 logger.Infof("image found but platform does not match: %s (image) != %s (platform)\n", imagePlatform, platform)
38
39 return false, nil
40}
41
42// RemoveImage removes image from local store, the function is used to run different
43// container image architectures
44func RemoveImage(ctx context.Context, imageName string, force bool, pruneChildren bool) (bool, error) {
45 cli, err := GetDockerClient(ctx)
46 if err != nil {
47 return false, err
48 }
49 defer cli.Close()
50
51 inspectImage, err := cli.ImageInspect(ctx, imageName)
52 if cerrdefs.IsNotFound(err) {
53 return false, nil
54 } else if err != nil {
55 return false, err
56 }
57
58 if _, err = cli.ImageRemove(ctx, inspectImage.ID, client.ImageRemoveOptions{
59 Force: force,
60 PruneChildren: pruneChildren,
61 }); err != nil {
62 return false, err
63 }
64
65 return true, nil
66}
Deleted act/pkg/container/docker_logger.go
@@ -1,83 +0,0 @@
1//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows || netbsd))
2
3package container
4
5import (
6 "bufio"
7 "encoding/json"
8 "errors"
9 "io"
10
11 "github.com/sirupsen/logrus"
12)
13
14type dockerMessage struct {
15 ID string `json:"id"`
16 Stream string `json:"stream"`
17 Error string `json:"error"`
18 ErrorDetail struct {
19 Message string
20 }
21 Status string `json:"status"`
22 Progress string `json:"progress"`
23}
24
25const logPrefix = " \U0001F433 "
26
27func logDockerResponse(logger logrus.FieldLogger, dockerResponse io.ReadCloser, isError bool) error {
28 if dockerResponse == nil {
29 return nil
30 }
31 defer dockerResponse.Close()
32
33 scanner := bufio.NewScanner(dockerResponse)
34 msg := dockerMessage{}
35
36 for scanner.Scan() {
37 line := scanner.Bytes()
38
39 msg.ID = ""
40 msg.Stream = ""
41 msg.Error = ""
42 msg.ErrorDetail.Message = ""
43 msg.Status = ""
44 msg.Progress = ""
45
46 if err := json.Unmarshal(line, &msg); err != nil {
47 writeLog(logger, false, "Unable to unmarshal line [%s] ==> %v", string(line), err)
48 continue
49 }
50
51 if msg.Error != "" {
52 writeLog(logger, isError, "%s", msg.Error)
53 return errors.New(msg.Error)
54 }
55
56 if msg.ErrorDetail.Message != "" {
57 writeLog(logger, isError, "%s", msg.ErrorDetail.Message)
58 return errors.New(msg.Error)
59 }
60
61 if msg.Status != "" {
62 if msg.Progress != "" {
63 writeLog(logger, isError, "%s :: %s :: %s\n", msg.Status, msg.ID, msg.Progress)
64 } else {
65 writeLog(logger, isError, "%s :: %s\n", msg.Status, msg.ID)
66 }
67 } else if msg.Stream != "" {
68 writeLog(logger, isError, "%s", msg.Stream)
69 } else {
70 writeLog(logger, false, "Unable to handle line: %s", string(line))
71 }
72 }
73
74 return nil
75}
76
77func writeLog(logger logrus.FieldLogger, isError bool, format string, args ...interface{}) {
78 if isError {
79 logger.Errorf(format, args...)
80 } else {
81 logger.Debugf(format, args...)
82 }
83}
Deleted act/pkg/container/docker_network.go
@@ -1,79 +0,0 @@
1//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows || netbsd))
2
3package container
4
5import (
6 "context"
7
8 "github.com/moby/moby/client"
9 "github.com/nektos/act/pkg/common"
10)
11
12func NewDockerNetworkCreateExecutor(name string) common.Executor {
13 return func(ctx context.Context) error {
14 cli, err := GetDockerClient(ctx)
15 if err != nil {
16 return err
17 }
18 defer cli.Close()
19
20 // Only create the network if it doesn't exist
21 networks, err := cli.NetworkList(ctx, client.NetworkListOptions{})
22 if err != nil {
23 return err
24 }
25 common.Logger(ctx).Debugf("%v", networks)
26 for _, network := range networks.Items {
27 if network.Name == name {
28 common.Logger(ctx).Debugf("Network %v exists", name)
29 return nil
30 }
31 }
32
33 _, err = cli.NetworkCreate(ctx, name, client.NetworkCreateOptions{
34 Driver: "bridge",
35 Scope: "local",
36 })
37 if err != nil {
38 return err
39 }
40
41 return nil
42 }
43}
44
45func NewDockerNetworkRemoveExecutor(name string) common.Executor {
46 return func(ctx context.Context) error {
47 cli, err := GetDockerClient(ctx)
48 if err != nil {
49 return err
50 }
51 defer cli.Close()
52
53 // Make sure that all network of the specified name are removed
54 // cli.NetworkRemove refuses to remove a network if there are duplicates
55 networks, err := cli.NetworkList(ctx, client.NetworkListOptions{})
56 if err != nil {
57 return err
58 }
59 common.Logger(ctx).Debugf("%v", networks)
60 for _, net := range networks.Items {
61 if net.Name == name {
62 result, err := cli.NetworkInspect(ctx, net.ID, client.NetworkInspectOptions{})
63 if err != nil {
64 return err
65 }
66
67 if len(result.Network.Containers) == 0 {
68 if _, err = cli.NetworkRemove(ctx, net.ID, client.NetworkRemoveOptions{}); err != nil {
69 common.Logger(ctx).Debugf("%v", err)
70 }
71 } else {
72 common.Logger(ctx).Debugf("Refusing to remove network %v because it still has active endpoints", name)
73 }
74 }
75 }
76
77 return err
78 }
79}
Deleted act/pkg/container/docker_pull.go
@@ -1,131 +0,0 @@
1//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows || netbsd))
2
3package container
4
5import (
6 "context"
7 "encoding/base64"
8 "encoding/json"
9 "fmt"
10 "strings"
11
12 "github.com/distribution/reference"
13 "github.com/moby/moby/api/types/registry"
14 "github.com/moby/moby/client"
15 specs "github.com/opencontainers/image-spec/specs-go/v1"
16
17 "github.com/nektos/act/pkg/common"
18)
19
20// NewDockerPullExecutor function to create a run executor for the container
21func NewDockerPullExecutor(input NewDockerPullExecutorInput) common.Executor {
22 return func(ctx context.Context) error {
23 logger := common.Logger(ctx)
24 logger.Debugf("%sdocker pull %v", logPrefix, input.Image)
25
26 if common.Dryrun(ctx) {
27 return nil
28 }
29
30 pull := input.ForcePull
31 if !pull {
32 imageExists, err := ImageExistsLocally(ctx, input.Image, input.Platform)
33 logger.Debugf("Image exists? %v", imageExists)
34 if err != nil {
35 return fmt.Errorf("unable to determine if image already exists for image '%s' (%s): %w", input.Image, input.Platform, err)
36 }
37
38 if !imageExists {
39 pull = true
40 }
41 }
42
43 if !pull {
44 return nil
45 }
46
47 imageRef := cleanImage(ctx, input.Image)
48 logger.Debugf("pulling image '%v' (%s)", imageRef, input.Platform)
49
50 cli, err := GetDockerClient(ctx)
51 if err != nil {
52 return err
53 }
54 defer cli.Close()
55
56 imagePullOptions, err := getImagePullOptions(ctx, input)
57 if err != nil {
58 return err
59 }
60
61 reader, err := cli.ImagePull(ctx, imageRef, imagePullOptions)
62
63 _ = logDockerResponse(logger, reader, err != nil)
64 if err != nil {
65 if imagePullOptions.RegistryAuth != "" && strings.Contains(err.Error(), "unauthorized") {
66 logger.Errorf("pulling image '%v' (%s) failed with credentials %s retrying without them, please check for stale docker config files", imageRef, input.Platform, err.Error())
67 imagePullOptions.RegistryAuth = ""
68 reader, err = cli.ImagePull(ctx, imageRef, imagePullOptions)
69
70 _ = logDockerResponse(logger, reader, err != nil)
71 }
72 return err
73 }
74 return nil
75 }
76}
77
78func getImagePullOptions(ctx context.Context, input NewDockerPullExecutorInput) (client.ImagePullOptions, error) {
79 imagePullOptions := client.ImagePullOptions{}
80 if input.Platform != "" {
81 parts := strings.SplitN(input.Platform, "/", 2)
82 if len(parts) == 2 {
83 imagePullOptions.Platforms = []specs.Platform{{OS: parts[0], Architecture: parts[1]}}
84 }
85 }
86 logger := common.Logger(ctx)
87
88 if input.Username != "" && input.Password != "" {
89 logger.Debugf("using authentication for docker pull")
90
91 authConfig := registry.AuthConfig{
92 Username: input.Username,
93 Password: input.Password,
94 }
95
96 encodedJSON, err := json.Marshal(authConfig)
97 if err != nil {
98 return imagePullOptions, err
99 }
100
101 imagePullOptions.RegistryAuth = base64.URLEncoding.EncodeToString(encodedJSON)
102 } else {
103 authConfig, err := LoadDockerAuthConfig(ctx, input.Image)
104 if err != nil {
105 return imagePullOptions, err
106 }
107 if authConfig.Username == "" && authConfig.Password == "" {
108 return imagePullOptions, nil
109 }
110 logger.Info("using DockerAuthConfig authentication for docker pull")
111
112 encodedJSON, err := json.Marshal(authConfig)
113 if err != nil {
114 return imagePullOptions, err
115 }
116
117 imagePullOptions.RegistryAuth = base64.URLEncoding.EncodeToString(encodedJSON)
118 }
119
120 return imagePullOptions, nil
121}
122
123func cleanImage(ctx context.Context, image string) string {
124 ref, err := reference.ParseAnyReference(image)
125 if err != nil {
126 common.Logger(ctx).Error(err)
127 return ""
128 }
129
130 return ref.String()
131}
Deleted act/pkg/container/docker_run.go
@@ -1,958 +0,0 @@
1//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows || netbsd))
2
3package container
4
5import (
6 "archive/tar"
7 "bytes"
8 "context"
9 "errors"
10 "fmt"
11 "io"
12 "os"
13 "path/filepath"
14 "regexp"
15 "runtime"
16 "strconv"
17 "strings"
18
19 "net/netip"
20
21 "dario.cat/mergo"
22 "github.com/Masterminds/semver"
23 "github.com/docker/cli/cli/connhelper"
24 "github.com/docker/go-connections/nat"
25 "github.com/go-git/go-billy/v5/helper/polyfill"
26 "github.com/go-git/go-billy/v5/osfs"
27 "github.com/go-git/go-git/v5/plumbing/format/gitignore"
28 "github.com/joho/godotenv"
29 "github.com/kballard/go-shellquote"
30 "github.com/moby/moby/api/pkg/stdcopy"
31 "github.com/moby/moby/api/types/container"
32 "github.com/moby/moby/api/types/mount"
33 "github.com/moby/moby/api/types/network"
34 "github.com/moby/moby/api/types/system"
35 "github.com/moby/moby/client"
36 specs "github.com/opencontainers/image-spec/specs-go/v1"
37 "github.com/spf13/pflag"
38 "golang.org/x/term"
39
40 "github.com/nektos/act/pkg/common"
41 "github.com/nektos/act/pkg/filecollector"
42)
43
44// NewContainer creates a reference to a container
45func NewContainer(input *NewContainerInput) ExecutionsEnvironment {
46 cr := new(containerReference)
47 cr.input = input
48 return cr
49}
50
51// supportsContainerImagePlatform returns true if the underlying Docker server
52// API version is 1.41 and beyond
53func supportsContainerImagePlatform(ctx context.Context, cli client.APIClient) bool {
54 logger := common.Logger(ctx)
55 ver, err := cli.ServerVersion(ctx, client.ServerVersionOptions{})
56 if err != nil {
57 logger.Panicf("Failed to get Docker API Version: %s", err)
58 return false
59 }
60 sv, err := semver.NewVersion(ver.APIVersion)
61 if err != nil {
62 logger.Panicf("Failed to unmarshal Docker Version: %s", err)
63 return false
64 }
65 constraint, _ := semver.NewConstraint(">= 1.41")
66 return constraint.Check(sv)
67}
68
69func (cr *containerReference) Create(capAdd []string, capDrop []string) common.Executor {
70 return common.
71 NewInfoExecutor("%sdocker create image=%s platform=%s entrypoint=%+q cmd=%+q network=%+q", logPrefix, cr.input.Image, cr.input.Platform, cr.input.Entrypoint, cr.input.Cmd, cr.input.NetworkMode).
72 Then(
73 common.NewPipelineExecutor(
74 cr.connect(),
75 cr.find(),
76 cr.create(capAdd, capDrop),
77 ).IfNot(common.Dryrun),
78 )
79}
80
81func (cr *containerReference) Start(attach bool) common.Executor {
82 return common.
83 NewInfoExecutor("%sdocker run image=%s platform=%s entrypoint=%+q cmd=%+q network=%+q", logPrefix, cr.input.Image, cr.input.Platform, cr.input.Entrypoint, cr.input.Cmd, cr.input.NetworkMode).
84 Then(
85 common.NewPipelineExecutor(
86 cr.connect(),
87 cr.find(),
88 cr.attach().IfBool(attach),
89 cr.start(),
90 cr.wait().IfBool(attach),
91 cr.tryReadUID(),
92 cr.tryReadGID(),
93 func(ctx context.Context) error {
94 // If this fails, then folders have wrong permissions on non root container
95 if cr.UID != 0 || cr.GID != 0 {
96 _ = cr.Exec([]string{"chown", "-R", fmt.Sprintf("%d:%d", cr.UID, cr.GID), cr.input.WorkingDir}, nil, "0", "")(ctx)
97 }
98 return nil
99 },
100 ).IfNot(common.Dryrun),
101 )
102}
103
104func (cr *containerReference) Pull(forcePull bool) common.Executor {
105 return common.
106 NewInfoExecutor("%sdocker pull image=%s platform=%s username=%s forcePull=%t", logPrefix, cr.input.Image, cr.input.Platform, cr.input.Username, forcePull).
107 Then(
108 NewDockerPullExecutor(NewDockerPullExecutorInput{
109 Image: cr.input.Image,
110 ForcePull: forcePull,
111 Platform: cr.input.Platform,
112 Username: cr.input.Username,
113 Password: cr.input.Password,
114 }),
115 )
116}
117
118func (cr *containerReference) Copy(destPath string, files ...*FileEntry) common.Executor {
119 return common.NewPipelineExecutor(
120 cr.connect(),
121 cr.find(),
122 cr.copyContent(destPath, files...),
123 ).IfNot(common.Dryrun)
124}
125
126func (cr *containerReference) CopyDir(destPath string, srcPath string, useGitIgnore bool) common.Executor {
127 return common.NewPipelineExecutor(
128 common.NewInfoExecutor("%sdocker cp src=%s dst=%s", logPrefix, srcPath, destPath),
129 cr.copyDir(destPath, srcPath, useGitIgnore),
130 func(ctx context.Context) error {
131 // If this fails, then folders have wrong permissions on non root container
132 if cr.UID != 0 || cr.GID != 0 {
133 _ = cr.Exec([]string{"chown", "-R", fmt.Sprintf("%d:%d", cr.UID, cr.GID), destPath}, nil, "0", "")(ctx)
134 }
135 return nil
136 },
137 ).IfNot(common.Dryrun)
138}
139
140func (cr *containerReference) GetContainerArchive(ctx context.Context, srcPath string) (io.ReadCloser, error) {
141 if common.Dryrun(ctx) {
142 return nil, fmt.Errorf("DRYRUN is not supported in GetContainerArchive")
143 }
144 result, err := cr.cli.CopyFromContainer(ctx, cr.id, client.CopyFromContainerOptions{SourcePath: srcPath})
145 return result.Content, err
146}
147
148func (cr *containerReference) UpdateFromEnv(srcPath string, env *map[string]string) common.Executor {
149 return parseEnvFile(cr, srcPath, env).IfNot(common.Dryrun)
150}
151
152func (cr *containerReference) UpdateFromImageEnv(env *map[string]string) common.Executor {
153 return cr.extractFromImageEnv(env).IfNot(common.Dryrun)
154}
155
156func (cr *containerReference) Exec(command []string, env map[string]string, user, workdir string) common.Executor {
157 return common.NewPipelineExecutor(
158 common.NewInfoExecutor("%sdocker exec cmd=[%s] user=%s workdir=%s", logPrefix, strings.Join(command, " "), user, workdir),
159 cr.connect(),
160 cr.find(),
161 cr.exec(command, env, user, workdir),
162 ).IfNot(common.Dryrun)
163}
164
165func (cr *containerReference) Remove() common.Executor {
166 return common.NewPipelineExecutor(
167 cr.connect(),
168 cr.find(),
169 ).Finally(
170 cr.remove(),
171 ).IfNot(common.Dryrun)
172}
173
174func (cr *containerReference) GetHealth(ctx context.Context) Health {
175 inspectResult, err := cr.cli.ContainerInspect(ctx, cr.id, client.ContainerInspectOptions{})
176 logger := common.Logger(ctx)
177 if err != nil {
178 logger.Errorf("failed to query container health %s", err)
179 return HealthUnHealthy
180 }
181 resp := inspectResult.Container
182 if resp.Config == nil || resp.Config.Healthcheck == nil || resp.State == nil || resp.State.Health == nil || len(resp.Config.Healthcheck.Test) == 1 && strings.EqualFold(resp.Config.Healthcheck.Test[0], "NONE") {
183 logger.Debugf("no container health check defined")
184 return HealthHealthy
185 }
186
187 logger.Infof("container health of %s (%s) is %s", cr.id, resp.Config.Image, resp.State.Health.Status)
188 switch resp.State.Health.Status {
189 case "starting":
190 return HealthStarting
191 case "healthy":
192 return HealthHealthy
193 case "unhealthy":
194 return HealthUnHealthy
195 }
196 return HealthUnHealthy
197}
198
199func (cr *containerReference) ReplaceLogWriter(stdout io.Writer, stderr io.Writer) (io.Writer, io.Writer) {
200 out := cr.input.Stdout
201 err := cr.input.Stderr
202
203 cr.input.Stdout = stdout
204 cr.input.Stderr = stderr
205
206 return out, err
207}
208
209type containerReference struct {
210 cli client.APIClient
211 id string
212 input *NewContainerInput
213 UID int
214 GID int
215 LinuxContainerEnvironmentExtensions
216}
217
218func GetDockerClient(_ context.Context) (cli client.APIClient, err error) {
219 dockerHost := os.Getenv("DOCKER_HOST")
220
221 if strings.HasPrefix(dockerHost, "ssh://") {
222 var helper *connhelper.ConnectionHelper
223
224 helper, err = connhelper.GetConnectionHelper(dockerHost)
225 if err != nil {
226 return nil, err
227 }
228 cli, err = client.New(
229 client.WithHost(helper.Host),
230 client.WithDialContext(helper.Dialer),
231 )
232 } else {
233 cli, err = client.New(client.FromEnv)
234 }
235 if err != nil {
236 return nil, fmt.Errorf("failed to connect to docker daemon: %w", err)
237 }
238
239 return cli, nil
240}
241
242func GetHostInfo(ctx context.Context) (info system.Info, err error) {
243 var cli client.APIClient
244 cli, err = GetDockerClient(ctx)
245 if err != nil {
246 return info, err
247 }
248 defer cli.Close()
249
250 result, err := cli.Info(ctx, client.InfoOptions{})
251 if err != nil {
252 return info, err
253 }
254
255 return result.Info, nil
256}
257
258// Arch fetches values from docker info and translates architecture to
259// GitHub actions compatible runner.arch values
260// https://github.com/github/docs/blob/main/data/reusables/actions/runner-arch-description.md
261func RunnerArch(ctx context.Context) string {
262 info, err := GetHostInfo(ctx)
263 if err != nil {
264 return ""
265 }
266
267 archMapper := map[string]string{
268 "x86_64": "X64",
269 "amd64": "X64",
270 "386": "X86",
271 "aarch64": "ARM64",
272 "arm64": "ARM64",
273 }
274 if arch, ok := archMapper[info.Architecture]; ok {
275 return arch
276 }
277 return info.Architecture
278}
279
280func (cr *containerReference) connect() common.Executor {
281 return func(ctx context.Context) error {
282 if cr.cli != nil {
283 return nil
284 }
285 cli, err := GetDockerClient(ctx)
286 if err != nil {
287 return err
288 }
289 cr.cli = cli
290 return nil
291 }
292}
293
294func (cr *containerReference) Close() common.Executor {
295 return func(_ context.Context) error {
296 if cr.cli != nil {
297 err := cr.cli.Close()
298 cr.cli = nil
299 if err != nil {
300 return fmt.Errorf("failed to close client: %w", err)
301 }
302 }
303 return nil
304 }
305}
306
307func (cr *containerReference) find() common.Executor {
308 return func(ctx context.Context) error {
309 if cr.id != "" {
310 return nil
311 }
312 result, err := cr.cli.ContainerList(ctx, client.ContainerListOptions{
313 All: true,
314 })
315 if err != nil {
316 return fmt.Errorf("failed to list containers: %w", err)
317 }
318
319 for _, c := range result.Items {
320 for _, name := range c.Names {
321 if name[1:] == cr.input.Name {
322 cr.id = c.ID
323 return nil
324 }
325 }
326 }
327
328 cr.id = ""
329 return nil
330 }
331}
332
333func (cr *containerReference) remove() common.Executor {
334 return func(ctx context.Context) error {
335 if cr.id == "" {
336 return nil
337 }
338
339 logger := common.Logger(ctx)
340 _, err := cr.cli.ContainerRemove(ctx, cr.id, client.ContainerRemoveOptions{
341 RemoveVolumes: true,
342 Force: true,
343 })
344 if err != nil {
345 logger.Error(fmt.Errorf("failed to remove container: %w", err))
346 }
347
348 logger.Debugf("Removed container: %v", cr.id)
349 cr.id = ""
350 return nil
351 }
352}
353
354func (cr *containerReference) mergeContainerConfigs(ctx context.Context, config *container.Config, hostConfig *container.HostConfig) (*container.Config, *container.HostConfig, error) {
355 logger := common.Logger(ctx)
356 input := cr.input
357
358 if input.Options == "" {
359 return config, hostConfig, nil
360 }
361
362 // parse configuration from CLI container.options
363 flags := pflag.NewFlagSet("container_flags", pflag.ContinueOnError)
364 copts := addFlags(flags)
365
366 optionsArgs, err := shellquote.Split(input.Options)
367 if err != nil {
368 return nil, nil, fmt.Errorf("Cannot split container options: '%s': '%w'", input.Options, err)
369 }
370
371 err = flags.Parse(optionsArgs)
372 if err != nil {
373 return nil, nil, fmt.Errorf("Cannot parse container options: '%s': '%w'", input.Options, err)
374 }
375
376 if len(copts.netMode.Value()) == 0 {
377 if err = copts.netMode.Set(cr.input.NetworkMode); err != nil {
378 return nil, nil, fmt.Errorf("Cannot parse networkmode=%s. This is an internal error and should not happen: '%w'", cr.input.NetworkMode, err)
379 }
380 }
381
382 containerConfig, err := parse(flags, copts, runtime.GOOS)
383 if err != nil {
384 return nil, nil, fmt.Errorf("Cannot process container options: '%s': '%w'", input.Options, err)
385 }
386
387 logger.Debugf("Custom container.Config from options ==> %+v", containerConfig.Config)
388
389 err = mergo.Merge(config, containerConfig.Config, mergo.WithOverride)
390 if err != nil {
391 return nil, nil, fmt.Errorf("Cannot merge container.Config options: '%s': '%w'", input.Options, err)
392 }
393 logger.Debugf("Merged container.Config ==> %+v", config)
394
395 logger.Debugf("Custom container.HostConfig from options ==> %+v", containerConfig.HostConfig)
396
397 hostConfig.Binds = append(hostConfig.Binds, containerConfig.HostConfig.Binds...)
398 hostConfig.Mounts = append(hostConfig.Mounts, containerConfig.HostConfig.Mounts...)
399 binds := hostConfig.Binds
400 mounts := hostConfig.Mounts
401 err = mergo.Merge(hostConfig, containerConfig.HostConfig, mergo.WithOverride)
402 if err != nil {
403 return nil, nil, fmt.Errorf("Cannot merge container.HostConfig options: '%s': '%w'", input.Options, err)
404 }
405 hostConfig.Binds = binds
406 hostConfig.Mounts = mounts
407 logger.Debugf("Merged container.HostConfig ==> %+v", hostConfig)
408
409 return config, hostConfig, nil
410}
411
412func (cr *containerReference) create(capAdd []string, capDrop []string) common.Executor {
413 return func(ctx context.Context) error {
414 if cr.id != "" {
415 return nil
416 }
417 logger := common.Logger(ctx)
418 isTerminal := term.IsTerminal(int(os.Stdout.Fd()))
419 input := cr.input
420
421 config := &container.Config{
422 Image: input.Image,
423 WorkingDir: input.WorkingDir,
424 Env: input.Env,
425 ExposedPorts: convertPortSet(input.ExposedPorts),
426 Tty: isTerminal,
427 }
428 logger.Debugf("Common container.Config ==> %+v", config)
429
430 if len(input.Cmd) != 0 {
431 config.Cmd = input.Cmd
432 }
433
434 if len(input.Entrypoint) != 0 {
435 config.Entrypoint = input.Entrypoint
436 }
437
438 mounts := make([]mount.Mount, 0)
439 for mountSource, mountTarget := range input.Mounts {
440 mounts = append(mounts, mount.Mount{
441 Type: mount.TypeVolume,
442 Source: mountSource,
443 Target: mountTarget,
444 })
445 }
446
447 var platSpecs *specs.Platform
448 if supportsContainerImagePlatform(ctx, cr.cli) && cr.input.Platform != "" {
449 desiredPlatform := strings.SplitN(cr.input.Platform, `/`, 2)
450
451 if len(desiredPlatform) != 2 {
452 return fmt.Errorf("incorrect container platform option '%s'", cr.input.Platform)
453 }
454
455 platSpecs = &specs.Platform{
456 Architecture: desiredPlatform[1],
457 OS: desiredPlatform[0],
458 }
459 }
460
461 hostConfig := &container.HostConfig{
462 CapAdd: capAdd,
463 CapDrop: capDrop,
464 Binds: input.Binds,
465 Mounts: mounts,
466 NetworkMode: container.NetworkMode(input.NetworkMode),
467 Privileged: input.Privileged,
468 UsernsMode: container.UsernsMode(input.UsernsMode),
469 PortBindings: convertPortMap(input.PortBindings),
470 }
471 logger.Debugf("Common container.HostConfig ==> %+v", hostConfig)
472
473 config, hostConfig, err := cr.mergeContainerConfigs(ctx, config, hostConfig)
474 if err != nil {
475 return err
476 }
477
478 var networkingConfig *network.NetworkingConfig
479 logger.Debugf("input.NetworkAliases ==> %v", input.NetworkAliases)
480 n := hostConfig.NetworkMode
481 // IsUserDefined and IsHost are broken on windows
482 if n.IsUserDefined() && n != "host" && len(input.NetworkAliases) > 0 {
483 endpointConfig := &network.EndpointSettings{
484 Aliases: input.NetworkAliases,
485 }
486 networkingConfig = &network.NetworkingConfig{
487 EndpointsConfig: map[string]*network.EndpointSettings{
488 input.NetworkMode: endpointConfig,
489 },
490 }
491 }
492
493 createResult, err := cr.cli.ContainerCreate(ctx, client.ContainerCreateOptions{
494 Config: config,
495 HostConfig: hostConfig,
496 NetworkingConfig: networkingConfig,
497 Platform: platSpecs,
498 Name: input.Name,
499 })
500 if err != nil {
501 return fmt.Errorf("failed to create container: '%w'", err)
502 }
503
504 logger.Debugf("Created container name=%s id=%v from image %v (platform: %s)", input.Name, createResult.ID, input.Image, input.Platform)
505 logger.Debugf("ENV ==> %v", input.Env)
506
507 cr.id = createResult.ID
508 return nil
509 }
510}
511
512func (cr *containerReference) extractFromImageEnv(env *map[string]string) common.Executor {
513 envMap := *env
514 return func(ctx context.Context) error {
515 logger := common.Logger(ctx)
516
517 inspectResult, err := cr.cli.ImageInspect(ctx, cr.input.Image)
518 if err != nil {
519 logger.Error(err)
520 return fmt.Errorf("inspect image: %w", err)
521 }
522
523 if inspectResult.Config == nil {
524 return nil
525 }
526
527 imageEnv, err := godotenv.Unmarshal(strings.Join(inspectResult.Config.Env, "\n"))
528 if err != nil {
529 logger.Error(err)
530 return fmt.Errorf("unmarshal image env: %w", err)
531 }
532
533 for k, v := range imageEnv {
534 if k == "PATH" {
535 if envMap[k] == "" {
536 envMap[k] = v
537 } else {
538 envMap[k] += `:` + v
539 }
540 } else if envMap[k] == "" {
541 envMap[k] = v
542 }
543 }
544
545 env = &envMap
546 return nil
547 }
548}
549
550func (cr *containerReference) exec(cmd []string, env map[string]string, user, workdir string) common.Executor {
551 return func(ctx context.Context) error {
552 logger := common.Logger(ctx)
553 // Fix slashes when running on Windows
554 if runtime.GOOS == "windows" {
555 var newCmd []string
556 for _, v := range cmd {
557 newCmd = append(newCmd, strings.ReplaceAll(v, `\`, `/`))
558 }
559 cmd = newCmd
560 }
561
562 logger.Debugf("Exec command '%s'", cmd)
563 isTerminal := term.IsTerminal(int(os.Stdout.Fd()))
564 envList := make([]string, 0)
565 for k, v := range env {
566 envList = append(envList, fmt.Sprintf("%s=%s", k, v))
567 }
568
569 var wd string
570 if workdir != "" {
571 if strings.HasPrefix(workdir, "/") {
572 wd = workdir
573 } else {
574 wd = fmt.Sprintf("%s/%s", cr.input.WorkingDir, workdir)
575 }
576 } else {
577 wd = cr.input.WorkingDir
578 }
579 logger.Debugf("Working directory '%s'", wd)
580
581 idResp, err := cr.cli.ExecCreate(ctx, cr.id, client.ExecCreateOptions{
582 User: user,
583 Cmd: cmd,
584 WorkingDir: wd,
585 Env: envList,
586 TTY: isTerminal,
587 AttachStderr: true,
588 AttachStdout: true,
589 })
590 if err != nil {
591 return fmt.Errorf("failed to create exec: %w", err)
592 }
593
594 resp, err := cr.cli.ExecAttach(ctx, idResp.ID, client.ExecAttachOptions{
595 TTY: isTerminal,
596 })
597 if err != nil {
598 return fmt.Errorf("failed to attach to exec: %w", err)
599 }
600 defer resp.Close()
601
602 err = cr.waitForCommand(ctx, isTerminal, resp.HijackedResponse)
603 if err != nil {
604 return err
605 }
606
607 inspectResp, err := cr.cli.ExecInspect(ctx, idResp.ID, client.ExecInspectOptions{})
608 if err != nil {
609 return fmt.Errorf("failed to inspect exec: %w", err)
610 }
611
612 switch inspectResp.ExitCode {
613 case 0:
614 return nil
615 case 127:
616 return fmt.Errorf("exitcode '%d': command not found, please refer to https://github.com/nektos/act/issues/107 for more information", inspectResp.ExitCode)
617 default:
618 return fmt.Errorf("exitcode '%d': failure", inspectResp.ExitCode)
619 }
620 }
621}
622
623func (cr *containerReference) tryReadID(opt string, cbk func(id int)) common.Executor {
624 return func(ctx context.Context) error {
625 idResp, err := cr.cli.ExecCreate(ctx, cr.id, client.ExecCreateOptions{
626 Cmd: []string{"id", opt},
627 AttachStdout: true,
628 AttachStderr: true,
629 })
630 if err != nil {
631 return nil
632 }
633
634 resp, err := cr.cli.ExecAttach(ctx, idResp.ID, client.ExecAttachOptions{})
635 if err != nil {
636 return nil
637 }
638 defer resp.Close()
639
640 sid, err := resp.Reader.ReadString('\n')
641 if err != nil {
642 return nil
643 }
644 exp := regexp.MustCompile(`\d+\n`)
645 found := exp.FindString(sid)
646 id, err := strconv.ParseInt(strings.TrimSpace(found), 10, 32)
647 if err != nil {
648 return nil
649 }
650 cbk(int(id))
651
652 return nil
653 }
654}
655
656func (cr *containerReference) tryReadUID() common.Executor {
657 return cr.tryReadID("-u", func(id int) { cr.UID = id })
658}
659
660func (cr *containerReference) tryReadGID() common.Executor {
661 return cr.tryReadID("-g", func(id int) { cr.GID = id })
662}
663
664func (cr *containerReference) waitForCommand(ctx context.Context, isTerminal bool, resp client.HijackedResponse) error {
665 logger := common.Logger(ctx)
666
667 cmdResponse := make(chan error)
668
669 go func() {
670 var outWriter io.Writer
671 outWriter = cr.input.Stdout
672 if outWriter == nil {
673 outWriter = os.Stdout
674 }
675 errWriter := cr.input.Stderr
676 if errWriter == nil {
677 errWriter = os.Stderr
678 }
679
680 var err error
681 if !isTerminal || os.Getenv("NORAW") != "" {
682 _, err = stdcopy.StdCopy(outWriter, errWriter, resp.Reader)
683 } else {
684 _, err = io.Copy(outWriter, resp.Reader)
685 }
686 cmdResponse <- err
687 }()
688
689 select {
690 case <-ctx.Done():
691 // send ctrl + c
692 _, err := resp.Conn.Write([]byte{3})
693 if err != nil {
694 logger.Warnf("Failed to send CTRL+C: %+s", err)
695 }
696
697 // we return the context canceled error to prevent other steps
698 // from executing
699 return ctx.Err()
700 case err := <-cmdResponse:
701 if err != nil {
702 logger.Error(err)
703 }
704
705 return nil
706 }
707}
708
709func (cr *containerReference) CopyTarStream(ctx context.Context, destPath string, tarStream io.Reader) error {
710 if common.Dryrun(ctx) {
711 return nil
712 }
713 // Mkdir
714 buf := &bytes.Buffer{}
715 tw := tar.NewWriter(buf)
716 _ = tw.WriteHeader(&tar.Header{
717 Name: destPath,
718 Mode: 0o777,
719 Typeflag: tar.TypeDir,
720 })
721 tw.Close()
722 _, err := cr.cli.CopyToContainer(ctx, cr.id, client.CopyToContainerOptions{DestinationPath: "/", Content: buf})
723 if err != nil {
724 return fmt.Errorf("failed to mkdir to copy content to container: %w", err)
725 }
726 // Copy Content
727 _, err = cr.cli.CopyToContainer(ctx, cr.id, client.CopyToContainerOptions{DestinationPath: destPath, Content: tarStream})
728 if err != nil {
729 return fmt.Errorf("failed to copy content to container: %w", err)
730 }
731 // If this fails, then folders have wrong permissions on non root container
732 if cr.UID != 0 || cr.GID != 0 {
733 _ = cr.Exec([]string{"chown", "-R", fmt.Sprintf("%d:%d", cr.UID, cr.GID), destPath}, nil, "0", "")(ctx)
734 }
735 return nil
736}
737
738func (cr *containerReference) copyDir(dstPath string, srcPath string, useGitIgnore bool) common.Executor {
739 return func(ctx context.Context) error {
740 logger := common.Logger(ctx)
741 tarFile, err := os.CreateTemp("", "act")
742 if err != nil {
743 return err
744 }
745 logger.Debugf("Writing tarball %s from %s", tarFile.Name(), srcPath)
746 defer func(tarFile *os.File) {
747 name := tarFile.Name()
748 err := tarFile.Close()
749 if !errors.Is(err, os.ErrClosed) {
750 logger.Error(err)
751 }
752 err = os.Remove(name)
753 if err != nil {
754 logger.Error(err)
755 }
756 }(tarFile)
757 tw := tar.NewWriter(tarFile)
758
759 srcPrefix := filepath.Dir(srcPath)
760 if !strings.HasSuffix(srcPrefix, string(filepath.Separator)) {
761 srcPrefix += string(filepath.Separator)
762 }
763 logger.Debugf("Stripping prefix:%s src:%s", srcPrefix, srcPath)
764
765 var ignorer gitignore.Matcher
766 if useGitIgnore {
767 ps, err := gitignore.ReadPatterns(polyfill.New(osfs.New(srcPath)), nil)
768 if err != nil {
769 logger.Debugf("Error loading .gitignore: %v", err)
770 }
771
772 ignorer = gitignore.NewMatcher(ps)
773 }
774
775 fc := &filecollector.FileCollector{
776 Fs: &filecollector.DefaultFs{},
777 Ignorer: ignorer,
778 SrcPath: srcPath,
779 SrcPrefix: srcPrefix,
780 Handler: &filecollector.TarCollector{
781 TarWriter: tw,
782 UID: cr.UID,
783 GID: cr.GID,
784 DstDir: dstPath[1:],
785 },
786 }
787
788 err = filepath.Walk(srcPath, fc.CollectFiles(ctx, []string{}))
789 if err != nil {
790 return err
791 }
792 if err := tw.Close(); err != nil {
793 return err
794 }
795
796 logger.Debugf("Extracting content from '%s' to '%s'", tarFile.Name(), dstPath)
797 _, err = tarFile.Seek(0, 0)
798 if err != nil {
799 return fmt.Errorf("failed to seek tar archive: %w", err)
800 }
801 _, err = cr.cli.CopyToContainer(ctx, cr.id, client.CopyToContainerOptions{DestinationPath: "/", Content: tarFile})
802 if err != nil {
803 return fmt.Errorf("failed to copy content to container: %w", err)
804 }
805 return nil
806 }
807}
808
809func (cr *containerReference) copyContent(dstPath string, files ...*FileEntry) common.Executor {
810 return func(ctx context.Context) error {
811 logger := common.Logger(ctx)
812 var buf bytes.Buffer
813 tw := tar.NewWriter(&buf)
814 for _, file := range files {
815 logger.Debugf("Writing entry to tarball %s len:%d", file.Name, len(file.Body))
816 hdr := &tar.Header{
817 Name: file.Name,
818 Mode: int64(file.Mode),
819 Size: int64(len(file.Body)),
820 Uid: cr.UID,
821 Gid: cr.GID,
822 }
823 if err := tw.WriteHeader(hdr); err != nil {
824 return err
825 }
826 if _, err := tw.Write([]byte(file.Body)); err != nil {
827 return err
828 }
829 }
830 if err := tw.Close(); err != nil {
831 return err
832 }
833
834 logger.Debugf("Extracting content to '%s'", dstPath)
835 _, err := cr.cli.CopyToContainer(ctx, cr.id, client.CopyToContainerOptions{DestinationPath: dstPath, Content: &buf})
836 if err != nil {
837 return fmt.Errorf("failed to copy content to container: %w", err)
838 }
839 return nil
840 }
841}
842
843func (cr *containerReference) attach() common.Executor {
844 return func(ctx context.Context) error {
845 out, err := cr.cli.ContainerAttach(ctx, cr.id, client.ContainerAttachOptions{
846 Stream: true,
847 Stdout: true,
848 Stderr: true,
849 })
850 if err != nil {
851 return fmt.Errorf("failed to attach to container: %w", err)
852 }
853 isTerminal := term.IsTerminal(int(os.Stdout.Fd()))
854
855 var outWriter io.Writer
856 outWriter = cr.input.Stdout
857 if outWriter == nil {
858 outWriter = os.Stdout
859 }
860 errWriter := cr.input.Stderr
861 if errWriter == nil {
862 errWriter = os.Stderr
863 }
864 go func() {
865 if !isTerminal || os.Getenv("NORAW") != "" {
866 _, err = stdcopy.StdCopy(outWriter, errWriter, out.Reader)
867 } else {
868 _, err = io.Copy(outWriter, out.Reader)
869 }
870 if err != nil {
871 common.Logger(ctx).Error(err)
872 }
873 }()
874 return nil
875 }
876}
877
878func (cr *containerReference) start() common.Executor {
879 return func(ctx context.Context) error {
880 logger := common.Logger(ctx)
881 logger.Debugf("Starting container: %v", cr.id)
882
883 if _, err := cr.cli.ContainerStart(ctx, cr.id, client.ContainerStartOptions{}); err != nil {
884 return fmt.Errorf("failed to start container: %w", err)
885 }
886
887 logger.Debugf("Started container: %v", cr.id)
888 return nil
889 }
890}
891
892func (cr *containerReference) wait() common.Executor {
893 return func(ctx context.Context) error {
894 logger := common.Logger(ctx)
895 waitResult := cr.cli.ContainerWait(ctx, cr.id, client.ContainerWaitOptions{Condition: container.WaitConditionNotRunning})
896 statusCh, errCh := waitResult.Result, waitResult.Error
897 var statusCode int64
898 select {
899 case err := <-errCh:
900 if err != nil {
901 return fmt.Errorf("failed to wait for container: %w", err)
902 }
903 case status := <-statusCh:
904 statusCode = status.StatusCode
905 }
906
907 logger.Debugf("Return status: %v", statusCode)
908
909 if statusCode == 0 {
910 return nil
911 }
912
913 return fmt.Errorf("exit with `FAILURE`: %v", statusCode)
914 }
915}
916
917// convertPortSet converts a nat.PortSet to a network.PortSet
918func convertPortSet(ps nat.PortSet) network.PortSet {
919 if ps == nil {
920 return nil
921 }
922 result := make(network.PortSet, len(ps))
923 for p := range ps {
924 np, err := network.ParsePort(string(p))
925 if err == nil {
926 result[np] = struct{}{}
927 }
928 }
929 return result
930}
931
932// convertPortMap converts a nat.PortMap to a network.PortMap
933func convertPortMap(pm nat.PortMap) network.PortMap {
934 if pm == nil {
935 return nil
936 }
937 result := make(network.PortMap, len(pm))
938 for p, bindings := range pm {
939 np, err := network.ParsePort(string(p))
940 if err != nil {
941 continue
942 }
943 nbs := make([]network.PortBinding, len(bindings))
944 for i, b := range bindings {
945 nbs[i] = network.PortBinding{
946 HostIP: netip.Addr{},
947 HostPort: b.HostPort,
948 }
949 if b.HostIP != "" {
950 if addr, err := netip.ParseAddr(b.HostIP); err == nil {
951 nbs[i].HostIP = addr
952 }
953 }
954 }
955 result[np] = nbs
956 }
957 return result
958}
Deleted act/pkg/container/docker_socket.go
@@ -1,134 +0,0 @@
1package container
2
3import (
4 "fmt"
5 "os"
6 "path/filepath"
7 "strings"
8
9 log "github.com/sirupsen/logrus"
10)
11
12var CommonSocketLocations = []string{
13 "/var/run/docker.sock",
14 "/run/podman/podman.sock",
15 "$HOME/.colima/docker.sock",
16 "$XDG_RUNTIME_DIR/docker.sock",
17 "$XDG_RUNTIME_DIR/podman/podman.sock",
18 `\\.\pipe\docker_engine`,
19 "$HOME/.docker/run/docker.sock",
20}
21
22// returns socket URI or false if not found any
23func socketLocation() (string, bool) {
24 if dockerHost, exists := os.LookupEnv("DOCKER_HOST"); exists {
25 return dockerHost, true
26 }
27
28 for _, p := range CommonSocketLocations {
29 if _, err := os.Lstat(os.ExpandEnv(p)); err == nil {
30 if strings.HasPrefix(p, `\\.\`) {
31 return "npipe://" + filepath.ToSlash(os.ExpandEnv(p)), true
32 }
33 return "unix://" + filepath.ToSlash(os.ExpandEnv(p)), true
34 }
35 }
36
37 return "", false
38}
39
40// This function, `isDockerHostURI`, takes a string argument `daemonPath`. It checks if the
41// `daemonPath` is a valid Docker host URI. It does this by checking if the scheme of the URI (the
42// part before "://") contains only alphabetic characters. If it does, the function returns true,
43// indicating that the `daemonPath` is a Docker host URI. If it doesn't, or if the "://" delimiter
44// is not found in the `daemonPath`, the function returns false.
45func isDockerHostURI(daemonPath string) bool {
46 if protoIndex := strings.Index(daemonPath, "://"); protoIndex != -1 {
47 scheme := daemonPath[:protoIndex]
48 if strings.IndexFunc(scheme, func(r rune) bool {
49 return (r < 'a' || r > 'z') && (r < 'A' || r > 'Z')
50 }) == -1 {
51 return true
52 }
53 }
54 return false
55}
56
57type SocketAndHost struct {
58 Socket string
59 Host string
60}
61
62func GetSocketAndHost(containerSocket string) (SocketAndHost, error) {
63 log.Debugf("Handling container host and socket")
64
65 // Prefer DOCKER_HOST, don't override it
66 dockerHost, hasDockerHost := socketLocation()
67 socketHost := SocketAndHost{Socket: containerSocket, Host: dockerHost}
68
69 // ** socketHost.Socket cases **
70 // Case 1: User does _not_ want to mount a daemon socket (passes a dash)
71 // Case 2: User passes a filepath to the socket; is that even valid?
72 // Case 3: User passes a valid socket; do nothing
73 // Case 4: User omitted the flag; set a sane default
74
75 // ** DOCKER_HOST cases **
76 // Case A: DOCKER_HOST is set; use it, i.e. do nothing
77 // Case B: DOCKER_HOST is empty; use sane defaults
78
79 // Set host for sanity's sake, when the socket isn't useful
80 if !hasDockerHost && (socketHost.Socket == "-" || !isDockerHostURI(socketHost.Socket) || socketHost.Socket == "") {
81 // Cases: 1B, 2B, 4B
82 socket, found := socketLocation()
83 socketHost.Host = socket
84 hasDockerHost = found
85 }
86
87 // A - (dash) in socketHost.Socket means don't mount, preserve this value
88 // otherwise if socketHost.Socket is a filepath don't use it as socket
89 // Exit early if we're in an invalid state (e.g. when no DOCKER_HOST and user supplied "-", a dash or omitted)
90 if !hasDockerHost && socketHost.Socket != "" && !isDockerHostURI(socketHost.Socket) {
91 // Cases: 1B, 2B
92 // Should we early-exit here, since there is no host nor socket to talk to?
93 return SocketAndHost{}, fmt.Errorf("DOCKER_HOST was not set, couldn't be found in the usual locations, and the container daemon socket ('%s') is invalid", socketHost.Socket)
94 }
95
96 // Default to DOCKER_HOST if set
97 if socketHost.Socket == "" && hasDockerHost {
98 // Cases: 4A
99 log.Debugf("Defaulting container socket to DOCKER_HOST")
100 socketHost.Socket = socketHost.Host
101 }
102 // Set sane default socket location if user omitted it
103 if socketHost.Socket == "" {
104 // Cases: 4B
105 socket, _ := socketLocation()
106 // socket is empty if it isn't found, so assignment here is at worst a no-op
107 log.Debugf("Defaulting container socket to default '%s'", socket)
108 socketHost.Socket = socket
109 }
110
111 // Exit if both the DOCKER_HOST and socket are fulfilled
112 if hasDockerHost {
113 // Cases: 1A, 2A, 3A, 4A
114 if !isDockerHostURI(socketHost.Socket) {
115 // Cases: 1A, 2A
116 log.Debugf("DOCKER_HOST is set, but socket is invalid '%s'", socketHost.Socket)
117 }
118 return socketHost, nil
119 }
120
121 // Set a sane DOCKER_HOST default if we can
122 if isDockerHostURI(socketHost.Socket) {
123 // Cases: 3B
124 log.Debugf("Setting DOCKER_HOST to container socket '%s'", socketHost.Socket)
125 socketHost.Host = socketHost.Socket
126 // Both DOCKER_HOST and container socket are valid; short-circuit exit
127 return socketHost, nil
128 }
129
130 // Here there is no DOCKER_HOST _and_ the supplied container socket is not a valid URI (either invalid or a file path)
131 // Cases: 2B <- but is already handled at the top
132 // I.e. this path should never be taken
133 return SocketAndHost{}, fmt.Errorf("no DOCKER_HOST and an invalid container socket '%s'", socketHost.Socket)
134}
Deleted act/pkg/container/docker_stub.go
@@ -1,69 +0,0 @@
1//go:build WITHOUT_DOCKER || !(linux || darwin || windows || netbsd)
2
3package container
4
5import (
6 "context"
7 "runtime"
8
9 "github.com/moby/moby/api/types/system"
10 "github.com/nektos/act/pkg/common"
11 "github.com/pkg/errors"
12)
13
14// ImageExistsLocally returns a boolean indicating if an image with the
15// requested name, tag and architecture exists in the local docker image store
16func ImageExistsLocally(ctx context.Context, imageName string, platform string) (bool, error) {
17 return false, errors.New("Unsupported Operation")
18}
19
20// RemoveImage removes image from local store, the function is used to run different
21// container image architectures
22func RemoveImage(ctx context.Context, imageName string, force bool, pruneChildren bool) (bool, error) {
23 return false, errors.New("Unsupported Operation")
24}
25
26// NewDockerBuildExecutor function to create a run executor for the container
27func NewDockerBuildExecutor(input NewDockerBuildExecutorInput) common.Executor {
28 return func(ctx context.Context) error {
29 return errors.New("Unsupported Operation")
30 }
31}
32
33// NewDockerPullExecutor function to create a run executor for the container
34func NewDockerPullExecutor(input NewDockerPullExecutorInput) common.Executor {
35 return func(ctx context.Context) error {
36 return errors.New("Unsupported Operation")
37 }
38}
39
40// NewContainer creates a reference to a container
41func NewContainer(input *NewContainerInput) ExecutionsEnvironment {
42 return nil
43}
44
45func RunnerArch(ctx context.Context) string {
46 return runtime.GOOS
47}
48
49func GetHostInfo(ctx context.Context) (info system.Info, err error) {
50 return system.Info{}, nil
51}
52
53func NewDockerVolumeRemoveExecutor(volume string, force bool) common.Executor {
54 return func(ctx context.Context) error {
55 return nil
56 }
57}
58
59func NewDockerNetworkCreateExecutor(name string) common.Executor {
60 return func(ctx context.Context) error {
61 return nil
62 }
63}
64
65func NewDockerNetworkRemoveExecutor(name string) common.Executor {
66 return func(ctx context.Context) error {
67 return nil
68 }
69}
Deleted act/pkg/container/docker_volume.go
@@ -1,54 +0,0 @@
1//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows || netbsd))
2
3package container
4
5import (
6 "context"
7
8 "github.com/moby/moby/client"
9 "github.com/nektos/act/pkg/common"
10)
11
12func NewDockerVolumeRemoveExecutor(volumeName string, force bool) common.Executor {
13 return func(ctx context.Context) error {
14 cli, err := GetDockerClient(ctx)
15 if err != nil {
16 return err
17 }
18 defer cli.Close()
19
20 list, err := cli.VolumeList(ctx, client.VolumeListOptions{})
21 if err != nil {
22 return err
23 }
24
25 for _, vol := range list.Items {
26 if vol.Name == volumeName {
27 return removeExecutor(volumeName, force)(ctx)
28 }
29 }
30
31 // Volume not found - do nothing
32 return nil
33 }
34}
35
36func removeExecutor(volume string, force bool) common.Executor {
37 return func(ctx context.Context) error {
38 logger := common.Logger(ctx)
39 logger.Debugf("%sdocker volume rm %s", logPrefix, volume)
40
41 if common.Dryrun(ctx) {
42 return nil
43 }
44
45 cli, err := GetDockerClient(ctx)
46 if err != nil {
47 return err
48 }
49 defer cli.Close()
50
51 _, err = cli.VolumeRemove(ctx, volume, client.VolumeRemoveOptions{Force: force})
52 return err
53 }
54}
Deleted act/pkg/container/executions_environment.go
@@ -1,15 +0,0 @@
1package container
2
3import "context"
4
5type ExecutionsEnvironment interface {
6 Container
7 ToContainerPath(string) string
8 GetActPath() string
9 GetPathVariableName() string
10 DefaultPathVariable() string
11 JoinPathVariable(...string) string
12 GetRunnerContext(ctx context.Context) map[string]interface{}
13 // On windows PATH and Path are the same key
14 IsEnvironmentCaseInsensitive() bool
15}
Deleted act/pkg/container/host_environment.go
@@ -1,470 +0,0 @@
1package container
2
3import (
4 "archive/tar"
5 "bytes"
6 "context"
7 "errors"
8 "fmt"
9 "io"
10 "io/fs"
11 "os"
12 "os/exec"
13 "path/filepath"
14 "runtime"
15 "strings"
16 "time"
17
18 "github.com/go-git/go-billy/v5/helper/polyfill"
19 "github.com/go-git/go-billy/v5/osfs"
20 "github.com/go-git/go-git/v5/plumbing/format/gitignore"
21 "golang.org/x/term"
22
23 "github.com/nektos/act/pkg/common"
24 "github.com/nektos/act/pkg/filecollector"
25 "github.com/nektos/act/pkg/lookpath"
26)
27
28type HostEnvironment struct {
29 Path string
30 TmpDir string
31 ToolCache string
32 Workdir string
33 ActPath string
34 CleanUp func()
35 StdOut io.Writer
36}
37
38func (e *HostEnvironment) Create(_ []string, _ []string) common.Executor {
39 return func(_ context.Context) error {
40 return nil
41 }
42}
43
44func (e *HostEnvironment) Close() common.Executor {
45 return func(_ context.Context) error {
46 return nil
47 }
48}
49
50func (e *HostEnvironment) Copy(destPath string, files ...*FileEntry) common.Executor {
51 return func(_ context.Context) error {
52 for _, f := range files {
53 if err := os.MkdirAll(filepath.Dir(filepath.Join(destPath, f.Name)), 0o777); err != nil {
54 return err
55 }
56 if err := os.WriteFile(filepath.Join(destPath, f.Name), []byte(f.Body), fs.FileMode(f.Mode)); err != nil {
57 return err
58 }
59 }
60 return nil
61 }
62}
63
64func (e *HostEnvironment) CopyTarStream(ctx context.Context, destPath string, tarStream io.Reader) error {
65 if err := os.RemoveAll(destPath); err != nil {
66 return err
67 }
68 tr := tar.NewReader(tarStream)
69 cp := &filecollector.CopyCollector{
70 DstDir: destPath,
71 }
72 for {
73 ti, err := tr.Next()
74 if errors.Is(err, io.EOF) {
75 return nil
76 } else if err != nil {
77 return err
78 }
79 if ti.FileInfo().IsDir() {
80 continue
81 }
82 if ctx.Err() != nil {
83 return fmt.Errorf("CopyTarStream has been cancelled")
84 }
85 if err := cp.WriteFile(ti.Name, ti.FileInfo(), ti.Linkname, tr); err != nil {
86 return err
87 }
88 }
89}
90
91func (e *HostEnvironment) CopyDir(destPath string, srcPath string, useGitIgnore bool) common.Executor {
92 return func(ctx context.Context) error {
93 logger := common.Logger(ctx)
94 srcPrefix := filepath.Dir(srcPath)
95 if !strings.HasSuffix(srcPrefix, string(filepath.Separator)) {
96 srcPrefix += string(filepath.Separator)
97 }
98 logger.Debugf("Stripping prefix:%s src:%s", srcPrefix, srcPath)
99 var ignorer gitignore.Matcher
100 if useGitIgnore {
101 ps, err := gitignore.ReadPatterns(polyfill.New(osfs.New(srcPath)), nil)
102 if err != nil {
103 logger.Debugf("Error loading .gitignore: %v", err)
104 }
105
106 ignorer = gitignore.NewMatcher(ps)
107 }
108 fc := &filecollector.FileCollector{
109 Fs: &filecollector.DefaultFs{},
110 Ignorer: ignorer,
111 SrcPath: srcPath,
112 SrcPrefix: srcPrefix,
113 Handler: &filecollector.CopyCollector{
114 DstDir: destPath,
115 },
116 }
117 return filepath.Walk(srcPath, fc.CollectFiles(ctx, []string{}))
118 }
119}
120
121func (e *HostEnvironment) GetContainerArchive(ctx context.Context, srcPath string) (io.ReadCloser, error) {
122 buf := &bytes.Buffer{}
123 tw := tar.NewWriter(buf)
124 defer tw.Close()
125 srcPath = filepath.Clean(srcPath)
126 fi, err := os.Lstat(srcPath)
127 if err != nil {
128 return nil, err
129 }
130 tc := &filecollector.TarCollector{
131 TarWriter: tw,
132 }
133 if fi.IsDir() {
134 srcPrefix := srcPath
135 if !strings.HasSuffix(srcPrefix, string(filepath.Separator)) {
136 srcPrefix += string(filepath.Separator)
137 }
138 fc := &filecollector.FileCollector{
139 Fs: &filecollector.DefaultFs{},
140 SrcPath: srcPath,
141 SrcPrefix: srcPrefix,
142 Handler: tc,
143 }
144 err = filepath.Walk(srcPath, fc.CollectFiles(ctx, []string{}))
145 if err != nil {
146 return nil, err
147 }
148 } else {
149 var f io.ReadCloser
150 var linkname string
151 if fi.Mode()&fs.ModeSymlink != 0 {
152 linkname, err = os.Readlink(srcPath)
153 if err != nil {
154 return nil, err
155 }
156 } else {
157 f, err = os.Open(srcPath)
158 if err != nil {
159 return nil, err
160 }
161 defer f.Close()
162 }
163 err := tc.WriteFile(fi.Name(), fi, linkname, f)
164 if err != nil {
165 return nil, err
166 }
167 }
168 return io.NopCloser(buf), nil
169}
170
171func (e *HostEnvironment) Pull(_ bool) common.Executor {
172 return func(_ context.Context) error {
173 return nil
174 }
175}
176
177func (e *HostEnvironment) Start(_ bool) common.Executor {
178 return func(_ context.Context) error {
179 return nil
180 }
181}
182
183type ptyWriter struct {
184 Out io.Writer
185 AutoStop bool
186 dirtyLine bool
187}
188
189func (w *ptyWriter) Write(buf []byte) (int, error) {
190 if w.AutoStop && len(buf) > 0 && buf[len(buf)-1] == 4 {
191 n, err := w.Out.Write(buf[:len(buf)-1])
192 if err != nil {
193 return n, err
194 }
195 if w.dirtyLine || len(buf) > 1 && buf[len(buf)-2] != '\n' {
196 _, _ = w.Out.Write([]byte("\n"))
197 return n, io.EOF
198 }
199 return n, io.EOF
200 }
201 w.dirtyLine = strings.LastIndex(string(buf), "\n") < len(buf)-1
202 return w.Out.Write(buf)
203}
204
205type localEnv struct {
206 env map[string]string
207}
208
209func (l *localEnv) Getenv(name string) string {
210 if runtime.GOOS == "windows" {
211 for k, v := range l.env {
212 if strings.EqualFold(name, k) {
213 return v
214 }
215 }
216 return ""
217 }
218 return l.env[name]
219}
220
221func lookupPathHost(cmd string, env map[string]string, writer io.Writer) (string, error) {
222 f, err := lookpath.LookPath2(cmd, &localEnv{env: env})
223 if err != nil {
224 err := "Cannot find: " + fmt.Sprint(cmd) + " in PATH"
225 if _, _err := writer.Write([]byte(err + "\n")); _err != nil {
226 return "", fmt.Errorf("%v: %w", err, _err)
227 }
228 return "", errors.New(err)
229 }
230 return f, nil
231}
232
233func setupPty(cmd *exec.Cmd, cmdline string) (*os.File, *os.File, error) {
234 ppty, tty, err := openPty()
235 if err != nil {
236 return nil, nil, err
237 }
238 if term.IsTerminal(int(tty.Fd())) {
239 _, err := term.MakeRaw(int(tty.Fd()))
240 if err != nil {
241 ppty.Close()
242 tty.Close()
243 return nil, nil, err
244 }
245 }
246 cmd.Stdin = tty
247 cmd.Stdout = tty
248 cmd.Stderr = tty
249 cmd.SysProcAttr = getSysProcAttr(cmdline, true)
250 return ppty, tty, nil
251}
252
253func writeKeepAlive(ppty io.Writer) {
254 c := 1
255 var err error
256 for c == 1 && err == nil {
257 c, err = ppty.Write([]byte{4})
258 <-time.After(time.Second)
259 }
260}
261
262func copyPtyOutput(writer io.Writer, ppty io.Reader, finishLog context.CancelFunc) {
263 defer func() {
264 finishLog()
265 }()
266 if _, err := io.Copy(writer, ppty); err != nil {
267 return
268 }
269}
270
271func (e *HostEnvironment) UpdateFromImageEnv(_ *map[string]string) common.Executor {
272 return func(_ context.Context) error {
273 return nil
274 }
275}
276
277func getEnvListFromMap(env map[string]string) []string {
278 envList := make([]string, 0)
279 for k, v := range env {
280 envList = append(envList, fmt.Sprintf("%s=%s", k, v))
281 }
282 return envList
283}
284
285func (e *HostEnvironment) exec(ctx context.Context, command []string, cmdline string, env map[string]string, _, workdir string) error {
286 envList := getEnvListFromMap(env)
287 var wd string
288 if workdir != "" {
289 if filepath.IsAbs(workdir) {
290 wd = workdir
291 } else {
292 wd = filepath.Join(e.Path, workdir)
293 }
294 } else {
295 wd = e.Path
296 }
297 f, err := lookupPathHost(command[0], env, e.StdOut)
298 if err != nil {
299 return err
300 }
301 cmd := exec.CommandContext(ctx, f)
302 cmd.Path = f
303 cmd.Args = command
304 cmd.Stdin = nil
305 cmd.Stdout = e.StdOut
306 cmd.Env = envList
307 cmd.Stderr = e.StdOut
308 cmd.Dir = wd
309 cmd.SysProcAttr = getSysProcAttr(cmdline, false)
310 var ppty *os.File
311 var tty *os.File
312 defer func() {
313 if ppty != nil {
314 ppty.Close()
315 }
316 if tty != nil {
317 tty.Close()
318 }
319 }()
320 if true /* allocate Terminal */ {
321 var err error
322 ppty, tty, err = setupPty(cmd, cmdline)
323 if err != nil {
324 common.Logger(ctx).Debugf("Failed to setup Pty %v\n", err.Error())
325 }
326 }
327 writer := &ptyWriter{Out: e.StdOut}
328 logctx, finishLog := context.WithCancel(context.Background())
329 if ppty != nil {
330 go copyPtyOutput(writer, ppty, finishLog)
331 } else {
332 finishLog()
333 }
334 if ppty != nil {
335 go writeKeepAlive(ppty)
336 }
337 err = cmd.Run()
338 if err != nil {
339 return err
340 }
341 if tty != nil {
342 writer.AutoStop = true
343 if _, err := tty.Write([]byte("\x04")); err != nil {
344 common.Logger(ctx).Debug("Failed to write EOT")
345 }
346 }
347 <-logctx.Done()
348
349 if ppty != nil {
350 ppty.Close()
351 ppty = nil
352 }
353 return err
354}
355
356func (e *HostEnvironment) Exec(command []string /*cmdline string, */, env map[string]string, user, workdir string) common.Executor {
357 return e.ExecWithCmdLine(command, "", env, user, workdir)
358}
359
360func (e *HostEnvironment) ExecWithCmdLine(command []string, cmdline string, env map[string]string, user, workdir string) common.Executor {
361 return func(ctx context.Context) error {
362 if err := e.exec(ctx, command, cmdline, env, user, workdir); err != nil {
363 select {
364 case <-ctx.Done():
365 return fmt.Errorf("this step has been cancelled: %w", err)
366 default:
367 return err
368 }
369 }
370 return nil
371 }
372}
373
374func (e *HostEnvironment) UpdateFromEnv(srcPath string, env *map[string]string) common.Executor {
375 return parseEnvFile(e, srcPath, env)
376}
377
378func (e *HostEnvironment) Remove() common.Executor {
379 return func(_ context.Context) error {
380 if e.CleanUp != nil {
381 e.CleanUp()
382 }
383 return os.RemoveAll(e.Path)
384 }
385}
386
387func (e *HostEnvironment) ToContainerPath(path string) string {
388 if bp, err := filepath.Rel(e.Workdir, path); err != nil {
389 return filepath.Join(e.Path, bp)
390 } else if filepath.Clean(e.Workdir) == filepath.Clean(path) {
391 return e.Path
392 }
393 return path
394}
395
396func (e *HostEnvironment) GetActPath() string {
397 actPath := e.ActPath
398 if runtime.GOOS == "windows" {
399 actPath = strings.ReplaceAll(actPath, "\\", "/")
400 }
401 return actPath
402}
403
404func (*HostEnvironment) GetPathVariableName() string {
405 if runtime.GOOS == "plan9" {
406 return "path"
407 } else if runtime.GOOS == "windows" {
408 return "Path" // Actually we need a case insensitive map
409 }
410 return "PATH"
411}
412
413func (e *HostEnvironment) DefaultPathVariable() string {
414 v, _ := os.LookupEnv(e.GetPathVariableName())
415 return v
416}
417
418func (*HostEnvironment) JoinPathVariable(paths ...string) string {
419 return strings.Join(paths, string(filepath.ListSeparator))
420}
421
422// Reference for Arch values for runner.arch
423// https://docs.github.com/en/actions/learn-github-actions/contexts#runner-context
424func goArchToActionArch(arch string) string {
425 archMapper := map[string]string{
426 "amd64": "X64",
427 "x86_64": "X64",
428 "386": "X86",
429 "aarch64": "ARM64",
430 }
431 if arch, ok := archMapper[arch]; ok {
432 return arch
433 }
434 return arch
435}
436
437func goOsToActionOs(os string) string {
438 osMapper := map[string]string{
439 "linux": "Linux",
440 "windows": "Windows",
441 "darwin": "macOS",
442 }
443 if os, ok := osMapper[os]; ok {
444 return os
445 }
446 return os
447}
448
449func (e *HostEnvironment) GetRunnerContext(_ context.Context) map[string]interface{} {
450 return map[string]interface{}{
451 "os": goOsToActionOs(runtime.GOOS),
452 "arch": goArchToActionArch(runtime.GOARCH),
453 "temp": e.TmpDir,
454 "tool_cache": e.ToolCache,
455 }
456}
457
458func (e *HostEnvironment) GetHealth(_ context.Context) Health {
459 return HealthHealthy
460}
461
462func (e *HostEnvironment) ReplaceLogWriter(stdout io.Writer, _ io.Writer) (io.Writer, io.Writer) {
463 org := e.StdOut
464 e.StdOut = stdout
465 return org, org
466}
467
468func (*HostEnvironment) IsEnvironmentCaseInsensitive() bool {
469 return runtime.GOOS == "windows"
470}
Deleted act/pkg/container/linux_container_environment_extensions.go
@@ -1,77 +0,0 @@
1package container
2
3import (
4 "context"
5 "path/filepath"
6 "regexp"
7 "runtime"
8 "strings"
9
10 log "github.com/sirupsen/logrus"
11)
12
13type LinuxContainerEnvironmentExtensions struct {
14}
15
16// Resolves the equivalent host path inside the container
17// This is required for windows and WSL 2 to translate things like C:\Users\Myproject to /mnt/users/Myproject
18// For use in docker volumes and binds
19func (*LinuxContainerEnvironmentExtensions) ToContainerPath(path string) string {
20 if runtime.GOOS == "windows" && strings.Contains(path, "/") {
21 log.Error("You cannot specify linux style local paths (/mnt/etc) on Windows as it does not understand them.")
22 return ""
23 }
24
25 abspath, err := filepath.Abs(path)
26 if err != nil {
27 log.Error(err)
28 return ""
29 }
30
31 // Test if the path is a windows path
32 windowsPathRegex := regexp.MustCompile(`^([a-zA-Z]):\\(.+)$`)
33 windowsPathComponents := windowsPathRegex.FindStringSubmatch(abspath)
34
35 // Return as-is if no match
36 if windowsPathComponents == nil {
37 return abspath
38 }
39
40 // Convert to WSL2-compatible path if it is a windows path
41 // NOTE: Cannot use filepath because it will use the wrong path separators assuming we want the path to be windows
42 // based if running on Windows, and because we are feeding this to Docker, GoLang auto-path-translate doesn't work.
43 driveLetter := strings.ToLower(windowsPathComponents[1])
44 translatedPath := strings.ReplaceAll(windowsPathComponents[2], `\`, `/`)
45 // Should make something like /mnt/c/Users/person/My Folder/MyActProject
46 result := strings.Join([]string{"/mnt", driveLetter, translatedPath}, `/`)
47 return result
48}
49
50func (*LinuxContainerEnvironmentExtensions) GetActPath() string {
51 return "/var/run/act"
52}
53
54func (*LinuxContainerEnvironmentExtensions) GetPathVariableName() string {
55 return "PATH"
56}
57
58func (*LinuxContainerEnvironmentExtensions) DefaultPathVariable() string {
59 return "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
60}
61
62func (*LinuxContainerEnvironmentExtensions) JoinPathVariable(paths ...string) string {
63 return strings.Join(paths, ":")
64}
65
66func (*LinuxContainerEnvironmentExtensions) GetRunnerContext(ctx context.Context) map[string]interface{} {
67 return map[string]interface{}{
68 "os": "Linux",
69 "arch": RunnerArch(ctx),
70 "temp": "/tmp",
71 "tool_cache": "/opt/hostedtoolcache",
72 }
73}
74
75func (*LinuxContainerEnvironmentExtensions) IsEnvironmentCaseInsensitive() bool {
76 return false
77}
Deleted act/pkg/container/parse_env_file.go
@@ -1,69 +0,0 @@
1package container
2
3import (
4 "archive/tar"
5 "bufio"
6 "context"
7 "fmt"
8 "io"
9 "strings"
10
11 "github.com/nektos/act/pkg/common"
12)
13
14func parseEnvFile(e Container, srcPath string, env *map[string]string) common.Executor {
15 localEnv := *env
16 return func(ctx context.Context) error {
17 envTar, err := e.GetContainerArchive(ctx, srcPath)
18 if err != nil {
19 return nil
20 }
21 defer envTar.Close()
22 reader := tar.NewReader(envTar)
23 _, err = reader.Next()
24 if err != nil && err != io.EOF {
25 return err
26 }
27 s := bufio.NewScanner(reader)
28 s.Buffer(nil, 1024*1024*1024) // increase buffer to 1GB to avoid scanner buffer overflow
29 firstLine := true
30 for s.Scan() {
31 line := s.Text()
32 if firstLine {
33 firstLine = false
34 // skip utf8 bom, powershell 5 legacy uses it for utf8
35 if len(line) >= 3 && line[0] == 239 && line[1] == 187 && line[2] == 191 {
36 line = line[3:]
37 }
38 }
39 singleLineEnv := strings.Index(line, "=")
40 multiLineEnv := strings.Index(line, "<<")
41 if singleLineEnv != -1 && (multiLineEnv == -1 || singleLineEnv < multiLineEnv) {
42 localEnv[line[:singleLineEnv]] = line[singleLineEnv+1:]
43 } else if multiLineEnv != -1 {
44 multiLineEnvContent := ""
45 multiLineEnvDelimiter := line[multiLineEnv+2:]
46 delimiterFound := false
47 for s.Scan() {
48 content := s.Text()
49 if content == multiLineEnvDelimiter {
50 delimiterFound = true
51 break
52 }
53 if multiLineEnvContent != "" {
54 multiLineEnvContent += "\n"
55 }
56 multiLineEnvContent += content
57 }
58 if !delimiterFound {
59 return fmt.Errorf("invalid format delimiter '%v' not found before end of file", multiLineEnvDelimiter)
60 }
61 localEnv[line[:multiLineEnv]] = multiLineEnvContent
62 } else {
63 return fmt.Errorf("invalid format '%v', expected a line with '=' or '<<'", line)
64 }
65 }
66 env = &localEnv
67 return s.Err()
68 }
69}
Deleted act/pkg/container/util.go
@@ -1,26 +0,0 @@
1//go:build (!windows && !plan9 && !openbsd) || (!windows && !plan9 && !mips64)
2
3package container
4
5import (
6 "os"
7 "syscall"
8
9 "github.com/creack/pty"
10)
11
12func getSysProcAttr(_ string, tty bool) *syscall.SysProcAttr {
13 if tty {
14 return &syscall.SysProcAttr{
15 Setsid: true,
16 Setctty: true,
17 }
18 }
19 return &syscall.SysProcAttr{
20 Setpgid: true,
21 }
22}
23
24func openPty() (*os.File, *os.File, error) {
25 return pty.Open()
26}
Deleted act/pkg/container/util_openbsd_mips64.go
@@ -1,17 +0,0 @@
1package container
2
3import (
4 "errors"
5 "os"
6 "syscall"
7)
8
9func getSysProcAttr(cmdLine string, tty bool) *syscall.SysProcAttr {
10 return &syscall.SysProcAttr{
11 Setpgid: true,
12 }
13}
14
15func openPty() (*os.File, *os.File, error) {
16 return nil, nil, errors.New("Unsupported")
17}
Deleted act/pkg/container/util_plan9.go
@@ -1,17 +0,0 @@
1package container
2
3import (
4 "errors"
5 "os"
6 "syscall"
7)
8
9func getSysProcAttr(cmdLine string, tty bool) *syscall.SysProcAttr {
10 return &syscall.SysProcAttr{
11 Rfork: syscall.RFNOTEG,
12 }
13}
14
15func openPty() (*os.File, *os.File, error) {
16 return nil, nil, errors.New("Unsupported")
17}
Deleted act/pkg/container/util_windows.go
@@ -1,15 +0,0 @@
1package container
2
3import (
4 "errors"
5 "os"
6 "syscall"
7)
8
9func getSysProcAttr(cmdLine string, tty bool) *syscall.SysProcAttr {
10 return &syscall.SysProcAttr{CmdLine: cmdLine, CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP}
11}
12
13func openPty() (*os.File, *os.File, error) {
14 return nil, nil, errors.New("Unsupported")
15}
Deleted act/pkg/exprparser/functions.go
@@ -1,295 +0,0 @@
1package exprparser
2
3import (
4 "crypto/sha256"
5 "encoding/hex"
6 "encoding/json"
7 "fmt"
8 "io"
9 "io/fs"
10 "os"
11 "path/filepath"
12 "reflect"
13 "strconv"
14 "strings"
15
16 "github.com/go-git/go-git/v5/plumbing/format/gitignore"
17
18 "github.com/nektos/act/pkg/model"
19 "github.com/rhysd/actionlint"
20)
21
22func (impl *interperterImpl) contains(search, item reflect.Value) (bool, error) {
23 switch search.Kind() {
24 case reflect.String, reflect.Int, reflect.Float64, reflect.Bool, reflect.Invalid:
25 return strings.Contains(
26 strings.ToLower(impl.coerceToString(search).String()),
27 strings.ToLower(impl.coerceToString(item).String()),
28 ), nil
29
30 case reflect.Slice:
31 for i := 0; i < search.Len(); i++ {
32 arrayItem := search.Index(i).Elem()
33 result, err := impl.compareValues(arrayItem, item, actionlint.CompareOpNodeKindEq)
34 if err != nil {
35 return false, err
36 }
37
38 if isEqual, ok := result.(bool); ok && isEqual {
39 return true, nil
40 }
41 }
42 }
43
44 return false, nil
45}
46
47func (impl *interperterImpl) startsWith(searchString, searchValue reflect.Value) (bool, error) {
48 return strings.HasPrefix(
49 strings.ToLower(impl.coerceToString(searchString).String()),
50 strings.ToLower(impl.coerceToString(searchValue).String()),
51 ), nil
52}
53
54func (impl *interperterImpl) endsWith(searchString, searchValue reflect.Value) (bool, error) {
55 return strings.HasSuffix(
56 strings.ToLower(impl.coerceToString(searchString).String()),
57 strings.ToLower(impl.coerceToString(searchValue).String()),
58 ), nil
59}
60
61const (
62 passThrough = iota
63 bracketOpen
64 bracketClose
65)
66
67func (impl *interperterImpl) format(str reflect.Value, replaceValue ...reflect.Value) (string, error) {
68 input := impl.coerceToString(str).String()
69 output := ""
70 replacementIndex := ""
71
72 state := passThrough
73 for _, character := range input {
74 switch state {
75 case passThrough: // normal buffer output
76 switch character {
77 case '{':
78 state = bracketOpen
79
80 case '}':
81 state = bracketClose
82
83 default:
84 output += string(character)
85 }
86
87 case bracketOpen: // found {
88 switch character {
89 case '{':
90 output += "{"
91 replacementIndex = ""
92 state = passThrough
93
94 case '}':
95 index, err := strconv.ParseInt(replacementIndex, 10, 32)
96 if err != nil {
97 return "", fmt.Errorf("The following format string is invalid: '%s'", input)
98 }
99
100 replacementIndex = ""
101
102 if len(replaceValue) <= int(index) {
103 return "", fmt.Errorf("The following format string references more arguments than were supplied: '%s'", input)
104 }
105
106 output += impl.coerceToString(replaceValue[index]).String()
107
108 state = passThrough
109
110 default:
111 replacementIndex += string(character)
112 }
113
114 case bracketClose: // found }
115 switch character {
116 case '}':
117 output += "}"
118 replacementIndex = ""
119 state = passThrough
120
121 default:
122 panic("Invalid format parser state")
123 }
124 }
125 }
126
127 if state != passThrough {
128 switch state {
129 case bracketOpen:
130 return "", fmt.Errorf("Unclosed brackets. The following format string is invalid: '%s'", input)
131
132 case bracketClose:
133 return "", fmt.Errorf("Closing bracket without opening one. The following format string is invalid: '%s'", input)
134 }
135 }
136
137 return output, nil
138}
139
140func (impl *interperterImpl) join(array reflect.Value, sep reflect.Value) (string, error) {
141 separator := impl.coerceToString(sep).String()
142 switch array.Kind() {
143 case reflect.Slice:
144 var items []string
145 for i := 0; i < array.Len(); i++ {
146 items = append(items, impl.coerceToString(array.Index(i).Elem()).String())
147 }
148
149 return strings.Join(items, separator), nil
150 default:
151 return strings.Join([]string{impl.coerceToString(array).String()}, separator), nil
152 }
153}
154
155func (impl *interperterImpl) toJSON(value reflect.Value) (string, error) {
156 if value.Kind() == reflect.Invalid {
157 return "null", nil
158 }
159
160 json, err := json.MarshalIndent(value.Interface(), "", " ")
161 if err != nil {
162 return "", fmt.Errorf("Cannot convert value to JSON. Cause: %v", err)
163 }
164
165 return string(json), nil
166}
167
168func (impl *interperterImpl) fromJSON(value reflect.Value) (interface{}, error) {
169 if value.Kind() != reflect.String {
170 return nil, fmt.Errorf("Cannot parse non-string type %v as JSON", value.Kind())
171 }
172
173 var data interface{}
174
175 err := json.Unmarshal([]byte(value.String()), &data)
176 if err != nil {
177 return nil, fmt.Errorf("Invalid JSON: %v", err)
178 }
179
180 return data, nil
181}
182
183func (impl *interperterImpl) hashFiles(paths ...reflect.Value) (string, error) {
184 var ps []gitignore.Pattern
185
186 const cwdPrefix = "." + string(filepath.Separator)
187 const excludeCwdPrefix = "!" + cwdPrefix
188 for _, path := range paths {
189 if path.Kind() == reflect.String {
190 cleanPath := path.String()
191 if strings.HasPrefix(cleanPath, cwdPrefix) {
192 cleanPath = cleanPath[len(cwdPrefix):]
193 } else if strings.HasPrefix(cleanPath, excludeCwdPrefix) {
194 cleanPath = "!" + cleanPath[len(excludeCwdPrefix):]
195 }
196 ps = append(ps, gitignore.ParsePattern(cleanPath, nil))
197 } else {
198 return "", fmt.Errorf("Non-string path passed to hashFiles")
199 }
200 }
201
202 matcher := gitignore.NewMatcher(ps)
203
204 var files []string
205 if err := filepath.Walk(impl.config.WorkingDir, func(path string, fi fs.FileInfo, err error) error {
206 if err != nil {
207 return err
208 }
209 sansPrefix := strings.TrimPrefix(path, impl.config.WorkingDir+string(filepath.Separator))
210 parts := strings.Split(sansPrefix, string(filepath.Separator))
211 if fi.IsDir() || !matcher.Match(parts, fi.IsDir()) {
212 return nil
213 }
214 files = append(files, path)
215 return nil
216 }); err != nil {
217 return "", fmt.Errorf("Unable to filepath.Walk: %v", err)
218 }
219
220 if len(files) == 0 {
221 return "", nil
222 }
223
224 hasher := sha256.New()
225
226 for _, file := range files {
227 f, err := os.Open(file)
228 if err != nil {
229 return "", fmt.Errorf("Unable to os.Open: %v", err)
230 }
231
232 if _, err := io.Copy(hasher, f); err != nil {
233 return "", fmt.Errorf("Unable to io.Copy: %v", err)
234 }
235
236 if err := f.Close(); err != nil {
237 return "", fmt.Errorf("Unable to Close file: %v", err)
238 }
239 }
240
241 return hex.EncodeToString(hasher.Sum(nil)), nil
242}
243
244func (impl *interperterImpl) getNeedsTransitive(job *model.Job) []string {
245 needs := job.Needs()
246
247 for _, need := range needs {
248 parentNeeds := impl.getNeedsTransitive(impl.config.Run.Workflow.GetJob(need))
249 needs = append(needs, parentNeeds...)
250 }
251
252 return needs
253}
254
255func (impl *interperterImpl) always() (bool, error) {
256 return true, nil
257}
258
259func (impl *interperterImpl) jobSuccess() (bool, error) {
260 jobs := impl.config.Run.Workflow.Jobs
261 jobNeeds := impl.getNeedsTransitive(impl.config.Run.Job())
262
263 for _, needs := range jobNeeds {
264 if jobs[needs].Result != "success" {
265 return false, nil
266 }
267 }
268
269 return true, nil
270}
271
272func (impl *interperterImpl) stepSuccess() (bool, error) {
273 return impl.env.Job.Status == "success", nil
274}
275
276func (impl *interperterImpl) jobFailure() (bool, error) {
277 jobs := impl.config.Run.Workflow.Jobs
278 jobNeeds := impl.getNeedsTransitive(impl.config.Run.Job())
279
280 for _, needs := range jobNeeds {
281 if jobs[needs].Result == "failure" {
282 return true, nil
283 }
284 }
285
286 return false, nil
287}
288
289func (impl *interperterImpl) stepFailure() (bool, error) {
290 return impl.env.Job.Status == "failure", nil
291}
292
293func (impl *interperterImpl) cancelled() (bool, error) {
294 return impl.env.Job.Status == "cancelled", nil
295}
Deleted act/pkg/exprparser/interpreter.go
@@ -1,669 +0,0 @@
1package exprparser
2
3import (
4 "encoding"
5 "fmt"
6 "math"
7 "reflect"
8 "strings"
9
10 "github.com/nektos/act/pkg/model"
11 "github.com/rhysd/actionlint"
12)
13
14type EvaluationEnvironment struct {
15 Github *model.GithubContext
16 Env map[string]string
17 Job *model.JobContext
18 Jobs *map[string]*model.WorkflowCallResult
19 Steps map[string]*model.StepResult
20 Runner map[string]interface{}
21 Secrets map[string]string
22 Vars map[string]string
23 Strategy map[string]interface{}
24 Matrix map[string]interface{}
25 Needs map[string]Needs
26 Inputs map[string]interface{}
27 HashFiles func([]reflect.Value) (interface{}, error)
28}
29
30type Needs struct {
31 Outputs map[string]string `json:"outputs"`
32 Result string `json:"result"`
33}
34
35type Config struct {
36 Run *model.Run
37 WorkingDir string
38 Context string
39}
40
41type DefaultStatusCheck int
42
43const (
44 DefaultStatusCheckNone DefaultStatusCheck = iota
45 DefaultStatusCheckSuccess
46 DefaultStatusCheckAlways
47 DefaultStatusCheckCanceled
48 DefaultStatusCheckFailure
49)
50
51func (dsc DefaultStatusCheck) String() string {
52 switch dsc {
53 case DefaultStatusCheckSuccess:
54 return "success"
55 case DefaultStatusCheckAlways:
56 return "always"
57 case DefaultStatusCheckCanceled:
58 return "cancelled"
59 case DefaultStatusCheckFailure:
60 return "failure"
61 }
62 return ""
63}
64
65type Interpreter interface {
66 Evaluate(input string, defaultStatusCheck DefaultStatusCheck) (interface{}, error)
67}
68
69type interperterImpl struct {
70 env *EvaluationEnvironment
71 config Config
72}
73
74func NewInterpeter(env *EvaluationEnvironment, config Config) Interpreter {
75 return &interperterImpl{
76 env: env,
77 config: config,
78 }
79}
80
81func (impl *interperterImpl) Evaluate(input string, defaultStatusCheck DefaultStatusCheck) (interface{}, error) {
82 input = strings.TrimPrefix(input, "${{")
83 if defaultStatusCheck != DefaultStatusCheckNone && input == "" {
84 input = "success()"
85 }
86 parser := actionlint.NewExprParser()
87 exprNode, err := parser.Parse(actionlint.NewExprLexer(input + "}}"))
88 if err != nil {
89 return nil, fmt.Errorf("Failed to parse: %s", err.Message)
90 }
91
92 if defaultStatusCheck != DefaultStatusCheckNone {
93 hasStatusCheckFunction := false
94 actionlint.VisitExprNode(exprNode, func(node, _ actionlint.ExprNode, entering bool) {
95 if funcCallNode, ok := node.(*actionlint.FuncCallNode); entering && ok {
96 switch strings.ToLower(funcCallNode.Callee) {
97 case "success", "always", "cancelled", "failure":
98 hasStatusCheckFunction = true
99 }
100 }
101 })
102
103 if !hasStatusCheckFunction {
104 exprNode = &actionlint.LogicalOpNode{
105 Kind: actionlint.LogicalOpNodeKindAnd,
106 Left: &actionlint.FuncCallNode{
107 Callee: defaultStatusCheck.String(),
108 Args: []actionlint.ExprNode{},
109 },
110 Right: exprNode,
111 }
112 }
113 }
114
115 result, err2 := impl.evaluateNode(exprNode)
116
117 return result, err2
118}
119
120func (impl *interperterImpl) evaluateNode(exprNode actionlint.ExprNode) (interface{}, error) {
121 switch node := exprNode.(type) {
122 case *actionlint.VariableNode:
123 return impl.evaluateVariable(node)
124 case *actionlint.BoolNode:
125 return node.Value, nil
126 case *actionlint.NullNode:
127 return nil, nil
128 case *actionlint.IntNode:
129 return node.Value, nil
130 case *actionlint.FloatNode:
131 return node.Value, nil
132 case *actionlint.StringNode:
133 return node.Value, nil
134 case *actionlint.IndexAccessNode:
135 return impl.evaluateIndexAccess(node)
136 case *actionlint.ObjectDerefNode:
137 return impl.evaluateObjectDeref(node)
138 case *actionlint.ArrayDerefNode:
139 return impl.evaluateArrayDeref(node)
140 case *actionlint.NotOpNode:
141 return impl.evaluateNot(node)
142 case *actionlint.CompareOpNode:
143 return impl.evaluateCompare(node)
144 case *actionlint.LogicalOpNode:
145 return impl.evaluateLogicalCompare(node)
146 case *actionlint.FuncCallNode:
147 return impl.evaluateFuncCall(node)
148 default:
149 return nil, fmt.Errorf("Fatal error! Unknown node type: %s node: %+v", reflect.TypeOf(exprNode), exprNode)
150 }
151}
152
153func (impl *interperterImpl) evaluateVariable(variableNode *actionlint.VariableNode) (interface{}, error) {
154 switch strings.ToLower(variableNode.Name) {
155 case "github":
156 return impl.env.Github, nil
157 case "env":
158 return impl.env.Env, nil
159 case "job":
160 return impl.env.Job, nil
161 case "jobs":
162 if impl.env.Jobs == nil {
163 return nil, fmt.Errorf("Unavailable context: jobs")
164 }
165 return impl.env.Jobs, nil
166 case "steps":
167 return impl.env.Steps, nil
168 case "runner":
169 return impl.env.Runner, nil
170 case "secrets":
171 return impl.env.Secrets, nil
172 case "vars":
173 return impl.env.Vars, nil
174 case "strategy":
175 return impl.env.Strategy, nil
176 case "matrix":
177 return impl.env.Matrix, nil
178 case "needs":
179 return impl.env.Needs, nil
180 case "inputs":
181 return impl.env.Inputs, nil
182 case "infinity":
183 return math.Inf(1), nil
184 case "nan":
185 return math.NaN(), nil
186 default:
187 return nil, fmt.Errorf("Unavailable context: %s", variableNode.Name)
188 }
189}
190
191func (impl *interperterImpl) evaluateIndexAccess(indexAccessNode *actionlint.IndexAccessNode) (interface{}, error) {
192 left, err := impl.evaluateNode(indexAccessNode.Operand)
193 if err != nil {
194 return nil, err
195 }
196
197 leftValue := reflect.ValueOf(left)
198
199 right, err := impl.evaluateNode(indexAccessNode.Index)
200 if err != nil {
201 return nil, err
202 }
203
204 rightValue := reflect.ValueOf(right)
205
206 switch rightValue.Kind() {
207 case reflect.String:
208 return impl.getPropertyValue(leftValue, rightValue.String())
209
210 case reflect.Int:
211 switch leftValue.Kind() {
212 case reflect.Slice:
213 if rightValue.Int() < 0 || rightValue.Int() >= int64(leftValue.Len()) {
214 return nil, nil
215 }
216 return leftValue.Index(int(rightValue.Int())).Interface(), nil
217 default:
218 return nil, nil
219 }
220
221 default:
222 return nil, nil
223 }
224}
225
226func (impl *interperterImpl) evaluateObjectDeref(objectDerefNode *actionlint.ObjectDerefNode) (interface{}, error) {
227 left, err := impl.evaluateNode(objectDerefNode.Receiver)
228 if err != nil {
229 return nil, err
230 }
231
232 _, receiverIsDeref := objectDerefNode.Receiver.(*actionlint.ArrayDerefNode)
233 if receiverIsDeref {
234 return impl.getPropertyValueDereferenced(reflect.ValueOf(left), objectDerefNode.Property)
235 }
236 return impl.getPropertyValue(reflect.ValueOf(left), objectDerefNode.Property)
237}
238
239func (impl *interperterImpl) evaluateArrayDeref(arrayDerefNode *actionlint.ArrayDerefNode) (interface{}, error) {
240 left, err := impl.evaluateNode(arrayDerefNode.Receiver)
241 if err != nil {
242 return nil, err
243 }
244
245 return impl.getSafeValue(reflect.ValueOf(left)), nil
246}
247
248func (impl *interperterImpl) getPropertyValue(left reflect.Value, property string) (value interface{}, err error) {
249 switch left.Kind() {
250 case reflect.Ptr:
251 return impl.getPropertyValue(left.Elem(), property)
252
253 case reflect.Struct:
254 leftType := left.Type()
255 for i := 0; i < leftType.NumField(); i++ {
256 jsonName := leftType.Field(i).Tag.Get("json")
257 if jsonName == property {
258 property = leftType.Field(i).Name
259 break
260 }
261 }
262
263 fieldValue := left.FieldByNameFunc(func(name string) bool {
264 return strings.EqualFold(name, property)
265 })
266
267 if fieldValue.Kind() == reflect.Invalid {
268 return "", nil
269 }
270
271 i := fieldValue.Interface()
272 // The type stepStatus int is an integer, but should be treated as string
273 if m, ok := i.(encoding.TextMarshaler); ok {
274 text, err := m.MarshalText()
275 if err != nil {
276 return nil, err
277 }
278 return string(text), nil
279 }
280 return i, nil
281
282 case reflect.Map:
283 iter := left.MapRange()
284
285 for iter.Next() {
286 key := iter.Key()
287
288 switch key.Kind() {
289 case reflect.String:
290 if strings.EqualFold(key.String(), property) {
291 return impl.getMapValue(iter.Value())
292 }
293
294 default:
295 return nil, fmt.Errorf("'%s' in map key not implemented", key.Kind())
296 }
297 }
298
299 return nil, nil
300
301 case reflect.Slice:
302 var values []interface{}
303
304 for i := 0; i < left.Len(); i++ {
305 value, err := impl.getPropertyValue(left.Index(i).Elem(), property)
306 if err != nil {
307 return nil, err
308 }
309
310 values = append(values, value)
311 }
312
313 return values, nil
314 }
315
316 return nil, nil
317}
318
319func (impl *interperterImpl) getPropertyValueDereferenced(left reflect.Value, property string) (value interface{}, err error) {
320 switch left.Kind() {
321 case reflect.Ptr:
322 return impl.getPropertyValue(left, property)
323
324 case reflect.Struct:
325 return impl.getPropertyValue(left, property)
326 case reflect.Map:
327 iter := left.MapRange()
328
329 var values []interface{}
330 for iter.Next() {
331 value, err := impl.getPropertyValue(iter.Value(), property)
332 if err != nil {
333 return nil, err
334 }
335
336 values = append(values, value)
337 }
338
339 return values, nil
340 case reflect.Slice:
341 return impl.getPropertyValue(left, property)
342 }
343
344 return nil, nil
345}
346
347func (impl *interperterImpl) getMapValue(value reflect.Value) (interface{}, error) {
348 if value.Kind() == reflect.Ptr {
349 return impl.getMapValue(value.Elem())
350 }
351
352 return value.Interface(), nil
353}
354
355func (impl *interperterImpl) evaluateNot(notNode *actionlint.NotOpNode) (interface{}, error) {
356 operand, err := impl.evaluateNode(notNode.Operand)
357 if err != nil {
358 return nil, err
359 }
360
361 return !IsTruthy(operand), nil
362}
363
364func (impl *interperterImpl) evaluateCompare(compareNode *actionlint.CompareOpNode) (interface{}, error) {
365 left, err := impl.evaluateNode(compareNode.Left)
366 if err != nil {
367 return nil, err
368 }
369
370 right, err := impl.evaluateNode(compareNode.Right)
371 if err != nil {
372 return nil, err
373 }
374
375 leftValue := reflect.ValueOf(left)
376 rightValue := reflect.ValueOf(right)
377
378 return impl.compareValues(leftValue, rightValue, compareNode.Kind)
379}
380
381func (impl *interperterImpl) compareValues(leftValue reflect.Value, rightValue reflect.Value, kind actionlint.CompareOpNodeKind) (interface{}, error) {
382 if leftValue.Kind() != rightValue.Kind() {
383 if !impl.isNumber(leftValue) {
384 leftValue = impl.coerceToNumber(leftValue)
385 }
386 if !impl.isNumber(rightValue) {
387 rightValue = impl.coerceToNumber(rightValue)
388 }
389 }
390
391 switch leftValue.Kind() {
392 case reflect.Bool:
393 return impl.compareNumber(float64(impl.coerceToNumber(leftValue).Int()), float64(impl.coerceToNumber(rightValue).Int()), kind)
394 case reflect.String:
395 return impl.compareString(strings.ToLower(leftValue.String()), strings.ToLower(rightValue.String()), kind)
396
397 case reflect.Int:
398 if rightValue.Kind() == reflect.Float64 {
399 return impl.compareNumber(float64(leftValue.Int()), rightValue.Float(), kind)
400 }
401
402 return impl.compareNumber(float64(leftValue.Int()), float64(rightValue.Int()), kind)
403
404 case reflect.Float64:
405 if rightValue.Kind() == reflect.Int {
406 return impl.compareNumber(leftValue.Float(), float64(rightValue.Int()), kind)
407 }
408
409 return impl.compareNumber(leftValue.Float(), rightValue.Float(), kind)
410
411 case reflect.Invalid:
412 if rightValue.Kind() == reflect.Invalid {
413 return true, nil
414 }
415
416 // not possible situation - params are converted to the same type in code above
417 return nil, fmt.Errorf("Compare params of Invalid type: left: %+v, right: %+v", leftValue.Kind(), rightValue.Kind())
418
419 default:
420 return nil, fmt.Errorf("Compare not implemented for types: left: %+v, right: %+v", leftValue.Kind(), rightValue.Kind())
421 }
422}
423
424func (impl *interperterImpl) coerceToNumber(value reflect.Value) reflect.Value {
425 switch value.Kind() {
426 case reflect.Invalid:
427 return reflect.ValueOf(0)
428
429 case reflect.Bool:
430 switch value.Bool() {
431 case true:
432 return reflect.ValueOf(1)
433 case false:
434 return reflect.ValueOf(0)
435 }
436
437 case reflect.String:
438 if value.String() == "" {
439 return reflect.ValueOf(0)
440 }
441
442 // try to parse the string as a number
443 evaluated, err := impl.Evaluate(value.String(), DefaultStatusCheckNone)
444 if err != nil {
445 return reflect.ValueOf(math.NaN())
446 }
447
448 if value := reflect.ValueOf(evaluated); impl.isNumber(value) {
449 return value
450 }
451 }
452
453 return reflect.ValueOf(math.NaN())
454}
455
456func (impl *interperterImpl) coerceToString(value reflect.Value) reflect.Value {
457 switch value.Kind() {
458 case reflect.Invalid:
459 return reflect.ValueOf("")
460
461 case reflect.Bool:
462 switch value.Bool() {
463 case true:
464 return reflect.ValueOf("true")
465 case false:
466 return reflect.ValueOf("false")
467 }
468
469 case reflect.String:
470 return value
471
472 case reflect.Int:
473 return reflect.ValueOf(fmt.Sprint(value))
474
475 case reflect.Float64:
476 if math.IsInf(value.Float(), 1) {
477 return reflect.ValueOf("Infinity")
478 } else if math.IsInf(value.Float(), -1) {
479 return reflect.ValueOf("-Infinity")
480 }
481 return reflect.ValueOf(fmt.Sprintf("%.15G", value.Float()))
482
483 case reflect.Slice:
484 return reflect.ValueOf("Array")
485
486 case reflect.Map:
487 return reflect.ValueOf("Object")
488 }
489
490 return value
491}
492
493func (impl *interperterImpl) compareString(left string, right string, kind actionlint.CompareOpNodeKind) (bool, error) {
494 switch kind {
495 case actionlint.CompareOpNodeKindLess:
496 return left < right, nil
497 case actionlint.CompareOpNodeKindLessEq:
498 return left <= right, nil
499 case actionlint.CompareOpNodeKindGreater:
500 return left > right, nil
501 case actionlint.CompareOpNodeKindGreaterEq:
502 return left >= right, nil
503 case actionlint.CompareOpNodeKindEq:
504 return left == right, nil
505 case actionlint.CompareOpNodeKindNotEq:
506 return left != right, nil
507 default:
508 return false, fmt.Errorf("TODO: not implemented to compare '%+v'", kind)
509 }
510}
511
512func (impl *interperterImpl) compareNumber(left float64, right float64, kind actionlint.CompareOpNodeKind) (bool, error) {
513 switch kind {
514 case actionlint.CompareOpNodeKindLess:
515 return left < right, nil
516 case actionlint.CompareOpNodeKindLessEq:
517 return left <= right, nil
518 case actionlint.CompareOpNodeKindGreater:
519 return left > right, nil
520 case actionlint.CompareOpNodeKindGreaterEq:
521 return left >= right, nil
522 case actionlint.CompareOpNodeKindEq:
523 return left == right, nil
524 case actionlint.CompareOpNodeKindNotEq:
525 return left != right, nil
526 default:
527 return false, fmt.Errorf("TODO: not implemented to compare '%+v'", kind)
528 }
529}
530
531func IsTruthy(input interface{}) bool {
532 value := reflect.ValueOf(input)
533 switch value.Kind() {
534 case reflect.Bool:
535 return value.Bool()
536
537 case reflect.String:
538 return value.String() != ""
539
540 case reflect.Int:
541 return value.Int() != 0
542
543 case reflect.Float64:
544 if math.IsNaN(value.Float()) {
545 return false
546 }
547
548 return value.Float() != 0
549
550 case reflect.Map, reflect.Slice:
551 return true
552
553 default:
554 return false
555 }
556}
557
558func (impl *interperterImpl) isNumber(value reflect.Value) bool {
559 switch value.Kind() {
560 case reflect.Int, reflect.Float64:
561 return true
562 default:
563 return false
564 }
565}
566
567func (impl *interperterImpl) getSafeValue(value reflect.Value) interface{} {
568 switch value.Kind() {
569 case reflect.Invalid:
570 return nil
571
572 case reflect.Float64:
573 if value.Float() == 0 {
574 return 0
575 }
576 }
577
578 return value.Interface()
579}
580
581func (impl *interperterImpl) evaluateLogicalCompare(compareNode *actionlint.LogicalOpNode) (interface{}, error) {
582 left, err := impl.evaluateNode(compareNode.Left)
583 if err != nil {
584 return nil, err
585 }
586
587 leftValue := reflect.ValueOf(left)
588
589 if IsTruthy(left) == (compareNode.Kind == actionlint.LogicalOpNodeKindOr) {
590 return impl.getSafeValue(leftValue), nil
591 }
592
593 right, err := impl.evaluateNode(compareNode.Right)
594 if err != nil {
595 return nil, err
596 }
597
598 rightValue := reflect.ValueOf(right)
599
600 switch compareNode.Kind {
601 case actionlint.LogicalOpNodeKindAnd:
602 return impl.getSafeValue(rightValue), nil
603 case actionlint.LogicalOpNodeKindOr:
604 return impl.getSafeValue(rightValue), nil
605 }
606
607 return nil, fmt.Errorf("Unable to compare incompatibles types '%s' and '%s'", leftValue.Kind(), rightValue.Kind())
608}
609
610//nolint:gocyclo
611func (impl *interperterImpl) evaluateFuncCall(funcCallNode *actionlint.FuncCallNode) (interface{}, error) {
612 args := make([]reflect.Value, 0)
613
614 for _, arg := range funcCallNode.Args {
615 value, err := impl.evaluateNode(arg)
616 if err != nil {
617 return nil, err
618 }
619
620 args = append(args, reflect.ValueOf(value))
621 }
622
623 switch strings.ToLower(funcCallNode.Callee) {
624 case "contains":
625 return impl.contains(args[0], args[1])
626 case "startswith":
627 return impl.startsWith(args[0], args[1])
628 case "endswith":
629 return impl.endsWith(args[0], args[1])
630 case "format":
631 return impl.format(args[0], args[1:]...)
632 case "join":
633 if len(args) == 1 {
634 return impl.join(args[0], reflect.ValueOf(","))
635 }
636 return impl.join(args[0], args[1])
637 case "tojson":
638 return impl.toJSON(args[0])
639 case "fromjson":
640 return impl.fromJSON(args[0])
641 case "hashfiles":
642 if impl.env.HashFiles != nil {
643 return impl.env.HashFiles(args)
644 }
645 return impl.hashFiles(args...)
646 case "always":
647 return impl.always()
648 case "success":
649 if impl.config.Context == "job" {
650 return impl.jobSuccess()
651 }
652 if impl.config.Context == "step" {
653 return impl.stepSuccess()
654 }
655 return nil, fmt.Errorf("Context '%s' must be one of 'job' or 'step'", impl.config.Context)
656 case "failure":
657 if impl.config.Context == "job" {
658 return impl.jobFailure()
659 }
660 if impl.config.Context == "step" {
661 return impl.stepFailure()
662 }
663 return nil, fmt.Errorf("Context '%s' must be one of 'job' or 'step'", impl.config.Context)
664 case "cancelled":
665 return impl.cancelled()
666 default:
667 return nil, fmt.Errorf("TODO: '%s' not implemented", funcCallNode.Callee)
668 }
669}
Deleted act/pkg/filecollector/file_collector.go
@@ -1,210 +0,0 @@
1package filecollector
2
3import (
4 "archive/tar"
5 "context"
6 "fmt"
7 "io"
8 "io/fs"
9 "os"
10 "path"
11 "path/filepath"
12 "strings"
13
14 git "github.com/go-git/go-git/v5"
15 "github.com/go-git/go-git/v5/plumbing/filemode"
16 "github.com/go-git/go-git/v5/plumbing/format/gitignore"
17 "github.com/go-git/go-git/v5/plumbing/format/index"
18)
19
20type Handler interface {
21 WriteFile(path string, fi fs.FileInfo, linkName string, f io.Reader) error
22}
23
24type TarCollector struct {
25 TarWriter *tar.Writer
26 UID int
27 GID int
28 DstDir string
29}
30
31func (tc TarCollector) WriteFile(fpath string, fi fs.FileInfo, linkName string, f io.Reader) error {
32 // create a new dir/file header
33 header, err := tar.FileInfoHeader(fi, linkName)
34 if err != nil {
35 return err
36 }
37
38 // update the name to correctly reflect the desired destination when untaring
39 header.Name = path.Join(tc.DstDir, fpath)
40 header.Mode = int64(fi.Mode())
41 header.ModTime = fi.ModTime()
42 header.Uid = tc.UID
43 header.Gid = tc.GID
44
45 // write the header
46 if err := tc.TarWriter.WriteHeader(header); err != nil {
47 return err
48 }
49
50 // this is a symlink no reader provided
51 if f == nil {
52 return nil
53 }
54
55 // copy file data into tar writer
56 if _, err := io.Copy(tc.TarWriter, f); err != nil {
57 return err
58 }
59 return nil
60}
61
62type CopyCollector struct {
63 DstDir string
64}
65
66func (cc *CopyCollector) WriteFile(fpath string, fi fs.FileInfo, linkName string, f io.Reader) error {
67 fdestpath := filepath.Join(cc.DstDir, fpath)
68 if err := os.MkdirAll(filepath.Dir(fdestpath), 0o777); err != nil {
69 return err
70 }
71 if linkName != "" {
72 return os.Symlink(linkName, fdestpath)
73 }
74 df, err := os.OpenFile(fdestpath, os.O_CREATE|os.O_WRONLY, fi.Mode())
75 if err != nil {
76 return err
77 }
78 defer df.Close()
79 if _, err := io.Copy(df, f); err != nil {
80 return err
81 }
82 return nil
83}
84
85type FileCollector struct {
86 Ignorer gitignore.Matcher
87 SrcPath string
88 SrcPrefix string
89 Fs Fs
90 Handler Handler
91}
92
93type Fs interface {
94 Walk(root string, fn filepath.WalkFunc) error
95 OpenGitIndex(path string) (*index.Index, error)
96 Open(path string) (io.ReadCloser, error)
97 Readlink(path string) (string, error)
98}
99
100type DefaultFs struct {
101}
102
103func (*DefaultFs) Walk(root string, fn filepath.WalkFunc) error {
104 return filepath.Walk(root, fn)
105}
106
107func (*DefaultFs) OpenGitIndex(path string) (*index.Index, error) {
108 r, err := git.PlainOpen(path)
109 if err != nil {
110 return nil, err
111 }
112 i, err := r.Storer.Index()
113 if err != nil {
114 return nil, err
115 }
116 return i, nil
117}
118
119func (*DefaultFs) Open(path string) (io.ReadCloser, error) {
120 return os.Open(path)
121}
122
123func (*DefaultFs) Readlink(path string) (string, error) {
124 return os.Readlink(path)
125}
126
127//nolint:gocyclo
128func (fc *FileCollector) CollectFiles(ctx context.Context, submodulePath []string) filepath.WalkFunc {
129 i, _ := fc.Fs.OpenGitIndex(path.Join(fc.SrcPath, path.Join(submodulePath...)))
130 return func(file string, fi os.FileInfo, err error) error {
131 if err != nil {
132 return err
133 }
134 if ctx != nil {
135 select {
136 case <-ctx.Done():
137 return fmt.Errorf("copy cancelled")
138 default:
139 }
140 }
141
142 sansPrefix := strings.TrimPrefix(file, fc.SrcPrefix)
143 split := strings.Split(sansPrefix, string(filepath.Separator))
144 // The root folders should be skipped, submodules only have the last path component set to "." by filepath.Walk
145 if fi.IsDir() && len(split) > 0 && split[len(split)-1] == "." {
146 return nil
147 }
148 var entry *index.Entry
149 if i != nil {
150 entry, err = i.Entry(strings.Join(split[len(submodulePath):], "/"))
151 } else {
152 err = index.ErrEntryNotFound
153 }
154 if err != nil && fc.Ignorer != nil && fc.Ignorer.Match(split, fi.IsDir()) {
155 if fi.IsDir() {
156 if i != nil {
157 ms, err := i.Glob(strings.Join(append(split[len(submodulePath):], "**"), "/"))
158 if err != nil || len(ms) == 0 {
159 return filepath.SkipDir
160 }
161 } else {
162 return filepath.SkipDir
163 }
164 } else {
165 return nil
166 }
167 }
168 if err == nil && entry.Mode == filemode.Submodule {
169 err = fc.Fs.Walk(file, fc.CollectFiles(ctx, split))
170 if err != nil {
171 return err
172 }
173 return filepath.SkipDir
174 }
175 path := filepath.ToSlash(sansPrefix)
176
177 // return on non-regular files (thanks to [kumo](https://medium.com/@komuw/just-like-you-did-fbdd7df829d3) for this suggested update)
178 if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
179 linkName, err := fc.Fs.Readlink(file)
180 if err != nil {
181 return fmt.Errorf("unable to readlink '%s': %w", file, err)
182 }
183 return fc.Handler.WriteFile(path, fi, linkName, nil)
184 } else if !fi.Mode().IsRegular() {
185 return nil
186 }
187
188 // open file
189 f, err := fc.Fs.Open(file)
190 if err != nil {
191 return err
192 }
193 defer f.Close()
194
195 if ctx != nil {
196 // make io.Copy cancellable by closing the file
197 cpctx, cpfinish := context.WithCancel(ctx)
198 defer cpfinish()
199 go func() {
200 select {
201 case <-cpctx.Done():
202 case <-ctx.Done():
203 f.Close()
204 }
205 }()
206 }
207
208 return fc.Handler.WriteFile(path, fi, "", f)
209 }
210}
Deleted act/pkg/gh/gh.go
@@ -1,40 +0,0 @@
1package gh
2
3import (
4 "bufio"
5 "bytes"
6 "context"
7 "os/exec"
8)
9
10func GetToken(ctx context.Context, workingDirectory string) (string, error) {
11 var token string
12
13 // Locate the 'gh' executable
14 path, err := exec.LookPath("gh")
15 if err != nil {
16 return "", err
17 }
18
19 // Command setup
20 cmd := exec.CommandContext(ctx, path, "auth", "token")
21 cmd.Dir = workingDirectory
22
23 // Capture the output
24 var out bytes.Buffer
25 cmd.Stdout = &out
26
27 // Run the command
28 err = cmd.Run()
29 if err != nil {
30 return "", err
31 }
32
33 // Read the first line of the output
34 scanner := bufio.NewScanner(&out)
35 if scanner.Scan() {
36 token = scanner.Text()
37 }
38
39 return token, nil
40}
Deleted act/pkg/lookpath/env.go
@@ -1,18 +0,0 @@
1package lookpath
2
3import "os"
4
5type Env interface {
6 Getenv(name string) string
7}
8
9type defaultEnv struct {
10}
11
12func (*defaultEnv) Getenv(name string) string {
13 return os.Getenv(name)
14}
15
16func LookPath(file string) (string, error) {
17 return LookPath2(file, &defaultEnv{})
18}
Deleted act/pkg/lookpath/error.go
@@ -1,10 +0,0 @@
1package lookpath
2
3type Error struct {
4 Name string
5 Err error
6}
7
8func (e *Error) Error() string {
9 return e.Err.Error()
10}
Deleted act/pkg/lookpath/lp_js.go
@@ -1,23 +0,0 @@
1// Copyright 2018 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5//go:build js && wasm
6
7package lookpath
8
9import (
10 "errors"
11)
12
13// ErrNotFound is the error resulting if a path search failed to find an executable file.
14var ErrNotFound = errors.New("executable file not found in $PATH")
15
16// LookPath searches for an executable named file in the
17// directories named by the PATH environment variable.
18// If file contains a slash, it is tried directly and the PATH is not consulted.
19// The result may be an absolute path or a path relative to the current directory.
20func LookPath2(file string, lenv Env) (string, error) {
21 // Wasm can not execute processes, so act as if there are no executables at all.
22 return "", &Error{file, ErrNotFound}
23}
Deleted act/pkg/lookpath/lp_plan9.go
@@ -1,56 +0,0 @@
1// Copyright 2011 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package lookpath
6
7import (
8 "errors"
9 "io/fs"
10 "os"
11 "path/filepath"
12 "strings"
13)
14
15// ErrNotFound is the error resulting if a path search failed to find an executable file.
16var ErrNotFound = errors.New("executable file not found in $path")
17
18func findExecutable(file string) error {
19 d, err := os.Stat(file)
20 if err != nil {
21 return err
22 }
23 if m := d.Mode(); !m.IsDir() && m&0111 != 0 {
24 return nil
25 }
26 return fs.ErrPermission
27}
28
29// LookPath searches for an executable named file in the
30// directories named by the path environment variable.
31// If file begins with "/", "#", "./", or "../", it is tried
32// directly and the path is not consulted.
33// The result may be an absolute path or a path relative to the current directory.
34func LookPath2(file string, lenv Env) (string, error) {
35 // skip the path lookup for these prefixes
36 skip := []string{"/", "#", "./", "../"}
37
38 for _, p := range skip {
39 if strings.HasPrefix(file, p) {
40 err := findExecutable(file)
41 if err == nil {
42 return file, nil
43 }
44 return "", &Error{file, err}
45 }
46 }
47
48 path := lenv.Getenv("path")
49 for _, dir := range filepath.SplitList(path) {
50 path := filepath.Join(dir, file)
51 if err := findExecutable(path); err == nil {
52 return path, nil
53 }
54 }
55 return "", &Error{file, ErrNotFound}
56}
Deleted act/pkg/lookpath/lp_unix.go
@@ -1,59 +0,0 @@
1// Copyright 2010 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
6
7package lookpath
8
9import (
10 "errors"
11 "io/fs"
12 "os"
13 "path/filepath"
14 "strings"
15)
16
17// ErrNotFound is the error resulting if a path search failed to find an executable file.
18var ErrNotFound = errors.New("executable file not found in $PATH")
19
20func findExecutable(file string) error {
21 d, err := os.Stat(file)
22 if err != nil {
23 return err
24 }
25 if m := d.Mode(); !m.IsDir() && m&0111 != 0 {
26 return nil
27 }
28 return fs.ErrPermission
29}
30
31// LookPath searches for an executable named file in the
32// directories named by the PATH environment variable.
33// If file contains a slash, it is tried directly and the PATH is not consulted.
34// The result may be an absolute path or a path relative to the current directory.
35func LookPath2(file string, lenv Env) (string, error) {
36 // NOTE(rsc): I wish we could use the Plan 9 behavior here
37 // (only bypass the path if file begins with / or ./ or ../)
38 // but that would not match all the Unix shells.
39
40 if strings.Contains(file, "/") {
41 err := findExecutable(file)
42 if err == nil {
43 return file, nil
44 }
45 return "", &Error{file, err}
46 }
47 path := lenv.Getenv("PATH")
48 for _, dir := range filepath.SplitList(path) {
49 if dir == "" {
50 // Unix shell semantics: path element "" means "."
51 dir = "."
52 }
53 path := filepath.Join(dir, file)
54 if err := findExecutable(path); err == nil {
55 return path, nil
56 }
57 }
58 return "", &Error{file, ErrNotFound}
59}
Deleted act/pkg/lookpath/lp_windows.go
@@ -1,94 +0,0 @@
1// Copyright 2010 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package lookpath
6
7import (
8 "errors"
9 "io/fs"
10 "os"
11 "path/filepath"
12 "strings"
13)
14
15// ErrNotFound is the error resulting if a path search failed to find an executable file.
16var ErrNotFound = errors.New("executable file not found in %PATH%")
17
18func chkStat(file string) error {
19 d, err := os.Stat(file)
20 if err != nil {
21 return err
22 }
23 if d.IsDir() {
24 return fs.ErrPermission
25 }
26 return nil
27}
28
29func hasExt(file string) bool {
30 i := strings.LastIndex(file, ".")
31 if i < 0 {
32 return false
33 }
34 return strings.LastIndexAny(file, `:\/`) < i
35}
36
37func findExecutable(file string, exts []string) (string, error) {
38 if len(exts) == 0 {
39 return file, chkStat(file)
40 }
41 if hasExt(file) {
42 if chkStat(file) == nil {
43 return file, nil
44 }
45 }
46 for _, e := range exts {
47 if f := file + e; chkStat(f) == nil {
48 return f, nil
49 }
50 }
51 return "", fs.ErrNotExist
52}
53
54// LookPath searches for an executable named file in the
55// directories named by the PATH environment variable.
56// If file contains a slash, it is tried directly and the PATH is not consulted.
57// LookPath also uses PATHEXT environment variable to match
58// a suitable candidate.
59// The result may be an absolute path or a path relative to the current directory.
60func LookPath2(file string, lenv Env) (string, error) {
61 var exts []string
62 x := lenv.Getenv(`PATHEXT`)
63 if x != "" {
64 for _, e := range strings.Split(strings.ToLower(x), `;`) {
65 if e == "" {
66 continue
67 }
68 if e[0] != '.' {
69 e = "." + e
70 }
71 exts = append(exts, e)
72 }
73 } else {
74 exts = []string{".com", ".exe", ".bat", ".cmd"}
75 }
76
77 if strings.ContainsAny(file, `:\/`) {
78 if f, err := findExecutable(file, exts); err == nil {
79 return f, nil
80 } else {
81 return "", &Error{file, err}
82 }
83 }
84 if f, err := findExecutable(filepath.Join(".", file), exts); err == nil {
85 return f, nil
86 }
87 path := lenv.Getenv("path")
88 for _, dir := range filepath.SplitList(path) {
89 if f, err := findExecutable(filepath.Join(dir, file), exts); err == nil {
90 return f, nil
91 }
92 }
93 return "", &Error{file, ErrNotFound}
94}
Deleted act/pkg/model/action.go
@@ -1,149 +0,0 @@
1package model
2
3import (
4 "fmt"
5 "io"
6 "strings"
7
8 "github.com/nektos/act/pkg/schema"
9 "gopkg.in/yaml.v3"
10)
11
12// ActionRunsUsing is the type of runner for the action
13type ActionRunsUsing string
14
15func (a *ActionRunsUsing) UnmarshalYAML(unmarshal func(interface{}) error) error {
16 var using string
17 if err := unmarshal(&using); err != nil {
18 return err
19 }
20
21 // Force input to lowercase for case insensitive comparison
22 format := ActionRunsUsing(strings.ToLower(using))
23 switch format {
24 case ActionRunsUsingNode24, ActionRunsUsingNode20, ActionRunsUsingNode16, ActionRunsUsingNode12, ActionRunsUsingDocker, ActionRunsUsingComposite:
25 *a = format
26 default:
27 return fmt.Errorf("The runs.using key in action.yml must be one of: %v, got %s", []string{
28 ActionRunsUsingComposite,
29 ActionRunsUsingDocker,
30 ActionRunsUsingNode12,
31 ActionRunsUsingNode16,
32 ActionRunsUsingNode20,
33 ActionRunsUsingNode24,
34 }, format)
35 }
36 return nil
37}
38
39const (
40 // ActionRunsUsingNode12 for running with node12
41 ActionRunsUsingNode12 = "node12"
42 // ActionRunsUsingNode16 for running with node16
43 ActionRunsUsingNode16 = "node16"
44 // ActionRunsUsingNode20 for running with node20
45 ActionRunsUsingNode20 = "node20"
46 // ActionRunsUsingNode24 for running with node24
47 ActionRunsUsingNode24 = "node24"
48 // ActionRunsUsingDocker for running with docker
49 ActionRunsUsingDocker = "docker"
50 // ActionRunsUsingComposite for running composite
51 ActionRunsUsingComposite = "composite"
52)
53
54func (a ActionRunsUsing) IsNode() bool {
55 switch a {
56 case ActionRunsUsingNode12, ActionRunsUsingNode16, ActionRunsUsingNode20, ActionRunsUsingNode24:
57 return true
58 default:
59 return false
60 }
61}
62
63func (a ActionRunsUsing) IsDocker() bool {
64 return a == ActionRunsUsingDocker
65}
66
67func (a ActionRunsUsing) IsComposite() bool {
68 return a == ActionRunsUsingComposite
69}
70
71// ActionRuns are a field in Action
72type ActionRuns struct {
73 Using ActionRunsUsing `yaml:"using"`
74 Env map[string]string `yaml:"env"`
75 Main string `yaml:"main"`
76 Pre string `yaml:"pre"`
77 PreIf string `yaml:"pre-if"`
78 Post string `yaml:"post"`
79 PostIf string `yaml:"post-if"`
80 Image string `yaml:"image"`
81 PreEntrypoint string `yaml:"pre-entrypoint"`
82 Entrypoint string `yaml:"entrypoint"`
83 PostEntrypoint string `yaml:"post-entrypoint"`
84 Args []string `yaml:"args"`
85 Steps []Step `yaml:"steps"`
86}
87
88// Action describes a metadata file for GitHub actions. The metadata filename must be either action.yml or action.yaml. The data in the metadata file defines the inputs, outputs and main entrypoint for your action.
89type Action struct {
90 Name string `yaml:"name"`
91 Author string `yaml:"author"`
92 Description string `yaml:"description"`
93 Inputs map[string]Input `yaml:"inputs"`
94 Outputs map[string]Output `yaml:"outputs"`
95 Runs ActionRuns `yaml:"runs"`
96 Branding struct {
97 Color string `yaml:"color"`
98 Icon string `yaml:"icon"`
99 } `yaml:"branding"`
100}
101
102func (a *Action) UnmarshalYAML(node *yaml.Node) error {
103 // TODO enable after verifying that this runner side feature has rolled out in actions/runner
104 // // Resolve yaml anchor aliases first
105 // if err := resolveAliases(node); err != nil {
106 // return err
107 // }
108 // Validate the schema before deserializing it into our model
109 if err := (&schema.Node{
110 Definition: "action-root",
111 Schema: schema.GetActionSchema(),
112 }).UnmarshalYAML(node); err != nil {
113 return err
114 }
115 type ActionDefault Action
116 return node.Decode((*ActionDefault)(a))
117}
118
119// Input parameters allow you to specify data that the action expects to use during runtime. GitHub stores input parameters as environment variables. Input ids with uppercase letters are converted to lowercase during runtime. We recommended using lowercase input ids.
120type Input struct {
121 Description string `yaml:"description"`
122 Required bool `yaml:"required"`
123 Default string `yaml:"default"`
124}
125
126// Output parameters allow you to declare data that an action sets. Actions that run later in a workflow can use the output data set in previously run actions. For example, if you had an action that performed the addition of two inputs (x + y = z), the action could output the sum (z) for other actions to use as an input.
127type Output struct {
128 Description string `yaml:"description"`
129 Value string `yaml:"value"`
130}
131
132// ReadAction reads an action from a reader
133func ReadAction(in io.Reader) (*Action, error) {
134 a := new(Action)
135 err := yaml.NewDecoder(in).Decode(a)
136 if err != nil {
137 return nil, err
138 }
139
140 // set defaults
141 if a.Runs.PreIf == "" {
142 a.Runs.PreIf = "always()"
143 }
144 if a.Runs.PostIf == "" {
145 a.Runs.PostIf = "always()"
146 }
147
148 return a, nil
149}
Deleted act/pkg/model/anchors.go
@@ -1,38 +0,0 @@
1package model
2
3import (
4 "errors"
5
6 "gopkg.in/yaml.v3"
7)
8
9func resolveAliasesExt(node *yaml.Node, path map[*yaml.Node]bool, skipCheck bool) error {
10 if !skipCheck && path[node] {
11 return errors.New("circular alias")
12 }
13 switch node.Kind {
14 case yaml.AliasNode:
15 aliasTarget := node.Alias
16 if aliasTarget == nil {
17 return errors.New("unresolved alias node")
18 }
19 path[node] = true
20 *node = *aliasTarget
21 if err := resolveAliasesExt(node, path, true); err != nil {
22 return err
23 }
24 delete(path, node)
25
26 case yaml.DocumentNode, yaml.MappingNode, yaml.SequenceNode:
27 for _, child := range node.Content {
28 if err := resolveAliasesExt(child, path, false); err != nil {
29 return err
30 }
31 }
32 }
33 return nil
34}
35
36func resolveAliases(node *yaml.Node) error {
37 return resolveAliasesExt(node, map[*yaml.Node]bool{}, false)
38}
Deleted act/pkg/model/github_context.go
@@ -1,217 +0,0 @@
1package model
2
3import (
4 "context"
5 "fmt"
6 "strings"
7
8 "github.com/nektos/act/pkg/common"
9 "github.com/nektos/act/pkg/common/git"
10)
11
12type GithubContext struct {
13 Event map[string]interface{} `json:"event"`
14 EventPath string `json:"event_path"`
15 Workflow string `json:"workflow"`
16 RunAttempt string `json:"run_attempt"`
17 RunID string `json:"run_id"`
18 RunNumber string `json:"run_number"`
19 Actor string `json:"actor"`
20 Repository string `json:"repository"`
21 EventName string `json:"event_name"`
22 Sha string `json:"sha"`
23 Ref string `json:"ref"`
24 RefName string `json:"ref_name"`
25 RefType string `json:"ref_type"`
26 HeadRef string `json:"head_ref"`
27 BaseRef string `json:"base_ref"`
28 Token string `json:"token"`
29 Workspace string `json:"workspace"`
30 Action string `json:"action"`
31 ActionPath string `json:"action_path"`
32 ActionRef string `json:"action_ref"`
33 ActionRepository string `json:"action_repository"`
34 Job string `json:"job"`
35 JobName string `json:"job_name"`
36 RepositoryOwner string `json:"repository_owner"`
37 RetentionDays string `json:"retention_days"`
38 RunnerPerflog string `json:"runner_perflog"`
39 RunnerTrackingID string `json:"runner_tracking_id"`
40 ServerURL string `json:"server_url"`
41 APIURL string `json:"api_url"`
42 GraphQLURL string `json:"graphql_url"`
43}
44
45func asString(v interface{}) string {
46 if v == nil {
47 return ""
48 } else if s, ok := v.(string); ok {
49 return s
50 }
51 return ""
52}
53
54func nestedMapLookup(m map[string]interface{}, ks ...string) (rval interface{}) {
55 var ok bool
56
57 if len(ks) == 0 { // degenerate input
58 return nil
59 }
60 if rval, ok = m[ks[0]]; !ok {
61 return nil
62 } else if len(ks) == 1 { // we've reached the final key
63 return rval
64 } else if m, ok = rval.(map[string]interface{}); !ok {
65 return nil
66 }
67 // 1+ more keys
68 return nestedMapLookup(m, ks[1:]...)
69}
70
71func withDefaultBranch(ctx context.Context, b string, event map[string]interface{}) map[string]interface{} {
72 repoI, ok := event["repository"]
73 if !ok {
74 repoI = make(map[string]interface{})
75 }
76
77 repo, ok := repoI.(map[string]interface{})
78 if !ok {
79 common.Logger(ctx).Warnf("unable to set default branch to %v", b)
80 return event
81 }
82
83 // if the branch is already there return with no changes
84 if _, ok = repo["default_branch"]; ok {
85 return event
86 }
87
88 repo["default_branch"] = b
89 event["repository"] = repo
90
91 return event
92}
93
94var findGitRef = git.FindGitRef
95var findGitRevision = git.FindGitRevision
96
97func (ghc *GithubContext) SetRef(ctx context.Context, defaultBranch string, repoPath string) {
98 logger := common.Logger(ctx)
99
100 // https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows
101 // https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads
102 switch ghc.EventName {
103 case "pull_request_target":
104 ghc.Ref = fmt.Sprintf("refs/heads/%s", ghc.BaseRef)
105 case "pull_request", "pull_request_review", "pull_request_review_comment":
106 ghc.Ref = fmt.Sprintf("refs/pull/%.0f/merge", ghc.Event["number"])
107 case "deployment", "deployment_status":
108 ghc.Ref = asString(nestedMapLookup(ghc.Event, "deployment", "ref"))
109 case "release":
110 ghc.Ref = fmt.Sprintf("refs/tags/%s", asString(nestedMapLookup(ghc.Event, "release", "tag_name")))
111 case "push", "create", "workflow_dispatch":
112 ghc.Ref = asString(ghc.Event["ref"])
113 default:
114 defaultBranch := asString(nestedMapLookup(ghc.Event, "repository", "default_branch"))
115 if defaultBranch != "" {
116 ghc.Ref = fmt.Sprintf("refs/heads/%s", defaultBranch)
117 }
118 }
119
120 if ghc.Ref == "" {
121 ref, err := findGitRef(ctx, repoPath)
122 if err != nil {
123 logger.Warningf("unable to get git ref: %v", err)
124 } else {
125 logger.Debugf("using github ref: %s", ref)
126 ghc.Ref = ref
127 }
128
129 // set the branch in the event data
130 if defaultBranch != "" {
131 ghc.Event = withDefaultBranch(ctx, defaultBranch, ghc.Event)
132 } else {
133 ghc.Event = withDefaultBranch(ctx, "master", ghc.Event)
134 }
135
136 if ghc.Ref == "" {
137 ghc.Ref = fmt.Sprintf("refs/heads/%s", asString(nestedMapLookup(ghc.Event, "repository", "default_branch")))
138 }
139 }
140}
141
142func (ghc *GithubContext) SetSha(ctx context.Context, repoPath string) {
143 logger := common.Logger(ctx)
144
145 // https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows
146 // https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads
147 switch ghc.EventName {
148 case "pull_request_target":
149 ghc.Sha = asString(nestedMapLookup(ghc.Event, "pull_request", "base", "sha"))
150 case "deployment", "deployment_status":
151 ghc.Sha = asString(nestedMapLookup(ghc.Event, "deployment", "sha"))
152 case "push", "create", "workflow_dispatch":
153 if deleted, ok := ghc.Event["deleted"].(bool); ok && !deleted {
154 ghc.Sha = asString(ghc.Event["after"])
155 }
156 }
157
158 if ghc.Sha == "" {
159 _, sha, err := findGitRevision(ctx, repoPath)
160 if err != nil {
161 logger.Warningf("unable to get git revision: %v", err)
162 } else {
163 ghc.Sha = sha
164 }
165 }
166}
167
168func (ghc *GithubContext) SetRepositoryAndOwner(ctx context.Context, githubInstance string, remoteName string, repoPath string) {
169 if ghc.Repository == "" {
170 repo, err := git.FindGithubRepo(ctx, repoPath, githubInstance, remoteName)
171 if err != nil {
172 common.Logger(ctx).Debugf("unable to get git repo (githubInstance: %v; remoteName: %v, repoPath: %v): %v", githubInstance, remoteName, repoPath, err)
173 // nektos/act is used as a default action, so why not a repo?
174 ghc.Repository = "nektos/act"
175 ghc.RepositoryOwner = strings.Split(ghc.Repository, "/")[0]
176 return
177 }
178 ghc.Repository = repo
179 }
180 ghc.RepositoryOwner = strings.Split(ghc.Repository, "/")[0]
181}
182
183func (ghc *GithubContext) SetRefTypeAndName() {
184 var refType, refName string
185
186 // https://docs.github.com/en/actions/learn-github-actions/environment-variables
187 if strings.HasPrefix(ghc.Ref, "refs/tags/") {
188 refType = "tag"
189 refName = ghc.Ref[len("refs/tags/"):]
190 } else if strings.HasPrefix(ghc.Ref, "refs/heads/") {
191 refType = "branch"
192 refName = ghc.Ref[len("refs/heads/"):]
193 } else if strings.HasPrefix(ghc.Ref, "refs/pull/") {
194 refType = ""
195 refName = ghc.Ref[len("refs/pull/"):]
196 }
197
198 if ghc.RefType == "" {
199 ghc.RefType = refType
200 }
201
202 if ghc.RefName == "" {
203 ghc.RefName = refName
204 }
205}
206
207func (ghc *GithubContext) SetBaseAndHeadRef() {
208 if ghc.EventName == "pull_request" || ghc.EventName == "pull_request_target" {
209 if ghc.BaseRef == "" {
210 ghc.BaseRef = asString(nestedMapLookup(ghc.Event, "pull_request", "base", "ref"))
211 }
212
213 if ghc.HeadRef == "" {
214 ghc.HeadRef = asString(nestedMapLookup(ghc.Event, "pull_request", "head", "ref"))
215 }
216 }
217}
Deleted act/pkg/model/job_context.go
@@ -1,12 +0,0 @@
1package model
2
3type JobContext struct {
4 Status string `json:"status"`
5 Container struct {
6 ID string `json:"id"`
7 Network string `json:"network"`
8 } `json:"container"`
9 Services map[string]struct {
10 ID string `json:"id"`
11 } `json:"services"`
12}
Deleted act/pkg/model/planner.go
@@ -1,399 +0,0 @@
1package model
2
3import (
4 "fmt"
5 "io"
6 "io/fs"
7 "math"
8 "os"
9 "path/filepath"
10 "regexp"
11 "sort"
12
13 log "github.com/sirupsen/logrus"
14)
15
16// WorkflowPlanner contains methods for creating plans
17type WorkflowPlanner interface {
18 PlanEvent(eventName string) (*Plan, error)
19 PlanJob(jobName string) (*Plan, error)
20 PlanAll() (*Plan, error)
21 GetEvents() []string
22}
23
24// Plan contains a list of stages to run in series
25type Plan struct {
26 Stages []*Stage
27}
28
29// Stage contains a list of runs to execute in parallel
30type Stage struct {
31 Runs []*Run
32}
33
34// Run represents a job from a workflow that needs to be run
35type Run struct {
36 Workflow *Workflow
37 JobID string
38}
39
40func (r *Run) String() string {
41 jobName := r.Job().Name
42 if jobName == "" {
43 jobName = r.JobID
44 }
45 return jobName
46}
47
48// Job returns the job for this Run
49func (r *Run) Job() *Job {
50 return r.Workflow.GetJob(r.JobID)
51}
52
53type WorkflowFiles struct {
54 workflowDirEntry os.DirEntry
55 dirPath string
56}
57
58// NewWorkflowPlanner will load a specific workflow, all workflows from a directory or all workflows from a directory and its subdirectories
59func NewWorkflowPlanner(path string, noWorkflowRecurse, strict bool) (WorkflowPlanner, error) {
60 path, err := filepath.Abs(path)
61 if err != nil {
62 return nil, err
63 }
64
65 fi, err := os.Stat(path)
66 if err != nil {
67 return nil, err
68 }
69
70 var workflows []WorkflowFiles
71
72 if fi.IsDir() {
73 log.Debugf("Loading workflows from '%s'", path)
74 if noWorkflowRecurse {
75 files, err := os.ReadDir(path)
76 if err != nil {
77 return nil, err
78 }
79
80 for _, v := range files {
81 workflows = append(workflows, WorkflowFiles{
82 dirPath: path,
83 workflowDirEntry: v,
84 })
85 }
86 } else {
87 log.Debug("Loading workflows recursively")
88 if err := filepath.Walk(path,
89 func(p string, f os.FileInfo, err error) error {
90 if err != nil {
91 return err
92 }
93
94 if !f.IsDir() {
95 log.Debugf("Found workflow '%s' in '%s'", f.Name(), p)
96 workflows = append(workflows, WorkflowFiles{
97 dirPath: filepath.Dir(p),
98 workflowDirEntry: fs.FileInfoToDirEntry(f),
99 })
100 }
101
102 return nil
103 }); err != nil {
104 return nil, err
105 }
106 }
107 } else {
108 log.Debugf("Loading workflow '%s'", path)
109 dirname := filepath.Dir(path)
110
111 workflows = append(workflows, WorkflowFiles{
112 dirPath: dirname,
113 workflowDirEntry: fs.FileInfoToDirEntry(fi),
114 })
115 }
116
117 wp := new(workflowPlanner)
118 for _, wf := range workflows {
119 ext := filepath.Ext(wf.workflowDirEntry.Name())
120 if ext == ".yml" || ext == ".yaml" {
121 f, err := os.Open(filepath.Join(wf.dirPath, wf.workflowDirEntry.Name()))
122 if err != nil {
123 return nil, err
124 }
125
126 log.Debugf("Reading workflow '%s'", f.Name())
127 workflow, err := ReadWorkflow(f, strict)
128 if err != nil {
129 _ = f.Close()
130 if err == io.EOF {
131 return nil, fmt.Errorf("unable to read workflow '%s': file is empty: %w", wf.workflowDirEntry.Name(), err)
132 }
133 return nil, fmt.Errorf("workflow is not valid. '%s': %w", wf.workflowDirEntry.Name(), err)
134 }
135 _, err = f.Seek(0, 0)
136 if err != nil {
137 _ = f.Close()
138 return nil, fmt.Errorf("error occurring when resetting io pointer in '%s': %w", wf.workflowDirEntry.Name(), err)
139 }
140
141 workflow.File = wf.workflowDirEntry.Name()
142 if workflow.Name == "" {
143 workflow.Name = wf.workflowDirEntry.Name()
144 }
145
146 err = validateJobName(workflow)
147 if err != nil {
148 _ = f.Close()
149 return nil, err
150 }
151
152 wp.workflows = append(wp.workflows, workflow)
153 _ = f.Close()
154 }
155 }
156
157 return wp, nil
158}
159
160func NewSingleWorkflowPlanner(name string, f io.Reader) (WorkflowPlanner, error) {
161 wp := new(workflowPlanner)
162
163 log.Debugf("Reading workflow %s", name)
164 workflow, err := ReadWorkflow(f, false)
165 if err != nil {
166 if err == io.EOF {
167 return nil, fmt.Errorf("unable to read workflow '%s': file is empty: %w", name, err)
168 }
169 return nil, fmt.Errorf("workflow is not valid. '%s': %w", name, err)
170 }
171 workflow.File = name
172 if workflow.Name == "" {
173 workflow.Name = name
174 }
175
176 err = validateJobName(workflow)
177 if err != nil {
178 return nil, err
179 }
180
181 wp.workflows = append(wp.workflows, workflow)
182
183 return wp, nil
184}
185
186func validateJobName(workflow *Workflow) error {
187 jobNameRegex := regexp.MustCompile(`^([[:alpha:]_][[:alnum:]_\-]*)$`)
188 for k := range workflow.Jobs {
189 if ok := jobNameRegex.MatchString(k); !ok {
190 return fmt.Errorf("workflow is not valid. '%s': Job name '%s' is invalid. Names must start with a letter or '_' and contain only alphanumeric characters, '-', or '_'", workflow.Name, k)
191 }
192 }
193 return nil
194}
195
196type workflowPlanner struct {
197 workflows []*Workflow
198}
199
200// PlanEvent builds a new list of runs to execute in parallel for an event name
201func (wp *workflowPlanner) PlanEvent(eventName string) (*Plan, error) {
202 plan := new(Plan)
203 if len(wp.workflows) == 0 {
204 log.Debug("no workflows found by planner")
205 return plan, nil
206 }
207 var lastErr error
208
209 for _, w := range wp.workflows {
210 events := w.On()
211 if len(events) == 0 {
212 log.Debugf("no events found for workflow: %s", w.File)
213 continue
214 }
215
216 for _, e := range events {
217 if e == eventName {
218 stages, err := createStages(w, w.GetJobIDs()...)
219 if err != nil {
220 log.Warn(err)
221 lastErr = err
222 } else {
223 plan.mergeStages(stages)
224 }
225 }
226 }
227 }
228 return plan, lastErr
229}
230
231// PlanJob builds a new run to execute in parallel for a job name
232func (wp *workflowPlanner) PlanJob(jobName string) (*Plan, error) {
233 plan := new(Plan)
234 if len(wp.workflows) == 0 {
235 log.Debugf("no jobs found for workflow: %s", jobName)
236 }
237 var lastErr error
238
239 for _, w := range wp.workflows {
240 stages, err := createStages(w, jobName)
241 if err != nil {
242 log.Warn(err)
243 lastErr = err
244 } else {
245 plan.mergeStages(stages)
246 }
247 }
248 return plan, lastErr
249}
250
251// PlanAll builds a new run to execute in parallel all
252func (wp *workflowPlanner) PlanAll() (*Plan, error) {
253 plan := new(Plan)
254 if len(wp.workflows) == 0 {
255 log.Debug("no workflows found by planner")
256 return plan, nil
257 }
258 var lastErr error
259
260 for _, w := range wp.workflows {
261 stages, err := createStages(w, w.GetJobIDs()...)
262 if err != nil {
263 log.Warn(err)
264 lastErr = err
265 } else {
266 plan.mergeStages(stages)
267 }
268 }
269
270 return plan, lastErr
271}
272
273// GetEvents gets all the events in the workflows file
274func (wp *workflowPlanner) GetEvents() []string {
275 events := make([]string, 0)
276 for _, w := range wp.workflows {
277 found := false
278 for _, e := range events {
279 for _, we := range w.On() {
280 if e == we {
281 found = true
282 break
283 }
284 }
285 if found {
286 break
287 }
288 }
289
290 if !found {
291 events = append(events, w.On()...)
292 }
293 }
294
295 // sort the list based on depth of dependencies
296 sort.Slice(events, func(i, j int) bool {
297 return events[i] < events[j]
298 })
299
300 return events
301}
302
303// MaxRunNameLen determines the max name length of all jobs
304func (p *Plan) MaxRunNameLen() int {
305 maxRunNameLen := 0
306 for _, stage := range p.Stages {
307 for _, run := range stage.Runs {
308 runNameLen := len(run.String())
309 if runNameLen > maxRunNameLen {
310 maxRunNameLen = runNameLen
311 }
312 }
313 }
314 return maxRunNameLen
315}
316
317// GetJobIDs will get all the job names in the stage
318func (s *Stage) GetJobIDs() []string {
319 names := make([]string, 0)
320 for _, r := range s.Runs {
321 names = append(names, r.JobID)
322 }
323 return names
324}
325
326// Merge stages with existing stages in plan
327func (p *Plan) mergeStages(stages []*Stage) {
328 newStages := make([]*Stage, int(math.Max(float64(len(p.Stages)), float64(len(stages)))))
329 for i := 0; i < len(newStages); i++ {
330 newStages[i] = new(Stage)
331 if i >= len(p.Stages) {
332 newStages[i].Runs = append(newStages[i].Runs, stages[i].Runs...)
333 } else if i >= len(stages) {
334 newStages[i].Runs = append(newStages[i].Runs, p.Stages[i].Runs...)
335 } else {
336 newStages[i].Runs = append(newStages[i].Runs, p.Stages[i].Runs...)
337 newStages[i].Runs = append(newStages[i].Runs, stages[i].Runs...)
338 }
339 }
340 p.Stages = newStages
341}
342
343func createStages(w *Workflow, jobIDs ...string) ([]*Stage, error) {
344 // first, build a list of all the necessary jobs to run, and their dependencies
345 jobDependencies := make(map[string][]string)
346 for len(jobIDs) > 0 {
347 newJobIDs := make([]string, 0)
348 for _, jID := range jobIDs {
349 // make sure we haven't visited this job yet
350 if _, ok := jobDependencies[jID]; !ok {
351 if job := w.GetJob(jID); job != nil {
352 jobDependencies[jID] = job.Needs()
353 newJobIDs = append(newJobIDs, job.Needs()...)
354 }
355 }
356 }
357 jobIDs = newJobIDs
358 }
359
360 // next, build an execution graph
361 stages := make([]*Stage, 0)
362 for len(jobDependencies) > 0 {
363 stage := new(Stage)
364 for jID, jDeps := range jobDependencies {
365 // make sure all deps are in the graph already
366 if listInStages(jDeps, stages...) {
367 stage.Runs = append(stage.Runs, &Run{
368 Workflow: w,
369 JobID: jID,
370 })
371 delete(jobDependencies, jID)
372 }
373 }
374 if len(stage.Runs) == 0 {
375 return nil, fmt.Errorf("unable to build dependency graph for %s (%s)", w.Name, w.File)
376 }
377 stages = append(stages, stage)
378 }
379
380 return stages, nil
381}
382
383// return true iff all strings in srcList exist in at least one of the stages
384func listInStages(srcList []string, stages ...*Stage) bool {
385 for _, src := range srcList {
386 found := false
387 for _, stage := range stages {
388 for _, search := range stage.GetJobIDs() {
389 if src == search {
390 found = true
391 }
392 }
393 }
394 if !found {
395 return false
396 }
397 }
398 return true
399}
Deleted act/pkg/model/step_result.go
@@ -1,45 +0,0 @@
1package model
2
3import "fmt"
4
5type stepStatus int
6
7const (
8 StepStatusSuccess stepStatus = iota
9 StepStatusFailure
10 StepStatusSkipped
11)
12
13var stepStatusStrings = [...]string{
14 "success",
15 "failure",
16 "skipped",
17}
18
19func (s stepStatus) MarshalText() ([]byte, error) {
20 return []byte(s.String()), nil
21}
22
23func (s *stepStatus) UnmarshalText(b []byte) error {
24 str := string(b)
25 for i, name := range stepStatusStrings {
26 if name == str {
27 *s = stepStatus(i)
28 return nil
29 }
30 }
31 return fmt.Errorf("invalid step status %q", str)
32}
33
34func (s stepStatus) String() string {
35 if int(s) >= len(stepStatusStrings) {
36 return ""
37 }
38 return stepStatusStrings[s]
39}
40
41type StepResult struct {
42 Outputs map[string]string `json:"outputs"`
43 Conclusion stepStatus `json:"conclusion"`
44 Outcome stepStatus `json:"outcome"`
45}
Deleted act/pkg/model/workflow.go
@@ -1,763 +0,0 @@
1package model
2
3import (
4 "errors"
5 "fmt"
6 "io"
7 "reflect"
8 "regexp"
9 "strconv"
10 "strings"
11
12 "github.com/nektos/act/pkg/common"
13 "github.com/nektos/act/pkg/schema"
14 log "github.com/sirupsen/logrus"
15 "gopkg.in/yaml.v3"
16)
17
18// Workflow is the structure of the files in .github/workflows
19type Workflow struct {
20 File string
21 Name string `yaml:"name"`
22 RawOn yaml.Node `yaml:"on"`
23 Env map[string]string `yaml:"env"`
24 Jobs map[string]*Job `yaml:"jobs"`
25 Defaults Defaults `yaml:"defaults"`
26}
27
28// On events for the workflow
29func (w *Workflow) On() []string {
30 switch w.RawOn.Kind {
31 case yaml.ScalarNode:
32 var val string
33 err := w.RawOn.Decode(&val)
34 if err != nil {
35 log.Fatal(err)
36 }
37 return []string{val}
38 case yaml.SequenceNode:
39 var val []string
40 err := w.RawOn.Decode(&val)
41 if err != nil {
42 log.Fatal(err)
43 }
44 return val
45 case yaml.MappingNode:
46 var val map[string]interface{}
47 err := w.RawOn.Decode(&val)
48 if err != nil {
49 log.Fatal(err)
50 }
51 var keys []string
52 for k := range val {
53 keys = append(keys, k)
54 }
55 return keys
56 }
57 return nil
58}
59
60func (w *Workflow) OnEvent(event string) interface{} {
61 if w.RawOn.Kind == yaml.MappingNode {
62 var val map[string]interface{}
63 if !decodeNode(w.RawOn, &val) {
64 return nil
65 }
66 return val[event]
67 }
68 return nil
69}
70
71func (w *Workflow) UnmarshalYAML(node *yaml.Node) error {
72 // Resolve yaml anchor aliases first
73 if err := resolveAliases(node); err != nil {
74 return err
75 }
76 // Validate the schema before deserializing it into our model
77 if err := (&schema.Node{
78 Definition: "workflow-root",
79 Schema: schema.GetWorkflowSchema(),
80 }).UnmarshalYAML(node); err != nil {
81 return errors.Join(err, fmt.Errorf("Actions YAML Schema Validation Error detected:\nFor more information, see: https://nektosact.com/usage/schema.html"))
82 }
83 type WorkflowDefault Workflow
84 return node.Decode((*WorkflowDefault)(w))
85}
86
87type WorkflowStrict Workflow
88
89func (w *WorkflowStrict) UnmarshalYAML(node *yaml.Node) error {
90 // Resolve yaml anchor aliases first
91 if err := resolveAliases(node); err != nil {
92 return err
93 }
94 // Validate the schema before deserializing it into our model
95 if err := (&schema.Node{
96 Definition: "workflow-root-strict",
97 Schema: schema.GetWorkflowSchema(),
98 }).UnmarshalYAML(node); err != nil {
99 return errors.Join(err, fmt.Errorf("Actions YAML Strict Schema Validation Error detected:\nFor more information, see: https://nektosact.com/usage/schema.html"))
100 }
101 type WorkflowDefault Workflow
102 return node.Decode((*WorkflowDefault)(w))
103}
104
105type WorkflowDispatchInput struct {
106 Description string `yaml:"description"`
107 Required bool `yaml:"required"`
108 Default string `yaml:"default"`
109 Type string `yaml:"type"`
110 Options []string `yaml:"options"`
111}
112
113type WorkflowDispatch struct {
114 Inputs map[string]WorkflowDispatchInput `yaml:"inputs"`
115}
116
117func (w *Workflow) WorkflowDispatchConfig() *WorkflowDispatch {
118 switch w.RawOn.Kind {
119 case yaml.ScalarNode:
120 var val string
121 if !decodeNode(w.RawOn, &val) {
122 return nil
123 }
124 if val == "workflow_dispatch" {
125 return &WorkflowDispatch{}
126 }
127 case yaml.SequenceNode:
128 var val []string
129 if !decodeNode(w.RawOn, &val) {
130 return nil
131 }
132 for _, v := range val {
133 if v == "workflow_dispatch" {
134 return &WorkflowDispatch{}
135 }
136 }
137 case yaml.MappingNode:
138 var val map[string]yaml.Node
139 if !decodeNode(w.RawOn, &val) {
140 return nil
141 }
142
143 n, found := val["workflow_dispatch"]
144 var workflowDispatch WorkflowDispatch
145 if found && decodeNode(n, &workflowDispatch) {
146 return &workflowDispatch
147 }
148 default:
149 return nil
150 }
151 return nil
152}
153
154type WorkflowCallInput struct {
155 Description string `yaml:"description"`
156 Required bool `yaml:"required"`
157 Default yaml.Node `yaml:"default"`
158 Type string `yaml:"type"`
159}
160
161type WorkflowCallOutput struct {
162 Description string `yaml:"description"`
163 Value string `yaml:"value"`
164}
165
166type WorkflowCall struct {
167 Inputs map[string]WorkflowCallInput `yaml:"inputs"`
168 Outputs map[string]WorkflowCallOutput `yaml:"outputs"`
169}
170
171type WorkflowCallResult struct {
172 Outputs map[string]string `json:"outputs"`
173 Result string `json:"result"`
174}
175
176func (w *Workflow) WorkflowCallConfig() *WorkflowCall {
177 if w.RawOn.Kind != yaml.MappingNode {
178 // The callers expect for "on: workflow_call" and "on: [ workflow_call ]" a non nil return value
179 return &WorkflowCall{}
180 }
181
182 var val map[string]yaml.Node
183 if !decodeNode(w.RawOn, &val) {
184 return &WorkflowCall{}
185 }
186
187 var config WorkflowCall
188 node := val["workflow_call"]
189 if !decodeNode(node, &config) {
190 return &WorkflowCall{}
191 }
192
193 return &config
194}
195
196// Job is the structure of one job in a workflow
197type Job struct {
198 Name string `yaml:"name"`
199 RawNeeds yaml.Node `yaml:"needs"`
200 RawRunsOn yaml.Node `yaml:"runs-on"`
201 Env yaml.Node `yaml:"env"`
202 If yaml.Node `yaml:"if"`
203 Steps []*Step `yaml:"steps"`
204 TimeoutMinutes string `yaml:"timeout-minutes"`
205 Services map[string]*ContainerSpec `yaml:"services"`
206 Strategy *Strategy `yaml:"strategy"`
207 RawContainer yaml.Node `yaml:"container"`
208 Defaults Defaults `yaml:"defaults"`
209 Outputs map[string]string `yaml:"outputs"`
210 Uses string `yaml:"uses"`
211 With map[string]interface{} `yaml:"with"`
212 RawSecrets yaml.Node `yaml:"secrets"`
213 Result string
214}
215
216// Strategy for the job
217type Strategy struct {
218 FailFast bool
219 MaxParallel int
220 FailFastString string `yaml:"fail-fast"`
221 MaxParallelString string `yaml:"max-parallel"`
222 RawMatrix yaml.Node `yaml:"matrix"`
223}
224
225// Default settings that will apply to all steps in the job or workflow
226type Defaults struct {
227 Run RunDefaults `yaml:"run"`
228}
229
230// Defaults for all run steps in the job or workflow
231type RunDefaults struct {
232 Shell string `yaml:"shell"`
233 WorkingDirectory string `yaml:"working-directory"`
234}
235
236// GetMaxParallel sets default and returns value for `max-parallel`
237func (s Strategy) GetMaxParallel() int {
238 // MaxParallel default value is `GitHub will maximize the number of jobs run in parallel depending on the available runners on GitHub-hosted virtual machines`
239 // So I take the liberty to hardcode default limit to 4 and this is because:
240 // 1: tl;dr: self-hosted does only 1 parallel job - https://github.com/actions/runner/issues/639#issuecomment-825212735
241 // 2: GH has 20 parallel job limit (for free tier) - https://github.com/github/docs/blob/3ae84420bd10997bb5f35f629ebb7160fe776eae/content/actions/reference/usage-limits-billing-and-administration.md?plain=1#L45
242 // 3: I want to add support for MaxParallel to act and 20! parallel jobs is a bit overkill IMHO
243 maxParallel := 4
244 if s.MaxParallelString != "" {
245 var err error
246 if maxParallel, err = strconv.Atoi(s.MaxParallelString); err != nil {
247 log.Errorf("Failed to parse 'max-parallel' option: %v", err)
248 }
249 }
250 return maxParallel
251}
252
253// GetFailFast sets default and returns value for `fail-fast`
254func (s Strategy) GetFailFast() bool {
255 // FailFast option is true by default: https://github.com/github/docs/blob/3ae84420bd10997bb5f35f629ebb7160fe776eae/content/actions/reference/workflow-syntax-for-github-actions.md?plain=1#L1107
256 failFast := true
257 log.Debug(s.FailFastString)
258 if s.FailFastString != "" {
259 var err error
260 if failFast, err = strconv.ParseBool(s.FailFastString); err != nil {
261 log.Errorf("Failed to parse 'fail-fast' option: %v", err)
262 }
263 }
264 return failFast
265}
266
267func (j *Job) InheritSecrets() bool {
268 if j.RawSecrets.Kind != yaml.ScalarNode {
269 return false
270 }
271
272 var val string
273 if !decodeNode(j.RawSecrets, &val) {
274 return false
275 }
276
277 return val == "inherit"
278}
279
280func (j *Job) Secrets() map[string]string {
281 if j.RawSecrets.Kind != yaml.MappingNode {
282 return nil
283 }
284
285 var val map[string]string
286 if !decodeNode(j.RawSecrets, &val) {
287 return nil
288 }
289
290 return val
291}
292
293// Container details for the job
294func (j *Job) Container() *ContainerSpec {
295 var val *ContainerSpec
296 switch j.RawContainer.Kind {
297 case yaml.ScalarNode:
298 val = new(ContainerSpec)
299 if !decodeNode(j.RawContainer, &val.Image) {
300 return nil
301 }
302 case yaml.MappingNode:
303 val = new(ContainerSpec)
304 if !decodeNode(j.RawContainer, val) {
305 return nil
306 }
307 }
308 return val
309}
310
311// Needs list for Job
312func (j *Job) Needs() []string {
313 switch j.RawNeeds.Kind {
314 case yaml.ScalarNode:
315 var val string
316 if !decodeNode(j.RawNeeds, &val) {
317 return nil
318 }
319 return []string{val}
320 case yaml.SequenceNode:
321 var val []string
322 if !decodeNode(j.RawNeeds, &val) {
323 return nil
324 }
325 return val
326 }
327 return nil
328}
329
330// RunsOn list for Job
331func (j *Job) RunsOn() []string {
332 switch j.RawRunsOn.Kind {
333 case yaml.MappingNode:
334 var val struct {
335 Group string
336 Labels yaml.Node
337 }
338
339 if !decodeNode(j.RawRunsOn, &val) {
340 return nil
341 }
342
343 labels := nodeAsStringSlice(val.Labels)
344
345 if val.Group != "" {
346 labels = append(labels, val.Group)
347 }
348
349 return labels
350 default:
351 return nodeAsStringSlice(j.RawRunsOn)
352 }
353}
354
355func nodeAsStringSlice(node yaml.Node) []string {
356 switch node.Kind {
357 case yaml.ScalarNode:
358 var val string
359 if !decodeNode(node, &val) {
360 return nil
361 }
362 return []string{val}
363 case yaml.SequenceNode:
364 var val []string
365 if !decodeNode(node, &val) {
366 return nil
367 }
368 return val
369 }
370 return nil
371}
372
373func environment(yml yaml.Node) map[string]string {
374 env := make(map[string]string)
375 if yml.Kind == yaml.MappingNode {
376 if !decodeNode(yml, &env) {
377 return nil
378 }
379 }
380 return env
381}
382
383// Environment returns string-based key=value map for a job
384func (j *Job) Environment() map[string]string {
385 return environment(j.Env)
386}
387
388// Matrix decodes RawMatrix YAML node
389func (j *Job) Matrix() map[string][]interface{} {
390 if j.Strategy.RawMatrix.Kind == yaml.MappingNode {
391 var val map[string][]interface{}
392 if !decodeNode(j.Strategy.RawMatrix, &val) {
393 return nil
394 }
395 return val
396 }
397 return nil
398}
399
400// GetMatrixes returns the matrix cross product
401// It skips includes and hard fails excludes for non-existing keys
402func (j *Job) GetMatrixes() ([]map[string]interface{}, error) {
403 matrixes := make([]map[string]interface{}, 0)
404 if j.Strategy != nil {
405 j.Strategy.FailFast = j.Strategy.GetFailFast()
406 j.Strategy.MaxParallel = j.Strategy.GetMaxParallel()
407
408 if m := j.Matrix(); m != nil {
409 includes := make([]map[string]interface{}, 0)
410 extraIncludes := make([]map[string]interface{}, 0)
411 for _, v := range m["include"] {
412 switch t := v.(type) {
413 case []interface{}:
414 for _, i := range t {
415 i := i.(map[string]interface{})
416 includes = append(includes, i)
417 }
418 case interface{}:
419 v := v.(map[string]interface{})
420 includes = append(includes, v)
421 }
422 }
423 delete(m, "include")
424
425 excludes := make([]map[string]interface{}, 0)
426 for _, e := range m["exclude"] {
427 e := e.(map[string]interface{})
428 for k := range e {
429 if _, ok := m[k]; ok {
430 excludes = append(excludes, e)
431 } else {
432 // We fail completely here because that's what GitHub does for non-existing matrix keys, fail on exclude, silent skip on include
433 return nil, fmt.Errorf("the workflow is not valid. Matrix exclude key %q does not match any key within the matrix", k)
434 }
435 }
436 }
437 delete(m, "exclude")
438
439 matrixProduct := common.CartesianProduct(m)
440 MATRIX:
441 for _, matrix := range matrixProduct {
442 for _, exclude := range excludes {
443 if commonKeysMatch(matrix, exclude) {
444 log.Debugf("Skipping matrix '%v' due to exclude '%v'", matrix, exclude)
445 continue MATRIX
446 }
447 }
448 matrixes = append(matrixes, matrix)
449 }
450 for _, include := range includes {
451 matched := false
452 for _, matrix := range matrixes {
453 if commonKeysMatch2(matrix, include, m) {
454 matched = true
455 log.Debugf("Adding include values '%v' to existing entry", include)
456 for k, v := range include {
457 matrix[k] = v
458 }
459 }
460 }
461 if !matched {
462 extraIncludes = append(extraIncludes, include)
463 }
464 }
465 for _, include := range extraIncludes {
466 log.Debugf("Adding include '%v'", include)
467 matrixes = append(matrixes, include)
468 }
469 if len(matrixes) == 0 {
470 matrixes = append(matrixes, make(map[string]interface{}))
471 }
472 } else {
473 matrixes = append(matrixes, make(map[string]interface{}))
474 }
475 } else {
476 matrixes = append(matrixes, make(map[string]interface{}))
477 log.Debugf("Empty Strategy, matrixes=%v", matrixes)
478 }
479 return matrixes, nil
480}
481
482func commonKeysMatch(a map[string]interface{}, b map[string]interface{}) bool {
483 for aKey, aVal := range a {
484 if bVal, ok := b[aKey]; ok && !reflect.DeepEqual(aVal, bVal) {
485 return false
486 }
487 }
488 return true
489}
490
491func commonKeysMatch2(a map[string]interface{}, b map[string]interface{}, m map[string][]interface{}) bool {
492 for aKey, aVal := range a {
493 _, useKey := m[aKey]
494 if bVal, ok := b[aKey]; useKey && ok && !reflect.DeepEqual(aVal, bVal) {
495 return false
496 }
497 }
498 return true
499}
500
501// JobType describes what type of job we are about to run
502type JobType int
503
504const (
505 // JobTypeDefault is all jobs that have a `run` attribute
506 JobTypeDefault JobType = iota
507
508 // JobTypeReusableWorkflowLocal is all jobs that have a `uses` that is a local workflow in the .github/workflows directory
509 JobTypeReusableWorkflowLocal
510
511 // JobTypeReusableWorkflowRemote is all jobs that have a `uses` that references a workflow file in a github repo
512 JobTypeReusableWorkflowRemote
513
514 // JobTypeInvalid represents a job which is not configured correctly
515 JobTypeInvalid
516)
517
518func (j JobType) String() string {
519 switch j {
520 case JobTypeDefault:
521 return "default"
522 case JobTypeReusableWorkflowLocal:
523 return "local-reusable-workflow"
524 case JobTypeReusableWorkflowRemote:
525 return "remote-reusable-workflow"
526 }
527 return "unknown"
528}
529
530// Type returns the type of the job
531func (j *Job) Type() (JobType, error) {
532 isReusable := j.Uses != ""
533
534 if isReusable {
535 isYaml, _ := regexp.MatchString(`\.(ya?ml)(?:$|@)`, j.Uses)
536
537 if isYaml {
538 isLocalPath := strings.HasPrefix(j.Uses, "./")
539 isRemotePath, _ := regexp.MatchString(`^[^.](.+?/){2,}.+\.ya?ml@`, j.Uses)
540 hasVersion, _ := regexp.MatchString(`\.ya?ml@`, j.Uses)
541
542 if isLocalPath {
543 return JobTypeReusableWorkflowLocal, nil
544 } else if isRemotePath && hasVersion {
545 return JobTypeReusableWorkflowRemote, nil
546 }
547 }
548
549 return JobTypeInvalid, fmt.Errorf("`uses` key references invalid workflow path '%s'. Must start with './' if it's a local workflow, or must start with '<org>/<repo>/' and include an '@' if it's a remote workflow", j.Uses)
550 }
551
552 return JobTypeDefault, nil
553}
554
555// ContainerSpec is the specification of the container to use for the job
556type ContainerSpec struct {
557 Image string `yaml:"image"`
558 Env map[string]string `yaml:"env"`
559 Ports []string `yaml:"ports"`
560 Volumes []string `yaml:"volumes"`
561 Options string `yaml:"options"`
562 Credentials map[string]string `yaml:"credentials"`
563 Entrypoint string
564 Args string
565 Name string
566 Reuse bool
567}
568
569// Step is the structure of one step in a job
570type Step struct {
571 ID string `yaml:"id"`
572 If yaml.Node `yaml:"if"`
573 Name string `yaml:"name"`
574 Uses string `yaml:"uses"`
575 Run string `yaml:"run"`
576 WorkingDirectory string `yaml:"working-directory"`
577 // WorkflowShell is the shell really configured in the job, directly at step level or higher in defaults.run.shell
578 WorkflowShell string `yaml:"-"`
579 Shell string `yaml:"shell"`
580 Env yaml.Node `yaml:"env"`
581 With map[string]string `yaml:"with"`
582 RawContinueOnError string `yaml:"continue-on-error"`
583 TimeoutMinutes string `yaml:"timeout-minutes"`
584}
585
586// String gets the name of step
587func (s *Step) String() string {
588 if s.Name != "" {
589 return s.Name
590 } else if s.Uses != "" {
591 return s.Uses
592 } else if s.Run != "" {
593 return s.Run
594 }
595 return s.ID
596}
597
598// Environment returns string-based key=value map for a step
599func (s *Step) Environment() map[string]string {
600 return environment(s.Env)
601}
602
603// GetEnv gets the env for a step
604func (s *Step) GetEnv() map[string]string {
605 env := s.Environment()
606
607 for k, v := range s.With {
608 envKey := regexp.MustCompile("[^A-Z0-9-]").ReplaceAllString(strings.ToUpper(k), "_")
609 envKey = fmt.Sprintf("INPUT_%s", strings.ToUpper(envKey))
610 env[envKey] = v
611 }
612 return env
613}
614
615// ShellCommand returns the command for the shell
616func (s *Step) ShellCommand() string {
617 shellCommand := ""
618
619 //Reference: https://github.com/actions/runner/blob/8109c962f09d9acc473d92c595ff43afceddb347/src/Runner.Worker/Handlers/ScriptHandlerHelpers.cs#L9-L17
620 switch s.Shell {
621 case "":
622 shellCommand = "bash -e {0}"
623 case "bash":
624 if s.WorkflowShell == "" {
625 shellCommand = "bash -e {0}"
626 } else {
627 shellCommand = "bash --noprofile --norc -e -o pipefail {0}"
628 }
629 case "pwsh":
630 shellCommand = "pwsh -command . '{0}'"
631 case "python":
632 shellCommand = "python {0}"
633 case "sh":
634 shellCommand = "sh -e {0}"
635 case "cmd":
636 shellCommand = "cmd /D /E:ON /V:OFF /S /C \"CALL \"{0}\"\""
637 case "powershell":
638 shellCommand = "powershell -command . '{0}'"
639 default:
640 shellCommand = s.Shell
641 }
642 return shellCommand
643}
644
645// StepType describes what type of step we are about to run
646type StepType int
647
648const (
649 // StepTypeRun is all steps that have a `run` attribute
650 StepTypeRun StepType = iota
651
652 // StepTypeUsesDockerURL is all steps that have a `uses` that is of the form `docker://...`
653 StepTypeUsesDockerURL
654
655 // StepTypeUsesActionLocal is all steps that have a `uses` that is a local action in a subdirectory
656 StepTypeUsesActionLocal
657
658 // StepTypeUsesActionRemote is all steps that have a `uses` that is a reference to a github repo
659 StepTypeUsesActionRemote
660
661 // StepTypeReusableWorkflowLocal is all steps that have a `uses` that is a local workflow in the .github/workflows directory
662 StepTypeReusableWorkflowLocal
663
664 // StepTypeReusableWorkflowRemote is all steps that have a `uses` that references a workflow file in a github repo
665 StepTypeReusableWorkflowRemote
666
667 // StepTypeInvalid is for steps that have invalid step action
668 StepTypeInvalid
669)
670
671func (s StepType) String() string {
672 switch s {
673 case StepTypeInvalid:
674 return "invalid"
675 case StepTypeRun:
676 return "run"
677 case StepTypeUsesActionLocal:
678 return "local-action"
679 case StepTypeUsesActionRemote:
680 return "remote-action"
681 case StepTypeUsesDockerURL:
682 return "docker"
683 case StepTypeReusableWorkflowLocal:
684 return "local-reusable-workflow"
685 case StepTypeReusableWorkflowRemote:
686 return "remote-reusable-workflow"
687 }
688 return "unknown"
689}
690
691// Type returns the type of the step
692func (s *Step) Type() StepType {
693 if s.Run == "" && s.Uses == "" {
694 return StepTypeInvalid
695 }
696
697 if s.Run != "" {
698 if s.Uses != "" {
699 return StepTypeInvalid
700 }
701 return StepTypeRun
702 } else if strings.HasPrefix(s.Uses, "docker://") {
703 return StepTypeUsesDockerURL
704 } else if strings.HasPrefix(s.Uses, "./.github/workflows") && (strings.HasSuffix(s.Uses, ".yml") || strings.HasSuffix(s.Uses, ".yaml")) {
705 return StepTypeReusableWorkflowLocal
706 } else if !strings.HasPrefix(s.Uses, "./") && strings.Contains(s.Uses, ".github/workflows") && (strings.Contains(s.Uses, ".yml@") || strings.Contains(s.Uses, ".yaml@")) {
707 return StepTypeReusableWorkflowRemote
708 } else if strings.HasPrefix(s.Uses, "./") {
709 return StepTypeUsesActionLocal
710 }
711 return StepTypeUsesActionRemote
712}
713
714// ReadWorkflow returns a list of jobs for a given workflow file reader
715func ReadWorkflow(in io.Reader, strict bool) (*Workflow, error) {
716 if strict {
717 w := new(WorkflowStrict)
718 err := yaml.NewDecoder(in).Decode(w)
719 return (*Workflow)(w), err
720 }
721 w := new(Workflow)
722 err := yaml.NewDecoder(in).Decode(w)
723 return w, err
724}
725
726// GetJob will get a job by name in the workflow
727func (w *Workflow) GetJob(jobID string) *Job {
728 for id, j := range w.Jobs {
729 if jobID == id {
730 if j.Name == "" {
731 j.Name = id
732 }
733 if j.If.Value == "" {
734 j.If.Value = "success()"
735 }
736 return j
737 }
738 }
739 return nil
740}
741
742// GetJobIDs will get all the job names in the workflow
743func (w *Workflow) GetJobIDs() []string {
744 ids := make([]string, 0)
745 for id := range w.Jobs {
746 ids = append(ids, id)
747 }
748 return ids
749}
750
751var OnDecodeNodeError = func(node yaml.Node, out interface{}, err error) {
752 log.Fatalf("Failed to decode node %v into %T: %v", node, out, err)
753}
754
755func decodeNode(node yaml.Node, out interface{}) bool {
756 if err := node.Decode(out); err != nil {
757 if OnDecodeNodeError != nil {
758 OnDecodeNodeError(node, out, err)
759 }
760 return false
761 }
762 return true
763}
Deleted act/pkg/runner/hashfiles/index.js

Diff too large to display.

Deleted act/pkg/runner/res/trampoline.js

Diff too large to display.

Deleted act/pkg/runner/action.go
@@ -1,689 +0,0 @@
1package runner
2
3import (
4 "context"
5 "embed"
6 "errors"
7 "fmt"
8 "io"
9 "io/fs"
10 "os"
11 "path"
12 "path/filepath"
13 "regexp"
14 "runtime"
15 "strings"
16
17 "github.com/kballard/go-shellquote"
18
19 "github.com/nektos/act/pkg/common"
20 "github.com/nektos/act/pkg/container"
21 "github.com/nektos/act/pkg/model"
22)
23
24type actionStep interface {
25 step
26
27 getActionModel() *model.Action
28 getCompositeRunContext(context.Context) *RunContext
29 getCompositeSteps() *compositeSteps
30}
31
32type readAction func(ctx context.Context, step *model.Step, actionDir string, actionPath string, readFile actionYamlReader, writeFile fileWriter) (*model.Action, error)
33
34type actionYamlReader func(filename string) (io.Reader, io.Closer, error)
35
36type fileWriter func(filename string, data []byte, perm fs.FileMode) error
37
38type runAction func(step actionStep, actionDir string, remoteAction *remoteAction) common.Executor
39
40//go:embed res/trampoline.js
41var trampoline embed.FS
42
43func readActionImpl(ctx context.Context, step *model.Step, actionDir string, actionPath string, readFile actionYamlReader, writeFile fileWriter) (*model.Action, error) {
44 logger := common.Logger(ctx)
45 allErrors := []error{}
46 addError := func(fileName string, err error) {
47 if err != nil {
48 allErrors = append(allErrors, fmt.Errorf("failed to read '%s' from action '%s' with path '%s' of step: %w", fileName, step.String(), actionPath, err))
49 } else {
50 // One successful read, clear error state
51 allErrors = nil
52 }
53 }
54 reader, closer, err := readFile("action.yml")
55 addError("action.yml", err)
56 if os.IsNotExist(err) {
57 reader, closer, err = readFile("action.yaml")
58 addError("action.yaml", err)
59 if os.IsNotExist(err) {
60 _, closer, err := readFile("Dockerfile")
61 addError("Dockerfile", err)
62 if err == nil {
63 closer.Close()
64 action := &model.Action{
65 Name: "(Synthetic)",
66 Runs: model.ActionRuns{
67 Using: "docker",
68 Image: "Dockerfile",
69 },
70 }
71 logger.Debugf("Using synthetic action %v for Dockerfile", action)
72 return action, nil
73 }
74 if step.With != nil {
75 if val, ok := step.With["args"]; ok {
76 var b []byte
77 if b, err = trampoline.ReadFile("res/trampoline.js"); err != nil {
78 return nil, err
79 }
80 err2 := writeFile(filepath.Join(actionDir, actionPath, "trampoline.js"), b, 0o400)
81 if err2 != nil {
82 return nil, err2
83 }
84 action := &model.Action{
85 Name: "(Synthetic)",
86 Inputs: map[string]model.Input{
87 "cwd": {
88 Description: "(Actual working directory)",
89 Required: false,
90 Default: filepath.Join(actionDir, actionPath),
91 },
92 "command": {
93 Description: "(Actual program)",
94 Required: false,
95 Default: val,
96 },
97 },
98 Runs: model.ActionRuns{
99 Using: "node12",
100 Main: "trampoline.js",
101 },
102 }
103 logger.Debugf("Using synthetic action %v", action)
104 return action, nil
105 }
106 }
107 }
108 }
109 if allErrors != nil {
110 return nil, errors.Join(allErrors...)
111 }
112 defer closer.Close()
113
114 action, err := model.ReadAction(reader)
115 logger.Debugf("Read action %v from '%s'", action, "Unknown")
116 return action, err
117}
118
119func maybeCopyToActionDir(ctx context.Context, step actionStep, actionDir string, actionPath string, containerActionDir string) error {
120 logger := common.Logger(ctx)
121 rc := step.getRunContext()
122 stepModel := step.getStepModel()
123
124 if stepModel.Type() != model.StepTypeUsesActionRemote {
125 return nil
126 }
127
128 var containerActionDirCopy string
129 containerActionDirCopy = strings.TrimSuffix(containerActionDir, actionPath)
130 logger.Debug(containerActionDirCopy)
131
132 if !strings.HasSuffix(containerActionDirCopy, `/`) {
133 containerActionDirCopy += `/`
134 }
135
136 if rc.Config != nil && rc.Config.ActionCache != nil {
137 raction := step.(*stepActionRemote)
138 ta, err := rc.Config.ActionCache.GetTarArchive(ctx, raction.cacheDir, raction.resolvedSha, "")
139 if err != nil {
140 return err
141 }
142 defer ta.Close()
143 return rc.JobContainer.CopyTarStream(ctx, containerActionDirCopy, ta)
144 }
145
146 if err := removeGitIgnore(ctx, actionDir); err != nil {
147 return err
148 }
149
150 return rc.JobContainer.CopyDir(containerActionDirCopy, actionDir+"/", rc.Config.UseGitIgnore)(ctx)
151}
152
153func runActionImpl(step actionStep, actionDir string, remoteAction *remoteAction) common.Executor {
154 rc := step.getRunContext()
155 stepModel := step.getStepModel()
156
157 return func(ctx context.Context) error {
158 logger := common.Logger(ctx)
159 actionPath := ""
160 if remoteAction != nil && remoteAction.Path != "" {
161 actionPath = remoteAction.Path
162 }
163
164 action := step.getActionModel()
165 logger.Debugf("About to run action %v", action)
166
167 err := setupActionEnv(ctx, step, remoteAction)
168 if err != nil {
169 return err
170 }
171
172 actionLocation := path.Join(actionDir, actionPath)
173 actionName, containerActionDir := getContainerActionPaths(stepModel, actionLocation, rc)
174
175 logger.Debugf("type=%v actionDir=%s actionPath=%s workdir=%s actionCacheDir=%s actionName=%s containerActionDir=%s", stepModel.Type(), actionDir, actionPath, rc.Config.Workdir, rc.ActionCacheDir(), actionName, containerActionDir)
176
177 x := action.Runs.Using
178 switch {
179 case x.IsNode():
180 if err := maybeCopyToActionDir(ctx, step, actionDir, actionPath, containerActionDir); err != nil {
181 return err
182 }
183 containerArgs := []string{rc.GetNodeToolForAction(ctx, string(action.Runs.Using)), path.Join(containerActionDir, action.Runs.Main)}
184 logger.Debugf("executing remote job container: %s", containerArgs)
185
186 rc.ApplyExtraPath(ctx, step.getEnv())
187
188 return rc.execJobContainer(containerArgs, *step.getEnv(), "", "")(ctx)
189 case x.IsDocker():
190 if remoteAction == nil {
191 actionDir = ""
192 actionPath = containerActionDir
193 }
194 return execAsDocker(ctx, step, actionName, actionDir, actionPath, remoteAction == nil, "entrypoint")
195 case x.IsComposite():
196 if err := maybeCopyToActionDir(ctx, step, actionDir, actionPath, containerActionDir); err != nil {
197 return err
198 }
199
200 return execAsComposite(step)(ctx)
201 default:
202 return fmt.Errorf("The runs.using key must be one of: %v, got %s", []string{
203 model.ActionRunsUsingDocker,
204 model.ActionRunsUsingNode12,
205 model.ActionRunsUsingNode16,
206 model.ActionRunsUsingNode20,
207 model.ActionRunsUsingNode24,
208 model.ActionRunsUsingComposite,
209 }, action.Runs.Using)
210 }
211 }
212}
213
214func setupActionEnv(ctx context.Context, step actionStep, _ *remoteAction) error {
215 rc := step.getRunContext()
216
217 // A few fields in the environment (e.g. GITHUB_ACTION_REPOSITORY)
218 // are dependent on the action. That means we can complete the
219 // setup only after resolving the whole action model and cloning
220 // the action
221 rc.withGithubEnv(ctx, step.getGithubContext(ctx), *step.getEnv())
222 populateEnvsFromSavedState(step.getEnv(), step, rc)
223 populateEnvsFromInput(ctx, step.getEnv(), step.getActionModel(), rc)
224
225 return nil
226}
227
228// https://github.com/nektos/act/issues/228#issuecomment-629709055
229// files in .gitignore are not copied in a Docker container
230// this causes issues with actions that ignore other important resources
231// such as `node_modules` for example
232func removeGitIgnore(ctx context.Context, directory string) error {
233 gitIgnorePath := path.Join(directory, ".gitignore")
234 if _, err := os.Stat(gitIgnorePath); err == nil {
235 // .gitignore exists
236 common.Logger(ctx).Debugf("Removing %s before docker cp", gitIgnorePath)
237 err := os.Remove(gitIgnorePath)
238 if err != nil {
239 return err
240 }
241 }
242 return nil
243}
244
245// TODO: break out parts of function to reduce complexicity
246//
247//nolint:gocyclo
248func execAsDocker(ctx context.Context, step actionStep, actionName, basedir, subpath string, localAction bool, entrypointType string) error {
249 logger := common.Logger(ctx)
250 rc := step.getRunContext()
251 action := step.getActionModel()
252
253 var prepImage common.Executor
254 var image string
255 forcePull := false
256 if strings.HasPrefix(action.Runs.Image, "docker://") {
257 image = strings.TrimPrefix(action.Runs.Image, "docker://")
258 // Apply forcePull only for prebuild docker images
259 forcePull = rc.Config.ForcePull
260 } else {
261 // "-dockeraction" enshures that "./", "./test " won't get converted to "act-:latest", "act-test-:latest" which are invalid docker image names
262 image = fmt.Sprintf("%s-dockeraction:%s", regexp.MustCompile("[^a-zA-Z0-9]").ReplaceAllString(actionName, "-"), "latest")
263 image = fmt.Sprintf("act-%s", strings.TrimLeft(image, "-"))
264 image = strings.ToLower(image)
265 contextDir, fileName := path.Split(path.Join(subpath, action.Runs.Image))
266
267 anyArchExists, err := container.ImageExistsLocally(ctx, image, "any")
268 if err != nil {
269 return err
270 }
271
272 correctArchExists, err := container.ImageExistsLocally(ctx, image, rc.Config.ContainerArchitecture)
273 if err != nil {
274 return err
275 }
276
277 if anyArchExists && !correctArchExists {
278 wasRemoved, err := container.RemoveImage(ctx, image, true, true)
279 if err != nil {
280 return err
281 }
282 if !wasRemoved {
283 return fmt.Errorf("failed to remove image '%s'", image)
284 }
285 }
286
287 if !correctArchExists || rc.Config.ForceRebuild {
288 logger.Debugf("image '%s' for architecture '%s' will be built from context '%s", image, rc.Config.ContainerArchitecture, contextDir)
289 var buildContext io.ReadCloser
290 if localAction {
291 buildContext, err = rc.JobContainer.GetContainerArchive(ctx, contextDir+"/.")
292 if err != nil {
293 return err
294 }
295 defer buildContext.Close()
296 } else if rc.Config.ActionCache != nil {
297 rstep := step.(*stepActionRemote)
298 buildContext, err = rc.Config.ActionCache.GetTarArchive(ctx, rstep.cacheDir, rstep.resolvedSha, contextDir)
299 if err != nil {
300 return err
301 }
302 defer buildContext.Close()
303 }
304 prepImage = container.NewDockerBuildExecutor(container.NewDockerBuildExecutorInput{
305 ContextDir: filepath.Join(basedir, contextDir),
306 Dockerfile: fileName,
307 ImageTag: image,
308 BuildContext: buildContext,
309 Platform: rc.Config.ContainerArchitecture,
310 })
311 } else {
312 logger.Debugf("image '%s' for architecture '%s' already exists", image, rc.Config.ContainerArchitecture)
313 }
314 }
315 eval := rc.NewStepExpressionEvaluator(ctx, step)
316 cmd, err := shellquote.Split(eval.Interpolate(ctx, step.getStepModel().With["args"]))
317 if err != nil {
318 return err
319 }
320 if len(cmd) == 0 {
321 cmd = action.Runs.Args
322 evalDockerArgs(ctx, step, action, &cmd)
323 }
324
325 entrypoint := strings.Fields(eval.Interpolate(ctx, step.getStepModel().With[entrypointType]))
326 if len(entrypoint) == 0 {
327 if entrypointType == "pre-entrypoint" && action.Runs.PreEntrypoint != "" {
328 entrypoint, err = shellquote.Split(action.Runs.PreEntrypoint)
329 if err != nil {
330 return err
331 }
332 } else if entrypointType == "entrypoint" && action.Runs.Entrypoint != "" {
333 entrypoint, err = shellquote.Split(action.Runs.Entrypoint)
334 if err != nil {
335 return err
336 }
337 } else if entrypointType == "post-entrypoint" && action.Runs.PostEntrypoint != "" {
338 entrypoint, err = shellquote.Split(action.Runs.PostEntrypoint)
339 if err != nil {
340 return err
341 }
342 } else {
343 entrypoint = nil
344 }
345 }
346 stepContainer := newStepContainer(ctx, step, image, cmd, entrypoint)
347 return common.NewPipelineExecutor(
348 prepImage,
349 stepContainer.Pull(forcePull),
350 stepContainer.Remove().IfBool(!rc.Config.ReuseContainers),
351 stepContainer.Create(rc.Config.ContainerCapAdd, rc.Config.ContainerCapDrop),
352 stepContainer.Start(true),
353 ).Finally(
354 stepContainer.Remove().IfBool(!rc.Config.ReuseContainers),
355 ).Finally(stepContainer.Close())(ctx)
356}
357
358func evalDockerArgs(ctx context.Context, step step, action *model.Action, cmd *[]string) {
359 rc := step.getRunContext()
360 stepModel := step.getStepModel()
361
362 inputs := make(map[string]string)
363 eval := rc.NewExpressionEvaluator(ctx)
364 // Set Defaults
365 for k, input := range action.Inputs {
366 inputs[k] = eval.Interpolate(ctx, input.Default)
367 }
368 if stepModel.With != nil {
369 for k, v := range stepModel.With {
370 inputs[k] = eval.Interpolate(ctx, v)
371 }
372 }
373 mergeIntoMap(step, step.getEnv(), inputs)
374
375 stepEE := rc.NewStepExpressionEvaluator(ctx, step)
376 for i, v := range *cmd {
377 (*cmd)[i] = stepEE.Interpolate(ctx, v)
378 }
379 mergeIntoMap(step, step.getEnv(), action.Runs.Env)
380
381 ee := rc.NewStepExpressionEvaluator(ctx, step)
382 for k, v := range *step.getEnv() {
383 (*step.getEnv())[k] = ee.Interpolate(ctx, v)
384 }
385}
386
387func newStepContainer(ctx context.Context, step step, image string, cmd []string, entrypoint []string) container.Container {
388 rc := step.getRunContext()
389 stepModel := step.getStepModel()
390 rawLogger := common.Logger(ctx).WithField("raw_output", true)
391 logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool {
392 if rc.Config.LogOutput {
393 rawLogger.Infof("%s", s)
394 } else {
395 rawLogger.Debugf("%s", s)
396 }
397 return true
398 })
399 envList := make([]string, 0)
400 for k, v := range *step.getEnv() {
401 envList = append(envList, fmt.Sprintf("%s=%s", k, v))
402 }
403
404 envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_TOOL_CACHE", "/opt/hostedtoolcache"))
405 envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_OS", "Linux"))
406 envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_ARCH", container.RunnerArch(ctx)))
407 envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_TEMP", "/tmp"))
408
409 binds, mounts := rc.GetBindsAndMounts()
410 networkMode := fmt.Sprintf("container:%s", rc.jobContainerName())
411 var workdir string
412 if rc.IsHostEnv(ctx) {
413 networkMode = "default"
414 ext := container.LinuxContainerEnvironmentExtensions{}
415 workdir = ext.ToContainerPath(rc.Config.Workdir)
416 } else {
417 workdir = rc.JobContainer.ToContainerPath(rc.Config.Workdir)
418 }
419 stepContainer := container.NewContainer(&container.NewContainerInput{
420 Cmd: cmd,
421 Entrypoint: entrypoint,
422 WorkingDir: workdir,
423 Image: image,
424 Username: rc.Config.Secrets["DOCKER_USERNAME"],
425 Password: rc.Config.Secrets["DOCKER_PASSWORD"],
426 Name: createContainerName(rc.jobContainerName(), stepModel.ID),
427 Env: envList,
428 Mounts: mounts,
429 NetworkMode: networkMode,
430 Binds: binds,
431 Stdout: logWriter,
432 Stderr: logWriter,
433 Privileged: rc.Config.Privileged,
434 UsernsMode: rc.Config.UsernsMode,
435 Platform: rc.Config.ContainerArchitecture,
436 Options: rc.Config.ContainerOptions,
437 })
438 return stepContainer
439}
440
441func populateEnvsFromSavedState(env *map[string]string, step actionStep, rc *RunContext) {
442 state, ok := rc.IntraActionState[step.getStepModel().ID]
443 if ok {
444 for name, value := range state {
445 envName := fmt.Sprintf("STATE_%s", name)
446 (*env)[envName] = value
447 }
448 }
449}
450
451func populateEnvsFromInput(ctx context.Context, env *map[string]string, action *model.Action, rc *RunContext) {
452 eval := rc.NewExpressionEvaluator(ctx)
453 for inputID, input := range action.Inputs {
454 envKey := regexp.MustCompile("[^A-Z0-9-]").ReplaceAllString(strings.ToUpper(inputID), "_")
455 envKey = fmt.Sprintf("INPUT_%s", envKey)
456 if _, ok := (*env)[envKey]; !ok {
457 (*env)[envKey] = eval.Interpolate(ctx, input.Default)
458 }
459 }
460}
461
462func getContainerActionPaths(step *model.Step, actionDir string, rc *RunContext) (string, string) {
463 actionName := ""
464 containerActionDir := "."
465 if step.Type() != model.StepTypeUsesActionRemote {
466 actionName = getOsSafeRelativePath(actionDir, rc.Config.Workdir)
467 containerActionDir = rc.JobContainer.ToContainerPath(rc.Config.Workdir) + "/" + actionName
468 actionName = "./" + actionName
469 } else if step.Type() == model.StepTypeUsesActionRemote {
470 actionName = getOsSafeRelativePath(actionDir, rc.ActionCacheDir())
471 containerActionDir = rc.JobContainer.GetActPath() + "/actions/" + actionName
472 }
473
474 if actionName == "" {
475 actionName = filepath.Base(actionDir)
476 if runtime.GOOS == "windows" {
477 actionName = strings.ReplaceAll(actionName, "\\", "/")
478 }
479 }
480 return actionName, containerActionDir
481}
482
483func getOsSafeRelativePath(s, prefix string) string {
484 actionName := strings.TrimPrefix(s, prefix)
485 if runtime.GOOS == "windows" {
486 actionName = strings.ReplaceAll(actionName, "\\", "/")
487 }
488 actionName = strings.TrimPrefix(actionName, "/")
489
490 return actionName
491}
492
493func shouldRunPreStep(step actionStep) common.Conditional {
494 return func(ctx context.Context) bool {
495 log := common.Logger(ctx)
496
497 if step.getActionModel() == nil {
498 log.Debugf("skip pre step for '%s': no action model available", step.getStepModel())
499 return false
500 }
501
502 return true
503 }
504}
505
506func hasPreStep(step actionStep) common.Conditional {
507 return func(_ context.Context) bool {
508 action := step.getActionModel()
509 return action.Runs.Using.IsComposite() ||
510 (action.Runs.Using.IsNode() &&
511 action.Runs.Pre != "") ||
512 (action.Runs.Using.IsDocker() &&
513 action.Runs.PreEntrypoint != "")
514 }
515}
516
517func runPreStep(step actionStep) common.Executor {
518 return func(ctx context.Context) error {
519 logger := common.Logger(ctx)
520 logger.Debugf("run pre step for '%s'", step.getStepModel())
521
522 rc := step.getRunContext()
523 stepModel := step.getStepModel()
524 action := step.getActionModel()
525
526 // defaults in pre steps were missing, however provided inputs are available
527 populateEnvsFromInput(ctx, step.getEnv(), action, rc)
528
529 // todo: refactor into step
530 var actionDir string
531 var actionPath string
532 var remoteAction *stepActionRemote
533 if remote, ok := step.(*stepActionRemote); ok {
534 actionPath = newRemoteAction(stepModel.Uses).Path
535 actionDir = fmt.Sprintf("%s/%s", rc.ActionCacheDir(), safeFilename(stepModel.Uses))
536 remoteAction = remote
537 } else {
538 actionDir = filepath.Join(rc.Config.Workdir, stepModel.Uses)
539 actionPath = ""
540 }
541
542 actionLocation := ""
543 if actionPath != "" {
544 actionLocation = path.Join(actionDir, actionPath)
545 } else {
546 actionLocation = actionDir
547 }
548
549 actionName, containerActionDir := getContainerActionPaths(stepModel, actionLocation, rc)
550
551 x := action.Runs.Using
552 switch {
553 case x.IsNode():
554 if err := maybeCopyToActionDir(ctx, step, actionDir, actionPath, containerActionDir); err != nil {
555 return err
556 }
557
558 containerArgs := []string{rc.GetNodeToolForAction(ctx, string(action.Runs.Using)), path.Join(containerActionDir, action.Runs.Pre)}
559 logger.Debugf("executing remote job container: %s", containerArgs)
560
561 rc.ApplyExtraPath(ctx, step.getEnv())
562
563 return rc.execJobContainer(containerArgs, *step.getEnv(), "", "")(ctx)
564
565 case x.IsDocker():
566 if remoteAction == nil {
567 actionDir = ""
568 actionPath = containerActionDir
569 }
570 return execAsDocker(ctx, step, actionName, actionDir, actionPath, remoteAction == nil, "pre-entrypoint")
571
572 case x.IsComposite():
573 if step.getCompositeSteps() == nil {
574 step.getCompositeRunContext(ctx)
575 }
576
577 if steps := step.getCompositeSteps(); steps != nil && steps.pre != nil {
578 return steps.pre(ctx)
579 }
580 return fmt.Errorf("missing steps in composite action")
581
582 default:
583 return nil
584 }
585 }
586}
587
588func shouldRunPostStep(step actionStep) common.Conditional {
589 return func(ctx context.Context) bool {
590 log := common.Logger(ctx)
591 stepResults := step.getRunContext().getStepsContext()
592 stepResult := stepResults[step.getStepModel().ID]
593
594 if stepResult == nil {
595 log.WithField("stepResult", model.StepStatusSkipped).Debugf("skipping post step for '%s'; step was not executed", step.getStepModel())
596 return false
597 }
598
599 if stepResult.Conclusion == model.StepStatusSkipped {
600 log.WithField("stepResult", model.StepStatusSkipped).Debugf("skipping post step for '%s'; main step was skipped", step.getStepModel())
601 return false
602 }
603
604 if step.getActionModel() == nil {
605 log.WithField("stepResult", model.StepStatusSkipped).Debugf("skipping post step for '%s': no action model available", step.getStepModel())
606 return false
607 }
608
609 return true
610 }
611}
612
613func hasPostStep(step actionStep) common.Conditional {
614 return func(_ context.Context) bool {
615 action := step.getActionModel()
616 return action.Runs.Using.IsComposite() ||
617 (action.Runs.Using.IsNode() &&
618 action.Runs.Post != "") ||
619 (action.Runs.Using.IsDocker() &&
620 action.Runs.PostEntrypoint != "")
621 }
622}
623
624func runPostStep(step actionStep) common.Executor {
625 return func(ctx context.Context) error {
626 logger := common.Logger(ctx)
627 logger.Debugf("run post step for '%s'", step.getStepModel())
628
629 rc := step.getRunContext()
630 stepModel := step.getStepModel()
631 action := step.getActionModel()
632
633 // todo: refactor into step
634 var actionDir string
635 var actionPath string
636 var remoteAction *stepActionRemote
637 if remote, ok := step.(*stepActionRemote); ok {
638 actionPath = newRemoteAction(stepModel.Uses).Path
639 actionDir = fmt.Sprintf("%s/%s", rc.ActionCacheDir(), safeFilename(stepModel.Uses))
640 remoteAction = remote
641 } else {
642 actionDir = filepath.Join(rc.Config.Workdir, stepModel.Uses)
643 actionPath = ""
644 }
645
646 actionLocation := ""
647 if actionPath != "" {
648 actionLocation = path.Join(actionDir, actionPath)
649 } else {
650 actionLocation = actionDir
651 }
652
653 actionName, containerActionDir := getContainerActionPaths(stepModel, actionLocation, rc)
654
655 x := action.Runs.Using
656 switch {
657 case x.IsNode():
658 populateEnvsFromSavedState(step.getEnv(), step, rc)
659 populateEnvsFromInput(ctx, step.getEnv(), step.getActionModel(), rc)
660
661 containerArgs := []string{rc.GetNodeToolForAction(ctx, string(action.Runs.Using)), path.Join(containerActionDir, action.Runs.Post)}
662 logger.Debugf("executing remote job container: %s", containerArgs)
663
664 rc.ApplyExtraPath(ctx, step.getEnv())
665
666 return rc.execJobContainer(containerArgs, *step.getEnv(), "", "")(ctx)
667
668 case x.IsDocker():
669 if remoteAction == nil {
670 actionDir = ""
671 actionPath = containerActionDir
672 }
673 return execAsDocker(ctx, step, actionName, actionDir, actionPath, remoteAction == nil, "post-entrypoint")
674
675 case x.IsComposite():
676 if err := maybeCopyToActionDir(ctx, step, actionDir, actionPath, containerActionDir); err != nil {
677 return err
678 }
679
680 if steps := step.getCompositeSteps(); steps != nil && steps.post != nil {
681 return steps.post(ctx)
682 }
683 return fmt.Errorf("missing steps in composite action")
684
685 default:
686 return nil
687 }
688 }
689}
Deleted act/pkg/runner/action_cache.go
@@ -1,228 +0,0 @@
1package runner
2
3import (
4 "archive/tar"
5 "context"
6 "crypto/rand"
7 "encoding/hex"
8 "errors"
9 "fmt"
10 "io"
11 "io/fs"
12 "path"
13 "strings"
14 "time"
15
16 git "github.com/go-git/go-git/v5"
17 config "github.com/go-git/go-git/v5/config"
18 "github.com/go-git/go-git/v5/plumbing"
19 "github.com/go-git/go-git/v5/plumbing/object"
20 "github.com/go-git/go-git/v5/plumbing/transport"
21 "github.com/go-git/go-git/v5/plumbing/transport/http"
22 "github.com/nektos/act/pkg/common"
23)
24
25type ActionCache interface {
26 Fetch(ctx context.Context, cacheDir, url, ref, token string) (string, error)
27 GetTarArchive(ctx context.Context, cacheDir, sha, includePrefix string) (io.ReadCloser, error)
28}
29
30type GoGitActionCache struct {
31 Path string
32}
33
34func (c GoGitActionCache) Fetch(ctx context.Context, cacheDir, url, ref, token string) (string, error) {
35 logger := common.Logger(ctx)
36
37 gitPath := path.Join(c.Path, safeFilename(cacheDir)+".git")
38
39 logger.Infof("GoGitActionCache fetch %s with ref %s at %s", url, ref, gitPath)
40
41 gogitrepo, err := git.PlainInit(gitPath, true)
42 if errors.Is(err, git.ErrRepositoryAlreadyExists) {
43 logger.Debugf("GoGitActionCache cache hit %s with ref %s at %s", url, ref, gitPath)
44 gogitrepo, err = git.PlainOpen(gitPath)
45 }
46 if err != nil {
47 return "", fmt.Errorf("GoGitActionCache failed to open bare git %s with ref %s at %s: %w", url, ref, gitPath, err)
48 }
49 tmpBranch := make([]byte, 12)
50 if _, err := rand.Read(tmpBranch); err != nil {
51 return "", fmt.Errorf("GoGitActionCache failed to generate random tmp branch %s with ref %s at %s: %w", url, ref, gitPath, err)
52 }
53 branchName := hex.EncodeToString(tmpBranch)
54
55 var auth transport.AuthMethod
56 if token != "" {
57 auth = &http.BasicAuth{
58 Username: "token",
59 Password: token,
60 }
61 }
62 remote, err := gogitrepo.CreateRemoteAnonymous(&config.RemoteConfig{
63 Name: "anonymous",
64 URLs: []string{
65 url,
66 },
67 })
68 if err != nil {
69 return "", fmt.Errorf("GoGitActionCache failed to create remote %s with ref %s at %s: %w", url, ref, gitPath, err)
70 }
71 defer func() {
72 _ = gogitrepo.DeleteBranch(branchName)
73 }()
74 if err := remote.FetchContext(ctx, &git.FetchOptions{
75 RefSpecs: []config.RefSpec{
76 config.RefSpec(ref + ":" + branchName),
77 },
78 Auth: auth,
79 Force: true,
80 Depth: 1,
81 }); err != nil {
82 return "", fmt.Errorf("GoGitActionCache failed to fetch %s with ref %s at %s: %w", url, ref, gitPath, err)
83 }
84 hash, err := gogitrepo.ResolveRevision(plumbing.Revision(branchName))
85 if err != nil {
86 return "", fmt.Errorf("GoGitActionCache failed to resolve sha %s with ref %s at %s: %w", url, ref, gitPath, err)
87 }
88 logger.Infof("GoGitActionCache fetch %s with ref %s at %s resolved to %s", url, ref, gitPath, hash.String())
89 return hash.String(), nil
90}
91
92type GitFileInfo struct {
93 name string
94 size int64
95 modTime time.Time
96 isDir bool
97 mode fs.FileMode
98}
99
100// IsDir implements fs.FileInfo.
101func (g *GitFileInfo) IsDir() bool {
102 return g.isDir
103}
104
105// ModTime implements fs.FileInfo.
106func (g *GitFileInfo) ModTime() time.Time {
107 return g.modTime
108}
109
110// Mode implements fs.FileInfo.
111func (g *GitFileInfo) Mode() fs.FileMode {
112 return g.mode
113}
114
115// Name implements fs.FileInfo.
116func (g *GitFileInfo) Name() string {
117 return g.name
118}
119
120// Size implements fs.FileInfo.
121func (g *GitFileInfo) Size() int64 {
122 return g.size
123}
124
125// Sys implements fs.FileInfo.
126func (g *GitFileInfo) Sys() any {
127 return nil
128}
129
130func (c GoGitActionCache) GetTarArchive(ctx context.Context, cacheDir, sha, includePrefix string) (io.ReadCloser, error) {
131 logger := common.Logger(ctx)
132
133 gitPath := path.Join(c.Path, safeFilename(cacheDir)+".git")
134
135 logger.Infof("GoGitActionCache get content %s with sha %s subpath '%s' at %s", cacheDir, sha, includePrefix, gitPath)
136
137 gogitrepo, err := git.PlainOpen(gitPath)
138 if err != nil {
139 return nil, fmt.Errorf("GoGitActionCache failed to open bare git %s with sha %s subpath '%s' at %s: %w", cacheDir, sha, includePrefix, gitPath, err)
140 }
141 commit, err := gogitrepo.CommitObject(plumbing.NewHash(sha))
142 if err != nil {
143 return nil, fmt.Errorf("GoGitActionCache failed to get commit %s with sha %s subpath '%s' at %s: %w", cacheDir, sha, includePrefix, gitPath, err)
144 }
145 t, err := commit.Tree()
146 if err != nil {
147 return nil, fmt.Errorf("GoGitActionCache failed to open git tree %s with sha %s subpath '%s' at %s: %w", cacheDir, sha, includePrefix, gitPath, err)
148 }
149 files, err := commit.Files()
150 if err != nil {
151 return nil, fmt.Errorf("GoGitActionCache failed to list files %s with sha %s subpath '%s' at %s: %w", cacheDir, sha, includePrefix, gitPath, err)
152 }
153 rpipe, wpipe := io.Pipe()
154 // Interrupt io.Copy using ctx
155 ch := make(chan int, 1)
156 go func() {
157 select {
158 case <-ctx.Done():
159 wpipe.CloseWithError(ctx.Err())
160 case <-ch:
161 }
162 }()
163 go func() {
164 defer wpipe.Close()
165 defer close(ch)
166 tw := tar.NewWriter(wpipe)
167 cleanIncludePrefix := path.Clean(includePrefix)
168 wpipe.CloseWithError(files.ForEach(func(f *object.File) error {
169 return actionCacheCopyFileOrDir(ctx, cleanIncludePrefix, t, tw, f.Name, f)
170 }))
171 }()
172 return rpipe, err
173}
174
175func actionCacheCopyFileOrDir(ctx context.Context, cleanIncludePrefix string, t *object.Tree, tw *tar.Writer, origin string, f *object.File) error {
176 if err := ctx.Err(); err != nil {
177 return err
178 }
179 name := origin
180 if strings.HasPrefix(name, cleanIncludePrefix+"/") {
181 name = name[len(cleanIncludePrefix)+1:]
182 } else if cleanIncludePrefix != "." && name != cleanIncludePrefix {
183 return nil
184 }
185 fmode, err := f.Mode.ToOSFileMode()
186 if err != nil {
187 return err
188 }
189 if fmode&fs.ModeSymlink == fs.ModeSymlink {
190 content, err := f.Contents()
191 if err != nil {
192 return err
193 }
194
195 destPath := path.Join(path.Dir(f.Name), content)
196
197 subtree, err := t.Tree(destPath)
198 if err == nil {
199 return subtree.Files().ForEach(func(ft *object.File) error {
200 return actionCacheCopyFileOrDir(ctx, cleanIncludePrefix, t, tw, origin+strings.TrimPrefix(ft.Name, f.Name), f)
201 })
202 }
203
204 f, err := t.File(destPath)
205 if err != nil {
206 return fmt.Errorf("%s (%s): %w", destPath, origin, err)
207 }
208 return actionCacheCopyFileOrDir(ctx, cleanIncludePrefix, t, tw, origin, f)
209 }
210 header, err := tar.FileInfoHeader(&GitFileInfo{
211 name: name,
212 mode: fmode,
213 size: f.Size,
214 }, "")
215 if err != nil {
216 return err
217 }
218 err = tw.WriteHeader(header)
219 if err != nil {
220 return err
221 }
222 reader, err := f.Reader()
223 if err != nil {
224 return err
225 }
226 _, err = io.Copy(tw, reader)
227 return err
228}
Deleted act/pkg/runner/action_cache_offline_mode.go
@@ -1,47 +0,0 @@
1package runner
2
3import (
4 "context"
5 "io"
6 "path"
7
8 git "github.com/go-git/go-git/v5"
9 "github.com/go-git/go-git/v5/plumbing"
10 "github.com/nektos/act/pkg/common"
11)
12
13type GoGitActionCacheOfflineMode struct {
14 Parent GoGitActionCache
15}
16
17func (c GoGitActionCacheOfflineMode) Fetch(ctx context.Context, cacheDir, url, ref, token string) (string, error) {
18 logger := common.Logger(ctx)
19
20 gitPath := path.Join(c.Parent.Path, safeFilename(cacheDir)+".git")
21
22 logger.Infof("GoGitActionCacheOfflineMode fetch content %s with ref %s at %s", url, ref, gitPath)
23
24 sha, fetchErr := c.Parent.Fetch(ctx, cacheDir, url, ref, token)
25 gogitrepo, err := git.PlainOpen(gitPath)
26 if err != nil {
27 return "", fetchErr
28 }
29 refName := plumbing.ReferenceName("refs/action-cache-offline/" + ref)
30 r, err := gogitrepo.Reference(refName, true)
31 if fetchErr == nil {
32 if err != nil || sha != r.Hash().String() {
33 if err == nil {
34 refName = r.Name()
35 }
36 ref := plumbing.NewHashReference(refName, plumbing.NewHash(sha))
37 _ = gogitrepo.Storer.SetReference(ref)
38 }
39 } else if err == nil {
40 return r.Hash().String(), nil
41 }
42 return sha, fetchErr
43}
44
45func (c GoGitActionCacheOfflineMode) GetTarArchive(ctx context.Context, cacheDir, sha, includePrefix string) (io.ReadCloser, error) {
46 return c.Parent.GetTarArchive(ctx, cacheDir, sha, includePrefix)
47}
Deleted act/pkg/runner/action_composite.go
@@ -1,236 +0,0 @@
1package runner
2
3import (
4 "context"
5 "fmt"
6 "regexp"
7 "strings"
8
9 "github.com/nektos/act/pkg/common"
10 "github.com/nektos/act/pkg/model"
11)
12
13func evaluateCompositeInputAndEnv(ctx context.Context, parent *RunContext, step actionStep) map[string]string {
14 env := make(map[string]string)
15 stepEnv := *step.getEnv()
16 for k, v := range stepEnv {
17 // do not set current inputs into composite action
18 // the required inputs are added in the second loop
19 if !strings.HasPrefix(k, "INPUT_") {
20 env[k] = v
21 }
22 }
23
24 ee := parent.NewStepExpressionEvaluator(ctx, step)
25
26 for inputID, input := range step.getActionModel().Inputs {
27 envKey := regexp.MustCompile("[^A-Z0-9-]").ReplaceAllString(strings.ToUpper(inputID), "_")
28 envKey = fmt.Sprintf("INPUT_%s", strings.ToUpper(envKey))
29
30 // lookup if key is defined in the step but the already
31 // evaluated value from the environment
32 _, defined := step.getStepModel().With[inputID]
33 if value, ok := stepEnv[envKey]; defined && ok {
34 env[envKey] = value
35 } else {
36 // defaults could contain expressions
37 env[envKey] = ee.Interpolate(ctx, input.Default)
38 }
39 }
40 gh := step.getGithubContext(ctx)
41 env["GITHUB_ACTION_REPOSITORY"] = gh.ActionRepository
42 env["GITHUB_ACTION_REF"] = gh.ActionRef
43
44 return env
45}
46
47func newCompositeRunContext(ctx context.Context, parent *RunContext, step actionStep, actionPath string) *RunContext {
48 env := evaluateCompositeInputAndEnv(ctx, parent, step)
49
50 // run with the global config but without secrets
51 configCopy := *(parent.Config)
52 configCopy.Secrets = nil
53
54 // create a run context for the composite action to run in
55 compositerc := &RunContext{
56 Name: parent.Name,
57 JobName: parent.JobName,
58 Run: &model.Run{
59 JobID: parent.Run.JobID,
60 Workflow: &model.Workflow{
61 Name: parent.Run.Workflow.Name,
62 Jobs: map[string]*model.Job{
63 parent.Run.JobID: {},
64 },
65 },
66 },
67 Config: &configCopy,
68 StepResults: map[string]*model.StepResult{},
69 JobContainer: parent.JobContainer,
70 ActionPath: actionPath,
71 Env: env,
72 GlobalEnv: parent.GlobalEnv,
73 Masks: parent.Masks,
74 ExtraPath: parent.ExtraPath,
75 Parent: parent,
76 EventJSON: parent.EventJSON,
77 nodeToolFullPath: parent.nodeToolFullPath,
78 }
79 compositerc.ExprEval = compositerc.NewExpressionEvaluator(ctx)
80
81 return compositerc
82}
83
84func execAsComposite(step actionStep) common.Executor {
85 rc := step.getRunContext()
86 action := step.getActionModel()
87
88 return func(ctx context.Context) error {
89 compositeRC := step.getCompositeRunContext(ctx)
90
91 steps := step.getCompositeSteps()
92
93 if steps == nil || steps.main == nil {
94 return fmt.Errorf("missing steps in composite action")
95 }
96
97 ctx = WithCompositeLogger(ctx, &compositeRC.Masks)
98
99 err := steps.main(ctx)
100
101 // Map outputs from composite RunContext to job RunContext
102 eval := compositeRC.NewExpressionEvaluator(ctx)
103 for outputName, output := range action.Outputs {
104 rc.setOutput(ctx, map[string]string{
105 "name": outputName,
106 }, eval.Interpolate(ctx, output.Value))
107 }
108
109 rc.Masks = append(rc.Masks, compositeRC.Masks...)
110 rc.ExtraPath = compositeRC.ExtraPath
111 // compositeRC.Env is dirty, contains INPUT_ and merged step env, only rely on compositeRC.GlobalEnv
112 mergeIntoMap := mergeIntoMapCaseSensitive
113 if rc.JobContainer.IsEnvironmentCaseInsensitive() {
114 mergeIntoMap = mergeIntoMapCaseInsensitive
115 }
116 if rc.GlobalEnv == nil {
117 rc.GlobalEnv = map[string]string{}
118 }
119 mergeIntoMap(rc.GlobalEnv, compositeRC.GlobalEnv)
120 mergeIntoMap(rc.Env, compositeRC.GlobalEnv)
121
122 return err
123 }
124}
125
126type compositeSteps struct {
127 pre common.Executor
128 main common.Executor
129 post common.Executor
130}
131
132// Executor returns a pipeline executor for all the steps in the job
133func (rc *RunContext) compositeExecutor(action *model.Action) *compositeSteps {
134 steps := make([]common.Executor, 0)
135 preSteps := make([]common.Executor, 0)
136 var postExecutor common.Executor
137
138 sf := &stepFactoryImpl{}
139
140 for i, step := range action.Runs.Steps {
141 if step.ID == "" {
142 step.ID = fmt.Sprintf("%d", i)
143 }
144
145 // create a copy of the step, since this composite action could
146 // run multiple times and we might modify the instance
147 stepcopy := step
148
149 step, err := sf.newStep(&stepcopy, rc)
150 if err != nil {
151 return &compositeSteps{
152 main: common.NewErrorExecutor(err),
153 }
154 }
155
156 stepID := step.getStepModel().ID
157 stepPre := rc.newCompositeCommandExecutor(step.pre())
158 preSteps = append(preSteps, newCompositeStepLogExecutor(stepPre, stepID))
159
160 steps = append(steps, func(ctx context.Context) error {
161 ctx = WithCompositeStepLogger(ctx, stepID)
162 logger := common.Logger(ctx)
163 err := rc.newCompositeCommandExecutor(step.main())(ctx)
164
165 if err != nil {
166 logger.Errorf("%v", err)
167 common.SetJobError(ctx, err)
168 } else if ctx.Err() != nil {
169 logger.Errorf("%v", ctx.Err())
170 common.SetJobError(ctx, ctx.Err())
171 }
172 return nil
173 })
174
175 // run the post executor in reverse order
176 if postExecutor != nil {
177 stepPost := rc.newCompositeCommandExecutor(step.post())
178 postExecutor = newCompositeStepLogExecutor(stepPost.Finally(postExecutor), stepID)
179 } else {
180 stepPost := rc.newCompositeCommandExecutor(step.post())
181 postExecutor = newCompositeStepLogExecutor(stepPost, stepID)
182 }
183 }
184
185 steps = append(steps, common.JobError)
186 return &compositeSteps{
187 pre: func(ctx context.Context) error {
188 return common.NewPipelineExecutor(preSteps...)(common.WithJobErrorContainer(ctx))
189 },
190 main: func(ctx context.Context) error {
191 return common.NewPipelineExecutor(steps...)(common.WithJobErrorContainer(ctx))
192 },
193 post: postExecutor,
194 }
195}
196
197func (rc *RunContext) newCompositeCommandExecutor(executor common.Executor) common.Executor {
198 return func(ctx context.Context) error {
199 ctx = WithCompositeLogger(ctx, &rc.Masks)
200
201 // We need to inject a composite RunContext related command
202 // handler into the current running job container
203 // We need this, to support scoping commands to the composite action
204 // executing.
205 rawLogger := common.Logger(ctx).WithField("raw_output", true)
206 logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool {
207 if rc.Config.LogOutput {
208 rawLogger.Infof("%s", s)
209 } else {
210 rawLogger.Debugf("%s", s)
211 }
212 return true
213 })
214
215 oldout, olderr := rc.JobContainer.ReplaceLogWriter(logWriter, logWriter)
216 defer rc.JobContainer.ReplaceLogWriter(oldout, olderr)
217
218 return executor(ctx)
219 }
220}
221
222func newCompositeStepLogExecutor(runStep common.Executor, stepID string) common.Executor {
223 return func(ctx context.Context) error {
224 ctx = WithCompositeStepLogger(ctx, stepID)
225 logger := common.Logger(ctx)
226 err := runStep(ctx)
227 if err != nil {
228 logger.Errorf("%v", err)
229 common.SetJobError(ctx, err)
230 } else if ctx.Err() != nil {
231 logger.Errorf("%v", ctx.Err())
232 common.SetJobError(ctx, ctx.Err())
233 }
234 return nil
235 }
236}
Deleted act/pkg/runner/command.go
@@ -1,210 +0,0 @@
1package runner
2
3import (
4 "context"
5 "regexp"
6 "strings"
7
8 "github.com/nektos/act/pkg/common"
9
10 "github.com/sirupsen/logrus"
11)
12
13var commandPatternGA *regexp.Regexp
14var commandPatternADO *regexp.Regexp
15
16func init() {
17 commandPatternGA = regexp.MustCompile("^::([^ ]+)( (.+))?::([^\r\n]*)[\r\n]+$")
18 commandPatternADO = regexp.MustCompile("^##\\[([^ ]+)( (.+))?]([^\r\n]*)[\r\n]+$")
19}
20
21func tryParseRawActionCommand(line string) (command string, kvPairs map[string]string, arg string, ok bool) {
22 if m := commandPatternGA.FindStringSubmatch(line); m != nil {
23 command = m[1]
24 kvPairs = parseKeyValuePairs(m[3], ",")
25 arg = m[4]
26 ok = true
27 } else if m := commandPatternADO.FindStringSubmatch(line); m != nil {
28 command = m[1]
29 kvPairs = parseKeyValuePairs(m[3], ";")
30 arg = m[4]
31 ok = true
32 }
33 return
34}
35
36func (rc *RunContext) commandHandler(ctx context.Context) common.LineHandler {
37 logger := common.Logger(ctx)
38 resumeCommand := ""
39 return func(line string) bool {
40 command, kvPairs, arg, ok := tryParseRawActionCommand(line)
41 if !ok {
42 return true
43 }
44
45 if resumeCommand != "" && command != resumeCommand {
46 logger.WithFields(logrus.Fields{"command": "ignored", "raw": line}).Infof(" \U00002699 %s", line)
47 return false
48 }
49 arg = unescapeCommandData(arg)
50 kvPairs = unescapeKvPairs(kvPairs)
51 if command == "error" || command == "warning" || command == "notice" {
52 kvPairs["level"] = command
53 rc.gitDataEmit(GitDataEvent{Type: "annotation", StepID: rc.CurrentStep, Text: arg, Properties: kvPairs})
54 }
55 defCommandLogger := logger.WithFields(logrus.Fields{"command": command, "kvPairs": kvPairs, "arg": arg, "raw": line})
56 switch command {
57 case "set-env":
58 if rc.Env["ACTIONS_ALLOW_UNSECURE_COMMANDS"] != "true" {
59 defCommandLogger.Errorf("The `set-env` command is disabled. Please upgrade to using Environment Files or opt into unsafe commands by setting the `ACTIONS_ALLOW_UNSECURE_COMMANDS` environment variable to `true`.")
60 break
61 }
62 rc.setEnv(ctx, kvPairs, arg)
63 case "set-output":
64 rc.setOutput(ctx, kvPairs, arg)
65 case "add-path":
66 if rc.Env["ACTIONS_ALLOW_UNSECURE_COMMANDS"] != "true" {
67 defCommandLogger.Errorf("The `add-path` command is disabled. Please upgrade to using Environment Files or opt into unsafe commands by setting the `ACTIONS_ALLOW_UNSECURE_COMMANDS` environment variable to `true`.")
68 break
69 }
70 rc.addPath(ctx, arg)
71 case "debug":
72 defCommandLogger.Debugf(" \U0001F4AC %s", line)
73 case "warning":
74 defCommandLogger.Warnf(" \U0001F6A7 %s", line)
75 case "error":
76 defCommandLogger.Errorf(" \U00002757 %s", line)
77 case "add-mask":
78 rc.AddMask(arg)
79 defCommandLogger.Infof(" \U00002699 %s", "***")
80 case "stop-commands":
81 resumeCommand = arg
82 defCommandLogger.Infof(" \U00002699 %s", line)
83 case resumeCommand:
84 resumeCommand = ""
85 defCommandLogger.Infof(" \U00002699 %s", line)
86 case "save-state":
87 defCommandLogger.Infof(" \U0001f4be %s", line)
88 rc.saveState(ctx, kvPairs, arg)
89 case "add-matcher":
90 defCommandLogger.Infof(" \U00002753 add-matcher %s", arg)
91 default:
92 defCommandLogger.Infof(" \U00002753 %s", line)
93 }
94
95 return false
96 }
97}
98
99func (rc *RunContext) setEnv(ctx context.Context, kvPairs map[string]string, arg string) {
100 name := kvPairs["name"]
101 common.Logger(ctx).WithFields(logrus.Fields{"command": "set-env", "name": name, "arg": arg}).Infof(" \U00002699 ::set-env:: %s=%s", name, arg)
102 if rc.Env == nil {
103 rc.Env = make(map[string]string)
104 }
105 if rc.GlobalEnv == nil {
106 rc.GlobalEnv = map[string]string{}
107 }
108 newenv := map[string]string{
109 name: arg,
110 }
111 mergeIntoMap := mergeIntoMapCaseSensitive
112 if rc.JobContainer != nil && rc.JobContainer.IsEnvironmentCaseInsensitive() {
113 mergeIntoMap = mergeIntoMapCaseInsensitive
114 }
115 mergeIntoMap(rc.Env, newenv)
116 mergeIntoMap(rc.GlobalEnv, newenv)
117}
118func (rc *RunContext) setOutput(ctx context.Context, kvPairs map[string]string, arg string) {
119 if kvPairs["name"] == "artifact-url" && rc.Config.GitDataRuntimeURL != "" && strings.Contains(arg, "/artifacts/") {
120 id := arg[strings.LastIndex(arg, "/artifacts/")+len("/artifacts/"):]
121 server, _ := rc.Config.GitDataGithub["server_url"].(string)
122 repo, _ := rc.Config.GitDataGithub["repository"].(string)
123 if server != "" && repo != "" {
124 arg = strings.TrimRight(server, "/") + "/api/v1/repos/" + repo + "/pipelines/actions-artifacts/" + id + "/download"
125 }
126 }
127 logger := common.Logger(ctx)
128 stepID := rc.CurrentStep
129 outputName := kvPairs["name"]
130 if outputMapping, ok := rc.OutputMappings[MappableOutput{StepID: stepID, OutputName: outputName}]; ok {
131 stepID = outputMapping.StepID
132 outputName = outputMapping.OutputName
133 }
134
135 result, ok := rc.StepResults[stepID]
136 if !ok {
137 logger.Infof(" \U00002757 no outputs used step '%s'", stepID)
138 return
139 }
140
141 logger.WithFields(logrus.Fields{"command": "set-output", "name": outputName, "arg": arg}).Infof(" \U00002699 ::set-output:: %s=%s", outputName, arg)
142 result.Outputs[outputName] = arg
143}
144func (rc *RunContext) addPath(ctx context.Context, arg string) {
145 common.Logger(ctx).WithFields(logrus.Fields{"command": "add-path", "arg": arg}).Infof(" \U00002699 ::add-path:: %s", arg)
146 extraPath := []string{arg}
147 for _, v := range rc.ExtraPath {
148 if v != arg {
149 extraPath = append(extraPath, v)
150 }
151 }
152 rc.ExtraPath = extraPath
153}
154
155func parseKeyValuePairs(kvPairs string, separator string) map[string]string {
156 rtn := make(map[string]string)
157 kvPairList := strings.Split(kvPairs, separator)
158 for _, kvPair := range kvPairList {
159 kv := strings.Split(kvPair, "=")
160 if len(kv) == 2 {
161 rtn[kv[0]] = kv[1]
162 }
163 }
164 return rtn
165}
166func unescapeCommandData(arg string) string {
167 escapeMap := map[string]string{
168 "%25": "%",
169 "%0D": "\r",
170 "%0A": "\n",
171 }
172 for k, v := range escapeMap {
173 arg = strings.ReplaceAll(arg, k, v)
174 }
175 return arg
176}
177func unescapeCommandProperty(arg string) string {
178 escapeMap := map[string]string{
179 "%25": "%",
180 "%0D": "\r",
181 "%0A": "\n",
182 "%3A": ":",
183 "%2C": ",",
184 }
185 for k, v := range escapeMap {
186 arg = strings.ReplaceAll(arg, k, v)
187 }
188 return arg
189}
190func unescapeKvPairs(kvPairs map[string]string) map[string]string {
191 for k, v := range kvPairs {
192 kvPairs[k] = unescapeCommandProperty(v)
193 }
194 return kvPairs
195}
196
197func (rc *RunContext) saveState(_ context.Context, kvPairs map[string]string, arg string) {
198 stepID := rc.CurrentStep
199 if stepID != "" {
200 if rc.IntraActionState == nil {
201 rc.IntraActionState = map[string]map[string]string{}
202 }
203 state, ok := rc.IntraActionState[stepID]
204 if !ok {
205 state = map[string]string{}
206 rc.IntraActionState[stepID] = state
207 }
208 state[kvPairs["name"]] = arg
209 }
210}
Deleted act/pkg/runner/expression.go
@@ -1,602 +0,0 @@
1package runner
2
3import (
4 "bytes"
5 "context"
6 "fmt"
7 "path"
8 "reflect"
9 "regexp"
10 "strings"
11 "time"
12
13 _ "embed"
14
15 "github.com/nektos/act/pkg/common"
16 "github.com/nektos/act/pkg/container"
17 "github.com/nektos/act/pkg/exprparser"
18 "github.com/nektos/act/pkg/model"
19 "gopkg.in/yaml.v3"
20)
21
22// ExpressionEvaluator is the interface for evaluating expressions
23type ExpressionEvaluator interface {
24 evaluate(context.Context, string, exprparser.DefaultStatusCheck) (interface{}, error)
25 EvaluateYamlNode(context.Context, *yaml.Node) error
26 Interpolate(context.Context, string) string
27}
28
29// NewExpressionEvaluator creates a new evaluator
30func (rc *RunContext) NewExpressionEvaluator(ctx context.Context) ExpressionEvaluator {
31 return rc.NewExpressionEvaluatorWithEnv(ctx, rc.GetEnv())
32}
33
34func (rc *RunContext) NewExpressionEvaluatorWithEnv(ctx context.Context, env map[string]string) ExpressionEvaluator {
35 var workflowCallResult map[string]*model.WorkflowCallResult
36
37 // todo: cleanup EvaluationEnvironment creation
38 using := make(map[string]exprparser.Needs)
39 strategy := make(map[string]interface{})
40 if rc.Run != nil {
41 job := rc.Run.Job()
42 if job != nil && job.Strategy != nil {
43 strategy["fail-fast"] = job.Strategy.FailFast
44 strategy["max-parallel"] = job.Strategy.MaxParallel
45 }
46
47 jobs := rc.Run.Workflow.Jobs
48 jobNeeds := rc.Run.Job().Needs()
49
50 for _, needs := range jobNeeds {
51 using[needs] = exprparser.Needs{
52 Outputs: jobs[needs].Outputs,
53 Result: jobs[needs].Result,
54 }
55 }
56
57 // only setup jobs context in case of workflow_call
58 // and existing expression evaluator (this means, jobs are at
59 // least ready to run)
60 if rc.caller != nil && rc.ExprEval != nil {
61 workflowCallResult = map[string]*model.WorkflowCallResult{}
62
63 for jobName, job := range jobs {
64 result := model.WorkflowCallResult{
65 Outputs: map[string]string{},
66 }
67 for k, v := range job.Outputs {
68 result.Outputs[k] = v
69 }
70 workflowCallResult[jobName] = &result
71 }
72 }
73 }
74
75 ghc := rc.getGithubContext(ctx)
76 inputs := getEvaluatorInputs(ctx, rc, nil, ghc)
77 if rc.Config.GitDataJobs != nil {
78 workflowCallResult = rc.Config.GitDataJobs
79 }
80
81 ee := &exprparser.EvaluationEnvironment{
82 Github: ghc,
83 Env: env,
84 Job: rc.getJobContext(),
85 Jobs: &workflowCallResult,
86 // todo: should be unavailable
87 // but required to interpolate/evaluate the step outputs on the job
88 Steps: rc.getStepsContext(),
89 Secrets: getWorkflowSecrets(ctx, rc),
90 Vars: getWorkflowVars(ctx, rc),
91 Strategy: strategy,
92 Matrix: rc.Matrix,
93 Needs: using,
94 Inputs: inputs,
95 HashFiles: getHashFilesFunction(ctx, rc),
96 }
97 if rc.JobContainer != nil {
98 ee.Runner = rc.JobContainer.GetRunnerContext(ctx)
99 }
100 return expressionEvaluator{
101 interpreter: exprparser.NewInterpeter(ee, exprparser.Config{
102 Run: rc.Run,
103 WorkingDir: rc.Config.Workdir,
104 Context: "job",
105 }),
106 }
107}
108
109//go:embed hashfiles/index.js
110var hashfiles string
111
112// NewStepExpressionEvaluator creates a new evaluator
113func (rc *RunContext) NewStepExpressionEvaluator(ctx context.Context, step step) ExpressionEvaluator {
114 return rc.NewStepExpressionEvaluatorExt(ctx, step, false)
115}
116
117// NewStepExpressionEvaluatorExt creates a new evaluator
118func (rc *RunContext) NewStepExpressionEvaluatorExt(ctx context.Context, step step, rcInputs bool) ExpressionEvaluator {
119 ghc := rc.getGithubContext(ctx)
120 if rcInputs {
121 return rc.newStepExpressionEvaluator(ctx, step, ghc, getEvaluatorInputs(ctx, rc, nil, ghc))
122 }
123 return rc.newStepExpressionEvaluator(ctx, step, ghc, getEvaluatorInputs(ctx, rc, step, ghc))
124}
125
126func (rc *RunContext) newStepExpressionEvaluator(ctx context.Context, step step, _ *model.GithubContext, inputs map[string]interface{}) ExpressionEvaluator {
127 // todo: cleanup EvaluationEnvironment creation
128 job := rc.Run.Job()
129 strategy := make(map[string]interface{})
130 if job.Strategy != nil {
131 strategy["fail-fast"] = job.Strategy.FailFast
132 strategy["max-parallel"] = job.Strategy.MaxParallel
133 }
134
135 jobs := rc.Run.Workflow.Jobs
136 jobNeeds := rc.Run.Job().Needs()
137
138 using := make(map[string]exprparser.Needs)
139 for _, needs := range jobNeeds {
140 using[needs] = exprparser.Needs{
141 Outputs: jobs[needs].Outputs,
142 Result: jobs[needs].Result,
143 }
144 }
145
146 ee := &exprparser.EvaluationEnvironment{
147 Github: step.getGithubContext(ctx),
148 Env: *step.getEnv(),
149 Job: rc.getJobContext(),
150 Steps: rc.getStepsContext(),
151 Secrets: getWorkflowSecrets(ctx, rc),
152 Vars: getWorkflowVars(ctx, rc),
153 Strategy: strategy,
154 Matrix: rc.Matrix,
155 Needs: using,
156 // todo: should be unavailable
157 // but required to interpolate/evaluate the inputs in actions/composite
158 Inputs: inputs,
159 HashFiles: getHashFilesFunction(ctx, rc),
160 }
161 if rc.JobContainer != nil {
162 ee.Runner = rc.JobContainer.GetRunnerContext(ctx)
163 }
164 return expressionEvaluator{
165 interpreter: exprparser.NewInterpeter(ee, exprparser.Config{
166 Run: rc.Run,
167 WorkingDir: rc.Config.Workdir,
168 Context: "step",
169 }),
170 }
171}
172
173func getHashFilesFunction(ctx context.Context, rc *RunContext) func(v []reflect.Value) (interface{}, error) {
174 hashFiles := func(v []reflect.Value) (interface{}, error) {
175 if rc.JobContainer != nil {
176 timeed, cancel := context.WithTimeout(ctx, time.Minute)
177 defer cancel()
178 name := "workflow/hashfiles/index.js"
179 hout := &bytes.Buffer{}
180 herr := &bytes.Buffer{}
181 patterns := []string{}
182 followSymlink := false
183
184 for i, p := range v {
185 s := p.String()
186 if i == 0 {
187 if strings.HasPrefix(s, "--") {
188 if strings.EqualFold(s, "--follow-symbolic-links") {
189 followSymlink = true
190 continue
191 }
192 return "", fmt.Errorf("Invalid glob option %s, available option: '--follow-symbolic-links'", s)
193 }
194 }
195 patterns = append(patterns, s)
196 }
197 env := map[string]string{}
198 for k, v := range rc.Env {
199 env[k] = v
200 }
201 env["patterns"] = strings.Join(patterns, "\n")
202 if followSymlink {
203 env["followSymbolicLinks"] = "true"
204 }
205
206 stdout, stderr := rc.JobContainer.ReplaceLogWriter(hout, herr)
207 _ = rc.JobContainer.Copy(rc.JobContainer.GetActPath(), &container.FileEntry{
208 Name: name,
209 Mode: 0o644,
210 Body: hashfiles,
211 }).
212 Then(rc.execJobContainer([]string{rc.GetNodeToolFullPath(ctx), path.Join(rc.JobContainer.GetActPath(), name)},
213 env, "", "")).
214 Finally(func(context.Context) error {
215 rc.JobContainer.ReplaceLogWriter(stdout, stderr)
216 return nil
217 })(timeed)
218 output := hout.String() + "\n" + herr.String()
219 guard := "__OUTPUT__"
220 outstart := strings.Index(output, guard)
221 if outstart != -1 {
222 outstart += len(guard)
223 outend := strings.Index(output[outstart:], guard)
224 if outend != -1 {
225 return output[outstart : outstart+outend], nil
226 }
227 }
228 }
229 return "", nil
230 }
231 return hashFiles
232}
233
234type expressionEvaluator struct {
235 interpreter exprparser.Interpreter
236}
237
238func (ee expressionEvaluator) evaluate(ctx context.Context, in string, defaultStatusCheck exprparser.DefaultStatusCheck) (interface{}, error) {
239 logger := common.Logger(ctx)
240 logger.Debugf("evaluating expression '%s'", in)
241 evaluated, err := ee.interpreter.Evaluate(in, defaultStatusCheck)
242
243 printable := regexp.MustCompile(`::add-mask::.*`).ReplaceAllString(fmt.Sprintf("%t", evaluated), "::add-mask::***)")
244 logger.Debugf("expression '%s' evaluated to '%s'", in, printable)
245
246 return evaluated, err
247}
248
249func (ee expressionEvaluator) evaluateScalarYamlNode(ctx context.Context, node *yaml.Node) (*yaml.Node, error) {
250 var in string
251 if err := node.Decode(&in); err != nil {
252 return nil, err
253 }
254 if !strings.Contains(in, "${{") || !strings.Contains(in, "}}") {
255 return nil, nil
256 }
257 expr, _ := rewriteSubExpression(ctx, in, false)
258 res, err := ee.evaluate(ctx, expr, exprparser.DefaultStatusCheckNone)
259 if err != nil {
260 return nil, err
261 }
262 ret := &yaml.Node{}
263 if err := ret.Encode(res); err != nil {
264 return nil, err
265 }
266 return ret, err
267}
268
269func (ee expressionEvaluator) evaluateMappingYamlNode(ctx context.Context, node *yaml.Node) (*yaml.Node, error) {
270 var ret *yaml.Node
271 // GitHub has this undocumented feature to merge maps, called insert directive
272 insertDirective := regexp.MustCompile(`\${{\s*insert\s*}}`)
273 for i := 0; i < len(node.Content)/2; i++ {
274 changed := func() error {
275 if ret == nil {
276 ret = &yaml.Node{}
277 if err := ret.Encode(node); err != nil {
278 return err
279 }
280 ret.Content = ret.Content[:i*2]
281 }
282 return nil
283 }
284 k := node.Content[i*2]
285 v := node.Content[i*2+1]
286 ev, err := ee.evaluateYamlNodeInternal(ctx, v)
287 if err != nil {
288 return nil, err
289 }
290 if ev != nil {
291 if err := changed(); err != nil {
292 return nil, err
293 }
294 } else {
295 ev = v
296 }
297 var sk string
298 // Merge the nested map of the insert directive
299 if k.Decode(&sk) == nil && insertDirective.MatchString(sk) {
300 if ev.Kind != yaml.MappingNode {
301 return nil, fmt.Errorf("failed to insert node %v into mapping %v unexpected type %v expected MappingNode", ev, node, ev.Kind)
302 }
303 if err := changed(); err != nil {
304 return nil, err
305 }
306 ret.Content = append(ret.Content, ev.Content...)
307 } else {
308 ek, err := ee.evaluateYamlNodeInternal(ctx, k)
309 if err != nil {
310 return nil, err
311 }
312 if ek != nil {
313 if err := changed(); err != nil {
314 return nil, err
315 }
316 } else {
317 ek = k
318 }
319 if ret != nil {
320 ret.Content = append(ret.Content, ek, ev)
321 }
322 }
323 }
324 return ret, nil
325}
326
327func (ee expressionEvaluator) evaluateSequenceYamlNode(ctx context.Context, node *yaml.Node) (*yaml.Node, error) {
328 var ret *yaml.Node
329 for i := 0; i < len(node.Content); i++ {
330 v := node.Content[i]
331 // Preserve nested sequences
332 wasseq := v.Kind == yaml.SequenceNode
333 ev, err := ee.evaluateYamlNodeInternal(ctx, v)
334 if err != nil {
335 return nil, err
336 }
337 if ev != nil {
338 if ret == nil {
339 ret = &yaml.Node{}
340 if err := ret.Encode(node); err != nil {
341 return nil, err
342 }
343 ret.Content = ret.Content[:i]
344 }
345 // GitHub has this undocumented feature to merge sequences / arrays
346 // We have a nested sequence via evaluation, merge the arrays
347 if ev.Kind == yaml.SequenceNode && !wasseq {
348 ret.Content = append(ret.Content, ev.Content...)
349 } else {
350 ret.Content = append(ret.Content, ev)
351 }
352 } else if ret != nil {
353 ret.Content = append(ret.Content, v)
354 }
355 }
356 return ret, nil
357}
358
359func (ee expressionEvaluator) evaluateYamlNodeInternal(ctx context.Context, node *yaml.Node) (*yaml.Node, error) {
360 switch node.Kind {
361 case yaml.ScalarNode:
362 return ee.evaluateScalarYamlNode(ctx, node)
363 case yaml.MappingNode:
364 return ee.evaluateMappingYamlNode(ctx, node)
365 case yaml.SequenceNode:
366 return ee.evaluateSequenceYamlNode(ctx, node)
367 default:
368 return nil, nil
369 }
370}
371
372func (ee expressionEvaluator) EvaluateYamlNode(ctx context.Context, node *yaml.Node) error {
373 ret, err := ee.evaluateYamlNodeInternal(ctx, node)
374 if err != nil {
375 return err
376 }
377 if ret != nil {
378 return ret.Decode(node)
379 }
380 return nil
381}
382
383func (ee expressionEvaluator) Interpolate(ctx context.Context, in string) string {
384 if !strings.Contains(in, "${{") || !strings.Contains(in, "}}") {
385 return in
386 }
387
388 expr, _ := rewriteSubExpression(ctx, in, true)
389 evaluated, err := ee.evaluate(ctx, expr, exprparser.DefaultStatusCheckNone)
390 if err != nil {
391 common.Logger(ctx).Errorf("Unable to interpolate expression '%s': %s", expr, err)
392 return ""
393 }
394
395 value, ok := evaluated.(string)
396 if !ok {
397 panic(fmt.Sprintf("Expression %s did not evaluate to a string", expr))
398 }
399
400 return value
401}
402
403// EvalBool evaluates an expression against given evaluator
404func EvalBool(ctx context.Context, evaluator ExpressionEvaluator, expr string, defaultStatusCheck exprparser.DefaultStatusCheck) (bool, error) {
405 nextExpr, _ := rewriteSubExpression(ctx, expr, false)
406
407 evaluated, err := evaluator.evaluate(ctx, nextExpr, defaultStatusCheck)
408 if err != nil {
409 return false, err
410 }
411
412 return exprparser.IsTruthy(evaluated), nil
413}
414
415func escapeFormatString(in string) string {
416 return strings.ReplaceAll(strings.ReplaceAll(in, "{", "{{"), "}", "}}")
417}
418
419func rewriteSubExpression(ctx context.Context, in string, forceFormat bool) (string, error) {
420 if !strings.Contains(in, "${{") || !strings.Contains(in, "}}") {
421 return in, nil
422 }
423
424 strPattern := regexp.MustCompile("(?:''|[^'])*'")
425 pos := 0
426 exprStart := -1
427 strStart := -1
428 var results []string
429 formatOut := ""
430 for pos < len(in) {
431 if strStart > -1 {
432 matches := strPattern.FindStringIndex(in[pos:])
433 if matches == nil {
434 panic("unclosed string.")
435 }
436
437 strStart = -1
438 pos += matches[1]
439 } else if exprStart > -1 {
440 exprEnd := strings.Index(in[pos:], "}}")
441 strStart = strings.Index(in[pos:], "'")
442
443 if exprEnd > -1 && strStart > -1 {
444 if exprEnd < strStart {
445 strStart = -1
446 } else {
447 exprEnd = -1
448 }
449 }
450
451 if exprEnd > -1 {
452 formatOut += fmt.Sprintf("{%d}", len(results))
453 results = append(results, strings.TrimSpace(in[exprStart:pos+exprEnd]))
454 pos += exprEnd + 2
455 exprStart = -1
456 } else if strStart > -1 {
457 pos += strStart + 1
458 } else {
459 panic("unclosed expression.")
460 }
461 } else {
462 exprStart = strings.Index(in[pos:], "${{")
463 if exprStart != -1 {
464 formatOut += escapeFormatString(in[pos : pos+exprStart])
465 exprStart = pos + exprStart + 3
466 pos = exprStart
467 } else {
468 formatOut += escapeFormatString(in[pos:])
469 pos = len(in)
470 }
471 }
472 }
473
474 if len(results) == 1 && formatOut == "{0}" && !forceFormat {
475 return in, nil
476 }
477
478 out := fmt.Sprintf("format('%s', %s)", strings.ReplaceAll(formatOut, "'", "''"), strings.Join(results, ", "))
479 if in != out {
480 common.Logger(ctx).Debugf("expression '%s' rewritten to '%s'", in, out)
481 }
482 return out, nil
483}
484
485func getEvaluatorInputs(ctx context.Context, rc *RunContext, step step, ghc *model.GithubContext) map[string]interface{} {
486 inputs := map[string]interface{}{}
487
488 setupWorkflowInputs(ctx, &inputs, rc)
489
490 var env map[string]string
491 if step != nil {
492 env = *step.getEnv()
493 } else {
494 env = rc.GetEnv()
495 }
496
497 for k, v := range env {
498 if strings.HasPrefix(k, "INPUT_") {
499 inputs[strings.ToLower(strings.TrimPrefix(k, "INPUT_"))] = v
500 }
501 }
502
503 if rc.caller == nil && ghc.EventName == "workflow_dispatch" {
504 config := rc.Run.Workflow.WorkflowDispatchConfig()
505 if config != nil && config.Inputs != nil {
506 for k, v := range config.Inputs {
507 value := nestedMapLookup(ghc.Event, "inputs", k)
508 if value == nil {
509 value = v.Default
510 }
511 if v.Type == "boolean" {
512 inputs[k] = value == "true"
513 } else {
514 inputs[k] = value
515 }
516 }
517 }
518 }
519
520 if ghc.EventName == "workflow_call" {
521 config := rc.Run.Workflow.WorkflowCallConfig()
522 if config != nil && config.Inputs != nil {
523 for k, v := range config.Inputs {
524 value := nestedMapLookup(ghc.Event, "inputs", k)
525 if value == nil {
526 if err := v.Default.Decode(&value); err != nil {
527 common.Logger(ctx).Debugf("error decoding default value for %s: %v", k, err)
528 }
529 }
530 if v.Type == "boolean" {
531 inputs[k] = value == "true"
532 } else {
533 inputs[k] = value
534 }
535 }
536 }
537 }
538 if rc.Config.GitDataInputs != nil && rc.Parent == nil {
539 for k, v := range rc.Config.GitDataInputs {
540 inputs[k] = v
541 }
542 }
543 return inputs
544}
545
546func setupWorkflowInputs(ctx context.Context, inputs *map[string]interface{}, rc *RunContext) {
547 if rc.caller != nil {
548 config := rc.Run.Workflow.WorkflowCallConfig()
549
550 for name, input := range config.Inputs {
551 value := rc.caller.runContext.Run.Job().With[name]
552
553 if value != nil {
554 node := yaml.Node{}
555 _ = node.Encode(value)
556 if rc.caller.runContext.ExprEval != nil {
557 // evaluate using the calling RunContext (outside)
558 _ = rc.caller.runContext.ExprEval.EvaluateYamlNode(ctx, &node)
559 }
560 _ = node.Decode(&value)
561 }
562
563 if value == nil && config != nil && config.Inputs != nil {
564 def := input.Default
565 if rc.ExprEval != nil {
566 // evaluate using the called RunContext (inside)
567 _ = rc.ExprEval.EvaluateYamlNode(ctx, &def)
568 }
569 _ = def.Decode(&value)
570 }
571
572 (*inputs)[name] = value
573 }
574 }
575}
576
577func getWorkflowSecrets(ctx context.Context, rc *RunContext) map[string]string {
578 if rc.caller != nil {
579 job := rc.caller.runContext.Run.Job()
580 secrets := job.Secrets()
581
582 if secrets == nil && job.InheritSecrets() {
583 secrets = rc.caller.runContext.Config.Secrets
584 }
585
586 if secrets == nil {
587 secrets = map[string]string{}
588 }
589
590 for k, v := range secrets {
591 secrets[k] = rc.caller.runContext.ExprEval.Interpolate(ctx, v)
592 }
593
594 return secrets
595 }
596
597 return rc.Config.Secrets
598}
599
600func getWorkflowVars(_ context.Context, rc *RunContext) map[string]string {
601 return rc.Config.Vars
602}
Deleted act/pkg/runner/gitdata.go
@@ -1,99 +0,0 @@
1package runner
2
3import (
4 "context"
5 "encoding/json"
6 "os"
7 "path"
8 "strings"
9
10 "github.com/nektos/act/pkg/common"
11 "github.com/nektos/act/pkg/container"
12 "github.com/nektos/act/pkg/exprparser"
13 "github.com/nektos/act/pkg/model"
14)
15
16type GitDataEvent struct {
17 Type string `json:"type"`
18 StepID string `json:"step_id,omitempty"`
19 Stage string `json:"stage,omitempty"`
20 Name string `json:"name,omitempty"`
21 Outcome string `json:"outcome,omitempty"`
22 Conclusion string `json:"conclusion,omitempty"`
23 Outputs map[string]string `json:"outputs,omitempty"`
24 Text string `json:"text,omitempty"`
25 Properties map[string]string `json:"properties,omitempty"`
26}
27
28func (rc *RunContext) gitDataEmit(event GitDataEvent) {
29 if rc.Config.GitDataObserver == nil {
30 return
31 }
32 for parent := rc.Parent; parent != nil; parent = parent.Parent {
33 event.StepID = parent.CurrentStep + "/" + event.StepID
34 }
35 rc.Config.GitDataObserver(event)
36}
37
38func (rc *RunContext) GitDataEvaluate(ctx context.Context, expression string, check exprparser.DefaultStatusCheck) (interface{}, error) {
39 return rc.NewExpressionEvaluator(ctx).evaluate(ctx, expression, check)
40}
41
42func (rc *RunContext) gitDataContext(ghc *model.GithubContext) bool {
43 if rc.Config.GitDataGithub == nil {
44 return false
45 }
46 data, err := json.Marshal(rc.Config.GitDataGithub)
47 if err != nil || json.Unmarshal(data, ghc) != nil {
48 return false
49 }
50 ghc.Job = rc.Run.JobID
51 ghc.Action = rc.CurrentStep
52 ghc.ActionPath = rc.ActionPath
53 ghc.Token = rc.Config.Token
54 if rc.EventJSON != "" {
55 _ = json.Unmarshal([]byte(rc.EventJSON), &ghc.Event)
56 }
57 if rc.JobContainer != nil {
58 ghc.Workspace = rc.JobContainer.ToContainerPath(rc.Config.Workdir)
59 ghc.EventPath = rc.JobContainer.GetActPath() + "/workflow/event.json"
60 }
61 ghc.SetRefTypeAndName()
62 return true
63}
64
65func (rc *RunContext) GetNodeToolForAction(ctx context.Context, using string) string {
66 if node := rc.Config.GitDataNodeRuntimes[strings.ToLower(using)]; node != "" {
67 if rc.IsHostEnv(ctx) {
68 return node
69 }
70 destination := path.Join(rc.JobContainer.GetActPath(), "node", strings.ToLower(using), "bin/node")
71 if rc.gitDataNodes == nil {
72 rc.gitDataNodes = map[string]string{}
73 }
74 if cached := rc.gitDataNodes[using]; cached != "" {
75 return cached
76 }
77 data, err := os.ReadFile(node)
78 if err != nil {
79 common.Logger(ctx).Errorf("Node runtime %s: %v", using, err)
80 return destination
81 }
82 if err := rc.JobContainer.Copy(rc.JobContainer.GetActPath(), &container.FileEntry{Name: path.Join("node", strings.ToLower(using), "bin/node"), Mode: 0755, Body: string(data)})(ctx); err != nil {
83 common.Logger(ctx).Errorf("copying Node runtime %s: %v", using, err)
84 return destination
85 }
86 rc.gitDataNodes[using] = destination
87 return destination
88 }
89 return rc.GetNodeToolFullPath(ctx)
90}
91
92func (rc *RunContext) gitDataArtifactEnvironment(step *model.Step, env map[string]string) {
93 if rc.Config.GitDataRuntimeURL == "" {
94 return
95 }
96 if strings.HasPrefix(step.Uses, "actions/upload-artifact@") || strings.HasPrefix(step.Uses, "actions/download-artifact@") {
97 env["GITHUB_SERVER_URL"] = "https://github.com"
98 }
99}
Deleted act/pkg/runner/job_executor.go

Diff too large to display.

Deleted act/pkg/runner/local_repository_cache.go

Diff too large to display.

Deleted act/pkg/runner/logger.go

Diff too large to display.

Deleted act/pkg/runner/reusable_workflow.go

Diff too large to display.

Deleted act/pkg/runner/run_context.go

Diff too large to display.

Deleted act/pkg/runner/runner.go

Diff too large to display.

Deleted act/pkg/runner/step.go

Diff too large to display.

Deleted act/pkg/runner/step_action_local.go

Diff too large to display.

Deleted act/pkg/runner/step_action_remote.go

Diff too large to display.

Deleted act/pkg/runner/step_docker.go

Diff too large to display.

Deleted act/pkg/runner/step_factory.go

Diff too large to display.

Deleted act/pkg/runner/step_run.go

Diff too large to display.

Deleted act/pkg/schema/action_schema.json

Diff too large to display.

Deleted act/pkg/schema/schema.go

Diff too large to display.

Deleted act/pkg/schema/workflow_schema.json

Diff too large to display.

Deleted act/pkg/workflowpattern/trace_writer.go

Diff too large to display.

Deleted act/pkg/workflowpattern/workflow_pattern.go

Diff too large to display.

Deleted act/.dockerignore
@@ -1,8 +0,0 @@
1.git
2.gitignore
3Makefile
4install.sh
5target
6docs
7*.md
8LICENSE*
Deleted act/.gitignore
@@ -1,34 +0,0 @@
1# Binaries for programs and plugins
2*.exe
3*.exe~
4*.dll
5*.so
6*.dylib
7
8# Test binary, build with `go test -c`
9*.test
10
11# Output of the go coverage tool, specifically when used with LiteIDE
12*.out
13
14/dist/
15.todo
16
17*.nupkg
18.vscode/
19.idea/
20
21# Path for actions cache created when running tests
22pkg/runner/act/
23
24# act binary
25dist/local/act
26
27coverage.txt
28unit-tests.xml
29
30.env
31.secrets
32
33# megalinter
34report/
Deleted act/Dockerfile
@@ -1,47 +0,0 @@
1# syntax=docker/dockerfile:1
2FROM golang:1.27-bookworm AS builder
3
4WORKDIR /src
5COPY go.mod go.sum ./
6COPY cmd ./cmd
7COPY pkg ./pkg
8RUN --mount=type=cache,target=/go/pkg/mod \
9 --mount=type=cache,target=/root/.cache/go-build \
10 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" \
11 -o /out/gitdata-actions-engine ./cmd/gitdata-actions-engine
12
13FROM debian:bookworm-slim AS node-runtimes
14
15RUN apt-get update && apt-get install -y --no-install-recommends \
16 ca-certificates \
17 curl \
18 xz-utils \
19 && rm -rf /var/lib/apt/lists/* \
20 && mkdir -p /opt/gitdata/node \
21 && for version in 12.22.12 16.20.2 20.19.0 24.21.0; do \
22 mkdir -p "/opt/gitdata/node/node${version%%.*}"; \
23 curl -fsSL "https://nodejs.org/dist/v${version}/node-v${version}-linux-x64.tar.xz" \
24 | tar -xJ --strip-components=1 -C "/opt/gitdata/node/node${version%%.*}"; \
25 done
26
27FROM debian:bookworm-slim
28
29RUN apt-get update && apt-get install -y --no-install-recommends \
30 ca-certificates \
31 git \
32 openssh-client \
33 tar \
34 gzip \
35 xz-utils \
36 && rm -rf /var/lib/apt/lists/* \
37 && useradd --system --create-home --uid 10001 gitdata \
38 && mkdir -p /work \
39 && chown -R gitdata:gitdata /work
40
41COPY --from=builder /out/gitdata-actions-engine /usr/local/bin/gitdata-actions-engine
42COPY --from=node-runtimes --chown=gitdata:gitdata /opt/gitdata/node /opt/gitdata/node
43
44USER gitdata
45WORKDIR /work
46
47ENTRYPOINT ["/usr/local/bin/gitdata-actions-engine"]
Deleted act/GITDATA_UPSTREAM
@@ -1 +0,0 @@
1nektos/act v0.2.89 4f411281417e88660bea1c1a1749aa71ae0bd60f
Deleted act/go.mod
@@ -1,111 +0,0 @@
1module github.com/nektos/act
2
3go 1.25.0
4
5require (
6 github.com/AlecAivazis/survey/v2 v2.3.7
7 github.com/Masterminds/semver v1.5.0
8 github.com/adrg/xdg v0.5.3
9 github.com/andreaskoch/go-fswatch v1.0.0
10 github.com/creack/pty v1.1.24
11 github.com/docker/cli v29.3.0+incompatible
12 github.com/docker/go-connections v0.6.0
13 github.com/go-git/go-billy/v5 v5.9.0
14 github.com/go-git/go-git/v5 v5.16.5
15 github.com/joho/godotenv v1.5.1
16 github.com/julienschmidt/httprouter v1.3.0
17 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
18 github.com/mattn/go-isatty v0.0.20
19 github.com/moby/patternmatcher v0.6.0
20 github.com/opencontainers/image-spec v1.1.1
21 github.com/opencontainers/selinux v1.13.1
22 github.com/pkg/errors v0.9.1
23 github.com/rhysd/actionlint v1.7.7
24 github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
25 github.com/sirupsen/logrus v1.9.3
26 github.com/spf13/cobra v1.10.1
27 github.com/spf13/pflag v1.0.9
28 github.com/stretchr/testify v1.11.1
29 github.com/timshannon/bolthold v0.0.0-20240314194003-30aac6950928
30 go.etcd.io/bbolt v1.4.3
31 golang.org/x/term v0.42.0
32 gopkg.in/yaml.v3 v3.0.1
33 gotest.tools/v3 v3.5.2
34)
35
36require (
37 dario.cat/mergo v1.0.2
38 github.com/containerd/errdefs v1.0.0
39 github.com/distribution/reference v0.6.0
40 github.com/golang-jwt/jwt/v5 v5.3.0
41 github.com/google/go-cmp v0.7.0
42 github.com/moby/go-archive v0.1.0
43 github.com/moby/moby/api v1.54.0
44 github.com/moby/moby/client v0.3.0
45 google.golang.org/protobuf v1.36.9
46 tags.cncf.io/container-device-interface v1.1.0
47)
48
49require (
50 cyphar.com/go-pathrs v0.2.1 // indirect
51 github.com/Microsoft/go-winio v0.6.2 // indirect
52 github.com/ProtonMail/go-crypto v1.1.6 // indirect
53 github.com/bmatcuk/doublestar/v4 v4.8.0 // indirect
54 github.com/cespare/xxhash/v2 v2.3.0 // indirect
55 github.com/cloudflare/circl v1.6.3 // indirect
56 github.com/containerd/errdefs/pkg v0.3.0 // indirect
57 github.com/containerd/log v0.1.0 // indirect
58 github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
59 github.com/cyphar/filepath-securejoin v0.6.1 // indirect
60 github.com/davecgh/go-spew v1.1.1 // indirect
61 github.com/docker/docker-credential-helpers v0.8.2 // indirect
62 github.com/docker/go-units v0.5.0 // indirect
63 github.com/emirpasic/gods v1.18.1 // indirect
64 github.com/fatih/color v1.18.0 // indirect
65 github.com/felixge/httpsnoop v1.0.4 // indirect
66 github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
67 github.com/go-logr/logr v1.4.3 // indirect
68 github.com/go-logr/stdr v1.2.2 // indirect
69 github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
70 github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
71 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
72 github.com/inconshreveable/mousetrap v1.1.0 // indirect
73 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
74 github.com/kevinburke/ssh_config v1.2.0 // indirect
75 github.com/klauspost/compress v1.18.0 // indirect
76 github.com/mattn/go-colorable v0.1.14 // indirect
77 github.com/mattn/go-runewidth v0.0.16 // indirect
78 github.com/mattn/go-shellwords v1.0.12 // indirect
79 github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
80 github.com/moby/docker-image-spec v1.3.1 // indirect
81 github.com/moby/sys/sequential v0.6.0 // indirect
82 github.com/moby/sys/user v0.4.0 // indirect
83 github.com/moby/sys/userns v0.1.0 // indirect
84 github.com/opencontainers/go-digest v1.0.0 // indirect
85 github.com/pjbgf/sha1cd v0.3.2 // indirect
86 github.com/pmezard/go-difflib v1.0.0 // indirect
87 github.com/rivo/uniseg v0.4.7 // indirect
88 github.com/robfig/cron/v3 v3.0.1 // indirect
89 github.com/russross/blackfriday/v2 v2.1.0 // indirect
90 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
91 github.com/skeema/knownhosts v1.3.1 // indirect
92 github.com/stretchr/objx v0.5.2 // indirect
93 github.com/xanzy/ssh-agent v0.3.3 // indirect
94 github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
95 github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
96 github.com/xeipuuv/gojsonschema v1.2.0 // indirect
97 go.opentelemetry.io/auto/sdk v1.2.1 // indirect
98 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
99 go.opentelemetry.io/otel v1.43.0 // indirect
100 go.opentelemetry.io/otel/metric v1.43.0 // indirect
101 go.opentelemetry.io/otel/sdk v1.43.0 // indirect
102 go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
103 go.opentelemetry.io/otel/trace v1.43.0 // indirect
104 go.yaml.in/yaml/v3 v3.0.4 // indirect
105 golang.org/x/crypto v0.50.0 // indirect
106 golang.org/x/net v0.53.0 // indirect
107 golang.org/x/sync v0.20.0 // indirect
108 golang.org/x/sys v0.43.0 // indirect
109 golang.org/x/text v0.36.0 // indirect
110 gopkg.in/warnings.v0 v0.1.2 // indirect
111)
Deleted act/go.sum
@@ -1,296 +0,0 @@
1cyphar.com/go-pathrs v0.2.1 h1:9nx1vOgwVvX1mNBWDu93+vaceedpbsDqo+XuBGL40b8=
2cyphar.com/go-pathrs v0.2.1/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc=
3dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
4dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
5github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
6github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
7github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
8github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
9github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
10github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
11github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
12github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
13github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
14github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
15github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
16github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
17github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
18github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
19github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
20github.com/andreaskoch/go-fswatch v1.0.0 h1:la8nP/HiaFCxP2IM6NZNUCoxgLWuyNFgH0RligBbnJU=
21github.com/andreaskoch/go-fswatch v1.0.0/go.mod h1:r5/iV+4jfwoY2sYqBkg8vpF04ehOvEl4qPptVGdxmqo=
22github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
23github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
24github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
25github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
26github.com/bmatcuk/doublestar/v4 v4.8.0 h1:DSXtrypQddoug1459viM9X9D3dp1Z7993fw36I2kNcQ=
27github.com/bmatcuk/doublestar/v4 v4.8.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
28github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
29github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
30github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
31github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
32github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
33github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
34github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
35github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
36github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
37github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
38github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
39github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
40github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
41github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
42github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
43github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
44github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
45github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
46github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
47github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
48github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
49github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
50github.com/docker/cli v29.3.0+incompatible h1:z3iWveU7h19Pqx7alZES8j+IeFQZ1lhTwb2F+V9SVvk=
51github.com/docker/cli v29.3.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
52github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
53github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
54github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
55github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
56github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
57github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
58github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
59github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
60github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
61github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
62github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
63github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
64github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
65github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
66github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
67github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
68github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
69github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
70github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA=
71github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
72github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
73github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
74github.com/go-git/go-git/v5 v5.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s=
75github.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M=
76github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
77github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
78github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
79github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
80github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
81github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
82github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
83github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
84github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
85github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
86github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
87github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
88github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
89github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
90github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
91github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
92github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
93github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
94github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
95github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
96github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
97github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
98github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
99github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
100github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
101github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
102github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
103github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
104github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
105github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
106github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
107github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
108github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
109github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
110github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
111github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
112github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
113github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
114github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
115github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
116github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
117github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
118github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
119github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
120github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
121github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
122github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
123github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
124github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
125github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
126github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
127github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
128github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
129github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
130github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
131github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
132github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
133github.com/moby/moby/api v1.54.0 h1:7kbUgyiKcoBhm0UrWbdrMs7RX8dnwzURKVbZGy2GnL0=
134github.com/moby/moby/api v1.54.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
135github.com/moby/moby/client v0.3.0 h1:UUGL5okry+Aomj3WhGt9Aigl3ZOxZGqR7XPo+RLPlKs=
136github.com/moby/moby/client v0.3.0/go.mod h1:HJgFbJRvogDQjbM8fqc1MCEm4mIAGMLjXbgwoZp6jCQ=
137github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
138github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
139github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
140github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
141github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
142github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
143github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
144github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
145github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
146github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
147github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
148github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
149github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
150github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
151github.com/opencontainers/selinux v1.13.1 h1:A8nNeceYngH9Ow++M+VVEwJVpdFmrlxsN22F+ISDCJE=
152github.com/opencontainers/selinux v1.13.1/go.mod h1:S10WXZ/osk2kWOYKy1x2f/eXF5ZHJoUs8UU/2caNRbg=
153github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
154github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
155github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
156github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
157github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
158github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
159github.com/rhysd/actionlint v1.7.7 h1:0KgkoNTrYY7vmOCs9BW2AHxLvvpoY9nEUzgBHiPUr0k=
160github.com/rhysd/actionlint v1.7.7/go.mod h1:AE6I6vJEkNaIfWqC2GNE5spIJNhxf8NCtLEKU4NnUXg=
161github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
162github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
163github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
164github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
165github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
166github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
167github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
168github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
169github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
170github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI=
171github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs=
172github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
173github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
174github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
175github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
176github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
177github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
178github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
179github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
180github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
181github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
182github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
183github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
184github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
185github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
186github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
187github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
188github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
189github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
190github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
191github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
192github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
193github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
194github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
195github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
196github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
197github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
198github.com/timshannon/bolthold v0.0.0-20240314194003-30aac6950928 h1:zjNCuOOhh1TKRU0Ru3PPPJt80z7eReswCao91gBLk00=
199github.com/timshannon/bolthold v0.0.0-20240314194003-30aac6950928/go.mod h1:PCFYfAEfKT+Nd6zWvUpsXduMR1bXFLf0uGSlEF05MCI=
200github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
201github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
202github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
203github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
204github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
205github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
206github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
207github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
208github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
209github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
210go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
211go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
212go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
213go.etcd.io/gofail v0.1.0/go.mod h1:VZBCXYGZhHAinaBiiqYvuDynvahNsAyLFwB3kEHKz1M=
214go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
215go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
216go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
217go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
218go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
219go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
220go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
221go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
222go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
223go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
224go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
225go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
226go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
227go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
228go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
229go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
230golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
231golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
232golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
233golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
234golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
235golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
236golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
237golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
238golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
239golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
240golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
241golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
242golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
243golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
244golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
245golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
246golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
247golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
248golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
249golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
250golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
251golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
252golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
253golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
254golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
255golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
256golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
257golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
258golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
259golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
260golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
261golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
262golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
263golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
264golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
265golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
266golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
267golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
268golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
269golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
270golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
271golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
272golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
273golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
274golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
275golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
276golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
277golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
278google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
279google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
280gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
281gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
282gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
283gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
284gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
285gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
286gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
287gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
288gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
289gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
290gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
291gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
292gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
293pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
294pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
295tags.cncf.io/container-device-interface v1.1.0 h1:RnxNhxF1JOu6CJUVpetTYvrXHdxw9j9jFYgZpI+anSY=
296tags.cncf.io/container-device-interface v1.1.0/go.mod h1:76Oj0Yqp9FwTx/pySDc8Bxjpg+VqXfDb50cKAXVJ34Q=
Deleted act/install.sh
@@ -1,433 +0,0 @@
1#!/bin/sh
2set -e
3# Code originally generated by godownloader on 2021-12-22T16:10:52Z. DO NOT EDIT.
4# (godownloader is deprecated, so changes to this script are maintained in install.sh in https://github.com/nektos/act)
5#
6
7usage() {
8 this=$1
9 cat <<EOF
10$this: download go binaries for nektos/act
11
12Usage: $this [-b bindir] [-d] [-f] [tag]
13 -b sets bindir or installation directory, Defaults to ./bin
14 -d turns on debug logging
15 -f forces installation, bypassing version checks
16 [tag] is a tag from
17 https://github.com/nektos/act/releases
18 If tag is missing, then the latest will be used.
19EOF
20 exit 2
21}
22
23parse_args() {
24 #BINDIR is ./bin unless set be ENV
25 # over-ridden by flag below
26
27 BINDIR=${BINDIR:-./bin}
28 while getopts "b:dfh?x" arg; do
29 case "$arg" in
30 b) BINDIR="$OPTARG" ;;
31 d) log_set_priority 10 ;;
32 f) FORCE_INSTALL="true" ;;
33 h | \?) usage "$0" ;;
34 x) set -x ;;
35 esac
36 done
37 shift $((OPTIND - 1))
38 TAG=$1
39}
40# this function wraps all the destructive operations
41# if a curl|bash cuts off the end of the script due to
42# network, either nothing will happen or will syntax error
43# out preventing half-done work
44execute() {
45 tmpdir=$(mktemp -d)
46 log_debug "downloading files into ${tmpdir}"
47 http_download "${tmpdir}/${TARBALL}" "${TARBALL_URL}"
48 http_download "${tmpdir}/${CHECKSUM}" "${CHECKSUM_URL}"
49 hash_sha256_verify "${tmpdir}/${TARBALL}" "${tmpdir}/${CHECKSUM}"
50 srcdir="${tmpdir}"
51 (cd "${tmpdir}" && untar "${TARBALL}")
52 test ! -d "${BINDIR}" && install -d "${BINDIR}"
53 for binexe in $BINARIES; do
54 if [ "$OS" = "windows" ]; then
55 binexe="${binexe}.exe"
56 fi
57 install "${srcdir}/${binexe}" "${BINDIR}/"
58 log_info "installed ${BINDIR}/${binexe}"
59 done
60 rm -rf "${tmpdir}"
61}
62get_binaries() {
63 case "$PLATFORM" in
64 darwin/386) BINARIES="act" ;;
65 darwin/amd64) BINARIES="act" ;;
66 darwin/arm64) BINARIES="act" ;;
67 darwin/armv6) BINARIES="act" ;;
68 darwin/armv7) BINARIES="act" ;;
69 linux/386) BINARIES="act" ;;
70 linux/amd64) BINARIES="act" ;;
71 linux/arm64) BINARIES="act" ;;
72 linux/armv6) BINARIES="act" ;;
73 linux/armv7) BINARIES="act" ;;
74 windows/386) BINARIES="act" ;;
75 windows/amd64) BINARIES="act" ;;
76 windows/arm64) BINARIES="act" ;;
77 windows/armv6) BINARIES="act" ;;
78 windows/armv7) BINARIES="act" ;;
79 *)
80 log_crit "platform $PLATFORM is not supported. Make sure this script is up-to-date and file request at https://github.com/${PREFIX}/issues/new"
81 exit 1
82 ;;
83 esac
84}
85tag_to_version() {
86 if [ -z "${TAG}" ]; then
87 log_info "checking GitHub for latest tag"
88 else
89 log_info "checking GitHub for tag '${TAG}'"
90 fi
91 REALTAG=$(github_release "$OWNER/$REPO" "${TAG}") && true
92 if test -z "$REALTAG"; then
93 log_crit "unable to find '${TAG}' - use 'latest' or see https://github.com/${PREFIX}/releases for details"
94 exit 1
95 fi
96 # if version starts with 'v', remove it
97 TAG="$REALTAG"
98 VERSION=${TAG#v}
99}
100adjust_format() {
101 # change format (tar.gz or zip) based on OS
102 case ${OS} in
103 windows) FORMAT=zip ;;
104 esac
105 true
106}
107adjust_os() {
108 # adjust archive name based on OS
109 case ${OS} in
110 386) OS=i386 ;;
111 amd64) OS=x86_64 ;;
112 darwin) OS=Darwin ;;
113 linux) OS=Linux ;;
114 windows) OS=Windows ;;
115 esac
116 true
117}
118adjust_arch() {
119 # adjust archive name based on ARCH
120 case ${ARCH} in
121 386) ARCH=i386 ;;
122 amd64) ARCH=x86_64 ;;
123 darwin) ARCH=Darwin ;;
124 linux) ARCH=Linux ;;
125 windows) ARCH=Windows ;;
126 esac
127 true
128}
129check_installed_version() {
130 # Check if force install flag is set
131 if [ "${FORCE_INSTALL}" = "true" ]; then
132 log_info "force install enabled. Skipping version check."
133 return
134 fi
135
136 # Check if the binary exists
137 if is_command "$BINARY"; then
138 # Extract installed version using cut
139 INSTALLED_VERSION=$($BINARY --version | cut -d' ' -f3)
140
141 if [ -z "$INSTALLED_VERSION" ]; then
142 log_err "failed to detect installed version. Proceeding with installation."
143 return
144 fi
145
146 log_info "found installed version: $INSTALLED_VERSION"
147
148 # Compare versions
149 if [ "$INSTALLED_VERSION" = "$VERSION" ]; then
150 log_info "$BINARY version $INSTALLED_VERSION is already installed."
151 exit 0
152 else
153 log_debug "updating $BINARY from version $INSTALLED_VERSION to $VERSION..."
154 fi
155 else
156 log_debug "$BINARY is not installed. Proceeding with installation..."
157 fi
158}
159
160
161cat /dev/null <<EOF
162------------------------------------------------------------------------
163https://github.com/client9/shlib - portable posix shell functions
164Public domain - http://unlicense.org
165https://github.com/client9/shlib/blob/master/LICENSE.md
166but credit (and pull requests) appreciated.
167------------------------------------------------------------------------
168EOF
169is_command() {
170 command -v "$1" >/dev/null
171}
172echoerr() {
173 echo "$@" 1>&2
174}
175log_prefix() {
176 echo "$0"
177}
178_logp=6
179log_set_priority() {
180 _logp="$1"
181}
182log_priority() {
183 if test -z "$1"; then
184 echo "$_logp"
185 return
186 fi
187 [ "$1" -le "$_logp" ]
188}
189log_tag() {
190 case $1 in
191 0) echo "emerg" ;;
192 1) echo "alert" ;;
193 2) echo "crit" ;;
194 3) echo "err" ;;
195 4) echo "warning" ;;
196 5) echo "notice" ;;
197 6) echo "info" ;;
198 7) echo "debug" ;;
199 *) echo "$1" ;;
200 esac
201}
202log_debug() {
203 log_priority 7 || return 0
204 echoerr "$(log_prefix)" "$(log_tag 7)" "$@"
205}
206log_info() {
207 log_priority 6 || return 0
208 echoerr "$(log_prefix)" "$(log_tag 6)" "$@"
209}
210log_err() {
211 log_priority 3 || return 0
212 echoerr "$(log_prefix)" "$(log_tag 3)" "$@"
213}
214log_crit() {
215 log_priority 2 || return 0
216 echoerr "$(log_prefix)" "$(log_tag 2)" "$@"
217}
218uname_os() {
219 os=$(uname -s | tr '[:upper:]' '[:lower:]')
220 case "$os" in
221 cygwin_nt*) os="windows" ;;
222 mingw*) os="windows" ;;
223 msys_nt*) os="windows" ;;
224 esac
225 echo "$os"
226}
227uname_arch() {
228 arch=$(uname -m)
229 case $arch in
230 x86_64) arch="amd64" ;;
231 x86) arch="386" ;;
232 i686) arch="386" ;;
233 i386) arch="386" ;;
234 aarch64) arch="arm64" ;;
235 armv5*) arch="armv5" ;;
236 armv6*) arch="armv6" ;;
237 armv7*) arch="armv7" ;;
238 esac
239 echo ${arch}
240}
241uname_os_check() {
242 os=$(uname_os)
243 case "$os" in
244 darwin) return 0 ;;
245 dragonfly) return 0 ;;
246 freebsd) return 0 ;;
247 linux) return 0 ;;
248 android) return 0 ;;
249 nacl) return 0 ;;
250 netbsd) return 0 ;;
251 openbsd) return 0 ;;
252 plan9) return 0 ;;
253 solaris) return 0 ;;
254 windows) return 0 ;;
255 esac
256 log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib"
257 return 1
258}
259uname_arch_check() {
260 arch=$(uname_arch)
261 case "$arch" in
262 386) return 0 ;;
263 amd64) return 0 ;;
264 arm64) return 0 ;;
265 armv5) return 0 ;;
266 armv6) return 0 ;;
267 armv7) return 0 ;;
268 ppc64) return 0 ;;
269 ppc64le) return 0 ;;
270 mips) return 0 ;;
271 mipsle) return 0 ;;
272 mips64) return 0 ;;
273 mips64le) return 0 ;;
274 s390x) return 0 ;;
275 amd64p32) return 0 ;;
276 esac
277 log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib"
278 return 1
279}
280untar() {
281 tarball=$1
282 case "${tarball}" in
283 *.tar.gz | *.tgz) tar --no-same-owner -xzf "${tarball}" ;;
284 *.tar) tar --no-same-owner -xf "${tarball}" ;;
285 *.zip) unzip "${tarball}" ;;
286 *)
287 log_err "untar unknown archive format for ${tarball}"
288 return 1
289 ;;
290 esac
291}
292http_download_curl() {
293 local_file=$1
294 source_url=$2
295 header=$3
296 if [ -z "$header" ]; then
297 code=$(curl -w '%{http_code}' -sL -o "$local_file" "$source_url")
298 else
299 code=$(curl -w '%{http_code}' -sL -H "$header" -o "$local_file" "$source_url")
300 fi
301 if [ "$code" != "200" ]; then
302 log_debug "http_download_curl received HTTP status $code"
303 return 1
304 fi
305 return 0
306}
307http_download_wget() {
308 local_file=$1
309 source_url=$2
310 header=$3
311 if [ -z "$header" ]; then
312 wget -q -O "$local_file" "$source_url"
313 else
314 wget -q --header "$header" -O "$local_file" "$source_url"
315 fi
316}
317http_download() {
318 log_debug "http_download $2"
319 if is_command curl; then
320 http_download_curl "$@"
321 return
322 elif is_command wget; then
323 http_download_wget "$@"
324 return
325 fi
326 log_crit "http_download unable to find wget or curl"
327 return 1
328}
329http_copy() {
330 tmp=$(mktemp)
331 http_download "${tmp}" "$1" "$2" || return 1
332 body=$(cat "$tmp")
333 rm -f "${tmp}"
334 echo "$body"
335}
336github_release() {
337 owner_repo=$1
338 version=$2
339 test -z "$version" && version="latest"
340 giturl="https://github.com/${owner_repo}/releases/${version}"
341 json=$(http_copy "$giturl" "Accept:application/json")
342 test -z "$json" && return 1
343 version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//')
344 test -z "$version" && return 1
345 echo "$version"
346}
347hash_sha256() {
348 TARGET=${1:-/dev/stdin}
349 if is_command gsha256sum; then
350 hash=$(gsha256sum "$TARGET") || return 1
351 echo "$hash" | cut -d ' ' -f 1
352 elif is_command sha256sum; then
353 hash=$(sha256sum "$TARGET") || return 1
354 echo "$hash" | cut -d ' ' -f 1
355 elif is_command shasum; then
356 hash=$(shasum -a 256 "$TARGET" 2>/dev/null) || return 1
357 echo "$hash" | cut -d ' ' -f 1
358 elif is_command openssl; then
359 hash=$(openssl -dst openssl dgst -sha256 "$TARGET") || return 1
360 echo "$hash" | cut -d ' ' -f a
361 else
362 log_crit "hash_sha256 unable to find command to compute sha-256 hash"
363 return 1
364 fi
365}
366hash_sha256_verify() {
367 TARGET=$1
368 checksums=$2
369 if [ -z "$checksums" ]; then
370 log_err "hash_sha256_verify checksum file not specified in arg2"
371 return 1
372 fi
373 BASENAME=${TARGET##*/}
374 want=$(grep "${BASENAME}" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1)
375 if [ -z "$want" ]; then
376 log_err "hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'"
377 return 1
378 fi
379 got=$(hash_sha256 "$TARGET")
380 if [ "$want" != "$got" ]; then
381 log_err "hash_sha256_verify checksum for '$TARGET' did not verify ${want} vs $got"
382 return 1
383 fi
384}
385cat /dev/null <<EOF
386------------------------------------------------------------------------
387End of functions from https://github.com/client9/shlib
388------------------------------------------------------------------------
389EOF
390
391PROJECT_NAME="act"
392OWNER=nektos
393REPO="act"
394BINARY=act
395FORMAT=tar.gz
396OS=$(uname_os)
397ARCH=$(uname_arch)
398PREFIX="$OWNER/$REPO"
399
400# use in logging routines
401log_prefix() {
402 echo "$PREFIX"
403}
404PLATFORM="${OS}/${ARCH}"
405GITHUB_DOWNLOAD=https://github.com/${OWNER}/${REPO}/releases/download
406
407uname_os_check "$OS"
408uname_arch_check "$ARCH"
409
410parse_args "$@"
411
412get_binaries
413
414tag_to_version
415
416check_installed_version
417
418adjust_format
419
420adjust_os
421
422adjust_arch
423
424log_info "found version: ${VERSION} for ${TAG}/${OS}/${ARCH}"
425
426NAME=${PROJECT_NAME}_${OS}_${ARCH}
427TARBALL=${NAME}.${FORMAT}
428TARBALL_URL=${GITHUB_DOWNLOAD}/${TAG}/${TARBALL}
429CHECKSUM=checksums.txt
430CHECKSUM_URL=${GITHUB_DOWNLOAD}/${TAG}/${CHECKSUM}
431
432
433execute
Deleted act/main.go
@@ -1,19 +0,0 @@
1package main
2
3import (
4 _ "embed"
5
6 "github.com/nektos/act/cmd"
7 "github.com/nektos/act/pkg/common"
8)
9
10//go:embed VERSION
11var version string
12
13func main() {
14 ctx, cancel := common.CreateGracefulJobCancellationContext()
15 defer cancel()
16
17 // run the command
18 cmd.Execute(ctx, version)
19}
Deleted act/Makefile
@@ -1,129 +0,0 @@
1PREFIX ?= /usr/local
2VERSION ?= $(shell git describe --tags --dirty --always | sed -e 's/^v//')
3IS_SNAPSHOT = $(if $(findstring -, $(VERSION)),true,false)
4MAJOR_VERSION = $(word 1, $(subst ., ,$(VERSION)))
5MINOR_VERSION = $(word 2, $(subst ., ,$(VERSION)))
6PATCH_VERSION = $(word 3, $(subst ., ,$(word 1,$(subst -, , $(VERSION)))))
7NEW_VERSION ?= $(MAJOR_VERSION).$(MINOR_VERSION).$(shell echo $$(( $(PATCH_VERSION) + 1)) )
8GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
9
10fix = false
11ifeq (true,$(fix))
12 FIX = --fix
13endif
14
15ACT ?= go run main.go
16
17HAS_TOKEN = $(if $(test -e ~/.config/github/token),true,false)
18ifeq (true,$(HAS_TOKEN))
19 export GITHUB_TOKEN := $(shell cat ~/.config/github/token)
20endif
21
22.PHONY: pr
23pr: tidy format-all lint test
24
25.PHONY: build
26build:
27 go build -ldflags "-X main.version=$(VERSION)" -o dist/local/act main.go
28
29.PHONY: format
30format:
31 go fmt ./...
32
33.PHONY: format-all
34format-all:
35 go fmt ./...
36 npx prettier --write .
37
38.PHONY: test
39test:
40 go test ./...
41 $(ACT)
42
43.PHONY: lint-go
44lint-go:
45 golangci-lint run $(FIX)
46
47.PHONY: lint-js
48lint-js:
49 npx standard $(FIX)
50
51.PHONY: lint-md
52lint-md:
53 npx markdownlint . $(FIX)
54
55.PHONY: lint-rest
56lint-rest:
57 docker run --rm -it \
58 -v $(PWD):/tmp/lint \
59 -e GITHUB_STATUS_REPORTER=false \
60 -e GITHUB_COMMENT_REPORTER=false \
61 megalinter/megalinter-go:v5
62
63.PHONY: lint
64lint: lint-go lint-rest
65
66.PHONY: lint-fix
67lint-fix: lint-md lint-go
68
69.PHONY: fix
70fix:
71 make lint-fix fix=true
72
73.PHONY: tidy
74tidy:
75 go mod tidy
76
77.PHONY: install
78install: build
79 @cp dist/local/act $(PREFIX)/bin/act
80 @chmod 755 $(PREFIX)/bin/act
81 @act --version
82
83.PHONY: installer
84installer:
85 @GO111MODULE=off go get github.com/goreleaser/godownloader
86 godownloader -r nektos/act -o install.sh
87
88.PHONY: promote
89promote:
90 @git fetch --tags
91 @echo "VERSION:$(VERSION) IS_SNAPSHOT:$(IS_SNAPSHOT) NEW_VERSION:$(NEW_VERSION)"
92ifeq (false,$(IS_SNAPSHOT))
93 @echo "Unable to promote a non-snapshot"
94 @exit 1
95endif
96ifneq ($(shell git status -s),)
97 @echo "Unable to promote a dirty workspace"
98 @exit 1
99endif
100 echo -n $(NEW_VERSION) > VERSION
101 git add VERSION
102 git commit -m "chore: bump VERSION to $(NEW_VERSION)"
103 git tag -a -m "releasing v$(NEW_VERSION)" v$(NEW_VERSION)
104 git push origin master
105 git push origin v$(NEW_VERSION)
106
107.PHONY: snapshot
108snapshot:
109 goreleaser build \
110 --clean \
111 --single-target \
112 --snapshot
113
114.PHONY: clean all
115
116.PHONY: upgrade
117upgrade:
118 go get -u
119 go mod tidy
120
121# "$(shell go env GOROOT)/bin/go" allows us to use an outdated global go tool and use the build toolchain defined by the project
122# go build auto upgrades to the same toolchain version as defined in the go.mod file
123.PHONY: deps-tools
124deps-tools: ## install tool dependencies
125 "$(shell go env GOROOT)/bin/go" install $(GOVULNCHECK_PACKAGE)
126
127.PHONY: security-check
128security-check: deps-tools
129 GOEXPERIMENT= "$(shell go env GOROOT)/bin/go" run $(GOVULNCHECK_PACKAGE) -show color ./...
Deleted act/VERIFICATION
@@ -1,5 +0,0 @@
1VERIFICATION
2Verification is intended to assist the Chocolatey moderators and community
3in verifying that this package's contents are trustworthy.
4
5Checksums: https://github.com/nektos/act/releases, in the checksums.txt file
Deleted act/VERSION
@@ -1 +0,0 @@
10.2.89
No newline at end of file
Modified charts/act/templates/_helpers.tpl View charts/act/templates/_helpers.tpl
@@ -47,7 +47,7 @@
4747{{- end -}}
4848
4949{{- define "runner.actionsImage" -}}
50{{- printf "%s:%s" .Values.actionsImage.repository (.Values.actionsImage.tag | default .Values.image.tag | default .Chart.AppVersion) -}}
50{{- printf "%s:%s" .Values.actionsImage.repository (default .Chart.AppVersion .Values.actionsImage.tag) -}}
5151{{- end -}}
5252
5353{{- define "runner.secretName" -}}
Modified charts/act/values.yaml View charts/act/values.yaml
@@ -13,10 +13,11 @@
1313 tag: ""
1414 pullPolicy: IfNotPresent
1515
16# act 引擎镜像:提供 gitdata-actions-engine 二进制 + 4 套 Node 运行时
16# act 引擎镜像:提供 gitdata-actions-engine 二进制 + 4 套 Node 运行时。
17# 引擎已独立成仓(git-data-actions),版本不再跟主镜像走,必须显式写 tag。
1718actionsImage:
1819 repository: harbor.gitdata.me/app/gitdata-act
19 tag: ""
20 tag: "0.2.89"
2021 pullPolicy: IfNotPresent
2122
2223imagePullSecrets: []
Modified charts/core/templates/_helpers.tpl View charts/core/templates/_helpers.tpl
@@ -46,7 +46,7 @@
4646{{- end -}}
4747
4848{{- define "gitdata.actionsImage" -}}
49{{- $tag := .Values.runner.actionsImage.tag | default .Values.image.tag | default .Chart.AppVersion -}}
49{{- $tag := .Values.runner.actionsImage.tag | default .Chart.AppVersion -}}
5050{{- printf "%s:%s" .Values.runner.actionsImage.repository $tag -}}
5151{{- end -}}
5252
Modified charts/prod/values/prod.yaml View charts/prod/values/prod.yaml
@@ -254,5 +254,6 @@
254254 enabled: false
255255 actionsImage:
256256 repository: harbor.gitdata.me/app/gitdata-act
257 tag: ""
257 # 引擎独立成仓,版本与主镜像解耦
258 tag: "0.2.89"
258259 pullPolicy: IfNotPresent
Modified charts/prod/values/runner-prod.yaml View charts/prod/values/runner-prod.yaml
@@ -17,8 +17,8 @@
1717
1818actionsImage:
1919 repository: harbor.gitdata.me/app/gitdata-act
20 # 留空 = 跟随 image.tag
21 tag: ""
20 # 引擎已独立成仓(git-data-actions),不跟随 image.tag,按引擎自己的版本号走
21 tag: "0.2.89"
2222 pullPolicy: IfNotPresent
2323
2424serviceAccount:
Modified lib/ci/examples/actions_fixture.rs View lib/ci/examples/actions_fixture.rs
@@ -62,10 +62,14 @@
6262 config.email.from_address = Some("fixture@localhost".into());
6363 config.ci.enabled = true;
6464 config.ci.secret_encryption_key = "QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUE=".into();
65 config.ci.actions_engine_binary = std::env::current_dir()?
66 .join("target/gitdata-actions-engine")
67 .to_string_lossy()
68 .into_owned();
65 // Built by the git-data-actions repository now; override with ACTIONS_ENGINE_BINARY.
66 config.ci.actions_engine_binary = std::env::var("ACTIONS_ENGINE_BINARY").unwrap_or_else(|_| {
67 std::env::current_dir()
68 .expect("current dir")
69 .join("target/gitdata-actions-engine")
70 .to_string_lossy()
71 .into_owned()
72 });
6973 config.ci.runtime_base_url = "http://127.0.0.1:43120".into();
7074 config.ci.event_queue_topic = "gitdata:ci-fixture:events".into();
7175 config.ci.lease_secs = 120;
Modified lib/ci/src/actions.rs View lib/ci/src/actions.rs
@@ -142,6 +142,11 @@
142142 Ok(())
143143 }
144144
145 /// One `gitdata-actions-engine` process per call: one JSON line in, one out. The
146 /// engine is built by the separate `git-data-actions` repository; its ops are
147 /// `parse`, `match`, `trigger`, `cron`, `evaluate`, `expand`, `execute`,
148 /// `oidc_sign`, `oidc_jwks`. Any request/response field change has to land in both
149 /// repositories together.
145150 pub async fn actions_engine(&self, request: Value) -> CiResult<Value> {
146151 let config = &self.config().ci;
147152 let mut child = Command::new(&config.actions_engine_binary)
Modified Dockerfile View Dockerfile
@@ -1,4 +1,10 @@
11# syntax=docker/dockerfile:1
2
3# The actions engine (gitdata-actions-engine) lives in its own repository now,
4# `git-data-actions`. Pin the tag on release — the runner pool pulls the very same
5# image, so an unset/`latest` value is only a convenience for local builds.
6ARG ACTIONS_IMAGE=harbor.gitdata.me/app/gitdata-act:latest
7
28# Bun bundles the web SPA from inside web/build.rs. Take the binary from the
39# official image instead of running the network installer: the installer pulls
410# its archive from GitHub releases, which regularly times out here.
@@ -51,18 +57,10 @@
5157########## actions engine ##########
5258# The server side of CI (workflow parse/trigger/evaluate/expand, cron matching,
5359# OIDC) shells out to `gitdata-actions-engine` on every operation, not just the
54# runner; ship it in this image too.
55FROM golang:1.27-bookworm AS actions-engine
56
57WORKDIR /src
58COPY act/go.mod act/go.sum ./
59COPY act/cmd ./cmd
60COPY act/pkg ./pkg
61
62RUN --mount=type=cache,target=/go/pkg/mod \
63 --mount=type=cache,target=/root/.cache/go-build \
64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" \
65 -o /out/gitdata-actions-engine ./cmd/gitdata-actions-engine
60# runner; ship it in this image too. The binary is lifted out of the published
61# actions image instead of built here, so the server and the runner pool always
62# run the very same engine build.
63FROM ${ACTIONS_IMAGE} AS actions-engine
6664
6765########## runtime ##########
6866FROM debian:bookworm-slim