seqpulse 0.3.0 → 0.5.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,221 @@
1
+ # CI/CD Snippets Multi-Platform
2
+
3
+ Snippets de reference pour integrer `SeqPulse` avec la meme logique sur plusieurs plateformes CI/CD:
4
+
5
+ 1. `trigger` avant le deploy
6
+ 2. deploy applicatif
7
+ 3. `finish` toujours execute, meme en cas d'echec
8
+
9
+ Tous les snippets ci-dessous utilisent le package publie `@nassir_gouomba/seqpulse@0.5.0` et la sortie ciblee `--output deploymentId`.
10
+
11
+ ## Variables requises
12
+
13
+ Definir ces secrets/variables dans votre plateforme CI:
14
+
15
+ - `SEQPULSE_BASE_URL`
16
+ - `SEQPULSE_API_KEY`
17
+ - `SEQPULSE_METRICS_ENDPOINT`
18
+
19
+ ## Design cible
20
+
21
+ - `trigger` reste non bloquant pour ne pas casser le deploy si SeqPulse est indisponible
22
+ - `deploymentId` est capture dans une variable CI, pas dans un JSON parse fragile
23
+ - `finish` recoit un statut explicite venant de la plateforme
24
+
25
+ ## GitHub Actions
26
+
27
+ ```yaml
28
+ name: deploy
29
+
30
+ on:
31
+ push:
32
+ branches: [main]
33
+
34
+ jobs:
35
+ deploy:
36
+ runs-on: ubuntu-latest
37
+ env:
38
+ SEQPULSE_BASE_URL: ${{ secrets.SEQPULSE_BASE_URL }}
39
+ SEQPULSE_API_KEY: ${{ secrets.SEQPULSE_API_KEY }}
40
+ SEQPULSE_METRICS_ENDPOINT: ${{ secrets.SEQPULSE_METRICS_ENDPOINT }}
41
+
42
+ steps:
43
+ - uses: actions/checkout@v4
44
+
45
+ - uses: actions/setup-node@v4
46
+ with:
47
+ node-version: 20
48
+ cache: npm
49
+
50
+ - name: Install
51
+ run: npm ci
52
+
53
+ - name: SeqPulse Trigger
54
+ id: seqpulse_trigger
55
+ run: |
56
+ DEPLOYMENT_ID="$(npx -y @nassir_gouomba/seqpulse@0.5.0 ci trigger \
57
+ --env prod \
58
+ --branch "${GITHUB_REF_NAME}" \
59
+ --non-blocking true \
60
+ --output deploymentId)"
61
+ echo "deployment_id=${DEPLOYMENT_ID}" >> "$GITHUB_OUTPUT"
62
+
63
+ - name: Deploy
64
+ run: ./deploy.sh
65
+
66
+ - name: SeqPulse Finish
67
+ if: ${{ always() }}
68
+ run: |
69
+ npx -y @nassir_gouomba/seqpulse@0.5.0 ci finish \
70
+ --deployment-id "${{ steps.seqpulse_trigger.outputs.deployment_id }}" \
71
+ --job-status "${{ job.status }}" \
72
+ --non-blocking true
73
+ ```
74
+
75
+ ## GitLab CI
76
+
77
+ ```yaml
78
+ deploy_prod:
79
+ image: node:20
80
+ stage: deploy
81
+ script:
82
+ - npm ci
83
+ - |
84
+ DEPLOYMENT_ID="$(npx -y @nassir_gouomba/seqpulse@0.5.0 ci trigger \
85
+ --env prod \
86
+ --branch "${CI_COMMIT_REF_NAME}" \
87
+ --non-blocking true \
88
+ --output deploymentId)"
89
+ printf 'SEQPULSE_DEPLOYMENT_ID=%s\n' "$DEPLOYMENT_ID" > .seqpulse.env
90
+ - ./deploy.sh
91
+ after_script:
92
+ - . ./.seqpulse.env 2>/dev/null || true
93
+ - |
94
+ npx -y @nassir_gouomba/seqpulse@0.5.0 ci finish \
95
+ --deployment-id "${SEQPULSE_DEPLOYMENT_ID}" \
96
+ --job-status "${CI_JOB_STATUS}" \
97
+ --non-blocking true
98
+ ```
99
+
100
+ ## CircleCI
101
+
102
+ ```yaml
103
+ version: 2.1
104
+
105
+ jobs:
106
+ deploy:
107
+ docker:
108
+ - image: cimg/node:20.11
109
+ steps:
110
+ - checkout
111
+
112
+ - run:
113
+ name: Install
114
+ command: npm ci
115
+
116
+ - run:
117
+ name: SeqPulse Trigger
118
+ command: |
119
+ DEPLOYMENT_ID="$(npx -y @nassir_gouomba/seqpulse@0.5.0 ci trigger \
120
+ --env prod \
121
+ --branch "${CIRCLE_BRANCH}" \
122
+ --non-blocking true \
123
+ --output deploymentId)"
124
+ echo "export SEQPULSE_DEPLOYMENT_ID=${DEPLOYMENT_ID}" >> "$BASH_ENV"
125
+
126
+ - run:
127
+ name: Deploy
128
+ command: |
129
+ set +e
130
+ ./deploy.sh
131
+ DEPLOY_EXIT_CODE=$?
132
+ if [ "$DEPLOY_EXIT_CODE" -eq 0 ]; then
133
+ echo 'export SEQPULSE_JOB_STATUS=success' >> "$BASH_ENV"
134
+ else
135
+ echo 'export SEQPULSE_JOB_STATUS=failed' >> "$BASH_ENV"
136
+ fi
137
+ exit "$DEPLOY_EXIT_CODE"
138
+
139
+ - run:
140
+ name: SeqPulse Finish
141
+ when: always
142
+ command: |
143
+ . "$BASH_ENV"
144
+ npx -y @nassir_gouomba/seqpulse@0.5.0 ci finish \
145
+ --deployment-id "${SEQPULSE_DEPLOYMENT_ID}" \
146
+ --job-status "${SEQPULSE_JOB_STATUS:-failed}" \
147
+ --non-blocking true
148
+ ```
149
+
150
+ ## Jenkins
151
+
152
+ ```groovy
153
+ pipeline {
154
+ agent any
155
+
156
+ options {
157
+ disableConcurrentBuilds()
158
+ timestamps()
159
+ timeout(time: 20, unit: 'MINUTES')
160
+ }
161
+
162
+ environment {
163
+ SEQPULSE_BASE_URL = credentials('seqpulse_base_url')
164
+ SEQPULSE_API_KEY = credentials('seqpulse_api_key')
165
+ SEQPULSE_METRICS_ENDPOINT = credentials('seqpulse_metrics_endpoint')
166
+ SEQPULSE_DEPLOYMENT_ID = ''
167
+ }
168
+
169
+ stages {
170
+ stage('Install') {
171
+ steps {
172
+ sh 'npm ci'
173
+ }
174
+ }
175
+
176
+ stage('SeqPulse Trigger') {
177
+ steps {
178
+ script {
179
+ def branch = env.CHANGE_BRANCH ?: env.BRANCH_NAME ?: env.GIT_BRANCH ?: 'main'
180
+ env.SEQPULSE_DEPLOYMENT_ID = sh(
181
+ script: """
182
+ npx -y @nassir_gouomba/seqpulse@0.5.0 ci trigger \
183
+ --env prod \
184
+ --branch "${branch}" \
185
+ --non-blocking true \
186
+ --output deploymentId
187
+ """,
188
+ returnStdout: true
189
+ ).trim()
190
+ }
191
+ }
192
+ }
193
+
194
+ stage('Deploy') {
195
+ steps {
196
+ sh './deploy.sh'
197
+ }
198
+ }
199
+ }
200
+
201
+ post {
202
+ always {
203
+ script {
204
+ def jobStatus = (currentBuild.currentResult ?: 'SUCCESS').toLowerCase()
205
+ sh """
206
+ npx -y @nassir_gouomba/seqpulse@0.5.0 ci finish \
207
+ --deployment-id "${env.SEQPULSE_DEPLOYMENT_ID ?: ''}" \
208
+ --job-status "${jobStatus}" \
209
+ --non-blocking true
210
+ """
211
+ }
212
+ }
213
+ }
214
+ }
215
+ ```
216
+
217
+ ## Notes pratiques
218
+
219
+ - Si le package est deja installe dans le projet, vous pouvez remplacer `npx -y @nassir_gouomba/seqpulse@0.5.0` par `npx seqpulse`.
220
+ - Pour les deploys `prod`, ajoutez un gate explicite sur `main`/tag si votre pipeline est multi-branches.
221
+ - Le pattern recommande est toujours le meme: `trigger -> deploy -> finish`.
package/README.md CHANGED
@@ -3,21 +3,21 @@
3
3
  SeqPulse Node SDK couvre deux usages:
