antelope-cli 1.2.4 → 1.2.6

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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  # Pinned published antelope-cli version that provides the `onboarding-ca` command.
3
- antelope_cli_version: "1.2.3"
3
+ antelope_cli_version: "1.2.5"
4
4
 
5
5
  # Where web-client-area is cloned on the control node.
6
6
  work_dir: "/tmp/onboard-ca/web-client-area"
@@ -45,7 +45,7 @@ dest_branches: ""
45
45
  # Require a signed-in `op` CLI on the control node (op signin, or OP_SERVICE_ACCOUNT_TOKEN).
46
46
  # Service-account auth requires an explicit vault for `op item get` by name (op errors
47
47
  # with "a vault query must be provided" otherwise) — the items live in the DevOps vault.
48
- onepassword_vault: "DevOps"
48
+ onepassword_vault: "jenkins"
49
49
  onepassword_bitbucket_item: "Bitbucket API - jenkins@xsites.co.il"
50
50
  onepassword_bitbucket_field: "credential"
51
51
  onepassword_consul_item: "New_Consul_Token"
@@ -0,0 +1,211 @@
1
+ // Self-service CRM onboarding — jenkins.xsites.xyz/job/self-service/job/onboarding/onboarding-crm
2
+ //
3
+ // Pipeline-as-Code (this file is the source of truth; the Jenkins job is just a
4
+ // "Pipeline script from SCM" pointer at antelope-cli@master, path ansible/onboarding-crm/Jenkinsfile).
5
+ //
6
+ // Design (best practice, replacing the legacy freestyle+vault-pem+canRoam pattern):
7
+ // - ephemeral containerized agent via `agent { dockerfile }`: the pinned runner image
8
+ // (ansible, node/npx, op, git) is BUILT on the spot fleet from runner.Dockerfile — no
9
+ // registry/ECR needed. Runs on any agent (the whole fleet is docker-capable).
10
+ // - ONE secret: a 1Password service-account token (onepassword-service-account-token) —
11
+ // op fetches the bitbucket/consul/jira tokens from the `jenkins` vault (matches the role)
12
+ // - a Bitbucket SSH key (bitbucket-jenkins-ssh) for the git push to web-crm
13
+ // - parameters named exactly as the configs.ts fields, validated up front
14
+ // - DRY_RUN preview (generate + show configs.ts, no writes) + human approval before any push
15
+ //
16
+ // Provisioning prereqs:
17
+ // - fleet agents on the docker-enabled AMI (the whole fleet carries docker)
18
+ // - credential 'onepassword-service-account-token' : Secret text = OP_SERVICE_ACCOUNT_TOKEN
19
+ // (1Password service account, read-only on the `jenkins` vault)
20
+ // - credential 'bitbucket-jenkins-ssh' : SSH key = jenkins Bitbucket push key
21
+ // (both folder-scoped to self-service/onboarding)
22
+
23
+ pipeline {
24
+ agent {
25
+ dockerfile {
26
+ dir 'ansible/onboarding-crm'
27
+ filename 'runner.Dockerfile'
28
+ additionalBuildArgs '--pull'
29
+ }
30
+ }
31
+
32
+ options {
33
+ timestamps()
34
+ ansiColor('xterm')
35
+ disableConcurrentBuilds()
36
+ buildDiscarder(logRotator(numToKeepStr: '30'))
37
+ }
38
+
39
+ parameters {
40
+ // --- named exactly as configs.ts fields ---
41
+ string(name: 'brandAssetsFolder', defaultValue: '', trim: true,
42
+ description: 'Antelope ID (lowercase alphanumeric). configs.ts brandAssetsFolder / devBrandName / colorTheme / templateTheme / favicon all derive from this. e.g. uzumy')
43
+ string(name: 'title', defaultValue: '', trim: true,
44
+ description: 'Brand name. configs.ts title becomes "<title> CRM". e.g. Excelttra')
45
+ choice(name: 'type', choices: ['CORP_FL', 'IB_FL', 'CORP_AFF_FL'],
46
+ description: 'configs.ts type')
47
+ string(name: 'baseUrl', defaultValue: '', trim: true,
48
+ description: 'configs.ts baseUrl. e.g. https://apicrm.mteeks.com/SignalsCRM/')
49
+ string(name: 'affiliatesApiUrl', defaultValue: '', trim: true,
50
+ description: 'configs.ts affiliatesApiUrl (FE host). e.g. apife.excelttra.com')
51
+ booleanParam(name: 'isArcticBrand', defaultValue: false,
52
+ description: 'configs.ts isArcticBrand')
53
+ // --- onboarding inputs (not configs.ts fields) ---
54
+ string(name: 'languages', defaultValue: 'en', trim: true,
55
+ description: 'Comma-separated 2-letter language codes. e.g. en,ar')
56
+ string(name: 'logo_url', defaultValue: '', trim: true,
57
+ description: 'Logo URL. Must be https://logos-<brandAssetsFolder>.s3.eu-west-1.amazonaws.com/logo-<title>.<ext>. Blank => derived.')
58
+ string(name: 'favicon', defaultValue: '', trim: true,
59
+ description: 'Favicon URL (configs.ts favicon). Must be https://logos-<brandAssetsFolder>.s3.eu-west-1.amazonaws.com/favicon-<title>.<ext>. Blank => derived.')
60
+ string(name: 'jira_key_crm_branding', defaultValue: '', trim: true,
61
+ description: 'Jira key for the branding PRs. e.g. ATLP-47345')
62
+ // --- safety ---
63
+ booleanParam(name: 'DRY_RUN', defaultValue: true,
64
+ description: 'Preview only: generate + show configs.ts, NO branch push / PR / Consul / Jira write')
65
+ }
66
+
67
+ environment {
68
+ ANSIBLE_HOST_KEY_CHECKING = 'False'
69
+ ANSIBLE_FORCE_COLOR = 'true'
70
+ WORK_DIR = '/tmp/onboard-crm/web-crm'
71
+ PLAYBOOK_DIR = 'ansible/onboarding-crm'
72
+ // The dockerfile agent runs the container as the Jenkins UID, which has no home dir
73
+ // (HOME=/), so ansible (~/.ansible/tmp), npm (~/.npm) and git (~/.gitconfig) can't write
74
+ // their dotdirs. Point HOME at the writable, correctly-owned workspace.
75
+ HOME = "${env.WORKSPACE}"
76
+ }
77
+
78
+ stages {
79
+ stage('Validate parameters') {
80
+ steps {
81
+ script {
82
+ def CRM_TYPES = ['CORP_FL', 'IB_FL', 'CORP_AFF_FL']
83
+ def errs = []
84
+ def req = { v, n -> if (!v?.trim()) errs << "${n} is required" }
85
+
86
+ req(params.brandAssetsFolder, 'brandAssetsFolder')
87
+ if (params.brandAssetsFolder && !(params.brandAssetsFolder ==~ /^[a-z0-9]+$/))
88
+ errs << "brandAssetsFolder must be lowercase alphanumeric (the Antelope ID); got '${params.brandAssetsFolder}'"
89
+
90
+ req(params.title, 'title')
91
+
92
+ if (!CRM_TYPES.contains(params.type))
93
+ errs << "type must be one of ${CRM_TYPES}; got '${params.type}'"
94
+
95
+ req(params.baseUrl, 'baseUrl')
96
+ if (params.baseUrl && !(params.baseUrl ==~ /^https:\/\/apicrm\.[^\/]+\/SignalsCRM\/$/))
97
+ errs << "baseUrl must look like https://apicrm.<domain>/SignalsCRM/ ; got '${params.baseUrl}'"
98
+
99
+ req(params.affiliatesApiUrl, 'affiliatesApiUrl')
100
+ if (params.affiliatesApiUrl && !(params.affiliatesApiUrl ==~ /^apife\.[a-z0-9.\-]+$/))
101
+ errs << "affiliatesApiUrl must look like apife.<ca-domain> (no scheme); got '${params.affiliatesApiUrl}'"
102
+
103
+ if (!(params.languages ==~ /^[a-z]{2}(,[a-z]{2})*$/))
104
+ errs << "languages must be comma-separated 2-letter codes; got '${params.languages}'"
105
+
106
+ // logo/favicon must follow logos-<antelope_id>/<kind>-<brand_name>.<ext> (when set; blank => derived)
107
+ def IMG_EXT = ['png', 'jpg', 'jpeg', 'svg', 'webp', 'ico', 'gif']
108
+ def checkImg = { url, kind ->
109
+ if (!url?.trim()) return null
110
+ def m = (url =~ /^https:\/\/logos-([a-z0-9]+)\.s3\.eu-west-1\.amazonaws\.com\/(logo|favicon)-(.+)\.([A-Za-z0-9]+)$/)
111
+ if (!m) return "${kind} must look like https://logos-<id>.s3.eu-west-1.amazonaws.com/${kind}-<brand>.<ext>; got '${url}'"
112
+ def (whole, bucketId, prefix, brand, ext) = m[0]
113
+ if (prefix != kind) return "${kind} filename must start with '${kind}-' (got '${prefix}-')"
114
+ if (bucketId != params.brandAssetsFolder) return "${kind} bucket must be logos-${params.brandAssetsFolder} (got logos-${bucketId})"
115
+ if (brand != params.title) return "${kind} filename brand must be '${params.title}' (got '${brand}')"
116
+ if (!IMG_EXT.contains(ext.toLowerCase())) return "${kind} extension '${ext}' not allowed (${IMG_EXT.join('/')})"
117
+ return null
118
+ }
119
+ [checkImg(params.logo_url, 'logo'), checkImg(params.favicon, 'favicon')].each { if (it) errs << it }
120
+
121
+ if (!(params.jira_key_crm_branding ==~ /^[A-Z][A-Z0-9]+-\d+$/))
122
+ errs << "jira_key_crm_branding must look like ATLP-12345; got '${params.jira_key_crm_branding}'"
123
+
124
+ if (errs) {
125
+ error("Parameter validation failed:\n - " + errs.join("\n - "))
126
+ }
127
+ echo "✅ Parameters valid — antelope_id=${params.brandAssetsFolder}, type=${params.type}, dry_run=${params.DRY_RUN}"
128
+ currentBuild.displayName = "#${env.BUILD_NUMBER} ${params.title}${params.DRY_RUN ? ' (dry-run)' : ''}"
129
+ }
130
+ }
131
+ }
132
+
133
+ stage('Onboard (dry-run preview)') {
134
+ when { expression { params.DRY_RUN } }
135
+ steps {
136
+ withCredentials([string(credentialsId: 'onepassword-service-account-token', variable: 'OP_SERVICE_ACCOUNT_TOKEN')]) {
137
+ sshagent(['bitbucket-jenkins-ssh']) {
138
+ sh label: 'ansible dry-run (no writes)', script: '''
139
+ cd "$PLAYBOOK_DIR"
140
+ ansible-playbook onboard-crm.yml \
141
+ -e antelope_id="$brandAssetsFolder" \
142
+ -e brand_name="$title" \
143
+ -e crm_type="$type" \
144
+ -e base_url="$baseUrl" \
145
+ -e affiliates_api_url="$affiliatesApiUrl" \
146
+ -e arctic_brand="$isArcticBrand" \
147
+ -e languages="$languages" \
148
+ ${logo_url:+-e logo_url=$logo_url} \
149
+ ${favicon:+-e favicon=$favicon} \
150
+ -e jira_key_crm_branding="$jira_key_crm_branding" \
151
+ -e dry_run=true \
152
+ < /dev/null
153
+ '''
154
+ }
155
+ }
156
+ script { archiveGeneratedConfigs() }
157
+ }
158
+ }
159
+
160
+ stage('Approve') {
161
+ when { expression { !params.DRY_RUN } }
162
+ steps {
163
+ script {
164
+ input message: "Open web-crm PR(s) + write Consul crm_env + stamp Jira for '${params.brandAssetsFolder}'?",
165
+ ok: 'Onboard'
166
+ }
167
+ }
168
+ }
169
+
170
+ stage('Onboard') {
171
+ when { expression { !params.DRY_RUN } }
172
+ steps {
173
+ withCredentials([string(credentialsId: 'onepassword-service-account-token', variable: 'OP_SERVICE_ACCOUNT_TOKEN')]) {
174
+ sshagent(['bitbucket-jenkins-ssh']) {
175
+ sh label: 'ansible onboard-crm', script: '''
176
+ cd "$PLAYBOOK_DIR"
177
+ ansible-playbook onboard-crm.yml \
178
+ -e antelope_id="$brandAssetsFolder" \
179
+ -e brand_name="$title" \
180
+ -e crm_type="$type" \
181
+ -e base_url="$baseUrl" \
182
+ -e affiliates_api_url="$affiliatesApiUrl" \
183
+ -e arctic_brand="$isArcticBrand" \
184
+ -e languages="$languages" \
185
+ ${logo_url:+-e logo_url=$logo_url} \
186
+ ${favicon:+-e favicon=$favicon} \
187
+ -e jira_key_crm_branding="$jira_key_crm_branding" \
188
+ < /dev/null
189
+ '''
190
+ }
191
+ }
192
+ script { archiveGeneratedConfigs() }
193
+ }
194
+ }
195
+ }
196
+
197
+ post {
198
+ always { cleanWs() }
199
+ failure { echo 'Onboarding failed — see the validation/ansible output above.' }
200
+ }
201
+ }
202
+
203
+ // Archive the generated (gitignored) configs.ts so the operator can copy it into Consul.
204
+ void archiveGeneratedConfigs() {
205
+ def cfg = "${env.WORK_DIR}/src/configs/configs.ts"
206
+ if (fileExists(cfg)) {
207
+ sh "mkdir -p out && cp '${cfg}' out/configs.ts"
208
+ archiveArtifacts artifacts: 'out/configs.ts', allowEmptyArchive: true
209
+ currentBuild.description = "configs.ts generated for ${params.brandAssetsFolder}"
210
+ }
211
+ }
@@ -67,7 +67,7 @@ token with `$` — the shell would try to expand it and send an empty token.
67
67
  |-----|----------|---------|-------|
