@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.
@@ -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.2.4",
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
- "src",
17
- "ci"
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
- log(`[cleanup] Deleting Amplify branch: ${branch} (app=${appId})`);
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(branch);
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
- '<!-- dv-gitlab:preview -->',
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
- '<!-- dv-gitlab:pipeline -->',
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
- <!-- dv-gitlab:pipeline -->
2
- ## 🔄 Automation Pipeline Triggered
1
+ <!-- amplify:pipeline-trigger -->
2
+ ## :gear: Automation Pipeline Triggered
3
3
 
4
- | | |
5
- | --------------- | ---------------------------------- |
6
- | **Pipeline** | [#{{pipelineId}}]({{pipelineUrl}}) |
7
- | **Project** | `{{project}}` |
8
- | **Ref** | `{{ref}}` |
9
- | **Environment** | `{{environment}}` |
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
- Results will be posted here once the pipeline completes.
16
+ ---
17
+ :robot: *Posted by Amplify*
@@ -1,11 +1,15 @@
1
- <!-- dv-gitlab:preview -->
2
- ## Preview Build Failed
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
- The Amplify preview build did not complete successfully. Check the [Amplify Console](https://{{region}}.console.aws.amazon.com/amplify/home#/{{appId}}/{{branch}}) for details.
14
+ ---
15
+ :robot: *Posted by Amplify*
@@ -1,19 +1,18 @@
1
- <!-- dv-gitlab:preview -->
2
- ## 🚀 Preview Environment Ready
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
- <details>
13
- <summary>Build details</summary>
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
- - **App ID:** `{{appId}}`
16
- - **Job ID:** `{{jobId}}`
17
- - **Region:** `{{region}}`
13
+ ```diff
14
+ + Build succeeded -- preview is live
15
+ ```
18
16
 
19
- </details>
17
+ ---
18
+ :robot: *Posted by Amplify*