@hellopearl/dv-gitlab 0.1.0 → 0.2.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,84 @@
1
+ # Reusable CI template: build a service's Docker image, run it, and execute
2
+ # pearl-test-automation API tests against it.
3
+ #
4
+ # Required from consumer:
5
+ # API_TEST_PROJECT - playwright project name (e.g. VoiceApi)
6
+ #
7
+ # Usage:
8
+ # include:
9
+ # - project: 'hellopearl/pearl-agentic-mono'
10
+ # file: '/packages/dev/dv-gitlab/ci/api-test.yml'
11
+ #
12
+ # api-tests:
13
+ # extends: .api-test
14
+ # variables:
15
+ # API_TEST_PROJECT: VoiceApi
16
+
17
+ .api-test:
18
+ stage: test
19
+ image: docker:24
20
+ services:
21
+ - name: docker:24-dind
22
+ alias: docker
23
+ before_script: []
24
+ variables:
25
+ DOCKER_TLS_CERTDIR: ''
26
+ API_TEST_SCOPE: 'heartbeat'
27
+ API_TEST_ENVIRONMENT: 'dev'
28
+ TEST_AUTOMATION_REF: 'main'
29
+ SERVICE_PORT: '4002'
30
+ SERVICE_HEALTHCHECK_PATH: '/status'
31
+ DOCKER_BUILD_ARGS: '--build-arg PEARL_NPM_TOKEN=${PEARL_NPM_TOKEN}'
32
+ script:
33
+ # ── Build ──
34
+ - echo "==> Building service image..."
35
+ - docker build ${DOCKER_BUILD_ARGS} -t sut .
36
+
37
+ # ── Run ──
38
+ - echo "==> Starting service on port ${SERVICE_PORT}..."
39
+ - env | grep -v "^CI_\|^DOCKER_\|^FF_\|^GITLAB_" > /tmp/sut.env
40
+ - docker run -d --name sut -p ${SERVICE_PORT}:${SERVICE_PORT} --env-file /tmp/sut.env sut
41
+ - echo " Container ID $(docker ps -q --filter name=sut)"
42
+
43
+ # ── Healthcheck ──
44
+ - echo "==> Waiting for healthcheck at docker:${SERVICE_PORT}${SERVICE_HEALTHCHECK_PATH}..."
45
+ - |
46
+ for i in $(seq 1 30); do
47
+ if wget -q --spider "http://docker:${SERVICE_PORT}${SERVICE_HEALTHCHECK_PATH}" 2>/dev/null; then
48
+ echo " Ready (${i}s)"
49
+ break
50
+ fi
51
+ if [ $i -eq 30 ]; then
52
+ echo "ERROR: Service failed to start after 30s"
53
+ echo "--- Container status ---"
54
+ docker ps -a --filter name=sut --format "table {{.Status}}\t{{.Ports}}"
55
+ echo "--- Last 50 lines of logs ---"
56
+ docker logs --tail 50 sut
57
+ exit 1
58
+ fi
59
+ sleep 1
60
+ done
61
+ - echo "==> Startup logs:"
62
+ - docker logs sut 2>&1 | tail -10
63
+
64
+ # ── Test ──
65
+ - echo "==> Running ${API_TEST_PROJECT} tests (@${API_TEST_SCOPE}, env ${API_TEST_ENVIRONMENT}, ref ${TEST_AUTOMATION_REF})"
66
+ - |
67
+ docker run --rm --network host -e ENV=${API_TEST_ENVIRONMENT} node:22 sh -c "
68
+ git clone --depth 1 --branch ${TEST_AUTOMATION_REF} \
69
+ https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/hellopearl/eng/pearl-test-automation.git /app
70
+ cd /app
71
+ npm ci --ignore-scripts
72
+ npx playwright test --project=${API_TEST_PROJECT} --grep='@${API_TEST_SCOPE}' --reporter=list
73
+ "
74
+ TEST_EXIT=$?
75
+ - echo "==> Test exit code ${TEST_EXIT}"
76
+ - echo "==> Final service logs:"
77
+ - docker logs --tail 20 sut 2>&1
78
+ - exit ${TEST_EXIT}
79
+
80
+ after_script:
81
+ - docker rm -f sut 2>/dev/null || true
82
+ - rm -f /tmp/sut.env
83
+ rules:
84
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
@@ -0,0 +1,16 @@
1
+ .cleanup-preview:
2
+ stage: .post
3
+ image: node:22-alpine
4
+ before_script: []
5
+ script:
6
+ - npx @hellopearl/dv-gitlab cleanup-branch
7
+ variables:
8
+ AWS_APP_ID: $AMPLIFY_APP_ID
9
+ AWS_DEFAULT_REGION: us-east-1
10
+ rules:
11
+ - if: $CI_COMMIT_REF_NAME =~ /^(development|develop|main|master|prod|stage|sandbox)$/
12
+ when: never
13
+ - if: $CI_MERGE_REQUEST_EVENT_TYPE == "merge_train"
14
+ when: on_success
15
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_STATE == "merged"
16
+ when: on_success
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hellopearl/dv-gitlab",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Unified GitLab CI tooling -- MR comments, pipeline triggers, preview env management",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -11,7 +11,8 @@
11
11
  "bin": "bin/dv-gitlab.mjs",
