Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
10 changes: 9 additions & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ root = true

# C# files
[*.cs]
charset = utf-8

#### Core EditorConfig Options ####

Expand Down Expand Up @@ -253,4 +254,11 @@ dotnet_diagnostic.IDE0052.severity = warning
dotnet_diagnostic.CA1811.severity = warning

# SA1210: Using directives should be ordered alphabetically by the namespaces
dotnet_diagnostic.SA1210.severity = error
dotnet_diagnostic.SA1210.severity = error
# C# project files
[*.csproj]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_size = 2
indent_style = space
517 changes: 495 additions & 22 deletions .github/ISSUE_TEMPLATE/model-verification.yml

Large diffs are not rendered by default.

44 changes: 43 additions & 1 deletion .github/actions/ai/fetch-models/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ inputs:
description: 'When true, the script rewrites the source file: auto-inserts new models with capabilities mapped from OpenRouter metadata, updates existing model capabilities/ContextLimit, and marks disappeared or expiring models as Deprecated = true'
required: false
default: 'false'
fail-on-validation-errors:
description: 'When true, fail if updated provider models are missing required defaults, contain pending capabilities, or contain realtime models'
required: false
default: 'false'

outputs:
models:
Expand Down Expand Up @@ -59,6 +63,8 @@ runs:
API_KEY: ${{ inputs.api-key }}
PROVIDER_API_KEY: ${{ inputs.provider-api-key }}
run: |
$ErrorActionPreference = 'Stop'

