Add bare JSON intent fallback and strengthen slot guidance #8
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |