Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 199 additions & 0 deletions .github/scripts/build-codeql-lib-versions-table.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
#!/usr/bin/env bash
# Builds a markdown section documenting the pinned CodeQL CLI version and
# comparing every direct `codeql/*` dependency declared in each language's
# `src/qlpack.yml` AND `lib/qlpack.yml` (e.g. `codeql/<lang>-all`, and
# `codeql/<lang>-queries` for the languages that also depend on the standard
# queries pack) against the canonical version github/codeql itself bundles
# for that CLI release.
#
# Both `src` and `lib` are checked independently, not just `src`. Each pack
# directory has its own `codeql-pack.lock.yml`, resolved separately by
# `codeql pack upgrade <dir>` - the `codeql/<lang>-all: '*'` dependency each
# of them declares can therefore drift out of sync with each other (e.g. if
# someone runs `codeql pack upgrade <lang>/src` alone to fix a compile error
# after a CLI bump, and forgets `<lang>/lib`). Nothing in this repo enforces
# they stay identical, so both are treated as independent sources of truth
# here rather than assuming one implies the other.
# `ext`/`ext-library-sources` packs are deliberately NOT checked: they pin
# their target via `extensionTargets:`, not `dependencies:`, which CodeQL
# does not resolve into a version-locked `codeql-pack.lock.yml` entry (their
# lock files have an empty `dependencies: {}`), so there is no locked version
# to compare there.
#
# Dependencies are discovered dynamically from each qlpack.yml (not
# hardcoded) so this stays correct if a language adds/drops a direct
# `codeql/*` dependency.
#
# Both the "our locked" and "upstream" version cells link directly to the
# exact file+line that pins that version, at the exact commit (ours) or CLI
# tag (upstream) being compared - so a maintainer can verify the claim with
# one click instead of trusting the table.
#
# This exists because CI's "codeql pack install" step is non-resolving: it
# only installs whatever is already pinned in the checked-in lock file, it
# never re-resolves/upgrades versions. If a maintainer bumps .codeqlversion
# but forgets to run `codeql pack upgrade` for one of a language's pack
# directories, that directory's lock file silently stays on a stale version
# forever, and CI stays green. This table is a machine-checkable tripwire for
# that drift - see CONTRIBUTING.md's "Updating the pinned CodeQL CLI/library
# version" section.
#
# Usage: build-codeql-lib-versions-table.sh
# Requires: gh (authenticated), git, awk, base64
set -euo pipefail

REPO="${GITHUB_REPOSITORY:-GitHubSecurityLab/CodeQL-Community-Packs}"
REPO_SHA="${GITHUB_SHA:-$(git rev-parse HEAD)}"

CODEQL_VERSION=$(tr -d '[:space:]' < .codeqlversion)
if [ -z "$CODEQL_VERSION" ]; then
echo "::error::Could not read .codeqlversion" >&2
exit 1
fi

LANGUAGES=(cpp csharp go java javascript python ruby)
PACKTYPES=(src lib)

lang_label() {
case "$1" in
cpp) echo "C++" ;;
csharp) echo "C#" ;;
go) echo "Go" ;;
java) echo "Java" ;;
javascript) echo "JavaScript" ;;
python) echo "Python" ;;
ruby) echo "Ruby" ;;
*) echo "$1" ;;
esac
}

# Lists the direct `codeql/*` dependencies declared in a qlpack.yml's
# `dependencies:` block (skips our own `githubsecuritylab/*` packages).
qlpack_codeql_deps() {
local qlpackfile="$1"
awk '
{ sub(/\r$/, "") }
/^dependencies:/ { found=1; next }
found && /^[[:space:]]+codeql\// {
line=$0
sub(/^[[:space:]]+/, "", line)
sub(/:.*/, "", line)
print line
next
}
found && /^[^[:space:]]/ { exit }
' "$qlpackfile"
}

# Reads the version pinned to a package in a codeql-pack.lock.yml, plus the
# line numbers of the package key and its version line (for permalinks).
# Prints: "<version> <key_line> <version_line>"
locked_version_and_lines() {
local lockfile="$1" pkg="$2"
awk -v pkg=" ${pkg}:" '
{ sub(/\r$/, "") }
$0 == pkg { found=1; keyline=NR; next }
found && /^[[:space:]]+version:/ {
gsub(/\r/, "")
print $2, keyline, NR
exit
}
found && /^[^[:space:]]/ { exit }
' "$lockfile"
}