$params = @{
Provider = '${{ inputs.provider }}'
ApiKey = $env:API_KEY
Expand All @@ -69,10 +75,39 @@ runs:
if ('${{ inputs.update-file }}' -eq 'true') {
$params['UpdateFile'] = $true
}
if ('${{ inputs.fail-on-validation-errors }}' -eq 'true') {
$params['FailOnValidationErrors'] = $true
}

try {
$reportJson = & .\tools\Update-ProviderModels.ps1 @params
$report = $reportJson | ConvertFrom-Json
$scriptExit = $LASTEXITCODE

# The script emits its JSON report on stdout regardless of validation
# outcome. Try to parse it even on non-zero exit so we can surface the
# validation details (the script also writes ::error:: annotations,
# but exposing the structured report lets the caller decide what to do).
$report = $null
if ($reportJson) {
try { $report = $reportJson | ConvertFrom-Json } catch { $report = $null }
}

if ($scriptExit -ne 0) {
Write-Host "::error::Update-ProviderModels.ps1 exited with code $scriptExit"
"success=false" >> $env:GITHUB_OUTPUT
"error=Update-ProviderModels.ps1 exited with code $scriptExit" >> $env:GITHUB_OUTPUT
"changed=false" >> $env:GITHUB_OUTPUT
"count=0" >> $env:GITHUB_OUTPUT
"models=[]" >> $env:GITHUB_OUTPUT
if ($report) {
"report<<EOF_REPORT" >> $env:GITHUB_OUTPUT
"$reportJson" >> $env:GITHUB_OUTPUT
"EOF_REPORT" >> $env:GITHUB_OUTPUT
} else {
"report={}" >> $env:GITHUB_OUTPUT
}
exit $scriptExit
}

$modelsJson = ($report.apiModels | ConvertTo-Json -Compress)
if ($modelsJson -eq 'null') { $modelsJson = '[]' }
Expand All @@ -91,10 +126,17 @@ runs:
"EOF_REPORT" >> $env:GITHUB_OUTPUT
}
catch {
# Print the failure visibly. The previous implementation wrote the
# message to $env:GITHUB_OUTPUT only, which hid the actual cause from
# the run log and made these jobs near-impossible to diagnose.
Write-Host "::error::Update-ProviderModels.ps1 failed: $($_.Exception.Message)"
if ($_.ScriptStackTrace) { Write-Host $_.ScriptStackTrace }

"success=false" >> $env:GITHUB_OUTPUT
"error=$($_.Exception.Message)" >> $env:GITHUB_OUTPUT
"changed=false" >> $env:GITHUB_OUTPUT
"count=0" >> $env:GITHUB_OUTPUT
"models=[]" >> $env:GITHUB_OUTPUT
"report={}" >> $env:GITHUB_OUTPUT
exit 1
}
4 changes: 4 additions & 0 deletions .github/actions/cherry-pick-to-branch/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ outputs:
branch-name:
description: 'Name of the patch branch pushed.'
value: ${{ steps.cherry-pick.outputs.branch-name }}
pr-title:
description: 'Title of the PR created (empty if skipped).'
value: ${{ steps.open-pr.outputs.pr-title }}
has-conflicts:
description: 'true if any cherry-pick required conflict markers to be committed.'
value: ${{ steps.cherry-pick.outputs.has-conflicts }}
Expand Down Expand Up @@ -181,6 +184,7 @@ runs:
first_sha=$(echo "$shas" | awk '{print $1}')
first_subject=$(git log -1 --pretty=%s "$first_sha")
title="$prefix $first_subject → $target"
echo "pr-title=$title" >> "$GITHUB_OUTPUT"

commit_list=""
for sha in $shas; do
Expand Down
66 changes: 66 additions & 0 deletions .github/actions/dispatch-required-pr-checks/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: Dispatch required PR checks
description: Dispatches the protected-branch PR check workflows for the current PR branch.

inputs:
token:
description: GitHub token with actions:write permission.
required: true
ref:
description: Branch or ref to run the checks against.
required: true
pr-number:
description: Pull request number.
required: false
default: ''
base-ref:
description: Pull request base branch.
required: false
default: ''
pr-title:
description: Pull request title.
required: false
default: ''

runs:
using: composite
steps:
- name: Dispatch workflows
shell: bash
env:
GH_TOKEN: ${{ inputs.token }}
REF: ${{ inputs.ref }}
PR_NUMBER: ${{ inputs.pr-number }}
BASE_REF: ${{ inputs.base-ref }}
PR_TITLE: ${{ inputs.pr-title }}
run: |
set -euo pipefail

workflows=(
"check-provider-models.yml"
"pr-license-headers.yml"
"pr-validation.yml"
"pr-build-hash-validation.yml"
"pr-version-validation.yml"
"ci-dotnet-tests.yml"
)

for workflow in "${workflows[@]}"; do
if [[ ! -f ".github/workflows/$workflow" ]]; then
echo "Skipping $workflow because it is not present on $REF"
continue
fi

args=(workflow run "$workflow" --ref "$REF")
if [[ -n "$PR_NUMBER" ]]; then
args+=(-f "pr_number=$PR_NUMBER")
fi
if [[ -n "$BASE_REF" ]]; then
args+=(-f "base_ref=$BASE_REF")
fi
if [[ -n "$PR_TITLE" ]]; then
args+=(-f "pr_title=$PR_TITLE")
fi

echo "Dispatching $workflow for $REF"
gh "${args[@]}"
done
6 changes: 5 additions & 1 deletion .github/actions/versioning/manage-milestones/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,11 @@ runs:
console.log(`✅ Created milestone: ${title}`);
return { title, created: true };
} catch (error) {
if (error.message.includes('already exists')) {
// GitHub returns 422 with error code "already_exists" when a milestone
// with the same title already exists. Older Octokit versions surfaced
// this as "already exists" in the message — handle both forms.
const msg = error.message || '';
if (error.status === 422 && /already[_ ]exists/.test(msg)) {
console.log(`ℹ️ Milestone ${title} already exists`);
return { title, created: false };
}
Expand Down
197 changes: 197 additions & 0 deletions .github/workflows/check-provider-models.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
name: ✅ Check Provider Models

on:
pull_request:
workflow_dispatch:
inputs:
pr_number:
description: Pull request number to validate.
required: false
type: string
base_ref:
description: Pull request base branch.
required: false
type: string
pr_title:
description: Pull request title.
required: false
type: string

permissions:
contents: read

jobs:
paths-filter:
name: Paths Filter
runs-on: ubuntu-latest
outputs:
should_run: ${{ steps.filter.outputs.should_run }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- id: filter
shell: bash
env:
BASE_REF: ${{ github.event.inputs.base_ref || github.event.pull_request.base.ref || 'main' }}
run: |
set -euo pipefail

git fetch origin "$BASE_REF" --depth=1

should_run=false
mapfile -t changed_files < <(git diff --name-only "origin/$BASE_REF"...HEAD)

for file in "${changed_files[@]}"; do
case "$file" in
tools/Update-ProviderModels.ps1)
should_run=true
break
;;
src/SmartHopper.Infrastructure/AIModels/AICapability.cs|src/SmartHopper.Providers.*/*ProviderModels.cs)
if python3 - "$BASE_REF" "$file" <<'PY'
import subprocess
import sys
from pathlib import Path

base_ref, path = sys.argv[1], sys.argv[2]
bom = b"\xef\xbb\xbf"

try:
base = subprocess.check_output(["git", "show", f"origin/{base_ref}:{path}"])
except subprocess.CalledProcessError:
sys.exit(1)

head = Path(path).read_bytes()
if base.startswith(bom):
base = base[len(bom):]
if head.startswith(bom):
head = head[len(bom):]

sys.exit(0 if base == head else 1)
PY
then
echo "Ignoring BOM-only provider model change: $file"
else
should_run=true
break
fi
;;
esac
done

echo "should_run=$should_run" >> "$GITHUB_OUTPUT"

validate-provider-models:
name: Validate provider model defaults
needs: paths-filter
if: needs.paths-filter.outputs.should_run == 'true'
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
provider:
- OpenAI
- MistralAI
- Anthropic
- OpenRouter
- DeepSeek
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Validate ${{ matrix.provider }} model declarations
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$provider = '${{ matrix.provider }}'

$rawOutput = .\tools\Update-ProviderModels.ps1 `
-Provider $provider `
-ValidateOnly
$output = @($rawOutput -split "`r?`n")

$output | ForEach-Object { Write-Host $_ }

$jsonStartIndex = -1
for ($i = 0; $i -lt $output.Count; $i++) {
if ($output[$i].TrimStart().StartsWith('{')) {
$jsonStartIndex = $i
break
}
}

if ($jsonStartIndex -lt 0) {
Write-Output "::error title=$provider provider model validation::Validation script did not emit a JSON report."
exit 1
}

$reportJson = ($output[$jsonStartIndex..($output.Count - 1)] -join "`n")
$report = $reportJson | ConvertFrom-Json

Write-Output "## $provider provider model validation" >> $env:GITHUB_STEP_SUMMARY
Write-Output "" >> $env:GITHUB_STEP_SUMMARY

if ($report.validation.success) {
Write-Output "✅ Validation passed." >> $env:GITHUB_STEP_SUMMARY
exit 0
}

Write-Output "❌ Validation failed with $($report.validation.errors.Count) error(s)." >> $env:GITHUB_STEP_SUMMARY
Write-Output "" >> $env:GITHUB_STEP_SUMMARY

if ($report.validation.missingDefaultCapabilities.Count -gt 0) {
Write-Output "### Missing default capabilities" >> $env:GITHUB_STEP_SUMMARY
foreach ($capability in $report.validation.missingDefaultCapabilities) {
Write-Output "- ``AICapability.$capability``" >> $env:GITHUB_STEP_SUMMARY
}
Write-Output "" >> $env:GITHUB_STEP_SUMMARY
}

if ($report.validation.pendingCapabilityModels.Count -gt 0) {
Write-Output "### Pending capability definitions" >> $env:GITHUB_STEP_SUMMARY
foreach ($model in $report.validation.pendingCapabilityModels) {
Write-Output "- ``$model``" >> $env:GITHUB_STEP_SUMMARY
}
Write-Output "" >> $env:GITHUB_STEP_SUMMARY
}

if ($report.validation.realtimeModels.Count -gt 0) {
Write-Output "### Realtime models" >> $env:GITHUB_STEP_SUMMARY
foreach ($model in $report.validation.realtimeModels) {
Write-Output "- ``$model``" >> $env:GITHUB_STEP_SUMMARY
}
Write-Output "" >> $env:GITHUB_STEP_SUMMARY
}

Write-Output "### Validation errors" >> $env:GITHUB_STEP_SUMMARY
foreach ($errorMessage in $report.validation.errors) {
$escaped = $errorMessage.Replace('%', '%25').Replace("`r", '%0D').Replace("`n", '%0A').Replace(':', '%3A').Replace(',', '%2C')
Write-Output "::error title=$provider provider model validation::$escaped"
Write-Output "- $errorMessage" >> $env:GITHUB_STEP_SUMMARY
}

exit 1

required-check:
name: ✅ Check Provider Models
needs: [paths-filter, validate-provider-models]
if: always()
runs-on: ubuntu-latest
steps:
- name: Verify required workflow jobs
shell: bash
run: |
paths_filter="${{ needs.paths-filter.result }}"
validate_provider_models="${{ needs.validate-provider-models.result }}"

if [[ "$paths_filter" != "success" ]]; then
echo "::error::Paths filter finished with $paths_filter"
exit 1
fi

if [[ "$validate_provider_models" != "success" && "$validate_provider_models" != "skipped" ]]; then
echo "::error::Provider model validation finished with $validate_provider_models"
exit 1
fi
Loading
Loading