diff --git a/Makefile b/Makefile index f945756d39..e2e7be361b 100644 --- a/Makefile +++ b/Makefile @@ -10,8 +10,10 @@ RUNNER_TAG ?= ${VERSION} TEST_REPO ?= ${DOCKER_USER}/actions-runner-controller TEST_ORG ?= TEST_ORG_REPO ?= +TEST_EPHEMERAL ?= false SYNC_PERIOD ?= 5m USE_RUNNERSET ?= +RUNNER_FEATURE_FLAG_EPHEMERAL ?= KUBECONTEXT ?= kind-acceptance CLUSTER ?= acceptance CERT_MANAGER_VERSION ?= v1.1.1 @@ -206,6 +208,8 @@ acceptance/deploy: NAME=${NAME} DOCKER_USER=${DOCKER_USER} VERSION=${VERSION} RUNNER_NAME=${RUNNER_NAME} RUNNER_TAG=${RUNNER_TAG} TEST_REPO=${TEST_REPO} \ TEST_ORG=${TEST_ORG} TEST_ORG_REPO=${TEST_ORG_REPO} SYNC_PERIOD=${SYNC_PERIOD} \ USE_RUNNERSET=${USE_RUNNERSET} \ + TEST_EPHEMERAL=${TEST_EPHEMERAL} \ + RUNNER_FEATURE_FLAG_EPHEMERAL=${RUNNER_FEATURE_FLAG_EPHEMERAL} \ acceptance/deploy.sh acceptance/tests: diff --git a/README.md b/README.md index d0413fe6f2..09767e5003 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ ToC: - [Runner Groups](#runner-groups) - [Using IRSA (IAM Roles for Service Accounts) in EKS](#using-irsa-iam-roles-for-service-accounts-in-eks) - [Stateful Runners](#stateful-runners) + - [Ephemeral Runners](#ephemeral-runners) - [Software Installed in the Runner Image](#software-installed-in-the-runner-image) - [Common Errors](#common-errors) - [Contributing](#contributing) @@ -351,6 +352,8 @@ This, in combination with a correctly configured HorizontalRunnerAutoscaler, all __**IMPORTANT : Due to limitations / a bug with GitHub's [routing engine](https://docs.github.com/en/actions/hosting-your-own-runners/using-self-hosted-runners-in-a-workflow#routing-precedence-for-self-hosted-runners) autoscaling does NOT work correctly with RunnerDeployments that target the enterprise level. Scaling activity works as expected however jobs fail to get assigned to the scaled out replicas. This was explored in issue [#470](https://github.com/actions-runner-controller/actions-runner-controller/issues/470). Once GitHub resolves the issue with their backend service we expect the solution to be able to support autoscaled enterprise runnerdeploments without any additional changes.**__ +__**NOTE: Once `workflow_job` webhook events are released on GitHub, the webhook-based autoscaling is the preferred way of autoscaling, because it is easy to configure and has the ability to accurately detect which runners to scale. See [Example 3: Scale on each `workflow_job` event](#example-3-scale-on-each-workflow_job-event)**___ + A `RunnerDeployment` (excluding enterprise runners) can scale the number of runners between `minReplicas` and `maxReplicas` fields based the chosen scaling metric as defined in the `metrics` attribute **Scaling Metrics** @@ -594,6 +597,34 @@ spec: See ["activity types"](https://docs.github.com/en/actions/reference/events-that-trigger-workflows#pull_request) for the list of valid values for `scaleUpTriggers[].githubEvent.pullRequest.types`. +###### Example 3: Scale on each `workflow_job` event + +> This feature depends on an unreleased GitHub feature + +```yaml +kind: RunnerDeployment: +metadata: + name: myrunners +spec: + repository: example/myrepo +--- +kind: HorizontalRunnerAutoscaler +spec: + scaleTargetRef: + name: myrunners + scaleUpTriggers: + - githubEvent: {} + duration: "30m" +``` + +You can configure your GitHub webhook settings to only include `Workflows Job` events, so that it sends us three kinds of `workflow_job` events per a job run. + +Each kind has a `status` of `queued`, `in_progress` and `completed`. +With the above configuration, `actions-runner-controller` adds one runner for a `workflow_job` event whose `status` is `queued`. Similarly, it removes one runner for a `workflow_job` event whose `status` is `completed`. + +Beware that a scale-down after a scale-up is deferred until `scaleDownDelaySecondsAfterScaleOut` elapses. Let's say you had configured `scaleDownDelaySecondsAfterScaleOut` of 60 seconds, 2 consequtive workflow jobs will result in immediately adding 2 runners. The 2 runners are removed only after 60 seconds. This basically gives you 60 seconds of a "grace period" that makes it possible for self-hosted runners to immediately run additional workflow jobs enqueued in that 60 seconds. + +You must not include `spec.metrics` like `PercentageRunnersBusy` when using this feature, as it is unnecessary. That is, if you've configured the webhook for `workflow_job`, it should be enough for all the scale-out need. #### Autoscaling to/from 0 @@ -1032,6 +1063,49 @@ Under the hood, `RunnerSet` relies on Kubernetes's `StatefulSet` and Mutating We We envision that `RunnerSet` will eventually replace `RunnerDeployment`, as `RunnerSet` provides a more standard API that is easy to learn and use because it is based on `StatefulSet`, and it has a support for `volumeClaimTemplates` which is crucial to manage dynamically provisioned persistent volumes. +### Ephemeral Runners + +Both `RunnerDeployment` and `RunnerSet` has ability to configure `ephemeral: true` in the spec. + +When it is configured, it passes a `--once` flag to every runner. + +`--once` is an experimental `actions/runner` feature that instructs the runner to stop after the first job run. But it is a known race issue that may fetch a job even when it's being terminated. If a runner fetched a job while terminating, the job is very likely to fail because the terminating runner doesn't wait for the job to complete. This is tracked in #466. + +> The below feature depends on an unreleased GitHub feature + +GitHub seems to be adding an another flag called `--ephemeral` that is race-free. The pull request to add it to `actions/runner` can be found at https://github.com/actions/runner/pull/660. + +`actions-runner-controller` has a feature flag to enable usign `--ephemeral` instead of `--once`. + +To use it, you need to build your own `actions/runner` binary built from https://github.com/actions/runner/pull/660 in the runner container image, and set the environment variable `RUNNER_FEATURE_FLAG_EPHEMERAL` to `true` on runner containers in your runner pods. + +Please see comments in [`runner/Dockerfile`](/runner/Dockerfile) for more information about how to build a custom image using your own `actions/runner` binary. + +For example, a `RunnerSet` config with the flag enabled looks like: + +``` +kind: RunnerSet +metadata: + name: example-runnerset +spec: + # ... + template: + metadata: + labels: + app: example-runnerset + spec: + containers: + - name: runner + imagePullPolicy: IfNotPresent + env: + - name: RUNNER_FEATURE_FLAG_EPHEMERAL + value: "true" +``` + +Note that once https://github.com/actions/runner/pull/660 becomes generally available on GitHub, you no longer need to build a custom runner image to use this feature. Just set `RUNNER_FEATURE_FLAG_EPHEMERAL` and it should use `--ephemeral`. + +In the future, `--once` might get removed in `actions/runner`. `actions-runner-controller` will make `--ephemeral` the default option for `ephemeral: true` runners until the legacy flag is removed. + ### Software Installed in the Runner Image **Cloud Tooling**<br /> diff --git a/acceptance/testdata/repo.runnerset.yaml b/acceptance/testdata/repo.runnerset.yaml index c81b8ca320..d9ae68dea9 100644 --- a/acceptance/testdata/repo.runnerset.yaml +++ b/acceptance/testdata/repo.runnerset.yaml @@ -18,7 +18,7 @@ spec: # From my limited testing, `ephemeral: true` is more reliable. # Seomtimes, updating already deployed runners from `ephemeral: false` to `ephemeral: true` seems to # result in queued jobs hanging forever. - ephemeral: false + ephemeral: ${TEST_EPHEMERAL} repository: ${TEST_REPO} # @@ -52,5 +52,8 @@ spec: containers: - name: runner imagePullPolicy: IfNotPresent + env: + - name: RUNNER_FEATURE_FLAG_EPHEMERAL + value: "${RUNNER_FEATURE_FLAG_EPHEMERAL}" #- name: docker # #image: mumoshu/actions-runner-dind:dev diff --git a/charts/actions-runner-controller/templates/githubwebhook.role.yaml b/charts/actions-runner-controller/templates/githubwebhook.role.yaml index d9d2290886..e175a45cb5 100644 --- a/charts/actions-runner-controller/templates/githubwebhook.role.yaml +++ b/charts/actions-runner-controller/templates/githubwebhook.role.yaml @@ -35,6 +35,14 @@ rules: - get - patch - update +- apiGroups: + - actions.summerwind.dev + resources: + - runnersets + verbs: + - get + - list + - watch - apiGroups: - actions.summerwind.dev resources: diff --git a/controllers/horizontal_runner_autoscaler_webhook.go b/controllers/horizontal_runner_autoscaler_webhook.go index 62e76f78ce..af7fcab9a4 100644 --- a/controllers/horizontal_runner_autoscaler_webhook.go +++ b/controllers/horizontal_runner_autoscaler_webhook.go @@ -183,6 +183,45 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) Handle(w http.Respons "action", e.GetAction(), ) } + case *gogithub.WorkflowJobEvent: + if workflowJob := e.GetWorkflowJob(); workflowJob != nil { + log = log.WithValues( + "workflowJob.status", workflowJob.GetStatus(), + "workflowJob.labels", workflowJob.Labels, + "repository.name", e.Repo.GetName(), + "repository.owner.login", e.Repo.Owner.GetLogin(), + "repository.owner.type", e.Repo.Owner.GetType(), + "action", e.GetAction(), + ) + } + + labels := e.WorkflowJob.Labels + + switch e.GetAction() { + case "queued", "completed": + target, err = autoscaler.getJobScaleUpTargetForRepoOrOrg( + context.TODO(), + log, + e.Repo.GetName(), + e.Repo.Owner.GetLogin(), + e.Repo.Owner.GetType(), + labels, + ) + + if target != nil { + if e.GetAction() == "queued" { + target.Amount = 1 + } else if e.GetAction() == "completed" { + // A nagative amount is processed in the tryScale func as a scale-down request, + // that erasese the oldest CapacityReservation with the same amount. + // If the first CapacityReservation was with Replicas=1, this negative scale target erases that, + // so that the resulting desired replicas decreases by 1. + target.Amount = -1 + } + } + default: + + } case *gogithub.PingEvent: ok = true @@ -227,7 +266,7 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) Handle(w http.Respons return } - if err := autoscaler.tryScaleUp(context.TODO(), target); err != nil { + if err := autoscaler.tryScale(context.TODO(), target); err != nil { log.Error(err, "could not scale up") return @@ -237,7 +276,7 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) Handle(w http.Respons w.WriteHeader(http.StatusOK) - msg := fmt.Sprintf("scaled %s by 1", target.Name) + msg := fmt.Sprintf("scaled %s by %d", target.Name, target.Amount) autoscaler.Log.Info(msg) @@ -394,7 +433,137 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) getScaleUpTarget(ctx return nil, nil } -func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) tryScaleUp(ctx context.Context, target *ScaleTarget) error { +func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) getJobScaleUpTargetForRepoOrOrg(ctx context.Context, log logr.Logger, repo, owner, ownerType string, labels []string) (*ScaleTarget, error) { + repositoryRunnerKey := owner + "/" + repo + + if target, err := autoscaler.getJobScaleTarget(ctx, repositoryRunnerKey, labels); err != nil { + log.Info("finding repository-wide runner", "repository", repositoryRunnerKey) + return nil, err + } else if target != nil { + log.Info("job scale up target is repository-wide runners", "repository", repo) + return target, nil + } + + if ownerType == "User" { + log.V(1).Info("no repository runner found", "organization", owner) + + return nil, nil + } + + if target, err := autoscaler.getJobScaleTarget(ctx, owner, labels); err != nil { + log.Info("finding organizational runner", "organization", owner) + return nil, err + } else if target != nil { + log.Info("job scale up target is organizational runners", "organization", owner) + return target, nil + } else { + log.V(1).Info("no repository runner or organizational runner found", + "repository", repositoryRunnerKey, + "organization", owner, + ) + } + + return nil, nil +} + +func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) getJobScaleTarget(ctx context.Context, name string, labels []string) (*ScaleTarget, error) { + hras, err := autoscaler.findHRAsByKey(ctx, name) + if err != nil { + return nil, err + } + + autoscaler.Log.V(1).Info(fmt.Sprintf("Found %d HRAs by key", len(hras)), "key", name) + +HRA: + for _, hra := range hras { + if !hra.ObjectMeta.DeletionTimestamp.IsZero() { + continue + } + + if len(hra.Spec.ScaleUpTriggers) > 1 { + autoscaler.Log.V(1).Info("Skipping this HRA as it has too many ScaleUpTriggers to be used in workflow_job based scaling", "hra", hra.Name) + + continue + } + + var duration metav1.Duration + + if len(hra.Spec.ScaleUpTriggers) > 0 { + duration = hra.Spec.ScaleUpTriggers[0].Duration + } + + if duration.Duration <= 0 { + // Try to release the reserved capacity after at least 10 minutes by default, + // we won't end up in the reserved capacity remained forever in case GitHub somehow stopped sending us "completed" workflow_job events. + // GitHub usually send us those but nothing is 100% guaranteed, e.g. in case of something went wrong on GitHub :) + // Probably we'd better make this configurable via custom resources in the future? + duration.Duration = 10 * time.Minute + } + + switch hra.Spec.ScaleTargetRef.Kind { + case "RunnerSet": + var rs v1alpha1.RunnerSet + + if err := autoscaler.Client.Get(context.Background(), types.NamespacedName{Namespace: hra.Namespace, Name: hra.Spec.ScaleTargetRef.Name}, &rs); err != nil { + return nil, err + } + + if len(labels) == 1 && labels[0] == "self-hosted" { + return &ScaleTarget{HorizontalRunnerAutoscaler: hra, ScaleUpTrigger: v1alpha1.ScaleUpTrigger{Duration: duration}}, nil + } + + // Ensure that the RunnerSet-managed runners have all the labels requested by the workflow_job. + for _, l := range labels { + var matched bool + for _, l2 := range rs.Spec.Labels { + if l == l2 { + matched = true + break + } + } + + if !matched { + continue HRA + } + } + + return &ScaleTarget{HorizontalRunnerAutoscaler: hra, ScaleUpTrigger: v1alpha1.ScaleUpTrigger{Duration: duration}}, nil + case "RunnerDeployment", "": + var rd v1alpha1.RunnerDeployment + + if err := autoscaler.Client.Get(context.Background(), types.NamespacedName{Namespace: hra.Namespace, Name: hra.Spec.ScaleTargetRef.Name}, &rd); err != nil { + return nil, err + } + + if len(labels) == 1 && labels[0] == "self-hosted" { + return &ScaleTarget{HorizontalRunnerAutoscaler: hra, ScaleUpTrigger: v1alpha1.ScaleUpTrigger{Duration: duration}}, nil + } + + // Ensure that the RunnerDeployment-managed runners have all the labels requested by the workflow_job. + for _, l := range labels { + var matched bool + for _, l2 := range rd.Spec.Template.Labels { + if l == l2 { + matched = true + break + } + } + + if !matched { + continue HRA + } + } + + return &ScaleTarget{HorizontalRunnerAutoscaler: hra, ScaleUpTrigger: v1alpha1.ScaleUpTrigger{Duration: duration}}, nil + default: + return nil, fmt.Errorf("unsupported scaleTargetRef.kind: %v", hra.Spec.ScaleTargetRef.Kind) + } + } + + return nil, nil +} + +func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) tryScale(ctx context.Context, target *ScaleTarget) error { if target == nil { return nil } @@ -403,16 +572,38 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) tryScaleUp(ctx contex amount := 1 - if target.ScaleUpTrigger.Amount > 0 { + if target.ScaleUpTrigger.Amount != 0 { amount = target.ScaleUpTrigger.Amount } capacityReservations := getValidCapacityReservations(copy) - copy.Spec.CapacityReservations = append(capacityReservations, v1alpha1.CapacityReservation{ - ExpirationTime: metav1.Time{Time: time.Now().Add(target.ScaleUpTrigger.Duration.Duration)}, - Replicas: amount, - }) + if amount > 0 { + copy.Spec.CapacityReservations = append(capacityReservations, v1alpha1.CapacityReservation{ + ExpirationTime: metav1.Time{Time: time.Now().Add(target.ScaleUpTrigger.Duration.Duration)}, + Replicas: amount, + }) + } else if amount < 0 { + var reservations []v1alpha1.CapacityReservation + + var found bool + + for _, r := range capacityReservations { + if !found && r.Replicas+amount == 0 { + found = true + } else { + reservations = append(reservations, r) + } + } + + copy.Spec.CapacityReservations = reservations + } + + autoscaler.Log.Info( + "Patching hra for capacityReservations update", + "before", target.HorizontalRunnerAutoscaler.Spec.CapacityReservations, + "after", copy.Spec.CapacityReservations, + ) if err := autoscaler.Client.Patch(ctx, copy, client.MergeFrom(&target.HorizontalRunnerAutoscaler)); err != nil { return fmt.Errorf("patching horizontalrunnerautoscaler to add capacity reservation: %w", err) @@ -450,13 +641,26 @@ func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) SetupWithManager(mgr return nil } - var rd v1alpha1.RunnerDeployment + switch hra.Spec.ScaleTargetRef.Kind { + case "", "RunnerDeployment": + var rd v1alpha1.RunnerDeployment - if err := autoscaler.Client.Get(context.Background(), types.NamespacedName{Namespace: hra.Namespace, Name: hra.Spec.ScaleTargetRef.Name}, &rd); err != nil { - return nil + if err := autoscaler.Client.Get(context.Background(), types.NamespacedName{Namespace: hra.Namespace, Name: hra.Spec.ScaleTargetRef.Name}, &rd); err != nil { + return nil + } + + return []string{rd.Spec.Template.Spec.Repository, rd.Spec.Template.Spec.Organization} + case "RunnerSet": + var rs v1alpha1.RunnerSet + + if err := autoscaler.Client.Get(context.Background(), types.NamespacedName{Namespace: hra.Namespace, Name: hra.Spec.ScaleTargetRef.Name}, &rs); err != nil { + return nil + } + + return []string{rs.Spec.Repository, rs.Spec.Organization} } - return []string{rd.Spec.Template.Spec.Repository, rd.Spec.Template.Spec.Organization} + return nil }); err != nil { return err } diff --git a/go.mod b/go.mod index e3d9b7fd9f..92fb404a4a 100644 --- a/go.mod +++ b/go.mod @@ -24,4 +24,4 @@ require ( sigs.k8s.io/yaml v1.2.0 ) -replace github.com/google/go-github/v37 => github.com/google/go-github/v37 v37.0.1-0.20210713230028-465df60a8ec3 +replace github.com/google/go-github/v37 => github.com/mumoshu/go-github/v37 v37.0.100 diff --git a/go.sum b/go.sum index 4a972f0719..2e93cd5670 100644 --- a/go.sum +++ b/go.sum @@ -292,6 +292,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mumoshu/go-github/v37 v37.0.100 h1:a0S2oEJ8naEW5M4y6S+wu3ufSe9PmKxu77C72VJ6LLw= +github.com/mumoshu/go-github/v37 v37.0.100/go.mod h1:LM7in3NmXDrX58GbEHy7FtNLbI2JijX93RnMKvWG3m4= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= diff --git a/runner/Dockerfile b/runner/Dockerfile index 8b9aa1a014..f41e08dbd3 100644 --- a/runner/Dockerfile +++ b/runner/Dockerfile @@ -67,6 +67,24 @@ RUN set -vx; \ ENV RUNNER_ASSETS_DIR=/runnertmp ENV HOME=/home/runner +# Uncomment the below COPY to use your own custom build of actions-runner. +# +# To build a custom runner: +# - Clone the actions/runner repo `git clone git@github.com:actions/runner.git $repo` +# - Run `cd $repo/src` +# - Run `./dev.sh layout Release linux-x64` +# - Run `./dev.sh package Release linux-x64` +# - Run cp ../_package/actions-runner-linux-x64-2.279.0.tar.gz ../../actions-runner-controller/runner/ +# - Beware that `2.279.0` might change across versions +# +# See https://github.com/actions/runner/blob/main/.github/workflows/release.yml for more informatino on how you can use dev.sh +# +# If you're willing to uncomment the following line, you'd also need to comment-out the +# && curl -L -o runner.tar.gz https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-${ARCH}-${RUNNER_VERSION}.tar.gz \ +# line in the next `RUN` command in this Dockerfile, to avoid overwiding this runner.tar.gz with a remote one. + +# COPY actions-runner-linux-x64-2.279.0.tar.gz /runnertmp/runner.tar.gz + # Runner download supports amd64 as x64. Externalstmp is needed for making mount points work inside DinD. # # libyaml-dev is required for ruby/setup-ruby action. @@ -76,6 +94,7 @@ RUN export ARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2) \ && if [ "$ARCH" = "amd64" ] || [ "$ARCH" = "x86_64" ] || [ "$ARCH" = "i386" ]; then export ARCH=x64 ; fi \ && mkdir -p "$RUNNER_ASSETS_DIR" \ && cd "$RUNNER_ASSETS_DIR" \ + # Comment-out the below curl invocation when you use your own build of actions/runner && curl -L -o runner.tar.gz https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-${ARCH}-${RUNNER_VERSION}.tar.gz \ && tar xzf ./runner.tar.gz \ && rm runner.tar.gz \ diff --git a/runner/entrypoint.sh b/runner/entrypoint.sh index a3bf4fc29a..7b248fa9fa 100755 --- a/runner/entrypoint.sh +++ b/runner/entrypoint.sh @@ -58,13 +58,20 @@ sudo chown -R runner:docker /runner mv /runnertmp/* /runner/ cd /runner + +config_args=() +if [ "${RUNNER_FEATURE_FLAG_EPHEMERAL:-}" == "true" -a "${RUNNER_EPHEMERAL}" != "false" ]; then + config_args+=(--ephemeral) + echo "Passing --ephemeral to config.sh to enable the ephemeral runner." +fi + ./config.sh --unattended --replace \ --name "${RUNNER_NAME}" \ --url "${GITHUB_URL}${ATTACH}" \ --token "${RUNNER_TOKEN}" \ --runnergroup "${RUNNER_GROUPS}" \ --labels "${RUNNER_LABELS}" \ - --work "${RUNNER_WORKDIR}" + --work "${RUNNER_WORKDIR}" "${config_args[@]}" if [ -f /runner/.runner ]; then echo Runner has successfully been configured with the following data. @@ -108,8 +115,9 @@ for f in runsvc.sh RunnerService.js; do done args=() -if [ "${RUNNER_EPHEMERAL}" != "false" ]; then +if [ "${RUNNER_FEATURE_FLAG_EPHEMERAL:-}" != "true" -a "${RUNNER_EPHEMERAL}" != "false" ]; then args+=(--once) + echo "Passing --once to runsvc.sh to enable the legacy ephemeral runner." fi unset RUNNER_NAME RUNNER_REPO RUNNER_TOKEN