68
68
  | `antelope_id` | yes | `xyzxx` | Antelope ID → CLI `--brand_id`; keys every generated file + the branch name (normalized lowercase/no-spaces) |
69
69
  | `brand_name` | yes | `Zenith Horizon Group` | → CLI `--brand_name`, PR title, configs title |
70
- | `crm_type` | yes | `CORP_FL` | → CLI `--type`; one of `CORP_FL`, `IB_FL`, `CORP_AFF_FL`, `CORP_AFF_IB_PORTAL`, `CORP_AFF_MSQ` |
70
+ | `crm_type` | yes | `CORP_FL` | → CLI `--type`; one of `CORP_FL`, `IB_FL`, `CORP_AFF_FL` |
71
71
  | `logo_url` | yes | *(url)* | → CLI `--logo_url` (black-background logo) |
72
72
  | `favicon_url` | yes | *(url)* | → CLI `--favicon_url` |
73
73
  | `languages` | yes | `en,ar` | → CLI `--languages`; CSV string **or** a YAML list (`[en, ar]`) |
@@ -44,14 +44,11 @@
44
44
  prompt: "Brand display name — recorded in BRAND_LOCATIONS.md / configs title (e.g. Zenith Horizon Group)"
45
45
  private: false
46
46
  - name: crm_type
