@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.
@@ -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.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
- "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": "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
- 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
 
@@ -224,13 +226,20 @@ export async function postbuild() {
224
226
  region,
225
227
  });
226
228
 
227
- const deleted = await client.deleteNotesByMarker(
228
- projectId,
229
- mr.iid,
229
+ const markers = [
230
230
  '<!-- amplify:build-status -->',
231
- );
232
- if (deleted) {
233
- debug(`[postbuild] removed ${deleted} stale preview comment(s)`);
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
  });