Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
1efca21
Initial commit with task details
konard Oct 6, 2026
886c0f9
Collect CI logs and lint baseline for issue #150
konard Oct 6, 2026
10a02cd
Run the C++ tests through CTest instead of only compiling them
konard Oct 6, 2026
f0e6a4c
Track raw CI logs and stop committing generated Conan presets
konard Oct 6, 2026
49eecb8
Add NuGet release scripts that detect unpublished versions and reject…
konard Oct 6, 2026
8459d8e
Test, pack and deploy C++ from local workflows
konard Oct 6, 2026
d80de73
Skip published C# versions, support trusted publishing, drop tj-actions
konard Oct 6, 2026
88efc9f
Fix Dependabot auto-merge and harden the remaining workflows
konard Oct 6, 2026
c77f922
Lint workflows with actionlint and zizmor and enforce the issue #150 …
konard Oct 6, 2026
ae578a5
Supersede stale runs and scan for committed secrets
konard Oct 6, 2026
1677e69
Record post-fix CI logs and the upstream issue reports
konard Oct 6, 2026
b8ef533
Add the issue #150 case study
konard Oct 6, 2026
7f0f06d
Pin zizmor-action to a commit hash
konard Oct 6, 2026
9f5f22d
Remove the PR creation placeholder
konard Oct 6, 2026
fe654f0
Fix the Codacy markdownlint findings in the case study
konard Oct 6, 2026
af5310c
Verify NUGET_TOKEN with nuget.org before pushing a release
konard Oct 6, 2026
8ff0b25
Test the C++ headers with gcc, clang, AppleClang, MSVC and sanitizers
konard Oct 6, 2026
df67b4e
Restore and compile against the C++ NuGet package before publishing
konard Oct 6, 2026
cb02abf
Drop the pipx setuptools warning from the macOS Conan install
konard Oct 6, 2026
cf27b7b
Format the C++ headers with clang-format 23.1.3
konard Oct 6, 2026
1ca660a
Check C++ formatting with a pinned clang-format, adopted from the C++…
konard Oct 6, 2026
07a59a5
Report an unverifiable NuGet key scope as unknown, with the reason
konard Oct 6, 2026
32f9de6
Require the default-branch Git config in every workflow that checks out
konard Oct 6, 2026
a2de26b
Add the C++ template comparison, its CI evidence and the upstream iss…
konard Oct 6, 2026
1c4ba2d
Record the C++ template comparison, its problems and the filed upstre…
konard Oct 6, 2026
b9d5aea
Ignore pipx 1.16's Requires-Python DeprecationWarning when installing…
konard Oct 6, 2026
83d7023
Give CodeQL's buildless C# restore an explicit NuGet feed to check
konard Oct 6, 2026
35f8162
Record the CI logs of 1c4ba2d and the two log-only warnings they showed
konard Oct 6, 2026
0c7e804
Revert "Give CodeQL's buildless C# restore an explicit NuGet feed to …
konard Oct 6, 2026
4f6ff2c
Turn off CodeQL's C# feed check that warns about an empty feed set
konard Oct 6, 2026
55351d0
Record the CI logs of 4f6ff2c
konard Oct 6, 2026
d269d46
Drop the advisory warnings of conan profile detect and run the extra …
konard Oct 6, 2026
cc86877
Record the CI logs of d269d46, the first run without log warnings
konard Oct 6, 2026
6a9e9e7
Report to Hive Mind that fix --ci-cd does not know the C++ template
konard Oct 6, 2026
8b87da9
Pass the languages to the Hive Mind experiment as JSON instead of a f…
konard Oct 6, 2026
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
5 changes: 5 additions & 0 deletions .codacy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Codacy configuration — https://docs.codacy.com/repositories-configure/codacy-configuration-file/
exclude_paths:
# Archived CI evidence for issue #150 (raw logs, captured reports), not code.
# secretlint still scans it.
- "dev/log/**"
9 changes: 9 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,13 @@ updates:
directory: "/"
schedule:
interval: daily
cooldown:
default-days: 7
open-pull-requests-limit: 10
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
cooldown:
default-days: 7
open-pull-requests-limit: 10
37 changes: 37 additions & 0 deletions .github/scripts/check-cpp-format.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# Check that every tracked C++ source matches .clang-format, as the
# link-foundation/cpp-ai-driven-development-pipeline-template lint step does
# (`clang-format --dry-run --Werror`). .clang-format links to the Settings
# submodule, so that submodule has to be checked out first.
#
# Environment:
# CLANG_FORMAT clang-format executable (default: clang-format); CI pins the
# PyPI wheel so local runs can use exactly the same version
# VERBOSE=1 or RUNNER_DEBUG=1 traces every command (off by default)
# Fix the reported files with: git ls-files -z -- 'cpp/*.h' 'cpp/*.cpp' | xargs -0 clang-format -i
set -euo pipefail

