diff --git a/.github/workflows/packages_publishing.yml b/.github/workflows/packages_publishing.yml index e5a7b4ef6822..54412b53bb30 100644 --- a/.github/workflows/packages_publishing.yml +++ b/.github/workflows/packages_publishing.yml @@ -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: @@ -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<> "$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 @@ -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 @@ -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 @/-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}' @@ -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 @/-sbom npm package to GitHub Packages. + # Produces @/-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