47
- prompt: "CRM type (one of: CORP_FL, IB_FL, CORP_AFF_FL, CORP_AFF_IB_PORTAL, CORP_AFF_MSQ)"
47
+ prompt: "CRM type (one of: CORP_FL, IB_FL, CORP_AFF_FL)"
48
48
  private: false
49
49
  - name: languages
50
50
  prompt: "Languages — 2-letter codes, comma-separated (e.g. en,ar)"
51
51
  private: false
52
- - name: fcm_sender_id
53
- prompt: "Firebase FCMSenderId"
54
- private: false
55
52
  - name: jira_key_crm_branding
56
53
  prompt: "Jira key (e.g. ATLP-47067)"
57
54
  private: false
@@ -1,36 +1,36 @@
1
1
  ---
2
2
  # Pinned published antelope-cli version that provides the `onboarding-crm` command.
3
- antelope_cli_version: "1.2.3"
3
+ antelope_cli_version: "1.2.5"
4
4
 
5
5
  # Where web-crm is cloned on the control node.
6
6
  work_dir: "/tmp/onboard-crm/web-crm"
7
7
 
8
8
  # --- CRM brand inputs (map to the `onboarding-crm` CLI flags) ------------------------
9
- # antelope_id => --brand_id (Antelope unique ID / connectedBrandName; keys every file the CLI writes)
10
- # brand_name, languages, fcm_sender_id, crm_type are REQUIRED; logo_url/favicon_url/base_url derive.
11
- # color is optional (blank => the CLI's $corp palette).
12
- #
13
- # favicon_url and base_url default to the SAME derivations as the deployment config
14
- # (xsites-devops ansible-deployment-new/roles/consul/templates/crm.json.j2), so the values
15
- # match what gets seeded into Consul. They resolve from the brand vars (antelope_id,
16
- # brand_name, crm_server_url) in the create_brand context; the standalone playbook's prompts
17
- # override them. crm_type is a per-brand value (not defaulted anywhere — set it in brand_vars
18
- # or -e). logo_url has no derivation in that template — must be provided explicitly.
9
+ # antelope_id => --brand_id (connectedBrandName; keys theme/folder/devBrandName/favicon in configs.ts).
10
+ # REQUIRED: antelope_id, brand_name, crm_type, logo_url, languages. base_url + affiliates_api_url
11
+ # derive from the brand vars in create_brand; pass -e for a standalone run. configs.ts favicon is
12
+ # DERIVED by the CLI (logos-<id>/favicon-<name>.png) and FCMSenderId is always '' (both placeholders).
19
13
  antelope_id: ""
20
- color: "" # --color hex, e.g. "#1a2b3c"; blank => default palette
21
- logo_url: "https://logos-{{ antelope_id }}.s3.eu-west-1.amazonaws.com/logo-{{ brand_name }}.png" # derived like favicon_url; override with -e
14
+ color: "" # --color hex; blank => default palette
15
+ logo_url: "https://logos-{{ antelope_id }}.s3.eu-west-1.amazonaws.com/logo-{{ brand_name }}.png" # brand-logos.json entry; override with -e
16
+ favicon: "https://logos-{{ antelope_id }}.s3.eu-west-1.amazonaws.com/favicon-{{ brand_name }}.png" # --favicon => configs.ts favicon; blank => CLI derives the same
22
17
  languages: "" # --languages: CSV "en,ar" or a YAML list [en, ar]
23
- fcm_sender_id: "" # --fcm_sender_id (Firebase) blank in crm.json.j2 but the CLI requires it
24
- crm_type: "" # --type: one of allowed_crm_types below (per-brand; set in brand_vars)
25
- favicon_url: "https://logos-{{ antelope_id }}.s3.eu-west-1.amazonaws.com/favicon-{{ brand_name }}.png"
18
+ crm_type: "" # --type: one of allowed_crm_types (per-brand; from group_vars derivation)
19
+ # baseUrl uses the BE/CRM domain; affiliatesApiUrl uses the FE/CA domain (fe_server_url_list.0).
26
20
  base_url: "{{ ('https://' ~ crm_server_url ~ '/SignalsCRM/') if (crm_server_url | default('') | length > 0) else '' }}"
21
+ affiliates_api_url: "{{ fe_server_url_list.0 | default('') }}"
22
+ arctic_brand: false # --arctic_brand => configs.ts isArcticBrand
23
+ # Consul KV key the generated configs.ts is PUT to (the env CRM config). Needs a consul_token
24
+ # with write on this path. brand_name = the Consul namespace segment (e.g. Antelope/excelttra/crm_env).
25
+ crm_env_consul_key: "Antelope/{{ brand_name }}/crm_env"
26
+ manage_consul_crm_env: true
27
+ # Preview mode: clone + generate configs.ts/brand-logos but SKIP commit/push/PR/Jira/Consul writes.
28
+ dry_run: false
27
29
  # Kept in sync with ALLOWED_TYPES in antelope-cli/onboarding-crm/index.js.
28
30
  allowed_crm_types:
29
31
  - CORP_FL
30
32
  - IB_FL
31
33
  - CORP_AFF_FL
32
- - CORP_AFF_IB_PORTAL
33
- - CORP_AFF_MSQ
34
34
 
35
35
  # Reviewers for the PR. Empty => auto-fetched from the repo's Bitbucket "Default reviewers"
36
36
  # config (the authenticating account is excluded automatically). Override with an explicit
@@ -67,7 +67,7 @@ dest_branches: ""
67
67
  # Require a signed-in `op` CLI on the control node (op signin, or OP_SERVICE_ACCOUNT_TOKEN).
68
68
  # Service-account auth requires an explicit vault for `op item get` by name (op errors
69
69
  # with "a vault query must be provided" otherwise) — the items live in the DevOps vault.
70
- onepassword_vault: "DevOps"
70
+ onepassword_vault: "jenkins"
71
71
  onepassword_bitbucket_item: "Bitbucket API - jenkins@xsites.co.il"
72
72
  onepassword_bitbucket_field: "credential"
73
73
  onepassword_consul_item: "New_Consul_Token"
@@ -75,16 +75,13 @@
75
75
  - brand_name is defined and (brand_name | length) > 0
76
76
  - jira_key_crm_branding is defined and (jira_key_crm_branding | length) > 0
77
77
  - logo_url is defined and (logo_url | length) > 0
78
- - favicon_url is defined and (favicon_url | length) > 0
79
78
  - languages is defined and (languages | length) > 0
80
- - fcm_sender_id is defined and (fcm_sender_id | length) > 0
81
79
  - crm_type is defined and (crm_type | length) > 0
82
80
  - crm_type in allowed_crm_types
83
81
  - bitbucket_token is defined and (bitbucket_token | length) > 0
84
82
  - bitbucket_email is defined and (bitbucket_email | length) > 0
85
83
  fail_msg: >-
86
- Required: antelope_id, brand_name, jira_key_crm_branding, logo_url, favicon_url, languages,
87
- fcm_sender_id, crm_type (one of {{ allowed_crm_types | join(', ') }}), bitbucket_email.
84
+ Required: antelope_id, brand_name, jira_key_crm_branding, logo_url, languages, crm_type (one of {{ allowed_crm_types | join(', ') }}), bitbucket_email.
88
85
  bitbucket_token must be provided (via --extra-vars / prompt) or resolvable from 1Password
89
86
  item '{{ onepassword_bitbucket_item }}' field '{{ onepassword_bitbucket_field }}' (op signin).
90
87
 
@@ -153,7 +150,7 @@
153
150
  # Fix Version, so stamp the release version (V<version/prod>) on the ticket BEFORE the
154
151
  # branches are pushed. Non-fatal — a missing token / unmatched version warns and skips.
155
152
  - name: Set the Jira Fix Version on the branding ticket
156
- when: manage_jira_fix_version | bool
153
+ when: manage_jira_fix_version | bool and not (dry_run | default(false) | bool)
157
154
  block:
158
155
  - name: Fetch the Jira token from 1Password
159
156
  when: (jira_token | default('') | length) == 0
@@ -322,3 +319,40 @@
322
319
  - name: Summary of pull requests
323
320
  ansible.builtin.debug:
324
321
  msg: "{{ pr_results | default([]) }}"
322
+
323
+ # --- Consul: publish the generated configs.ts to the brand's crm_env KV --------------
324
+ # configs.ts is gitignored (not committed) — it is the env's CRM config, so push it to Consul
325
+ # (Antelope/<brand>/crm_env) in addition to the PR body. Non-fatal: warns if the token lacks write.
326
+ - name: Read generated configs.ts for Consul crm_env
327
+ when: manage_consul_crm_env | default(true) | bool
328
+ ansible.builtin.slurp:
329
+ src: "{{ work_dir }}/src/configs/configs.ts"
330
+ register: crm_configs_ts_final
331
+ failed_when: false
332
+
333
+ - name: Update Consul crm_env with the generated configs.ts
334
+ when:
335
+ - manage_consul_crm_env | default(true) | bool
336
+ - crm_configs_ts_final is not skipped
337
+ - (crm_configs_ts_final.content | default('') | length) > 0
338
+ - (consul_token | default('') | length) > 0
339
+ - not (dry_run | default(false) | bool)
340
+ ansible.builtin.uri:
341
+ url: "{{ consul_url }}/v1/kv/{{ crm_env_consul_key }}"
342
+ method: PUT
343
+ headers:
344
+ X-Consul-Token: "{{ consul_token }}"
345
+ body: "{{ crm_configs_ts_final.content | b64decode }}"
346
+ status_code: [200]
347
+ register: consul_crm_env_put
348
+ failed_when: false
349
+ no_log: false
350
+
351
+ - name: Confirm the Consul crm_env update
352
+ when: manage_consul_crm_env | default(true) | bool and (consul_crm_env_put is defined)
353
+ ansible.builtin.debug:
354
+ msg: >-
355
+ {{ 'Updated Consul ' ~ crm_env_consul_key
356
+ if (consul_crm_env_put.status | default(0)) == 200
357
+ else 'Consul crm_env NOT updated (HTTP ' ~ (consul_crm_env_put.status | default('n/a'))
358
+ ~ ') - token may lack write on ' ~ crm_env_consul_key }}
@@ -48,17 +48,35 @@
48
48
  '--brand_id', antelope_id_norm,