if [ "${VERBOSE:-}" = 1 ] || [ "${RUNNER_DEBUG:-}" = 1 ]; then
set -x
fi

cd "$(dirname "$0")/../.."
clang_format="${CLANG_FORMAT:-clang-format}"

if [ ! -f .clang-format ]; then
echo "::error title=clang-format style missing::.clang-format does not resolve; run: git submodule update --init Settings" >&2
exit 1
fi
"$clang_format" --version

# -z keeps file names such as "ICounter[TResult, TArgument].h" intact.
count="$(git ls-files -z -- 'cpp/*.h' 'cpp/*.cpp' | tr -cd '\0' | wc -c)"
if [ "$count" -eq 0 ]; then
echo "::error title=clang-format checked nothing::no C++ files are tracked under cpp/" >&2
exit 1
fi
if ! git ls-files -z -- 'cpp/*.h' 'cpp/*.cpp' | xargs -0 "$clang_format" --dry-run --Werror; then
echo "::error title=C++ formatting::run clang-format -i on the files above (see $0)" >&2
exit 1
fi
echo "clang-format: $count C++ files are formatted"
69 changes: 69 additions & 0 deletions .github/scripts/check-cpp-nuget-package.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env bash
# Consume the packed C++ NuGet package the way a project does, so a package
# that packs but cannot be used fails before it is published. Adapted from
# link-foundation/cpp-ai-driven-development-pipeline-template
# (scripts/check-nuget-package.sh): push it to a local folder feed, let
# `dotnet restore` extract it through <PackageDownload> (which skips the TFM
# check that a native package fails in an SDK-style project), check the MSBuild
# targets and headers, then compile a program against the restored headers.
#
# Usage: check-cpp-nuget-package.sh <package.nupkg>
# Environment:
# CXX C++20 compiler for the consumer program (default: c++)
# VERBOSE=1 or RUNNER_DEBUG=1 traces every command (off by default)
set -euo pipefail

if [ "${VERBOSE:-}" = 1 ] || [ "${RUNNER_DEBUG:-}" = 1 ]; then
set -x
fi

package="${1:?Usage: check-cpp-nuget-package.sh <package.nupkg>}"
package="$(cd "$(dirname "$package")" && pwd)/$(basename "$package")"
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT

# <id>.<version>.nupkg; the version is the last three dot-separated fields.
name="$(basename "$package" .nupkg)"
version="$(printf '%s\n' "$name" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+([-+].*)?$')"
id="${name%."$version"}"

mkdir -p "$work/feed" "$work/project"
dotnet nuget push "$package" --source "$work/feed"
cat > "$work/project/check.csproj" <<XML
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup><TargetFramework>netstandard2.0</TargetFramework><DisableImplicitNuGetFallbackFolder>true</DisableImplicitNuGetFallbackFolder><DisableImplicitFrameworkReferences>true</DisableImplicitFrameworkReferences></PropertyGroup>
<ItemGroup><PackageDownload Include="$id" Version="[$version]" /></ItemGroup>
</Project>
XML
cat > "$work/project/nuget.config" <<XML
<configuration><packageSources><clear /><add key="local" value="$work/feed" /></packageSources></configuration>
XML
dotnet restore "$work/project/check.csproj" --packages "$work/packages"

