From d14092ea564de730d05220bc18c89340939fa589 Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Sun, 3 Aug 2025 11:58:58 +0200 Subject: [PATCH] fix: add service container health check Refs: https://github.com/nektos/act/pull/2354/files Signed-off-by: https://github.com/ChristopherHX --- act/container/container_types.go | 9 +++++ act/container/docker_run.go | 24 +++++++++++++ act/container/host_environment.go | 4 +++ act/runner/run_context.go | 35 +++++++++++++++++++ act/runner/runner_test.go | 1 + .../push.yml | 19 ++++++++++ act/runner/testdata/services/push.yaml | 4 +-- 7 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 act/runner/testdata/mysql-service-container-with-health-check/push.yml diff --git a/act/container/container_types.go b/act/container/container_types.go index 3d3a5162..0dea315a 100644 --- a/act/container/container_types.go +++ b/act/container/container_types.go @@ -63,6 +63,7 @@ type Container interface { Remove() common.Executor Close() common.Executor ReplaceLogWriter(io.Writer, io.Writer) (io.Writer, io.Writer) + GetHealth(ctx context.Context) Health } // NewDockerBuildExecutorInput the input for the NewDockerBuildExecutor function @@ -82,3 +83,11 @@ type NewDockerPullExecutorInput struct { Username string Password string } + +type Health int + +const ( + HealthStarting Health = iota + HealthHealthy + HealthUnHealthy +) diff --git a/act/container/docker_run.go b/act/container/docker_run.go index 1c790d6a..4d3073c8 100644 --- a/act/container/docker_run.go +++ b/act/container/docker_run.go @@ -191,6 +191,30 @@ func (cr *containerReference) Remove() common.Executor { ).IfNot(common.Dryrun) } +func (cr *containerReference) GetHealth(ctx context.Context) Health { + resp, err := cr.cli.ContainerInspect(ctx, cr.id) + logger := common.Logger(ctx) + if err != nil { + logger.Errorf("failed to query container health %s", err) + return HealthUnHealthy + } + 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") { + logger.Debugf("no container health check defined") + return HealthHealthy + } + + logger.Infof("container health of %s (%s) is %s", cr.id, resp.Config.Image, resp.State.Health.Status) + switch resp.State.Health.Status { + case "starting": + return HealthStarting + case "healthy": + return HealthHealthy + case "unhealthy": + return HealthUnHealthy + } + return HealthUnHealthy +} + func (cr *containerReference) ReplaceLogWriter(stdout, stderr io.Writer) (io.Writer, io.Writer) { out := cr.input.Stdout err := cr.input.Stderr diff --git a/act/container/host_environment.go b/act/container/host_environment.go index 045d7774..8f36049a 100644 --- a/act/container/host_environment.go +++ b/act/container/host_environment.go @@ -493,6 +493,10 @@ func (e *HostEnvironment) GetRunnerContext(_ context.Context) map[string]interfa } } +func (e *HostEnvironment) GetHealth(ctx context.Context) Health { + return HealthHealthy +} + func (e *HostEnvironment) ReplaceLogWriter(stdout, _ io.Writer) (io.Writer, io.Writer) { org := e.StdOut e.StdOut = stdout diff --git a/act/runner/run_context.go b/act/runner/run_context.go index d1fc1cd8..d3f14f00 100644 --- a/act/runner/run_context.go +++ b/act/runner/run_context.go @@ -610,6 +610,7 @@ func (rc *RunContext) startJobContainer() common.Executor { Mode: 0o666, Body: "", }), + rc.waitForServiceContainers(), )(ctx) } } @@ -744,6 +745,40 @@ func (rc *RunContext) startServiceContainers(_ string) common.Executor { } } +func (rc *RunContext) waitForServiceContainer(c container.ExecutionsEnvironment) common.Executor { + return func(ctx context.Context) error { + sctx, cancel := context.WithTimeout(ctx, time.Minute*5) + defer cancel() + var health container.Health + delay := time.Second + for i := 0; ; i++ { + health = c.GetHealth(sctx) + if health != container.HealthStarting || i > 30 { + break + } + time.Sleep(delay) + delay *= 2 + if delay > 10*time.Second { + delay = 10 * time.Second + } + } + if health == container.HealthHealthy { + return nil + } + return fmt.Errorf("service container failed to start") + } +} + +func (rc *RunContext) waitForServiceContainers() common.Executor { + return func(ctx context.Context) error { + execs := []common.Executor{} + for _, c := range rc.ServiceContainers { + execs = append(execs, rc.waitForServiceContainer(c)) + } + return common.NewParallelExecutor(len(execs), execs...)(ctx) + } +} + func (rc *RunContext) stopServiceContainers() common.Executor { return func(ctx context.Context) error { execs := []common.Executor{} diff --git a/act/runner/runner_test.go b/act/runner/runner_test.go index 0a11a5ea..60d370ab 100644 --- a/act/runner/runner_test.go +++ b/act/runner/runner_test.go @@ -322,6 +322,7 @@ func TestRunner_RunEvent(t *testing.T) { // services {workdir, "services", "push", "", platforms, secrets}, {workdir, "services-with-container", "push", "", platforms, secrets}, + {workdir, "mysql-service-container-with-health-check", "push", "", platforms, secrets}, } for _, table := range tables { diff --git a/act/runner/testdata/mysql-service-container-with-health-check/push.yml b/act/runner/testdata/mysql-service-container-with-health-check/push.yml new file mode 100644 index 00000000..e48cdbb1 --- /dev/null +++ b/act/runner/testdata/mysql-service-container-with-health-check/push.yml @@ -0,0 +1,19 @@ +name: service-container +on: push +jobs: + service-container-test: + runs-on: ubuntu-latest + container: code.forgejo.org/oci/mysql:8.4 + services: + maindb: + image: code.forgejo.org/oci/mysql:8.4 + env: + MYSQL_DATABASE: dbname + MYSQL_USER: dbuser + MYSQL_PASSWORD: dbpass + MYSQL_RANDOM_ROOT_PASSWORD: yes + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + steps: + - run: mysql -u dbuser -D dbname -pdbpass -h maindb -e "create table T(id INT NOT NULL AUTO_INCREMENT, val VARCHAR(255), PRIMARY KEY (id))" + - run: mysql -u dbuser -D dbname -pdbpass -h maindb -e "insert into T(val) values ('test'),('h')" + - run: mysql -u dbuser -D dbname -pdbpass -h maindb -e "select * from T" diff --git a/act/runner/testdata/services/push.yaml b/act/runner/testdata/services/push.yaml index 03ac0855..3884a43f 100644 --- a/act/runner/testdata/services/push.yaml +++ b/act/runner/testdata/services/push.yaml @@ -6,7 +6,7 @@ jobs: runs-on: ubuntu-latest services: postgres: - image: code.forgejo.org/oci/bitnami/postgresql:16 + image: code.forgejo.org/oci/postgres:16 env: POSTGRES_USER: runner POSTGRES_PASSWORD: mysecretdbpass @@ -15,7 +15,7 @@ jobs: --health-cmd pg_isready --health-interval 10s --health-timeout 5s - --health-retries 5 + --health-retries 20 ports: - 5432:5432 steps: