antelope-cli 1.2.5 → 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.
- package/ansible/onboarding-ca/roles/onboard_ca/defaults/main.yml +2 -2
- package/ansible/onboarding-crm/Jenkinsfile +211 -0
- package/ansible/onboarding-crm/README.md +1 -1
- package/ansible/onboarding-crm/onboard-crm.yml +1 -1
- package/ansible/onboarding-crm/roles/onboard_crm/defaults/main.yml +5 -4
- package/ansible/onboarding-crm/roles/onboard_crm/tasks/main.yml +2 -1
- package/ansible/onboarding-crm/roles/onboard_crm/tasks/per_branch.yml +10 -3
- package/ansible/onboarding-crm/runner.Dockerfile +47 -0
- package/ansible/onboarding-logos/Jenkinsfile +105 -0
- package/onboarding-crm/brand.js +2 -1
- package/onboarding-crm/createConfig.js +1 -1
- package/onboarding-crm/index.js +9 -4
- package/package.json +1 -1
|
@@ -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
|
+
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: "
|
|
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
|
|
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,7 +44,7 @@
|
|
|
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
|
|
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)"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
# Pinned published antelope-cli version that provides the `onboarding-crm` command.
|
|
3
|
-
antelope_cli_version: "1.2.
|
|
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"
|
|
@@ -13,6 +13,7 @@ work_dir: "/tmp/onboard-crm/web-crm"
|
|
|
13
13
|
antelope_id: ""
|
|
14
14
|
color: "" # --color hex; blank => default palette
|
|
15
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
|
|
16
17
|
languages: "" # --languages: CSV "en,ar" or a YAML list [en, ar]
|
|
17
18
|
crm_type: "" # --type: one of allowed_crm_types (per-brand; from group_vars derivation)
|
|
18
19
|
# baseUrl uses the BE/CRM domain; affiliatesApiUrl uses the FE/CA domain (fe_server_url_list.0).
|
|
@@ -23,13 +24,13 @@ arctic_brand: false # --arctic_brand => configs.ts isAr
|
|
|
23
24
|
# with write on this path. brand_name = the Consul namespace segment (e.g. Antelope/excelttra/crm_env).
|
|
24
25
|
crm_env_consul_key: "Antelope/{{ brand_name }}/crm_env"
|
|
25
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
|
|
26
29
|
# Kept in sync with ALLOWED_TYPES in antelope-cli/onboarding-crm/index.js.
|
|
27
30
|
allowed_crm_types:
|
|
28
31
|
- CORP_FL
|
|
29
32
|
- IB_FL
|
|
30
33
|
- CORP_AFF_FL
|
|
31
|
-
- CORP_AFF_IB_PORTAL
|
|
32
|
-
- CORP_AFF_MSQ
|
|
33
34
|
|
|
34
35
|
# Reviewers for the PR. Empty => auto-fetched from the repo's Bitbucket "Default reviewers"
|
|
35
36
|
# config (the authenticating account is excluded automatically). Override with an explicit
|
|
@@ -66,7 +67,7 @@ dest_branches: ""
|
|
|
66
67
|
# Require a signed-in `op` CLI on the control node (op signin, or OP_SERVICE_ACCOUNT_TOKEN).
|
|
67
68
|
# Service-account auth requires an explicit vault for `op item get` by name (op errors
|
|
68
69
|
# with "a vault query must be provided" otherwise) — the items live in the DevOps vault.
|
|
69
|
-
onepassword_vault: "
|
|
70
|
+
onepassword_vault: "jenkins"
|
|
70
71
|
onepassword_bitbucket_item: "Bitbucket API - jenkins@xsites.co.il"
|
|
71
72
|
onepassword_bitbucket_field: "credential"
|
|
72
73
|
onepassword_consul_item: "New_Consul_Token"
|
|
@@ -150,7 +150,7 @@
|
|
|
150
150
|
# Fix Version, so stamp the release version (V<version/prod>) on the ticket BEFORE the
|
|
151
151
|
# branches are pushed. Non-fatal — a missing token / unmatched version warns and skips.
|
|
152
152
|
- name: Set the Jira Fix Version on the branding ticket
|
|
153
|
-
when: manage_jira_fix_version | bool
|
|
153
|
+
when: manage_jira_fix_version | bool and not (dry_run | default(false) | bool)
|
|
154
154
|
block:
|
|
155
155
|
- name: Fetch the Jira token from 1Password
|
|
156
156
|
when: (jira_token | default('') | length) == 0
|
|
@@ -336,6 +336,7 @@
|
|
|
336
336
|
- crm_configs_ts_final is not skipped
|
|
337
337
|
- (crm_configs_ts_final.content | default('') | length) > 0
|
|
338
338
|
- (consul_token | default('') | length) > 0
|
|
339
|
+
- not (dry_run | default(false) | bool)
|
|
339
340
|
ansible.builtin.uri:
|
|
340
341
|
url: "{{ consul_url }}/v1/kv/{{ crm_env_consul_key }}"
|
|
341
342
|
method: PUT
|
|
@@ -53,7 +53,8 @@
|
|
|
53
53
|
'--affiliates_api_url', affiliates_api_url,
|
|
54
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"
|
|
@@ -96,15 +97,20 @@
|
|
|
96
97
|
git -c user.name="{{ git_user_name }}" -c user.email="{{ git_user_email }}"
|
|
97
98
|
commit -m "{{ jira_key_crm_branding }}: On-Boarding {{ brand_name }} CRM Branding"
|
|
98
99
|
chdir: "{{ work_dir }}"
|
|
99
|
-
when:
|
|
100
|
+
when:
|
|
101
|
+
- staged.rc == 1
|
|
102
|
+
- not (dry_run | default(false) | bool)
|
|
100
103
|
|
|
101
104
|
- name: "[{{ dest_branch }}] Push source branch"
|
|
102
105
|
ansible.builtin.command:
|
|
103
106
|
cmd: "git push -u origin {{ source_branch }}"
|
|
104
107
|
chdir: "{{ work_dir }}"
|
|
105
|
-
when:
|
|
108
|
+
when:
|
|
109
|
+
- staged.rc == 1
|
|
110
|
+
- not (dry_run | default(false) | bool)
|
|
106
111
|
|
|
107
112
|
- name: "[{{ dest_branch }}] Open pull request"
|
|
113
|
+
when: not (dry_run | default(false) | bool)
|
|
108
114
|
ansible.builtin.uri:
|
|
109
115
|
url: "{{ api_base }}/repositories/{{ workspace }}/{{ repo_slug }}/pullrequests"
|
|
110
116
|
method: POST
|
|
@@ -128,6 +134,7 @@
|
|
|
128
134
|
register: pr_response
|
|
129
135
|
|
|
130
136
|
- name: "[{{ dest_branch }}] Record PR result"
|
|
137
|
+
when: not (dry_run | default(false) | bool)
|
|
131
138
|
ansible.builtin.set_fact:
|
|
132
139
|
pr_results: >-
|
|
133
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
|
+
}
|
package/onboarding-crm/brand.js
CHANGED
|
@@ -32,7 +32,7 @@ const updateConfigFile = () => {
|
|
|
32
32
|
templateTheme: 'theme-template-${id}',
|
|
33
33
|
brandAssetsFolder: '${id}',
|
|
34
34
|
devBrandName: '${id}',
|
|
35
|
-
favicon: 'https://logos-${id}.s3.eu-west-1.amazonaws.com/favicon-${brand.brandName}.png',
|
|
35
|
+
favicon: '${brand.favicon || `https://logos-${id}.s3.eu-west-1.amazonaws.com/favicon-${brand.brandName}.png`}',
|
|
36
36
|
gaTrackingID: 'UA-70456753-2',
|
|
37
37
|
FCMSenderId: '',
|
|
38
38
|
isFnsStatusActive: true,
|
package/onboarding-crm/index.js
CHANGED
|
@@ -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'
|
|
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 = `
|
|
@@ -34,8 +34,9 @@ Options:
|
|
|
34
34
|
--arctic_brand <t|f> configs.ts isArcticBrand. Default: false
|
|
35
35
|
--type <type> One of: ${ALLOWED_TYPES.join(', ')} → configs.ts (required)
|
|
36
36
|
--base_url <url> configs.ts baseUrl. Default: ${DEFAULT_BASE_URL}
|
|
37
|
+
--favicon <url> configs.ts favicon. Empty => derived (logos-<id>/favicon-<name>.png)
|
|
37
38
|
|
|
38
|
-
Note:
|
|
39
|
+
Note: --favicon defaults to the derived logos-<id>/favicon-<name>.png; FCMSenderId is always '';
|
|
39
40
|
both are placeholders that get hand-edited per brand. configs.ts is gitignored (Consul copy-paste).
|
|
40
41
|
-h, --help Show this help
|
|
41
42
|
|
|
@@ -51,7 +52,8 @@ const FLAGS = {
|
|
|
51
52
|
'--affiliates_api_url': 'affiliatesApiUrl',
|
|
52
53
|
'--arctic_brand': 'arcticBrand',
|
|
53
54
|
'--type': 'type',
|
|
54
|
-
'--base_url': 'baseUrl'
|
|
55
|
+
'--base_url': 'baseUrl',
|
|
56
|
+
'--favicon': 'favicon'
|
|
55
57
|
};
|
|
56
58
|
|
|
57
59
|
function parseArgs(argv) {
|
|
@@ -114,6 +116,8 @@ async function run() {
|
|
|
114
116
|
// from the id + brand_name; FCMSenderId is always ''. isArcticBrand defaults false.
|
|
115
117
|
const affiliatesApiUrl = opts.affiliatesApiUrl || '';
|
|
116
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 || '';
|
|
117
121
|
|
|
118
122
|
// Validate required fields.
|
|
119
123
|
const missing = [];
|
|
@@ -153,7 +157,8 @@ async function run() {
|
|
|
153
157
|
affiliatesApiUrl,
|
|
154
158
|
isArcticBrand,
|
|
155
159
|
type,
|
|
156
|
-
baseUrl
|
|
160
|
+
baseUrl,
|
|
161
|
+
favicon
|
|
157
162
|
});
|
|
158
163
|
|
|
159
164
|
console.log(chalk.bold(chalk.yellow(`\nOnboarding "${brandName}" (id: ${connectedBrandName})`)));
|