extracted="$work/packages/$(printf '%s' "$id" | tr '[:upper:]' '[:lower:]')/$version"
include="$extracted/lib/native/include"
if [ ! -f "$extracted/build/native/$id.targets" ]; then
echo "::error title=C++ NuGet package unusable::build/native/$id.targets is missing"
exit 1
fi
if ! grep -q 'lib\\native\\include' "$extracted/build/native/$id.targets"; then
echo "::error title=C++ NuGet package unusable::build/native/$id.targets does not add lib/native/include to the include path"
exit 1
fi

# The nuspec tells users to include Platform.Interfaces.h only, so it has to
# pull in every header it needs from the package.
cat > "$work/consumer.cpp" <<'CPP'
#include <Platform.Interfaces.h>
#include <vector>

static_assert(Platform::Interfaces::CList<std::vector<int>>);

int main() { return 0; }
CPP
if ! "${CXX:-c++}" -std=c++20 -I "$include" -o "$work/consumer" "$work/consumer.cpp"; then
echo "::error title=C++ NuGet package unusable::A program that includes Platform.Interfaces.h from the restored package does not compile"
exit 1
fi
"$work/consumer"
echo "NuGet restore and use of $id $version: OK"
61 changes: 61 additions & 0 deletions .github/scripts/check-cpp-nuget-package.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/usr/bin/env node

