@hellopearl/dv-gitlab 0.1.0 → 0.2.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/api-test.yml +84 -0
- package/ci/cleanup-preview.yml +16 -0
- package/package.json +10 -5
- package/src/commands/cleanup-branch.mjs +2 -3
- package/src/commands/postbuild.mjs +29 -4
- package/src/lib/gitlab-client.mjs +35 -0
- package/src/templates/pipeline-triggered.md +7 -6
- package/src/templates/preview-failed.md +4 -3
- package/src/templates/preview-ready.md +5 -4
package/ci/api-test.yml
ADDED
|
@@ -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
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Unified GitLab CI tooling -- MR comments, pipeline triggers, preview env management",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -8,10 +8,13 @@
|
|
|
8
8
|
"exports": {
|
|
9
9
|
".": "./src/index.mjs"
|
|
10
10
|
},
|
|
11
|
-
"bin":
|
|
11
|
+
"bin": {
|
|
12
|
+
"dv-gitlab": "./bin/dv-gitlab.mjs"
|
|
13
|
+
},
|
|
12
14
|
"files": [
|
|
13
15
|
"bin",
|
|
14
|
-
"src"
|
|
16
|
+
"src",
|
|
17
|
+
"ci"
|
|
15
18
|
],
|
|
16
19
|
"scripts": {
|
|
17
20
|
"deps": "dv-deps-runner",
|
|
@@ -38,11 +41,13 @@
|
|
|
38
41
|
"publishConfig": {
|
|
39
42
|
"registry": "https://registry.npmjs.org/"
|
|
40
43
|
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@aws-sdk/client-amplify": "^3.1078.0"
|
|
46
|
+
},
|
|
41
47
|
"devDependencies": {
|
|
42
48
|
"@hellopearl/dv-deps": "*",
|
|
43
49
|
"@hellopearl/dv-lint": "*",
|
|
44
50
|
"@hellopearl/dv-prettier": "*",
|
|
45
51
|
"@hellopearl/dv-test": "*"
|
|
46
|
-
}
|
|
47
|
-
"gitHead": "2da7b9fc23900bbaf9c4fcf6042ecec76d538bc2"
|
|
52
|
+
}
|
|
48
53
|
}
|
|
@@ -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
|
-
|
|
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(
|
|
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
|
|
201
|
-
|
|
202
|
-
|
|
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**
|
|
6
|
-
| **Project**
|
|
7
|
-
| **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,9 +1,10 @@
|
|
|
1
|
+
<!-- dv-gitlab:preview -->
|
|
1
2
|
## 🚀 Preview Environment Ready
|
|
2
3
|
|
|
3
|
-
|
|
|
4
|
-
|
|
5
|
-
| **Branch**
|
|
6
|
-
| **Commit**
|
|
4
|
+
| | |
|
|
5
|
+
| --------------- | -------------------------------- |
|
|
6
|
+
| **Branch** | `{{branch}}` |
|
|
7
|
+
| **Commit** | `{{commitId}}` |
|
|
7
8
|
| **Preview URL** | [{{previewUrl}}]({{previewUrl}}) |
|
|
8
9
|
|
|
9
10
|
---
|