# Maps a direct dependency name to the path github/codeql uses for the
# corresponding pack's qlpack.yml. Returns empty if unmapped (unknown dep
# shape) so the caller can surface that instead of guessing.
upstream_path_for_pkg() {
local lang="$1" pkg="$2"
case "$pkg" in
"codeql/${lang}-all") echo "${lang}/ql/lib/qlpack.yml" ;;
"codeql/${lang}-queries") echo "${lang}/ql/src/qlpack.yml" ;;
*) echo "" ;;
esac
}

# Fetches a file from github/codeql at the tag for our pinned CLI version,
# and prints "<version> <version_line>" from its top-level `version:` field.
upstream_version_and_line() {
local path="$1"
local content
content=$(gh api "repos/github/codeql/contents/${path}?ref=refs%2Ftags%2Fcodeql-cli%2Fv${CODEQL_VERSION}" \
--jq '.content' 2>/dev/null | base64 -d 2>/dev/null) || return 1
[ -n "$content" ] || return 1
echo "$content" | awk '/^version:/ { print $2, NR; exit }'
}

MISMATCH=0

echo "## CodeQL standard library & query pack versions"
echo
echo "Pinned CodeQL CLI/library version ([\`.codeqlversion\`](https://github.com/${REPO}/blob/${REPO_SHA}/.codeqlversion)): [\`v${CODEQL_VERSION}\`](https://github.com/github/codeql-cli-binaries/releases/tag/v${CODEQL_VERSION})"
echo
echo "_Comparing each language's direct \`codeql/*\` dependencies (from both \`src/qlpack.yml\` and \`lib/qlpack.yml\`, each resolved in its own \`codeql-pack.lock.yml\`) against the versions [github/codeql](https://github.com/github/codeql) itself ships for CLI \`v${CODEQL_VERSION}\` (tag [\`codeql-cli/v${CODEQL_VERSION}\`](https://github.com/github/codeql/tree/codeql-cli/v${CODEQL_VERSION})). \`src\` and \`lib\` are checked independently because they're separately-resolved lock files that can drift from each other. Each cell links to the exact file/line backing it - our side at the commit this table was generated from, upstream at the CLI tag - so the claim can be verified with one click. A mismatch means \`codeql pack upgrade <lang>/<pack>\` hasn't been run for that specific directory since the last \`.codeqlversion\` bump - see [CONTRIBUTING.md: Updating the pinned CodeQL CLI/library version](https://github.com/${REPO}/blob/${REPO_SHA}/CONTRIBUTING.md#updating-the-pinned-codeql-clilibrary-version)._"
echo
echo "| Language | Pack | Dependency | Our locked version | Upstream (CLI v${CODEQL_VERSION} ships) | Status |"
echo "| --- | --- | --- | --- | --- | --- |"

for lang in "${LANGUAGES[@]}"; do
label=$(lang_label "$lang")

for packtype in "${PACKTYPES[@]}"; do
qlpackfile="${lang}/${packtype}/qlpack.yml"
lockfile="${lang}/${packtype}/codeql-pack.lock.yml"

if [ ! -f "$qlpackfile" ] || [ ! -f "$lockfile" ]; then
echo "| $label | \`$packtype\` | - | ❓ missing qlpack.yml or lock file | - | ❓ |"
MISMATCH=1
continue
fi

deps=$(qlpack_codeql_deps "$qlpackfile")
if [ -z "$deps" ]; then
echo "| $label | \`$packtype\` | - | ❓ no direct \`codeql/*\` dependency found | - | ❓ |"
MISMATCH=1
continue
fi

while IFS= read -r pkg; do
[ -n "$pkg" ] || continue

read -r locked keyline verline <<< "$(locked_version_and_lines "$lockfile" "$pkg")"
if [ -z "${locked:-}" ]; then
echo "| $label | \`$packtype\` | \`$pkg\` | ❓ not found in lock file | - | ❓ |"
MISMATCH=1
continue
fi
locked_link="https://github.com/${REPO}/blob/${REPO_SHA}/${lockfile}#L${keyline}-L${verline}"
locked_cell="[\`$locked\`]($locked_link)"

