Skip to content

Commit eb6cd3d

Browse files
chmouelclaude
andcommitted
test: use dynamic projects and smee for GitLab E2E
GitLab E2E tests previously relied on a static pre-created project (TEST_GITLAB_PROJECT_ID) and direct webhook delivery to the controller URL. This broke in Kind-based CI environments where the controller runs inside the cluster and is not reachable from the external GitLab instance (gitlab.pipelinesascode.com). This was causing issue when multiple e2e runs were executed in parallel, as they would all share the same project and webhook, leading to conflicts and unreliable test results when testing comments, commit statuses, and merge request events. Refactor GitLab E2E tests to dynamically create and delete projects per test run, and route webhooks through gosmee/smee — the same pattern already used by Gitea tests. Each test now creates a fresh GitLab project with a webhook pointing to a smee channel, and gosmee forwards payloads to the in-cluster controller. Key changes: - CreateGitLabProject() now accepts hookURL and webhookSecret as parameters instead of reading TEST_EL_URL from the environment, mirroring CreateGiteaRepo() in test/pkg/gitea/scm.go. - TestMR() reads TEST_GITLAB_SMEEURL and passes it as the webhook target when creating projects. TearDown() deletes the project on cleanup. - CI workflow generates a unique smee channel for the gitlab_bitbucket matrix job and runs a gosmee client to forward webhooks, matching the existing Gitea gosmee setup. - Add SmeeURL field to GitLabConfig in the YAML-based test configuration, with corresponding test coverage. - Add hack/cleanup-gitlab-projects.py to garbage-collect stale test projects older than 7 days (dry-run by default, --force to delete). - Fix title assertions in TestGitlabIssueGitopsComment, TestGitlabConsistentCommitStatusOnMR, and TestGitlabMergeRequestCelPrefix. These tests expected custom titles but the repository status always reflects the commit message set by TestMR ("Committing files from test on ..."). The mismatch was latent in the refactoring and surfaced when running the full suite. - Document manual GitLab test setup in test/README.md including smee channel generation, required env vars, and project cleanup. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Chmouel Boudjnah <chmouel@redhat.com>
1 parent f055cdf commit eb6cd3d

17 files changed

+831
-960
lines changed

.github/workflows/e2e.yaml

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ jobs:
8888
TEST_GITHUB_SECOND_REPO_OWNER_GITHUBAPP: pipelines-as-code/e2e
8989
TEST_GITHUB_SECOND_TOKEN: ${{ secrets.TEST_GITHUB_SECOND_TOKEN }}
9090
TEST_GITHUB_TOKEN: ${{ secrets.GH_APPS_TOKEN }}
91-
TEST_GITLAB_PROJECT_ID: ${{ vars.TEST_GITLAB_PROJECT_ID }}
9291
TEST_GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN }}
9392
steps:
9493
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
@@ -320,6 +319,20 @@ jobs:
320319
run: |
321320
nohup gosmee client --saveDir /tmp/gosmee-replay "${TEST_GITEA_SMEEURL}" "https://${CONTROLLER_DOMAIN_URL}" >> /tmp/gosmee-main.log 2>&1 &
322321
322+
- name: Generate unique gosmee URL for GitLab tests
323+
if: matrix.provider == 'gitlab_bitbucket'
324+
id: gosmee-gitlab-url
325+
run: |
326+
SMEE_URL=$(curl -s https://hook.pipelinesascode.com -o /dev/null -w '%{redirect_url}')
327+
echo "Generated unique GitLab smee URL: ${SMEE_URL}"
328+
echo "url=${SMEE_URL}" >> "$GITHUB_OUTPUT"
329+
echo "TEST_GITLAB_SMEEURL=${SMEE_URL}" >> "$GITHUB_ENV"
330+
331+
- name: Run gosmee for GitLab tests
332+
if: matrix.provider == 'gitlab_bitbucket'
333+
run: |
334+
nohup gosmee client --saveDir /tmp/gosmee-replay-gitlab "${TEST_GITLAB_SMEEURL}" "https://${CONTROLLER_DOMAIN_URL}" >> /tmp/gosmee-gitlab.log 2>&1 &
335+
323336
- name: Run gosmee for second controller (GHE)
324337
if: startsWith(matrix.provider, 'github_ghe') || matrix.provider == 'concurrency'
325338
run: |
@@ -366,7 +379,8 @@ jobs:
366379
TEST_GITHUB_SECOND_WEBHOOK_SECRET: ${{ secrets.TEST_GITHUB_SECOND_WEBHOOK_SECRET }}
367380
TEST_GITHUB_TOKEN: ${{ secrets.GH_APPS_TOKEN }}
368381
TEST_GITLAB_API_URL: https://gitlab.com
369-
TEST_GITLAB_PROJECT_ID: ${{ vars.TEST_GITLAB_PROJECT_ID }}
382+
TEST_GITLAB_GROUP: pac-e2e-tests
383+
TEST_GITLAB_SMEEURL: ${{ env.TEST_GITLAB_SMEEURL }}
370384
TEST_GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN }}
371385
TEST_PROVIDER: ${{ matrix.provider }}
372386
if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled }}
@@ -466,6 +480,7 @@ jobs:
466480
if: ${{ always() }}
467481
env:
468482
TEST_GITHUB_SECOND_SMEE_URL: ${{ secrets.TEST_GITHUB_SECOND_SMEE_URL }}
483+
TEST_GITLAB_SMEEURL: ${{ steps.gosmee-gitlab-url.outputs.url }}
469484
run: |
470485
./hack/gh-workflow-ci.sh collect_logs
471486

