Skip to content
143 changes: 132 additions & 11 deletions .github/workflows/packages_publishing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ env:
NX_SKIP_NX_CACHE: true
FILTER: ${{ github.event_name == 'workflow_dispatch' && inputs.filter || '' }}
SET_TIMESTAMP_VERSION: ${{ inputs.tag == 'daily' }}
SBOM_PACKAGE_NAMES: devextreme,devextreme-angular,devextreme-react,devextreme-vue,devextreme-themebuilder

jobs:
build:
Expand Down Expand Up @@ -52,40 +53,100 @@ jobs:
BUILD_INTERNAL_PACKAGE: true
run: pnpm run all:build

# Builds the dx-make-sbom package argument list from known package names and the tgz files produced in artifacts/npm.
# Produces SBOM_PACKAGES for the SBOM build step.
- name: Prepare SBOM package inputs
run: |
package_version=$(node -p "require('./package.json').version")
IFS=',' read -ra package_names <<< "$SBOM_PACKAGE_NAMES"
sbom_packages=()

for package_name in "${package_names[@]}"; do
tgz_path="artifacts/npm/$package_name-$package_version.tgz"

if [ ! -f "$tgz_path" ]; then
echo "Expected package tarball not found: $tgz_path"
exit 1
fi

sbom_packages+=("$package_name(../../$tgz_path)")
done

sbom_packages_value=$(IFS=,; echo "${sbom_packages[*]}")
echo "SBOM_PACKAGES=$sbom_packages_value" >> "$GITHUB_ENV"
echo "$sbom_packages_value"

- name: Set GitHub Packages auth
run: pnpm set //npm.pkg.github.com/:_authToken='${NODE_AUTH_TOKEN}'

# Generates CycloneDX SBOM JSON files for the selected packages using the just-built tgz files.
# Produces packages/sbom/dist/*.cdx.json.
- name: Build SBOMs
working-directory: packages/sbom
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: pnpm nx build sbom
run: |
pnpm install --frozen-lockfile
rm -rf dist/
pnpm dx-make-sbom ../../ dist/ "$SBOM_PACKAGES"
cp dist/devextreme.cdx.json dist/devextreme-dist.cdx.json

# Collects concrete SBOM file paths for validation because the shared action expects explicit file names.
# Produces the sbomFiles.outputs.files multiline output.
- name: Collect SBOM files
id: sbomFiles
run: |
shopt -s nullglob
sbom_files=(packages/sbom/dist/*.cdx.json)

if [ ${#sbom_files[@]} -eq 0 ]; then
echo "No SBOM files found in packages/sbom/dist"
exit 1
fi

{
echo "files<<EOF"
printf '%s\n' "${sbom_files[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"

# Validates every generated CycloneDX SBOM file with the shared validation action.
# Produces no artifact; fails the workflow if any SBOM is invalid.
- name: Validate SBOMs
uses: DevExpress/github-actions/validate-sbom@5034a6d5e0fd18fc2826ed20a5140f9c83b8994f
with:
input-format: json
input-files: ${{ steps.sbomFiles.outputs.files }}

- name: Build artifacts package
run: pnpm run make-artifacts-package

- name: Upload SBOM artifact
# Saves generated SBOM files for the publish job.
# Produces the sbom-packages workflow artifact.
- name: Upload SBOMs
uses: actions/upload-artifact@v7
with:
name: sbom
name: sbom-packages
path: packages/sbom/dist
retention-days: 7
if-no-files-found: error
retention-days: 1

- name: Upload packages
uses: actions/upload-artifact@v7
with:
name: packages
name: npm-packages
path: artifacts/npm/*.tgz
retention-days: 2
if-no-files-found: error
retention-days: 1

- name: Filter packages
id: filter
working-directory: artifacts/npm
shell: bash
run: ls *.tgz | grep -E -i "$FILTER" | sed -r 's/^(.*).tgz$/"\1"/g' | paste -sd "," - | sed -r 's/(.*)/packages=[\1]/' >> "$GITHUB_OUTPUT"

publish:
name: Publish package
runs-on: ubuntu-latest
runs-on: ubuntu-slim
needs: build
strategy:
fail-fast: false
Expand All @@ -95,10 +156,19 @@ jobs:
- name: Get sources
uses: actions/checkout@v6

- name: Download artifacts
- name: Download packages
uses: actions/download-artifact@v8
with:
name: packages
name: npm-packages
path: npm-packages

# Restores generated SBOM files from the build job.
# Produces the local sbom-packages directory for matrix publishing.
- name: Download SBOMs
uses: actions/download-artifact@v8
with:
name: sbom-packages
path: sbom-packages

- name: Use Node.js
uses: actions/setup-node@v6
Expand All @@ -118,13 +188,50 @@ jobs:
PACKAGE: ${{ matrix.package }}
run: |
SCOPE=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]');
PACKAGE_DIR=$(pnpm --silent run change-package-scope --tgz $PACKAGE.tgz --scope $SCOPE)
PACKAGE_DIR=$(pnpm --silent run change-package-scope --tgz npm-packages/$PACKAGE.tgz --scope $SCOPE)
echo "packageDir=$PACKAGE_DIR" >> "$GITHUB_OUTPUT";
cd $PACKAGE_DIR;
pnpm pkg get name | tr -d '"' | sed -r 's/(.*)/name=\1/' >> "$GITHUB_OUTPUT";
pnpm pkg get version | tr -d '"' | sed -r 's/(.*)/version=\1/' >> "$GITHUB_OUTPUT";
pnpm pkg get version | tr -d '"' | sed -r 's/([0-9]+\.[0-9]+).*/majorVersion=\1/' >> "$GITHUB_OUTPUT";

# Wraps the matching SBOM JSON file into a minimal @<owner>/<package>-sbom npm package when the matrix package has an SBOM.
# Produces sbomPackage outputs used by the publish step.
- name: Build SBOM package
id: sbomPackage
env:
PACKAGE_NAME: ${{ steps.scopedPackage.outputs.name }}
PACKAGE_VERSION: ${{ steps.scopedPackage.outputs.version }}
run: |
UNSCOPED_PACKAGE_NAME=$(echo "$PACKAGE_NAME" | sed -r 's#^@[^/]+/##');
SBOM_FILE="sbom-packages/$UNSCOPED_PACKAGE_NAME.cdx.json";

if [[ ",$SBOM_PACKAGE_NAMES," != *",$UNSCOPED_PACKAGE_NAME,"* ]]; then
echo "SBOM publishing is not configured for $UNSCOPED_PACKAGE_NAME"
echo "hasSbom=false" >> "$GITHUB_OUTPUT";
exit 0;
fi

if [ ! -f "$SBOM_FILE" ]; then
echo "No SBOM found for $UNSCOPED_PACKAGE_NAME"
echo "hasSbom=false" >> "$GITHUB_OUTPUT";
exit 0;
fi

OWNER=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]');
SBOM_PACKAGE_NAME="@$OWNER/$UNSCOPED_PACKAGE_NAME-sbom";
SBOM_PACKAGE_DIR="sbom-package/$SBOM_PACKAGE_NAME";