upath=$(upstream_path_for_pkg "$lang" "$pkg")
if [ -z "$upath" ]; then
echo "| $label | \`$packtype\` | \`$pkg\` | $locked_cell | ❓ no known upstream mapping | ❓ |"
MISMATCH=1
continue
fi

read -r upstream uverline <<< "$(upstream_version_and_line "$upath" || true)"
if [ -z "${upstream:-}" ]; then
echo "| $label | \`$packtype\` | \`$pkg\` | $locked_cell | ❓ fetch failed | ❓ |"
MISMATCH=1
continue
fi
upstream_link="https://github.com/github/codeql/blob/codeql-cli/v${CODEQL_VERSION}/${upath}#L${uverline}"
upstream_cell="[\`$upstream\`]($upstream_link)"

if [ "$locked" == "$upstream" ]; then
echo "| $label | \`$packtype\` | \`$pkg\` | $locked_cell | $upstream_cell | ✅ |"
else
echo "| $label | \`$packtype\` | \`$pkg\` | $locked_cell | $upstream_cell | ⚠️ drift |"
MISMATCH=1
fi
done <<< "$deps"
done
done

if [ "$MISMATCH" -eq 1 ]; then
echo "::warning::One or more CodeQL standard library/query pack versions are out of sync with CLI v${CODEQL_VERSION}. Run 'codeql pack upgrade <lang>/<pack>' for the affected language/pack directory(ies)." >&2
fi
105 changes: 105 additions & 0 deletions .github/scripts/build-publish-summary.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#!/usr/bin/env bash
# Builds a markdown table summarizing the package versions produced by a
# publish.yml run, from the per-job JSON result fragments it wrote.
#
# Usage: build-publish-summary.sh <results-dir>
#
# Each fragment in <results-dir> 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 <results-dir>}"
REPO="${GITHUB_REPOSITORY:-GitHubSecurityLab/CodeQL-Community-Packs}"
RUN_URL="${GITHUB_SERVER_URL:-https://github.com}/${REPO}/actions/runs/${GITHUB_RUN_ID:-}"

