From 74912416338ca4ebb51f8d73adabf1d28edc7d91 Mon Sep 17 00:00:00 2001 From: Norman Abramovitz Date: Fri, 6 Mar 2026 14:18:59 -0800 Subject: [PATCH 01/27] Rewrite packaging script for pre-built CF deploy Replace the old bin/package that compiled from source with a script that uses Makefile build targets and produces a zip for cf push -p with binary_buildpack. Old script kept as package.old until verified by Kevin Rutten. --- bin/package | 307 +++++++++++++++++++++++++++++------------------- bin/package.old | 134 +++++++++++++++++++++ 2 files changed, 317 insertions(+), 124 deletions(-) create mode 100755 bin/package.old diff --git a/bin/package b/bin/package index e107cf4fae..4343d5c878 100755 --- a/bin/package +++ b/bin/package @@ -1,134 +1,193 @@ #!/usr/bin/env bash -# shellcheck disable=SC2317 - -set -xeuo pipefail - -declare git_url git_tag work_dir -declare -x TAG NODE_VERSION TMP_DIR - -git_url="https://github.com/cloudfoundry/stratos.git" -git_tag="${TAG:-develop}" -work_dir="${PWD}" -NODE_VERSION="${NODE_VERISON:-24.10.0}" -NODE_SHA2="${NODE_SHA2:-TBD_NEED_TO_LOOKUP_FROM_CF_BUILDPACKS}" -TMP_DIR=/tmp - -node::install() { - local download_file - download_file="${TMP_DIR}/node${NODE_VERSION}.tar.gz" - - export node_install_dir="/tmp/node${NODE_VERSION}" - #export node_dir="${node_install_dir}/node-v${NODE_VERSION}-linux-x64" - - mkdir -p "${node_install_dir}" - - if [[ ! -f "${node_install_dir}/bin/node" ]]; then -# - name: node -# version: 20.13.1 -# uri: https://buildpacks.cloudfoundry.org/dependencies/node/node_20.13.1_linux_x64_cflinuxfs4_71ec5c92.tgz -# sha256: 71ec5c92b35770170dad21ff65130f3e4201e8a0bcd32986e5dcf32b57f379e6 -# cf_stacks: -# - cflinuxfs4 -# source: https://nodejs.org/dist/v20.13.1/node-v20.13.1.tar.gz -# source_sha256: a85ee53aa0a5c2f5ca94fa414cdbceb91eb7d18a77fc498358512c14cc6c6991 - - URL=https://buildpacks.cloudfoundry.org/dependencies/node/node_${NODE_VERSION}_linux_x64_cflinuxfs4_71ec5c92.tgz - - echo "-----> Download Nodejs ${NODE_VERSION}" - curl -s -L --retry 15 --retry-delay 2 "$URL" -o "${download_file}" - - DOWNLOAD_SHA2=$(sha256sum "${download_file}" | cut -d ' ' -f 1) - - if [[ ${DOWNLOAD_SHA2} != "${NODE_SHA2}" ]]; then - echo " **ERROR** MD5 mismatch: got $DOWNLOAD_SHA2 expected $NODE_SHA2" - exit 1 - fi - - tar xzf "${download_file}" -C "${node_install_dir}" - rm "${download_file}" +# +# Package Stratos for Cloud Foundry deployment. +# +# Builds the frontend and backend, then stages a directory that can be +# deployed with: cf push -p dist/cf-package +# +# Usage: +# bin/package # build + package +# bin/package --skip-build # package from existing build artifacts +# +# Environment: +# VERSION - override version (default: from package.json) +# GOOS - target OS for backend binary (default: linux) +# GOARCH - target arch for backend binary (default: amd64) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" + +# Ensure we're running from the repo root +cd "${ROOT_DIR}" + +VERSION="${VERSION:-$(node -p "require('./package.json').version" 2>/dev/null || echo "dev")}" +GOOS="${GOOS:-linux}" +GOARCH="${GOARCH:-amd64}" +SKIP_BUILD="${1:-}" + +DIST_DIR="${ROOT_DIR}/dist" +PKG_DIR="${DIST_DIR}/cf-package" +ZIP_FILE="${DIST_DIR}/stratos-cf-${VERSION}.zip" + +log() { echo "-----> $1"; } +error() { echo "ERROR: $1" >&2; exit 1; } + +# ------------------------------------------------------------------- +# Preflight checks +# ------------------------------------------------------------------- +preflight() { + local missing=() + command -v node >/dev/null || missing+=(node) + command -v go >/dev/null || missing+=(go) + command -v zip >/dev/null || missing+=(zip) + + if [[ ${#missing[@]} -gt 0 ]]; then + error "Required tools not found: ${missing[*]}" fi - - if [[ ! -f "${node_install_dir}/bin/node" ]]; then - echo " **ERROR** Could not download nodejs" - exit 1 +} + +# ------------------------------------------------------------------- +# Build frontend + backend +# ------------------------------------------------------------------- +build() { + if [[ "${SKIP_BUILD}" == "--skip-build" ]]; then + log "Skipping build (--skip-build)" + return + fi + + log "Building frontend..." + make build-frontend + + log "Building backend (${GOOS}/${GOARCH})..." + cd "${ROOT_DIR}/src/jetstream" + + # Generate OpenAPI docs if swag is available + if command -v swag >/dev/null 2>&1; then + swag init --parseDependency 2>/dev/null || true fi - - export NODE_HOME="${node_install_dir}" + + GOOS="${GOOS}" GOARCH="${GOARCH}" go build \ + -ldflags "-X main.appVersion=${VERSION}" \ + -o "${DIST_DIR}/jetstream" + + cd "${ROOT_DIR}" } +# ------------------------------------------------------------------- +# Stage the CF deployment layout +# ------------------------------------------------------------------- +stage() { + log "Staging CF package..." + + # Clean previous package + rm -rf "${PKG_DIR}" + mkdir -p "${PKG_DIR}" + + # Backend binary + if [[ -f "${DIST_DIR}/jetstream" ]]; then + cp "${DIST_DIR}/jetstream" "${PKG_DIR}/jetstream" + else + error "Backend binary not found at ${DIST_DIR}/jetstream. Run without --skip-build." + fi + chmod +x "${PKG_DIR}/jetstream" + + # Frontend assets — backend serves from ui/ + local ui_src="" + if [[ -d "${DIST_DIR}/browser" ]]; then + ui_src="${DIST_DIR}/browser" + elif [[ -d "${DIST_DIR}/stratos" ]]; then + ui_src="${DIST_DIR}/stratos" + else + # Angular output may be directly in dist/ (excluding our packaging artifacts) + ui_src="" + for item in "${DIST_DIR}"/*; do + base="$(basename "$item")" + case "${base}" in + jetstream|cf-package|release|bin|"stratos-cf-"*) continue ;; + esac + if [[ -d "$item" ]]; then + ui_src="$item" + break + fi + done + fi -if [[ -z ${USE_LOCAL:-""} ]] ; then + if [[ -z "${ui_src}" ]]; then + error "Frontend build output not found in ${DIST_DIR}/" + fi + cp -r "${ui_src}" "${PKG_DIR}/ui" + + # Config + cp "${ROOT_DIR}/deploy/cloud-foundry/config.properties" "${PKG_DIR}/" - git clone "${git_url}" stratos-ui || true + # Plugins + if [[ -f "${ROOT_DIR}/src/jetstream/plugins.yaml" ]]; then + cp "${ROOT_DIR}/src/jetstream/plugins.yaml" "${PKG_DIR}/" + fi - if [[ -n ${git_tag} ]]; then - pushd stratos-ui - git checkout "${git_tag}" - export stratos_version="${git_tag}" - popd + # User invite templates + if [[ -d "${ROOT_DIR}/src/jetstream/templates" ]]; then + cp -r "${ROOT_DIR}/src/jetstream/templates" "${PKG_DIR}/templates" fi -else - echo "Using local checked out copy on stratos-ui" -fi - -if [[ -n ${VERSION:-""} ]] ; then - export stratos_version="${VERSION}" # Will be tagged on publish in Concourse -fi - -exit_trap() { - # See: install_nodejs.sh - [[ -d "${TMP_DIR}/node${NODE_VERSION}" ]] && rm -rf "${TMP_DIR}/node${NODE_VERSION}" - [[ -f "${TMP_DIR}/node${NODE_VERSION}.tar.gz" ]] && rm -rf "${TMP_DIR}/node${NODE_VERSION}.tar.gz" - true + + # Procfile for CF + echo "web: ./jetstream" > "${PKG_DIR}/Procfile" + + # Generate CF manifest for pre-built package + cat > "${PKG_DIR}/manifest.yml" < /dev/null; then - node::install - export PATH="${NODE_HOME}/bin:$PATH" -else - npm_location="$(which npm)" - export NODE_HOME="${npm_location%%/bin/npm}" -fi - -mkdir -p cache -build_dir="${work_dir}/stratos-ui" - -# Fix the "authenticity of host can't be established" error during build -#ssh-keyscan "bitbucket.org" >> ~/.ssh/known_hosts - -# prebuild ui -cd stratos-ui -if [[ -n "${stratos_version:-""}" ]]; then - sed -i package.json -e 's/"version": ".*",$/"version": "'"$stratos_version"'",/' -fi -npm install --legacy-peer-deps -npm run prebuild-ui -rm -Rf ./dist - -# Actually build Stratos -bash -x deploy/cloud-foundry/build.sh "${build_dir}" "${work_dir}/cache" -cd "${work_dir}" - -# Remove build artifacts (node_modules & bower_components) -if [[ -d "${build_dir}/node_modules" ]]; then - rm -rf "${build_dir}/node_modules" -fi - -if [[ -d "${build_dir}/bower_components" ]]; then - rm -rf "${build_dir}/bower_components" -fi - -echo "web: ./deploy/cloud-foundry/start.sh" > "${build_dir}/Procfile" - -ls -lah "${build_dir}" -cd "${build_dir}" -package_version="${stratos_version:-"dev-$(date +%Y%m%d%H%M%S)"}" -if [[ -n ${RELEASE_DIR:-""} ]] ; then - zip -r -x@exclude.lst "${work_dir}/${RELEASE_DIR}/stratos-ui-${package_version}.zip" ./* -else - zip -r -x@exclude.lst "${work_dir}/stratos-ui-${package_version}.zip" ./* -fi -cd "${work_dir}" - -exit 0 + +# ------------------------------------------------------------------- +# Create the zip +# ------------------------------------------------------------------- +archive() { + log "Creating ${ZIP_FILE}..." + rm -f "${ZIP_FILE}" + cd "${PKG_DIR}" + zip -r "${ZIP_FILE}" . -x '*.git*' '*.DS_Store' + cd "${ROOT_DIR}" +} + +# ------------------------------------------------------------------- +# Summary +# ------------------------------------------------------------------- +summary() { + echo "" + log "Package complete!" + echo " Version: ${VERSION}" + echo " Archive: ${ZIP_FILE}" + echo " Size: $(du -h "${ZIP_FILE}" | cut -f1)" + echo "" + echo "Deploy with:" + echo " cf push -p ${ZIP_FILE}" + echo " # or" + echo " cf push -p ${PKG_DIR}" + echo "" +} + +# ------------------------------------------------------------------- +# Main +# ------------------------------------------------------------------- +preflight +build +stage +archive +summary diff --git a/bin/package.old b/bin/package.old new file mode 100755 index 0000000000..e107cf4fae --- /dev/null +++ b/bin/package.old @@ -0,0 +1,134 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2317 + +set -xeuo pipefail + +declare git_url git_tag work_dir +declare -x TAG NODE_VERSION TMP_DIR + +git_url="https://github.com/cloudfoundry/stratos.git" +git_tag="${TAG:-develop}" +work_dir="${PWD}" +NODE_VERSION="${NODE_VERISON:-24.10.0}" +NODE_SHA2="${NODE_SHA2:-TBD_NEED_TO_LOOKUP_FROM_CF_BUILDPACKS}" +TMP_DIR=/tmp + +node::install() { + local download_file + download_file="${TMP_DIR}/node${NODE_VERSION}.tar.gz" + + export node_install_dir="/tmp/node${NODE_VERSION}" + #export node_dir="${node_install_dir}/node-v${NODE_VERSION}-linux-x64" + + mkdir -p "${node_install_dir}" + + if [[ ! -f "${node_install_dir}/bin/node" ]]; then +# - name: node +# version: 20.13.1 +# uri: https://buildpacks.cloudfoundry.org/dependencies/node/node_20.13.1_linux_x64_cflinuxfs4_71ec5c92.tgz +# sha256: 71ec5c92b35770170dad21ff65130f3e4201e8a0bcd32986e5dcf32b57f379e6 +# cf_stacks: +# - cflinuxfs4 +# source: https://nodejs.org/dist/v20.13.1/node-v20.13.1.tar.gz +# source_sha256: a85ee53aa0a5c2f5ca94fa414cdbceb91eb7d18a77fc498358512c14cc6c6991 + + URL=https://buildpacks.cloudfoundry.org/dependencies/node/node_${NODE_VERSION}_linux_x64_cflinuxfs4_71ec5c92.tgz + + echo "-----> Download Nodejs ${NODE_VERSION}" + curl -s -L --retry 15 --retry-delay 2 "$URL" -o "${download_file}" + + DOWNLOAD_SHA2=$(sha256sum "${download_file}" | cut -d ' ' -f 1) + + if [[ ${DOWNLOAD_SHA2} != "${NODE_SHA2}" ]]; then + echo " **ERROR** MD5 mismatch: got $DOWNLOAD_SHA2 expected $NODE_SHA2" + exit 1 + fi + + tar xzf "${download_file}" -C "${node_install_dir}" + rm "${download_file}" + fi + + if [[ ! -f "${node_install_dir}/bin/node" ]]; then + echo " **ERROR** Could not download nodejs" + exit 1 + fi + + export NODE_HOME="${node_install_dir}" +} + + +if [[ -z ${USE_LOCAL:-""} ]] ; then + + git clone "${git_url}" stratos-ui || true + + if [[ -n ${git_tag} ]]; then + pushd stratos-ui + git checkout "${git_tag}" + export stratos_version="${git_tag}" + popd + fi +else + echo "Using local checked out copy on stratos-ui" +fi + +if [[ -n ${VERSION:-""} ]] ; then + export stratos_version="${VERSION}" # Will be tagged on publish in Concourse +fi + +exit_trap() { + # See: install_nodejs.sh + [[ -d "${TMP_DIR}/node${NODE_VERSION}" ]] && rm -rf "${TMP_DIR}/node${NODE_VERSION}" + [[ -f "${TMP_DIR}/node${NODE_VERSION}.tar.gz" ]] && rm -rf "${TMP_DIR}/node${NODE_VERSION}.tar.gz" + true +} +trap exit_trap EXIT + +if ! which npm > /dev/null; then + node::install + export PATH="${NODE_HOME}/bin:$PATH" +else + npm_location="$(which npm)" + export NODE_HOME="${npm_location%%/bin/npm}" +fi + +mkdir -p cache +build_dir="${work_dir}/stratos-ui" + +# Fix the "authenticity of host can't be established" error during build +#ssh-keyscan "bitbucket.org" >> ~/.ssh/known_hosts + +# prebuild ui +cd stratos-ui +if [[ -n "${stratos_version:-""}" ]]; then + sed -i package.json -e 's/"version": ".*",$/"version": "'"$stratos_version"'",/' +fi +npm install --legacy-peer-deps +npm run prebuild-ui +rm -Rf ./dist + +# Actually build Stratos +bash -x deploy/cloud-foundry/build.sh "${build_dir}" "${work_dir}/cache" +cd "${work_dir}" + +# Remove build artifacts (node_modules & bower_components) +if [[ -d "${build_dir}/node_modules" ]]; then + rm -rf "${build_dir}/node_modules" +fi + +if [[ -d "${build_dir}/bower_components" ]]; then + rm -rf "${build_dir}/bower_components" +fi + +echo "web: ./deploy/cloud-foundry/start.sh" > "${build_dir}/Procfile" + +ls -lah "${build_dir}" +cd "${build_dir}" +package_version="${stratos_version:-"dev-$(date +%Y%m%d%H%M%S)"}" +if [[ -n ${RELEASE_DIR:-""} ]] ; then + zip -r -x@exclude.lst "${work_dir}/${RELEASE_DIR}/stratos-ui-${package_version}.zip" ./* +else + zip -r -x@exclude.lst "${work_dir}/stratos-ui-${package_version}.zip" ./* +fi +cd "${work_dir}" + +exit 0 From b79a8b411dec65b4931190663a95bcd862fc24fc Mon Sep 17 00:00:00 2001 From: Norman Abramovitz Date: Fri, 6 Mar 2026 14:58:30 -0800 Subject: [PATCH 02/27] Produce amd64 and arm64 CF deploy zips Use make build-backend-all to cross-compile for all platforms, then package linux/amd64 and linux/arm64 zips for CF deployment. --- bin/package | 155 +++++++++++++++++++++++++--------------------------- 1 file changed, 73 insertions(+), 82 deletions(-) diff --git a/bin/package b/bin/package index 4343d5c878..971230566c 100755 --- a/bin/package +++ b/bin/package @@ -2,34 +2,29 @@ # # Package Stratos for Cloud Foundry deployment. # -# Builds the frontend and backend, then stages a directory that can be -# deployed with: cf push -p dist/cf-package +# Builds the frontend and backend for all platforms, then produces +# per-architecture zips deployable with: +# cf push -f dist/cf-package-/manifest.yml -p dist/stratos-cf--.zip # # Usage: # bin/package # build + package # bin/package --skip-build # package from existing build artifacts # # Environment: -# VERSION - override version (default: from package.json) -# GOOS - target OS for backend binary (default: linux) -# GOARCH - target arch for backend binary (default: amd64) +# VERSION - override version (default: from package.json) set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" -# Ensure we're running from the repo root cd "${ROOT_DIR}" VERSION="${VERSION:-$(node -p "require('./package.json').version" 2>/dev/null || echo "dev")}" -GOOS="${GOOS:-linux}" -GOARCH="${GOARCH:-amd64}" SKIP_BUILD="${1:-}" DIST_DIR="${ROOT_DIR}/dist" -PKG_DIR="${DIST_DIR}/cf-package" -ZIP_FILE="${DIST_DIR}/stratos-cf-${VERSION}.zip" +CF_ARCHITECTURES=(amd64 arm64) log() { echo "-----> $1"; } error() { echo "ERROR: $1" >&2; exit 1; } @@ -49,7 +44,7 @@ preflight() { } # ------------------------------------------------------------------- -# Build frontend + backend +# Build everything via Makefile # ------------------------------------------------------------------- build() { if [[ "${SKIP_BUILD}" == "--skip-build" ]]; then @@ -57,86 +52,80 @@ build() { return fi - log "Building frontend..." + log "Building frontend and backend for all platforms..." make build-frontend + make build-backend-all +} - log "Building backend (${GOOS}/${GOARCH})..." - cd "${ROOT_DIR}/src/jetstream" - - # Generate OpenAPI docs if swag is available - if command -v swag >/dev/null 2>&1; then - swag init --parseDependency 2>/dev/null || true +# ------------------------------------------------------------------- +# Find the frontend build output directory +# ------------------------------------------------------------------- +find_ui_source() { + if [[ -d "${DIST_DIR}/browser" ]]; then + echo "${DIST_DIR}/browser" + return fi - - GOOS="${GOOS}" GOARCH="${GOARCH}" go build \ - -ldflags "-X main.appVersion=${VERSION}" \ - -o "${DIST_DIR}/jetstream" - - cd "${ROOT_DIR}" + if [[ -d "${DIST_DIR}/stratos" ]]; then + echo "${DIST_DIR}/stratos" + return + fi + for item in "${DIST_DIR}"/*; do + base="$(basename "$item")" + case "${base}" in + jetstream*|cf-package*|release|bin|"stratos-cf-"*) continue ;; + esac + if [[ -d "$item" ]]; then + echo "$item" + return + fi + done + error "Frontend build output not found in ${DIST_DIR}/" } # ------------------------------------------------------------------- -# Stage the CF deployment layout +# Stage and archive one architecture # ------------------------------------------------------------------- -stage() { - log "Staging CF package..." +package_arch() { + local arch=$1 + local binary="${DIST_DIR}/bin/jetstream-linux-${arch}" + local pkg_dir="${DIST_DIR}/cf-package-${arch}" + local zip_file="${DIST_DIR}/stratos-cf-${VERSION}-${arch}.zip" + + if [[ ! -f "${binary}" ]]; then + error "Backend binary not found: ${binary}" + fi - # Clean previous package - rm -rf "${PKG_DIR}" - mkdir -p "${PKG_DIR}" + log "Staging CF package (${arch})..." + rm -rf "${pkg_dir}" + mkdir -p "${pkg_dir}" # Backend binary - if [[ -f "${DIST_DIR}/jetstream" ]]; then - cp "${DIST_DIR}/jetstream" "${PKG_DIR}/jetstream" - else - error "Backend binary not found at ${DIST_DIR}/jetstream. Run without --skip-build." - fi - chmod +x "${PKG_DIR}/jetstream" + cp "${binary}" "${pkg_dir}/jetstream" + chmod +x "${pkg_dir}/jetstream" # Frontend assets — backend serves from ui/ - local ui_src="" - if [[ -d "${DIST_DIR}/browser" ]]; then - ui_src="${DIST_DIR}/browser" - elif [[ -d "${DIST_DIR}/stratos" ]]; then - ui_src="${DIST_DIR}/stratos" - else - # Angular output may be directly in dist/ (excluding our packaging artifacts) - ui_src="" - for item in "${DIST_DIR}"/*; do - base="$(basename "$item")" - case "${base}" in - jetstream|cf-package|release|bin|"stratos-cf-"*) continue ;; - esac - if [[ -d "$item" ]]; then - ui_src="$item" - break - fi - done - fi - - if [[ -z "${ui_src}" ]]; then - error "Frontend build output not found in ${DIST_DIR}/" - fi - cp -r "${ui_src}" "${PKG_DIR}/ui" + local ui_src + ui_src="$(find_ui_source)" + cp -r "${ui_src}" "${pkg_dir}/ui" # Config - cp "${ROOT_DIR}/deploy/cloud-foundry/config.properties" "${PKG_DIR}/" + cp "${ROOT_DIR}/deploy/cloud-foundry/config.properties" "${pkg_dir}/" # Plugins if [[ -f "${ROOT_DIR}/src/jetstream/plugins.yaml" ]]; then - cp "${ROOT_DIR}/src/jetstream/plugins.yaml" "${PKG_DIR}/" + cp "${ROOT_DIR}/src/jetstream/plugins.yaml" "${pkg_dir}/" fi # User invite templates if [[ -d "${ROOT_DIR}/src/jetstream/templates" ]]; then - cp -r "${ROOT_DIR}/src/jetstream/templates" "${PKG_DIR}/templates" + cp -r "${ROOT_DIR}/src/jetstream/templates" "${pkg_dir}/templates" fi # Procfile for CF - echo "web: ./jetstream" > "${PKG_DIR}/Procfile" + echo "web: ./jetstream" > "${pkg_dir}/Procfile" - # Generate CF manifest for pre-built package - cat > "${PKG_DIR}/manifest.yml" < "${pkg_dir}/manifest.yml" </manifest.yml -p dist/stratos-cf-${VERSION}-.zip" echo "" } @@ -188,6 +176,9 @@ summary() { # ------------------------------------------------------------------- preflight build -stage -archive + +for arch in "${CF_ARCHITECTURES[@]}"; do + package_arch "${arch}" +done + summary From 8614e44ad295aca2aeecf054658039fc69614def Mon Sep 17 00:00:00 2001 From: Norman Abramovitz Date: Fri, 6 Mar 2026 20:45:25 -0800 Subject: [PATCH 03/27] Add GOOS to package output naming Include OS name in generated zip files and staging directories (e.g. stratos-cf-4.4.0-linux-amd64.zip) to support future OS/arch combinations. Update README packaging section to match. --- README.md | 37 ++++++++++++++++++++++++++++--------- bin/package | 34 ++++++++++++++++++++-------------- 2 files changed, 48 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 20d4859a7d..f827e0ed68 100644 --- a/README.md +++ b/README.md @@ -159,23 +159,42 @@ Note: `sgs` creates security groups the first time, upgrades do not use `sgs`. ## Packaging -Golang is required, and version 1.21 is recommended as this is the version used by the Stratos build system. +Requires `node`, `go`, and `zip`. -When you want to build the `4.8.1` tag in -[Stratos UI releases](https://github.com/cloudfoundry/stratos/releases), -run this command: +Build and package for all targets (linux/amd64, linux/arm64): ```bash -./bin/package +bin/package ``` -OR to package a specific tag + +To skip the build step and package from existing artifacts: + +```bash +bin/package --skip-build +``` + +To override the version (default: from `package.json`): + +```bash +VERSION="4.8.1" bin/package +``` + +This produces per-target zips in `dist/`: + +``` +dist/stratos-cf---.zip +``` + +Deploy with: + ```bash -TAG="4.8.1" ./bin/package +cf push -f dist/cf-package--/manifest.yml \ + -p dist/stratos-cf---.zip ``` ### NOTE -The original code for this feature can be found in the -[Orange Cloud foundry Github Repository](https://github.com/orange-cloudfoundry/stratos-ui-cf-packager/). +The original packaging code was based on work from the +[Orange Cloud Foundry Github Repository](https://github.com/orange-cloudfoundry/stratos-ui-cf-packager/). Many thanks to Benjamin & Arthur, we appreciate you both! ## License diff --git a/bin/package b/bin/package index 971230566c..4c7eb74aa5 100755 --- a/bin/package +++ b/bin/package @@ -4,7 +4,7 @@ # # Builds the frontend and backend for all platforms, then produces # per-architecture zips deployable with: -# cf push -f dist/cf-package-/manifest.yml -p dist/stratos-cf--.zip +# cf push -f dist/cf-package--/manifest.yml -p dist/stratos-cf---.zip # # Usage: # bin/package # build + package @@ -24,7 +24,8 @@ VERSION="${VERSION:-$(node -p "require('./package.json').version" 2>/dev/null || SKIP_BUILD="${1:-}" DIST_DIR="${ROOT_DIR}/dist" -CF_ARCHITECTURES=(amd64 arm64) +GOOS="linux" +CF_TARGETS=("${GOOS}:amd64" "${GOOS}:arm64") log() { echo "-----> $1"; } error() { echo "ERROR: $1" >&2; exit 1; } @@ -85,17 +86,18 @@ find_ui_source() { # ------------------------------------------------------------------- # Stage and archive one architecture # ------------------------------------------------------------------- -package_arch() { - local arch=$1 - local binary="${DIST_DIR}/bin/jetstream-linux-${arch}" - local pkg_dir="${DIST_DIR}/cf-package-${arch}" - local zip_file="${DIST_DIR}/stratos-cf-${VERSION}-${arch}.zip" +package_target() { + local os=$1 + local arch=$2 + local binary="${DIST_DIR}/bin/jetstream-${os}-${arch}" + local pkg_dir="${DIST_DIR}/cf-package-${os}-${arch}" + local zip_file="${DIST_DIR}/stratos-cf-${VERSION}-${os}-${arch}.zip" if [[ ! -f "${binary}" ]]; then error "Backend binary not found: ${binary}" fi - log "Staging CF package (${arch})..." + log "Staging CF package (${os}/${arch})..." rm -rf "${pkg_dir}" mkdir -p "${pkg_dir}" @@ -159,15 +161,17 @@ summary() { log "Package complete!" echo " Version: ${VERSION}" echo "" - for arch in "${CF_ARCHITECTURES[@]}"; do - local zip_file="${DIST_DIR}/stratos-cf-${VERSION}-${arch}.zip" + for target in "${CF_TARGETS[@]}"; do + local os="${target%%:*}" + local arch="${target##*:}" + local zip_file="${DIST_DIR}/stratos-cf-${VERSION}-${os}-${arch}.zip" if [[ -f "${zip_file}" ]]; then - echo " ${arch}: ${zip_file} ($(du -h "${zip_file}" | cut -f1))" + echo " ${os}/${arch}: ${zip_file} ($(du -h "${zip_file}" | cut -f1))" fi done echo "" echo "Deploy with:" - echo " cf push -f dist/cf-package-/manifest.yml -p dist/stratos-cf-${VERSION}-.zip" + echo " cf push -f dist/cf-package--/manifest.yml -p dist/stratos-cf-${VERSION}--.zip" echo "" } @@ -177,8 +181,10 @@ summary() { preflight build -for arch in "${CF_ARCHITECTURES[@]}"; do - package_arch "${arch}" +for target in "${CF_TARGETS[@]}"; do + os="${target%%:*}" + arch="${target##*:}" + package_target "${os}" "${arch}" done summary From 5be03365053e2c2160bfee2bc32e068f5d5b99f2 Mon Sep 17 00:00:00 2001 From: Norman Abramovitz Date: Fri, 6 Mar 2026 21:29:23 -0800 Subject: [PATCH 04/27] Improve e2e test portability Auto-resolve org/space GUIDs from names via cf CLI when not explicitly provided in secrets.yaml. Detect SSO vs local login in check spec to support both deployment types. --- e2e/helpers/secrets-helpers.ts | 48 ++++++++++++++++++++++++++++++++-- e2e/secrets.yaml.template | 5 ++-- e2e/tests/core/check.spec.ts | 42 +++++++++++++++++------------ 3 files changed, 75 insertions(+), 20 deletions(-) diff --git a/e2e/helpers/secrets-helpers.ts b/e2e/helpers/secrets-helpers.ts index bce35a9d7f..08c455b1ef 100644 --- a/e2e/helpers/secrets-helpers.ts +++ b/e2e/helpers/secrets-helpers.ts @@ -1,3 +1,4 @@ +import { execSync } from 'child_process'; import * as fs from 'fs'; import * as yaml from 'js-yaml'; import * as path from 'path'; @@ -10,6 +11,46 @@ import * as path from 'path'; export class SecretsHelper { private static SECRETS_FILE = 'secrets.yaml'; + /** + * Resolve a CF org or space GUID using the cf CLI. + * Returns the GUID string or empty string on failure. + */ + private static resolveCfGuid(type: 'org' | 'space', name: string): string { + try { + const cmd = type === 'org' + ? `cf org "${name}" --guid` + : `cf space "${name}" --guid`; + return execSync(cmd, { encoding: 'utf8', timeout: 10000 }).trim(); + } catch { + return ''; + } + } + + /** + * Ensure CF endpoint configs have GUIDs resolved. + * If testOrgGuid/testSpaceGuid are missing but testOrg/testSpace names + * are present, resolve them via the cf CLI. + */ + private static resolveEndpointGuids(cfEndpoints: any[]): any[] { + if (!Array.isArray(cfEndpoints)) return cfEndpoints; + + for (const ep of cfEndpoints) { + if (ep.testOrg && !ep.testOrgGuid) { + ep.testOrgGuid = this.resolveCfGuid('org', ep.testOrg); + } + if (ep.testSpace && !ep.testSpaceGuid) { + // Target the org first so cf space --guid works + if (ep.testOrg) { + try { + execSync(`cf target -o "${ep.testOrg}" > /dev/null 2>&1`, { timeout: 10000 }); + } catch { /* best effort */ } + } + ep.testSpaceGuid = this.resolveCfGuid('space', ep.testSpace); + } + } + return cfEndpoints; + } + /** * Load secrets from secrets.yaml file * Throws error if file doesn't exist or is invalid @@ -27,6 +68,10 @@ export class SecretsHelper { try { const secrets = yaml.load(fs.readFileSync(secretsPath, 'utf8')) as any; + // Get CF endpoints and resolve any missing GUIDs from names + const cfEndpoints = secrets.cloudFoundry || secrets.endpoints?.cf || []; + this.resolveEndpointGuids(cfEndpoints); + return { // Console user credentials console: { @@ -41,8 +86,7 @@ export class SecretsHelper { }, // Cloud Foundry configuration - // Support both 'cloudFoundry' (root level) and 'endpoints.cf' (nested) formats - cloudFoundry: secrets.cloudFoundry || secrets.endpoints?.cf || [], + cloudFoundry: cfEndpoints, // GitHub configuration github: { diff --git a/e2e/secrets.yaml.template b/e2e/secrets.yaml.template index 440a72304e..df6ade0173 100644 --- a/e2e/secrets.yaml.template +++ b/e2e/secrets.yaml.template @@ -36,10 +36,11 @@ endpoints: # Test Organization and Space [REQUIRED] # Create with: cf create-org e2e && cf create-space e2e -o e2e + # GUIDs are auto-resolved from names via cf CLI if omitted testOrg: e2e # Test org name - testOrgGuid: # Get with: cf org e2e --guid testSpace: e2e # Test space name - testSpaceGuid: # Get with: cf space e2e --guid + # testOrgGuid: # Optional - auto-resolved from testOrg + # testSpaceGuid: # Optional - auto-resolved from testSpace # Test Service [OPTIONAL] - For service binding tests # testService: local-volume diff --git a/e2e/tests/core/check.spec.ts b/e2e/tests/core/check.spec.ts index 4e26269394..d181ba49b9 100644 --- a/e2e/tests/core/check.spec.ts +++ b/e2e/tests/core/check.spec.ts @@ -1,21 +1,20 @@ import { test, expect } from '../../fixtures/test-base'; import { LoginPage } from '../../pages/login.page'; +import { SSOLoginPage } from '../../pages/sso-login.page'; /** * System Availability Check Tests * Migrated from src/test-e2e/check/check-login-e2e.spec.ts * - * Basic smoke tests to verify system availability + * Basic smoke tests to verify system availability. + * Supports both local (username/password) and SSO (UAA) login flows. */ test.describe('Check Availability of System', () => { - let loginPage: LoginPage; - test.beforeEach(async ({ page }) => { - loginPage = new LoginPage(page); + test('should reach log in page', async ({ page }) => { + const loginPage = new LoginPage(page); await loginPage.navigateTo(); - }); - test('should reach log in page', async ({ page }) => { // Verify we're on the login page expect(await loginPage.isLoginPage()).toBeTruthy(); @@ -24,17 +23,28 @@ test.describe('Check Availability of System', () => { }); test('should be able to login', async ({ page, secrets }) => { - // Enter valid credentials - await loginPage.enterLogin( - secrets.console.admin.username, - secrets.console.admin.password - ); - - // Verify button is enabled - await expect(loginPage.loginButton()).toBeEnabled(); + const loginPage = new LoginPage(page); + await loginPage.navigateTo(); - // Click login - await loginPage.clickLogin(); + // Detect login type: SSO has a submit button but no username input + const hasUsernameInput = await page.locator('input[name="username"]').isVisible({ timeout: 2000 }).catch(() => false); + + if (hasUsernameInput) { + // Local login flow + await loginPage.enterLogin( + secrets.console.admin.username, + secrets.console.admin.password + ); + await expect(loginPage.loginButton()).toBeEnabled(); + await loginPage.clickLogin(); + } else { + // SSO login flow + const ssoPage = new SSOLoginPage(page); + await ssoPage.login( + secrets.console.admin.username, + secrets.console.admin.password + ); + } // Wait for application page await loginPage.waitForApplicationPage(); From 735f1c0a143352a1f52f110f15ff4edc304bf1f2 Mon Sep 17 00:00:00 2001 From: Norman Abramovitz Date: Fri, 6 Mar 2026 21:46:35 -0800 Subject: [PATCH 05/27] Add encrypt/decrypt scripts for e2e secrets OpenSSL aes-256-cbc with pbkdf2 key derivation. Allows secrets.yaml.enc to be committed safely while keeping plaintext secrets.yaml gitignored. --- e2e/scripts/secrets-decrypt.sh | 34 ++++++++++++++++++++++++++++++++++ e2e/scripts/secrets-encrypt.sh | 26 ++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100755 e2e/scripts/secrets-decrypt.sh create mode 100755 e2e/scripts/secrets-encrypt.sh diff --git a/e2e/scripts/secrets-decrypt.sh b/e2e/scripts/secrets-decrypt.sh new file mode 100755 index 0000000000..5d9c9d99bd --- /dev/null +++ b/e2e/scripts/secrets-decrypt.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# +# Decrypt secrets.yaml.enc → secrets.yaml using openssl aes-256-cbc +# +# Usage: e2e/scripts/secrets-decrypt.sh [passphrase] +# If passphrase is not provided, you will be prompted. + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +PLAIN="${ROOT_DIR}/secrets.yaml" +ENCRYPTED="${ROOT_DIR}/secrets.yaml.enc" + +if [[ ! -f "${ENCRYPTED}" ]]; then + echo "Error: ${ENCRYPTED} not found" >&2 + exit 1 +fi + +if [[ -f "${PLAIN}" ]]; then + echo "Warning: ${PLAIN} already exists and will be overwritten" + read -r -p "Continue? [y/N] " confirm + if [[ "${confirm}" != [yY] ]]; then + echo "Aborted" + exit 0 + fi +fi + +if [[ -n "${1:-}" ]]; then + openssl enc -aes-256-cbc -d -pbkdf2 -in "${ENCRYPTED}" -out "${PLAIN}" -pass "pass:$1" +else + openssl enc -aes-256-cbc -d -pbkdf2 -in "${ENCRYPTED}" -out "${PLAIN}" +fi + +echo "Decrypted: ${PLAIN}" diff --git a/e2e/scripts/secrets-encrypt.sh b/e2e/scripts/secrets-encrypt.sh new file mode 100755 index 0000000000..9ceb559855 --- /dev/null +++ b/e2e/scripts/secrets-encrypt.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# +# Encrypt secrets.yaml → secrets.yaml.enc using openssl aes-256-cbc +# +# Usage: e2e/scripts/secrets-encrypt.sh [passphrase] +# If passphrase is not provided, you will be prompted. + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +PLAIN="${ROOT_DIR}/secrets.yaml" +ENCRYPTED="${ROOT_DIR}/secrets.yaml.enc" + +if [[ ! -f "${PLAIN}" ]]; then + echo "Error: ${PLAIN} not found" >&2 + exit 1 +fi + +if [[ -n "${1:-}" ]]; then + openssl enc -aes-256-cbc -salt -pbkdf2 -in "${PLAIN}" -out "${ENCRYPTED}" -pass "pass:$1" +else + openssl enc -aes-256-cbc -salt -pbkdf2 -in "${PLAIN}" -out "${ENCRYPTED}" +fi + +echo "Encrypted: ${ENCRYPTED}" +echo "You can safely remove ${PLAIN} or keep it in .gitignore" From 4a48e3d260ab302b411080b69dee1a6ca8503d06 Mon Sep 17 00:00:00 2001 From: Norman Abramovitz Date: Fri, 6 Mar 2026 22:06:17 -0800 Subject: [PATCH 06/27] Add test for timestamp visibility across layouts Verifies recent apps timestamps persist when switching between single and two column layouts. Covers FWT-678. --- e2e/tests/core/home-layout.spec.ts | 55 ++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 e2e/tests/core/home-layout.spec.ts diff --git a/e2e/tests/core/home-layout.spec.ts b/e2e/tests/core/home-layout.spec.ts new file mode 100644 index 0000000000..987c307033 --- /dev/null +++ b/e2e/tests/core/home-layout.spec.ts @@ -0,0 +1,55 @@ +import { test, expect } from '../../fixtures/test-base'; + +/** + * Home Page Layout Tests + * Verifies that recent applications data persists across layout changes + * + * Bug: FWT-678 - Timestamps disappear when switching from single to two column + * Root cause: cfhome-card.component.html binds [showDate]="layout.x === 1" + */ +test.describe('Home Page Layout', () => { + + test('recent apps should show timestamps in all column layouts', async ({ connectedEndpointsAdminPage }) => { + const page = connectedEndpointsAdminPage.page; + await page.goto('/home'); + await page.waitForLoadState('networkidle'); + + // Wait for recent apps to load + const recentAppsSection = page.locator('text=Recently updated applications'); + await recentAppsSection.waitFor({ timeout: 15000 }); + + // Open the layout dropdown (column selector button in top right) + const layoutButton = page.locator('button.home-layout-select, [matMenuTriggerFor]').first(); + await layoutButton.click(); + + // Switch to Single Column + await page.locator('button:has-text("Single Column")').click(); + await page.waitForTimeout(1000); + + // Verify timestamps are visible in single column + const appRows = page.locator('app-compact-app-card'); + const firstRowTimestamp = appRows.first().locator('.compact-app-card__date, .app-card-date, time'); + await expect(firstRowTimestamp).toBeVisible({ timeout: 5000 }); + const singleColumnDate = await firstRowTimestamp.textContent(); + expect(singleColumnDate).toBeTruthy(); + + // Switch to Two Column + await layoutButton.click(); + await page.locator('button:has-text("Two Column")').click(); + await page.waitForTimeout(1000); + + // Verify timestamps are still visible in two column layout + const twoColTimestamp = appRows.first().locator('.compact-app-card__date, .app-card-date, time'); + await expect(twoColTimestamp).toBeVisible({ timeout: 5000 }); + const twoColumnDate = await twoColTimestamp.textContent(); + expect(twoColumnDate).toBeTruthy(); + + // Switch back to Single Column and verify timestamps persist + await layoutButton.click(); + await page.locator('button:has-text("Single Column")').click(); + await page.waitForTimeout(1000); + + const revertedTimestamp = appRows.first().locator('.compact-app-card__date, .app-card-date, time'); + await expect(revertedTimestamp).toBeVisible({ timeout: 5000 }); + }); +}); From 2040bffdb5836b430eaffe6422b770bc2bbe4969 Mon Sep 17 00:00:00 2001 From: Norman Abramovitz Date: Fri, 6 Mar 2026 22:07:16 -0800 Subject: [PATCH 07/27] Show timestamps in all home page layouts Remove layout-conditional showDate binding that hid timestamps in multi-column views. FWT-678. --- .../src/features/home/cfhome-card/cfhome-card.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/packages/cloud-foundry/src/features/home/cfhome-card/cfhome-card.component.html b/src/frontend/packages/cloud-foundry/src/features/home/cfhome-card/cfhome-card.component.html index 8e7629c2bb..814318f17c 100644 --- a/src/frontend/packages/cloud-foundry/src/features/home/cfhome-card/cfhome-card.component.html +++ b/src/frontend/packages/cloud-foundry/src/features/home/cfhome-card/cfhome-card.component.html @@ -19,7 +19,7 @@ @if (cardLoaded && recentAppsRows > 0) { + dateMode="subtle" [showDate]="true" [maxRows]="recentAppsRows" mode="plain" [endpoint]="guid"> } @if (!cardLoaded && recentAppsRows > 0) { From 26ab68b79a28456f65e9318019d527effe87a96a Mon Sep 17 00:00:00 2001 From: Norman Abramovitz Date: Sat, 7 Mar 2026 09:07:52 -0800 Subject: [PATCH 08/27] Fix layout signal propagation on home page Replace layout$ | async with direct signal read layout() in the template. With OnPush and zoneless change detection, the async pipe can miss the initial emission from the signal-to- observable bridge. --- .../core/src/features/home/home/home-page.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/packages/core/src/features/home/home/home-page.component.html b/src/frontend/packages/core/src/features/home/home/home-page.component.html index 4293af67a1..b83b064fc3 100644 --- a/src/frontend/packages/core/src/features/home/home/home-page.component.html +++ b/src/frontend/packages/core/src/features/home/home/home-page.component.html @@ -51,7 +51,7 @@