mkdir -p "$SBOM_PACKAGE_DIR";
cp "$SBOM_FILE" "$SBOM_PACKAGE_DIR/";
cd "$SBOM_PACKAGE_DIR";
node -e "const fs = require('fs'); const [name, version] = process.argv.slice(1); fs.writeFileSync('package.json', JSON.stringify({ name, version }, null, 2));" "$SBOM_PACKAGE_NAME" "$PACKAGE_VERSION";
echo "hasSbom=true" >> "$GITHUB_OUTPUT";
echo "packageDir=$PWD" >> "$GITHUB_OUTPUT";
pnpm pkg get name | tr -d '"' | sed -r 's/(.*)/name=\1/' >> "$GITHUB_OUTPUT";
pnpm pkg get version | tr -d '"' | sed -r 's/(.*)/version=\1/' >> "$GITHUB_OUTPUT";
pnpm pkg get version | tr -d '"' | sed -r 's/([0-9]+\.[0-9]+).*/majorVersion=\1/' >> "$GITHUB_OUTPUT";

- name: Set GitHub Packages auth
run: pnpm set //npm.pkg.github.com/:_authToken='${NODE_AUTH_TOKEN}'

Expand All @@ -142,6 +249,20 @@ jobs:
pnpm publish --no-git-checks --quiet --ignore-scripts --tag $PACKAGE_VERSION_MAJOR-${{ inputs.tag }} --registry https://npm.pkg.github.com;
pnpm dist-tag add $PACKAGE_NAME@$PACKAGE_VERSION latest --registry=https://npm.pkg.github.com;

# Publishes the generated @<owner>/<package>-sbom npm package to GitHub Packages.
# Produces @<owner>/<package>-sbom in the npm.pkg.github.com feed.
- name: Publish SBOM to GitHub Packages
if: ${{ steps.sbomPackage.outputs.hasSbom == 'true' }}
working-directory: ${{ steps.sbomPackage.outputs.packageDir }}
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PACKAGE_NAME: ${{ steps.sbomPackage.outputs.name }}
PACKAGE_VERSION: ${{ steps.sbomPackage.outputs.version }}
PACKAGE_VERSION_MAJOR: ${{ steps.sbomPackage.outputs.majorVersion }}
run: |
pnpm publish --no-git-checks --quiet --ignore-scripts --tag $PACKAGE_VERSION_MAJOR-${{ inputs.tag }} --registry https://npm.pkg.github.com;
pnpm dist-tag add $PACKAGE_NAME@$PACKAGE_VERSION latest --registry=https://npm.pkg.github.com;

notify:
runs-on: ubuntu-latest
name: Send notifications
Expand Down
Loading