From 6d8dc7518f82295a93061fd99ff3a96b90913219 Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Sun, 19 Apr 2026 21:54:54 +0200 Subject: [PATCH 1/3] Add e2e pipeline restart test --- e2e/scenarios/restart_test.go | 94 +++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 e2e/scenarios/restart_test.go diff --git a/e2e/scenarios/restart_test.go b/e2e/scenarios/restart_test.go new file mode 100644 index 00000000000..bd2d977391b --- /dev/null +++ b/e2e/scenarios/restart_test.go @@ -0,0 +1,94 @@ +// Copyright 2026 Woodpecker Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build test + +package scenarios + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.woodpecker-ci.org/woodpecker/v3/e2e/setup" + forge_types "go.woodpecker-ci.org/woodpecker/v3/server/forge/types" + "go.woodpecker-ci.org/woodpecker/v3/server/model" + "go.woodpecker-ci.org/woodpecker/v3/server/pipeline" +) + +// TestRestartPipeline verifies pipeline.Restart produces a distinct pipeline +// linked to the original via Parent, with its own fresh workflow rows, and +// that the original's workflows are untouched. This exercises the same +// workflow-persistence path as Create (saveWorkflowsFromPipelineBuilder → +// WorkflowsCreate) — guarding against double-insert and ID-reuse regressions. +func TestRestartPipeline(t *testing.T) { + env := setup.StartServer(t.Context(), t, []*forge_types.FileMeta{ + {Name: ".woodpecker.yaml", Data: simpleSuccessYAML}, + }) + agent := setup.StartAgent(t.Context(), t, env.GRPCAddr) + setup.WaitForAgentRegistered(t, env.Store, agent) + + // First run. + original, err := pipeline.Create(t.Context(), env.Store, env.Fixtures.Repo, &model.Pipeline{ + Event: model.EventPush, + Branch: "main", + Commit: "deadbeef", + Ref: "refs/heads/main", + Author: env.Fixtures.Owner.Login, + Sender: env.Fixtures.Owner.Login, + }) + require.NoError(t, err, "create original pipeline") + originalFinished := setup.WaitForPipeline(t, env.Store, original.ID) + require.Equal(t, model.StatusSuccess, originalFinished.Status, "original should succeed") + + originalWorkflows, err := env.Store.WorkflowGetTree(originalFinished) + require.NoError(t, err) + require.Len(t, originalWorkflows, 1, "original should have exactly one workflow") + originalWorkflowID := originalWorkflows[0].ID + + // Restart it. + restarted, err := pipeline.Restart(t.Context(), env.Store, originalFinished, env.Fixtures.Owner, env.Fixtures.Repo, nil) + require.NoError(t, err, "restart pipeline") + require.NotNil(t, restarted) + + // Parent/ID invariants. + assert.NotEqual(t, originalFinished.ID, restarted.ID, "restart should have a new ID") + assert.NotEqual(t, originalFinished.Number, restarted.Number, "restart should have a new number") + assert.Equal(t, originalFinished.Number, restarted.Parent, "restart.Parent should point at original.Number") + + // The restart runs through the same start path — wait for it to finish. + restartedFinished := setup.WaitForPipeline(t, env.Store, restarted.ID) + assert.Equal(t, model.StatusSuccess, restartedFinished.Status, "restarted pipeline should succeed") + + // Restart should have its OWN workflows, not reuse the originals. + restartedWorkflows, err := env.Store.WorkflowGetTree(restartedFinished) + require.NoError(t, err) + require.Len(t, restartedWorkflows, 1, "restart should produce its own workflow") + assert.NotEqual(t, originalWorkflowID, restartedWorkflows[0].ID, + "restart should insert a new workflow row, not reassign the original") + assert.Equal(t, restartedFinished.ID, restartedWorkflows[0].PipelineID, + "restarted workflow must be linked to the restarted pipeline") + assert.Equal(t, model.StatusSuccess, restartedWorkflows[0].State) + assert.Greater(t, restartedWorkflows[0].AgentID, int64(0)) + + // Original's workflows must remain pointing at the original pipeline. + originalAfter, err := env.Store.WorkflowGetTree(originalFinished) + require.NoError(t, err) + require.Len(t, originalAfter, 1) + assert.Equal(t, originalWorkflowID, originalAfter[0].ID, + "restart must not mutate the original's workflow row") + assert.Equal(t, originalFinished.ID, originalAfter[0].PipelineID, + "original's workflow must still be linked to the original pipeline") +} From fce151abc00ad21a32c437abc404bdf325cb009b Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Sun, 19 Apr 2026 21:57:03 +0200 Subject: [PATCH 2/3] rm unesseary comment --- e2e/scenarios/restart_test.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/e2e/scenarios/restart_test.go b/e2e/scenarios/restart_test.go index bd2d977391b..d46013e9623 100644 --- a/e2e/scenarios/restart_test.go +++ b/e2e/scenarios/restart_test.go @@ -30,9 +30,7 @@ import ( // TestRestartPipeline verifies pipeline.Restart produces a distinct pipeline // linked to the original via Parent, with its own fresh workflow rows, and -// that the original's workflows are untouched. This exercises the same -// workflow-persistence path as Create (saveWorkflowsFromPipelineBuilder → -// WorkflowsCreate) — guarding against double-insert and ID-reuse regressions. +// that the original's workflows are untouched. func TestRestartPipeline(t *testing.T) { env := setup.StartServer(t.Context(), t, []*forge_types.FileMeta{ {Name: ".woodpecker.yaml", Data: simpleSuccessYAML}, From f67d5f1725928c95f974edf72da1de5fa8634347 Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Sun, 19 Apr 2026 21:58:38 +0200 Subject: [PATCH 3/3] rm unesseary aloc --- e2e/scenarios/restart_test.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/e2e/scenarios/restart_test.go b/e2e/scenarios/restart_test.go index d46013e9623..051f022bc6a 100644 --- a/e2e/scenarios/restart_test.go +++ b/e2e/scenarios/restart_test.go @@ -54,7 +54,6 @@ func TestRestartPipeline(t *testing.T) { originalWorkflows, err := env.Store.WorkflowGetTree(originalFinished) require.NoError(t, err) require.Len(t, originalWorkflows, 1, "original should have exactly one workflow") - originalWorkflowID := originalWorkflows[0].ID // Restart it. restarted, err := pipeline.Restart(t.Context(), env.Store, originalFinished, env.Fixtures.Owner, env.Fixtures.Repo, nil) @@ -74,7 +73,7 @@ func TestRestartPipeline(t *testing.T) { restartedWorkflows, err := env.Store.WorkflowGetTree(restartedFinished) require.NoError(t, err) require.Len(t, restartedWorkflows, 1, "restart should produce its own workflow") - assert.NotEqual(t, originalWorkflowID, restartedWorkflows[0].ID, + assert.NotEqual(t, originalWorkflows[0].ID, restartedWorkflows[0].ID, "restart should insert a new workflow row, not reassign the original") assert.Equal(t, restartedFinished.ID, restartedWorkflows[0].PipelineID, "restarted workflow must be linked to the restarted pipeline") @@ -85,7 +84,7 @@ func TestRestartPipeline(t *testing.T) { originalAfter, err := env.Store.WorkflowGetTree(originalFinished) require.NoError(t, err) require.Len(t, originalAfter, 1) - assert.Equal(t, originalWorkflowID, originalAfter[0].ID, + assert.Equal(t, originalWorkflows[0].ID, originalAfter[0].ID, "restart must not mutate the original's workflow row") assert.Equal(t, originalFinished.ID, originalAfter[0].PipelineID, "original's workflow must still be linked to the original pipeline")