@docupace/pipeline 1.0.121 → 1.0.122-connect.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,6 +7,291 @@ clone:
7
7
  definitions:
8
8
  caches:
9
9
  pnpm: ~/.pnpm-store
10
+
11
+ # Pipelines under `definitions.pipelines` are the ones other repos can pull in
12
+ # with `import: pipeline-shared-scripts:master:<name>`. Names under
13
+ # `pipelines.custom` below are local to this repo and are NOT importable.
14
+ pipelines:
15
+ xray-scan: &xray-scan-pipeline
16
+ - step:
17
+ name: JFrog Xray Scan (dependencies + Docker image)
18
+ runs-on:
19
+ - self.hosted
20
+ - linux.shell
21
+ - onprem
22
+ script:
23
+ - curl -fL https://install-cli.jfrog.io | sh
24
+ - jf --version
25
+ - |
26
+ # JFROG_XRAY_URL is stored without a scheme in some workspaces; the
27
+ # jf CLI needs an absolute URL or every call fails with
28
+ # "unsupported protocol scheme".
29
+ case "${JFROG_XRAY_URL}" in
30
+ http://*|https://*) XRAY_URL="${JFROG_XRAY_URL}" ;;
31
+ "") echo "JFROG_XRAY_URL is not set — cannot scan."; exit 1 ;;
32
+ *) XRAY_URL="https://${JFROG_XRAY_URL}" ;;
33
+ esac
34
+ export XRAY_URL
35
+ echo "Using Xray URL: ${XRAY_URL}"
36
+ # Self-hosted runners keep ~/.jfrog between builds, so a server left
37
+ # behind by an earlier failed run makes `config add` fail. Drop it first.
38
+ - jf config remove scan1 --quiet || true
39
+ - jf config add scan1 --url="${XRAY_URL}" --user=$JFROG_XRAY_USER --access-token=$JFROG_XRAY_PASSWORD --interactive=false
40
+ - jf config use scan1
41
+ - jf c show
42
+ - mkdir -p xray-report
43
+ - |
44
+ # A repo can hold more than one app (e.g. a frontend and a backend
45
+ # with separate lockfiles and Dockerfiles), and one context would
46
+ # only ever scan one of them. SCAN_TARGETS lists them all:
47
+ #
48
+ # label:auditDir:buildContext:dockerfile[;label:...]
49
+ #
50
+ # An empty auditDir skips the dependency scan for that target; an
51
+ # empty buildContext skips the image scan. dockerfile defaults to
52
+ # <buildContext>/Dockerfile. Example for two apps in one repo:
53
+ #
54
+ # SCAN_TARGETS="frontend:atlas-mui:atlas-mui:atlas-mui/Dockerfile;backend:intel-server:.:Dockerfile"
55
+ #
56
+ # With SCAN_TARGETS unset we fall back to the older single-target
57
+ # XRAY_BUILD_CONTEXT / XRAY_DOCKERFILE_PATH variables.
58
+ if [ -z "${SCAN_TARGETS:-}" ]; then
59
+ CTX="${XRAY_BUILD_CONTEXT:-.}"
60
+ SCAN_TARGETS="app:${CTX}:${CTX}:${XRAY_DOCKERFILE_PATH:-${CTX}/Dockerfile}"
61
+ echo "SCAN_TARGETS not set — scanning a single target: ${SCAN_TARGETS}"
62
+ fi
63
+ printf '%s' "${SCAN_TARGETS}" > /tmp/scan-targets
64
+ - |
65
+ # These runners inherit DOCKER_HOST=tcp://localhost:2375 (no daemon
66
+ # there) and DOCKER_CONTEXT is reserved by the docker CLI for
67
+ # selecting a named context — clear both so we use the local socket.
68
+ export DOCKER_HOST=""
69
+ unset DOCKER_CONTEXT
70
+ SHORT_SHA="$(printf '%s' "${BITBUCKET_COMMIT:-local}" | cut -c1-7)"
71
+ WORKDIR="$(pwd)"
72
+ # xray.txt contains the human-readable table output; kept as an
73
+ # artifact for quick log reading. bin/xray.sh reads simple-json.
74
+ : > xray.txt
75
+ # One "label:file" per line, consumed by the HTML report and the
76
+ # Code Insights publisher below.
77
+ : > /tmp/xray-report-args
78
+ # Split SCAN_TARGETS on ';' into positional params. Piping into a
79
+ # `while read` loop instead would let a command in the body (docker,
80
+ # jf) consume the rest of the list from stdin — which silently scans
81
+ # only the first target.
82
+ OLD_IFS="${IFS}"
83
+ IFS=';'
84
+ set -- $(cat /tmp/scan-targets)
85
+ IFS="${OLD_IFS}"
86
+ for target in "$@"; do
87
+ [ -z "${target}" ] && continue
88
+ label="$(printf '%s' "${target}" | cut -d: -f1)"
89
+ audit_dir="$(printf '%s' "${target}" | cut -d: -f2)"
90
+ build_ctx="$(printf '%s' "${target}" | cut -d: -f3)"
91
+ dockerfile="$(printf '%s' "${target}" | cut -d: -f4)"
92
+ [ -z "${label}" ] && label="app"
93
+ # Labels reach filenames and image tags, so keep them tame.
94
+ slug="$(printf '%s' "${label}" | tr -c 'A-Za-z0-9._-' '-')"
95
+ [ -n "${build_ctx}" ] && [ -z "${dockerfile}" ] && dockerfile="${build_ctx}/Dockerfile"
96
+
97
+ echo "=============================================================="
98
+ echo "Target '${label}': deps='${audit_dir:-none}' image='${dockerfile:-none}'"
99
+ echo "=============================================================="
100
+
101
+ if [ -z "${audit_dir}" ]; then
102
+ echo "No audit directory for '${label}' — skipping dependency scan."
103
+ elif [ ! -d "${audit_dir}" ]; then
104
+ echo "Audit directory '${audit_dir}' does not exist — skipping dependency scan for '${label}'."
105
+ else
106
+ # `jf audit` resolves the package manager from the working
107
+ # directory, so cd in rather than passing a path — and write to
108
+ # absolute paths so output lands back in xray-report/.
109
+ ( cd "${audit_dir}" \
110
+ && jf audit --format=table > "${WORKDIR}/xray-report/${slug}-audit.txt" 2>&1 \
111
+ ; jf audit --format=simple-json > "${WORKDIR}/xray-report/${slug}-audit.json" 2>/dev/null ) || true
112
+ echo "--- ${label}: dependency scan ---"
113
+ cat "xray-report/${slug}-audit.txt" || true
114
+ {
115
+ echo "### ${label} — dependencies (${audit_dir})"
116
+ cat "xray-report/${slug}-audit.txt" 2>/dev/null
117
+ } >> xray.txt
118
+ echo "${label} deps:xray-report/${slug}-audit.json" >> /tmp/xray-report-args
119
+ fi
120
+
121
+ if [ -z "${dockerfile}" ]; then
122
+ echo "No Dockerfile configured for '${label}' — skipping image scan."
123
+ elif [ ! -f "${dockerfile}" ]; then
124
+ echo "No Dockerfile at '${dockerfile}' — skipping image scan for '${label}'."
125
+ else
126
+ SCAN_IMAGE="${BITBUCKET_REPO_SLUG:-image}-${slug}:xray-${SHORT_SHA}"
127
+ echo "Building ${SCAN_IMAGE} from ${dockerfile} (context ${build_ctx})..."
128
+ # --load is buildx-only; the legacy builder loads locally anyway.
129
+ LOAD_FLAG=""
130
+ docker buildx version >/dev/null 2>&1 && LOAD_FLAG="--load"
131
+ if docker build "${build_ctx}" --file "${dockerfile}" --tag "${SCAN_IMAGE}" ${LOAD_FLAG}; then
132
+ jf docker scan "${SCAN_IMAGE}" --format=table > "xray-report/${slug}-docker.txt" 2>&1 || true
133
+ echo "--- ${label}: image scan ---"
134
+ cat "xray-report/${slug}-docker.txt" || true
135
+ jf docker scan "${SCAN_IMAGE}" --format=simple-json > "xray-report/${slug}-docker.json" 2>/dev/null || true
136
+ {
137
+ echo "### ${label} — image ${SCAN_IMAGE}"
138
+ cat "xray-report/${slug}-docker.txt" 2>/dev/null
139
+ } >> xray.txt
140
+ echo "${label} image:xray-report/${slug}-docker.json" >> /tmp/xray-report-args
141
+ docker rmi -f "${SCAN_IMAGE}" || true
142
+ else
143
+ echo "Docker build failed for '${label}' — no image scan for this target."
144
+ fi
145
+ fi
146
+ done
147
+ cp xray.txt xray-report/xray-audit.txt || true
148
+ cp /tmp/xray-report-args xray-report/scanned-targets.txt || true
149
+ - |
150
+ # The report/notify scripts live in bin/ of this shared repo, but an
151
+ # import only brings the YAML across — so consumers have to fetch
152
+ # them. The repo is private, so anonymous https fails; try each auth
153
+ # method the runners actually have, in order of preference.
154
+ # Local bin/ is used only when all three scripts are present (i.e.
155
+ # we're running inside the shared repo itself).
156
+ XRAY_BIN=""
157
+ if [ -f ./bin/xray.sh ] && [ -f ./bin/xray-html-report.sh ] && [ -f ./bin/xray-bitbucket-insights.sh ]; then
158
+ XRAY_BIN=./bin
159
+ echo "Using shared scripts from ./bin"
160
+ else
161
+ DEST=/tmp/pipeline-shared-scripts
162
+ rm -rf "${DEST}"
163
+ # Parallel arrays of label + URL; only the label is ever printed,
164
+ # so credentials never reach the build log.
165
+ LABELS=""; URLS=""
166
+ add_src() { LABELS="${LABELS}${LABELS:+|}$1"; URLS="${URLS}${URLS:+|}$2"; }
167
+ [ -n "${SCRIPTS_REPO:-}" ] && add_src "SCRIPTS_REPO" "${SCRIPTS_REPO}"
168
+ if [ -n "${BITBUCKET_USERNAME:-}" ] && [ -n "${BITBUCKET_APP_PASSWORD:-}" ]; then
169
+ add_src "BITBUCKET_USERNAME+APP_PASSWORD" "https://${BITBUCKET_USERNAME}:${BITBUCKET_APP_PASSWORD}@bitbucket.org/dp-cloud/pipeline-shared-scripts.git"
170
+ fi
171
+ [ -n "${BITBUCKET_STEP_OAUTH_TOKEN:-}" ] && add_src "step OAuth token" "https://x-token-auth:${BITBUCKET_STEP_OAUTH_TOKEN}@bitbucket.org/dp-cloud/pipeline-shared-scripts.git"
172
+ add_src "ssh" "git@bitbucket.org:dp-cloud/pipeline-shared-scripts.git"
173
+ i=1
174
+ while :; do
175
+ label="$(printf '%s' "${LABELS}" | cut -d'|' -f${i})"
176
+ url="$(printf '%s' "${URLS}" | cut -d'|' -f${i})"
177
+ [ -z "${label}" ] && break
178
+ echo "Fetching shared scripts via ${label}..."
179
+ # GIT_TERMINAL_PROMPT=0 so a private repo fails fast instead of
180
+ # blocking on a username prompt.
181
+ if GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=accept-new -o BatchMode=yes" \
182
+ git clone --depth 1 --quiet "${url}" "${DEST}" 2>/dev/null; then
183
+ XRAY_BIN="${DEST}/bin"
184
+ echo "Fetched shared scripts via ${label}."
185
+ break
186
+ fi
187
+ rm -rf "${DEST}"
188
+ i=$((i + 1))
189
+ done
190
+ if [ -z "${XRAY_BIN}" ]; then
191
+ echo "Could not fetch shared scripts (tried: $(printf '%s' "${LABELS}" | tr '|' ' ')). HTML report, Code Insights and Slack notify will be skipped; JSON/table artifacts are still produced."
192
+ fi
193
+ fi
194
+ [ -n "${XRAY_BIN}" ] && [ -f "${XRAY_BIN}/xray.sh" ] && cp "${XRAY_BIN}/xray.sh" ./xray-notify.sh
195
+ echo "${XRAY_BIN}" > /tmp/xray-bin-path
196
+ # Render the browsable HTML report (and PDF when XRAY_REPORT_PDF=true).
197
+ # Both consumers take one "label:file" argument per scan that ran,
198
+ # so every target shows up as its own section / report entry.
199
+ - |
200
+ XRAY_BIN="$(cat /tmp/xray-bin-path 2>/dev/null || echo '')"
201
+ if [ -n "${XRAY_BIN}" ] && [ -f "${XRAY_BIN}/xray-html-report.sh" ]; then
202
+ set -- xray-report/xray-report.html
203
+ while IFS= read -r a; do [ -n "${a}" ] && set -- "$@" "${a}"; done < /tmp/xray-report-args
204
+ bash "${XRAY_BIN}/xray-html-report.sh" "$@" || true
205
+ else
206
+ echo "xray-html-report.sh unavailable — skipping HTML report."
207
+ fi
208
+ # Publish to Bitbucket Code Insights: a report on the commit plus
209
+ # inline annotations in the PR diff. Never fails the build.
210
+ - |
211
+ XRAY_BIN="$(cat /tmp/xray-bin-path 2>/dev/null || echo '')"
212
+ if [ -n "${XRAY_BIN}" ] && [ -f "${XRAY_BIN}/xray-bitbucket-insights.sh" ]; then
213
+ set --
214
+ while IFS= read -r a; do [ -n "${a}" ] && set -- "$@" "${a}"; done < /tmp/xray-report-args
215
+ if [ "$#" -gt 0 ]; then
216
+ bash "${XRAY_BIN}/xray-bitbucket-insights.sh" "$@" || true
217
+ else
218
+ echo "No scan output to publish — skipping Code Insights."
219
+ fi
220
+ else
221
+ echo "xray-bitbucket-insights.sh unavailable — skipping Code Insights."
222
+ fi
223
+ - |
224
+ if [ -f ./xray-notify.sh ]; then
225
+ if [ -n "${XRAY_SLACK_TOKEN:-}" ] && [ -n "${XRAY_SLACK_CHANNEL:-}" ]; then
226
+ bash ./xray-notify.sh || true
227
+ else
228
+ echo "XRAY_SLACK_TOKEN / XRAY_SLACK_CHANNEL not set — skipping Slack notification."
229
+ fi
230
+ fi
231
+ - rm -f ./xray-notify.sh
232
+ # after-script always runs, so credentials don't survive a failed step.
233
+ after-script:
234
+ - jf config remove scan1 --quiet || true
235
+ artifacts:
236
+ - xray-report/**
237
+
238
+ - step: &tenable-scan-step
239
+ name: Tenable Cloud Security Scan (container image)
240
+ runs-on:
241
+ - self.hosted
242
+ - linux.shell
243
+ - onprem
244
+ script:
245
+ # bin/tenable-scan.sh lives in this shared repo; an import only brings
246
+ # the YAML across, so fetch it the same way the Xray step does.
247
+ - |
248
+ TENABLE_BIN=""
249
+ if [ -f ./bin/tenable-scan.sh ]; then
250
+ TENABLE_BIN=./bin
251
+ else
252
+ DEST=/tmp/pipeline-shared-scripts
253
+ rm -rf "${DEST}"
254
+ LABELS=""; URLS=""
255
+ add_src() { LABELS="${LABELS}${LABELS:+|}$1"; URLS="${URLS}${URLS:+|}$2"; }
256
+ [ -n "${SCRIPTS_REPO:-}" ] && add_src "SCRIPTS_REPO" "${SCRIPTS_REPO}"
257
+ if [ -n "${BITBUCKET_USERNAME:-}" ] && [ -n "${BITBUCKET_APP_PASSWORD:-}" ]; then
258
+ add_src "BITBUCKET_USERNAME+APP_PASSWORD" "https://${BITBUCKET_USERNAME}:${BITBUCKET_APP_PASSWORD}@bitbucket.org/dp-cloud/pipeline-shared-scripts.git"
259
+ fi
260
+ [ -n "${BITBUCKET_STEP_OAUTH_TOKEN:-}" ] && add_src "step OAuth token" "https://x-token-auth:${BITBUCKET_STEP_OAUTH_TOKEN}@bitbucket.org/dp-cloud/pipeline-shared-scripts.git"
261
+ add_src "ssh" "git@bitbucket.org:dp-cloud/pipeline-shared-scripts.git"
262
+ i=1
263
+ while :; do
264
+ label="$(printf '%s' "${LABELS}" | cut -d'|' -f${i})"
265
+ url="$(printf '%s' "${URLS}" | cut -d'|' -f${i})"
266
+ [ -z "${label}" ] && break
267
+ echo "Fetching shared scripts via ${label}..."
268
+ if GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=accept-new -o BatchMode=yes" \
269
+ git clone --depth 1 --quiet "${url}" "${DEST}" 2>/dev/null; then
270
+ TENABLE_BIN="${DEST}/bin"
271
+ echo "Fetched shared scripts via ${label}."
272
+ break
273
+ fi
274
+ rm -rf "${DEST}"
275
+ i=$((i + 1))
276
+ done
277
+ fi
278
+ if [ -z "${TENABLE_BIN}" ]; then
279
+ echo "Could not fetch shared scripts (tried: $(printf '%s' "${LABELS}" | tr '|' ' ')) — skipping Tenable scan."
280
+ exit 0
281
+ fi
282
+ if [ ! -f "${TENABLE_BIN}/tenable-scan.sh" ]; then
283
+ echo "tenable-scan.sh not present in ${TENABLE_BIN} — skipping Tenable scan."
284
+ exit 0
285
+ fi
286
+ bash "${TENABLE_BIN}/tenable-scan.sh"
287
+ artifacts:
288
+ - tenable-report/**
289
+
290
+ # Runs the Tenable image scan on its own, for repos that want it separately
291
+ # from the Xray scan: import: pipeline-shared-scripts:master:tenable-scan
292
+ tenable-scan:
293
+ - step: *tenable-scan-step
294
+
10
295
  steps:
11
296
  - step: &version-step
12
297
  runs-on:
@@ -21,6 +306,12 @@ definitions:
21
306
  - node src/changeset/cli.js version-commit-tag --install-latest
22
307
 
23
308
  pipelines:
309
+ # Lets this repo run the shared scans against itself from the Bitbucket UI.
310
+ custom:
311
+ xray-scan: *xray-scan-pipeline
312
+ tenable-scan:
313
+ - step: *tenable-scan-step
314
+
24
315
  pull-requests:
25
316
  '**':
26
317
  - step:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docupace/pipeline",
3
- "version": "1.0.121",
3
+ "version": "1.0.122-connect.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -133,7 +133,12 @@ program
133
133
  .command('version')
134
134
  .description('Run Changeset versioning')
135
135
  .action(async () => {
136
- await versionCommand();
136
+ try {
137
+ await versionCommand();
138
+ } catch (error) {
139
+ console.error('❌ Changeset versioning failed:', error);
140
+ process.exit(1);
141
+ }
137
142
  });
138
143
 
139
144
  // Command: commit-and-tag
@@ -154,10 +159,15 @@ program
154
159
  )
155
160
  .action(async (options) => {
156
161
  console.log('📦 Running all steps...');
157
- const releases = await versionCommand(false, options.installLatest);
158
- await updateJiraWithVersion(releases);
159
- await updateWikiWithVersion(releases);
160
- await commitTagCommand(releases);
162
+ try {
163
+ const releases = await versionCommand(false, options.installLatest);
164
+ await updateJiraWithVersion(releases);
165
+ await updateWikiWithVersion(releases);
166
+ await commitTagCommand(releases);
167
+ } catch (error) {
168
+ console.error('❌ Version-commit-tag failed:', error);
169
+ process.exit(1);
170
+ }
161
171
  });
162
172
 
163
173
  program
@@ -165,38 +175,44 @@ program
165
175
  .description('Closes a version')
166
176
  .action(async () => {
167
177
  console.log('📦 Running all steps...');
168
- const releases = await versionCommand(true);
169
- await commitTagCommand(releases);
170
-
171
- const { stdout: currentBranch } = await $`git rev-parse --abbrev-ref HEAD`;
172
-
173
- if (isMonoRepo()) {
174
- for (const release of getAppReleases(releases)) {
175
- const appName = getAppNameFromPackageName(release.name);
176
- const newVersion = toShortVersion(release.newVersion);
178
+ try {
179
+ const releases = await versionCommand(true);
180
+ await commitTagCommand(releases);
181
+
182
+ const { stdout: currentBranch } =
183
+ await $`git rev-parse --abbrev-ref HEAD`;
184
+
185
+ if (isMonoRepo()) {
186
+ for (const release of getAppReleases(releases)) {
187
+ const appName = getAppNameFromPackageName(release.name);
188
+ const newVersion = toShortVersion(release.newVersion);
189
+ if (!newVersion) {
190
+ throw new Error('Error calculating branch name');
191
+ }
192
+ await $`git checkout -b release/${appName}/${newVersion}`;
193
+ await $`git push origin release/${appName}/${newVersion}`;
194
+ }
195
+ } else {
196
+ const newVersion = toShortVersion(releases[0].newVersion);
177
197
  if (!newVersion) {
178
198
  throw new Error('Error calculating branch name');
179
199
  }
180
- await $`git checkout -b release/${appName}/${newVersion}`;
181
- await $`git push origin release/${appName}/${newVersion}`;
200
+ await $`git checkout -b release/${newVersion}`;
201
+ await $`git push origin release/${newVersion}`;
182
202
  }
183
- } else {
184
- const newVersion = toShortVersion(releases[0].newVersion);
185
- if (!newVersion) {
186
- throw new Error('Error calculating branch name');
187
- }
188
- await $`git checkout -b release/${newVersion}`;
189
- await $`git push origin release/${newVersion}`;
190
- }
191
203
 
192
- // Return to original branch
193
- await $`git checkout ${currentBranch.trim()}`;
204
+ // Return to original branch
205
+ await $`git checkout ${currentBranch.trim()}`;
194
206
 
195
- // Open next alpha version
196
- await $`pnpm changeset pre enter alpha`;
197
- await $`git add .`;
198
- await $`git commit -m "Open next alpha version"`;
199
- await $`git push origin`;
207
+ // Open next alpha version
208
+ await $`pnpm changeset pre enter alpha`;
209
+ await $`git add .`;
210
+ await $`git commit -m "Open next alpha version"`;
211
+ await $`git push origin`;
212
+ } catch (error) {
213
+ console.error('❌ Close-version failed:', error);
214
+ process.exit(1);
215
+ }
200
216
  });
201
217
 
202
218
  program
@@ -213,7 +229,12 @@ program
213
229
  .command('convert-prepare <version> [changelogPath]')
214
230
  .description('Convert release notes to Canny format - Part 1 (Prepare)')
215
231
  .action(async (version, changelogPath) => {
216
- await convertPrepare(version, changelogPath);
232
+ try {
233
+ await convertPrepare(version, changelogPath);
234
+ } catch (error) {
235
+ console.error('❌ Convert-prepare failed:', error);
236
+ process.exit(1);
237
+ }
217
238
  });
218
239
 
219
240
  // Command: convert-complete
@@ -221,7 +242,12 @@ program
221
242
  .command('convert-complete [changelogPath]')
222
243
  .description('Convert release notes to Canny format - Part 2 (Complete)')
223
244
  .action(async (changelogPath) => {
224
- await convertComplete(changelogPath);
245
+ try {
246
+ await convertComplete(changelogPath);
247
+ } catch (error) {
248
+ console.error('❌ Convert-complete failed:', error);
249
+ process.exit(1);
250
+ }
225
251
  });
226
252
 
227
253
  // Parse arguments
@@ -361,6 +361,13 @@ export async function convertPrepare(
361
361
  console.log(` Processed: ${processedCount}`);
362
362
  console.log(` Errors: ${errorCount}`);
363
363
  console.log(`\n📄 Updated changelog at: ${resolvedChangelogPath}`);
364
+
365
+ if (errorCount > 0) {
366
+ console.error(
367
+ `❌ Convert-prepare failed to process ${errorCount} ticket(s).`,
368
+ );
369
+ process.exit(1);
370
+ }
364
371
  }
365
372
 
366
373
  /**
@@ -1,14 +1,24 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import { fileURLToPath } from 'url';
3
4
  import { $ } from 'zx';
4
5
 
5
6
  import { setGitInfo } from '../next/util.js';
6
7
  import { versionStatus } from './version.js';
7
8
 
9
+ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
10
+ const PACKAGE_VERSION = JSON.parse(
11
+ fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf8'),
12
+ ).version;
13
+
8
14
  export async function validateChangesets() {
15
+ console.log(`validateChangesets: @docupace/pipeline version = ${PACKAGE_VERSION}`);
9
16
  await setGitInfo();
10
17
  const destBranch = process.env.BITBUCKET_PR_DESTINATION_BRANCH;
18
+ console.log(`validateChangesets: destBranch = ${destBranch}`);
19
+ console.log(`validateChangesets: fetching origin ${destBranch}:${destBranch}`);
11
20
  await $`git fetch origin ${destBranch}:${destBranch}`;
21
+ console.log('validateChangesets: fetch succeeded');
12
22
  let releases;
13
23
 
14
24
  try {
@@ -89,19 +89,23 @@ export async function versionCommand(
89
89
  }
90
90
 
91
91
  let originalBaseBranch;
92
+ let releases;
92
93
  try {
93
94
  originalBaseBranch = updateBaseBranch();
94
- const releases = await versionStatus();
95
- revertBaseBranch(originalBaseBranch);
96
- await $`pnpm changeset version`;
97
-
98
- await removeChangesetOutput();
99
-
100
- return releases;
95
+ releases = await versionStatus();
101
96
  } catch {
102
97
  revertBaseBranch(originalBaseBranch);
103
98
  return [];
104
99
  }
100
+ revertBaseBranch(originalBaseBranch);
101
+
102
+ // Intentionally outside the try/catch above: a failure here (e.g. release
103
+ // notes processing) must fail the build, not be swallowed as "no releases".
104
+ await $`pnpm changeset version`;
105
+
106
+ await removeChangesetOutput();
107
+
108
+ return releases;
105
109
  }
106
110
 
107
111
  export async function updateJiraWithVersion(releases) {
@@ -66,7 +66,7 @@ async function updateAndCommitConnectVersion(
66
66
 
67
67
  await $`cd ${connectDeployPath} && git add .`;
68
68
  await $`cd ${connectDeployPath} && git commit -m "Update Connect version for ${toSite} in ${toEnv} to ${getComparedVersion}" || echo "No changes to commit"`;
69
- await $`cd ${connectDeployPath} && git push`;
69
+ await pushWithRetry(connectDeployPath);
70
70
 
71
71
  console.log('🧹 Cleaning up...');
72
72
  await $`rm -rf ${connectDeployPath}`;
@@ -114,6 +114,24 @@ async function compareGetConnectSiteVersions(fromEnv, fromSite, toEnv, toSite) {
114
114
  throw error;
115
115
  }
116
116
  }
117
+ /**
118
+ * Push commits in repoPath, retrying once with a pull --rebase if the push
119
+ * is rejected because the remote branch moved (non-fast-forward race).
120
+ * @param {string} repoPath - The path to the local repository.
121
+ */
122
+ async function pushWithRetry(repoPath) {
123
+ try {
124
+ await $`cd ${repoPath} && git push`;
125
+ } catch (error) {
126
+ console.warn(
127
+ '⚠️ Push rejected, likely because the remote branch moved. Pulling and retrying once...',
128
+ );
129
+ console.warn(error.message);
130
+ await $`cd ${repoPath} && git pull --rebase`;
131
+ await $`cd ${repoPath} && git push`;
132
+ }
133
+ }
134
+
117
135
  function getRepoLocalPathByRepoSlug(repoSlug) {
118
136
  const repoName = `${repoSlug}`;
119
137
  const repoPath = path.join(process.cwd(), repoName);
@@ -270,7 +288,7 @@ export async function pushDeployConfig(repoPath, commitMessage) {
270
288
 
271
289
  await $`cd ${repoPath} && git add .`;
272
290
  await $`cd ${repoPath} && git commit -m ${commitMessage}`;
273
- await $`cd ${repoPath} && git push`;
291
+ await pushWithRetry(repoPath);
274
292
  console.log('Changes pushed to the remote repository.');
275
293
 
276
294
  console.log('🧹 Cleaning up...');
package/src/next/util.js CHANGED
@@ -85,11 +85,21 @@ export async function getChangedAppPackages() {
85
85
  }
86
86
 
87
87
  export async function setGitInfo() {
88
+ console.log(`setGitInfo: SET_GITOPS_USER=${process.env.SET_GITOPS_USER || '(unset)'}`);
88
89
  if (process.env.SET_GITOPS_USER) {
90
+ const before = (await $`git remote get-url origin`).stdout.trim();
91
+ console.log(`setGitInfo: origin before = ${before}`);
92
+
89
93
  await $`git config --global user.email "gitops@docupace.com"`;
90
94
  await $`git config --global user.name "Gitops"`;
91
95
  const repoUrl = `git@bitbucket.org:dp-cloud/${process.env.BITBUCKET_REPO_SLUG}.git`;
92
96
  console.log(`Setting new origin: ${repoUrl}`);
93
97
  await $`git remote set-url origin ${repoUrl}`;
98
+
99
+ const after = (await $`git remote get-url origin`).stdout.trim();
100
+ console.log(`setGitInfo: origin after = ${after}`);
101
+ } else {
102
+ const current = (await $`git remote get-url origin`).stdout.trim();
103
+ console.log(`setGitInfo: SET_GITOPS_USER not set, leaving origin as-is = ${current}`);
94
104
  }
95
105
  }