diff --git a/.github/workflows/code_scanning.yml b/.github/workflows/code_scanning.yml index b714c141b..377858661 100644 --- a/.github/workflows/code_scanning.yml +++ b/.github/workflows/code_scanning.yml @@ -27,7 +27,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Prepare local CodeQL model packs run: | @@ -92,7 +92,7 @@ jobs: - name: Upload sarif change if: steps.validate.outcome != 'success' - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: sarif path: | diff --git a/.github/workflows/release-codeql.yml b/.github/workflows/release-codeql.yml new file mode 100644 index 000000000..4e09fef19 --- /dev/null +++ b/.github/workflows/release-codeql.yml @@ -0,0 +1,186 @@ +name: Release CodeQL - Publish and Bundle CodeQL Packs + +on: + workflow_call: + inputs: + publish_codeql_packs: + default: true + description: 'Publish CodeQL packs to GHCR. Disable for pre-release or re-run scenarios where packs already exist.' + required: false + type: boolean + version: + description: 'Release version tag (e.g., vX.Y.Z or vX.Y.Z-suffix). Must start with "v".' + required: true + type: string + outputs: + release_name: + description: 'The release name without "v" prefix (e.g., X.Y.Z or X.Y.Z-alpha)' + value: ${{ jobs.publish-codeql-packs.outputs.release_name }} + version: + description: 'The full version string with "v" prefix (e.g., vX.Y.Z or vX.Y.Z-alpha)' + value: ${{ jobs.publish-codeql-packs.outputs.version }} + +# Note: This workflow is called exclusively via workflow_call from release.yml. +# It does NOT have a workflow_dispatch trigger to keep release.yml as the single +# entry point for all release operations. To re-publish CodeQL packs standalone, +# use workflow_dispatch on release.yml with create_github_release=false. + +permissions: + contents: read + +jobs: + publish-codeql-packs: + name: Publish and Bundle CodeQL Packs + runs-on: ubuntu-latest + + environment: release-codeql + + permissions: + contents: read + packages: write + + outputs: + release_name: ${{ steps.version.outputs.release_name }} + version: ${{ steps.version.outputs.version }} + + env: + PUBLISHABLE_PACKS_LIST: | + javascript/frameworks/cap/src + javascript/frameworks/cap/ext + javascript/frameworks/cap/lib + javascript/frameworks/ui5/src + javascript/frameworks/ui5/ext + javascript/frameworks/ui5/lib + javascript/frameworks/xsjs/src + javascript/frameworks/xsjs/ext + javascript/frameworks/xsjs/lib + javascript/heuristic-models/ext + + steps: + - name: CodeQL - Validate and parse version + id: version + run: | + VERSION="${{ inputs.version }}" + if [[ ! "${VERSION}" =~ ^v ]]; then + echo "::error::Version '${VERSION}' must start with 'v'" + exit 1 + fi + echo "version=${VERSION}" >> $GITHUB_OUTPUT + echo "release_name=${VERSION#v}" >> $GITHUB_OUTPUT + + - name: CodeQL - Checkout tag + uses: actions/checkout@v6 + with: + ref: refs/tags/${{ steps.version.outputs.version }} + + - name: CodeQL - Install QLT + id: install-qlt + uses: advanced-security/codeql-development-toolkit/.github/actions/install-qlt@main + with: + qlt-version: 'latest' + add-to-path: true + + - name: CodeQL - Install CodeQL + shell: bash + run: | + echo "Installing CodeQL" + qlt codeql run install + echo "-----------------------------" + echo "CodeQL Home: $QLT_CODEQL_HOME" + echo "CodeQL Binary: $QLT_CODEQL_PATH" + + - name: CodeQL - Install pack dependencies + shell: bash + run: | + export PATH="$(dirname "$QLT_CODEQL_PATH"):$PATH" + chmod +x ./scripts/install-packs.sh + ./scripts/install-packs.sh + + - name: CodeQL - Validate version consistency + run: | + RELEASE_NAME="${{ steps.version.outputs.release_name }}" + echo "Validating all version-bearing files match ${RELEASE_NAME}..." + chmod +x ./scripts/update-release-version.sh + ./scripts/update-release-version.sh --check "${RELEASE_NAME}" + + - name: CodeQL - Publish CodeQL packs + if: inputs.publish_codeql_packs + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Read the shared pack list from the job-level environment variable. + mapfile -t PUBLISHABLE_PACKS <<< "${PUBLISHABLE_PACKS_LIST}" + + echo "Publishing CodeQL packs..." + for pack_dir in "${PUBLISHABLE_PACKS[@]}"; do + if [ -d "${pack_dir}" ]; then + pack_name=$(grep -m1 "^name:" "${pack_dir}/qlpack.yml" | awk '{print $2}') + echo "đŸ“Ļ Publishing ${pack_name} from ${pack_dir}..." + $QLT_CODEQL_PATH pack publish --threads=-1 -- "${pack_dir}" + echo "✅ Published ${pack_name}" + else + echo "âš ī¸ Skipping: ${pack_dir} not found" + fi + done + + - name: CodeQL - Skip pack publishing + if: '!inputs.publish_codeql_packs' + run: echo "â­ī¸ CodeQL pack publishing disabled via workflow input" + + - name: CodeQL - Bundle CodeQL packs + run: | + mkdir -p dist-packs + + # Bundle all publishable packs + # Read the pack list from the environment into a Bash array. + # Each line in PUBLISHABLE_PACKS_LIST becomes one element. + mapfile -t PUBLISHABLE_PACKS <<< "${PUBLISHABLE_PACKS_LIST}" + + echo "Bundling CodeQL packs..." + for pack_dir in "${PUBLISHABLE_PACKS[@]}"; do + if [ -d "${pack_dir}" ]; then + pack_name=$(grep -m1 "^name:" "${pack_dir}/qlpack.yml" | awk '{print $2}') + # Convert pack name to filename: advanced-security/foo -> foo + bundle_name="${pack_name#advanced-security/}" + output="dist-packs/${bundle_name}.tar.gz" + echo "đŸ“Ļ Bundling ${pack_name} -> ${output}..." + $QLT_CODEQL_PATH pack bundle --threads=-1 --output="${output}" -- "${pack_dir}" + echo "✅ Bundled ${bundle_name}" + fi + done + echo "" + echo "Bundled packs:" + ls -lh dist-packs/ + + - name: CodeQL - Upload pack artifacts + uses: actions/upload-artifact@v6 + with: + name: codeql-pack-bundles-${{ steps.version.outputs.version }} + path: dist-packs/*.tar.gz + + - name: CodeQL - Summary + run: | + VERSION="${{ steps.version.outputs.version }}" + RELEASE_NAME="${{ steps.version.outputs.release_name }}" + echo "## CodeQL Packs Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${{ inputs.publish_codeql_packs }}" == "true" ]; then + echo "✅ Published CodeQL packs to GHCR" >> $GITHUB_STEP_SUMMARY + else + echo "â­ī¸ CodeQL pack publishing was disabled" >> $GITHUB_STEP_SUMMARY + fi + echo "✅ Bundled CodeQL packs as artifacts" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### CodeQL Packs" >> $GITHUB_STEP_SUMMARY + echo "| Pack | Version |" >> $GITHUB_STEP_SUMMARY + echo "| ---- | ------- |" >> $GITHUB_STEP_SUMMARY + echo "| \`advanced-security/javascript-sap-cap-queries\` | ${RELEASE_NAME} |" >> $GITHUB_STEP_SUMMARY + echo "| \`advanced-security/javascript-sap-cap-models\` | ${RELEASE_NAME} |" >> $GITHUB_STEP_SUMMARY + echo "| \`advanced-security/javascript-sap-cap-all\` | ${RELEASE_NAME} |" >> $GITHUB_STEP_SUMMARY + echo "| \`advanced-security/javascript-sap-ui5-queries\` | ${RELEASE_NAME} |" >> $GITHUB_STEP_SUMMARY + echo "| \`advanced-security/javascript-sap-ui5-models\` | ${RELEASE_NAME} |" >> $GITHUB_STEP_SUMMARY + echo "| \`advanced-security/javascript-sap-ui5-all\` | ${RELEASE_NAME} |" >> $GITHUB_STEP_SUMMARY + echo "| \`advanced-security/javascript-sap-xsjs-queries\` | ${RELEASE_NAME} |" >> $GITHUB_STEP_SUMMARY + echo "| \`advanced-security/javascript-sap-xsjs-models\` | ${RELEASE_NAME} |" >> $GITHUB_STEP_SUMMARY + echo "| \`advanced-security/javascript-sap-xsjs-all\` | ${RELEASE_NAME} |" >> $GITHUB_STEP_SUMMARY + echo "| \`advanced-security/javascript-heuristic-models\` | ${RELEASE_NAME} |" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml new file mode 100644 index 000000000..0c0c7d6b0 --- /dev/null +++ b/.github/workflows/release-tag.yml @@ -0,0 +1,227 @@ +name: Release Tag - Create Version Tag + +on: + workflow_call: + inputs: + version: + description: 'Release version (e.g., vX.Y.Z or vX.Y.Z-suffix). Must start with "v".' + required: true + type: string + outputs: + release_name: + description: 'The release name without "v" prefix (e.g., X.Y.Z or X.Y.Z-alpha)' + value: ${{ jobs.create-tag.outputs.release_name }} + tag_sha: + description: 'The commit SHA that the tag points to' + value: ${{ jobs.create-tag.outputs.tag_sha }} + version: + description: 'The full version string with "v" prefix (e.g., vX.Y.Z or vX.Y.Z-alpha)' + value: ${{ jobs.create-tag.outputs.version }} + +# Note: This workflow is called exclusively via workflow_call from release.yml. +# It does NOT have a workflow_dispatch trigger to keep release.yml as the single +# entry point for all release operations. + +permissions: + contents: read + +jobs: + create-tag: + name: Create Version Tag + runs-on: ubuntu-latest + + environment: release-tag + + permissions: + contents: write + + outputs: + release_name: ${{ steps.version.outputs.release_name }} + tag_sha: ${{ steps.final-sha.outputs.tag_sha }} + version: ${{ steps.version.outputs.version }} + + steps: + - name: Tag - Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Tag - Validate and parse version + id: version + run: | + VERSION="${{ inputs.version }}" + # Validate version starts with 'v' + if [[ ! "${VERSION}" =~ ^v ]]; then + echo "::error::Version '${VERSION}' must start with 'v'" + exit 1 + fi + echo "version=${VERSION}" >> $GITHUB_OUTPUT + echo "release_name=${VERSION#v}" >> $GITHUB_OUTPUT + + - name: Tag - Check if tag already exists + id: check-tag + run: | + TAG="${{ steps.version.outputs.version }}" + if git rev-parse "refs/tags/${TAG}" >/dev/null 2>&1; then + TAG_SHA=$(git rev-parse "refs/tags/${TAG}^{commit}" 2>/dev/null || git rev-parse "refs/tags/${TAG}") + echo "tag_exists=true" >> $GITHUB_OUTPUT + echo "tag_sha=${TAG_SHA}" >> $GITHUB_OUTPUT + echo "â„šī¸ Tag ${TAG} already exists at commit ${TAG_SHA:0:8}" + else + echo "tag_exists=false" >> $GITHUB_OUTPUT + echo "â„šī¸ Tag ${TAG} does not exist yet" + fi + + - name: Tag - Update release version + if: steps.check-tag.outputs.tag_exists != 'true' + run: | + TAG_VERSION="${{ steps.version.outputs.release_name }}" + echo "Updating all version-bearing files to '${TAG_VERSION}'..." + chmod +x ./scripts/update-release-version.sh + ./scripts/update-release-version.sh "${TAG_VERSION}" + + - name: Tag - Install QLT + if: steps.check-tag.outputs.tag_exists != 'true' + id: install-qlt + uses: advanced-security/codeql-development-toolkit/.github/actions/install-qlt@main + with: + qlt-version: 'latest' + add-to-path: true + + - name: Tag - Install CodeQL + if: steps.check-tag.outputs.tag_exists != 'true' + shell: bash + run: | + echo "Installing CodeQL" + qlt codeql run install + echo "-----------------------------" + echo "CodeQL Home: $QLT_CODEQL_HOME" + echo "CodeQL Binary: $QLT_CODEQL_PATH" + + - name: Tag - Upgrade CodeQL pack lock files + if: steps.check-tag.outputs.tag_exists != 'true' + shell: bash + run: | + echo "Upgrading CodeQL pack lock files" + find . -name "qlpack.yml" -type f | sort | while read -r qlpack_file; do + pack_dir=$(dirname "$qlpack_file") + echo "Upgrading pack in directory: $pack_dir" + cd "$pack_dir" + $QLT_CODEQL_PATH pack upgrade + cd - > /dev/null + done + echo "Finished upgrading all CodeQL pack lock files" + + - name: Tag - Install QL packs + if: steps.check-tag.outputs.tag_exists != 'true' + shell: bash + run: | + export PATH="$(dirname "$QLT_CODEQL_PATH"):$PATH" + chmod +x ./scripts/install-packs.sh + ./scripts/install-packs.sh + + - name: Tag - Setup Node.js for CDS compilation + if: steps.check-tag.outputs.tag_exists != 'true' + uses: actions/setup-node@v6 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: 'extractors/cds/tools/package-lock.json' + + - name: Tag - Compile CAP CDS files + if: steps.check-tag.outputs.tag_exists != 'true' + run: | + chmod +x ./extractors/cds/tools/workflow/cds-compilation-for-actions.sh + ./extractors/cds/tools/workflow/cds-compilation-for-actions.sh + + - name: Tag - Run CodeQL unit tests + if: steps.check-tag.outputs.tag_exists != 'true' + env: + LGTM_INDEX_XML_MODE: all + LGTM_INDEX_FILETYPES: ".json:JSON\n.cds:JSON" + shell: bash + run: | + echo "Running CodeQL unit tests to validate release..." + $QLT_CODEQL_PATH test run \ + --threads=0 \ + --strict-test-discovery \ + --additional-packs="${GITHUB_WORKSPACE}" \ + -- javascript/ + + - name: Tag - Validate version consistency + if: steps.check-tag.outputs.tag_exists != 'true' + run: | + RELEASE_NAME="${{ steps.version.outputs.release_name }}" + echo "Validating all version-bearing files match ${RELEASE_NAME}..." + ./scripts/update-release-version.sh --check "${RELEASE_NAME}" + + - name: Tag - Commit version changes and create tag + id: create-tag + if: steps.check-tag.outputs.tag_exists != 'true' + run: | + TAG="${{ steps.version.outputs.version }}" + RELEASE_NAME="${{ steps.version.outputs.release_name }}" + + # Configure git + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + # Stage version-bearing files and lockfile changes + git add -A + # Ensure generated artifacts (CodeQL, CAP compilation) are not staged for commit + git restore --staged .codeql || true + git restore --staged '*.qlx' || true + git restore --staged 'javascript/frameworks/cap/test/**/model.cds.json' || true + + # Check if there are changes to commit + if git diff --cached --quiet; then + echo "â„šī¸ No changes to commit (versions already up to date)" + CURRENT_SHA=$(git rev-parse HEAD) + else + git commit -m "Release ${TAG}: update versions to ${RELEASE_NAME}" + CURRENT_SHA=$(git rev-parse HEAD) + git push origin HEAD + echo "✅ Committed version changes at ${CURRENT_SHA:0:8}" + fi + + # Create and push the tag + git tag -a "${TAG}" -m "Release ${TAG}" "${CURRENT_SHA}" + git push origin "${TAG}" + echo "✅ Created and pushed tag ${TAG} at commit ${CURRENT_SHA:0:8}" + echo "tag_sha=${CURRENT_SHA}" >> $GITHUB_OUTPUT + + - name: Tag - Output existing tag SHA + id: existing-tag + if: steps.check-tag.outputs.tag_exists == 'true' + run: | + echo "tag_sha=${{ steps.check-tag.outputs.tag_sha }}" >> $GITHUB_OUTPUT + + - name: Tag - Set final tag SHA output + id: final-sha + run: | + if [ "${{ steps.check-tag.outputs.tag_exists }}" == "true" ]; then + SHA="${{ steps.check-tag.outputs.tag_sha }}" + else + SHA="${{ steps.create-tag.outputs.tag_sha }}" + fi + echo "tag_sha=${SHA}" >> $GITHUB_OUTPUT + + - name: Tag - Summary + run: | + TAG="${{ steps.version.outputs.version }}" + echo "## Release Tag Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${{ steps.check-tag.outputs.tag_exists }}" == "true" ]; then + echo "â„šī¸ Tag \`${TAG}\` already existed at \`${{ steps.check-tag.outputs.tag_sha }}\`" >> $GITHUB_STEP_SUMMARY + else + echo "✅ Created tag \`${TAG}\` at \`${{ steps.create-tag.outputs.tag_sha }}\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Step | Status |" >> $GITHUB_STEP_SUMMARY + echo "| ---- | ------ |" >> $GITHUB_STEP_SUMMARY + echo "| Version update | ✅ All files updated to ${{ steps.version.outputs.release_name }} |" >> $GITHUB_STEP_SUMMARY + echo "| Pack lock upgrade | ✅ Passed |" >> $GITHUB_STEP_SUMMARY + echo "| CodeQL unit tests | ✅ Passed |" >> $GITHUB_STEP_SUMMARY + echo "| Version consistency | ✅ All files match |" >> $GITHUB_STEP_SUMMARY + echo "| Tag creation | ✅ ${TAG} |" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..2e95e4789 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,171 @@ +name: Release - CodeQL SAP JavaScript + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + create_github_release: + default: true + description: 'Create GitHub Release with CodeQL pack bundles. Disable to only publish packs without creating a release.' + required: false + type: boolean + publish_codeql_packs: + default: true + description: 'Publish CodeQL packs to GHCR. Disable for pre-release or re-run scenarios where packs already exist. Packs are always bundled as release artifacts regardless of this setting.' + required: false + type: boolean + version: + description: 'Release version (e.g., vX.Y.Z or vX.Y.Z-suffix). Must start with "v". Supports pre-release suffixes like -alpha, -beta, -rc1.' + required: true + type: string + +permissions: + contents: read + +jobs: + # ───────────────────────────────────────────────────────────────────────────── + # Step 1: Determine the release version + # + # Resolves the version from either the tag push event or the workflow_dispatch + # input, and validates the format. This output is consumed by all downstream + # jobs. + # ───────────────────────────────────────────────────────────────────────────── + resolve-version: + name: Resolve Release Version + runs-on: ubuntu-latest + + outputs: + create_github_release: ${{ steps.resolve.outputs.create_github_release }} + publish_codeql_packs: ${{ steps.resolve.outputs.publish_codeql_packs }} + release_name: ${{ steps.resolve.outputs.release_name }} + version: ${{ steps.resolve.outputs.version }} + + steps: + - name: Version - Resolve and validate + id: resolve + run: | + if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + VERSION="${{ github.event.inputs.version }}" + else + VERSION="${{ github.ref_name }}" + fi + + # Validate version starts with 'v' + if [[ ! "${VERSION}" =~ ^v ]]; then + echo "::error::Version '${VERSION}' must start with 'v'" + exit 1 + fi + + # Resolve publish flags (default true for tag pushes) + if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + CREATE_RELEASE="${{ github.event.inputs.create_github_release }}" + PUBLISH_PACKS="${{ github.event.inputs.publish_codeql_packs }}" + else + CREATE_RELEASE="true" + PUBLISH_PACKS="true" + fi + + echo "version=${VERSION}" >> $GITHUB_OUTPUT + echo "release_name=${VERSION#v}" >> $GITHUB_OUTPUT + echo "create_github_release=${CREATE_RELEASE}" >> $GITHUB_OUTPUT + echo "publish_codeql_packs=${PUBLISH_PACKS}" >> $GITHUB_OUTPUT + + # ───────────────────────────────────────────────────────────────────────────── + # Step 2: Ensure the release tag exists + # + # For workflow_dispatch, ensures a properly validated tag exists. For tag push + # events, this is a no-op (tag already exists). The release-tag workflow + # handles version updates, pack lock upgrades, test validation, and tag + # creation. + # ───────────────────────────────────────────────────────────────────────────── + ensure-tag: + name: Ensure Release Tag + needs: resolve-version + permissions: + contents: write + uses: ./.github/workflows/release-tag.yml + with: + version: ${{ needs.resolve-version.outputs.version }} + + # ───────────────────────────────────────────────────────────────────────────── + # Step 3: Bundle and optionally publish CodeQL packs + # + # Checks out the clean tag, installs CodeQL, and bundles packs for release. + # Publishing to GHCR is controlled by the publish_codeql_packs flag; bundling + # always runs so that pack artifacts are available for the GitHub Release. + # ───────────────────────────────────────────────────────────────────────────── + publish-codeql: + name: Publish CodeQL Packs + needs: [resolve-version, ensure-tag] + permissions: + contents: read + packages: write + uses: ./.github/workflows/release-codeql.yml + with: + publish_codeql_packs: ${{ needs.resolve-version.outputs.publish_codeql_packs == 'true' }} + version: ${{ needs.resolve-version.outputs.version }} + + # ───────────────────────────────────────────────────────────────────────────── + # Step 4: Create GitHub Release + # + # Downloads the CodeQL pack bundles and creates the GitHub Release with + # release notes and attached artifacts. + # ───────────────────────────────────────────────────────────────────────────── + create-release: + name: Create GitHub Release + if: >- + always() && !failure() && !cancelled() + && needs.resolve-version.outputs.create_github_release == 'true' + needs: [resolve-version, ensure-tag, publish-codeql] + runs-on: ubuntu-latest + + permissions: + contents: write + + steps: + - name: Release - Download CodeQL pack artifacts + uses: actions/download-artifact@v7 + with: + name: codeql-pack-bundles-${{ needs.resolve-version.outputs.version }} + path: dist-packs + + - name: Release - Create GitHub Release + uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0 + with: + files: | + dist-packs/*.tar.gz + generate_release_notes: true + tag_name: ${{ needs.resolve-version.outputs.version }} + + - name: Release - Summary + run: | + VERSION="${{ needs.resolve-version.outputs.version }}" + RELEASE_NAME="${{ needs.resolve-version.outputs.release_name }}" + echo "## Release Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Step | Status |" >> $GITHUB_STEP_SUMMARY + echo "| ---- | ------ |" >> $GITHUB_STEP_SUMMARY + echo "| Tag | ✅ ${VERSION} |" >> $GITHUB_STEP_SUMMARY + echo "| Version validation | ✅ All files match ${RELEASE_NAME} |" >> $GITHUB_STEP_SUMMARY + if [ "${{ needs.resolve-version.outputs.publish_codeql_packs }}" == "true" ]; then + echo "| CodeQL pack publish | ✅ Published to GHCR |" >> $GITHUB_STEP_SUMMARY + else + echo "| CodeQL pack publish | â­ī¸ Skipped (packs bundled only) |" >> $GITHUB_STEP_SUMMARY + fi + echo "| GitHub Release | ✅ Created |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Published CodeQL Packs" >> $GITHUB_STEP_SUMMARY + echo "| Pack | Version |" >> $GITHUB_STEP_SUMMARY + echo "| ---- | ------- |" >> $GITHUB_STEP_SUMMARY + echo "| \`advanced-security/javascript-sap-cap-queries\` | ${RELEASE_NAME} |" >> $GITHUB_STEP_SUMMARY + echo "| \`advanced-security/javascript-sap-cap-models\` | ${RELEASE_NAME} |" >> $GITHUB_STEP_SUMMARY + echo "| \`advanced-security/javascript-sap-cap-all\` | ${RELEASE_NAME} |" >> $GITHUB_STEP_SUMMARY + echo "| \`advanced-security/javascript-sap-ui5-queries\` | ${RELEASE_NAME} |" >> $GITHUB_STEP_SUMMARY + echo "| \`advanced-security/javascript-sap-ui5-models\` | ${RELEASE_NAME} |" >> $GITHUB_STEP_SUMMARY + echo "| \`advanced-security/javascript-sap-ui5-all\` | ${RELEASE_NAME} |" >> $GITHUB_STEP_SUMMARY + echo "| \`advanced-security/javascript-sap-xsjs-queries\` | ${RELEASE_NAME} |" >> $GITHUB_STEP_SUMMARY + echo "| \`advanced-security/javascript-sap-xsjs-models\` | ${RELEASE_NAME} |" >> $GITHUB_STEP_SUMMARY + echo "| \`advanced-security/javascript-sap-xsjs-all\` | ${RELEASE_NAME} |" >> $GITHUB_STEP_SUMMARY + echo "| \`advanced-security/javascript-heuristic-models\` | ${RELEASE_NAME} |" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/update-codeql.yml b/.github/workflows/update-codeql.yml index e68907f7b..40f3686a1 100644 --- a/.github/workflows/update-codeql.yml +++ b/.github/workflows/update-codeql.yml @@ -1,111 +1,153 @@ -name: "Update the CodeQL CLI dependencies" +name: Update CodeQL CLI Dependencies on: - workflow_dispatch: - # nightly runs to update the CodeQL CLI dependencies - schedule: - - cron: '30 0 * * *' + workflow_dispatch: + # Nightly check for new CodeQL CLI releases + schedule: + - cron: '30 0 * * *' permissions: - contents: write - pull-requests: write + contents: read jobs: - update-codeql: - name: Update CodeQL CLI dependencies - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Check latest CodeQL CLI version and update qlt.conf.json - id: check-version - env: - GH_TOKEN: ${{ github.token }} - run: | - echo "Checking latest CodeQL CLI version" - current_version=$(jq .CodeQLCLI qlt.conf.json -r) - latest_version=$(gh release list --repo github/codeql-cli-binaries --json 'tagName,isLatest' --jq '.[] | select(.isLatest == true) | .tagName') - echo "Current CodeQL CLI version: $current_version" - echo "Latest CodeQL CLI version: $latest_version" - - # Remove 'v' prefix if present for comparison with current version - latest_clean=$(echo "$latest_version" | sed 's/^v//') - - if [ "$latest_clean" != "$current_version" ]; then - echo "Updating CodeQL CLI from $current_version to $latest_clean" - echo "update_needed=true" >> $GITHUB_OUTPUT - echo "latest_version=$latest_clean" >> $GITHUB_OUTPUT - echo "latest_version_tag=$latest_version" >> $GITHUB_OUTPUT - - # Update qlt.conf.json with all properties - echo "Updating qlt.conf.json with all properties for version $latest_clean" - jq --arg cli_version "$latest_clean" \ - --arg std_lib "codeql-cli/$latest_version" \ - --arg bundle "codeql-bundle-$latest_version" \ - '.CodeQLCLI = $cli_version | .CodeQLStandardLibrary = $std_lib | .CodeQLCLIBundle = $bundle' \ - qlt.conf.json > qlt.conf.json.tmp && mv qlt.conf.json.tmp qlt.conf.json - - echo "Updated qlt.conf.json contents:" - cat qlt.conf.json - else - echo "CodeQL CLI is already up-to-date at version $current_version." - echo "update_needed=false" >> $GITHUB_OUTPUT - fi - - - name: Install QLT - if: steps.check-version.outputs.update_needed == 'true' - id: install-qlt - uses: advanced-security/codeql-development-toolkit/.github/actions/install-qlt@main - with: - qlt-version: 'latest' - add-to-path: true - - - name: Install CodeQL - if: steps.check-version.outputs.update_needed == 'true' - id: install-codeql - shell: bash - run: | - echo "Installing CodeQL" - qlt codeql run install - echo "-----------------------------" - echo "CodeQL Home: $QLT_CODEQL_HOME" - echo "CodeQL Binary: $QLT_CODEQL_PATH" - - - name: Upgrade CodeQL pack lock files - if: steps.check-version.outputs.update_needed == 'true' - shell: bash - run: | - echo "Upgrading CodeQL pack lock files" - echo "Finding all directories with qlpack.yml files..." - - # Find all directories containing qlpack.yml files - find . -name "qlpack.yml" -type f | while read -r qlpack_file; do - pack_dir=$(dirname "$qlpack_file") - echo "Upgrading pack in directory: $pack_dir" - - # Change to the directory and run codeql pack upgrade - cd "$pack_dir" - $QLT_CODEQL_PATH pack upgrade - cd - > /dev/null - done - - echo "Finished upgrading all CodeQL pack lock files" - - - name: Create Pull Request - if: steps.check-version.outputs.update_needed == 'true' - uses: peter-evans/create-pull-request@v8 - with: - title: "Upgrade CodeQL CLI dependency to ${{ steps.check-version.outputs.latest_version_tag }}" - body: | - This PR upgrades the CodeQL CLI version to ${{ steps.check-version.outputs.latest_version_tag }}. - - **Changes made:** - - Updated `CodeQLCLI` to `${{ steps.check-version.outputs.latest_version }}` - - Updated `CodeQLStandardLibrary` to `codeql-cli/${{ steps.check-version.outputs.latest_version_tag }}` - - Updated `CodeQLCLIBundle` to `codeql-bundle-${{ steps.check-version.outputs.latest_version_tag }}` - - Upgraded all CodeQL pack lock files using `codeql pack upgrade` - commit-message: "Upgrade CodeQL CLI dependency to ${{ steps.check-version.outputs.latest_version_tag }}" - delete-branch: true - branch: "codeql/upgrade-to-${{ steps.check-version.outputs.latest_version_tag }}" + # ───────────────────────────────────────────────────────────────────────────── + # Step 1: Detect new CodeQL CLI version + # + # Compares the current CodeQL CLI version in qlt.conf.json against the latest + # release from github/codeql-cli-binaries. If a newer version is available, + # downstream jobs orchestrate a full release using the same child workflows + # as release.yml, guarded by environment approval gates. + # ───────────────────────────────────────────────────────────────────────────── + detect-update: + name: Detect CodeQL CLI Update + runs-on: ubuntu-latest + + outputs: + current_version: ${{ steps.check-version.outputs.current_version }} + latest_version: ${{ steps.check-version.outputs.latest_version }} + update_needed: ${{ steps.check-version.outputs.update_needed }} + version: ${{ steps.check-version.outputs.version }} + + steps: + - name: Detect - Checkout repository + uses: actions/checkout@v6 + + - name: Detect - Check latest CodeQL CLI version + id: check-version + env: + GH_TOKEN: ${{ github.token }} + run: | + echo "Checking latest CodeQL CLI version..." + current_version=$(jq -r .CodeQLCLI qlt.conf.json) + latest_tag=$(gh release list --repo github/codeql-cli-binaries --json 'tagName,isLatest' --jq '.[] | select(.isLatest == true) | .tagName') + latest_clean="${latest_tag#v}" + + echo "Current CodeQL CLI version: ${current_version}" + echo "Latest CodeQL CLI version: ${latest_clean}" + + if [ "${latest_clean}" != "${current_version}" ]; then + echo "✅ Update available: ${current_version} → ${latest_clean}" + echo "update_needed=true" >> $GITHUB_OUTPUT + echo "current_version=${current_version}" >> $GITHUB_OUTPUT + echo "latest_version=${latest_clean}" >> $GITHUB_OUTPUT + echo "version=v${latest_clean}" >> $GITHUB_OUTPUT + else + echo "â„šī¸ CodeQL CLI is already up-to-date at version ${current_version}" + echo "update_needed=false" >> $GITHUB_OUTPUT + fi + + - name: Detect - Summary + run: | + echo "## CodeQL CLI Update Check" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${{ steps.check-version.outputs.update_needed }}" == "true" ]; then + echo "✅ Update available: ${{ steps.check-version.outputs.current_version }} → ${{ steps.check-version.outputs.latest_version }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Initiating release pipeline for \`v${{ steps.check-version.outputs.latest_version }}\`..." >> $GITHUB_STEP_SUMMARY + else + echo "â„šī¸ CodeQL CLI is already up-to-date. No release needed." >> $GITHUB_STEP_SUMMARY + fi + + # ───────────────────────────────────────────────────────────────────────────── + # Step 2: Create release tag + # + # Calls the same release-tag workflow used by release.yml. This ensures the + # version update, CodeQL installation, pack lock upgrade, unit tests, and tag + # creation all follow the same validated process. + # + # The release-tag environment approval gate provides human-in-the-loop review + # before any changes are committed. + # ───────────────────────────────────────────────────────────────────────────── + ensure-tag: + name: Ensure Release Tag + needs: detect-update + if: needs.detect-update.outputs.update_needed == 'true' + permissions: + contents: write + uses: ./.github/workflows/release-tag.yml + with: + version: ${{ needs.detect-update.outputs.version }} + + # ───────────────────────────────────────────────────────────────────────────── + # Step 3: Publish and bundle CodeQL packs + # + # Calls the same release-codeql workflow used by release.yml. Publishes packs + # to GHCR and bundles them as artifacts for the GitHub Release. + # ───────────────────────────────────────────────────────────────────────────── + publish-codeql: + name: Publish CodeQL Packs + needs: [detect-update, ensure-tag] + if: needs.detect-update.outputs.update_needed == 'true' + permissions: + contents: read + packages: write + uses: ./.github/workflows/release-codeql.yml + with: + publish_codeql_packs: true + version: ${{ needs.detect-update.outputs.version }} + + # ───────────────────────────────────────────────────────────────────────────── + # Step 4: Create GitHub Release + # + # Downloads the CodeQL pack bundles and creates the GitHub Release with + # auto-generated release notes and attached pack artifacts. + # ───────────────────────────────────────────────────────────────────────────── + create-release: + name: Create GitHub Release + needs: [detect-update, ensure-tag, publish-codeql] + if: >- + always() && !failure() && !cancelled() + && needs.detect-update.outputs.update_needed == 'true' + runs-on: ubuntu-latest + + permissions: + contents: write + + steps: + - name: Release - Download CodeQL pack artifacts + uses: actions/download-artifact@v7 + with: + name: codeql-pack-bundles-${{ needs.detect-update.outputs.version }} + path: dist-packs + + - name: Release - Create GitHub Release + uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0 + with: + files: | + dist-packs/*.tar.gz + generate_release_notes: true + tag_name: ${{ needs.detect-update.outputs.version }} + + - name: Release - Summary + run: | + VERSION="${{ needs.detect-update.outputs.version }}" + RELEASE_NAME="${{ needs.detect-update.outputs.latest_version }}" + echo "## Automated Release Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Triggered by CodeQL CLI update: ${{ needs.detect-update.outputs.current_version }} → ${RELEASE_NAME}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Step | Status |" >> $GITHUB_STEP_SUMMARY + echo "| ---- | ------ |" >> $GITHUB_STEP_SUMMARY + echo "| Tag | ✅ ${VERSION} |" >> $GITHUB_STEP_SUMMARY + echo "| CodeQL pack publish | ✅ Published to GHCR |" >> $GITHUB_STEP_SUMMARY + echo "| GitHub Release | ✅ Created |" >> $GITHUB_STEP_SUMMARY diff --git a/.gitignore b/.gitignore index b3593c113..533f3b6dc 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,10 @@ dbs *.cds.json .cds-extractor-cache +# CodeQL-generated artifacts +.codeql/ +*.qlx + +# workspace customization file +codeql-sap-js.code-workspace + diff --git a/scripts/install-packs.sh b/scripts/install-packs.sh new file mode 100755 index 000000000..0ebad75c1 --- /dev/null +++ b/scripts/install-packs.sh @@ -0,0 +1,126 @@ +#!/usr/bin/env bash +set -euo pipefail + +## install-packs.sh +## Install CodeQL pack dependencies for all packs in the codeql-sap-js repository. +## +## This script installs dependencies for both source and test packs, using +## --additional-packs for workspace-local resolution of internal pack references. +## +## Usage: +## ./scripts/install-packs.sh +## ./scripts/install-packs.sh --framework cap +## ./scripts/install-packs.sh --framework ui5 + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" + +FRAMEWORK="" + +usage() { + cat < Install packs only for the specified framework + Valid values: cap, ui5, ui5-webcomponents, xsjs, heuristic-models + -h, --help Show this help message + +By default, the script installs packs for all frameworks. +EOF +} + +while [[ $# -gt 0 ]]; do + case $1 in + --framework) + if [[ $# -lt 2 || "${2-}" == -* ]]; then + echo "Error: --framework requires a value" >&2 + usage >&2 + exit 1 + fi + FRAMEWORK="$2" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Error: Unknown option $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +## Validate framework if provided +VALID_FRAMEWORKS=("cap" "ui5" "ui5-webcomponents" "xsjs" "heuristic-models") +if [[ -n "${FRAMEWORK}" ]]; then + FRAMEWORK_VALID=false + for valid_fw in "${VALID_FRAMEWORKS[@]}"; do + if [[ "${FRAMEWORK}" == "${valid_fw}" ]]; then + FRAMEWORK_VALID=true + break + fi + done + + if [[ "${FRAMEWORK_VALID}" == false ]]; then + echo "Error: Invalid framework '${FRAMEWORK}'" >&2 + echo "Valid frameworks: ${VALID_FRAMEWORKS[*]}" >&2 + exit 1 + fi +fi + +cd "${REPO_ROOT}" + +## Install packs for a given qlpack.yml directory +install_pack() { + local pack_dir="$1" + if [[ -d "${pack_dir}" ]]; then + echo "INFO: Running 'codeql pack install' for '${pack_dir}'..." + codeql pack install --no-strict-mode --additional-packs="${REPO_ROOT}/javascript" -- "${pack_dir}" + else + echo "WARNING: Directory '${pack_dir}' not found, skipping" >&2 + fi +} + +## Install packs for a framework (all subdirectories that contain qlpack.yml) +install_framework() { + local framework_path="$1" + echo "Installing packs for: ${framework_path}" + + # Find all qlpack.yml files under this framework and install their packs + find "${REPO_ROOT}/${framework_path}" -name "qlpack.yml" -type f | sort | while read -r qlpack_file; do + local pack_dir + pack_dir=$(dirname "${qlpack_file}") + # Use relative path for cleaner output + local rel_path="${pack_dir#${REPO_ROOT}/}" + install_pack "${rel_path}" + done +} + +if [[ -n "${FRAMEWORK}" ]]; then + case "${FRAMEWORK}" in + heuristic-models) + install_framework "javascript/heuristic-models" + ;; + ui5-webcomponents) + install_framework "javascript/frameworks/ui5-webcomponents" + ;; + *) + install_framework "javascript/frameworks/${FRAMEWORK}" + ;; + esac +else + echo "Installing packs for all frameworks..." + install_framework "javascript/frameworks/cap" + install_framework "javascript/frameworks/ui5" + install_framework "javascript/frameworks/ui5-webcomponents" + install_framework "javascript/frameworks/xsjs" + install_framework "javascript/heuristic-models" +fi + +echo "" +echo "✅ All CodeQL pack dependencies installed successfully." diff --git a/scripts/update-release-version.sh b/scripts/update-release-version.sh new file mode 100755 index 000000000..156ff7dd3 --- /dev/null +++ b/scripts/update-release-version.sh @@ -0,0 +1,395 @@ +#!/usr/bin/env bash +set -euo pipefail + +## update-release-version.sh +## Deterministically updates the release version across all version-bearing files +## in the codeql-sap-js repository. +## +## Version-bearing files (15 qlpack.yml files): +## javascript/frameworks/cap/ext/qlpack.yml +## javascript/frameworks/cap/lib/qlpack.yml +## javascript/frameworks/cap/src/qlpack.yml +## javascript/frameworks/cap/test/qlpack.yml +## javascript/frameworks/ui5/ext/qlpack.yml +## javascript/frameworks/ui5/lib/qlpack.yml +## javascript/frameworks/ui5/src/qlpack.yml +## javascript/frameworks/ui5/test/qlpack.yml +## javascript/frameworks/ui5-webcomponents/test/qlpack.yml +## javascript/frameworks/xsjs/ext/qlpack.yml +## javascript/frameworks/xsjs/lib/qlpack.yml +## javascript/frameworks/xsjs/src/qlpack.yml +## javascript/frameworks/xsjs/test/qlpack.yml +## javascript/heuristic-models/ext/qlpack.yml +## javascript/heuristic-models/tests/qlpack.yml +## +## Additionally updates: +## - Internal dependency references within qlpack.yml files +## that reference other packs in this repository (e.g., ^X.Y.Z constraints). +## - qlt.conf.json (CodeQLCLI, CodeQLStandardLibrary, CodeQLCLIBundle) +## using the base version (X.Y.Z) derived by stripping any pre-release suffix. +## +## Usage: +## ./scripts/update-release-version.sh +## ./scripts/update-release-version.sh --check [] +## +## Examples: +## ./scripts/update-release-version.sh 2.4.0 +## ./scripts/update-release-version.sh 2.4.0-rc1 +## ./scripts/update-release-version.sh --check +## ./scripts/update-release-version.sh --check 2.4.0 + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" + +## All qlpack.yml file paths relative to repo root +QLPACK_FILES=( + "javascript/frameworks/cap/ext/qlpack.yml" + "javascript/frameworks/cap/lib/qlpack.yml" + "javascript/frameworks/cap/src/qlpack.yml" + "javascript/frameworks/cap/test/qlpack.yml" + "javascript/frameworks/ui5/ext/qlpack.yml" + "javascript/frameworks/ui5/lib/qlpack.yml" + "javascript/frameworks/ui5/src/qlpack.yml" + "javascript/frameworks/ui5/test/qlpack.yml" + "javascript/frameworks/ui5-webcomponents/test/qlpack.yml" + "javascript/frameworks/xsjs/ext/qlpack.yml" + "javascript/frameworks/xsjs/lib/qlpack.yml" + "javascript/frameworks/xsjs/src/qlpack.yml" + "javascript/frameworks/xsjs/test/qlpack.yml" + "javascript/heuristic-models/ext/qlpack.yml" + "javascript/heuristic-models/tests/qlpack.yml" +) + +## Pack names that belong to this repository (for updating internal dependency refs) +INTERNAL_PACKS=( + "advanced-security/javascript-sap-cap-models" + "advanced-security/javascript-sap-cap-all" + "advanced-security/javascript-sap-cap-queries" + "advanced-security/javascript-sap-ui5-models" + "advanced-security/javascript-sap-ui5-all" + "advanced-security/javascript-sap-ui5-queries" + "advanced-security/javascript-sap-xsjs-models" + "advanced-security/javascript-sap-xsjs-all" + "advanced-security/javascript-sap-xsjs-queries" + "advanced-security/javascript-heuristic-models" +) + +usage() { + cat < + $0 --check [] + +Deterministically updates the release version across all version-bearing files. + +ARGUMENTS: + The new version to set (e.g., 2.4.0 or 2.4.0-alpha). + The 'v' prefix is optional and will be normalized. + Supports pre-release suffixes: -alpha, -beta, -rc1, etc. + +OPTIONS: + --check [] Check version consistency across all files. + If is provided, also validates that all + files match the expected version. + --dry-run Show what would be changed without modifying files. + -h, --help Show this help message. + +EXAMPLES: + $0 2.4.0 Update all files to version 2.4.0 + $0 2.4.0-alpha Update all files to pre-release version 2.4.0-alpha + $0 v2.4.0 Same as above (v prefix is stripped automatically) + $0 --check Verify all version-bearing files are consistent + $0 --check 2.4.0 Verify all files contain version 2.4.0 + $0 --dry-run 2.4.0 Preview changes without writing files +EOF +} + +## Collect all version-bearing files and their current versions +collect_versions() { + local versions=() + + for qlpack_file in "${QLPACK_FILES[@]}"; do + local full_path="${REPO_ROOT}/${qlpack_file}" + if [[ -f "${full_path}" ]]; then + local pack_version + pack_version=$(grep -m1 "^version:" "${full_path}" | awk '{print $2}') + if [[ -z "${pack_version}" ]]; then + echo "ERROR: ${qlpack_file} is missing a 'version:' field" >&2 + return 1 + fi + versions+=("${qlpack_file}|${pack_version}") + else + echo "ERROR: ${qlpack_file} not found" >&2 + return 1 + fi + done + + printf '%s\n' "${versions[@]}" +} + +## Check version consistency +check_versions() { + local expected_version="${1:-}" + local all_consistent=true + local first_version="" + local file_count=0 + + echo "=== Version Consistency Check ===" + echo "" + + local version_output + if ! version_output=$(collect_versions); then + echo "❌ Failed to collect versions" >&2 + return 1 + fi + + while IFS='|' read -r file version; do + file_count=$((file_count + 1)) + + if [[ -z "${first_version}" ]]; then + first_version="${version}" + fi + + if [[ -n "${expected_version}" ]]; then + if [[ "${version}" == "${expected_version}" ]]; then + echo " ✅ ${file}: ${version}" + else + echo " ❌ ${file}: ${version} (expected ${expected_version})" + all_consistent=false + fi + else + if [[ "${version}" == "${first_version}" ]]; then + echo " ✅ ${file}: ${version}" + else + echo " ❌ ${file}: ${version} (differs from ${first_version})" + all_consistent=false + fi + fi + done <<< "${version_output}" + + ## Also check qlt.conf.json consistency + local qlt_config="${REPO_ROOT}/qlt.conf.json" + if [[ -f "${qlt_config}" ]]; then + local cli_version + cli_version=$(grep -o '"CodeQLCLI":[[:space:]]*"[^"]*"' "${qlt_config}" | grep -o '"[^"]*"$' | tr -d '"') + ## Derive expected base version: strip pre-release suffix from first_version or expected_version + local check_base="${expected_version:-${first_version}}" + check_base="${check_base%%-*}" + if [[ "${cli_version}" == "${check_base}" ]]; then + echo " ✅ qlt.conf.json: CodeQLCLI ${cli_version}" + else + echo " ❌ qlt.conf.json: CodeQLCLI ${cli_version} (expected ${check_base})" + all_consistent=false + fi + fi + + echo "" + echo "Checked ${file_count} version-bearing files + qlt.conf.json." + + if [[ "${all_consistent}" == true ]]; then + if [[ -n "${expected_version}" ]]; then + echo "✅ All files match expected version: ${expected_version}" + else + echo "✅ All files are consistent at version: ${first_version}" + fi + return 0 + else + echo "❌ Version inconsistency detected!" + return 1 + fi +} + +## Validate version format (X.Y.Z or X.Y.Z-suffix) +validate_version() { + local version="$1" + if [[ ! "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+)?$ ]]; then + echo "ERROR: Invalid version format '${version}'" >&2 + echo "Expected format: X.Y.Z or X.Y.Z-suffix (e.g., 2.4.0, 2.4.0-alpha, 2.4.0-rc1)" >&2 + return 1 + fi +} + +## Update a qlpack.yml file's version field using sed +update_pack_version() { + local file="$1" + local new_version="$2" + sed -i.bak "s/^version:[[:space:]]*.*/version: ${new_version}/" "${file}" + rm -f "${file}.bak" +} + +## Update qlt.conf.json with the base version (suffix stripped) +## e.g., 2.4.0-alpha -> CodeQLCLI: "2.4.0", CodeQLStandardLibrary: "codeql-cli/v2.4.0", etc. +update_qlt_config() { + local new_version="$1" + local dry_run="${2:-false}" + local qlt_config="${REPO_ROOT}/qlt.conf.json" + + # Derive the base version by stripping any pre-release suffix + local base_version="${new_version%%-*}" + + if [[ ! -f "${qlt_config}" ]]; then + echo "WARNING: qlt.conf.json not found, skipping" >&2 + return 0 + fi + + if [[ "${dry_run}" == true ]]; then + echo " [DRY RUN] qlt.conf.json: CodeQLCLI -> ${base_version}" + return 0 + fi + + # Use jq for reliable JSON manipulation (learned from update-codeql.yml) + if command -v jq &>/dev/null; then + jq --arg cli_version "${base_version}" \ + --arg std_lib "codeql-cli/v${base_version}" \ + --arg bundle "codeql-bundle-v${base_version}" \ + '.CodeQLCLI = $cli_version | .CodeQLStandardLibrary = $std_lib | .CodeQLCLIBundle = $bundle' \ + "${qlt_config}" > "${qlt_config}.tmp" && mv "${qlt_config}.tmp" "${qlt_config}" + else + # Fallback to sed if jq is not available + local tmp_file + tmp_file=$(mktemp) + sed \ + -e "s/\"CodeQLCLI\":[[:space:]]*\"[^\"]*\"/\"CodeQLCLI\": \"${base_version}\"/" \ + -e "s/\"CodeQLStandardLibrary\":[[:space:]]*\"[^\"]*\"/\"CodeQLStandardLibrary\": \"codeql-cli\/v${base_version}\"/" \ + -e "s/\"CodeQLCLIBundle\":[[:space:]]*\"[^\"]*\"/\"CodeQLCLIBundle\": \"codeql-bundle-v${base_version}\"/" \ + "${qlt_config}" > "${tmp_file}" + mv "${tmp_file}" "${qlt_config}" + fi + echo " ✅ qlt.conf.json: CodeQLCLI -> ${base_version}" +} + +## Update internal dependency references in a qlpack.yml file +## e.g., advanced-security/javascript-sap-cap-models: "^2.3.0" -> "^2.4.0" +## e.g., advanced-security/javascript-sap-cap-models: "^2.3.0" -> "^2.4.0-alpha" +## and advanced-security/javascript-heuristic-models: 2.3.0 -> 2.4.0 +update_internal_deps() { + local file="$1" + local old_version="$2" + local new_version="$3" + + # Escape dots in the old version (e.g., '2.3.0' -> '2\.3\.0') for use in sed regex + local escaped_old_version + escaped_old_version=$(printf '%s' "${old_version}" | sed 's/\./\\./g') + + for pack_name in "${INTERNAL_PACKS[@]}"; do + # Update quoted caret-prefixed versions: "^X.Y.Z" + sed -i.bak "s|${pack_name}: \"\\^${escaped_old_version}\"|${pack_name}: \"^${new_version}\"|g" "${file}" + rm -f "${file}.bak" + # Update unquoted exact versions: X.Y.Z + sed -i.bak "s|${pack_name}: ${escaped_old_version}$|${pack_name}: ${new_version}|g" "${file}" + rm -f "${file}.bak" + done +} + +## Update all version-bearing files +update_versions() { + local new_version="$1" + local dry_run="${2:-false}" + local updated_count=0 + + echo "=== Updating Release Version to ${new_version} ===" + echo "" + + # Determine the current version from the first qlpack.yml file + local current_version="" + local first_file="${REPO_ROOT}/${QLPACK_FILES[0]}" + if [[ -f "${first_file}" ]]; then + current_version=$(grep -m1 "^version:" "${first_file}" | awk '{print $2}') + fi + + if [[ -z "${current_version}" ]]; then + echo "ERROR: Could not determine current version from ${QLPACK_FILES[0]}" >&2 + return 1 + fi + + echo " Current version: ${current_version}" + echo " New version: ${new_version}" + echo "" + + if [[ "${current_version}" == "${new_version}" ]]; then + echo "â„šī¸ Version is already ${new_version}. Nothing to update." + return 0 + fi + + ## Update all qlpack.yml files + for qlpack_file in "${QLPACK_FILES[@]}"; do + local full_path="${REPO_ROOT}/${qlpack_file}" + if [[ -f "${full_path}" ]]; then + local old_version + old_version=$(grep -m1 "^version:" "${full_path}" | awk '{print $2}') + if [[ "${dry_run}" == true ]]; then + echo " [DRY RUN] ${qlpack_file}: ${old_version} -> ${new_version}" + else + update_pack_version "${full_path}" "${new_version}" + update_internal_deps "${full_path}" "${current_version}" "${new_version}" + echo " ✅ ${qlpack_file}: ${old_version} -> ${new_version}" + fi + updated_count=$((updated_count + 1)) + fi + done + + ## Update qlt.conf.json + update_qlt_config "${new_version}" "${dry_run}" + + echo "" + if [[ "${dry_run}" == true ]]; then + echo "Would update ${updated_count} qlpack files + qlt.conf.json. (Dry run — no files modified)" + else + echo "Updated ${updated_count} qlpack files + qlt.conf.json to version ${new_version}." + echo "" + echo "Next steps:" + echo " 1. Run 'codeql pack upgrade' on all packs to update lock files" + echo " 2. Run CodeQL unit tests to validate the changes" + echo " 3. Commit the changes and tag with 'v${new_version}'" + fi +} + +## Parse arguments +CHECK_MODE=false +DRY_RUN=false +NEW_VERSION="" + +while [[ $# -gt 0 ]]; do + case $1 in + --check) + CHECK_MODE=true + shift + ## Optional expected version argument + if [[ $# -gt 0 && ! "$1" =~ ^-- ]]; then + NEW_VERSION="${1#v}" + shift + fi + ;; + --dry-run) + DRY_RUN=true + shift + ;; + -h|--help) + usage + exit 0 + ;; + -*) + echo "Error: Unknown option $1" >&2 + usage >&2 + exit 1 + ;; + *) + NEW_VERSION="${1#v}" ## Strip optional v prefix + shift + ;; + esac +done + +if [[ "${CHECK_MODE}" == true ]]; then + check_versions "${NEW_VERSION}" + exit $? +fi + +if [[ -z "${NEW_VERSION}" ]]; then + echo "Error: No version specified" >&2 + echo "" >&2 + usage >&2 + exit 1 +fi + +validate_version "${NEW_VERSION}" +update_versions "${NEW_VERSION}" "${DRY_RUN}"