Skip to content

Add bare JSON intent fallback and strengthen slot guidance #8

Add bare JSON intent fallback and strengthen slot guidance

Add bare JSON intent fallback and strengthen slot guidance #8

name: Product Updates
on:
push:
branches: [main]
workflow_dispatch:
inputs:
backfill_count:
description: 'Number of historical commits to backfill (0 = disabled)'
required: false
default: '0'
force_update:
description: 'Update existing entries instead of skipping duplicates (true/false)'
required: false
default: 'false'
env:
API_URL: https://privstack.io
jobs:
process-commits:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Process commits
env:
PRIVSTACK_ADMIN_TOKEN: ${{ secrets.PRIVSTACK_ADMIN_TOKEN }}
run: |
set -eu
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'
BACKFILL_COUNT="${{ github.event.inputs.backfill_count || '0' }}"
FORCE_UPDATE="${{ github.event.inputs.force_update || 'false' }}"
# Determine which commits to process
if [ "$BACKFILL_COUNT" -gt 0 ] 2>/dev/null; then
echo -e "${YELLOW}Backfill mode: processing last $BACKFILL_COUNT commits${NC}"
COMMITS=$(git log --reverse --format='%H' -n "$BACKFILL_COUNT")
elif [ "${{ github.event_name }}" = "push" ]; then
COMMITS=$(git log --reverse --format='%H' ${{ github.event.before }}..${{ github.sha }} 2>/dev/null || echo "${{ github.sha }}")
else
echo "No commits to process"
exit 0
fi
PROCESSED=0
SKIPPED=0
ERRORS=0
for SHA in $COMMITS; do
# Skip merge commits (more than 1 parent)
PARENT_COUNT=$(git cat-file -p "$SHA" | grep -c '^parent ' || true)
if [ "$PARENT_COUNT" -gt 1 ]; then
echo -e "${YELLOW}Skip merge commit: $SHA${NC}"
SKIPPED=$((SKIPPED + 1))
continue
fi
# Get commit metadata
SUBJECT=$(git log -1 --format='%s' "$SHA")
BODY=$(git log -1 --format='%b' "$SHA")
AUTHOR_DATE=$(git log -1 --format='%aI' "$SHA")
SHORT_SHA=$(echo "$SHA" | cut -c1-8)
# Get changed files
CHANGED_FILES=$(git diff-tree --no-commit-id --name-only -r "$SHA" 2>/dev/null || echo "")
if [ -z "$CHANGED_FILES" ]; then
echo -e "${YELLOW}Skip (no changed files): $SHORT_SHA $SUBJECT${NC}"
SKIPPED=$((SKIPPED + 1))
continue
fi
# Noise filter — skip if ALL changed files are tests, docs, or CI
NOISE_ONLY=true
while IFS= read -r file; do
case "$file" in
tests/*|*.Tests/*) continue ;;
README.md|CLAUDE.md|patterns.md) continue ;;
.github/*) continue ;;
*) NOISE_ONLY=false; break ;;
esac
done <<< "$CHANGED_FILES"
if [ "$NOISE_ONLY" = true ]; then
echo -e "${YELLOW}Skip (noise only): $SHORT_SHA $SUBJECT${NC}"
SKIPPED=$((SKIPPED + 1))
continue
fi
# Extract IO-specific tags from changed paths
TAGS=""
while IFS= read -r file; do
case "$file" in
core/*) TAGS="$TAGS,Core" ;;
desktop/PrivStack.Desktop/*) TAGS="$TAGS,Desktop Shell" ;;
desktop/PrivStack.Sdk/*) TAGS="$TAGS,SDK" ;;
desktop/PrivStack.UI.Adaptive/*) TAGS="$TAGS,UI Components" ;;
desktop/PrivStack.Services/*) TAGS="$TAGS,Services" ;;
desktop/PrivStack.Server/*) TAGS="$TAGS,Server" ;;
relay/*) TAGS="$TAGS,Relay" ;;
esac
done <<< "$CHANGED_FILES"
# Dedupe tags
if [ -n "$TAGS" ]; then
TAGS=$(echo "$TAGS" | tr ',' '\n' | sort -u | grep -v '^$' | tr '\n' ',' | sed 's/,$//')
fi
# Fallback
if [ -z "$TAGS" ]; then
TAGS="IO"
fi
# Build JSON tags array
TAGS_JSON=$(echo "$TAGS" | tr ',' '\n' | grep -v '^$' | sed 's/.*/"&"/' | paste -sd ',' | sed 's/^/[/;s/$/]/')
# Version extraction — always include current version for touched components
VERSIONS_JSON="null"
VERSION_ENTRIES=""
# Helpers: read version from a file at a given commit
extract_csproj_version() {
git show "$1:$2" 2>/dev/null | sed -n 's/.*<Version>\([^<]*\)<\/Version>.*/\1/p' | head -1
}
extract_sdk_prop_version() {
git show "$1:$2" 2>/dev/null | sed -n 's/.*<PrivStackSdkVersion>\([^<]*\)<\/PrivStackSdkVersion>.*/\1/p' | head -1
}
extract_cargo_version() {
git show "$1:$2" 2>/dev/null | sed -n '/^\[workspace\.package\]/,/^\[/s/^version = "\([^"]*\)".*/\1/p; /^\[package\]/,/^\[/s/^version = "\([^"]*\)".*/\1/p' | head -1
}
# Append version entry: from→to if changed, otherwise just current version
add_version_entry() {
local label="$1" new_ver="$2" old_ver="$3"
if [ -z "$new_ver" ]; then return; fi
if [ -n "$old_ver" ] && [ "$new_ver" != "$old_ver" ]; then
VERSION_ENTRIES="$VERSION_ENTRIES,\"$label\":{\"from\":\"$old_ver\",\"to\":\"$new_ver\"}"
else
VERSION_ENTRIES="$VERSION_ENTRIES,\"$label\":{\"version\":\"$new_ver\"}"
fi
}
# Map: tag name → version file path + extraction method
# For each tag present, read the current version at this commit
SEEN_VERSIONS=""
if echo "$TAGS" | grep -q "Core"; then
add_version_entry "Core" \
"$(extract_cargo_version "$SHA" "core/Cargo.toml")" \
"$(extract_cargo_version "$SHA^" "core/Cargo.toml")"
SEEN_VERSIONS="$SEEN_VERSIONS,Core"
fi
if echo "$TAGS" | grep -q "Relay"; then
add_version_entry "Relay" \
"$(extract_cargo_version "$SHA" "relay/Cargo.toml")" \
"$(extract_cargo_version "$SHA^" "relay/Cargo.toml")"
SEEN_VERSIONS="$SEEN_VERSIONS,Relay"
fi
if echo "$TAGS" | grep -q "Desktop Shell"; then
add_version_entry "Desktop" \
"$(extract_csproj_version "$SHA" "desktop/PrivStack.Desktop/PrivStack.Desktop.csproj")" \
"$(extract_csproj_version "$SHA^" "desktop/PrivStack.Desktop/PrivStack.Desktop.csproj")"
SEEN_VERSIONS="$SEEN_VERSIONS,Desktop"
fi
if echo "$TAGS" | grep -q "SDK"; then
add_version_entry "SDK" \
"$(extract_sdk_prop_version "$SHA" "desktop/Directory.Build.props")" \
"$(extract_sdk_prop_version "$SHA^" "desktop/Directory.Build.props")"
SEEN_VERSIONS="$SEEN_VERSIONS,SDK"
fi
if echo "$TAGS" | grep -q "UI Components"; then
add_version_entry "UI Components" \
"$(extract_csproj_version "$SHA" "desktop/PrivStack.UI.Adaptive/PrivStack.UI.Adaptive.csproj")" \
"$(extract_csproj_version "$SHA^" "desktop/PrivStack.UI.Adaptive/PrivStack.UI.Adaptive.csproj")"
SEEN_VERSIONS="$SEEN_VERSIONS,UI Components"
fi
if echo "$TAGS" | grep -q "Services"; then
add_version_entry "Services" \
"$(extract_csproj_version "$SHA" "desktop/PrivStack.Services/PrivStack.Services.csproj")" \
"$(extract_csproj_version "$SHA^" "desktop/PrivStack.Services/PrivStack.Services.csproj")"
SEEN_VERSIONS="$SEEN_VERSIONS,Services"
fi
if echo "$TAGS" | grep -q "Server"; then
add_version_entry "Server" \
"$(extract_csproj_version "$SHA" "desktop/PrivStack.Server/PrivStack.Server.csproj")" \
"$(extract_csproj_version "$SHA^" "desktop/PrivStack.Server/PrivStack.Server.csproj")"
SEEN_VERSIONS="$SEEN_VERSIONS,Server"
fi
if [ -n "$VERSION_ENTRIES" ]; then
VERSIONS_JSON="{${VERSION_ENTRIES#,}}"
fi
# Determine update type from version bumps (from→to entries only)
UPDATE_TYPE="patch"
if [ "$VERSIONS_JSON" != "null" ]; then
for entry in $(echo "$VERSIONS_JSON" | jq -r 'to_entries[] | select(.value.from) | "\(.value.from)|\(.value.to)"' 2>/dev/null); do
OLD_VER=$(echo "$entry" | cut -d'|' -f1)
NEW_VER=$(echo "$entry" | cut -d'|' -f2)
OLD_MAJOR=$(echo "$OLD_VER" | cut -d. -f1)
NEW_MAJOR=$(echo "$NEW_VER" | cut -d. -f1)
OLD_MINOR=$(echo "$OLD_VER" | cut -d. -f2)
NEW_MINOR=$(echo "$NEW_VER" | cut -d. -f2)
if [ "$NEW_MAJOR" != "$OLD_MAJOR" ]; then
UPDATE_TYPE="major"
break
elif [ "$NEW_MINOR" != "$OLD_MINOR" ] && [ "$UPDATE_TYPE" != "major" ]; then
UPDATE_TYPE="minor"
fi
done
fi
# Build slug
SLUG_TITLE=$(echo "$SUBJECT" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//')
SLUG="${SHORT_SHA}-${SLUG_TITLE}"
# Build description from commit body
DESCRIPTION=""
if [ -n "$BODY" ]; then
DESCRIPTION="$BODY"
fi
# Build upsert flag
UPSERT_VAL="false"
if [ "$FORCE_UPDATE" = "true" ]; then
UPSERT_VAL="true"
fi
# POST to API
HTTP_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "${API_URL}/api/admin/product-updates" \
-H "Authorization: Bearer ${PRIVSTACK_ADMIN_TOKEN}" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg title "$SUBJECT" \
--arg slug "$SLUG" \
--arg description "$DESCRIPTION" \
--argjson tags "$TAGS_JSON" \
--argjson versions "$VERSIONS_JSON" \
--arg update_type "$UPDATE_TYPE" \
--arg repo "IO" \
--arg commit_sha "$SHA" \
--arg commit_date "$AUTHOR_DATE" \
--argjson upsert "$UPSERT_VAL" \
'{title: $title, slug: $slug, description: $description, tags: $tags, versions: $versions, update_type: $update_type, repo: $repo, commit_sha: $commit_sha, commit_date: $commit_date, upsert: $upsert}')")
HTTP_CODE=$(echo "$HTTP_RESPONSE" | tail -1)
HTTP_BODY=$(echo "$HTTP_RESPONSE" | sed '$d')
if [ "$HTTP_CODE" = "201" ]; then
echo -e "${GREEN}Created: $SHORT_SHA $SUBJECT${NC}"
PROCESSED=$((PROCESSED + 1))
elif [ "$HTTP_CODE" = "200" ]; then
echo -e "${GREEN}Updated: $SHORT_SHA $SUBJECT${NC}"
PROCESSED=$((PROCESSED + 1))
elif [ "$HTTP_CODE" = "409" ]; then
echo -e "${YELLOW}Duplicate (skipped): $SHORT_SHA $SUBJECT${NC}"
SKIPPED=$((SKIPPED + 1))
else
echo -e "${RED}Error ($HTTP_CODE): $SHORT_SHA $SUBJECT — $HTTP_BODY${NC}"
ERRORS=$((ERRORS + 1))
fi
# Rate limit for backfill mode
if [ "$BACKFILL_COUNT" -gt 0 ] 2>/dev/null; then
sleep 0.5
fi
done
echo ""
echo "========================================="
echo -e "Processed: ${GREEN}$PROCESSED${NC} | Skipped: ${YELLOW}$SKIPPED${NC} | Errors: ${RED}$ERRORS${NC}"
echo "========================================="
if [ "$ERRORS" -gt 0 ]; then
exit 1
fi