Home

#endpointsPanel> @for (ep of endpoints$ | async; track ep?.guid ?? $index) {
- +
} From 2c68b86620fb734f00a419fd1582c8904f41a8d5 Mon Sep 17 00:00:00 2001 From: Norman Abramovitz Date: Sat, 7 Mar 2026 09:08:33 -0800 Subject: [PATCH 09/27] Show Routes metric in two-column home layout Change condition from layout.x === 1 to layout.x <= 2 so the Routes count appears in both single and two-column layouts. --- .../src/features/home/cfhome-card/cfhome-card.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/packages/cloud-foundry/src/features/home/cfhome-card/cfhome-card.component.html b/src/frontend/packages/cloud-foundry/src/features/home/cfhome-card/cfhome-card.component.html index 814318f17c..56d5031b94 100644 --- a/src/frontend/packages/cloud-foundry/src/features/home/cfhome-card/cfhome-card.component.html +++ b/src/frontend/packages/cloud-foundry/src/features/home/cfhome-card/cfhome-card.component.html @@ -8,7 +8,7 @@
- @if (layout.x === 1) { + @if (layout.x <= 2) { From 00a5dfb34ff25a473f7cba052a832342f0dad527 Mon Sep 17 00:00:00 2001 From: Norman Abramovitz Date: Sat, 7 Mar 2026 09:08:53 -0800 Subject: [PATCH 10/27] Keep metric tiles on single horizontal row Use flex layout with wrap for the metrics tile group so tiles stay in one row when there is enough width and wrap on narrow viewports. --- .../features/home/cfhome-card/cfhome-card.component.scss | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/frontend/packages/cloud-foundry/src/features/home/cfhome-card/cfhome-card.component.scss b/src/frontend/packages/cloud-foundry/src/features/home/cfhome-card/cfhome-card.component.scss index a80c13761b..f3a2f9c22d 100644 --- a/src/frontend/packages/cloud-foundry/src/features/home/cfhome-card/cfhome-card.component.scss +++ b/src/frontend/packages/cloud-foundry/src/features/home/cfhome-card/cfhome-card.component.scss @@ -1,8 +1,16 @@ .cf-home-card { &__plain-tiles { + display: flex !important; + flex-wrap: wrap; + gap: 0.5rem; margin-left: 0; margin-right: 1em; margin-top: 1em; + + > app-tile { + flex: 1 1 120px; + min-width: 120px; + } } &__no-apps { align-items: center; From 60c8094689cdbf473aefbf04e0ce00bb0fdbd6da Mon Sep 17 00:00:00 2001 From: Norman Abramovitz Date: Sat, 7 Mar 2026 09:12:22 -0800 Subject: [PATCH 11/27] Reduce sidebar width in multi-column layout Use w-48 (192px) instead of w-80 for the favorites/shortcuts sidebar when cards are in a multi-column grid. Prevents the sidebar from compressing main card content. --- .../home-page-endpoint-card.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/packages/core/src/features/home/home/home-page-endpoint-card/home-page-endpoint-card.component.html b/src/frontend/packages/core/src/features/home/home/home-page-endpoint-card/home-page-endpoint-card.component.html index 518aac82bc..1fad9362ed 100644 --- a/src/frontend/packages/core/src/features/home/home/home-page-endpoint-card/home-page-endpoint-card.component.html +++ b/src/frontend/packages/core/src/features/home/home/home-page-endpoint-card/home-page-endpoint-card.component.html @@ -54,7 +54,7 @@

} @if (!fullView) { -