@hellopearl/dv-gitlab 0.2.4 → 0.4.0
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 +102 -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 +8 -2
- package/src/templates/pipeline-triggered.md +14 -10
- package/src/templates/preview-failed.md +11 -7
- package/src/templates/preview-ready.md +14 -15
|
@@ -0,0 +1,102 @@
|
|
|
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
|
+
BRANCH_SLUG=$(echo "$CI_COMMIT_REF_NAME" | tr '[:upper:]' '[:lower:]' | sed 's|/|-|g')
|
|
40
|
+
PREVIEW_URL="https://${BRANCH_SLUG}.${PREVIEW_DOMAIN}"
|
|
41
|
+
echo "[e2e-enforce] Preview URL: $PREVIEW_URL"
|
|
42
|
+
echo "[e2e-enforce] Project: $E2E_PROJECT | Scope: $E2E_SCOPE | Env: $E2E_ENV"
|
|
43
|
+
|
|
44
|
+
# ── Wait for Amplify preview ──
|
|
45
|
+
echo "[e2e-enforce] Waiting for preview to go live..."
|
|
46
|
+
for i in $(seq 1 "$PREVIEW_WAIT_ATTEMPTS"); do
|
|
47
|
+
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$PREVIEW_URL" 2>/dev/null || echo "000")
|
|
48
|
+
[ "$STATUS" = "200" ] && break
|
|
49
|
+
echo "[e2e-enforce] Attempt $i/${PREVIEW_WAIT_ATTEMPTS} — HTTP $STATUS"
|
|
50
|
+
sleep "$PREVIEW_WAIT_INTERVAL"
|
|
51
|
+
done
|
|
52
|
+
if [ "$STATUS" != "200" ]; then
|
|
53
|
+
echo "[e2e-enforce] Preview not available — skipping (non-blocking)"
|
|
54
|
+
exit 0
|
|
55
|
+
fi
|
|
56
|
+
echo "[e2e-enforce] Preview is live."
|
|
57
|
+
|
|
58
|
+
# ── Trigger pearl-test-automation ──
|
|
59
|
+
RESPONSE=$(curl -s --header "PRIVATE-TOKEN: ${GITLAB_API_TOKEN}" \
|
|
60
|
+
--header "Content-Type: application/json" \
|
|
61
|
+
--request POST \
|
|
62
|
+
"https://gitlab.com/api/v4/projects/${QA_PROJECT_ID}/pipeline" \
|
|
63
|
+
--data "{
|
|
64
|
+
\"ref\": \"main\",
|
|
65
|
+
\"variables\": [
|
|
66
|
+
{\"key\": \"PROJECT\", \"value\": \"$E2E_PROJECT\"},
|
|
67
|
+
{\"key\": \"ENV\", \"value\": \"$E2E_ENV\"},
|
|
68
|
+
{\"key\": \"SCOPE\", \"value\": \"$E2E_SCOPE\"},
|
|
69
|
+
{\"key\": \"PREVIEW_URL\", \"value\": \"$PREVIEW_URL\"},
|
|
70
|
+
{\"key\": \"SOURCE_PROJECT_ID\", \"value\": \"$CI_PROJECT_ID\"},
|
|
71
|
+
{\"key\": \"SOURCE_MR_IID\", \"value\": \"$CI_MERGE_REQUEST_IID\"}
|
|
72
|
+
]
|
|
73
|
+
}")
|
|
74
|
+
PIPELINE_ID=$(echo "$RESPONSE" | jq -r '.id // empty')
|
|
75
|
+
PIPELINE_URL=$(echo "$RESPONSE" | jq -r '.web_url // empty')
|
|
76
|
+
|
|
77
|
+
if [ -z "$PIPELINE_ID" ]; then
|
|
78
|
+
echo "[e2e-enforce] Failed to trigger: $RESPONSE"
|
|
79
|
+
exit 1
|
|
80
|
+
fi
|
|
81
|
+
echo "[e2e-enforce] Triggered: $PIPELINE_URL"
|
|
82
|
+
|
|
83
|
+
# ── Poll for result ──
|
|
84
|
+
for i in $(seq 1 "$PIPELINE_POLL_ATTEMPTS"); do
|
|
85
|
+
sleep "$PIPELINE_POLL_INTERVAL"
|
|
86
|
+
PSTATUS=$(curl -s --header "PRIVATE-TOKEN: ${GITLAB_API_TOKEN}" \
|
|
87
|
+
"https://gitlab.com/api/v4/projects/${QA_PROJECT_ID}/pipelines/$PIPELINE_ID" \
|
|
88
|
+
| jq -r '.status')
|
|
89
|
+
case "$PSTATUS" in
|
|
90
|
+
success)
|
|
91
|
+
echo "[e2e-enforce] ✅ E2E PASSED — $PIPELINE_URL"
|
|
92
|
+
exit 0 ;;
|
|
93
|
+
failed|canceled)
|
|
94
|
+
echo "[e2e-enforce] ❌ E2E $PSTATUS — $PIPELINE_URL"
|
|
95
|
+
exit 1 ;;
|
|
96
|
+
esac
|
|
97
|
+
[ $((i % 6)) -eq 0 ] && echo "[e2e-enforce] Still running ($((i * PIPELINE_POLL_INTERVAL))s)..."
|
|
98
|
+
done
|
|
99
|
+
echo "[e2e-enforce] ❌ Timed out — $PIPELINE_URL"
|
|
100
|
+
exit 1
|
|
101
|
+
rules:
|
|
102
|
+
- 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.0",
|
|
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": "b64d016cde27cd43ab1abfec4da8f5b8787355bc"
|
|
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
|
|
|
@@ -227,7 +229,7 @@ export async function postbuild() {
|
|
|
227
229
|
const deleted = await client.deleteNotesByMarker(
|
|
228
230
|
projectId,
|
|
229
231
|
mr.iid,
|
|
230
|
-
'<!--
|
|
232
|
+
'<!-- amplify:build-status -->',
|
|
231
233
|
);
|
|
232
234
|
if (deleted) {
|
|
233
235
|
debug(`[postbuild] removed ${deleted} stale preview comment(s)`);
|
|
@@ -249,24 +251,28 @@ export async function postbuild() {
|
|
|
249
251
|
await client.deleteNotesByMarker(
|
|
250
252
|
projectId,
|
|
251
253
|
mr.iid,
|
|
252
|
-
'<!--
|
|
254
|
+
'<!-- amplify:pipeline-trigger -->',
|
|
253
255
|
);
|
|
254
256
|
|
|
255
257
|
const triggerResult = await triggerPipelinePreview({
|
|
256
258
|
appId,
|
|
257
259
|
branch,
|
|
258
260
|
mrIid: mr.iid.toString(),
|
|
261
|
+
previewUrl,
|
|
259
262
|
projectId: projectId.toString(),
|
|
260
263
|
token,
|
|
261
264
|
});
|
|
262
265
|
|
|
263
266
|
if (triggerResult) {
|
|
264
267
|
const triggerBody = renderTemplate('pipeline-triggered', {
|
|
268
|
+
branch,
|
|
269
|
+
capture: process.env.QA_PIPELINE_CAPTURE || '',
|
|
265
270
|
environment: process.env.QA_PIPELINE_ENVIRONMENT || '',
|
|
266
271
|
pipelineId: triggerResult.id.toString(),
|
|
267
272
|
pipelineUrl: triggerResult.webUrl,
|
|
268
273
|
project: process.env.QA_PIPELINE_PROJECT || '',
|
|
269
274
|
ref: process.env.QA_PIPELINE_REF || '',
|
|
275
|
+
scope: process.env.QA_PIPELINE_SCOPE || '',
|
|
270
276
|
});
|
|
271
277
|
|
|
272
278
|
const triggerPosted = await client.postNote(projectId, mr.iid, triggerBody);
|
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
##
|
|
1
|
+
<!-- amplify:pipeline-trigger -->
|
|
2
|
+
## :gear: Automation Pipeline Triggered
|
|
3
3
|
|
|
4
|
-
|
|
|
5
|
-
|
|
6
|
-
| **Pipeline**
|
|
7
|
-
| **
|
|
8
|
-
| **Ref**
|
|
9
|
-
| **
|
|
4
|
+
| | |
|
|
5
|
+
|---|---|
|
|
6
|
+
| **Pipeline** | [#{{pipelineId}}]({{pipelineUrl}}) |
|
|
7
|
+
| **Branch** | `{{branch}}` |
|
|
8
|
+
| **Ref** | `{{ref}}` |
|
|
9
|
+
| **Project** | `{{project}}` |
|
|
10
|
+
| **Environment** | `{{environment}}` |
|
|
11
|
+
| **Scope** | `{{scope}}` |
|
|
12
|
+
| **Capture** | `{{capture}}` |
|
|
10
13
|
|
|
11
|
-
|
|
14
|
+
:hourglass_flowing_sand: Results will be posted here when the pipeline completes.
|
|
12
15
|
|
|
13
|
-
|
|
16
|
+
---
|
|
17
|
+
:robot: *Posted by Amplify*
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
##
|
|
1
|
+
<!-- amplify:build-status -->
|
|
2
|
+
## :x: Preview Build Failed
|
|
3
3
|
|
|
4
|
-
|
|
|
5
|
-
|
|
6
|
-
| **Branch** | `{{branch}}`
|
|
4
|
+
| | |
|
|
5
|
+
|---|---|
|
|
6
|
+
| **Branch** | `{{branch}}` |
|
|
7
7
|
| **Commit** | `{{commitId}}` |
|
|
8
|
+
| **Console** | [Amplify Build Logs](https://{{region}}.console.aws.amazon.com/amplify/home#/{{appId}}/{{branch}}/{{jobId}}) |
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
```diff
|
|
11
|
+
- Build failed -- check console for details
|
|
12
|
+
```
|
|
10
13
|
|
|
11
|
-
|
|
14
|
+
---
|
|
15
|
+
:robot: *Posted by Amplify*
|
|
@@ -1,19 +1,18 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
##
|
|
1
|
+
<!-- amplify:build-status -->
|
|
2
|
+
## :white_check_mark: Preview Environment Ready
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
| --------------- | -------------------------------- |
|
|
6
|
-
| **Branch** | `{{branch}}` |
|
|
7
|
-
| **Commit** | `{{commitId}}` |
|
|
8
|
-
| **Preview URL** | [{{previewUrl}}]({{previewUrl}}) |
|
|
4
|
+
:rocket: **[Open Preview]({{previewUrl}})**
|
|
9
5
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
| | |
|
|
7
|
+
|---|---|
|
|
8
|
+
| **Branch** | `{{branch}}` |
|
|
9
|
+
| **Commit** | `{{commitId}}` |
|
|
10
|
+
| **Preview** | {{previewUrl}} |
|
|
11
|
+
| **Console** | [Amplify Build Logs](https://{{region}}.console.aws.amazon.com/amplify/home#/{{appId}}/{{branch}}/{{jobId}}) |
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
```diff
|
|
14
|
+
+ Build succeeded -- preview is live
|
|
15
|
+
```
|
|
18
16
|
|
|
19
|
-
|
|
17
|
+
---
|
|
18
|
+
:robot: *Posted by Amplify*
|