hack/cleanup-gitlab-projects.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
#!/usr/bin/env -S uv --quiet run --script
2+
# /// script
3+
# requires-python = ">=3.12"
4+
# dependencies = [
5+
# "requests",
6+
# ]
7+
# ///
8+
"""Clean up GitLab projects older than a given number of days in a group.
9+
10+
Uses TEST_GITLAB_API_URL, TEST_GITLAB_TOKEN, and TEST_GITLAB_GROUP environment
11+
variables by default (same as the E2E test suite), but all values can be
12+
overridden via CLI flags.
13+
14+
Examples:
15+
# Dry-run (default) — show what would be deleted:
16+
./hack/cleanup-gitlab-projects.py
17+
18+
# Actually delete:
19+
./hack/cleanup-gitlab-projects.py --force
20+
21+
# Custom age threshold:
22+
./hack/cleanup-gitlab-projects.py --days 3 --force
23+
"""
24+
25+
import argparse
26+
import os
27+
import sys
28+
from datetime import datetime, timezone
29+
30+
import requests
31+
32+
33+
def get_projects(base_url: str, token: str, group: str) -> list[dict]:
34+
"""Return all projects in the given group, handling pagination."""
35+
headers = {"PRIVATE-TOKEN": token}
36+
url = f"{base_url}/api/v4/groups/{requests.utils.quote(group, safe='')}/projects"
37+
params: dict = {"per_page": 100, "page": 1, "include_subgroups": False}
38+
projects: list[dict] = []
39+
while True:
40+
resp = requests.get(url, headers=headers, params=params, timeout=30)
41+
resp.raise_for_status()
42+
batch = resp.json()
43+
if not batch:
44+
break
45+
projects.extend(batch)
46+
params["page"] += 1
47+
return projects
48+
49+
50+
def delete_project(base_url: str, token: str, project_id: int) -> None:
51+
headers = {"PRIVATE-TOKEN": token}
52+
url = f"{base_url}/api/v4/projects/{project_id}"
53+
resp = requests.delete(url, headers=headers, timeout=30)
54+
resp.raise_for_status()
55+
56+
57+
def main() -> None:
58+
parser = argparse.ArgumentParser(
59+
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
60+
)
61+
parser.add_argument(
62+
"--api-url",
63+
default=os.getenv("TEST_GITLAB_API_URL", "https://gitlab.pipelinesascode.com"),
64+
help="GitLab API base URL (default: $TEST_GITLAB_API_URL)",
65+
)
66+
parser.add_argument(
67+
"--token",
68+
default=os.getenv("TEST_GITLAB_TOKEN", ""),
69+
help="GitLab private token (default: $TEST_GITLAB_TOKEN)",
70+
)
71+
parser.add_argument(
72+
"--group",
73+
default=os.getenv("TEST_GITLAB_GROUP", "pac-e2e-tests"),
74+
help="GitLab group path (default: $TEST_GITLAB_GROUP)",
75+
)
76+
parser.add_argument(
77+
"--days",
78+
type=int,
79+
default=7,
80+
help="Delete projects older than this many days (default: 7)",
81+
)
82+
parser.add_argument(
83+
"--force",
84+
action="store_true",
85+
help="Actually delete projects (default is dry-run)",
86+
)
87+
args = parser.parse_args()
88+
89+
if not args.token:
90+
print(
91+
"ERROR: GitLab token is required. Set TEST_GITLAB_TOKEN or pass --token.",
92+
file=sys.stderr,
93+
)
94+
sys.exit(1)
95+
96+
base_url = args.api_url.rstrip("/")
97+
now = datetime.now(tz=timezone.utc)
98+
99+
print(f"Listing projects in group '{args.group}' on {base_url} ...")
100+
projects = get_projects(base_url, args.token, args.group)
101+
print(f"Found {len(projects)} project(s).")
102+
103+
to_delete = []
104+
for proj in projects:
105+
created = datetime.fromisoformat(proj["created_at"])
106+
age = now - created
107+
if age.days >= args.days:
108+
to_delete.append((proj, age))
109+
110+
if not to_delete:
111+
print(f"No projects older than {args.days} day(s). Nothing to do.")
112+
return
113+
114+
print(f"\n{len(to_delete)} project(s) older than {args.days} day(s):\n")
115+
for proj, age in to_delete:
116+
print(
117+
f" {proj['path_with_namespace']} (ID {proj['id']}, created {proj['created_at']}, {age.days}d old)"
118+
)
119+
120+
if not args.force:
121+
print("\nDry-run mode. Pass --force to delete these projects.")
122+
return
123+
124+
print()
125+
errors = 0
126+
for proj, age in to_delete:
127+
name = proj["path_with_namespace"]
128+
try:
129+
delete_project(base_url, args.token, proj["id"])
130+
print(f" Deleted {name} (ID {proj['id']})")
131+
except requests.HTTPError as exc:
132+
print(f" ERROR deleting {name}: {exc}", file=sys.stderr)
133+
errors += 1
134+
135+
deleted = len(to_delete) - errors
136+
print(f"\nDone. Deleted {deleted} project(s).", end="")
137+
if errors:
138+
print(f" {errors} error(s).", end="")
139+
print()
140+
141+
142+
if __name__ == "__main__":
143+
main()