49
49
  '--brand_name', brand_name,
50
50
  '--logo_url', logo_url,
51
- '--favicon_url', favicon_url,
52
51
  '--languages', languages_csv,
53
- '--fcm_sender_id', (fcm_sender_id | string),
54
- '--type', crm_type]
52
+ '--type', crm_type,
53
+ '--affiliates_api_url', affiliates_api_url,
54
+ '--arctic_brand', (arctic_brand | default(false) | string | lower)]
55
55
  + (['--color', color] if (color | default('') | length) > 0 else [])
56
- + (['--base_url', base_url] if (base_url | default('') | length) > 0 else []) }}
56
+ + (['--base_url', base_url] if (base_url | default('') | length) > 0 else [])
57
+ + (['--favicon', favicon] if (favicon | default('') | length) > 0 else []) }}
57
58
  chdir: "{{ work_dir }}"
58
59
  environment:
59
60
  CI: "true"
60
61
  changed_when: true
61
62
 
63
+ - name: "[{{ dest_branch }}] Read generated configs.ts (gitignored) for the PR body / Consul"
64
+ ansible.builtin.slurp:
65
+ src: "{{ work_dir }}/src/configs/configs.ts"
66
+ register: crm_configs_ts
67
+ failed_when: false
68
+
69
+ - name: "[{{ dest_branch }}] Compose PR description (ticket + configs.ts for Consul copy-paste)"
70
+ ansible.builtin.set_fact:
71
+ pr_description: |-
72
+ {{ jira_key_crm_branding }}: On-Boarding {{ brand_name }} CRM Branding
73
+
74
+ configs.ts — copy into Consul `{{ crm_env_consul_key }}`:
75
+
76
+ ```ts
77
+ {{ crm_configs_ts.content | default('') | b64decode }}
78
+ ```
79
+
62
80
  - name: "[{{ dest_branch }}] Stage changes"
63
81
  ansible.builtin.command:
64
82
  cmd: git add -A
@@ -79,15 +97,20 @@
79
97
  git -c user.name="{{ git_user_name }}" -c user.email="{{ git_user_email }}"
80
98
  commit -m "{{ jira_key_crm_branding }}: On-Boarding {{ brand_name }} CRM Branding"
81
99
  chdir: "{{ work_dir }}"
82
- when: staged.rc == 1
100
+ when:
101
+ - staged.rc == 1
102
+ - not (dry_run | default(false) | bool)
83
103
 
84
104
  - name: "[{{ dest_branch }}] Push source branch"
85
105
  ansible.builtin.command:
86
106
  cmd: "git push -u origin {{ source_branch }}"
87
107
  chdir: "{{ work_dir }}"
88
- when: staged.rc == 1
108
+ when:
109
+ - staged.rc == 1
110
+ - not (dry_run | default(false) | bool)
89
111
 
90
112
  - name: "[{{ dest_branch }}] Open pull request"
113
+ when: not (dry_run | default(false) | bool)
91
114
  ansible.builtin.uri:
92
115
  url: "{{ api_base }}/repositories/{{ workspace }}/{{ repo_slug }}/pullrequests"
93
116
  method: POST
@@ -97,7 +120,7 @@
97
120
  body_format: json
98
121
  body:
99
122
  title: "{{ jira_key_crm_branding }}: On-Boarding {{ brand_name }} CRM Branding"
100
- description: "{{ jira_key_crm_branding }}: On-Boarding {{ brand_name }} CRM Branding"
123
+ description: "{{ pr_description }}"
101
124
  source:
102
125
  branch:
103
126
  name: "{{ source_branch }}"
@@ -111,6 +134,7 @@
111
134
  register: pr_response
112
135
 
113
136
  - name: "[{{ dest_branch }}] Record PR result"
137
+ when: not (dry_run | default(false) | bool)
114
138
  ansible.builtin.set_fact:
115
139
  pr_results: >-
