diff --git a/.github/actions/setup-codeql-environment/action.yml b/.github/actions/setup-codeql-environment/action.yml index 9b7bd1d4..27b60b95 100644 --- a/.github/actions/setup-codeql-environment/action.yml +++ b/.github/actions/setup-codeql-environment/action.yml @@ -10,10 +10,23 @@ inputs: description: 'Whether to install language-specific runtimes and build tools' required: false default: 'true' + enable-cache: + description: "Whether to restore/save caches (gh-codeql, language runtimes, package managers). MUST be set to 'false' for release-generating workflows to avoid cache-poisoning attacks where a feature-branch contributor can populate the cache that a release job later restores." + required: false + default: 'true' # Language selection (only used if install-language-runtimes is true) languages: - description: 'Comma-separated list of target programming languages for which dependencies should be installed' + description: | + Comma-separated list of target programming languages for which + dependencies should be installed. The default list intentionally does + NOT include 'rust' because the Rust toolchain (rustc + cargo + rust-src) + is a several-hundred-megabyte download and is only needed for + Rust-specific CodeQL extraction (e.g. macro expansion in PrintAST / + PrintCFG tests). Callers that need Rust support MUST pass an explicit + `languages:` override that includes 'rust' (for example + `languages: 'rust'` from a per-language matrix entry, or + `languages: 'java,rust'` for a multi-language job). required: false default: 'csharp,go,java,javascript,python,ruby' @@ -38,6 +51,15 @@ inputs: description: 'Ruby version to install' required: false default: '3.2' + rust-version: + description: | + Rust toolchain version to install. Only takes effect when the + `languages` input explicitly includes 'rust' (Rust is NOT installed by + default — see the `languages` input documentation). A specific version + (e.g. 1.80.0) is recommended so that the Rust extractor produces + deterministic macro expansions across local and CI runs. + required: false + default: '1.80.0' outputs: codeql-home: @@ -81,7 +103,7 @@ runs: - name: Cache `gh-codeql` extension and CodeQL packages (Unix) id: cache-codeql-unix - if: runner.os != 'Windows' + if: runner.os != 'Windows' && inputs.enable-cache == 'true' uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5 with: path: | @@ -93,7 +115,7 @@ runs: - name: Cache `gh-codeql` extension and CodeQL packages (Windows) id: cache-codeql-windows - if: runner.os == 'Windows' + if: runner.os == 'Windows' && inputs.enable-cache == 'true' uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5 with: path: | @@ -318,18 +340,24 @@ runs: echo "No .NET dependency files found" fi - - name: Setup Node.js - if: inputs.install-language-runtimes == 'true' + - name: Setup Node.js (with cache) + if: inputs.install-language-runtimes == 'true' && inputs.enable-cache == 'true' uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 with: cache: 'npm' cache-dependency-path: 'package-lock.json' node-version-file: '.node-version' + - name: Setup Node.js (without cache) + if: inputs.install-language-runtimes == 'true' && inputs.enable-cache != 'true' + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + with: + node-version-file: '.node-version' + # Cache language runtimes to avoid repeated downloads (excluding .NET which is cached separately) - name: Cache language runtimes id: cache-runtimes - if: inputs.install-language-runtimes == 'true' + if: inputs.install-language-runtimes == 'true' && inputs.enable-cache == 'true' uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5 with: path: | @@ -343,20 +371,20 @@ runs: language-runtimes-${{ runner.os }}- - name: Setup Python (with cache) - if: inputs.install-language-runtimes == 'true' && contains(inputs.languages, 'python') && steps.check-deps.outputs.python-deps == 'true' + if: inputs.install-language-runtimes == 'true' && contains(format(',{0},', inputs.languages), ',python,') && steps.check-deps.outputs.python-deps == 'true' && inputs.enable-cache == 'true' uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 with: python-version: ${{ inputs.python-version }} cache: 'pip' - name: Setup Python (without cache) - if: inputs.install-language-runtimes == 'true' && contains(inputs.languages, 'python') && steps.check-deps.outputs.python-deps == 'false' + if: inputs.install-language-runtimes == 'true' && contains(format(',{0},', inputs.languages), ',python,') && (steps.check-deps.outputs.python-deps == 'false' || inputs.enable-cache != 'true') uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 with: python-version: ${{ inputs.python-version }} - name: Setup Java (with cache) - if: inputs.install-language-runtimes == 'true' && contains(inputs.languages, 'java') && steps.check-deps.outputs.java-deps == 'true' + if: inputs.install-language-runtimes == 'true' && contains(format(',{0},', inputs.languages), ',java,') && steps.check-deps.outputs.java-deps == 'true' && inputs.enable-cache == 'true' uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 with: distribution: 'temurin' @@ -364,21 +392,21 @@ runs: cache: 'maven' - name: Setup Java (without cache) - if: inputs.install-language-runtimes == 'true' && contains(inputs.languages, 'java') && steps.check-deps.outputs.java-deps == 'false' + if: inputs.install-language-runtimes == 'true' && contains(format(',{0},', inputs.languages), ',java,') && (steps.check-deps.outputs.java-deps == 'false' || inputs.enable-cache != 'true') uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 with: distribution: 'temurin' java-version: ${{ inputs.java-version }} - name: Setup Go (with cache) - if: inputs.install-language-runtimes == 'true' && contains(inputs.languages, 'go') && steps.check-deps.outputs.go-deps == 'true' + if: inputs.install-language-runtimes == 'true' && contains(format(',{0},', inputs.languages), ',go,') && steps.check-deps.outputs.go-deps == 'true' && inputs.enable-cache == 'true' uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6 with: go-version: ${{ inputs.go-version }} cache: true - name: Setup Go (without cache) - if: inputs.install-language-runtimes == 'true' && contains(inputs.languages, 'go') && steps.check-deps.outputs.go-deps == 'false' + if: inputs.install-language-runtimes == 'true' && contains(format(',{0},', inputs.languages), ',go,') && (steps.check-deps.outputs.go-deps == 'false' || inputs.enable-cache != 'true') uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6 with: go-version: ${{ inputs.go-version }} @@ -386,7 +414,7 @@ runs: # Cache .NET packages and tools - name: Cache .NET packages - if: inputs.install-language-runtimes == 'true' && contains(inputs.languages, 'csharp') + if: inputs.install-language-runtimes == 'true' && contains(format(',{0},', inputs.languages), ',csharp,') && inputs.enable-cache == 'true' id: cache-dotnet-packages uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5 with: @@ -399,55 +427,98 @@ runs: dotnet-packages-${{ runner.os }}- - name: Setup .NET (for C#) - if: inputs.install-language-runtimes == 'true' && contains(inputs.languages, 'csharp') + if: inputs.install-language-runtimes == 'true' && contains(format(',{0},', inputs.languages), ',csharp,') uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5 with: dotnet-version: ${{ inputs.dotnet-version }} - name: Setup Ruby (with cache) - if: inputs.install-language-runtimes == 'true' && contains(inputs.languages, 'ruby') && steps.check-deps.outputs.ruby-deps == 'true' + if: inputs.install-language-runtimes == 'true' && contains(format(',{0},', inputs.languages), ',ruby,') && steps.check-deps.outputs.ruby-deps == 'true' && inputs.enable-cache == 'true' uses: ruby/setup-ruby@4dc28cf14d77b0afa6832d9765ac422dbf0dfedd # v1 with: ruby-version: ${{ inputs.ruby-version }} bundler-cache: true - name: Setup Ruby (without cache) - if: inputs.install-language-runtimes == 'true' && contains(inputs.languages, 'ruby') && steps.check-deps.outputs.ruby-deps == 'false' + if: inputs.install-language-runtimes == 'true' && contains(format(',{0},', inputs.languages), ',ruby,') && (steps.check-deps.outputs.ruby-deps == 'false' || inputs.enable-cache != 'true') uses: ruby/setup-ruby@4dc28cf14d77b0afa6832d9765ac422dbf0dfedd # v1 with: ruby-version: ${{ inputs.ruby-version }} bundler-cache: false + # Cache Rust toolchain components and cargo registry. The CodeQL rust + # extractor invokes cargo/rustc to resolve std-library macro expansions + # (format!, println!, vec!, ...). Without a working toolchain, the + # extractor produces partial AST nodes with no getMacroCallExpansion() + # subtrees, which breaks PrintAST/PrintCFG tests. + # + # NOTE: 'rust' is NOT part of the default `languages` value. The Rust + # toolchain is only installed when a caller passes an explicit + # `languages:` override that includes 'rust' (e.g. from a per-language + # matrix entry). See the `languages` input documentation above. + - name: Cache Rust toolchain and cargo registry + if: inputs.install-language-runtimes == 'true' && contains(format(',{0},', inputs.languages), ',rust,') && inputs.enable-cache == 'true' + id: cache-rust + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5 + with: + path: | + ~/.cargo/bin + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git/db + ~/.rustup + key: rust-${{ runner.os }}-${{ inputs.rust-version }}-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + rust-${{ runner.os }}-${{ inputs.rust-version }}- + + - name: Setup Rust toolchain + if: inputs.install-language-runtimes == 'true' && contains(format(',{0},', inputs.languages), ',rust,') + uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 + with: + toolchain: ${{ inputs.rust-version }} + components: rust-src + - name: Verify language-specific tools if: inputs.install-language-runtimes == 'true' && inputs.languages != '' shell: bash + env: + LANGUAGES: ${{ inputs.languages }} run: | echo "=== Language-specific tool verification ===" - if [[ "${{ inputs.languages }}" == *"javascript"* ]] || [[ "${{ inputs.languages }}" == *"typescript"* ]]; then + # Wrap the comma-separated list in commas so glob patterns can match + # whole tokens (',java,' vs ',javascript,') instead of substrings. + _LANGS=",${LANGUAGES}," + + if [[ "${_LANGS}" == *",javascript,"* ]] || [[ "${_LANGS}" == *",typescript,"* ]]; then echo "Node.js version: $(node --version)" echo "npm version: $(npm --version)" fi - if [[ "${{ inputs.languages }}" == *"python"* ]]; then + if [[ "${_LANGS}" == *",python,"* ]]; then echo "Python version: $(python --version)" echo "pip version: $(pip --version)" fi - if [[ "${{ inputs.languages }}" == *"java"* ]]; then + if [[ "${_LANGS}" == *",java,"* ]]; then echo "Java version: $(java -version 2>&1 | head -1)" fi - if [[ "${{ inputs.languages }}" == *"go"* ]]; then + if [[ "${_LANGS}" == *",go,"* ]]; then echo "Go version: $(go version)" fi - if [[ "${{ inputs.languages }}" == *"csharp"* ]]; then + if [[ "${_LANGS}" == *",csharp,"* ]]; then echo "dotnet version: $(dotnet --version)" fi - if [[ "${{ inputs.languages }}" == *"ruby"* ]]; then + if [[ "${_LANGS}" == *",ruby,"* ]]; then echo "Ruby version: $(ruby --version)" fi + if [[ "${_LANGS}" == *",rust,"* ]]; then + echo "Rust version: $(rustc --version 2>/dev/null || echo 'rustc not found')" + echo "Cargo version: $(cargo --version 2>/dev/null || echo 'cargo not found')" + fi + echo "=================================" diff --git a/.github/workflows/query-unit-tests.yml b/.github/workflows/query-unit-tests.yml index c3f8f215..68b76f2a 100644 --- a/.github/workflows/query-unit-tests.yml +++ b/.github/workflows/query-unit-tests.yml @@ -36,7 +36,7 @@ permissions: jobs: query-unit-tests: name: Query Unit Tests - ${{ matrix.language }} - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 strategy: fail-fast: false @@ -63,6 +63,7 @@ jobs: uses: ./.github/actions/setup-codeql-environment with: install-language-runtimes: true + languages: ${{ matrix.language }} ## Install packs used in the unit tests that we're about to run. - name: Query Unit Tests - ${{ matrix.language }} - Install CodeQL packs used in unit tests diff --git a/.github/workflows/release-codeql.yml b/.github/workflows/release-codeql.yml index ae394a38..3d1e50ff 100644 --- a/.github/workflows/release-codeql.yml +++ b/.github/workflows/release-codeql.yml @@ -9,7 +9,7 @@ on: required: false type: boolean version: - description: 'Release version tag (e.g., vX.Y.Z). Must start with "v".' + description: 'Release version tag (e.g., vX.Y.Z or vX.Y.Z-PRERELEASE). Must match ^vMAJOR.MINOR.PATCH(-PRERELEASE)?$ where PRERELEASE may contain alphanumerics, dots, and hyphens.' required: true type: string outputs: @@ -32,7 +32,7 @@ permissions: jobs: publish-codeql-packs: name: Publish and Bundle CodeQL Packs - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 environment: release-codeql @@ -47,25 +47,32 @@ jobs: steps: - name: CodeQL - Validate and parse version id: version + env: + RAW_VERSION: ${{ inputs.version }} run: | - VERSION="${{ inputs.version }}" - if [[ ! "${VERSION}" =~ ^v ]]; then - echo "::error::Version '${VERSION}' must start with 'v'" + if [[ ! "${RAW_VERSION}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.-]+)?$ ]]; then + echo "::error::Version '${RAW_VERSION}' does not match ^vMAJOR.MINOR.PATCH(-PRERELEASE)?$" + exit 1 + fi + if ! git check-ref-format "refs/tags/${RAW_VERSION}" >/dev/null 2>&1; then + echo "::error::Version '${RAW_VERSION}' is not a valid git tag ref name" exit 1 fi - echo "version=${VERSION}" >> $GITHUB_OUTPUT - echo "release_name=${VERSION#v}" >> $GITHUB_OUTPUT + echo "version=${RAW_VERSION}" >> "$GITHUB_OUTPUT" + echo "release_name=${RAW_VERSION#v}" >> "$GITHUB_OUTPUT" - name: CodeQL - Checkout tag uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: ref: refs/tags/${{ steps.version.outputs.version }} + persist-credentials: false - name: CodeQL - Setup CodeQL environment uses: ./.github/actions/setup-codeql-environment with: add-to-path: true install-language-runtimes: false + enable-cache: false - name: CodeQL - Install CodeQL pack dependencies run: server/scripts/install-packs.sh @@ -84,11 +91,12 @@ jobs: RELEASE_NAME="${{ steps.version.outputs.release_name }}" LANGUAGES="actions cpp csharp go java javascript python ruby rust swift" - # Prerelease versions (containing a hyphen) require --allow-prerelease - PRERELEASE_FLAG="" + # Prerelease versions (containing a hyphen) require --allow-prerelease. + # Use an array to avoid word-splitting / quoting hazards. + PUBLISH_ARGS=(--threads=-1) if [[ "${RELEASE_NAME}" == *-* ]]; then - PRERELEASE_FLAG="--allow-prerelease" - echo "Detected prerelease version — using ${PRERELEASE_FLAG}" + PUBLISH_ARGS+=(--allow-prerelease) + echo "Detected prerelease version — using --allow-prerelease" fi echo "Publishing CodeQL tool query packs..." @@ -96,7 +104,7 @@ jobs: PACK_DIR="server/ql/${lang}/tools/src" if [ -d "${PACK_DIR}" ]; then echo "📦 Publishing ${PACK_DIR}..." - codeql pack publish --threads=-1 ${PRERELEASE_FLAG} -- "${PACK_DIR}" + codeql pack publish "${PUBLISH_ARGS[@]}" -- "${PACK_DIR}" echo "✅ Published ${lang} tool query pack" else echo "⚠️ Skipping ${lang}: ${PACK_DIR} not found" diff --git a/.github/workflows/release-npm.yml b/.github/workflows/release-npm.yml index 1bb005af..2f3ae591 100644 --- a/.github/workflows/release-npm.yml +++ b/.github/workflows/release-npm.yml @@ -4,7 +4,7 @@ on: workflow_call: inputs: version: - description: 'Release version tag (e.g., vX.Y.Z). Must start with "v".' + description: 'Release version tag (e.g., vX.Y.Z or vX.Y.Z-PRERELEASE). Must match ^vMAJOR.MINOR.PATCH(-PRERELEASE)?$ where PRERELEASE may contain alphanumerics, dots, and hyphens.' required: true type: string outputs: @@ -29,7 +29,7 @@ permissions: jobs: publish-npm: name: Publish npm Package - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 environment: release-npm @@ -44,24 +44,29 @@ jobs: steps: - name: npm - Validate and parse version id: version + env: + RAW_VERSION: ${{ inputs.version }} run: | - VERSION="${{ inputs.version }}" - if [[ ! "${VERSION}" =~ ^v ]]; then - echo "::error::Version '${VERSION}' must start with 'v'" + if [[ ! "${RAW_VERSION}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.-]+)?$ ]]; then + echo "::error::Version '${RAW_VERSION}' does not match ^vMAJOR.MINOR.PATCH(-PRERELEASE)?$" exit 1 fi - echo "version=${VERSION}" >> $GITHUB_OUTPUT - echo "release_name=${VERSION#v}" >> $GITHUB_OUTPUT + if ! git check-ref-format "refs/tags/${RAW_VERSION}" >/dev/null 2>&1; then + echo "::error::Version '${RAW_VERSION}' is not a valid git tag ref name" + exit 1 + fi + echo "version=${RAW_VERSION}" >> "$GITHUB_OUTPUT" + echo "release_name=${RAW_VERSION#v}" >> "$GITHUB_OUTPUT" - name: npm - Checkout tag uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: ref: refs/tags/${{ steps.version.outputs.version }} + persist-credentials: false - name: npm - Setup Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: - cache: 'npm' node-version-file: '.node-version' registry-url: 'https://registry.npmjs.org' diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index 9f8a2aca..dab543db 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -4,7 +4,7 @@ on: workflow_call: inputs: version: - description: 'Release version (e.g., vX.Y.Z). Must start with "v".' + description: 'Release version (e.g., vX.Y.Z or vX.Y.Z-PRERELEASE). Must match ^vMAJOR.MINOR.PATCH(-PRERELEASE)?$ where PRERELEASE may contain alphanumerics, dots, and hyphens.' required: true type: string outputs: @@ -28,7 +28,7 @@ permissions: jobs: create-tag: name: Create Version Tag - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 environment: release-tag @@ -49,15 +49,19 @@ jobs: - name: Tag - Validate and parse version id: version + env: + RAW_VERSION: ${{ inputs.version }} run: | - VERSION="${{ inputs.version }}" - # Validate version starts with 'v' - if [[ ! "${VERSION}" =~ ^v ]]; then - echo "::error::Version '${VERSION}' must start with 'v'" + if [[ ! "${RAW_VERSION}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.-]+)?$ ]]; then + echo "::error::Version '${RAW_VERSION}' does not match ^vMAJOR.MINOR.PATCH(-PRERELEASE)?$" exit 1 fi - echo "version=${VERSION}" >> $GITHUB_OUTPUT - echo "release_name=${VERSION#v}" >> $GITHUB_OUTPUT + if ! git check-ref-format "refs/tags/${RAW_VERSION}" >/dev/null 2>&1; then + echo "::error::Version '${RAW_VERSION}' is not a valid git tag ref name" + exit 1 + fi + echo "version=${RAW_VERSION}" >> "$GITHUB_OUTPUT" + echo "release_name=${RAW_VERSION#v}" >> "$GITHUB_OUTPUT" - name: Tag - Check if tag already exists id: check-tag @@ -95,12 +99,12 @@ jobs: with: add-to-path: true install-language-runtimes: false + enable-cache: false - name: Tag - Setup Node.js if: steps.check-tag.outputs.tag_exists != 'true' uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: - cache: 'npm' node-version-file: '.node-version' - name: Tag - Update release version diff --git a/.github/workflows/release-vsix.yml b/.github/workflows/release-vsix.yml index 52f4178e..ac46128b 100644 --- a/.github/workflows/release-vsix.yml +++ b/.github/workflows/release-vsix.yml @@ -4,7 +4,7 @@ on: workflow_call: inputs: version: - description: 'Release version tag (e.g., vX.Y.Z). Must start with "v".' + description: 'Release version tag (e.g., vX.Y.Z or vX.Y.Z-PRERELEASE). Must match ^vMAJOR.MINOR.PATCH(-PRERELEASE)?$ where PRERELEASE may contain alphanumerics, dots, and hyphens.' required: true type: string outputs: @@ -30,7 +30,7 @@ permissions: jobs: publish-vsix: name: Build and Package VSIX Extension - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 environment: release-vsix @@ -45,24 +45,29 @@ jobs: steps: - name: VSIX - Validate and parse version id: version + env: + RAW_VERSION: ${{ inputs.version }} run: | - VERSION="${{ inputs.version }}" - if [[ ! "${VERSION}" =~ ^v ]]; then - echo "::error::Version '${VERSION}' must start with 'v'" + if [[ ! "${RAW_VERSION}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.-]+)?$ ]]; then + echo "::error::Version '${RAW_VERSION}' does not match ^vMAJOR.MINOR.PATCH(-PRERELEASE)?$" exit 1 fi - echo "version=${VERSION}" >> $GITHUB_OUTPUT - echo "release_name=${VERSION#v}" >> $GITHUB_OUTPUT + if ! git check-ref-format "refs/tags/${RAW_VERSION}" >/dev/null 2>&1; then + echo "::error::Version '${RAW_VERSION}' is not a valid git tag ref name" + exit 1 + fi + echo "version=${RAW_VERSION}" >> "$GITHUB_OUTPUT" + echo "release_name=${RAW_VERSION#v}" >> "$GITHUB_OUTPUT" - name: VSIX - Checkout tag uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: ref: refs/tags/${{ steps.version.outputs.version }} + persist-credentials: false - name: VSIX - Setup Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: - cache: 'npm' node-version-file: '.node-version' - name: VSIX - Install dependencies diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f89c5d74..74eac45a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,7 @@ on: required: false type: boolean version: - description: 'Release version (e.g., vX.Y.Z). Must start with "v".' + description: 'Release version (e.g., vX.Y.Z or vX.Y.Z-PRERELEASE). Must match ^vMAJOR.MINOR.PATCH(-PRERELEASE)?$ where PRERELEASE may contain alphanumerics, dots, and hyphens.' required: true type: string @@ -31,7 +31,10 @@ permissions: concurrency: group: release-${{ github.event.inputs.version || github.ref_name }} - cancel-in-progress: true + # A mid-flight release that publishes to npm and GHCR but never tags (or + # vice versa) leaves the ecosystem in an inconsistent state. Releases must + # not be cancellable by a newer dispatch of the same version. + cancel-in-progress: false jobs: # ───────────────────────────────────────────────────────────────────────────── @@ -43,7 +46,7 @@ jobs: # ───────────────────────────────────────────────────────────────────────────── resolve-version: name: Resolve Release Version - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 outputs: create_github_release: ${{ steps.resolve.outputs.create_github_release }} @@ -55,35 +58,48 @@ jobs: steps: - name: Version - Resolve and validate id: resolve + env: + DISPATCH_VERSION: ${{ github.event.inputs.version }} + REF_NAME: ${{ github.ref_name }} + EVENT_NAME: ${{ github.event_name }} + CREATE_RELEASE_INPUT: ${{ github.event.inputs.create_github_release }} + PUBLISH_PACKS_INPUT: ${{ github.event.inputs.publish_codeql_packs }} + PUBLISH_NPM_INPUT: ${{ github.event.inputs.publish_npm }} run: | - if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then - VERSION="${{ github.event.inputs.version }}" + if [ "${EVENT_NAME}" == "workflow_dispatch" ]; then + VERSION="${DISPATCH_VERSION}" else - VERSION="${{ github.ref_name }}" + VERSION="${REF_NAME}" fi - # Validate version starts with 'v' - if [[ ! "${VERSION}" =~ ^v ]]; then - echo "::error::Version '${VERSION}' must start with 'v'" + # Validate the resolved version: strict regex first, then + # git check-ref-format so values like 'v1.2.3-..' (regex-valid but + # rejected by git) are caught here instead of in actions/checkout. + if [[ ! "${VERSION}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.-]+)?$ ]]; then + echo "::error::Version '${VERSION}' does not match ^vMAJOR.MINOR.PATCH(-PRERELEASE)?$" + exit 1 + fi + if ! git check-ref-format "refs/tags/${VERSION}" >/dev/null 2>&1; then + echo "::error::Version '${VERSION}' is not a valid git tag ref name" 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 }}" - PUBLISH_NPM="${{ github.event.inputs.publish_npm }}" + if [ "${EVENT_NAME}" == "workflow_dispatch" ]; then + CREATE_RELEASE="${CREATE_RELEASE_INPUT}" + PUBLISH_PACKS="${PUBLISH_PACKS_INPUT}" + PUBLISH_NPM="${PUBLISH_NPM_INPUT}" else CREATE_RELEASE="true" PUBLISH_PACKS="true" PUBLISH_NPM="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 - echo "publish_npm=${PUBLISH_NPM}" >> $GITHUB_OUTPUT + 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" + echo "publish_npm=${PUBLISH_NPM}" >> "$GITHUB_OUTPUT" # ───────────────────────────────────────────────────────────────────────────── # Step 2: Ensure the release tag exists @@ -174,7 +190,7 @@ jobs: && needs.resolve-version.outputs.create_github_release == 'true' && needs.resolve-version.outputs.publish_npm == 'true' needs: [resolve-version, ensure-tag, publish-npm, publish-codeql, build-vsix] - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: contents: write diff --git a/CHANGELOG.md b/CHANGELOG.md index ebf1b518..63d427ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,35 @@ release cadence. _Changes on `main` since the latest tagged release that have not yet been included in a stable release._ +### Highlights + +- **Second supply-chain hardening pass for release workflows** — The `release.yml`, `release-tag.yml`, `release-npm.yml`, `release-vsix.yml`, and `release-codeql.yml` workflows are now resistant to cache-poisoning attacks: the shared `setup-codeql-environment` composite action exposes a new `enable-cache` input (set to `false` for every release-generating job), every inline `cache: 'npm'` was dropped, runners are pinned to `ubuntu-24.04`, `cancel-in-progress` is `false` on the parent release workflow, version inputs are validated against the strict regex `^vMAJOR.MINOR.PATCH(-PRERELEASE)?$` via `env:` intermediaries, and non-pushing checkouts use `persist-credentials: false`. ([#279](https://github.com/advanced-security/codeql-development-mcp-server/pull/279)) +- **First-class Rust toolchain support in CI** — `setup-codeql-environment` now installs a pinned Rust toolchain (default `1.80.0`, via a pinned `dtolnay/rust-toolchain` action with `rust-src`) for any matrix entry that includes `rust`, so the CodeQL rust extractor can expand `format!` / `println!` / `vec!` macros against the standard library on Linux runners. The `query-unit-tests.yml` workflow now passes `languages: ${{ matrix.language }}` so each matrix entry only installs its own runtime. ([#279](https://github.com/advanced-security/codeql-development-mcp-server/pull/279)) + +### Security + +- **Hardened release workflows against cache poisoning.** All four reusable release workflows (`release-tag.yml`, `release-npm.yml`, `release-vsix.yml`, `release-codeql.yml`) and the orchestrating `release.yml` now opt out of every `actions/cache`, `setup-node` npm cache, `setup-go` cache, `setup-java` Maven cache, `setup-python` pip cache, `setup-ruby` bundler cache, and `actions/cache` for `.nuget`/`~/.rustup`, so a feature-branch contributor can no longer prime a cache entry that a release job would later restore. The `cancel-in-progress: false` change ensures a release cannot be cancelled mid-publish, preventing inconsistent npm/GHCR/tag state. Non-pushing checkouts now use `persist-credentials: false` to limit accidental token reuse, and the `release-codeql.yml` `codeql pack publish` invocation now uses a bash array to avoid word-splitting on the `--allow-prerelease` flag. ([#279](https://github.com/advanced-security/codeql-development-mcp-server/pull/279)) + +### Changed + +#### Infrastructure & CI/CD + +- The `setup-codeql-environment` composite action gained an `enable-cache` input (default `true`) that gates every cache-related step it owns (gh-codeql, language-runtimes, .NET packages) and every `cache:` feature it passes to `actions/setup-*`. Release workflows now pass `enable-cache: false`. ([#279](https://github.com/advanced-security/codeql-development-mcp-server/pull/279)) +- The `setup-codeql-environment` composite action now sets up Rust (default `1.80.0`, configurable via `rust-version`) when `rust` is in the `languages` input, including a dedicated cache for `~/.cargo`/`~/.rustup` and a Cargo dependency-file detector. **Rust is opt-in only** — it is intentionally NOT included in the default `languages` value (`csharp,go,java,javascript,python,ruby`) because the Rust toolchain (rustc + cargo + rust-src) is a several-hundred-megabyte download. Callers that need Rust support must pass an explicit `languages:` override that includes `rust` (e.g. `languages: ${{ matrix.language }}` from a per-language matrix entry). ([#279](https://github.com/advanced-security/codeql-development-mcp-server/pull/279)) +- The `setup-codeql-environment` composite action now matches the `languages` input with comma-bounded tokens (`contains(format(',{0},', inputs.languages), ',java,')`) instead of bare substring `contains()`. This fixes a latent bug where the JavaScript matrix entry triggered the Java setup steps because `contains('javascript', 'java')` was true. ([#279](https://github.com/advanced-security/codeql-development-mcp-server/pull/279)) +- `query-unit-tests.yml` matrix entries now pass `languages: ${{ matrix.language }}` to the composite action so each language matrix entry installs only its own runtime instead of the full default set, and `runs-on` is pinned to `ubuntu-24.04`. ([#279](https://github.com/advanced-security/codeql-development-mcp-server/pull/279)) +- All release workflow `version` inputs (in `release.yml`, `release-tag.yml`, `release-npm.yml`, `release-vsix.yml`, `release-codeql.yml`) now document the actual accepted format (`^vMAJOR.MINOR.PATCH(-PRERELEASE)?$` where `PRERELEASE` may contain alphanumerics, dots, and hyphens) and are validated against that regex via an `env:` intermediate variable. Validation also runs `git check-ref-format "refs/tags/${VERSION}"` so values that pass the regex but are rejected by git as a tag ref (e.g. `v1.2.3-..`, `v1.2.3-foo.`, `v1.2.3-foo.lock`, `v1.2.3-a..b`) are caught up front instead of failing later in `actions/checkout`. ([#279](https://github.com/advanced-security/codeql-development-mcp-server/pull/279)) + +### Fixed + +- **Rust `PrintAST` and `PrintCFG` unit tests failed on CI.** Two distinct root causes: (1) CI had no Rust toolchain installed, so the extractor could not expand `format!`/`println!`/`vec!` and the entire `getMacroCallExpansion()` subtrees were missing from `PrintAST` output; (2) the legacy rust test extractor produces non-deterministic CFG entity ordering under parallel evaluation, which made the `PrintCFG` snapshot test flaky (5 distinct outputs across 5 runs with `--threads=-1`, identical output across every run with `--threads=1`). Fixes: install Rust in CI via the composite action; regenerate the rust `PrintAST.expected` baseline; and force `--threads=1` for the rust language entry in `run-query-unit-tests.sh` so `PrintCFG` produces deterministic output. ([#279](https://github.com/advanced-security/codeql-development-mcp-server/pull/279)) +- **`run-query-unit-tests.sh` broke the Swift macOS workflow with `unbound variable`.** Bash 3.2 (the default `/bin/bash` on macOS GitHub Actions runners) errors when expanding an empty array under `set -u`. Replaced the `local _threads_arg=()` array with a plain scalar string and unquoted expansion so the script is portable across Bash 3.2 and 4+. ([#279](https://github.com/advanced-security/codeql-development-mcp-server/pull/279)) + +### Dependencies + +- Bumped `actions/dependency-review-action` from 4.9.0 to 5.0.0. ([#278](https://github.com/advanced-security/codeql-development-mcp-server/pull/278)) +- Added `dtolnay/rust-toolchain` (pinned to the `stable` branch commit SHA `29eef336d9b2848a0b548edc03f92a220660cdb8`) as a new dependency of `setup-codeql-environment` for the rust language. ([#279](https://github.com/advanced-security/codeql-development-mcp-server/pull/279)) + ## [v2.25.4] — 2026-05-08 ### Highlights diff --git a/server/ql/rust/tools/test/PrintAST/PrintAST.expected b/server/ql/rust/tools/test/PrintAST/PrintAST.expected index 0c222136..776e89d1 100644 --- a/server/ql/rust/tools/test/PrintAST/PrintAST.expected +++ b/server/ql/rust/tools/test/PrintAST/PrintAST.expected @@ -58,54 +58,49 @@ Example1.rs: # 15| getTokenTree(): [TokenTree] TokenTree # 15| getMacroCallExpansion(): [BlockExpr] { ... } # 15| getStmtList(): [StmtList] StmtList -# 15| getTailExpr(): [CallExpr] ...::must_use(...) -# 15| getArgList(): [ArgList] ArgList -# 15| getArg(0): [BlockExpr] { ... } -# 15| getStmtList(): [StmtList] StmtList -# 15| getTailExpr(): [CallExpr] ...::format(...) -# 15| getArgList(): [ArgList] ArgList -# 15| getArg(0): [MacroExpr] MacroExpr -# 15| getMacroCall(): [MacroCall] ...::format_args!... -# 15| getPath(): [Path] ...::format_args -# 15| getQualifier(): [Path] ...::__export -# 15| getQualifier(): [Path] $crate -# 15| getSegment(): [PathSegment] $crate -# 15| getIdentifier(): [NameRef] $crate -# 15| getSegment(): [PathSegment] __export -# 15| getIdentifier(): [NameRef] __export -# 15| getSegment(): [PathSegment] format_args -# 15| getIdentifier(): [NameRef] format_args -# 15| getTokenTree(): [TokenTree] TokenTree -# 15| getMacroCallExpansion(): [FormatArgsExpr] FormatArgsExpr -# 15| getArg(0): [FormatArgsArg] FormatArgsArg -# 15| getExpr(): [FieldExpr] self.name -# 15| getContainer(): [VariableAccess] self -# 15| getPath(): [Path] self -# 15| getSegment(): [PathSegment] self -# 15| getIdentifier(): [NameRef] self -# 15| getIdentifier(): [NameRef] name -# 15| getTemplate(): [StringLiteralExpr] "Hello, {}!" -# 15| getFormat(0): [Format] {} -# 15| getFunction(): [PathExpr] ...::format -# 15| getPath(): [Path] ...::format -# 15| getQualifier(): [Path] ...::fmt -# 15| getQualifier(): [Path] $crate -# 15| getSegment(): [PathSegment] $crate -# 15| getIdentifier(): [NameRef] $crate -# 15| getSegment(): [PathSegment] fmt -# 15| getIdentifier(): [NameRef] fmt -# 15| getSegment(): [PathSegment] format -# 15| getIdentifier(): [NameRef] format -# 15| getFunction(): [PathExpr] ...::must_use -# 15| getPath(): [Path] ...::must_use -# 15| getQualifier(): [Path] ...::__export -# 15| getQualifier(): [Path] $crate -# 15| getSegment(): [PathSegment] $crate -# 15| getIdentifier(): [NameRef] $crate -# 15| getSegment(): [PathSegment] __export -# 15| getIdentifier(): [NameRef] __export -# 15| getSegment(): [PathSegment] must_use -# 15| getIdentifier(): [NameRef] must_use +# 15| getTailExpr(): [BlockExpr] { ... } +# 15| getStmtList(): [StmtList] StmtList +# 15| getStatement(0): [LetStmt] let ... = ... +# 15| getInitializer(): [CallExpr] ...::format(...) +# 15| getArgList(): [ArgList] ArgList +# 15| getArg(0): [MacroExpr] MacroExpr +# 15| getMacroCall(): [MacroCall] ...::format_args!... +# 15| getPath(): [Path] ...::format_args +# 15| getQualifier(): [Path] ...::__export +# 15| getQualifier(): [Path] $crate +# 15| getSegment(): [PathSegment] $crate +# 15| getIdentifier(): [NameRef] $crate +# 15| getSegment(): [PathSegment] __export +# 15| getIdentifier(): [NameRef] __export +# 15| getSegment(): [PathSegment] format_args +# 15| getIdentifier(): [NameRef] format_args +# 15| getTokenTree(): [TokenTree] TokenTree +# 15| getMacroCallExpansion(): [FormatArgsExpr] FormatArgsExpr +# 15| getArg(0): [FormatArgsArg] FormatArgsArg +# 15| getExpr(): [FieldExpr] self.name +# 15| getContainer(): [VariableAccess] self +# 15| getPath(): [Path] self +# 15| getSegment(): [PathSegment] self +# 15| getIdentifier(): [NameRef] self +# 15| getIdentifier(): [NameRef] name +# 15| getTemplate(): [StringLiteralExpr] "Hello, {}!" +# 15| getFormat(0): [Format] {} +# 15| getFunction(): [PathExpr] ...::format +# 15| getPath(): [Path] ...::format +# 15| getQualifier(): [Path] ...::fmt +# 15| getQualifier(): [Path] $crate +# 15| getSegment(): [PathSegment] $crate +# 15| getIdentifier(): [NameRef] $crate +# 15| getSegment(): [PathSegment] fmt +# 15| getIdentifier(): [NameRef] fmt +# 15| getSegment(): [PathSegment] format +# 15| getIdentifier(): [NameRef] format +# 15| getPat(): [IdentPat] res +# 15| getName(): [Name] res +# 15| getTailExpr(): [VariableAccess] res +# 15| getPath(): [Path] res +# 15| getSegment(): [PathSegment] res +# 15| getIdentifier(): [NameRef] res # 14| getName(): [Name] greet # 14| getRetType(): [RetTypeRepr] RetTypeRepr # 14| getTypeRepr(): [PathTypeRepr] String @@ -145,32 +140,50 @@ Example1.rs: # 24| getSegment(): [PathSegment] vec # 24| getIdentifier(): [NameRef] vec # 24| getTokenTree(): [TokenTree] TokenTree -# 24| getMacroCallExpansion(): [CallExpr] ...::into_vec(...) -# 24| getArgList(): [ArgList] ArgList -# 24| getArg(0): [CallExpr] ...::box_new(...) -# 24| getArgList(): [ArgList] ArgList -# 24| getArg(0): [ArrayListExpr] [...] -# 24| getExpr(0): [IntegerLiteralExpr] 1 -# 24| getExpr(1): [IntegerLiteralExpr] 2 -# 24| getExpr(2): [IntegerLiteralExpr] 3 -# 24| getFunction(): [PathExpr] ...::box_new -# 24| getPath(): [Path] ...::box_new -# 24| getQualifier(): [Path] ...::boxed -# 24| getQualifier(): [Path] $crate -# 24| getSegment(): [PathSegment] $crate -# 24| getIdentifier(): [NameRef] $crate -# 24| getSegment(): [PathSegment] boxed -# 24| getIdentifier(): [NameRef] boxed -# 24| getSegment(): [PathSegment] box_new -# 24| getIdentifier(): [NameRef] box_new -# 24| getFunction(): [PathExpr] ...::into_vec -# 24| getPath(): [Path] ...::into_vec -# 24| getQualifier(): [Path] <...> -# 24| getSegment(): [PathSegment] <...> -# 24| getTypeRepr(): [SliceTypeRepr] SliceTypeRepr -# 24| getTypeRepr(): [InferTypeRepr] _ -# 24| getSegment(): [PathSegment] into_vec -# 24| getIdentifier(): [NameRef] into_vec +# 24| getMacroCallExpansion(): [MacroExpr] MacroExpr +# 24| getMacroCall(): [MacroCall] ...::__rust_force_expr!... +# 24| getPath(): [Path] ...::__rust_force_expr +# 24| getQualifier(): [Path] $crate +# 24| getSegment(): [PathSegment] $crate +# 24| getIdentifier(): [NameRef] $crate +# 24| getSegment(): [PathSegment] __rust_force_expr +# 24| getIdentifier(): [NameRef] __rust_force_expr +# 24| getTokenTree(): [TokenTree] TokenTree +# 24| getMacroCallExpansion(): [ParenExpr] (...) +# 24| getExpr(): [CallExpr] ...::into_vec(...) +# 24| getArgList(): [ArgList] ArgList +# 24| getArg(0): [CallExpr] ...::new(...) +# 24| getArgList(): [ArgList] ArgList +# 24| getArg(0): [ArrayListExpr] [...] +# 24| getExpr(0): [IntegerLiteralExpr] 1 +# 24| getExpr(1): [IntegerLiteralExpr] 2 +# 24| getExpr(2): [IntegerLiteralExpr] 3 +# 24| getAttr(0): [Attr] Attr +# 24| getMeta(): [Meta] Meta +# 24| getPath(): [Path] rustc_box +# 24| getSegment(): [PathSegment] rustc_box +# 24| getIdentifier(): [NameRef] rustc_box +# 24| getFunction(): [PathExpr] ...::new +# 24| getPath(): [Path] ...::new +# 24| getQualifier(): [Path] ...::Box +# 24| getQualifier(): [Path] ...::boxed +# 24| getQualifier(): [Path] $crate +# 24| getSegment(): [PathSegment] $crate +# 24| getIdentifier(): [NameRef] $crate +# 24| getSegment(): [PathSegment] boxed +# 24| getIdentifier(): [NameRef] boxed +# 24| getSegment(): [PathSegment] Box +# 24| getIdentifier(): [NameRef] Box +# 24| getSegment(): [PathSegment] new +# 24| getIdentifier(): [NameRef] new +# 24| getFunction(): [PathExpr] ...::into_vec +# 24| getPath(): [Path] ...::into_vec +# 24| getQualifier(): [Path] <...> +# 24| getSegment(): [PathSegment] <...> +# 24| getTypeRepr(): [SliceTypeRepr] SliceTypeRepr +# 24| getTypeRepr(): [InferTypeRepr] _ +# 24| getSegment(): [PathSegment] into_vec +# 24| getIdentifier(): [NameRef] into_vec # 24| getPat(): [IdentPat] numbers # 24| getName(): [Name] numbers # 27| getStatement(1): [ExprStmt] ExprStmt diff --git a/server/scripts/run-query-unit-tests.sh b/server/scripts/run-query-unit-tests.sh index ef312ca5..27739e77 100755 --- a/server/scripts/run-query-unit-tests.sh +++ b/server/scripts/run-query-unit-tests.sh @@ -73,15 +73,34 @@ cd "${REPO_ROOT_DIR}" run_tests() { local _tools_dir="$1" local _test_dir="${_tools_dir}/test" - + if [ -d "${_test_dir}" ]; then echo "INFO: Running 'codeql test run' for '${_test_dir}' directory..." - + + # Determine the thread count for this language. + # + # Rust requires --threads=1 because the legacy rust test extractor + # produces non-deterministic CFG entity ordering under parallel + # evaluation, which makes the snapshot-based PrintCFG test flaky. + # All other languages run in parallel using the CodeQL default. + # + # NOTE: We use a plain string (not an array) because macOS still + # ships Bash 3.2 as /bin/bash, and `"${arr[@]}"` on an empty array + # errors under `set -u` ("unbound variable"). A scalar string with + # unquoted expansion is portable across Bash 3.2 and 4+. + local _threads_arg="" + case "${_tools_dir}" in + */rust/tools) + _threads_arg="--threads=1" + echo "INFO: Forcing --threads=1 for rust (deterministic CFG ordering)" + ;; + esac + # Capture the output and exit code # Explicitly set --failing-exitcode=1 to ensure we get proper exit codes local _output local _exit_code=0 - _output=$(codeql test run --format=text --failing-exitcode=1 --additional-packs="${_tools_dir}" -- "${_test_dir}" 2>&1) || _exit_code=$? + _output=$(codeql test run ${_threads_arg} --format=text --failing-exitcode=1 --additional-packs="${_tools_dir}" -- "${_test_dir}" 2>&1) || _exit_code=$? # Print the output echo "${_output}"