Product Updates #3
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' | |
| 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' }}" | |
| # 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" ;; | |
| 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: check Directory.Build.props and Desktop csproj | |
| VERSIONS_JSON="null" | |
| VERSION_ENTRIES="" | |
| # Check SDK version in Directory.Build.props | |
| PROPS_FILE="desktop/Directory.Build.props" | |
| if echo "$CHANGED_FILES" | grep -q "$PROPS_FILE"; then | |
| NEW_VER=$(git show "$SHA:$PROPS_FILE" 2>/dev/null | sed -n 's/.*<PrivStackSdkVersion>\([^<]*\)<\/PrivStackSdkVersion>.*/\1/p' | head -1) | |
| OLD_VER=$(git show "$SHA^:$PROPS_FILE" 2>/dev/null | sed -n 's/.*<PrivStackSdkVersion>\([^<]*\)<\/PrivStackSdkVersion>.*/\1/p' | head -1) | |
| if [ -n "$NEW_VER" ] && [ -n "$OLD_VER" ] && [ "$NEW_VER" != "$OLD_VER" ]; then | |
| VERSION_ENTRIES="$VERSION_ENTRIES,\"SDK\":{\"from\":\"$OLD_VER\",\"to\":\"$NEW_VER\"}" | |
| fi | |
| fi | |
| # Check Desktop version | |
| DESKTOP_CSPROJ="desktop/PrivStack.Desktop/PrivStack.Desktop.csproj" | |
| if echo "$CHANGED_FILES" | grep -q "$DESKTOP_CSPROJ"; then | |
| NEW_VER=$(git show "$SHA:$DESKTOP_CSPROJ" 2>/dev/null | sed -n 's/.*<Version>\([^<]*\)<\/Version>.*/\1/p' | head -1) | |
| OLD_VER=$(git show "$SHA^:$DESKTOP_CSPROJ" 2>/dev/null | sed -n 's/.*<Version>\([^<]*\)<\/Version>.*/\1/p' | head -1) | |
| if [ -n "$NEW_VER" ] && [ -n "$OLD_VER" ] && [ "$NEW_VER" != "$OLD_VER" ]; then | |
| VERSION_ENTRIES="$VERSION_ENTRIES,\"Desktop\":{\"from\":\"$OLD_VER\",\"to\":\"$NEW_VER\"}" | |
| fi | |
| fi | |
| if [ -n "$VERSION_ENTRIES" ]; then | |
| VERSIONS_JSON="{${VERSION_ENTRIES#,}}" | |
| fi | |
| # Determine update type from version changes | |
| UPDATE_TYPE="patch" | |
| if [ "$VERSIONS_JSON" != "null" ]; then | |
| # Check SDK version bump | |
| if echo "$CHANGED_FILES" | grep -q "$PROPS_FILE"; then | |
| NEW_VER=$(git show "$SHA:$PROPS_FILE" 2>/dev/null | sed -n 's/.*<PrivStackSdkVersion>\([^<]*\)<\/PrivStackSdkVersion>.*/\1/p' | head -1) | |
| OLD_VER=$(git show "$SHA^:$PROPS_FILE" 2>/dev/null | sed -n 's/.*<PrivStackSdkVersion>\([^<]*\)<\/PrivStackSdkVersion>.*/\1/p' | head -1) | |
| if [ -n "$NEW_VER" ] && [ -n "$OLD_VER" ] && [ "$NEW_VER" != "$OLD_VER" ]; then | |
| 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" | |
| elif [ "$NEW_MINOR" != "$OLD_MINOR" ]; then | |
| UPDATE_TYPE="minor" | |
| fi | |
| fi | |
| fi | |
| 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 | |
| # 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" \ | |
| '{title: $title, slug: $slug, description: $description, tags: $tags, versions: $versions, update_type: $update_type, repo: $repo, commit_sha: $commit_sha, commit_date: $commit_date}')") | |
| 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" = "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 |