hack/gh-workflow-ci.sh

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ set -exufo pipefail
55

66
export PAC_API_INSTRUMENTATION_DIR=/tmp/api-instrumentation
77
export TEST_GITLAB_API_URL=https://gitlab.pipelinesascode.com
8+
export TEST_GITLAB_GROUP=pac-e2e-tests
89

910
create_pac_github_app_secret() {
1011
# Read from environment variables instead of arguments
@@ -231,6 +232,7 @@ collect_logs() {
231232
# Read from environment variables (use default empty value for optional vars)
232233
local test_gitea_smee_url="${TEST_GITEA_SMEEURL:-}"
233234
local github_ghe_smee_url="${TEST_GITHUB_SECOND_SMEE_URL:-}"
235+
local test_gitlab_smee_url="${TEST_GITLAB_SMEEURL:-}"
234236

235237
mkdir -p /tmp/logs
236238
# Output logs to stdout so we can see via the web interface directly
@@ -244,6 +246,8 @@ collect_logs() {
244246
[[ -d /tmp/gosmee-replay-ghe ]] && cp -a /tmp/gosmee-replay-ghe /tmp/logs/gosmee/replay-ghe
245247
[[ -f /tmp/gosmee-main.log ]] && cp /tmp/gosmee-main.log /tmp/logs/gosmee/main.log
246248
[[ -f /tmp/gosmee-ghe.log ]] && cp /tmp/gosmee-ghe.log /tmp/logs/gosmee/ghe.log
249+
[[ -d /tmp/gosmee-replay-gitlab ]] && cp -a /tmp/gosmee-replay-gitlab /tmp/logs/gosmee/replay-gitlab
250+
[[ -f /tmp/gosmee-gitlab.log ]] && cp /tmp/gosmee-gitlab.log /tmp/logs/gosmee/gitlab.log
247251

248252
kubectl get pipelineruns -A -o yaml >/tmp/logs/pac-pipelineruns.yaml
249253
kubectl get repositories.pipelinesascode.tekton.dev -A -o yaml >/tmp/logs/pac-repositories.yaml
@@ -264,7 +268,7 @@ collect_logs() {
264268
cp -a ${PAC_API_INSTRUMENTATION_DIR} /tmp/logs/$(basename ${PAC_API_INSTRUMENTATION_DIR})
265269
fi
266270

267-
for url in "${test_gitea_smee_url}" "${github_ghe_smee_url}"; do
271+
for url in "${test_gitea_smee_url}" "${github_ghe_smee_url}" "${test_gitlab_smee_url}"; do
268272
[[ -z "${url}" ]] && continue
269273
find /tmp/logs -type f -exec grep -l "${url}" {} \; | xargs -r sed -i "s|${url}|SMEE_URL|g"
270274
done

test/README.md

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,9 @@ this repo should differ from the one which is configured as part of `TEST_GITHUB
4545
- `TEST_BITBUCKET_CLOUD_E2E_REPOSITORY` - Bitbucket Cloud repository (i.e. `project/repo`)
4646
- `TEST_BITBUCKET_CLOUD_TOKEN` - Bitbucket Cloud token
4747
- `TEST_GITLAB_API_URL` - Gitlab API URL i.e: `https://gitlab.com`
48-
- `TEST_GITLAB_PROJECT_ID` - Gitlab project ID (you can get it in the repo details/settings)
48+
- `TEST_GITLAB_GROUP` - Gitlab group/namespace where test projects will be created and deleted
4949
- `TEST_GITLAB_TOKEN` - Gitlab Token
50+
- `TEST_GITLAB_SMEEURL` - Smee URL for forwarding GitLab webhooks to the controller
5051
- `TEST_GITEA_API_URL` - URL where GITEA is running (i.e: [GITEA_HOST](http://localhost:3000))
5152
- `TEST_GITEA_SMEEURL` - URL of smee
5253
- `TEST_GITEA_PASSWORD` - set password as **pac**
@@ -103,6 +104,52 @@ You can specify only a subsets of test to run with :
103104

104105
same goes for `TestGitlab` or other methods.
105106

107+
### Running GitLab tests manually
108+
109+
GitLab tests require a smee URL to forward webhooks from the external GitLab
110+
instance to your local controller (the same pattern as Gitea tests).
111+
112+
1. Create your own group on the GitLab instance
113+
(e.g. `https://gitlab.pipelinesascode.com`) to hold the temporary test
114+
projects. Each test run creates a project inside this group and deletes it
115+
on cleanup. Use `TEST_GITLAB_GROUP` to point to your group.
116+
117+
2. Generate a smee channel and start the gosmee client to forward webhooks to
118+
your controller:
119+
120+
```shell
121+
# Generate a new smee channel URL
122+
SMEE_URL=$(curl -s https://hook.pipelinesascode.com -o /dev/null -w '%{redirect_url}')
123+
124+
# Start forwarding webhooks to your controller
125+
gosmee client "${SMEE_URL}" "https://your-controller-url"
126+
```
127+
128+
3. Set the required environment variables (or use a
129+
[YAML config file](#yaml-configuration-file)):
130+
131+
```shell
132+
export TEST_GITLAB_API_URL=https://gitlab.pipelinesascode.com
133+
export TEST_GITLAB_TOKEN=<your-token>
134+
export TEST_GITLAB_GROUP=<your-group>
135+
export TEST_GITLAB_SMEEURL="${SMEE_URL}"
136+
export TEST_EL_URL=https://your-controller-url
137+
export TEST_EL_WEBHOOK_SECRET=<your-webhook-secret>
138+
```
139+
140+
4. Run the tests:
141+
142+
```shell
143+
cd test/; go test -tags=e2e -v -run TestGitlab .
144+
```
145+
146+
To clean up stale test projects (older than 7 days) left from previous runs:
147+
148+
```shell
149+
./hack/cleanup-gitlab-projects.py # dry-run
150+
./hack/cleanup-gitlab-projects.py --force # actually delete
151+
```
152+
106153
If you need to update the golden files in the end-to-end test, add the `-update` flag to the [go test](https://pkg.go.dev/cmd/go#hdr-Test_packages) command to refresh those files. First, run it if you expect the test output to change (or for a new test), then run it again without the flag to ensure everything is correct.
107154

108155
## Running nightly tests

test/e2e-config.yaml.example

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,9 @@ github_enterprise:
4444
gitlab:
4545
api_url: "https://gitlab.com"
4646
token: ""
47-
# Project ID (find in repo Settings > General)
48-
project_id: ""
47+
# Group/namespace where test projects will be created and deleted
48+
group: "pac-e2e-tests"
49+
smee_url: "" # Auto-generated in CI; set manually for local testing
4950

5051
# Gitea / Forgejo
5152
gitea:

test/gitlab_delete_tag_event_test.go

Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,52 +3,35 @@
33
package test
44

55
import (
6-
"context"
7-
"net/http"
86
"regexp"
97
"testing"
108

11-
"github.com/openshift-pipelines/pipelines-as-code/test/pkg/cctx"
129
tgitlab "github.com/openshift-pipelines/pipelines-as-code/test/pkg/gitlab"
1310
twait "github.com/openshift-pipelines/pipelines-as-code/test/pkg/wait"
1411
"github.com/tektoncd/pipeline/pkg/names"
1512
"gotest.tools/v3/assert"
1613
)
1714

1815
func TestGitlabDeleteTagEvent(t *testing.T) {
19-
targetNS := names.SimpleNameGenerator.RestrictLengthWithRandomSuffix("pac-e2e-ns")
20-
ctx := context.Background()
21-
runcnx, opts, glprovider, err := tgitlab.Setup(ctx)
22-
assert.NilError(t, err)
23-
ctx, err = cctx.GetControllerCtxInfo(ctx, runcnx)
24-
assert.NilError(t, err)
25-
runcnx.Clients.Log.Info("Testing with Gitlab")
26-
27-
projectinfo, resp, err := glprovider.Client().Projects.GetProject(opts.ProjectID, nil)
28-
assert.NilError(t, err)
29-
if resp != nil && resp.StatusCode == http.StatusNotFound {
30-
t.Errorf("Repository %s not found in %s", opts.Organization, opts.Repo)
16+
topts := &tgitlab.TestOpts{
17+
NoMRCreation: true,
3118
}
32-
defer tgitlab.TearDown(ctx, t, runcnx, glprovider, -1, "", targetNS, opts.ProjectID)
33-
34-
err = tgitlab.CreateCRD(ctx, projectinfo, runcnx, opts, targetNS, nil)
35-
assert.NilError(t, err)
19+
ctx, cleanup := tgitlab.TestMR(t, topts)
20+
defer cleanup()
3621

3722
tagName := names.SimpleNameGenerator.RestrictLengthWithRandomSuffix("v1.0")
38-
err = tgitlab.CreateTag(glprovider.Client(), int(projectinfo.ID), tagName)
39-
// if something goes wrong in creating tag and tag remains in
40-
// repository CleanTag will clear that and doesn't throw any error.
41-
defer tgitlab.CleanTag(glprovider.Client(), int(projectinfo.ID), tagName)
23+
err := tgitlab.CreateTag(topts.GLProvider.Client(), topts.ProjectID, tagName)
24+
defer tgitlab.CleanTag(topts.GLProvider.Client(), topts.ProjectID, tagName)
4225
assert.NilError(t, err)
43-
runcnx.Clients.Log.Infof("Created Tag %s in %s repository", tagName, projectinfo.Name)
26+
topts.ParamsRun.Clients.Log.Infof("Created Tag %s in project %d", tagName, topts.ProjectID)
4427

45-
err = tgitlab.DeleteTag(glprovider.Client(), int(projectinfo.ID), tagName)
28+
err = tgitlab.DeleteTag(topts.GLProvider.Client(), topts.ProjectID, tagName)
4629
assert.NilError(t, err)
47-
runcnx.Clients.Log.Infof("Deleted Tag %s in %s repository", tagName, projectinfo.Name)
30+
topts.ParamsRun.Clients.Log.Infof("Deleted Tag %s in project %d", tagName, topts.ProjectID)
4831

4932
logLinesToCheck := int64(1000)
5033
reg := regexp.MustCompile("event Delete Tag Push Hook is not supported.*")
51-
err = twait.RegexpMatchingInControllerLog(ctx, runcnx, *reg, 10, "controller", &logLinesToCheck, nil)
34+
err = twait.RegexpMatchingInControllerLog(ctx, topts.ParamsRun, *reg, 10, "controller", &logLinesToCheck, nil)
5235
assert.NilError(t, err)
5336
}
5437

0 commit comments

Comments
 (0)