import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { copyFileSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { after, before, test } from "node:test";

// Packs the real package once, then checks it and broken copies of it with
// the real dotnet client and compiler, so this needs the .NET SDK, zip and c++.
const script = new URL("./check-cpp-nuget-package.sh", import.meta.url).pathname;
const packScript = new URL("./pack-cpp-nuget.mjs", import.meta.url).pathname;
const missing = ["dotnet", "zip", "c++"].filter(
(tool) => spawnSync("bash", ["-c", `command -v ${tool}`]).status !== 0,
);
const options = { skip: missing.length > 0 && `${missing.join(", ")} not installed`, timeout: 300_000 };

let directory;
let packed;
before(() => {
if (missing.length > 0) {
return;
}
directory = mkdtempSync(path.join(tmpdir(), "check-cpp-nuget-"));
const pack = spawnSync("node", [packScript, path.join(directory, "packed")], { encoding: "utf8" });
assert.equal(pack.status, 0, pack.stdout + pack.stderr);
const name = readdirSync(path.join(directory, "packed")).find((file) => file.endsWith(".nupkg"));
packed = path.join(directory, "packed", name);
});
after(() => directory && rmSync(directory, { recursive: true, force: true }));

// Copies the package into its own folder, deletes `entries` from it and checks it.
const checkWithout = (label, entries) => {
const copy = path.join(directory, label, path.basename(packed));
spawnSync("mkdir", ["-p", path.dirname(copy)]);
copyFileSync(packed, copy);
if (entries.length > 0) {
const zip = spawnSync("zip", ["-q", "-d", copy, ...entries], { encoding: "utf8" });
assert.equal(zip.status, 0, zip.stderr);
}
return spawnSync("bash", [script, copy], { encoding: "utf8" });
};

test("restores and compiles against the packed package", options, () => {
const result = checkWithout("complete", []);
assert.equal(result.status, 0, result.stdout + result.stderr);
assert.match(result.stdout, /NuGet restore and use of Platform\.Interfaces\.TemplateLibrary .*: OK/);
});

test("fails when the MSBuild targets are missing", options, () => {
const result = checkWithout("no-targets", ["build/native/*"]);
assert.notEqual(result.status, 0);
assert.match(result.stdout, /::error title=C\+\+ NuGet package unusable::build\/native\/.*\.targets is missing/);
});

test("fails when a header that Platform.Interfaces.h includes is missing", options, () => {
const result = checkWithout("no-header", ["lib/native/include/CArray.h"]);
assert.notEqual(result.status, 0);
assert.match(result.stdout, /does not compile/);
});
13 changes: 12 additions & 1 deletion .github/scripts/codeql-workflow-policy.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const workflowPath = new URL("../workflows/codeql.yml", import.meta.url);
const workflow = readFileSync(workflowPath, "utf8");

test("uses current CodeQL language identifiers and build-free extraction", () => {
assert.match(workflow, /language: \[ 'c-cpp', 'csharp' \]/);
assert.match(workflow, /language: \[ 'actions', 'c-cpp', 'csharp' \]/);
assert.match(workflow, /build-mode: none/);
assert.doesNotMatch(workflow, /language: \[ 'cpp'/);
assert.doesNotMatch(workflow, /build-mode: manual/);
Expand Down Expand Up @@ -38,3 +38,14 @@ test("prevents checkout's default-branch warning", () => {
assert.match(workflow, /GIT_CONFIG_KEY_0: init\.defaultBranch/);
assert.match(workflow, /GIT_CONFIG_VALUE_0: main/);
});

test("reviews dependency changes on pull requests", () => {
assert.match(workflow, /actions\/dependency-review-action@v5/);
assert.match(workflow, /fail-on-severity: high/);
});

test("turns off the C# feed check that warns about an empty feed set", () => {
// github/codeql#22766: the check logs "No NuGet feeds are reachable" for an
// empty set even when nuget.org is reachable.
assert.match(workflow, /CODEQL_EXTRACTOR_CSHARP_BUILDLESS_NUGET_FEEDS_CHECK: 'false'/);
});
58 changes: 58 additions & 0 deletions .github/scripts/cpp-build-and-test.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/usr/bin/env node

// Runs cpp/build-and-test.sh against a stub conan that prints what Conan
// 2.33's `conan profile detect` prints on stderr, and stops at `conan install`.

import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";

const script = fileURLToPath(new URL("../../cpp/build-and-test.sh", import.meta.url));

// Copied from the Test cpp jobs of run 37531301224.
const advice = [
"WARN: This profile is a guess of your environment, please check it.",
"WARN: Defaulted to cppstd='gnu17' for apple-clang.",
"WARN: The output of this command is not guaranteed to be stable and can change in future Conan versions.",
"WARN: Use your own profile files for stability.",
];
const unrelated = "WARN: Unable to detect the compiler version";

const runWithStubConan = () => {
const bin = mkdtempSync(join(tmpdir(), "conan-stub-"));
try {
writeFileSync(
join(bin, "conan"),
[
"#!/usr/bin/env bash",
'case "$1 $2" in',
' "profile path") exit 1 ;;',
` "profile detect") echo "[settings]"; printf '%s\\n' ${[...advice, unrelated].map((line) => JSON.stringify(line)).join(" ")} >&2 ;;`,
" *) exit 3 ;;",
"esac",
"",
].join("\n"),
);
chmodSync(join(bin, "conan"), 0o755);
return spawnSync("bash", [script], {
encoding: "utf8",
env: { ...process.env, PATH: `${bin}:${process.env.PATH}`, VERBOSE: "", RUNNER_DEBUG: "" },
});
} finally {
rmSync(bin, { recursive: true, force: true });
}
};

test("drops only the advisory warnings of conan profile detect", () => {
const result = runWithStubConan();
assert.equal(result.status, 3, "the script must stop at the stubbed conan install");
assert.match(result.stdout, /\[settings\]/);
for (const line of advice) {
assert.ok(!result.stderr.includes(line), `still printed: ${line}`);
}
assert.ok(result.stderr.includes(unrelated), "an unrelated warning was dropped");
});
98 changes: 98 additions & 0 deletions .github/scripts/cpp-workflow-policy.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#!/usr/bin/env node

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { getJobs } from "./workflow-jobs.mjs";

const read = (name) => readFileSync(new URL(`../workflows/${name}`, import.meta.url), "utf8");
const testWorkflow = read("cpp-test.yml");
const deployWorkflow = read("deploy-cpp.yml");
const testJobs = getJobs(testWorkflow);
const deployJobs = getJobs(deployWorkflow);

test("keeps the workflow names that README badges reference", () => {
assert.match(testWorkflow, /^name: Test cpp$/m);
assert.match(deployWorkflow, /^name: Deploy new cpp version$/m);
});

test("tests pull requests whenever C++ sources change", () => {
const pullRequest = testWorkflow.slice(
testWorkflow.indexOf(" pull_request:"),
testWorkflow.indexOf(" workflow_call:"),
);
assert.doesNotMatch(pullRequest, /types:/);
assert.match(pullRequest, /- 'cpp\/\*\*'/);
});

test("runs the tests instead of only compiling them", () => {
const job = testJobs.get("test");
assert.match(job, /\n {10}cpp\/build-and-test\.sh\n/);
const script = readFileSync(new URL("../../cpp/build-and-test.sh", import.meta.url), "utf8");
assert.match(script, /ctest .*--no-tests=error/);
});

test("tests every compiler that consumes the headers, with sanitizers", () => {
const job = testJobs.get("test");
for (const os of ["ubuntu-24.04", "macos-15", "windows-2025"]) {
assert.match(job, new RegExp(`os: ${os}\\n`));
}
assert.match(job, /cxx: g\+\+/);
assert.match(job, /cxx: clang\+\+/);
assert.match(job, /sanitizers: address;undefined/);
assert.match(job, /fail-fast: false/);
// Matrix jobs share a job-level group unless it names the matrix entry.
assert.match(job, /group: .*\$\{\{ matrix\.name \}\}/);
const cmake = readFileSync(new URL("../../cpp/CMakeLists.txt", import.meta.url), "utf8");
assert.match(cmake, /-fsanitize=\$\{sanitizers\}/);
});

test("lets the build script decide whether tests are built", () => {
const cmake = readFileSync(new URL("../../cpp/CMakeLists.txt", import.meta.url), "utf8");
assert.doesNotMatch(cmake, /set\(LINKS_PLATFORM_TESTS (TRUE|ON)\)/);
assert.doesNotMatch(cmake, /CMAKE_CXX_FLAGS.*-march/);
const script = readFileSync(new URL("../../cpp/build-and-test.sh", import.meta.url), "utf8");
assert.match(script, /-DLINKS_PLATFORM_TESTS=ON/);
assert.match(script, /cmake --build .*--config "\$build_type"/);
assert.match(script, /ctest .*--build-config "\$build_type"/);
});

test("checks the C++ formatting with a pinned clang-format", () => {
const job = testJobs.get("format");
assert.match(job, /git submodule update --init --depth 1 Settings/);
assert.match(job, /pipx install clang-format==\d+\.\d+\.\d+\n/);
assert.match(job, /run: \.github\/scripts\/check-cpp-format\.sh\n/);
const script = readFileSync(new URL("./check-cpp-format.sh", import.meta.url), "utf8");
assert.match(script, /--dry-run --Werror/);
});

test("packs with dotnet and verifies the package on every run", () => {
const job = testJobs.get("pack");
assert.match(job, /pack-cpp-nuget\.mjs "\$RUNNER_TEMP\/cpp-nuget"/);
assert.match(job, /check-cpp-nuget-package\.sh "\$PACKAGE_PATH"/);
assert.match(job, /check-cpp-nuget-package\.test\.mjs/);
assert.match(job, /name: cpp-nuget-package/);
assert.match(job, /if-no-files-found: error/);
});

test("deploys only what the local test workflow built and tested", () => {
assert.match(deployJobs.get("test"), /uses: \.\/\.github\/workflows\/cpp-test\.yml/);
assert.match(deployJobs.get("releasePreflight"), /needs: test/);
assert.match(deployJobs.get("releasePreflight"), /preflight-nuget-release\.mjs/);
assert.match(deployJobs.get("pushToNuget"), /needs: \[test, releasePreflight\]/);
assert.match(deployJobs.get("pushToNuget"), /push-nuget-package\.sh "\$RUNNER_TEMP\/cpp-nuget"\/\*\.nupkg/);
});

test("supports trusted publishing with the API key as a fallback", () => {
const job = deployJobs.get("pushToNuget");
assert.match(job, /\n id-token: write/);
assert.match(job, /uses: NuGet\/login@[0-9a-f]{40} # v\d/);
assert.match(job, /NUGET_API_KEY: \$\{\{ steps\.nuget-login\.outputs\.NUGET_API_KEY \|\| secrets\.NUGET_TOKEN \}\}/);
});

test("creates the GitHub release only after the NuGet push succeeded", () => {
const job = deployJobs.get("publishRelease");
assert.match(job, /needs: \[test, pushToNuget\]/);
assert.doesNotMatch(job, /\n if:/);
assert.match(job, /gh release create "\$tag" "\$archive"/);
});
Loading
Loading