12
12
  "files": [
13
13
  "bin",
14
- "src"
14
+ "src",
15
+ "ci"
15
16
  ],
16
17
  "scripts": {
17
18
  "deps": "dv-deps-runner",
@@ -38,11 +39,13 @@
38
39
  "publishConfig": {
39
40
  "registry": "https://registry.npmjs.org/"
40
41
  },
42
+ "dependencies": {
43
+ "@aws-sdk/client-amplify": "^3.1078.0"
44
+ },
41
45
  "devDependencies": {
42
46
  "@hellopearl/dv-deps": "*",
43
47
  "@hellopearl/dv-lint": "*",
44
48
  "@hellopearl/dv-prettier": "*",
45
49
  "@hellopearl/dv-test": "*"
46
- },
47
- "gitHead": "2da7b9fc23900bbaf9c4fcf6042ecec76d538bc2"
50
+ }
48
51
  }
@@ -26,11 +26,10 @@ export async function cleanupBranch(flags) {
26
26
 
27
27
  debug(`cleanup-branch branch=${branch} appId=${appId} region=${region}`);
28
28
 
29
- const amplifyBranch = branch.toLowerCase().replace(/\//g, '-');
30
- log(`[cleanup] Deleting Amplify branch: ${amplifyBranch} (app=${appId})`);
29
+ log(`[cleanup] Deleting Amplify branch: ${branch} (app=${appId})`);
31
30
 
32
31
  const client = new AmplifyClient({ appId, region });
33
- const ok = await client.deleteBranch(amplifyBranch);
32
+ const ok = await client.deleteBranch(branch);
34
33
 
35
34
  if (ok) {
36
35
  log(`[cleanup] Branch ${amplifyBranch} removed successfully`);
@@ -197,11 +197,21 @@ export async function postbuild() {
197
197
  : '';
198
198
  const shortCommit = commitId.slice(0, 8);
199
199
 
200
- const buildDir = process.env.CODEBUILD_SRC_DIR
201
- ? `${process.env.CODEBUILD_SRC_DIR}/build`
202
- : 'build';
200
+ const buildCandidates = [
201
+ process.env.CODEBUILD_SRC_DIR && `${process.env.CODEBUILD_SRC_DIR}/build`,
202
+ 'build',
203
+ 'dist',
204
+ ].filter(Boolean);
205
+
206
+ let buildExists = false;
207
+ for (const dir of buildCandidates) {
208
+ if (await dirExists(dir)) {
209
+ buildExists = true;
210
+ debug(`[postbuild] build dir found: ${dir}`);
211
+ break;
212
+ }
213
+ }
203
214
 
204
- const buildExists = await dirExists(buildDir);
205
215
  const templateName = buildExists ? 'preview-ready' : 'preview-failed';
206
216
 
207
217
  const commentBody = renderTemplate(templateName, {
@@ -213,6 +223,15 @@ export async function postbuild() {
213
223
  region,
214
224
  });
215
225
 
226
+ const deleted = await client.deleteNotesByMarker(
227
+ projectId,
228
+ mr.iid,
229
+ '<!-- dv-gitlab:preview -->',
230
+ );
231
+ if (deleted) {
232
+ debug(`[postbuild] removed ${deleted} stale preview comment(s)`);
233
+ }
234
+
216
235
  const posted = await client.postNote(projectId, mr.iid, commentBody);
217
236
  if (posted) {
218
237
  log(`[postbuild] preview comment posted on MR !${mr.iid}`);
@@ -226,6 +245,12 @@ export async function postbuild() {
226
245
  return;
227
246
  }
228
247
 
248
+ await client.deleteNotesByMarker(
249
+ projectId,
250
+ mr.iid,
251
+ '<!-- dv-gitlab:pipeline -->',
252
+ );
253
+
229
254
  const triggerResult = await triggerPipelinePreview({
230
255
  appId,
231
256
  branch,
@@ -45,6 +45,41 @@ export class GitLabClient {
45
45
  return { description: mrs[0].description || '', iid: mrs[0].iid };
46
46
  }
47
47
 
48
+ /**
49
+ * Deletes all MR notes that contain a given marker string.
50
+ * Used to remove stale bot comments before posting an updated one.
51
+ * @param {string} projectId
52
+ * @param {number} mrIid
53
+ * @param {string} marker
54
+ * @returns {Promise<number>} count of deleted notes
55
+ */
56
+ async deleteNotesByMarker(projectId, mrIid, marker) {
57
+ const url = `${this.apiBase}/projects/${projectId}/merge_requests/${mrIid}/notes?per_page=100`;
58
+ debug(`deleteNotesByMarker GET ${url}`);
59
+
60
+ const res = await this._fetch(url);
61
+ if (!res.ok) {
62
+ debug(`deleteNotesByMarker list failed http=${res.status}`);
63
+ return 0;
64
+ }
65
+
66
+ const notes = await res.json();
67
+ let deleted = 0;
68
+
69
+ for (const note of notes) {
70
+ if (note.body && note.body.includes(marker)) {
71
+ const delUrl = `${this.apiBase}/projects/${projectId}/merge_requests/${mrIid}/notes/${note.id}`;
72
+ const delRes = await this._fetch(delUrl, { method: 'DELETE' });
73
+ if (delRes.ok || delRes.status === 204) {
74
+ deleted++;
75
+ }
76
+ debug(`deleteNotesByMarker id=${note.id} http=${delRes.status}`);
77
+ }
78
+ }
79
+
80
+ return deleted;
81
+ }
82
+
48
83
  /**
49
84
  * Posts a markdown note on an MR.
50
85
  * @param {string} projectId
@@ -1,11 +1,12 @@
1
+ <!-- dv-gitlab:pipeline -->
1
2
  ## 🔄 Automation Pipeline Triggered
2
3
 
3
- | | |
4
- |---|---|
5
- | **Pipeline** | [#{{pipelineId}}]({{pipelineUrl}}) |
6
- | **Project** | `{{project}}` |
7
- | **Ref** | `{{ref}}` |
8
- | **Environment** | `{{environment}}` |
4
+ | | |
5
+ | --------------- | ---------------------------------- |
6
+ | **Pipeline** | [#{{pipelineId}}]({{pipelineUrl}}) |
7
+ | **Project** | `{{project}}` |
8
+ | **Ref** | `{{ref}}` |
9
+ | **Environment** | `{{environment}}` |
9
10
 
10
11
  ---
11
12
 
@@ -1,8 +1,9 @@
1
+ <!-- dv-gitlab:preview -->
1
2
  ## ❌ Preview Build Failed
2
3
 
3
- | | |
4
- |---|---|
5
- | **Branch** | `{{branch}}` |
4
+ | | |
5
+ | ---------- | -------------- |
6
+ | **Branch** | `{{branch}}` |
6
7
  | **Commit** | `{{commitId}}` |
7
8
 
8
9
  ---
@@ -1,9 +1,10 @@
1
+ <!-- dv-gitlab:preview -->
1
2
  ## 🚀 Preview Environment Ready
2
3
 
3
- | | |
4
- |---|---|
5
- | **Branch** | `{{branch}}` |
6
- | **Commit** | `{{commitId}}` |
4
+ | | |
5
+ | --------------- | -------------------------------- |
6
+ | **Branch** | `{{branch}}` |
7
+ | **Commit** | `{{commitId}}` |
7
8
  | **Preview URL** | [{{previewUrl}}]({{previewUrl}}) |
8
9
 
9
10
  ---