4
4
 
5
5
  - Runtime application: instrumentation HTTP + endpoint metrics + validation HMAC v2
6
- - CI/CD integration: client `trigger/finish` pour orchestrer un deployment SeqPulse
6
+ - CI/CD integration: client `trigger/finish` + CLI `seqpulse ci ...`
7
7
 
8
8
  ## Install
9
9
 
10
10
  ```bash
11
- npm install seqpulse
11
+ npm install @nassir_gouomba/seqpulse
12
12
  # or
13
- pnpm add seqpulse
13
+ pnpm add @nassir_gouomba/seqpulse
14
14
  ```
15
15
 
16
16
  ## Runtime (Express)
17
17
 
18
18
  ```js
19
19
  const express = require("express");
20
- const seqpulse = require("seqpulse");
20
+ const seqpulse = require("@nassir_gouomba/seqpulse");
21
21
 
22
22
  const app = express();
23
23
 
@@ -53,7 +53,7 @@ app.listen(3000, () => {
53
53
  ## CI/CD client (trigger/finish)
54
54
 
55
55
  ```js
56
- const seqpulse = require("seqpulse");
56
+ const seqpulse = require("@nassir_gouomba/seqpulse");
57
57
 
58
58
  const client = seqpulse.createCIClient({
59
59
  baseUrl: process.env.SEQPULSE_BASE_URL,
@@ -66,7 +66,7 @@ const client = seqpulse.createCIClient({
66
66
  async function runDeployment() {
67
67
  const trigger = await client.trigger({
68
68
  branch: process.env.GITHUB_REF_NAME,
69
- idempotencyKey: `gha-${process.env.GITHUB_RUN_ID}-${process.env.GITHUB_RUN_ATTEMPT}`,
69
+ idempotencyKey: seqpulse.buildCiIdempotencyKey(),
70
70
  });
71
71
 
72
72
  // Deploy your app here...
@@ -81,11 +81,30 @@ async function runDeployment() {
81
81
  }
82
82
  ```
83
83
 
84
- ### CI behavior
84
+ ## CI/CD CLI (short YAML)
85
85
 
86
- - `nonBlocking: true` (default): returns a skipped/error result instead of throwing.
87
- - `nonBlocking: false`: throws on API/config errors.
88
- - idempotency key helper available: `seqpulse.buildCiIdempotencyKey()`.
86
+ ```bash
87
+ # Trigger
88
+ DEPLOYMENT_ID="$(npx -y @nassir_gouomba/seqpulse@0.5.0 ci trigger \
89
+ --env prod \
90
+ --output deploymentId)"
91
+
92
+ # Finish
93
+ npx -y @nassir_gouomba/seqpulse@0.5.0 ci finish \
94
+ --deployment-id "$DEPLOYMENT_ID" \
95
+ --job-status "$JOB_STATUS"
96
+ ```
97
+
98
+ Notes:
99
+
100
+ - `--output deploymentId` permet de capturer directement l'identifiant de deployment.
101
+ - `--github-output` ecrit `deployment_id`, `status`, `http_status`.
102
+ - mode par defaut: non-bloquant (`--non-blocking true`).
103
+ - pour mode strict: `--blocking`.
104
+
105
+ ## Documentation
106
+
107
+ - [Multi-platform CI/CD snippets](./CI_CD_SNIPPETS_MULTI-PLATFORM.md)
89
108
 
90
109
  ## Compatibility note
91
110
 
@@ -93,7 +112,7 @@ Cette evolution **n'ecrase pas** le SDK runtime actuel:
93
112
 
94
113
  - l'endpoint `/seqpulse-metrics` reste le meme
95
114
  - la logique HMAC runtime reste la meme
96
- - le client CI est ajoute en couche supplementaire
115
+ - la CLI CI est une surcouche du client SDK existant
97
116
 
98
117
  ## Local smoke test
99
118
 
@@ -0,0 +1,242 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const fs = require("node:fs");
5
+ const seqpulse = require("../index.js");
6
+
7
+ function printUsage() {
8
+ console.error(`Usage:
9
+ seqpulse ci trigger [options]
10
+ seqpulse ci finish [options]
11
+
12
+ Commands:
13
+ ci trigger Trigger SeqPulse deployment in CI
14
+ ci finish Finish SeqPulse deployment in CI
15
+
16
+ Common options:
17
+ --base-url <url>
18
+ --api-key <key>
19
+ --metrics-endpoint <url>
20
+ --timeout-ms <number>
21
+ --env <name> (default: prod)
22
+ --non-blocking <true|false> (default: true)
23
+ --blocking (equivalent to --non-blocking false)
24
+ --output <field> (default: json)
25
+ --github-output <path> (writes outputs for GitHub Actions)
26
+
27
+ Trigger options:
28
+ --branch <name>
29
+ --idempotency-key <key>
30
+
31
+ Finish options:
32
+ --deployment-id <id>
33
+ --result <success|failed>
34
+ --job-status <status>
35
+ `);
36
+ }
37
+
38
+ function parseArgs(argv) {
39
+ const positional = [];
40
+ const options = {};
41
+
42
+ for (let i = 0; i < argv.length; i += 1) {
43
+ const token = argv[i];
44
+ if (!token.startsWith("--")) {
45
+ positional.push(token);
46
+ continue;
47
+ }
48
+
49
+ const eq = token.indexOf("=");
50
+ if (eq > -1) {
51
+ const key = token.slice(2, eq);
52
+ const value = token.slice(eq + 1);
53
+ options[key] = value;
54
+ continue;
55
+ }
56
+
57
+ const key = token.slice(2);
58
+ const next = argv[i + 1];
59
+ if (!next || next.startsWith("--")) {
60
+ options[key] = true;
61
+ continue;
62
+ }
63
+
64
+ options[key] = next;
65
+ i += 1;
66
+ }
67
+
68
+ return { positional, options };
69
+ }
70
+
71
+ function parseBoolean(value, defaultValue) {
72
+ if (value === undefined) return defaultValue;
73
+ if (typeof value === "boolean") return value;
74
+ const normalized = String(value).trim().toLowerCase();
75
+ if (["1", "true", "yes", "y", "on"].includes(normalized)) return true;
76
+ if (["0", "false", "no", "n", "off"].includes(normalized)) return false;
77
+ throw new Error(`Invalid boolean value: ${value}`);
78
+ }
79
+
80
+ function parseNumber(value, defaultValue) {
81
+ if (value === undefined) return defaultValue;
82
+ const num = Number(value);
83
+ if (!Number.isFinite(num) || num <= 0) {
84
+ throw new Error(`Invalid numeric value: ${value}`);
85
+ }
86
+ return num;
87
+ }
88
+
89
+ function sanitizeOutput(value) {
90
+ return String(value === undefined || value === null ? "" : value).replace(/[\r\n]/g, "");
91
+ }
92
+
93
+ function appendGithubOutput(filePath, pairs) {
94
+ if (!filePath) return;
95
+ const lines = Object.entries(pairs)
96
+ .map(([k, v]) => `${k}=${sanitizeOutput(v)}`)
97
+ .join("\n");
98
+ fs.appendFileSync(filePath, `${lines}\n`);
99
+ }
100
+
101
+ function resolveOutputValue(result, output) {
102
+ const mode = String(output || "json").trim();
103
+ if (!mode || mode === "json") {
104
+ return JSON.stringify(result);
105
+ }
106
+
107
+ const aliases = {
108
+ deployment_id: "deploymentId",
109
+ http_status: "httpStatus",
110
+ };
111
+ const path = (aliases[mode] || mode).split(".");
112
+
113
+ let current = result;
114
+ for (const segment of path) {
115
+ if (current === undefined || current === null) {
116
+ current = "";
117
+ break;
118
+ }
119
+ current = current[segment];
120
+ }
121
+
122
+ if (current === undefined || current === null) {
123
+ return "";
124
+ }
125
+
126
+ if (typeof current === "object") {
127
+ return JSON.stringify(current);
128
+ }
129
+
130
+ return String(current);
131
+ }
132
+
133
+ function printResult(result, output) {
134
+ console.log(sanitizeOutput(resolveOutputValue(result, output)));
135
+ }
136
+
137
+ function resolveNonBlocking(options) {
138
+ if (options.blocking !== undefined) return false;
139
+ if (options["non-blocking"] !== undefined) return parseBoolean(options["non-blocking"], true);
140
+ if (options.nonblocking !== undefined) return parseBoolean(options.nonblocking, true);
141
+ return true;
142
+ }
143
+
144
+ function buildClient(options) {
145
+ return {
146
+ baseUrl: options["base-url"],
147
+ apiKey: options["api-key"],
148
+ metricsEndpoint: options["metrics-endpoint"],
149
+ env: options.env || "prod",
150
+ timeoutMs: parseNumber(options["timeout-ms"], undefined),
151
+ nonBlocking: resolveNonBlocking(options),
152
+ };
153
+ }
154
+
155
+ async function runTrigger(options) {
156
+ const clientConfig = buildClient(options);
157
+ const client = seqpulse.createCIClient(clientConfig);
158
+ const result = await client.trigger({
159
+ env: options.env || clientConfig.env || "prod",
160
+ branch: options.branch,
161
+ idempotencyKey: options["idempotency-key"] || seqpulse.buildCiIdempotencyKey(),
162
+ });
163
+
164
+ appendGithubOutput(options["github-output"], {
165
+ deployment_id: result.ok ? result.deploymentId || "" : "",
166
+ status: result.status || result.error || "",
167
+ http_status: result.httpStatus || "",
168
+ });
169
+
170
+ printResult({ action: "trigger", ...result }, options.output);
171
+
172
+ if (!result.ok && !clientConfig.nonBlocking) {
173
+ process.exitCode = 1;
174
+ }
175
+ }
176
+
177
+ async function runFinish(options) {
178
+ const clientConfig = buildClient(options);
179
+ const deploymentId = options["deployment-id"] || process.env.SEQPULSE_DEPLOYMENT_ID || "";
180
+ const jobStatus =
181
+ options["job-status"] ||
182
+ process.env.JOB_STATUS ||
183
+ process.env.CI_JOB_STATUS ||
184
+ process.env.BUILD_RESULT ||
185
+ "success";
186
+ const resultValue = options.result || seqpulse.inferResultFromPipelineStatus(jobStatus);
187
+
188
+ const client = seqpulse.createCIClient(clientConfig);
189
+ const result = await client.finish({
190
+ deploymentId,
191
+ result: resultValue,
192
+ });
193
+
194
+ appendGithubOutput(options["github-output"], {
195
+ status: result.status || result.error || "",
196
+ http_status: result.httpStatus || "",
197
+ });
198
+
199
+ printResult({ action: "finish", ...result }, options.output);
200
+
201
+ if (!result.ok && !clientConfig.nonBlocking) {
202
+ process.exitCode = 1;
203
+ }
204
+ }
205
+
206
+ async function main() {
207
+ const { positional, options } = parseArgs(process.argv.slice(2));
208
+ const [scope, command] = positional;
209
+
210
+ if (options.help || options.h) {
211
+ printUsage();
212
+ process.exit(0);
213
+ return;
214
+ }
215
+
216
+ if (!scope || !command) {
217
+ printUsage();
218
+ process.exit(1);
219
+ return;
220
+ }
221
+
222
+ if (scope !== "ci") {
223
+ throw new Error(`Unsupported scope: ${scope}`);
224
+ }
225
+
226
+ if (command === "trigger") {
227
+ await runTrigger(options);
228
+ return;
229
+ }
230
+
231
+ if (command === "finish") {
232
+ await runFinish(options);
233
+ return;
234
+ }
235
+
236
+ throw new Error(`Unsupported command: ${command}`);
237
+ }
238
+
239
+ main().catch((error) => {
240
+ console.error(`[seqpulse] ${error && error.message ? error.message : String(error)}`);
241
+ process.exit(1);
242
+ });
package/index.js CHANGED
@@ -228,9 +228,11 @@ function buildCiIdempotencyKey() {
228
228
 
229
229
  function inferBranchName() {
230
230
  return (
231
+ process.env.CHANGE_BRANCH ||
231
232
  process.env.GITHUB_REF_NAME ||
232
233
  process.env.CI_COMMIT_REF_NAME ||
233
234
  process.env.CIRCLE_BRANCH ||
235
+ process.env.GIT_BRANCH ||
234
236
  process.env.BRANCH_NAME ||
235
237
  "unknown"
236
238
  );
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "seqpulse",
3
- "version": "0.3.0",
3
+ "version": "0.5.1",
4
4
  "description": "SeqPulse SDK for metrics endpoint instrumentation and HMAC validation",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
7
+ "bin": {
8
+ "seqpulse": "bin/seqpulse.js"
9
+ },
7
10
  "exports": {
8
11
  ".": {
9
12
  "require": "./index.js",
@@ -13,7 +16,9 @@
13
16
  "files": [
14
17
  "index.js",
15
18
  "index.d.ts",
19
+ "bin/seqpulse.js",
16
20
  "README.md",
21
+ "CI_CD_SNIPPETS_MULTI-PLATFORM.md",
17
22
  "LICENSE",
18
23
  "scripts/smoke.js"
19
24
  ],