diff --git a/.github/scripts/build-codeql-lib-versions-table.sh b/.github/scripts/build-codeql-lib-versions-table.sh new file mode 100755 index 00000000..8a7e6436 --- /dev/null +++ b/.github/scripts/build-codeql-lib-versions-table.sh @@ -0,0 +1,199 @@ +#!/usr/bin/env bash +# Builds a markdown section documenting the pinned CodeQL CLI version and +# comparing every direct `codeql/*` dependency declared in each language's +# `src/qlpack.yml` AND `lib/qlpack.yml` (e.g. `codeql/-all`, and +# `codeql/-queries` for the languages that also depend on the standard +# queries pack) against the canonical version github/codeql itself bundles +# for that CLI release. +# +# Both `src` and `lib` are checked independently, not just `src`. Each pack +# directory has its own `codeql-pack.lock.yml`, resolved separately by +# `codeql pack upgrade ` - the `codeql/-all: '*'` dependency each +# of them declares can therefore drift out of sync with each other (e.g. if +# someone runs `codeql pack upgrade /src` alone to fix a compile error +# after a CLI bump, and forgets `/lib`). Nothing in this repo enforces +# they stay identical, so both are treated as independent sources of truth +# here rather than assuming one implies the other. +# `ext`/`ext-library-sources` packs are deliberately NOT checked: they pin +# their target via `extensionTargets:`, not `dependencies:`, which CodeQL +# does not resolve into a version-locked `codeql-pack.lock.yml` entry (their +# lock files have an empty `dependencies: {}`), so there is no locked version +# to compare there. +# +# Dependencies are discovered dynamically from each qlpack.yml (not +# hardcoded) so this stays correct if a language adds/drops a direct +# `codeql/*` dependency. +# +# Both the "our locked" and "upstream" version cells link directly to the +# exact file+line that pins that version, at the exact commit (ours) or CLI +# tag (upstream) being compared - so a maintainer can verify the claim with +# one click instead of trusting the table. +# +# This exists because CI's "codeql pack install" step is non-resolving: it +# only installs whatever is already pinned in the checked-in lock file, it +# never re-resolves/upgrades versions. If a maintainer bumps .codeqlversion +# but forgets to run `codeql pack upgrade` for one of a language's pack +# directories, that directory's lock file silently stays on a stale version +# forever, and CI stays green. This table is a machine-checkable tripwire for +# that drift - see CONTRIBUTING.md's "Updating the pinned CodeQL CLI/library +# version" section. +# +# Usage: build-codeql-lib-versions-table.sh +# Requires: gh (authenticated), git, awk, base64 +set -euo pipefail + +REPO="${GITHUB_REPOSITORY:-GitHubSecurityLab/CodeQL-Community-Packs}" +REPO_SHA="${GITHUB_SHA:-$(git rev-parse HEAD)}" + +CODEQL_VERSION=$(tr -d '[:space:]' < .codeqlversion) +if [ -z "$CODEQL_VERSION" ]; then + echo "::error::Could not read .codeqlversion" >&2 + exit 1 +fi + +LANGUAGES=(cpp csharp go java javascript python ruby) +PACKTYPES=(src lib) + +lang_label() { + case "$1" in + cpp) echo "C++" ;; + csharp) echo "C#" ;; + go) echo "Go" ;; + java) echo "Java" ;; + javascript) echo "JavaScript" ;; + python) echo "Python" ;; + ruby) echo "Ruby" ;; + *) echo "$1" ;; + esac +} + +# Lists the direct `codeql/*` dependencies declared in a qlpack.yml's +# `dependencies:` block (skips our own `githubsecuritylab/*` packages). +qlpack_codeql_deps() { + local qlpackfile="$1" + awk ' + { sub(/\r$/, "") } + /^dependencies:/ { found=1; next } + found && /^[[:space:]]+codeql\// { + line=$0 + sub(/^[[:space:]]+/, "", line) + sub(/:.*/, "", line) + print line + next + } + found && /^[^[:space:]]/ { exit } + ' "$qlpackfile" +} + +# Reads the version pinned to a package in a codeql-pack.lock.yml, plus the +# line numbers of the package key and its version line (for permalinks). +# Prints: " " +locked_version_and_lines() { + local lockfile="$1" pkg="$2" + awk -v pkg=" ${pkg}:" ' + { sub(/\r$/, "") } + $0 == pkg { found=1; keyline=NR; next } + found && /^[[:space:]]+version:/ { + gsub(/\r/, "") + print $2, keyline, NR + exit + } + found && /^[^[:space:]]/ { exit } + ' "$lockfile" +} + +# Maps a direct dependency name to the path github/codeql uses for the +# corresponding pack's qlpack.yml. Returns empty if unmapped (unknown dep +# shape) so the caller can surface that instead of guessing. +upstream_path_for_pkg() { + local lang="$1" pkg="$2" + case "$pkg" in + "codeql/${lang}-all") echo "${lang}/ql/lib/qlpack.yml" ;; + "codeql/${lang}-queries") echo "${lang}/ql/src/qlpack.yml" ;; + *) echo "" ;; + esac +} + +# Fetches a file from github/codeql at the tag for our pinned CLI version, +# and prints " " from its top-level `version:` field. +upstream_version_and_line() { + local path="$1" + local content + content=$(gh api "repos/github/codeql/contents/${path}?ref=refs%2Ftags%2Fcodeql-cli%2Fv${CODEQL_VERSION}" \ + --jq '.content' 2>/dev/null | base64 -d 2>/dev/null) || return 1 + [ -n "$content" ] || return 1 + echo "$content" | awk '/^version:/ { print $2, NR; exit }' +} + +MISMATCH=0 + +echo "## CodeQL standard library & query pack versions" +echo +echo "Pinned CodeQL CLI/library version ([\`.codeqlversion\`](https://github.com/${REPO}/blob/${REPO_SHA}/.codeqlversion)): [\`v${CODEQL_VERSION}\`](https://github.com/github/codeql-cli-binaries/releases/tag/v${CODEQL_VERSION})" +echo +echo "_Comparing each language's direct \`codeql/*\` dependencies (from both \`src/qlpack.yml\` and \`lib/qlpack.yml\`, each resolved in its own \`codeql-pack.lock.yml\`) against the versions [github/codeql](https://github.com/github/codeql) itself ships for CLI \`v${CODEQL_VERSION}\` (tag [\`codeql-cli/v${CODEQL_VERSION}\`](https://github.com/github/codeql/tree/codeql-cli/v${CODEQL_VERSION})). \`src\` and \`lib\` are checked independently because they're separately-resolved lock files that can drift from each other. Each cell links to the exact file/line backing it - our side at the commit this table was generated from, upstream at the CLI tag - so the claim can be verified with one click. A mismatch means \`codeql pack upgrade /\` hasn't been run for that specific directory since the last \`.codeqlversion\` bump - see [CONTRIBUTING.md: Updating the pinned CodeQL CLI/library version](https://github.com/${REPO}/blob/${REPO_SHA}/CONTRIBUTING.md#updating-the-pinned-codeql-clilibrary-version)._" +echo +echo "| Language | Pack | Dependency | Our locked version | Upstream (CLI v${CODEQL_VERSION} ships) | Status |" +echo "| --- | --- | --- | --- | --- | --- |" + +for lang in "${LANGUAGES[@]}"; do + label=$(lang_label "$lang") + + for packtype in "${PACKTYPES[@]}"; do + qlpackfile="${lang}/${packtype}/qlpack.yml" + lockfile="${lang}/${packtype}/codeql-pack.lock.yml" + + if [ ! -f "$qlpackfile" ] || [ ! -f "$lockfile" ]; then + echo "| $label | \`$packtype\` | - | ❓ missing qlpack.yml or lock file | - | ❓ |" + MISMATCH=1 + continue + fi + + deps=$(qlpack_codeql_deps "$qlpackfile") + if [ -z "$deps" ]; then + echo "| $label | \`$packtype\` | - | ❓ no direct \`codeql/*\` dependency found | - | ❓ |" + MISMATCH=1 + continue + fi + + while IFS= read -r pkg; do + [ -n "$pkg" ] || continue + + read -r locked keyline verline <<< "$(locked_version_and_lines "$lockfile" "$pkg")" + if [ -z "${locked:-}" ]; then + echo "| $label | \`$packtype\` | \`$pkg\` | ❓ not found in lock file | - | ❓ |" + MISMATCH=1 + continue + fi + locked_link="https://github.com/${REPO}/blob/${REPO_SHA}/${lockfile}#L${keyline}-L${verline}" + locked_cell="[\`$locked\`]($locked_link)" + + upath=$(upstream_path_for_pkg "$lang" "$pkg") + if [ -z "$upath" ]; then + echo "| $label | \`$packtype\` | \`$pkg\` | $locked_cell | ❓ no known upstream mapping | ❓ |" + MISMATCH=1 + continue + fi + + read -r upstream uverline <<< "$(upstream_version_and_line "$upath" || true)" + if [ -z "${upstream:-}" ]; then + echo "| $label | \`$packtype\` | \`$pkg\` | $locked_cell | ❓ fetch failed | ❓ |" + MISMATCH=1 + continue + fi + upstream_link="https://github.com/github/codeql/blob/codeql-cli/v${CODEQL_VERSION}/${upath}#L${uverline}" + upstream_cell="[\`$upstream\`]($upstream_link)" + + if [ "$locked" == "$upstream" ]; then + echo "| $label | \`$packtype\` | \`$pkg\` | $locked_cell | $upstream_cell | ✅ |" + else + echo "| $label | \`$packtype\` | \`$pkg\` | $locked_cell | $upstream_cell | ⚠️ drift |" + MISMATCH=1 + fi + done <<< "$deps" + done +done + +if [ "$MISMATCH" -eq 1 ]; then + echo "::warning::One or more CodeQL standard library/query pack versions are out of sync with CLI v${CODEQL_VERSION}. Run 'codeql pack upgrade /' for the affected language/pack directory(ies)." >&2 +fi diff --git a/.github/scripts/build-publish-summary.sh b/.github/scripts/build-publish-summary.sh new file mode 100755 index 00000000..69098e86 --- /dev/null +++ b/.github/scripts/build-publish-summary.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +# Builds a markdown table summarizing the package versions produced by a +# publish.yml run, from the per-job JSON result fragments it wrote. +# +# Usage: build-publish-summary.sh +# +# Each fragment in looks like: +# { +# "language": "cpp", +# "type": "src", +# "package": "codeql-cpp-queries", +# "previous_version": "0.2.1", +# "target_version": "0.2.2", +# "status": "published" # published | up-to-date | failed +# } +set -euo pipefail + +RESULTS_DIR="${1:?usage: build-publish-summary.sh }" +REPO="${GITHUB_REPOSITORY:-GitHubSecurityLab/CodeQL-Community-Packs}" +RUN_URL="${GITHUB_SERVER_URL:-https://github.com}/${REPO}/actions/runs/${GITHUB_RUN_ID:-}" + +shopt -s nullglob +FRAGMENTS=("$RESULTS_DIR"/*.json) +if [ "${#FRAGMENTS[@]}" -eq 0 ]; then + MERGED='[]' +else + MERGED=$(jq -s '.' "${FRAGMENTS[@]}") +fi + +LANGUAGES=(cpp csharp go java javascript python ruby) +TYPES=(src lib ext ext-library-sources) +EXT_LANGUAGES=(csharp java) + +lang_label() { + case "$1" in + cpp) echo "C++" ;; + csharp) echo "C#" ;; + go) echo "Go" ;; + java) echo "Java" ;; + javascript) echo "JavaScript" ;; + python) echo "Python" ;; + ruby) echo "Ruby" ;; + *) echo "$1" ;; + esac +} + +is_ext_language() { + local lang="$1" + for l in "${EXT_LANGUAGES[@]}"; do + [ "$l" == "$lang" ] && return 0 + done + return 1 +} + +cell() { + local lang="$1" type="$2" + local entry + entry=$(echo "$MERGED" | jq -c --arg l "$lang" --arg t "$type" \ + '[.[] | select(.language == $l and .type == $t)] | first // empty') + + if [ -z "$entry" ] || [ "$entry" == "null" ]; then + # This combo is expected to publish (unlike the "n/a" combos filtered out + # by the caller) but no result fragment was found for it. + echo "❓ no result" + return + fi + + local package version status previous + package=$(echo "$entry" | jq -r '.package') + version=$(echo "$entry" | jq -r '.target_version') + status=$(echo "$entry" | jq -r '.status') + previous=$(echo "$entry" | jq -r '.previous_version') + local url="https://github.com/${REPO}/pkgs/container/${package}?tag=${version}" + + case "$status" in + published) echo "[${version}](${url}) 🆕" ;; + up-to-date) echo "[${version}](${url})" ;; + failed) + if [ -n "$previous" ] && [ "$previous" != "null" ]; then + echo "⚠️ publish failed (still \`${previous}\`)" + else + echo "⚠️ publish failed (previous version unknown - version-check step itself failed)" + fi + ;; + *) echo "❓ unknown" ;; + esac +} + +echo "## Publish summary" +echo +echo "_Generated by [publish.yml run #${GITHUB_RUN_NUMBER:-}](${RUN_URL}) — 🆕 marks a package republished by this run._" +echo +echo "| Language | Queries (\`src\`) | Library (\`lib\`) | Extensions (\`ext\`) | Library sources (\`ext-library-sources\`) |" +echo "| --- | --- | --- | --- | --- |" +for lang in "${LANGUAGES[@]}"; do + row="| $(lang_label "$lang") |" + for type in "${TYPES[@]}"; do + if { [ "$type" == "ext" ] || [ "$type" == "ext-library-sources" ]; } && ! is_ext_language "$lang"; then + row="$row n/a |" + continue + fi + row="$row $(cell "$lang" "$type") |" + done + echo "$row" +done diff --git a/.github/scripts/upsert-release-table.sh b/.github/scripts/upsert-release-table.sh new file mode 100755 index 00000000..175b3e08 --- /dev/null +++ b/.github/scripts/upsert-release-table.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# Upserts a generated markdown block (e.g. the publish-summary table from +# build-publish-summary.sh, or the CodeQL library-versions table from +# build-codeql-lib-versions-table.sh) into the GitHub Release matching +# .release.yml's current version, creating that release as a pre-release if +# it doesn't exist yet. +# +# Usage: upsert-release-table.sh [block-name] +# +# block-name defaults to "publish-summary" for backwards compatibility. Each +# distinct block-name gets its own marker-comment pair, so multiple blocks +# (e.g. "publish-summary" and "codeql-lib-versions") can be upserted +# independently into the same release body without clobbering each other. +# Re-runs only replace the matching block and leave the rest of the release +# body (e.g. "What's Changed") alone. +set -euo pipefail + +TABLE_FILE="${1:?usage: upsert-release-table.sh [block-name]}" +BLOCK_NAME="${2:-publish-summary}" +START_MARKER="" +END_MARKER="" + +VERSION=$(grep -E '^version:' .release.yml | head -1 | sed -E 's/^version:[[:space:]]*"?([^"[:space:]]*)"?/\1/') +if [ -z "$VERSION" ]; then + echo "::error::Could not read version from .release.yml" >&2 + exit 1 +fi +TAG="v${VERSION}" + +BLOCK_FILE=$(mktemp) +{ + echo "$START_MARKER" + cat "$TABLE_FILE" + echo "$END_MARKER" +} > "$BLOCK_FILE" + +if gh release view "$TAG" >/dev/null 2>&1; then + echo "Release $TAG exists; refreshing its $BLOCK_NAME block." + CURRENT_BODY=$(mktemp) + gh release view "$TAG" --json body -q .body > "$CURRENT_BODY" + + NEW_BODY=$(mktemp) + if grep -qF "$START_MARKER" "$CURRENT_BODY" && grep -qF "$END_MARKER" "$CURRENT_BODY"; then + awk -v start="$START_MARKER" -v end="$END_MARKER" -v blockfile="$BLOCK_FILE" ' + $0 == start { + while ((getline line < blockfile) > 0) print line + skip = 1 + next + } + $0 == end { + skip = 0 + next + } + skip { next } + { print } + ' "$CURRENT_BODY" > "$NEW_BODY" + else + # Older release predating this automation, or a corrupted/partial marker + # pair: append the block once rather than risk truncating the body. + cat "$CURRENT_BODY" > "$NEW_BODY" + echo "" >> "$NEW_BODY" + cat "$BLOCK_FILE" >> "$NEW_BODY" + fi + + gh release edit "$TAG" --notes-file "$NEW_BODY" +else + echo "Release $TAG does not exist yet; creating it as a pre-release." + gh release create "$TAG" \ + --title "$TAG" \ + --notes-file "$BLOCK_FILE" \ + --generate-notes \ + --prerelease \ + --target "${GITHUB_SHA:-main}" +fi diff --git a/.github/workflows/detect-codeql-release.yml b/.github/workflows/detect-codeql-release.yml new file mode 100644 index 00000000..2290055f --- /dev/null +++ b/.github/workflows/detect-codeql-release.yml @@ -0,0 +1,87 @@ +name: Detect New CodeQL CLI Release + +# Weekly tripwire for a new upstream CodeQL CLI release. Compares .codeqlversion +# against github/codeql-cli-binaries' latest release and keeps a single persistent +# tracking issue in sync: opens/updates it while we're behind, auto-closes it once +# .codeqlversion catches up. Never opens a PR itself - kicking off the actual bump is +# a deliberate action (see "Update CodeQL CLI Version" workflow) because fixing the +# compile/test breakage a new CLI can introduce isn't safe to run unattended. +on: + schedule: + - cron: "0 14 * * 1" # Every Monday at 14:00 UTC + workflow_dispatch: + +permissions: + contents: read + issues: write + +env: + TRACKING_ISSUE_TITLE: "CodeQL CLI update available" + +jobs: + detect: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Compare pinned vs latest upstream CodeQL CLI release + id: check + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + PINNED=$(tr -d '[:space:]' < .codeqlversion) + LATEST=$(gh api repos/github/codeql-cli-binaries/releases/latest --jq .tag_name | sed 's/^v//') + echo "pinned=$PINNED" >> "$GITHUB_OUTPUT" + echo "latest=$LATEST" >> "$GITHUB_OUTPUT" + if [ "$PINNED" != "$LATEST" ]; then + echo "drift=true" >> "$GITHUB_OUTPUT" + else + echo "drift=false" >> "$GITHUB_OUTPUT" + fi + + - name: Find existing tracking issue + id: existing + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + NUMBER=$(gh issue list --state open --search "\"${TRACKING_ISSUE_TITLE}\" in:title" --json number -q '.[0].number // empty') + echo "number=$NUMBER" >> "$GITHUB_OUTPUT" + + - name: Open or update tracking issue + if: steps.check.outputs.drift == 'true' + env: + GITHUB_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + PINNED: ${{ steps.check.outputs.pinned }} + LATEST: ${{ steps.check.outputs.latest }} + EXISTING: ${{ steps.existing.outputs.number }} + run: | + set -euo pipefail + BODY="A newer CodeQL CLI release is available upstream. + + - Pinned (\`.codeqlversion\`): [\`v${PINNED}\`](https://github.com/github/codeql-cli-binaries/releases/tag/v${PINNED}) + - Latest upstream: [\`v${LATEST}\`](https://github.com/github/codeql-cli-binaries/releases/tag/v${LATEST}) + + To start the update, run the [\"Update CodeQL CLI Version\"](https://github.com/${REPO}/actions/workflows/update-codeql-version.yml) workflow (\`workflow_dispatch\`) with \`codeql_version: ${LATEST}\`. See [CONTRIBUTING.md: Updating the pinned CodeQL CLI/library version](https://github.com/${REPO}/blob/main/CONTRIBUTING.md#updating-the-pinned-codeql-clilibrary-version) for the full process. + + This issue is kept up to date automatically by the \"Detect New CodeQL CLI Release\" workflow and will auto-close once \`.codeqlversion\` catches up." + + if [ -n "$EXISTING" ]; then + gh issue edit "$EXISTING" --body "$BODY" + echo "Updated existing tracking issue #$EXISTING" + else + gh issue create --title "$TRACKING_ISSUE_TITLE" --body "$BODY" --label "version" + fi + + - name: Close tracking issue if caught up + if: steps.check.outputs.drift == 'false' && steps.existing.outputs.number != '' + env: + GITHUB_TOKEN: ${{ github.token }} + EXISTING: ${{ steps.existing.outputs.number }} + PINNED: ${{ steps.check.outputs.pinned }} + run: | + set -euo pipefail + gh issue close "$EXISTING" --comment ".codeqlversion is now \`v${PINNED}\`, matching the latest upstream CodeQL CLI release. Closing." diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 77c52e44..3f1feae4 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,8 +1,15 @@ name: Publish CodeQL Packs +# Auto-publish only fires when .release.yml itself changes - that's the +# deliberate "cut a release" signal (see update-release.yml, which bumps +# .release.yml and every pack version together in one PR). Individual pack +# version bumps merged outside of that flow do NOT auto-publish; use +# workflow_dispatch for one-off hotfixes (see CONTRIBUTING.md). on: push: branches: [main] + paths: + - ".release.yml" workflow_dispatch: @@ -27,12 +34,21 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - PUBLISHED_VERSION=$(gh api /orgs/githubsecuritylab/packages/container/codeql-${{ matrix.language }}-queries/versions --jq '.[0].metadata.container.tags[0]') - CURRENT_VERSION=$(grep version ${{ matrix.language }}/src/qlpack.yml | awk '{print $2}') + # `|| true` inside the substitution (not after it) matters: `run:` steps default to + # `bash -e`, and a brand-new pack's container doesn't exist on GHCR yet, so `gh api` + # returns 404 (non-zero). Without this, that failure would abort the step here and + # fail the job, blocking the first-ever publish of a new pack. Swallowing it leaves + # PUBLISHED_VERSION empty, which correctly differs from CURRENT_VERSION below and + # triggers a first publish. + PUBLISHED_VERSION=$(gh api /orgs/githubsecuritylab/packages/container/codeql-${{ matrix.language }}-queries/versions --jq '.[0].metadata.container.tags[0]' 2>/dev/null || true) + CURRENT_VERSION=$(grep -E '^version:' ${{ matrix.language }}/src/qlpack.yml | head -1 | awk '{print $2}') echo "Published version: $PUBLISHED_VERSION" echo "Local version: $CURRENT_VERSION" + echo "published_version=$PUBLISHED_VERSION" >> $GITHUB_OUTPUT + echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT + if [ "$PUBLISHED_VERSION" != "$CURRENT_VERSION" ]; then echo "publish=true" >> $GITHUB_OUTPUT fi @@ -42,6 +58,7 @@ jobs: uses: ./.github/actions/install-codeql - name: Publish codeql-LANG-queries (src) pack. + id: publish if: steps.check_version.outputs.publish == 'true' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -50,6 +67,38 @@ jobs: codeql pack install "${{ matrix.language }}/src" codeql pack publish "${{ matrix.language }}/src" + - name: Record publish result + if: always() + run: | + mkdir -p publish-results + STATUS="up-to-date" + if [ "${{ steps.check_version.outcome }}" != "success" ]; then + STATUS="failed" + elif [ "${{ steps.check_version.outputs.publish }}" == "true" ]; then + if [ "${{ steps.publish.outcome }}" == "success" ]; then + STATUS="published" + else + STATUS="failed" + fi + fi + jq -n \ + --arg language "${{ matrix.language }}" \ + --arg type "src" \ + --arg package "codeql-${{ matrix.language }}-queries" \ + --arg previous_version "${{ steps.check_version.outputs.published_version }}" \ + --arg target_version "${{ steps.check_version.outputs.current_version }}" \ + --arg status "$STATUS" \ + '{language: $language, type: $type, package: $package, previous_version: $previous_version, target_version: $target_version, status: $status}' \ + > "publish-results/${{ matrix.language }}-src.json" + + - name: Upload publish result + if: always() + uses: actions/upload-artifact@v4 + with: + name: publish-result-${{ matrix.language }}-src + path: publish-results/${{ matrix.language }}-src.json + retention-days: 1 + library: runs-on: ubuntu-latest @@ -70,12 +119,18 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - PUBLISHED_VERSION=$(gh api /orgs/githubsecuritylab/packages/container/codeql-${{ matrix.language }}-libs/versions --jq '.[0].metadata.container.tags[0]') - CURRENT_VERSION=$(grep version ${{ matrix.language }}/lib/qlpack.yml | awk '{print $2}') + # See the `queries` job above for why `|| true` is inside the substitution: a + # brand-new pack's GHCR container doesn't exist yet, so `gh api` 404s; without this, + # `bash -e` would abort here and block the pack's first-ever publish. + PUBLISHED_VERSION=$(gh api /orgs/githubsecuritylab/packages/container/codeql-${{ matrix.language }}-libs/versions --jq '.[0].metadata.container.tags[0]' 2>/dev/null || true) + CURRENT_VERSION=$(grep -E '^version:' ${{ matrix.language }}/lib/qlpack.yml | head -1 | awk '{print $2}') echo "Published version: $PUBLISHED_VERSION" echo "Local version: $CURRENT_VERSION" + echo "published_version=$PUBLISHED_VERSION" >> $GITHUB_OUTPUT + echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT + if [ "$PUBLISHED_VERSION" != "$CURRENT_VERSION" ]; then echo "publish=true" >> $GITHUB_OUTPUT fi @@ -85,6 +140,7 @@ jobs: uses: ./.github/actions/install-codeql - name: Publish codeql-LANG-libs (lib) pack + id: publish if: steps.check_version.outputs.publish == 'true' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -93,6 +149,38 @@ jobs: codeql pack install "${{ matrix.language }}/lib" codeql pack publish "${{ matrix.language }}/lib" + - name: Record publish result + if: always() + run: | + mkdir -p publish-results + STATUS="up-to-date" + if [ "${{ steps.check_version.outcome }}" != "success" ]; then + STATUS="failed" + elif [ "${{ steps.check_version.outputs.publish }}" == "true" ]; then + if [ "${{ steps.publish.outcome }}" == "success" ]; then + STATUS="published" + else + STATUS="failed" + fi + fi + jq -n \ + --arg language "${{ matrix.language }}" \ + --arg type "lib" \ + --arg package "codeql-${{ matrix.language }}-libs" \ + --arg previous_version "${{ steps.check_version.outputs.published_version }}" \ + --arg target_version "${{ steps.check_version.outputs.current_version }}" \ + --arg status "$STATUS" \ + '{language: $language, type: $type, package: $package, previous_version: $previous_version, target_version: $target_version, status: $status}' \ + > "publish-results/${{ matrix.language }}-lib.json" + + - name: Upload publish result + if: always() + uses: actions/upload-artifact@v4 + with: + name: publish-result-${{ matrix.language }}-lib + path: publish-results/${{ matrix.language }}-lib.json + retention-days: 1 + extensions: runs-on: ubuntu-latest @@ -113,11 +201,16 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - PUBLISHED_VERSION=$(gh api /orgs/githubsecuritylab/packages/container/codeql-${{ matrix.language }}-extensions/versions --jq '.[0].metadata.container.tags[0]') - CURRENT_VERSION=$(grep version ${{ matrix.language }}/ext/qlpack.yml | awk '{print $2}') + # See the `queries` job above for why `|| true` is inside the substitution: a + # brand-new pack's GHCR container doesn't exist yet, so `gh api` 404s; without this, + # `bash -e` would abort here and block the pack's first-ever publish. + PUBLISHED_VERSION=$(gh api /orgs/githubsecuritylab/packages/container/codeql-${{ matrix.language }}-extensions/versions --jq '.[0].metadata.container.tags[0]' 2>/dev/null || true) + CURRENT_VERSION=$(grep -E '^version:' ${{ matrix.language }}/ext/qlpack.yml | head -1 | awk '{print $2}') echo "Published version: $PUBLISHED_VERSION" echo "Local version: $CURRENT_VERSION" + echo "published_version=$PUBLISHED_VERSION" >> $GITHUB_OUTPUT + echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT if [ "$PUBLISHED_VERSION" != "$CURRENT_VERSION" ]; then echo "publish=true" >> $GITHUB_OUTPUT fi @@ -127,6 +220,7 @@ jobs: uses: ./.github/actions/install-codeql - name: Publish codeql-LANG-extensions (ext) pack + id: publish if: steps.check_version.outputs.publish == 'true' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -135,6 +229,38 @@ jobs: codeql pack install "${{ matrix.language }}/ext" codeql pack publish "${{ matrix.language }}/ext" + - name: Record publish result + if: always() + run: | + mkdir -p publish-results + STATUS="up-to-date" + if [ "${{ steps.check_version.outcome }}" != "success" ]; then + STATUS="failed" + elif [ "${{ steps.check_version.outputs.publish }}" == "true" ]; then + if [ "${{ steps.publish.outcome }}" == "success" ]; then + STATUS="published" + else + STATUS="failed" + fi + fi + jq -n \ + --arg language "${{ matrix.language }}" \ + --arg type "ext" \ + --arg package "codeql-${{ matrix.language }}-extensions" \ + --arg previous_version "${{ steps.check_version.outputs.published_version }}" \ + --arg target_version "${{ steps.check_version.outputs.current_version }}" \ + --arg status "$STATUS" \ + '{language: $language, type: $type, package: $package, previous_version: $previous_version, target_version: $target_version, status: $status}' \ + > "publish-results/${{ matrix.language }}-ext.json" + + - name: Upload publish result + if: always() + uses: actions/upload-artifact@v4 + with: + name: publish-result-${{ matrix.language }}-ext + path: publish-results/${{ matrix.language }}-ext.json + retention-days: 1 + library_sources_extensions: runs-on: ubuntu-latest @@ -156,11 +282,16 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - PUBLISHED_VERSION=$(gh api /orgs/githubsecuritylab/packages/container/codeql-${{ matrix.language }}-library-sources/versions --jq '.[0].metadata.container.tags[0]') - CURRENT_VERSION=$(grep version ${{ matrix.language }}/ext-library-sources/qlpack.yml | awk '{print $2}') + # See the `queries` job above for why `|| true` is inside the substitution: a + # brand-new pack's GHCR container doesn't exist yet, so `gh api` 404s; without this, + # `bash -e` would abort here and block the pack's first-ever publish. + PUBLISHED_VERSION=$(gh api /orgs/githubsecuritylab/packages/container/codeql-${{ matrix.language }}-library-sources/versions --jq '.[0].metadata.container.tags[0]' 2>/dev/null || true) + CURRENT_VERSION=$(grep -E '^version:' ${{ matrix.language }}/ext-library-sources/qlpack.yml | head -1 | awk '{print $2}') echo "Published version: $PUBLISHED_VERSION" echo "Local version: $CURRENT_VERSION" + echo "published_version=$PUBLISHED_VERSION" >> $GITHUB_OUTPUT + echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT if [ "$PUBLISHED_VERSION" != "$CURRENT_VERSION" ]; then echo "publish=true" >> $GITHUB_OUTPUT fi @@ -170,6 +301,7 @@ jobs: uses: ./.github/actions/install-codeql - name: Publish codeql-LANG-library-sources (ext-library-sources) pack + id: publish if: steps.check_version.outputs.publish == 'true' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -177,3 +309,84 @@ jobs: echo "Publishing codeql-${{ matrix.language }}-library-sources." codeql pack install "${{ matrix.language }}/ext-library-sources" codeql pack publish "${{ matrix.language }}/ext-library-sources" + + - name: Record publish result + if: always() + run: | + mkdir -p publish-results + STATUS="up-to-date" + if [ "${{ steps.check_version.outcome }}" != "success" ]; then + STATUS="failed" + elif [ "${{ steps.check_version.outputs.publish }}" == "true" ]; then + if [ "${{ steps.publish.outcome }}" == "success" ]; then + STATUS="published" + else + STATUS="failed" + fi + fi + jq -n \ + --arg language "${{ matrix.language }}" \ + --arg type "ext-library-sources" \ + --arg package "codeql-${{ matrix.language }}-library-sources" \ + --arg previous_version "${{ steps.check_version.outputs.published_version }}" \ + --arg target_version "${{ steps.check_version.outputs.current_version }}" \ + --arg status "$STATUS" \ + '{language: $language, type: $type, package: $package, previous_version: $previous_version, target_version: $target_version, status: $status}' \ + > "publish-results/${{ matrix.language }}-ext-library-sources.json" + + - name: Upload publish result + if: always() + uses: actions/upload-artifact@v4 + with: + name: publish-result-${{ matrix.language }}-ext-library-sources + path: publish-results/${{ matrix.language }}-ext-library-sources.json + retention-days: 1 + + summary: + name: Publish summary + needs: [queries, library, extensions, library_sources_extensions] + if: always() + runs-on: ubuntu-latest + + permissions: + contents: write + + steps: + - uses: actions/checkout@v7 + + - name: Download publish results + uses: actions/download-artifact@v4 + continue-on-error: true + with: + pattern: publish-result-* + path: publish-results + merge-multiple: true + + - name: Build summary table + run: | + mkdir -p publish-results + echo "Result fragments found:" + ls -la publish-results + .github/scripts/build-publish-summary.sh publish-results > summary.md + echo "summary.md is $(wc -l < summary.md) lines, $(wc -c < summary.md) bytes" + cat summary.md >> "$GITHUB_STEP_SUMMARY" + + - name: Build CodeQL library & query pack versions table + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + .github/scripts/build-codeql-lib-versions-table.sh > codeql-lib-versions.md + cat codeql-lib-versions.md >> "$GITHUB_STEP_SUMMARY" + + - name: Upsert release table + # Only the release-cut path (push to .release.yml) should touch a GitHub Release's + # notes. A workflow_dispatch run is often a one-off hotfix publish for a single pack + # (see CONTRIBUTING.md's "Manual/one-off hotfix publish") while .release.yml still + # points at the last cut release - upserting this run's tables into that release would + # misleadingly overwrite its notes with a summary of an unrelated hotfix run. + if: github.event_name == 'push' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + .github/scripts/upsert-release-table.sh summary.md publish-summary + .github/scripts/upsert-release-table.sh codeql-lib-versions.md codeql-lib-versions diff --git a/.github/workflows/update-codeql-version.yml b/.github/workflows/update-codeql-version.yml new file mode 100644 index 00000000..a4c31693 --- /dev/null +++ b/.github/workflows/update-codeql-version.yml @@ -0,0 +1,107 @@ +name: Update CodeQL CLI Version + +# Bumps the pinned CodeQL CLI/library version (.codeqlversion) and refreshes every +# pack's codeql-pack.lock.yml against it, then opens a PR with the result. +# +# This does NOT publish anything by itself and does NOT bump any pack's `version:` +# field - it only prepares the dependency-refresh half of the process documented in +# CONTRIBUTING.md's "Updating the pinned CodeQL CLI/library version" section. A human +# (or a delegated Copilot coding agent) still needs to fix any compilation/test +# breakage the new CLI/library versions introduce before merging, and the existing +# "CodeQL Update Release" workflow (update-release.yml) is still what bumps every +# pack's version and triggers the real batch publish once this PR is merged. +on: + workflow_dispatch: + inputs: + codeql_version: + description: "New CodeQL CLI version to pin, e.g. 2.22.0 (a leading 'v' is fine too)" + required: true + type: string + +jobs: + update-codeql-version: + runs-on: ubuntu-latest + permissions: + contents: read # PR creation uses a scoped GitHub App token (SECLABS_APP_ID/SECLABS_APP_KEY), not GITHUB_TOKEN + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Validate and normalize version input + id: version + run: | + set -euo pipefail + VERSION="${{ inputs.codeql_version }}" + VERSION="${VERSION#v}" # tolerate an accidental leading "v" + if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::codeql_version must look like X.Y.Z, got: ${{ inputs.codeql_version }}" + exit 1 + fi + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - name: Update .codeqlversion + run: | + echo "${{ steps.version.outputs.version }}" > .codeqlversion + echo "Pinned CodeQL CLI version bumped to ${{ steps.version.outputs.version }}" + + - name: Setup CodeQL + uses: ./.github/actions/install-codeql + + - name: Upgrade every pack's dependencies + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + for dir in $(find . -name qlpack.yml -exec dirname {} \;); do + echo "::group::codeql pack upgrade $dir" + codeql pack upgrade "$dir" + echo "::endgroup::" + done + + - name: Summarize changed files + run: | + { + echo "## Files changed by this CodeQL CLI bump" + echo + git status --porcelain | sed 's/^/- /' + } >> "$GITHUB_STEP_SUMMARY" + + - name: Get Token + id: get_workflow_token + uses: actions/create-github-app-token@v3 + with: + app-id: ${{ secrets.SECLABS_APP_ID }} + private-key: ${{ secrets.SECLABS_APP_KEY }} + + - name: Create Pull Request + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + with: + token: ${{ steps.get_workflow_token.outputs.token }} + title: "chore: bump pinned CodeQL CLI to v${{ steps.version.outputs.version }}" + commit-message: "chore: bump pinned CodeQL CLI to v${{ steps.version.outputs.version }}" + body: | + Automated CLI version bump, requested via the "Update CodeQL CLI Version" + workflow (`workflow_dispatch`, `codeql_version: ${{ steps.version.outputs.version }}`). + + This PR: + - Updates `.codeqlversion` to `${{ steps.version.outputs.version }}`. + - Runs `codeql pack upgrade ` for every pack directory to refresh its + `codeql-pack.lock.yml` against the new CLI. + + **This PR does not publish anything by itself** - no pack `version:` field is + bumped here, so `publish.yml`'s version-diff trigger won't fire for it yet. + Remaining steps (see CONTRIBUTING.md's "Updating the pinned CodeQL CLI/library + version" section): + + - [ ] Check CI on this PR - fix any compilation/test errors caused by upstream + API changes. This is usually the hardest part; consider delegating it to a + Copilot coding agent session pointed at this PR/branch. + - [ ] Update the "Supported CodeQL versions" table in CONTRIBUTING.md. + - [ ] Review and merge. + - [ ] Once merged, run the "CodeQL Update Release" workflow + (`update-release.yml`) to bump every pack's `version:` in lockstep via + `.release.yml` and trigger the real batch publish. + branch: "chore/update-codeql-cli-${{ steps.version.outputs.version }}" + labels: "version" + delete-branch: true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b0e282f4..2640df08 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -64,9 +64,19 @@ After the query is merged, we welcome pull requests to improve it. Every query pack in this repository is compiled and tested against a specific version of the upstream CodeQL standard libraries (e.g. `codeql/java-all`). These queries are **only guaranteed to compile** against the exact library versions shown below: newer or older CodeQL CLI/library versions may rename or remove APIs the queries depend on (see [#151](https://github.com/GitHubSecurityLab/CodeQL-Community-Packs/pull/151) for an example, and [#145](https://github.com/GitHubSecurityLab/CodeQL-Community-Packs/issues/145) for the ongoing effort to refresh these pins). -The pinning is codified in two places per language: +The pinning is codified per language across: - [`.codeqlversion`][codeqlversion] (repo root): the CodeQL **CLI** version CI installs and compiles/tests against. -- `/src/codeql-pack.lock.yml`: the exact resolved (locked) version of `codeql/-all` and its transitive dependencies, generated by `codeql pack install` against the CLI above. The `qlpack.yml` files themselves only declare an unpinned `'*'` range, so the lock file, not the qlpack.yml, is the real source of truth. +- `/src/codeql-pack.lock.yml` **and** `/lib/codeql-pack.lock.yml`: the exact resolved + (locked) version of `codeql/-all` (and, for `src`, `codeql/-queries` where used), + generated by `codeql pack install`/`codeql pack upgrade` against the CLI above. The `qlpack.yml` files + themselves only declare an unpinned `'*'` range, so the lock files, not the `qlpack.yml`, are the real + source of truth. **`src` and `lib` each have their own independently-resolved lock file** - nothing + keeps them in sync automatically, so it's possible (if `codeql pack upgrade` is run against one + directory but not the other) for them to drift apart. Always upgrade both when bumping + `.codeqlversion` (the [automated workflow](#updating-the-pinned-codeql-clilibrary-version) does this + for every pack directory in one pass); the table below only shows `src` for brevity, but the + auto-generated table in every publish summary (see [Cutting a release](#cutting-a-release)) checks + both and will flag drift between them. CodeQL CLI: `v2.21.1` (released 2025-04-16) ([Bundle](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.21.1) / [Binary](https://github.com/github/codeql-cli-binaries/releases/tag/v2.21.1)) @@ -93,26 +103,27 @@ process for each; most of it is manual today. ### Shipping a change to a query/library pack -[`publish.yml`][publish-workflow] runs on every push to `main`. It's organized as four jobs: one -per pack type (`queries` for `src`, `library` for `lib`, `extensions` for `ext`, -`library_sources_extensions` for `ext-library-sources`), each matrixed over every language that has -that pack type (`ext`/`ext-library-sources` only run for `csharp`/`java` today, see [#144][pr-144]). +[`publish.yml`][publish-workflow] is organized as five jobs: four publish jobs, one per pack type +(`queries` for `src`, `library` for `lib`, `extensions` for `ext`, `library_sources_extensions` for +`ext-library-sources`), each matrixed over every language that has that pack type +(`ext`/`ext-library-sources` only run for `csharp`/`java` today, see [#144][pr-144]) - plus a fifth +`summary` job that runs after the other four (`if: always()`, so it still runs even if one of them +fails), aggregates their per-pack results into the publish-summary and CodeQL library/query pack +version tables, and (on the release-cut `push` trigger only) upserts both tables into the GitHub +Release's notes. **Each `` × `` combination is checked and published completely independently.** For every matrix entry, the job compares the `version:` in that one pack's `qlpack.yml` on `main` to the version currently published on [GHCR][ghcr-packages], and only installs + publishes *that specific pack* if they differ. It never touches any other language or -pack type. Concretely, this means: - -- **You only need to bump the version of the pack(s) you actually changed.** If your PR only - touches `java/src`, bumping `java/src/qlpack.yml`'s version is enough; you do not need to touch - `csharp`, `go`, `python`, or any other language/pack type for `codeql-java-queries` to publish. -- **Nothing cascades automatically.** If a single PR changes both `java/src` and `csharp/lib`, each - needs its own version bump: bumping one does not publish the other, and bumping neither means - neither publishes. -- **Merging a change alone does not publish it.** The workflow runs on every merge, but a pack's own - `version:` field must have changed since the last publish, or that pack's matrix entry does nothing - and the change sits on `main` unpublished indefinitely. +pack type. + +**Merging your change does *not* publish it by itself.** `publish.yml` only auto-triggers when +[`.release.yml`][release-config] itself changes on `main` — that's the deliberate "cut a release" +signal, produced by the [Cutting a release](#cutting-a-release) flow below, not by every ordinary +merge. Bumping your pack's own `version:` in a regular PR just *stages* the change: it sits on +`main`, unpublished, until the next release is cut (or someone runs a manual one-off publish, see +[Manual/one-off hotfix publish](#manualone-off-hotfix-publish)). To ship a change: @@ -126,9 +137,8 @@ To ship a change: dependents pin an exact version of it (e.g. `/lib/qlpack.yml`) and bump that pin too (these can drift out of sync otherwise, see [#155][pr-155]). - [ ] Open a PR and get it reviewed/merged to `main`. -- [ ] Nothing further to do: `publish.yml` detects the version diff for that pack and publishes it - automatically on merge. There's no separate "publish" button or manual trigger step for a - version that's already bumped. +- [ ] That's it for your PR — the change publishes the next time a release is cut (see below), not + immediately on merge. There is no in-repo inventory of "what's currently published" today; check the [GHCR Packages page][ghcr-packages] for this repo directly, or compare against the `version:` field @@ -138,18 +148,34 @@ in each language's `qlpack.yml` on `main` to see what will publish next. Bumping the CodeQL CLI/library version this repo builds against (tracked in [`.codeqlversion`][codeqlversion], see [Supported CodeQL versions](#supported-codeql-versions) -above) is also a fully manual process today. Nothing currently detects new upstream CodeQL CLI -releases automatically. A maintainer has to notice one exists and kick off this checklist by hand -(see [#118][pr-118] for an open proposal to at least automate the `codeql pack upgrade` step): - -- [ ] Update `.codeqlversion` to the new CLI version. -- [ ] Run `codeql pack upgrade ` for each pack directory to refresh its `codeql-pack.lock.yml`. -- [ ] Fix any compilation/test errors caused by upstream API changes (usually the hardest part, see - [#124][pr-124] for an example of what this can involve). -- [ ] Bump the `version:` field of every pack that changed, so the "Shipping a change" steps above - actually publish the update. -- [ ] Update the "Supported CodeQL versions" table above. -- [ ] Open a PR and get it reviewed/merged. +above) is semi-automated across three workflows, but still needs a human (or a delegated Copilot +coding agent) in the loop for the hard part — fixing whatever the new CLI breaks: + +1. **Detection** — [`detect-codeql-release.yml`][detect-codeql-release-workflow] runs weekly + (and on `workflow_dispatch`) comparing `.codeqlversion` against + [github/codeql-cli-binaries][codeql-cli-binaries]' latest release. While we're behind, it + opens/updates a single persistent tracking issue titled "CodeQL CLI update available"; once + `.codeqlversion` catches up, it auto-closes that issue. It never opens a PR itself — deciding + when to actually take the upgrade (and deal with any breakage) is a deliberate call, not + something to run unattended. +2. **Dependency refresh** — run [`update-codeql-version.yml`][update-codeql-version-workflow] + (`workflow_dispatch`, input the new CLI version, e.g. `2.22.0`). It updates `.codeqlversion`, + runs `codeql pack upgrade ` for every pack directory to refresh each + `codeql-pack.lock.yml`, and opens a PR (via the same GitHub App token as + [`update-release.yml`][update-release-workflow], so CI actually runs on it — a plain + `GITHUB_TOKEN`-authored PR would not trigger downstream workflows). This is the automated + version of [#118][pr-118]'s original proposal, extended to also own the `.codeqlversion` bump + itself (not just the `codeql pack upgrade` loop) and to use a token that triggers CI. +3. **Fix breakage and finish the checklist** — this PR does **not** publish anything by itself (no + pack `version:` field is touched), so there's no rush, but it still needs: + - [ ] Fix any compilation/test errors CI surfaces from upstream API changes (usually the + hardest part, see [#124][pr-124] for an example of what this can involve). Consider + delegating this step to a Copilot coding agent session pointed at the PR/branch. + - [ ] Update the "Supported CodeQL versions" table above. + - [ ] Review and merge. + - [ ] Once merged, run [`update-release.yml`][update-release-workflow] as described in + [Cutting a release](#cutting-a-release) below to bump every pack's `version:` in + lockstep and trigger the real batch publish. > [!WARNING] > The `.codeqlversion` bump and the pack version bumps don't have to land in the same PR, but @@ -160,30 +186,88 @@ releases automatically. A maintainer has to notice one exists and kick off this > content even though `main` had already moved on. Don't assume a merged dependency-refresh PR means > consumers received it. Check that the pack's `version:` actually changed and published too. +> [!NOTE] +> Forgetting `codeql pack upgrade ` for one language after bumping `.codeqlversion` is the +> other common failure mode: CI's "Install Packs" step only runs `codeql pack install`, which is +> non-resolving — it installs whatever's already pinned in the checked-in lock file and never +> re-resolves or upgrades it, so a stale lock file stays green in CI indefinitely. Every +> [`publish.yml`][publish-workflow] run now cross-checks this automatically: its "CodeQL standard +> library & query pack versions" table (in the run's job summary and upserted into the matching GitHub +> Release, see [Cutting a release](#cutting-a-release) below) compares every direct `codeql/*` +> dependency declared in each language's `src/qlpack.yml` (typically `codeql/-all`, plus +> `codeql/-queries` for C++/C#, which also depend on the standard queries pack) against the +> version [github/codeql](https://github.com/github/codeql) itself ships for the pinned +> `.codeqlversion` (read from the matching `/ql/lib|src/qlpack.yml` at tag +> `codeql-cli/v`) and flags any mismatch with a build warning (`::warning::`) and a ⚠️ in the +> table. Every version in the table links straight to the exact file/line backing it — our side at +> the commit the table was generated from, upstream at the CLI tag — so you can verify a row without +> leaving the release page. This doesn't block the workflow — it's a tripwire to catch drift, not a +> gate. + +### Cutting a release + +`.release.yml` is the **single source of truth** for the repo-wide version, and a release is now +what actually triggers a real, atomic batch publish of every changed pack — this is the *only* +supported way to bump `.release.yml`: + +- [ ] Run [`update-release.yml`][update-release-workflow] (`workflow_dispatch`, pick + patch/minor/major). It runs the [`42ByteLabs/patch-release-me`][patch-release-me] tool, which + reads `.release.yml`'s current `version:`, computes the bump, and opens a PR that: + - bumps `.release.yml`'s `version:` to the new value, and + - patches **every** matching pack's own `version:` field to match (the "CodeQL Pack Versions" + location in `.release.yml`, added in [#158][pr-158] — this is what makes `.release.yml` a real + lever over publishing today, not just a changelog label). +- [ ] Review and merge that PR like any other. +- [ ] Merging it is what changes `.release.yml` on `main`, which auto-triggers + [`publish.yml`][publish-workflow] for a real batch publish: every pack whose version actually + changed gets published to [GHCR][ghcr-packages] in that one run. +- [ ] The run's `summary` job posts two markdown tables to the job summary **and** upserts both into + the matching GitHub Release (`vX.Y.Z`), auto-creating it as a pre-release if it doesn't exist + yet: a publish summary (what published) and a CodeQL standard library & query pack versions + table (whether each language's locked `codeql/*` dependencies match what the pinned CodeQL CLI + actually ships upstream — see the note under + [Updating the pinned CodeQL CLI/library version](#updating-the-pinned-codeql-clilibrary-version)). + +> [!WARNING] +> **Never hand-edit `.release.yml`'s `version:` field directly** — `patch-release-me` computes its +> bump as a *delta* from whatever `.release.yml` currently says, then finds and replaces that exact old +> value across every pack. If you set `.release.yml` straight to a target version yourself, the tool +> has no delta left to apply and running it will overshoot to the *next* version instead of catching +> anything up. If this happens, you have to bump the remaining packs by hand to match what +> `.release.yml` already claims (see [#159][pr-159] for exactly this recovery). + +### Manual/one-off hotfix publish + +`workflow_dispatch` on `publish.yml` remains available outside the release-cut flow above, for +urgent fixes that can't wait for the next batch release (a fatal crash, for example). Two things to +consider before using it: + +- **Prefer a semver pre-release suffix** for the hotfixed pack's version (e.g. `0.2.3-alpha.1` + instead of `0.2.3`) unless you're intentionally shipping the next real version early. GHCR has no + "pre-release" flag the way GitHub Releases do, so the version string is the only signal; a + `-alpha.N` suffix keeps it out of `'*'`-range dependency resolution elsewhere in the repo (semver + ranges exclude pre-release versions from wildcard matches), so it won't get silently picked up + ahead of the real release. +- **[#155][pr-155] is an accepted one-off exception** to this: it shipped a clean `0.2.3` (no + `-alpha` suffix) because it merged before this gated-trigger design and the `-alpha.N` convention + existed. Don't treat it as a precedent for future hotfixes. + ### What GitHub Releases are for -The [Releases][releases] tab (`v0.2.0`, `v0.2.1`, ...) is a **repo-wide changelog**, unrelated to the -per-pack publishing described above: - -- [`.release.yml`][release-config] is config for the [`42ByteLabs/patch-release-me`][patch-release-me] - tool. It tracks a single repository-wide `version:` and defines two patch locations: one targeting - `configs/*.yml` (has never matched anything in this repo's history; those configs never pin an - exact version, so this is effectively dead), and one targeting the exact `codeql--libs:` - dependency pin in `**/qlpack.yml` (only 5 of 7 languages, cpp, go, javascript, python, ruby, pin an - exact version there; csharp/java use `'*'`). Either way, it does **not** bump any pack's own - top-level `version:` field, so bumping it alone doesn't publish anything. -- [`update-release.yml`][update-release-workflow] is a manual `workflow_dispatch` (pick - patch/minor/major) that runs that tool and opens a PR with the bumped `.release.yml` and patched - references. -- The GitHub Release itself is created manually afterwards by a maintainer via GitHub's "Draft a new - release" UI with auto-generated notes. +The [Releases][releases] tab (`v0.2.0`, `v0.2.1`, ...) is a repo-wide changelog tied to cutting a +release as described above. Each release's auto-generated notes are supplemented with the publish +summary table and the CodeQL standard library & query pack versions table (see +[Cutting a release](#cutting-a-release)), so you can see exactly which packs published at that +version — and whether the library/query pack versions they're compiled against are still in sync +with the pinned CodeQL CLI, with a direct link to the exact upstream file/line — without +cross-referencing [GHCR][ghcr-packages] or `github/codeql` separately. > [!NOTE] -> These systems have no mechanism keeping them in sync, and have drifted in practice: `.release.yml`'s -> `version:`, the latest GitHub Release tag, and individual packs' published `version:` fields can all -> disagree with each other at any given time. Don't use the Releases tab or `.release.yml` to infer -> what's currently published. Check [GHCR][ghcr-packages] or a pack's `qlpack.yml` directly, per -> "Shipping a change" above. +> A GitHub Release can still exist as a pre-release ahead of every pack in it actually catching up +> (e.g. if a hotfix or a hand-fixed gap like [#159][pr-159] shipped some packs early/out-of-band). +> The publish summary table in the release body reflects the true, live state of every pack at the +> time of that run — trust that table (or [GHCR][ghcr-packages]/a pack's `qlpack.yml` directly) over +> the release tag or title alone. ## Using your personal data @@ -196,6 +280,9 @@ Please do get in touch (privacy@github.com) if you have any questions about this [codeqlversion]: ./.codeqlversion [publish-workflow]: ./.github/workflows/publish.yml [update-release-workflow]: ./.github/workflows/update-release.yml +[update-codeql-version-workflow]: ./.github/workflows/update-codeql-version.yml +[detect-codeql-release-workflow]: ./.github/workflows/detect-codeql-release.yml +[codeql-cli-binaries]: https://github.com/github/codeql-cli-binaries/releases [release-config]: ./.release.yml [patch-release-me]: https://github.com/42ByteLabs/patch-release-me [releases]: https://github.com/GitHubSecurityLab/CodeQL-Community-Packs/releases @@ -205,3 +292,5 @@ Please do get in touch (privacy@github.com) if you have any questions about this [pr-126]: https://github.com/GitHubSecurityLab/CodeQL-Community-Packs/pull/126 [pr-144]: https://github.com/GitHubSecurityLab/CodeQL-Community-Packs/pull/144 [pr-155]: https://github.com/GitHubSecurityLab/CodeQL-Community-Packs/pull/155 +[pr-158]: https://github.com/GitHubSecurityLab/CodeQL-Community-Packs/pull/158 +[pr-159]: https://github.com/GitHubSecurityLab/CodeQL-Community-Packs/pull/159