shopt -s nullglob
FRAGMENTS=("$RESULTS_DIR"/*.json)
if [ "${#FRAGMENTS[@]}" -eq 0 ]; then
MERGED='[]'
else
MERGED=$(jq -s '.' "${FRAGMENTS[@]}")
fi

LANGUAGES=(cpp csharp go java javascript python ruby)
TYPES=(src lib ext ext-library-sources)
EXT_LANGUAGES=(csharp java)

lang_label() {
case "$1" in
cpp) echo "C++" ;;
csharp) echo "C#" ;;
go) echo "Go" ;;
java) echo "Java" ;;
javascript) echo "JavaScript" ;;
python) echo "Python" ;;
ruby) echo "Ruby" ;;
*) echo "$1" ;;
esac
}

is_ext_language() {
local lang="$1"
for l in "${EXT_LANGUAGES[@]}"; do
[ "$l" == "$lang" ] && return 0
done
return 1
}

cell() {
local lang="$1" type="$2"
local entry
entry=$(echo "$MERGED" | jq -c --arg l "$lang" --arg t "$type" \
'[.[] | select(.language == $l and .type == $t)] | first // empty')

if [ -z "$entry" ] || [ "$entry" == "null" ]; then
# This combo is expected to publish (unlike the "n/a" combos filtered out
# by the caller) but no result fragment was found for it.
echo "❓ no result"
return
fi

local package version status previous
package=$(echo "$entry" | jq -r '.package')
version=$(echo "$entry" | jq -r '.target_version')
status=$(echo "$entry" | jq -r '.status')
previous=$(echo "$entry" | jq -r '.previous_version')
local url="https://github.com/${REPO}/pkgs/container/${package}?tag=${version}"

case "$status" in
published) echo "[${version}](${url}) 🆕" ;;
up-to-date) echo "[${version}](${url})" ;;
failed)
if [ -n "$previous" ] && [ "$previous" != "null" ]; then
echo "⚠️ publish failed (still \`${previous}\`)"
else
echo "⚠️ publish failed (previous version unknown - version-check step itself failed)"
fi
;;
*) echo "❓ unknown" ;;
esac
}

echo "## Publish summary"
echo
echo "_Generated by [publish.yml run #${GITHUB_RUN_NUMBER:-}](${RUN_URL}) — 🆕 marks a package republished by this run._"
echo
echo "| Language | Queries (\`src\`) | Library (\`lib\`) | Extensions (\`ext\`) | Library sources (\`ext-library-sources\`) |"
echo "| --- | --- | --- | --- | --- |"
for lang in "${LANGUAGES[@]}"; do
row="| $(lang_label "$lang") |"
for type in "${TYPES[@]}"; do
if { [ "$type" == "ext" ] || [ "$type" == "ext-library-sources" ]; } && ! is_ext_language "$lang"; then
row="$row n/a |"
continue
fi
row="$row $(cell "$lang" "$type") |"
done
echo "$row"
done
74 changes: 74 additions & 0 deletions .github/scripts/upsert-release-table.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/env bash
# Upserts a generated markdown block (e.g. the publish-summary table from
# build-publish-summary.sh, or the CodeQL library-versions table from
# build-codeql-lib-versions-table.sh) into the GitHub Release matching
# .release.yml's current version, creating that release as a pre-release if
# it doesn't exist yet.
#
# Usage: upsert-release-table.sh <table-markdown-file> [block-name]
#
# block-name defaults to "publish-summary" for backwards compatibility. Each
# distinct block-name gets its own marker-comment pair, so multiple blocks
# (e.g. "publish-summary" and "codeql-lib-versions") can be upserted
# independently into the same release body without clobbering each other.
# Re-runs only replace the matching block and leave the rest of the release
# body (e.g. "What's Changed") alone.
set -euo pipefail

TABLE_FILE="${1:?usage: upsert-release-table.sh <table-markdown-file> [block-name]}"
BLOCK_NAME="${2:-publish-summary}"
START_MARKER="<!-- ${BLOCK_NAME}:start -->"
END_MARKER="<!-- ${BLOCK_NAME}:end -->"

VERSION=$(grep -E '^version:' .release.yml | head -1 | sed -E 's/^version:[[:space:]]*"?([^"[:space:]]*)"?/\1/')
if [ -z "$VERSION" ]; then
echo "::error::Could not read version from .release.yml" >&2
exit 1
fi
TAG="v${VERSION}"

BLOCK_FILE=$(mktemp)
{
echo "$START_MARKER"
cat "$TABLE_FILE"
echo "$END_MARKER"
} > "$BLOCK_FILE"

if gh release view "$TAG" >/dev/null 2>&1; then
echo "Release $TAG exists; refreshing its $BLOCK_NAME block."
CURRENT_BODY=$(mktemp)
gh release view "$TAG" --json body -q .body > "$CURRENT_BODY"

NEW_BODY=$(mktemp)
if grep -qF "$START_MARKER" "$CURRENT_BODY" && grep -qF "$END_MARKER" "$CURRENT_BODY"; then
awk -v start="$START_MARKER" -v end="$END_MARKER" -v blockfile="$BLOCK_FILE" '
$0 == start {
while ((getline line < blockfile) > 0) print line
skip = 1
next
}
$0 == end {
skip = 0
next
}
skip { next }
{ print }
' "$CURRENT_BODY" > "$NEW_BODY"
else
# Older release predating this automation, or a corrupted/partial marker
# pair: append the block once rather than risk truncating the body.
cat "$CURRENT_BODY" > "$NEW_BODY"
echo "" >> "$NEW_BODY"
cat "$BLOCK_FILE" >> "$NEW_BODY"
fi

gh release edit "$TAG" --notes-file "$NEW_BODY"
else
echo "Release $TAG does not exist yet; creating it as a pre-release."
gh release create "$TAG" \
--title "$TAG" \
--notes-file "$BLOCK_FILE" \
--generate-notes \
--prerelease \
--target "${GITHUB_SHA:-main}"
fi
Comment thread
felickz marked this conversation as resolved.
Loading
Loading