@hellopearl/dv-gitlab 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ci/e2e-enforce.yml +107 -0
- package/ci/quality-gate.yml +59 -0
- package/package.json +6 -7
- package/src/commands/cleanup-branch.mjs +3 -2
- package/src/commands/postbuild.mjs +16 -12
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Reusable template: enforce green E2E on MRs.
|
|
2
|
+
# Waits for an Amplify preview to go live, triggers pearl-test-automation,
|
|
3
|
+
# then polls until the pipeline finishes — failing if tests are red.
|
|
4
|
+
#
|
|
5
|
+
# Consumer usage:
|
|
6
|
+
# include:
|
|
7
|
+
# - project: 'hellopearl/pearl-agentic-mono'
|
|
8
|
+
# ref: main
|
|
9
|
+
# file: '/packages/dev/dv-gitlab/ci/e2e-enforce.yml'
|
|
10
|
+
#
|
|
11
|
+
# e2e:
|
|
12
|
+
# extends: .e2e-enforce
|
|
13
|
+
# variables:
|
|
14
|
+
# E2E_PROJECT: Voice # pearl-test-automation project name
|
|
15
|
+
# PREVIEW_DOMAIN: voice.hellopearl.com
|
|
16
|
+
|
|
17
|
+
.e2e-enforce:
|
|
18
|
+
stage: e2e
|
|
19
|
+
image: node:24-alpine
|
|
20
|
+
before_script:
|
|
21
|
+
- apk add --no-cache curl jq
|
|
22
|
+
variables:
|
|
23
|
+
QA_PROJECT_ID: "59469690"
|
|
24
|
+
E2E_PROJECT: ""
|
|
25
|
+
E2E_SCOPE: "All"
|
|
26
|
+
E2E_ENV: "dev"
|
|
27
|
+
PREVIEW_DOMAIN: ""
|
|
28
|
+
PREVIEW_WAIT_ATTEMPTS: "60"
|
|
29
|
+
PREVIEW_WAIT_INTERVAL: "10"
|
|
30
|
+
PIPELINE_POLL_ATTEMPTS: "180"
|
|
31
|
+
PIPELINE_POLL_INTERVAL: "10"
|
|
32
|
+
script:
|
|
33
|
+
- |
|
|
34
|
+
if [ -z "$E2E_PROJECT" ] || [ -z "$PREVIEW_DOMAIN" ]; then
|
|
35
|
+
echo "[e2e-enforce] ERROR: E2E_PROJECT and PREVIEW_DOMAIN must be set"
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
if [ -z "$GITLAB_API_TOKEN" ]; then
|
|
40
|
+
echo "[e2e-enforce] GITLAB_API_TOKEN not set — skipping E2E (configure in CI/CD variables)"
|
|
41
|
+
exit 0
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
BRANCH_SLUG=$(echo "$CI_COMMIT_REF_NAME" | tr '[:upper:]' '[:lower:]' | sed 's|/|-|g')
|
|
45
|
+
PREVIEW_URL="https://${BRANCH_SLUG}.${PREVIEW_DOMAIN}"
|
|
46
|
+
echo "[e2e-enforce] Preview URL: $PREVIEW_URL"
|
|
47
|
+
echo "[e2e-enforce] Project: $E2E_PROJECT | Scope: $E2E_SCOPE | Env: $E2E_ENV"
|
|
48
|
+
|
|
49
|
+
# ── Wait for Amplify preview ──
|
|
50
|
+
echo "[e2e-enforce] Waiting for preview to go live..."
|
|
51
|
+
for i in $(seq 1 "$PREVIEW_WAIT_ATTEMPTS"); do
|
|
52
|
+
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$PREVIEW_URL" 2>/dev/null || echo "000")
|
|
53
|
+
[ "$STATUS" = "200" ] && break
|
|
54
|
+
echo "[e2e-enforce] Attempt $i/${PREVIEW_WAIT_ATTEMPTS} — HTTP $STATUS"
|
|
55
|
+
sleep "$PREVIEW_WAIT_INTERVAL"
|
|
56
|
+
done
|
|
57
|
+
if [ "$STATUS" != "200" ]; then
|
|
58
|
+
echo "[e2e-enforce] Preview not available — skipping (non-blocking)"
|
|
59
|
+
exit 0
|
|
60
|
+
fi
|
|
61
|
+
echo "[e2e-enforce] Preview is live."
|
|
62
|
+
|
|
63
|
+
# ── Trigger pearl-test-automation ──
|
|
64
|
+
RESPONSE=$(curl -s --header "PRIVATE-TOKEN: ${GITLAB_API_TOKEN}" \
|
|
65
|
+
--header "Content-Type: application/json" \
|
|
66
|
+
--request POST \
|
|
67
|
+
"https://gitlab.com/api/v4/projects/${QA_PROJECT_ID}/pipeline" \
|
|
68
|
+
--data "{
|
|
69
|
+
\"ref\": \"main\",
|
|
70
|
+
\"variables\": [
|
|
71
|
+
{\"key\": \"PROJECT\", \"value\": \"$E2E_PROJECT\"},
|
|
72
|
+
{\"key\": \"ENV\", \"value\": \"$E2E_ENV\"},
|
|
73
|
+
{\"key\": \"SCOPE\", \"value\": \"$E2E_SCOPE\"},
|
|
74
|
+
{\"key\": \"PREVIEW_URL\", \"value\": \"$PREVIEW_URL\"},
|
|
75
|
+
{\"key\": \"SOURCE_PROJECT_ID\", \"value\": \"$CI_PROJECT_ID\"},
|
|
76
|
+
{\"key\": \"SOURCE_MR_IID\", \"value\": \"$CI_MERGE_REQUEST_IID\"}
|
|
77
|
+
]
|
|
78
|
+
}")
|
|
79
|
+
PIPELINE_ID=$(echo "$RESPONSE" | jq -r '.id // empty')
|
|
80
|
+
PIPELINE_URL=$(echo "$RESPONSE" | jq -r '.web_url // empty')
|
|
81
|
+
|
|
82
|
+
if [ -z "$PIPELINE_ID" ]; then
|
|
83
|
+
echo "[e2e-enforce] Failed to trigger: $RESPONSE"
|
|
84
|
+
exit 1
|
|
85
|
+
fi
|
|
86
|
+
echo "[e2e-enforce] Triggered: $PIPELINE_URL"
|
|
87
|
+
|
|
88
|
+
# ── Poll for result ──
|
|
89
|
+
for i in $(seq 1 "$PIPELINE_POLL_ATTEMPTS"); do
|
|
90
|
+
sleep "$PIPELINE_POLL_INTERVAL"
|
|
91
|
+
PSTATUS=$(curl -s --header "PRIVATE-TOKEN: ${GITLAB_API_TOKEN}" \
|
|
92
|
+
"https://gitlab.com/api/v4/projects/${QA_PROJECT_ID}/pipelines/$PIPELINE_ID" \
|
|
93
|
+
| jq -r '.status')
|
|
94
|
+
case "$PSTATUS" in
|
|
95
|
+
success)
|
|
96
|
+
echo "[e2e-enforce] ✅ E2E PASSED — $PIPELINE_URL"
|
|
97
|
+
exit 0 ;;
|
|
98
|
+
failed|canceled)
|
|
99
|
+
echo "[e2e-enforce] ❌ E2E $PSTATUS — $PIPELINE_URL"
|
|
100
|
+
exit 1 ;;
|
|
101
|
+
esac
|
|
102
|
+
[ $((i % 6)) -eq 0 ] && echo "[e2e-enforce] Still running ($((i * PIPELINE_POLL_INTERVAL))s)..."
|
|
103
|
+
done
|
|
104
|
+
echo "[e2e-enforce] ❌ Timed out — $PIPELINE_URL"
|
|
105
|
+
exit 1
|
|
106
|
+
rules:
|
|
107
|
+
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Reusable template: code quality gate for MRs.
|
|
2
|
+
# Runs format check (Prettier), lint (ESLint), and optional style lint.
|
|
3
|
+
# Uses @hellopearl/dv-prettier and @hellopearl/dv-lint from the monorepo.
|
|
4
|
+
#
|
|
5
|
+
# Consumer usage:
|
|
6
|
+
# include:
|
|
7
|
+
# - project: 'hellopearl/pearl-agentic-mono'
|
|
8
|
+
# ref: main
|
|
9
|
+
# file: '/packages/dev/dv-gitlab/ci/quality-gate.yml'
|
|
10
|
+
#
|
|
11
|
+
# quality:
|
|
12
|
+
# extends: .quality-gate
|
|
13
|
+
# variables:
|
|
14
|
+
# QUALITY_LINT_PROFILE: client # server | client | e2e
|
|
15
|
+
|
|
16
|
+
.quality-gate:
|
|
17
|
+
stage: test
|
|
18
|
+
image: node:24-alpine
|
|
19
|
+
before_script:
|
|
20
|
+
- corepack enable || true
|
|
21
|
+
- yarn install --immutable 2>/dev/null || npm ci 2>/dev/null || true
|
|
22
|
+
variables:
|
|
23
|
+
QUALITY_FORMAT_CHECK: "true"
|
|
24
|
+
QUALITY_LINT: "true"
|
|
25
|
+
QUALITY_STYLELINT: "false"
|
|
26
|
+
QUALITY_LINT_PROFILE: "client"
|
|
27
|
+
QUALITY_FORMAT_GLOB: "src/**/*.{ts,tsx,js,jsx,mjs}"
|
|
28
|
+
QUALITY_STYLELINT_GLOB: "src/**/*.scss"
|
|
29
|
+
script:
|
|
30
|
+
- |
|
|
31
|
+
EXIT=0
|
|
32
|
+
|
|
33
|
+
if [ "$QUALITY_FORMAT_CHECK" = "true" ]; then
|
|
34
|
+
echo "══════════════════════════════════════════"
|
|
35
|
+
echo "[quality] Prettier (@hellopearl/dv-prettier)"
|
|
36
|
+
echo "══════════════════════════════════════════"
|
|
37
|
+
yarn format:check \
|
|
38
|
+
|| { echo "[quality] ❌ Format check failed — run: yarn format"; EXIT=1; }
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
if [ "$QUALITY_LINT" = "true" ]; then
|
|
42
|
+
echo "══════════════════════════════════════════"
|
|
43
|
+
echo "[quality] ESLint (@hellopearl/dv-lint)"
|
|
44
|
+
echo "══════════════════════════════════════════"
|
|
45
|
+
yarn lint \
|
|
46
|
+
|| { echo "[quality] ❌ Lint failed"; EXIT=1; }
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
if [ "$QUALITY_STYLELINT" = "true" ]; then
|
|
50
|
+
echo "══════════════════════════════════════════"
|
|
51
|
+
echo "[quality] Stylelint"
|
|
52
|
+
echo "══════════════════════════════════════════"
|
|
53
|
+
npx stylelint "$QUALITY_STYLELINT_GLOB" \
|
|
54
|
+
|| { echo "[quality] ❌ Stylelint failed"; EXIT=1; }
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
exit $EXIT
|
|
58
|
+
rules:
|
|
59
|
+
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hellopearl/dv-gitlab",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Unified GitLab CI tooling -- MR comments, pipeline triggers, preview env management",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -8,13 +8,11 @@
|
|
|
8
8
|
"exports": {
|
|
9
9
|
".": "./src/index.mjs"
|
|
10
10
|
},
|
|
11
|
-
"bin":
|
|
12
|
-
"dv-gitlab": "./bin/dv-gitlab.mjs"
|
|
13
|
-
},
|
|
11
|
+
"bin": "./bin/dv-gitlab.mjs",
|
|
14
12
|
"files": [
|
|
15
13
|
"bin",
|
|
16
|
-
"
|
|
17
|
-
"
|
|
14
|
+
"ci",
|
|
15
|
+
"src"
|
|
18
16
|
],
|
|
19
17
|
"scripts": {
|
|
20
18
|
"deps": "dv-deps-runner",
|
|
@@ -46,5 +44,6 @@
|
|
|
46
44
|
"@hellopearl/dv-lint": "*",
|
|
47
45
|
"@hellopearl/dv-prettier": "*",
|
|
48
46
|
"@hellopearl/dv-test": "*"
|
|
49
|
-
}
|
|
47
|
+
},
|
|
48
|
+
"gitHead": "5fe68171d5da113bf355f6ee8eda1ce4bbceb800"
|
|
50
49
|
}
|
|
@@ -26,10 +26,11 @@ export async function cleanupBranch(flags) {
|
|
|
26
26
|
|
|
27
27
|
debug(`cleanup-branch branch=${branch} appId=${appId} region=${region}`);
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
const amplifyBranch = branch.toLowerCase().replace(/\//g, '-');
|
|
30
|
+
log(`[cleanup] Deleting Amplify branch: ${amplifyBranch} (app=${appId})`);
|
|
30
31
|
|
|
31
32
|
const client = new AmplifyClient({ appId, region });
|
|
32
|
-
const ok = await client.deleteBranch(
|
|
33
|
+
const ok = await client.deleteBranch(amplifyBranch);
|
|
33
34
|
|
|
34
35
|
if (ok) {
|
|
35
36
|
log(`[cleanup] Branch ${amplifyBranch} removed successfully`);
|
|
@@ -90,6 +90,7 @@ async function triggerPipelinePreview({
|
|
|
90
90
|
mrIid,
|
|
91
91
|
branch,
|
|
92
92
|
appId,
|
|
93
|
+
previewUrl,
|
|
93
94
|
}) {
|
|
94
95
|
const pipelineUrl = process.env.QA_PIPELINE_URL;
|
|
95
96
|
const triggerToken = process.env.QA_PIPELINE_TRIGGER_TOKEN || token;
|
|
@@ -113,6 +114,7 @@ async function triggerPipelinePreview({
|
|
|
113
114
|
{ key: 'BRANCH', value: branch },
|
|
114
115
|
{ key: 'AMPLIFY_APP_ID', value: appId },
|
|
115
116
|
{ key: 'PREVIEW_DOMAIN', value: process.env.PREVIEW_DOMAIN || '' },
|
|
117
|
+
{ key: 'PREVIEW_URL', value: previewUrl || '' },
|
|
116
118
|
],
|
|
117
119
|
};
|
|
118
120
|
|
|
@@ -224,13 +226,20 @@ export async function postbuild() {
|
|
|
224
226
|
region,
|
|
225
227
|
});
|
|
226
228
|
|
|
227
|
-
const
|
|
228
|
-
projectId,
|
|
229
|
-
mr.iid,
|
|
229
|
+
const markers = [
|
|
230
230
|
'<!-- amplify:build-status -->',
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
231
|
+
'<!-- amplify:pipeline-trigger -->',
|
|
232
|
+
'<!-- qa:automation-results -->',
|
|
233
|
+
];
|
|
234
|
+
for (const marker of markers) {
|
|
235
|
+
const deleted = await client.deleteNotesByMarker(
|
|
236
|
+
projectId,
|
|
237
|
+
mr.iid,
|
|
238
|
+
marker,
|
|
239
|
+
);
|
|
240
|
+
if (deleted) {
|
|
241
|
+
debug(`[postbuild] removed ${deleted} stale comment(s) [${marker}]`);
|
|
242
|
+
}
|
|
234
243
|
}
|
|
235
244
|
|
|
236
245
|
const posted = await client.postNote(projectId, mr.iid, commentBody);
|
|
@@ -246,16 +255,11 @@ export async function postbuild() {
|
|
|
246
255
|
return;
|
|
247
256
|
}
|
|
248
257
|
|
|
249
|
-
await client.deleteNotesByMarker(
|
|
250
|
-
projectId,
|
|
251
|
-
mr.iid,
|
|
252
|
-
'<!-- amplify:pipeline-trigger -->',
|
|
253
|
-
);
|
|
254
|
-
|
|
255
258
|
const triggerResult = await triggerPipelinePreview({
|
|
256
259
|
appId,
|
|
257
260
|
branch,
|
|
258
261
|
mrIid: mr.iid.toString(),
|
|
262
|
+
previewUrl,
|
|
259
263
|
projectId: projectId.toString(),
|
|
260
264
|
token,
|
|
261
265
|
});
|