116
140
  {{ (pr_results | default([])) + [{
@@ -0,0 +1,47 @@
1
+ # Onboarding runner image for the self-service/onboarding/onboarding-crm Jenkins pipeline.
2
+ # Built on the fly by the declarative `agent { dockerfile { … } }` on a docker-capable fleet
3
+ # agent (no registry/ECR). Bakes in the pinned toolchain the onboard_crm role needs:
4
+ # ansible + community.general, node/npx (for `npx antelope-cli`), git + ssh (push to web-crm),
5
+ # and the 1Password CLI (op, reads the bitbucket/consul/jira tokens from the `jenkins` vault
6
+ # via the OP_SERVICE_ACCOUNT_TOKEN Jenkins credential).
7
+ FROM python:3.12-slim
8
+
9
+ ARG ANSIBLE_VERSION=10.3.0
10
+ ARG OP_VERSION=2.31.1
11
+ ARG NODE_MAJOR=20
12
+
13
+ RUN apt-get update && apt-get install -y --no-install-recommends \
14
+ git openssh-client curl ca-certificates gnupg unzip jq \
15
+ && rm -rf /var/lib/apt/lists/*
16
+
17
+ # Node.js — provides npx for `npx antelope-cli@<ver> onboarding-crm`
18
+ RUN curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash - \
19
+ && apt-get update && apt-get install -y --no-install-recommends nodejs \
20
+ && rm -rf /var/lib/apt/lists/*
21
+
22
+ # Ansible + community.general (dict_kv filter used by the role)
23
+ RUN pip install --no-cache-dir "ansible==${ANSIBLE_VERSION}" \
24
+ && ansible-galaxy collection install community.general
25
+
26
+ # 1Password CLI (op)
27
+ RUN curl -fsSL "https://cache.agilebits.com/dist/1P/op2/pkg/v${OP_VERSION}/op_linux_amd64_v${OP_VERSION}.zip" -o /tmp/op.zip \
28
+ && unzip -o /tmp/op.zip op -d /usr/local/bin/ && rm -f /tmp/op.zip && chmod +x /usr/local/bin/op
29
+
30
+ # AWS CLI v2 — used by the onboarding-logos pipeline to upload logo/favicon to the brand's
31
+ # logos-<antelope_id> S3 bucket (creds fetched from the brand's Consul folder at runtime).
32
+ RUN curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o /tmp/awscliv2.zip \
33
+ && unzip -q /tmp/awscliv2.zip -d /tmp && /tmp/aws/install && rm -rf /tmp/aws /tmp/awscliv2.zip
34
+
35
+ # Pre-trust Bitbucket's host key so `git push` over SSH is non-interactive
36
+ RUN mkdir -p /etc/ssh && ssh-keyscan bitbucket.org >> /etc/ssh/ssh_known_hosts 2>/dev/null || true
37
+
38
+ # Jenkins runs this container as `-u 1000:1000`. `op` (and other tools) resolve "the current
39
+ # user" via getpwuid(); with no /etc/passwd entry for uid 1000 it fails with
40
+ # 'couldn't start daemon … not owned by the current user' and never enters service-account
41
+ # mode. Bake a uid/gid 1000 user so getpwuid succeeds. Also make /etc/passwd group-writable
42
+ # (root group) so a differently-numbered agent uid can self-register if ever needed.
43
+ RUN groupadd -g 1000 jenkins 2>/dev/null || true \
44
+ && useradd -u 1000 -g 1000 -m -d /home/jenkins -s /bin/bash jenkins 2>/dev/null || true \
45
+ && chmod g+w /etc/passwd
46
+
47
+ WORKDIR /work
@@ -0,0 +1,105 @@
1
+ // Self-service logo/favicon upload — jenkins.xsites.xyz/job/self-service/job/onboarding/job/onboarding-logos
2
+ //
3
+ // Pipeline-as-Code (this file is the source of truth; the Jenkins job is a "Pipeline script from SCM"
4
+ // pointer at antelope-cli@master, path ansible/onboarding-logos/Jenkinsfile).
5
+ //
6
+ // Flow: operator uploads the logo + favicon image files -> the pipeline reads the brand's AWS creds
7
+ // from its Consul folder (Antelope/<brand_name>/AWS_ACCESS_KEY_ID|AWS_SECRET_ACCESS_KEY|AWS_DEFAULT_REGION)
8
+ // -> uploads them to the brand's S3 bucket logos-<antelope_id> as logo-<brand_name>.<ext> /
9
+ // favicon-<brand_name>.<ext> (the exact objects configs.ts / brand-logos.json reference).
10
+ //
11
+ // Reuses the onboarding-crm runner image (op + curl + jq + aws + uid-1000 user). Requires the
12
+ // file-parameters plugin (base64File) and the onepassword-service-account-token credential
13
+ // (op reads the Consul token from the `jenkins` vault).
14
+ pipeline {
15
+ agent {
16
+ dockerfile {
17
+ dir 'ansible/onboarding-crm'
18
+ filename 'runner.Dockerfile'
19
+ additionalBuildArgs '--pull'
20
+ }
21
+ }
22
+
23
+ options {
24
+ timestamps()
25
+ ansiColor('xterm')
26
+ disableConcurrentBuilds()
27
+ buildDiscarder(logRotator(numToKeepStr: '30'))
28
+ }
29
+
30
+ parameters {
31
+ string(name: 'brandAssetsFolder', defaultValue: '', trim: true,
32
+ description: 'Antelope ID (lowercase alphanumeric). Target bucket = logos-<brandAssetsFolder>. e.g. uzumy')
33
+ string(name: 'brand_name', defaultValue: '', trim: true,
34
+ description: 'Brand name — the Consul folder Antelope/<brand_name>/ AND the filename token logo-<brand_name>/favicon-<brand_name>. e.g. excelttra')
35
+ base64File(name: 'logo_file', description: 'Logo image file (uploaded → logo-<brand_name>.<logo_type>)')
36
+ base64File(name: 'favicon_file', description: 'Favicon image file (uploaded → favicon-<brand_name>.<favicon_type>)')
37
+ choice(name: 'logo_type', choices: ['png', 'jpg', 'jpeg', 'svg', 'webp', 'gif'], description: 'Logo file extension')
38
+ choice(name: 'favicon_type', choices: ['png', 'ico', 'jpg', 'jpeg', 'svg', 'webp'], description: 'Favicon file extension')
39
+ booleanParam(name: 'DRY_RUN', defaultValue: true,
40
+ description: 'Preview only: resolve creds + validate + aws s3 cp --dryrun (no actual upload)')
41
+ }
42
+
43
+ environment {
44
+ HOME = "${env.WORKSPACE}"
45
+ CONSUL_URL = 'https://consul.xsites.xyz'
46
+ }
47
+
48
+ stages {
49
+ stage('Validate parameters') {
50
+ steps {
51
+ script {
52
+ def IMG_EXT = ['png', 'jpg', 'jpeg', 'svg', 'webp', 'ico', 'gif']
53
+ def errs = []
54
+ if (!(params.brandAssetsFolder ==~ /^[a-z0-9]+$/))
55
+ errs << "brandAssetsFolder must be lowercase alphanumeric (the Antelope ID); got '${params.brandAssetsFolder}'"
56
+ // brand_name must be filename-safe so logo-<brand_name>.<ext> / favicon-<brand_name>.<ext> are valid keys
57
+ if (!(params.brand_name ==~ /^[A-Za-z0-9._-]+$/))
58
+ errs << "brand_name must be filename-safe (letters/digits/._-, no spaces); got '${params.brand_name}'"
59
+ if (!IMG_EXT.contains(params.logo_type)) errs << "logo_type '${params.logo_type}' not allowed"
60
+ if (!IMG_EXT.contains(params.favicon_type)) errs << "favicon_type '${params.favicon_type}' not allowed"
61
+ if (errs) error("Parameter validation failed:\n - " + errs.join("\n - "))
62
+ env.LOGO_KEY = "logo-${params.brand_name}.${params.logo_type}"
63
+ env.FAVICON_KEY = "favicon-${params.brand_name}.${params.favicon_type}"
64
+ echo "✅ Valid — bucket=logos-${params.brandAssetsFolder}, ${env.LOGO_KEY}, ${env.FAVICON_KEY}, dry_run=${params.DRY_RUN}"
65
+ currentBuild.displayName = "#${env.BUILD_NUMBER} ${params.brand_name}${params.DRY_RUN ? ' (dry-run)' : ''}"
66
+ }
67
+ }
68
+ }
69
+
70
+ stage('Upload to S3') {
71
+ steps {
72
+ withCredentials([string(credentialsId: 'onepassword-service-account-token', variable: 'OP_SERVICE_ACCOUNT_TOKEN')]) {
73
+ withFileParameter('logo_file') {
74
+ withFileParameter('favicon_file') {
75
+ sh label: 'resolve Consul AWS creds + upload', script: '''
76
+ set -eu
77
+ CT=$(op read "op://jenkins/New_Consul_Token/password")
78
+ base="$CONSUL_URL/v1/kv/Antelope/$brand_name"
79
+ AWS_ACCESS_KEY_ID=$(curl -sf -H "X-Consul-Token: $CT" "$base/AWS_ACCESS_KEY_ID?raw")
80
+ AWS_SECRET_ACCESS_KEY=$(curl -sf -H "X-Consul-Token: $CT" "$base/AWS_SECRET_ACCESS_KEY?raw")
81
+ AWS_DEFAULT_REGION=$(curl -sf -H "X-Consul-Token: $CT" "$base/AWS_DEFAULT_REGION?raw" || echo eu-west-1)
82
+ export AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_DEFAULT_REGION
83
+ if [ -z "$AWS_ACCESS_KEY_ID" ] || [ -z "$AWS_SECRET_ACCESS_KEY" ]; then
84
+ echo "ERROR: no AWS creds under Consul Antelope/$brand_name/ (AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY)"; exit 1
85
+ fi
86
+ ct() { case "$1" in png) echo image/png;; jpg|jpeg) echo image/jpeg;; svg) echo image/svg+xml;; webp) echo image/webp;; gif) echo image/gif;; ico) echo image/x-icon;; *) echo application/octet-stream;; esac; }
87
+ DRY=""; [ "$DRY_RUN" = "true" ] && DRY="--dryrun"
88
+ echo "Uploading to s3://logos-$brandAssetsFolder/ (region $AWS_DEFAULT_REGION)${DRY:+ [DRY-RUN]}"
89
+ aws s3 cp "$logo_file" "s3://logos-$brandAssetsFolder/$LOGO_KEY" --content-type "$(ct $logo_type)" $DRY
90
+ aws s3 cp "$favicon_file" "s3://logos-$brandAssetsFolder/$FAVICON_KEY" --content-type "$(ct $favicon_type)" $DRY
91
+ echo "Done: $LOGO_KEY, $FAVICON_KEY"
92
+ '''
93
+ }
94
+ }
95
+ }
96
+ }
97
+ }
98
+ }
99
+
100
+ post {
101
+ always { cleanWs() }
102
+ success { echo "logos ${params.DRY_RUN ? 'previewed' : 'uploaded'} to logos-${params.brandAssetsFolder}: ${env.LOGO_KEY}, ${env.FAVICON_KEY}" }
103
+ failure { echo 'Logo upload failed — see the validation/aws output above.' }
104
+ }
105
+ }
@@ -5,10 +5,11 @@ let brand = {
5
5
  color: '',
6
6
  logo: '',
7
7
  languages: null,
8
- favicon: '',
9
- FCMSenderId: '',
8
+ affiliatesApiUrl: '',
9
+ isArcticBrand: false,
10
10
  type: '',
11
- baseUrl: ''
11
+ baseUrl: '',
12
+ favicon: ''
12
13
  };
13
14
 
14
15
  function updateBrand(updatedBrand) {
@@ -21,20 +21,27 @@ function start() {
21
21
 
22
22
  const updateConfigFile = () => {
23
23
  const brand = brandModule.getBrand();
24
+ const id = brand.connectedBrandName;
24
25
  return `export const configs: WebCrmConfig = {
25
- baseUrl: '${brand.baseUrl}',
26
- appId: 0,
27
- type: '${brand.type}',
28
- defaultLanguage: 'en',
29
- gaTrackingID: 'UA-70456753-2',
30
- colorTheme: 'theme-${brand.connectedBrandName}',
31
- templateTheme: 'theme-template-${brand.connectedBrandName}',
32
- title: '${brand.brandName}',
33
- favicon: '${brand.favicon}',
34
- logo: '${brand.logo}',
35
- FCMSenderId: '${brand.FCMSenderId}',
36
- brandAssetsFolder: '${brand.connectedBrandName}'
37
- };`;
26
+ baseUrl: '${brand.baseUrl}',
27
+ affiliatesApiUrl: '${brand.affiliatesApiUrl}',
28
+ appId: 0,
29
+ type: '${brand.type}',
30
+ title: '${brand.brandName} CRM',
31
+ colorTheme: 'theme-${id}',
32
+ templateTheme: 'theme-template-${id}',
33
+ brandAssetsFolder: '${id}',
34
+ devBrandName: '${id}',
35
+ favicon: '${brand.favicon || `https://logos-${id}.s3.eu-west-1.amazonaws.com/favicon-${brand.brandName}.png`}',
36
+ gaTrackingID: 'UA-70456753-2',
37
+ FCMSenderId: '',
38
+ isFnsStatusActive: true,
39
+ showTradingInfo: true,
40
+ CrmAvailableCurrencies: ['USD'],
41
+ showNewClientCard: true,
42
+ newAppSubdomain: 'ant-crm',
43
+ isArcticBrand: ${brand.isArcticBrand ? 'true' : 'false'}
44
+ };`;
38
45
  };
39
46
 
40
47
  module.exports = { start };
@@ -8,7 +8,7 @@ const createConfig = require('./createConfig.js');
8
8
  const brandLogos = require('./brandLogos.js');
9
9
  const brandLocations = require('./brandLocations.js');
10
10
 
11
- const ALLOWED_TYPES = ['CORP_FL', 'IB_FL', 'CORP_AFF_FL', 'CORP_AFF_IB_PORTAL', 'CORP_AFF_MSQ'];
11
+ const ALLOWED_TYPES = ['CORP_FL', 'IB_FL', 'CORP_AFF_FL'];
12
12
  const DEFAULT_BASE_URL = 'https://dev-apicrm.antelopesystem.com/SignalsCRM/';
13
13
 
14
14
  const HELP = `
@@ -29,10 +29,15 @@ Options:
29
29
  --color <hex> Brand hex color, e.g. "#1a2b3c". Empty => default palette ($corp)
30
30
  --logo_url <url> Logo image URL (black background) (required)
31
31
  --languages <csv> 2-letter language codes, comma-separated, e.g. "en,ar" (required)
32
- --favicon_url <url> Favicon image URL configs.ts favicon (required)
33
- --fcm_sender_id <id> Firebase FCMSenderId configs.ts (required)
32
+ --affiliates_api_url configs.ts affiliatesApiUrl the FE host, e.g. "apife.<ca-domain>"
33
+ (apife.excelttra.com). Not derivable from base_url (that's the BE domain).
34
+ --arctic_brand <t|f> configs.ts isArcticBrand. Default: false
34
35
  --type <type> One of: ${ALLOWED_TYPES.join(', ')} → configs.ts (required)
35
36
  --base_url <url> configs.ts baseUrl. Default: ${DEFAULT_BASE_URL}
37
+ --favicon <url> configs.ts favicon. Empty => derived (logos-<id>/favicon-<name>.png)
38
+
39
+ Note: --favicon defaults to the derived logos-<id>/favicon-<name>.png; FCMSenderId is always '';
40
+ both are placeholders that get hand-edited per brand. configs.ts is gitignored (Consul copy-paste).
36
41
  -h, --help Show this help
37
42
 
38
43
  Run from the root of a web-crm checkout — files are written relative to the current directory.
@@ -44,10 +49,11 @@ const FLAGS = {
44
49
  '--color': 'color',
45
50
  '--logo_url': 'logo',
46
51
  '--languages': 'languages',
47
- '--favicon_url': 'favicon',
48
- '--fcm_sender_id': 'fcmSenderId',
52
+ '--affiliates_api_url': 'affiliatesApiUrl',
53
+ '--arctic_brand': 'arcticBrand',
49
54
  '--type': 'type',
50
- '--base_url': 'baseUrl'
55
+ '--base_url': 'baseUrl',
56
+ '--favicon': 'favicon'
51
57
  };
52
58
 
53
59
  function parseArgs(argv) {
@@ -97,9 +103,7 @@ async function run() {
97
103
  : (interactive ? await ask(rl, `Please set a color for "${connectedBrandName}", use hex value, leave empty for default.\n`) : '');
98
104
 
99
105
  const logo = await resolve(opts, 'logo', 'Please enter the URL to the logo image suitable for a black background:\n', interactive, rl);
100
- const favicon = await resolve(opts, 'favicon', 'Please provide the url to your favicon image:\n', interactive, rl);
101
106
  const languagesInput = await resolve(opts, 'languages', 'Enter the languages (2-letter codes) separated by commas:\n', interactive, rl);
102
- const fcmSenderId = await resolve(opts, 'fcmSenderId', 'Enter the FCMSenderId value (see https://xsites.atlassian.net/wiki/spaces/IKB/pages/777650347/Onboarding+Branding+Manual+for+devs#Firebase): ', interactive, rl);
103
107
  const typeInput = await resolve(opts, 'type', `Enter the type value (allowed values: ${ALLOWED_TYPES.join(', ')}): `, interactive, rl);
104
108
 
105
109
  // baseUrl is optional (defaults to the dev URL).
@@ -107,14 +111,20 @@ async function run() {
107
111
  ? opts.baseUrl
108
112
  : (interactive ? (await ask(rl, `Enter the configs.ts baseUrl (leave empty for ${DEFAULT_BASE_URL}):\n`)) || DEFAULT_BASE_URL : DEFAULT_BASE_URL);
109
113
 
114
+ // affiliatesApiUrl (FE host, e.g. apife.<ca-domain>) → configs.ts. Explicit input; NOT
115
+ // derivable from baseUrl (baseUrl is the BE/CRM domain). configs.ts favicon is derived
116
+ // from the id + brand_name; FCMSenderId is always ''. isArcticBrand defaults false.
117
+ const affiliatesApiUrl = opts.affiliatesApiUrl || '';
118
+ const isArcticBrand = String(opts.arcticBrand).toLowerCase() === 'true';
119
+ // favicon: explicit override, else createConfig derives logos-<id>/favicon-<name>.png
120
+ const favicon = opts.favicon || '';
121
+
110
122
  // Validate required fields.
111
123
  const missing = [];
112
124
  if (!connectedBrandName) missing.push('--brand_id');
113
125
  if (!brandName) missing.push('--brand_name');
114
126
  if (!logo) missing.push('--logo_url');
115
- if (!favicon) missing.push('--favicon_url');
116
127
  if (!languagesInput) missing.push('--languages');
117
- if (!fcmSenderId) missing.push('--fcm_sender_id');
118
128
  if (!typeInput) missing.push('--type');
119
129
  if (missing.length) {
120
130
  console.error(chalk.red(`Error: missing required value(s): ${missing.join(', ')}`));
@@ -144,10 +154,11 @@ async function run() {
144
154
  color: color || '',
145
155
  logo,
146
156
  languages,
147
- favicon,
148
- FCMSenderId: fcmSenderId,
157
+ affiliatesApiUrl,
158
+ isArcticBrand,
149
159
  type,
150
- baseUrl
160
+ baseUrl,
161
+ favicon
151
162
  });
152
163
 
153
164
  console.log(chalk.bold(chalk.yellow(`\nOnboarding "${brandName}" (id: ${connectedBrandName})`)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antelope-cli",
3
- "version": "1.2.4",
3
+ "version": "1.2.6",
4
4
  "description": "CLI-Tool for automating processes for Antelope-Systems ",
5
5
  "main": "index.js",
6
6
  "bin": {