From 3d932eb1f89683d07c15c7a50119e9da69d38f6a Mon Sep 17 00:00:00 2001 From: Chad Bentz <1760475+felickz@users.noreply.github.com> Date: Thu, 9 Jul 2026 18:11:17 -0400 Subject: [PATCH 01/13] feat(publish): add publish-summary table + GitHub Release upsert publish.yml now records a per-pack JSON result fragment (published / up-to-date / failed) from each of the 4 jobs' matrix entries, uploads them as artifacts, and a new summary job downloads and merges them into a unified markdown table (languages x pack types) that is: 1. written to the Actions run summary ($GITHUB_STEP_SUMMARY) 2. upserted into the matching GitHub Release body (.release.yml's version -> vX.Y.Z tag), inside marker comments so "What's Changed" and other release content is preserved across runs New scripts: - .github/scripts/build-publish-summary.sh - .github/scripts/upsert-release-table.sh Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/scripts/build-publish-summary.sh | 99 +++++++++++++ .github/scripts/upsert-release-table.sh | 66 +++++++++ .github/workflows/publish.yml | 172 +++++++++++++++++++++++ 3 files changed, 337 insertions(+) create mode 100755 .github/scripts/build-publish-summary.sh create mode 100755 .github/scripts/upsert-release-table.sh diff --git a/.github/scripts/build-publish-summary.sh b/.github/scripts/build-publish-summary.sh new file mode 100755 index 00000000..34f6b250 --- /dev/null +++ b/.github/scripts/build-publish-summary.sh @@ -0,0 +1,99 @@ +#!/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) echo "⚠️ publish failed (still \`${previous}\`)" ;; + *) 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..6939faa5 --- /dev/null +++ b/.github/scripts/upsert-release-table.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# Upserts the publish-summary table (built by build-publish-summary.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 +# +# The table is stored between marker comments so re-runs only replace that +# 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 }" +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 publish-summary block." + CURRENT_BODY=$(mktemp) + gh release view "$TAG" --json body -q .body > "$CURRENT_BODY" + + NEW_BODY=$(mktemp) + if grep -qF "$START_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: append the block once. + 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/publish.yml b/.github/workflows/publish.yml index 77c52e44..905eb7fa 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -33,6 +33,9 @@ jobs: 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 +45,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 +54,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 @@ -76,6 +112,9 @@ jobs: 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 +124,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 +133,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 @@ -118,6 +190,8 @@ jobs: 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 +201,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 +210,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 @@ -161,6 +268,8 @@ jobs: 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 +279,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 +287,65 @@ 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 + .github/scripts/build-publish-summary.sh publish-results | tee summary.md >> "$GITHUB_STEP_SUMMARY" + + - name: Upsert release table + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: .github/scripts/upsert-release-table.sh summary.md From e5b4080c1f2d453f9f3f7509db90d5d845066ffa Mon Sep 17 00:00:00 2001 From: Chad Bentz <1760475+felickz@users.noreply.github.com> Date: Thu, 9 Jul 2026 18:18:52 -0400 Subject: [PATCH 02/13] fix(publish): avoid tee+pipe fragility in Build summary table step The previous form piped the script's stdout through tee and redirected tee's own stdout (not the file write) into GITHUB_STEP_SUMMARY. Combined with 'bash -e' (no pipefail) at the step level, this masked failures and produced an empty summary.md in a real run, upserting empty publish-summary markers into the v0.2.2 release. Switch to plain sequential redirects and add debug output (fragment listing, line/byte counts) to make future failures visible in the step log. --- .github/workflows/publish.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 905eb7fa..f811fd09 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -343,7 +343,11 @@ jobs: - name: Build summary table run: | mkdir -p publish-results - .github/scripts/build-publish-summary.sh publish-results | tee summary.md >> "$GITHUB_STEP_SUMMARY" + 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: Upsert release table env: From 75e4b8815cf173e3a12328cc10c7f9bbbdee9f66 Mon Sep 17 00:00:00 2001 From: Chad Bentz <1760475+felickz@users.noreply.github.com> Date: Fri, 10 Jul 2026 00:28:51 -0400 Subject: [PATCH 03/13] feat(publish): gate auto-publish on .release.yml changes; document release process - publish.yml's push trigger now only fires when .release.yml changes (paths filter), not on every push to main. workflow_dispatch remains as the manual escape hatch for one-off hotfix publishes. - Rewrite CONTRIBUTING.md's Releases & publishing section to match: shipping a pack change now just stages it until the next release cut or a manual dispatch; add a Cutting a release walkthrough for update-release.yml/patch-release-me now that .release.yml's CodeQL Pack Versions location (#158) actually drives every pack's version; document the -alpha.N + workflow_dispatch hotfix convention and note #155 as an accepted one-off exception; note #159 as the recovery path if .release.yml's version is ever hand-edited directly. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/publish.yml | 7 +++ CONTRIBUTING.md | 108 ++++++++++++++++++++++------------ 2 files changed, 77 insertions(+), 38 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f811fd09..36c89814 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: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b0e282f4..bed4a777 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -93,26 +93,23 @@ 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 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]). **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 +123,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 @@ -160,30 +156,64 @@ 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. +### 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 a markdown table of what published to the job summary **and** + upserts it into the matching GitHub Release (`vX.Y.Z`), auto-creating it as a pre-release if it + doesn't exist yet. + +> [!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 find/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 (see [Cutting a release](#cutting-a-release)), so you can see exactly which packs +published at that version without cross-referencing [GHCR][ghcr-packages] 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 @@ -205,3 +235,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 From cd50d6a61336901c43c77cd9ebf5ce43e4d72a71 Mon Sep 17 00:00:00 2001 From: Chad Bentz <1760475+felickz@users.noreply.github.com> Date: Fri, 10 Jul 2026 00:51:42 -0400 Subject: [PATCH 04/13] fix: address Copilot review feedback on PR #160 - Anchor CURRENT_VERSION extraction to '^version:' (with head -1) in all four publish.yml jobs so a stray 'version' substring elsewhere in a qlpack.yml (e.g. a comment) can't be matched instead of the real field. - upsert-release-table.sh: only take the in-place marker-replacement path when BOTH the start and end marker are present in the existing release body; otherwise append, so a corrupted/partial marker pair can't cause the awk script to silently truncate the rest of the release notes. - CONTRIBUTING.md: grammar fix (find/replaces -> finds and replaces). Verified gh release create --notes-file + --generate-notes together is NOT mutually exclusive in gh 2.96.0 (contrary to a review comment) -- confirmed empirically: the v0.2.3 release this script created has both the publish-summary table (from --notes-file) and the auto-generated 'What's Changed' changelog (from --generate-notes) in its body. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/scripts/upsert-release-table.sh | 5 +++-- .github/workflows/publish.yml | 8 ++++---- CONTRIBUTING.md | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/scripts/upsert-release-table.sh b/.github/scripts/upsert-release-table.sh index 6939faa5..5f8a1c4a 100755 --- a/.github/scripts/upsert-release-table.sh +++ b/.github/scripts/upsert-release-table.sh @@ -33,7 +33,7 @@ if gh release view "$TAG" >/dev/null 2>&1; then gh release view "$TAG" --json body -q .body > "$CURRENT_BODY" NEW_BODY=$(mktemp) - if grep -qF "$START_MARKER" "$CURRENT_BODY"; then + 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 @@ -48,7 +48,8 @@ if gh release view "$TAG" >/dev/null 2>&1; then { print } ' "$CURRENT_BODY" > "$NEW_BODY" else - # Older release predating this automation: append the block once. + # 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" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 36c89814..97cd2dc5 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -35,7 +35,7 @@ jobs: 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}') + 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" @@ -114,7 +114,7 @@ jobs: 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}') + 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" @@ -193,7 +193,7 @@ jobs: 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}') + 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" @@ -271,7 +271,7 @@ jobs: 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}') + 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" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bed4a777..3b42975d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -179,7 +179,7 @@ supported way to bump `.release.yml`: > [!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 find/replaces that exact old +> 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 From ec6dd17bd408cd1424e38fefec095dbefc772b75 Mon Sep 17 00:00:00 2001 From: Chad Bentz <1760475+felickz@users.noreply.github.com> Date: Fri, 10 Jul 2026 01:17:01 -0400 Subject: [PATCH 05/13] feat(publish): add CodeQL standard library versions cross-check table Every publish.yml run now builds and upserts a second table into the matching GitHub Release: for each language, the locked codeql/-all version (from lib/codeql-pack.lock.yml) versus the version github/codeql itself bundles with the pinned CodeQL CLI (.codeqlversion), read from its own /ql/lib/qlpack.yml at tag codeql-cli/v. Mismatches are flagged with an actions warning annotation and a table marker, giving a machine-checkable tripwire for the drift risk documented in CONTRIBUTING.md (CI's 'codeql pack install' step is non-resolving, so a forgotten 'codeql pack upgrade ' after a .codeqlversion bump silently pins a language to a stale library version forever). - Add .github/scripts/build-codeql-lib-versions-table.sh - Parameterize upsert-release-table.sh with an optional block-name arg so multiple independent marker-comment blocks can coexist in one release body without clobbering each other - Wire both into publish.yml's summary job - Document the new table and cross-check in CONTRIBUTING.md Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../build-codeql-lib-versions-table.sh | 111 ++++++++++++++++++ .github/scripts/upsert-release-table.sh | 27 +++-- .github/workflows/publish.yml | 11 +- CONTRIBUTING.md | 28 ++++- 4 files changed, 161 insertions(+), 16 deletions(-) create mode 100644 .github/scripts/build-codeql-lib-versions-table.sh diff --git a/.github/scripts/build-codeql-lib-versions-table.sh b/.github/scripts/build-codeql-lib-versions-table.sh new file mode 100644 index 00000000..d6df3e91 --- /dev/null +++ b/.github/scripts/build-codeql-lib-versions-table.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash +# Builds a markdown section documenting the pinned CodeQL CLI version and +# comparing our locked `codeql/-all` library versions (from each +# language's lib/codeql-pack.lock.yml) against the canonical versions +# bundled with that CLI release (from github/codeql's own +# /ql/lib/qlpack.yml at tag codeql-cli/v<.codeqlversion>). +# +# 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 /lib` for a language, that +# language's lock file silently stays on a stale library 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), awk, base64 +set -euo pipefail + +REPO="${GITHUB_REPOSITORY:-GitHubSecurityLab/CodeQL-Community-Packs}" + +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) + +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 +} + +# Reads the version pinned to `codeql/-all` in a codeql-pack.lock.yml. +locked_version() { + local lockfile="$1" pkg="$2" + awk -v pkg=" ${pkg}:" ' + { sub(/\r$/, "") } + $0 == pkg { found=1; next } + found && /^[[:space:]]+version:/ { gsub(/\r/, ""); print $2; exit } + found && /^[^[:space:]]/ { exit } + ' "$lockfile" +} + +# Fetches the version github/codeql itself ships for codeql/-all at +# the tag corresponding to our pinned CLI version. +upstream_version() { + local lang="$1" + gh api "repos/github/codeql/contents/${lang}/ql/lib/qlpack.yml?ref=refs%2Ftags%2Fcodeql-cli%2Fv${CODEQL_VERSION}" \ + --jq '.content' 2>/dev/null | base64 -d 2>/dev/null | \ + grep -E '^version:' | head -1 | awk '{print $2}' || true +} + +MISMATCH=0 + +echo "## CodeQL standard library versions" +echo +echo "Pinned CodeQL CLI/library version ([\`.codeqlversion\`](https://github.com/${REPO}/blob/main/.codeqlversion)): [\`v${CODEQL_VERSION}\`](https://github.com/github/codeql-cli-binaries/releases/tag/v${CODEQL_VERSION})" +echo +echo "_Comparing our locked \`codeql/-all\` version (from each language's \`lib/codeql-pack.lock.yml\`) against the version [github/codeql](https://github.com/github/codeql) itself bundles with CLI \`v${CODEQL_VERSION}\` (tag [\`codeql-cli/v${CODEQL_VERSION}\`](https://github.com/github/codeql/tree/codeql-cli/v${CODEQL_VERSION})). A mismatch means \`codeql pack upgrade /lib\` (and \`src\`/\`ext\` as needed) hasn't been run since the last \`.codeqlversion\` bump - see [CONTRIBUTING.md: Updating the pinned CodeQL CLI/library version](https://github.com/${REPO}/blob/main/CONTRIBUTING.md#updating-the-pinned-codeql-clilibrary-version)._" +echo +echo "| Language | Our locked \`codeql/-all\` | Upstream CLI v${CODEQL_VERSION} bundles | Status |" +echo "| --- | --- | --- | --- |" + +for lang in "${LANGUAGES[@]}"; do + lockfile="${lang}/lib/codeql-pack.lock.yml" + pkg="codeql/${lang}-all" + label=$(lang_label "$lang") + + if [ ! -f "$lockfile" ]; then + echo "| $label | ❓ no lock file | - | ❓ |" + MISMATCH=1 + continue + fi + + locked=$(locked_version "$lockfile" "$pkg") + upstream=$(upstream_version "$lang") + + if [ -z "$locked" ]; then + echo "| $label | ❓ not found in lock file | - | ❓ |" + MISMATCH=1 + continue + fi + + if [ -z "$upstream" ]; then + echo "| $label | \`$locked\` | ❓ fetch failed | ❓ |" + MISMATCH=1 + continue + fi + + if [ "$locked" == "$upstream" ]; then + echo "| $label | \`$locked\` | \`$upstream\` | ✅ |" + else + echo "| $label | \`$locked\` | \`$upstream\` | ⚠️ drift |" + MISMATCH=1 + fi +done + +if [ "$MISMATCH" -eq 1 ]; then + echo "::warning::One or more CodeQL standard library versions are out of sync with CLI v${CODEQL_VERSION}. Run 'codeql pack upgrade /lib' (and src/ext as needed) for the affected language(s)." >&2 +fi diff --git a/.github/scripts/upsert-release-table.sh b/.github/scripts/upsert-release-table.sh index 5f8a1c4a..175b3e08 100755 --- a/.github/scripts/upsert-release-table.sh +++ b/.github/scripts/upsert-release-table.sh @@ -1,17 +1,24 @@ #!/usr/bin/env bash -# Upserts the publish-summary table (built by build-publish-summary.sh) into -# the GitHub Release matching .release.yml's current version, creating that -# release as a pre-release if it doesn't exist yet. +# 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 +# Usage: upsert-release-table.sh [block-name] # -# The table is stored between marker comments so re-runs only replace that -# block and leave the rest of the release body (e.g. "What's Changed") alone. +# 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 }" -START_MARKER="" -END_MARKER="" +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 @@ -28,7 +35,7 @@ BLOCK_FILE=$(mktemp) } > "$BLOCK_FILE" if gh release view "$TAG" >/dev/null 2>&1; then - echo "Release $TAG exists; refreshing its publish-summary block." + echo "Release $TAG exists; refreshing its $BLOCK_NAME block." CURRENT_BODY=$(mktemp) gh release view "$TAG" --json body -q .body > "$CURRENT_BODY" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 97cd2dc5..1b22cc89 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -356,7 +356,16 @@ jobs: echo "summary.md is $(wc -l < summary.md) lines, $(wc -c < summary.md) bytes" cat summary.md >> "$GITHUB_STEP_SUMMARY" + - name: Build CodeQL library 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 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: .github/scripts/upsert-release-table.sh summary.md + 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/CONTRIBUTING.md b/CONTRIBUTING.md index 3b42975d..acc81d09 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -156,6 +156,19 @@ 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 versions" table (in the run's job summary and upserted into the matching GitHub Release, +> see [Cutting a release](#cutting-a-release) below) compares each language's locked +> `codeql/-all` version against the version [github/codeql](https://github.com/github/codeql) +> itself ships for the pinned `.codeqlversion` (read from its `/ql/lib/qlpack.yml` at tag +> `codeql-cli/v`) and flags any mismatch with a build warning (`::warning::`) and a ⚠️ in the +> table. 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 @@ -173,9 +186,12 @@ supported way to bump `.release.yml`: - [ ] 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 a markdown table of what published to the job summary **and** - upserts it into the matching GitHub Release (`vX.Y.Z`), auto-creating it as a pre-release if it - doesn't exist yet. +- [ ] 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 versions table (whether + each language's locked `codeql/-all` matches what the pinned CodeQL CLI actually + bundles 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 @@ -205,8 +221,10 @@ consider before using it: 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 (see [Cutting a release](#cutting-a-release)), so you can see exactly which packs -published at that version without cross-referencing [GHCR][ghcr-packages] separately. +summary table and the CodeQL standard library versions table (see +[Cutting a release](#cutting-a-release)), so you can see exactly which packs published at that +version — and whether the library versions they're compiled against are still in sync with the +pinned CodeQL CLI — without cross-referencing [GHCR][ghcr-packages] or `github/codeql` separately. > [!NOTE] > A GitHub Release can still exist as a pre-release ahead of every pack in it actually catching up From 7b75ce1161acb730e7e409773d32e960e63dfa6d Mon Sep 17 00:00:00 2001 From: Chad Bentz <1760475+felickz@users.noreply.github.com> Date: Fri, 10 Jul 2026 01:21:48 -0400 Subject: [PATCH 06/13] fix: mark build-codeql-lib-versions-table.sh executable git-created file lost the exec bit, causing publish.yml's new step to fail with 'Permission denied' (exit 126). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/scripts/build-codeql-lib-versions-table.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 .github/scripts/build-codeql-lib-versions-table.sh diff --git a/.github/scripts/build-codeql-lib-versions-table.sh b/.github/scripts/build-codeql-lib-versions-table.sh old mode 100644 new mode 100755 From 065959588cfd421d276ef6088ca459638b4a74a5 Mon Sep 17 00:00:00 2001 From: Chad Bentz <1760475+felickz@users.noreply.github.com> Date: Fri, 10 Jul 2026 01:57:30 -0400 Subject: [PATCH 07/13] fix(build-codeql-lib-versions-table): check src lock file, not lib CONTRIBUTING.md's 'Supported CodeQL versions' table already documents /src/codeql-pack.lock.yml as the source-of-truth lock file (the queries pack CI actually compiles/tests against), not lib. Reading from lib/codeql-pack.lock.yml instead meant the tripwire could miss drift in the src lock, or flag drift that didn't actually affect the compiled queries. Switch the check (and its remediation guidance) to src, per Copilot review feedback on PR #160. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../build-codeql-lib-versions-table.sh | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/.github/scripts/build-codeql-lib-versions-table.sh b/.github/scripts/build-codeql-lib-versions-table.sh index d6df3e91..63c036d2 100755 --- a/.github/scripts/build-codeql-lib-versions-table.sh +++ b/.github/scripts/build-codeql-lib-versions-table.sh @@ -1,18 +1,22 @@ #!/usr/bin/env bash # Builds a markdown section documenting the pinned CodeQL CLI version and -# comparing our locked `codeql/-all` library versions (from each -# language's lib/codeql-pack.lock.yml) against the canonical versions -# bundled with that CLI release (from github/codeql's own -# /ql/lib/qlpack.yml at tag codeql-cli/v<.codeqlversion>). +# comparing our locked `codeql/-all` library version (from each +# language's src/codeql-pack.lock.yml - the queries pack CONTRIBUTING.md's +# "Supported CodeQL versions" table treats as the source of truth) against +# the canonical version bundled with that CLI release (from github/codeql's +# own /ql/lib/qlpack.yml at tag codeql-cli/v<.codeqlversion>). # # 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 /lib` for a language, that -# language's lock file silently stays on a stale library 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. +# but forgets to run `codeql pack upgrade /src` (and lib/ext) for a +# language, that language's lock file silently stays on a stale library +# 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. It checks one representative +# directory (src) per language as a coarse signal, not every pack +# directory; a mismatch there is reason enough to re-run +# `codeql pack upgrade` across all of that language's directories. # # Usage: build-codeql-lib-versions-table.sh # Requires: gh (authenticated), awk, base64 @@ -67,13 +71,13 @@ echo "## CodeQL standard library versions" echo echo "Pinned CodeQL CLI/library version ([\`.codeqlversion\`](https://github.com/${REPO}/blob/main/.codeqlversion)): [\`v${CODEQL_VERSION}\`](https://github.com/github/codeql-cli-binaries/releases/tag/v${CODEQL_VERSION})" echo -echo "_Comparing our locked \`codeql/-all\` version (from each language's \`lib/codeql-pack.lock.yml\`) against the version [github/codeql](https://github.com/github/codeql) itself bundles with CLI \`v${CODEQL_VERSION}\` (tag [\`codeql-cli/v${CODEQL_VERSION}\`](https://github.com/github/codeql/tree/codeql-cli/v${CODEQL_VERSION})). A mismatch means \`codeql pack upgrade /lib\` (and \`src\`/\`ext\` as needed) hasn't been run since the last \`.codeqlversion\` bump - see [CONTRIBUTING.md: Updating the pinned CodeQL CLI/library version](https://github.com/${REPO}/blob/main/CONTRIBUTING.md#updating-the-pinned-codeql-clilibrary-version)._" +echo "_Comparing our locked \`codeql/-all\` version (from each language's \`src/codeql-pack.lock.yml\`) against the version [github/codeql](https://github.com/github/codeql) itself bundles with CLI \`v${CODEQL_VERSION}\` (tag [\`codeql-cli/v${CODEQL_VERSION}\`](https://github.com/github/codeql/tree/codeql-cli/v${CODEQL_VERSION})). A mismatch means \`codeql pack upgrade /src\` (and \`lib\`/\`ext\` as needed) hasn't been run since the last \`.codeqlversion\` bump - see [CONTRIBUTING.md: Updating the pinned CodeQL CLI/library version](https://github.com/${REPO}/blob/main/CONTRIBUTING.md#updating-the-pinned-codeql-clilibrary-version)._" echo echo "| Language | Our locked \`codeql/-all\` | Upstream CLI v${CODEQL_VERSION} bundles | Status |" echo "| --- | --- | --- | --- |" for lang in "${LANGUAGES[@]}"; do - lockfile="${lang}/lib/codeql-pack.lock.yml" + lockfile="${lang}/src/codeql-pack.lock.yml" pkg="codeql/${lang}-all" label=$(lang_label "$lang") @@ -107,5 +111,5 @@ for lang in "${LANGUAGES[@]}"; do done if [ "$MISMATCH" -eq 1 ]; then - echo "::warning::One or more CodeQL standard library versions are out of sync with CLI v${CODEQL_VERSION}. Run 'codeql pack upgrade /lib' (and src/ext as needed) for the affected language(s)." >&2 + echo "::warning::One or more CodeQL standard library versions are out of sync with CLI v${CODEQL_VERSION}. Run 'codeql pack upgrade /src' (and lib/ext as needed) for the affected language(s)." >&2 fi From 16c7cb0d55b7b6578d73d4ef0c5ab19a48016d58 Mon Sep 17 00:00:00 2001 From: Chad Bentz <1760475+felickz@users.noreply.github.com> Date: Fri, 10 Jul 2026 02:12:44 -0400 Subject: [PATCH 08/13] feat(publish): commit/tag-anchored links + queries pack in lib-versions table - Discover direct codeql/* dependencies dynamically from each language's src/qlpack.yml instead of hardcoding codeql/-all, so this also covers codeql/-queries for C++/C# (which additionally depend on the upstream standard queries pack). - Link every "our locked" version to the exact commit/file/line in this repo (blob//...#L) and every "upstream" version to the exact file/line in github/codeql at the matching codeql-cli/vX tag, so each row can be verified with one click instead of trusting the table. - Rename the section/table to "CodeQL standard library & query pack versions" and update CONTRIBUTING.md references accordingly. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../build-codeql-lib-versions-table.sh | 168 +++++++++++++----- .github/workflows/publish.yml | 2 +- CONTRIBUTING.md | 28 +-- 3 files changed, 137 insertions(+), 61 deletions(-) diff --git a/.github/scripts/build-codeql-lib-versions-table.sh b/.github/scripts/build-codeql-lib-versions-table.sh index 63c036d2..a18663ba 100755 --- a/.github/scripts/build-codeql-lib-versions-table.sh +++ b/.github/scripts/build-codeql-lib-versions-table.sh @@ -1,28 +1,39 @@ #!/usr/bin/env bash # Builds a markdown section documenting the pinned CodeQL CLI version and -# comparing our locked `codeql/-all` library version (from each -# language's src/codeql-pack.lock.yml - the queries pack CONTRIBUTING.md's -# "Supported CodeQL versions" table treats as the source of truth) against -# the canonical version bundled with that CLI release (from github/codeql's -# own /ql/lib/qlpack.yml at tag codeql-cli/v<.codeqlversion>). +# comparing every direct `codeql/*` dependency declared in each language's +# src/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. +# +# Dependencies are discovered dynamically from each `/src/qlpack.yml` +# (not hardcoded) so this stays correct if a language adds/drops a direct +# `codeql/*` dependency. Only `src/codeql-pack.lock.yml` is read for locked +# versions - CONTRIBUTING.md's "Supported CodeQL versions" table treats it as +# the source of truth. This checks one representative directory (src) per +# language as a coarse signal, not every pack directory; a mismatch there is +# reason enough to re-run `codeql pack upgrade` across all of that language's +# directories (src/lib/ext as applicable). +# +# 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 /src` (and lib/ext) for a -# language, that language's lock file silently stays on a stale library -# 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. It checks one representative -# directory (src) per language as a coarse signal, not every pack -# directory; a mismatch there is reason enough to re-run -# `codeql pack upgrade` across all of that language's directories. +# language, that language'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), awk, base64 +# 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 @@ -45,71 +56,130 @@ lang_label() { esac } -# Reads the version pinned to `codeql/-all` in a codeql-pack.lock.yml. -locked_version() { +# 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; next } - found && /^[[:space:]]+version:/ { gsub(/\r/, ""); print $2; exit } + $0 == pkg { found=1; keyline=NR; next } + found && /^[[:space:]]+version:/ { + gsub(/\r/, "") + print $2, keyline, NR + exit + } found && /^[^[:space:]]/ { exit } ' "$lockfile" } -# Fetches the version github/codeql itself ships for codeql/-all at -# the tag corresponding to our pinned CLI version. -upstream_version() { - local lang="$1" - gh api "repos/github/codeql/contents/${lang}/ql/lib/qlpack.yml?ref=refs%2Ftags%2Fcodeql-cli%2Fv${CODEQL_VERSION}" \ - --jq '.content' 2>/dev/null | base64 -d 2>/dev/null | \ - grep -E '^version:' | head -1 | awk '{print $2}' || true +# 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 versions" +echo "## CodeQL standard library & query pack versions" echo -echo "Pinned CodeQL CLI/library version ([\`.codeqlversion\`](https://github.com/${REPO}/blob/main/.codeqlversion)): [\`v${CODEQL_VERSION}\`](https://github.com/github/codeql-cli-binaries/releases/tag/v${CODEQL_VERSION})" +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 our locked \`codeql/-all\` version (from each language's \`src/codeql-pack.lock.yml\`) against the version [github/codeql](https://github.com/github/codeql) itself bundles with CLI \`v${CODEQL_VERSION}\` (tag [\`codeql-cli/v${CODEQL_VERSION}\`](https://github.com/github/codeql/tree/codeql-cli/v${CODEQL_VERSION})). A mismatch means \`codeql pack upgrade /src\` (and \`lib\`/\`ext\` as needed) hasn't been run since the last \`.codeqlversion\` bump - see [CONTRIBUTING.md: Updating the pinned CodeQL CLI/library version](https://github.com/${REPO}/blob/main/CONTRIBUTING.md#updating-the-pinned-codeql-clilibrary-version)._" +echo "_Comparing each language's direct \`codeql/*\` dependencies (from \`/src/qlpack.yml\`, resolved in \`src/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})). 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 /src\` (and \`lib\`/\`ext\` as needed) hasn't been run 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 | Our locked \`codeql/-all\` | Upstream CLI v${CODEQL_VERSION} bundles | Status |" -echo "| --- | --- | --- | --- |" +echo "| Language | Dependency | Our locked version | Upstream (CLI v${CODEQL_VERSION} ships) | Status |" +echo "| --- | --- | --- | --- | --- |" for lang in "${LANGUAGES[@]}"; do + qlpackfile="${lang}/src/qlpack.yml" lockfile="${lang}/src/codeql-pack.lock.yml" - pkg="codeql/${lang}-all" label=$(lang_label "$lang") - if [ ! -f "$lockfile" ]; then - echo "| $label | ❓ no lock file | - | ❓ |" + if [ ! -f "$qlpackfile" ] || [ ! -f "$lockfile" ]; then + echo "| $label | - | ❓ missing qlpack.yml or lock file | - | ❓ |" MISMATCH=1 continue fi - locked=$(locked_version "$lockfile" "$pkg") - upstream=$(upstream_version "$lang") - - if [ -z "$locked" ]; then - echo "| $label | ❓ not found in lock file | - | ❓ |" + deps=$(qlpack_codeql_deps "$qlpackfile") + if [ -z "$deps" ]; then + echo "| $label | - | ❓ no direct \`codeql/*\` dependency found | - | ❓ |" MISMATCH=1 continue fi - if [ -z "$upstream" ]; then - echo "| $label | \`$locked\` | ❓ fetch failed | ❓ |" - MISMATCH=1 - continue - fi + while IFS= read -r pkg; do + [ -n "$pkg" ] || continue - if [ "$locked" == "$upstream" ]; then - echo "| $label | \`$locked\` | \`$upstream\` | ✅ |" - else - echo "| $label | \`$locked\` | \`$upstream\` | ⚠️ drift |" - MISMATCH=1 - fi + read -r locked keyline verline <<< "$(locked_version_and_lines "$lockfile" "$pkg")" + if [ -z "${locked:-}" ]; then + echo "| $label | \`$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 | \`$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 | \`$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 | \`$pkg\` | $locked_cell | $upstream_cell | ✅ |" + else + echo "| $label | \`$pkg\` | $locked_cell | $upstream_cell | ⚠️ drift |" + MISMATCH=1 + fi + done <<< "$deps" done if [ "$MISMATCH" -eq 1 ]; then - echo "::warning::One or more CodeQL standard library versions are out of sync with CLI v${CODEQL_VERSION}. Run 'codeql pack upgrade /src' (and lib/ext as needed) for the affected language(s)." >&2 + echo "::warning::One or more CodeQL standard library/query pack versions are out of sync with CLI v${CODEQL_VERSION}. Run 'codeql pack upgrade /src' (and lib/ext as needed) for the affected language(s)." >&2 fi diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1b22cc89..06c95723 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -356,7 +356,7 @@ jobs: echo "summary.md is $(wc -l < summary.md) lines, $(wc -c < summary.md) bytes" cat summary.md >> "$GITHUB_STEP_SUMMARY" - - name: Build CodeQL library versions table + - name: Build CodeQL library & query pack versions table env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index acc81d09..46c163b9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -162,12 +162,17 @@ releases automatically. A maintainer has to notice one exists and kick off this > 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 versions" table (in the run's job summary and upserted into the matching GitHub Release, -> see [Cutting a release](#cutting-a-release) below) compares each language's locked -> `codeql/-all` version against the version [github/codeql](https://github.com/github/codeql) -> itself ships for the pinned `.codeqlversion` (read from its `/ql/lib/qlpack.yml` at tag +> 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. This doesn't block the workflow — it's a tripwire to catch drift, not a gate. +> 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 @@ -188,9 +193,9 @@ supported way to bump `.release.yml`: 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 versions table (whether - each language's locked `codeql/-all` matches what the pinned CodeQL CLI actually - bundles upstream — see the note under + 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] @@ -221,10 +226,11 @@ consider before using it: 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 versions table (see +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 versions they're compiled against are still in sync with the -pinned CodeQL CLI — without cross-referencing [GHCR][ghcr-packages] or `github/codeql` separately. +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] > A GitHub Release can still exist as a pre-release ahead of every pack in it actually catching up From 1341faea600241c9602e4a4363d17a7f5a6fce97 Mon Sep 17 00:00:00 2001 From: Chad Bentz <1760475+felickz@users.noreply.github.com> Date: Fri, 10 Jul 2026 02:28:10 -0400 Subject: [PATCH 09/13] feat: automate CodeQL CLI version bump detection and dependency refresh Adds the two workflows needed to close the gap identified while reviewing PR #118 (codeql pack upgrade weekly cron): - detect-codeql-release.yml: weekly (+ workflow_dispatch) tripwire that compares .codeqlversion against github/codeql-cli-binaries' latest release and keeps a single tracking issue open/updated while behind, auto-closing it once caught up. Never opens a PR itself. - update-codeql-version.yml: workflow_dispatch with a codeql_version input. Bumps .codeqlversion, runs codeql pack upgrade for every pack, and opens a PR via the same GitHub App token pattern as update-release.yml so CI actually runs on it (a GITHUB_TOKEN-authored PR would not trigger downstream workflows). Together these cover detection + mechanical dependency refresh; fixing compile/test breakage from upstream API changes and bumping each pack's own version: still needs a human (or delegated Copilot coding agent) - documented as a 3-step process in CONTRIBUTING.md. Rewrites CONTRIBUTING.md's "Updating the pinned CodeQL CLI/library version" section to describe this new process and adds reference-style links for both new workflows and codeql-cli-binaries. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/detect-codeql-release.yml | 87 ++++++++++++++++ .github/workflows/update-codeql-version.yml | 107 ++++++++++++++++++++ CONTRIBUTING.md | 43 +++++--- 3 files changed, 225 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/detect-codeql-release.yml create mode 100644 .github/workflows/update-codeql-version.yml 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/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 46c163b9..fe0d34a6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -134,18 +134,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 @@ -250,6 +266,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 From d518638e92c6e449198a8c84d520b636b7be3d00 Mon Sep 17 00:00:00 2001 From: Chad Bentz <1760475+felickz@users.noreply.github.com> Date: Fri, 10 Jul 2026 08:56:40 -0400 Subject: [PATCH 10/13] fix(publish): gate release-table upsert to push trigger, clarify failed-check cell Two fixes from Copilot review on PR #160: - publish.yml: only run the "Upsert release table" step on the push (release-cut) trigger, not workflow_dispatch. A manual one-off hotfix publish (see CONTRIBUTING.md) still points .release.yml at the last cut release, so upserting that run's tables into it would overwrite an already-published release's notes with a misleading summary of an unrelated hotfix run. - build-publish-summary.sh: when a pack's version-check step itself fails, previous_version can be empty/null, which rendered as the confusing "publish failed (still ``)". Now shows an explicit "previous version unknown" message in that case. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/scripts/build-publish-summary.sh | 8 +++++++- .github/workflows/publish.yml | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/scripts/build-publish-summary.sh b/.github/scripts/build-publish-summary.sh index 34f6b250..69098e86 100755 --- a/.github/scripts/build-publish-summary.sh +++ b/.github/scripts/build-publish-summary.sh @@ -75,7 +75,13 @@ cell() { case "$status" in published) echo "[${version}](${url}) 🆕" ;; up-to-date) echo "[${version}](${url})" ;; - failed) echo "⚠️ publish failed (still \`${previous}\`)" ;; + 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 } diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 06c95723..adfdece1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -364,6 +364,12 @@ jobs: 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: | From 0843ff8d4d17b8148a1cd54fbd9ad99fab9f17b2 Mon Sep 17 00:00:00 2001 From: Chad Bentz <1760475+felickz@users.noreply.github.com> Date: Fri, 10 Jul 2026 09:08:20 -0400 Subject: [PATCH 11/13] fix(publish): check both src and lib lock files for CodeQL version drift src/qlpack.yml and lib/qlpack.yml each declare their own codeql/-all dependency and resolve it into their own independent codeql-pack.lock.yml. Nothing keeps these two lock files in sync automatically - codeql pack upgrade resolves each pack directory separately, so running it against one and not the other silently lets them drift. The standard-library-versions table previously only checked src as a 'coarse signal', which meant a stale lib lock could go undetected while the table showed all green. Now checks both src and lib for every language (ext/ext-library-sources are still out of scope: they pin via extensionTargets, not dependencies, so CodeQL never resolves a locked version for them). Adds a Pack column to the table to distinguish rows. Also updates CONTRIBUTING.md's Supported CodeQL versions section, which similarly implied a single lock file was the source of truth per language. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../build-codeql-lib-versions-table.sh | 130 ++++++++++-------- CONTRIBUTING.md | 14 +- 2 files changed, 84 insertions(+), 60 deletions(-) diff --git a/.github/scripts/build-codeql-lib-versions-table.sh b/.github/scripts/build-codeql-lib-versions-table.sh index a18663ba..8a7e6436 100755 --- a/.github/scripts/build-codeql-lib-versions-table.sh +++ b/.github/scripts/build-codeql-lib-versions-table.sh @@ -1,18 +1,28 @@ #!/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 (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. +# `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. # -# Dependencies are discovered dynamically from each `/src/qlpack.yml` -# (not hardcoded) so this stays correct if a language adds/drops a direct -# `codeql/*` dependency. Only `src/codeql-pack.lock.yml` is read for locked -# versions - CONTRIBUTING.md's "Supported CodeQL versions" table treats it as -# the source of truth. This checks one representative directory (src) per -# language as a coarse signal, not every pack directory; a mismatch there is -# reason enough to re-run `codeql pack upgrade` across all of that language's -# directories (src/lib/ext as applicable). +# 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 @@ -22,8 +32,8 @@ # 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 /src` (and lib/ext) for a -# language, that language's lock file silently stays on a stale version +# 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. @@ -42,6 +52,7 @@ if [ -z "$CODEQL_VERSION" ]; then fi LANGUAGES=(cpp csharp go java javascript python ruby) +PACKTYPES=(src lib) lang_label() { case "$1" in @@ -120,66 +131,69 @@ 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 \`/src/qlpack.yml\`, resolved in \`src/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})). 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 /src\` (and \`lib\`/\`ext\` as needed) hasn't been run 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 "_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 | Dependency | Our locked version | Upstream (CLI v${CODEQL_VERSION} ships) | Status |" -echo "| --- | --- | --- | --- | --- |" +echo "| Language | Pack | Dependency | Our locked version | Upstream (CLI v${CODEQL_VERSION} ships) | Status |" +echo "| --- | --- | --- | --- | --- | --- |" for lang in "${LANGUAGES[@]}"; do - qlpackfile="${lang}/src/qlpack.yml" - lockfile="${lang}/src/codeql-pack.lock.yml" label=$(lang_label "$lang") - if [ ! -f "$qlpackfile" ] || [ ! -f "$lockfile" ]; then - echo "| $label | - | ❓ missing qlpack.yml or lock file | - | ❓ |" - MISMATCH=1 - continue - fi - - deps=$(qlpack_codeql_deps "$qlpackfile") - if [ -z "$deps" ]; then - echo "| $label | - | ❓ 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 | \`$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)" + for packtype in "${PACKTYPES[@]}"; do + qlpackfile="${lang}/${packtype}/qlpack.yml" + lockfile="${lang}/${packtype}/codeql-pack.lock.yml" - upath=$(upstream_path_for_pkg "$lang" "$pkg") - if [ -z "$upath" ]; then - echo "| $label | \`$pkg\` | $locked_cell | ❓ no known upstream mapping | ❓ |" + if [ ! -f "$qlpackfile" ] || [ ! -f "$lockfile" ]; then + echo "| $label | \`$packtype\` | - | ❓ missing qlpack.yml or lock file | - | ❓ |" MISMATCH=1 continue fi - read -r upstream uverline <<< "$(upstream_version_and_line "$upath" || true)" - if [ -z "${upstream:-}" ]; then - echo "| $label | \`$pkg\` | $locked_cell | ❓ fetch failed | ❓ |" + deps=$(qlpack_codeql_deps "$qlpackfile") + if [ -z "$deps" ]; then + echo "| $label | \`$packtype\` | - | ❓ no direct \`codeql/*\` dependency found | - | ❓ |" 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 | \`$pkg\` | $locked_cell | $upstream_cell | ✅ |" - else - echo "| $label | \`$pkg\` | $locked_cell | $upstream_cell | ⚠️ drift |" - MISMATCH=1 - fi - done <<< "$deps" + 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 /src' (and lib/ext as needed) for the affected language(s)." >&2 + 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/CONTRIBUTING.md b/CONTRIBUTING.md index fe0d34a6..f28987c3 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)) From faa5fe62810ee714985fedd47fbfd8c3836bc9d7 Mon Sep 17 00:00:00 2001 From: Chad Bentz <1760475+felickz@users.noreply.github.com> Date: Fri, 10 Jul 2026 11:14:51 -0400 Subject: [PATCH 12/13] docs: fix publish.yml job count in CONTRIBUTING.md (five jobs, not four) CONTRIBUTING.md said publish.yml is organized as four jobs, but it also has a fifth 'summary' job (needs: [queries, library, extensions, library_sources_extensions], if: always()) that aggregates results into the publish-summary and CodeQL lib-versions tables and upserts them into the Release notes. Clarify the doc to describe all five. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- CONTRIBUTING.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f28987c3..2640df08 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -103,10 +103,14 @@ process for each; most of it is manual today. ### Shipping a change to a query/library pack -[`publish.yml`][publish-workflow] is organized as four jobs: one per pack type (`queries` for -`src`, `library` for `lib`, `extensions` for `ext`, `library_sources_extensions` for +[`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]). +(`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 From bda49651cce9b5e2c4d5b768f82580358e1feb5c Mon Sep 17 00:00:00 2001 From: Chad Bentz <1760475+felickz@users.noreply.github.com> Date: Fri, 10 Jul 2026 11:45:42 -0400 Subject: [PATCH 13/13] fix(publish): don't fail check_version when a pack's GHCR container doesn't exist yet All four publish jobs (queries/library/extensions/library_sources_extensions) read the currently-published version via 'gh api .../versions'. run: steps default to bash -e, so a 404 (package never published, e.g. a brand-new language/pack) aborted the step and failed the job before it could ever publish that pack for the first time. Move '|| true' inside the command substitution so a fetch failure just leaves PUBLISHED_VERSION empty (correctly differing from CURRENT_VERSION, triggering a first publish) instead of killing the step under set -e. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/publish.yml | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index adfdece1..3f1feae4 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -34,7 +34,13 @@ 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]') + # `|| 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" @@ -113,7 +119,10 @@ 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]') + # 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" @@ -192,7 +201,10 @@ 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]') + # 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" @@ -270,7 +282,10 @@ 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]') + # 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"