Skip to content

Add force_update flag to product-updates workflow #6

Add force_update flag to product-updates workflow

Add force_update flag to product-updates workflow #6

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
VERSIONS_JSON="null"
VERSION_ENTRIES=""
# Helper: extract <Version> from a .csproj via git show
extract_csproj_version() {
git show "$1:$2" 2>/dev/null | sed -n 's/.*<Version>\([^<]*\)<\/Version>.*/\1/p' | head -1
}
# Helper: extract <PrivStackSdkVersion> from Directory.Build.props
extract_sdk_prop_version() {
git show "$1:$2" 2>/dev/null | sed -n 's/.*<PrivStackSdkVersion>\([^<]*\)<\/PrivStackSdkVersion>.*/\1/p' | head -1
}
# Helper: extract version from Cargo.toml [workspace.package] or [package] section
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
}
# Helper: compare versions and append to VERSION_ENTRIES
check_version() {
local label="$1" new_ver="$2" old_ver="$3"
if [ -n "$new_ver" ] && [ -n "$old_ver" ] && [ "$new_ver" != "$old_ver" ]; then
VERSION_ENTRIES="$VERSION_ENTRIES,\"$label\":{\"from\":\"$old_ver\",\"to\":\"$new_ver\"}"
fi
}
# SDK version property in Directory.Build.props
PROPS_FILE="desktop/Directory.Build.props"
if echo "$CHANGED_FILES" | grep -q "$PROPS_FILE"; then
check_version "SDK" \
"$(extract_sdk_prop_version "$SHA" "$PROPS_FILE")" \
"$(extract_sdk_prop_version "$SHA^" "$PROPS_FILE")"
fi
# All desktop .csproj files (Desktop, Services, UI.Adaptive, Server)
CSPROJ_MAP="desktop/PrivStack.Desktop/PrivStack.Desktop.csproj:Desktop
desktop/PrivStack.Sdk/PrivStack.Sdk.csproj:SDK Package
desktop/PrivStack.Services/PrivStack.Services.csproj:Services
desktop/PrivStack.UI.Adaptive/PrivStack.UI.Adaptive.csproj:UI Components
desktop/PrivStack.Server/PrivStack.Server.csproj:Server"
while IFS=: read -r csproj_path csproj_label; do
csproj_path=$(echo "$csproj_path" | xargs)
csproj_label=$(echo "$csproj_label" | xargs)
if echo "$CHANGED_FILES" | grep -q "$csproj_path"; then
check_version "$csproj_label" \
"$(extract_csproj_version "$SHA" "$csproj_path")" \
"$(extract_csproj_version "$SHA^" "$csproj_path")"
fi
done <<< "$CSPROJ_MAP"
# Rust core (workspace Cargo.toml)
CORE_CARGO="core/Cargo.toml"
if echo "$CHANGED_FILES" | grep -q "$CORE_CARGO"; then
check_version "Core" \
"$(extract_cargo_version "$SHA" "$CORE_CARGO")" \
"$(extract_cargo_version "$SHA^" "$CORE_CARGO")"
fi
# Relay
RELAY_CARGO="relay/Cargo.toml"
if echo "$CHANGED_FILES" | grep -q "$RELAY_CARGO"; then
check_version "Relay" \
"$(extract_cargo_version "$SHA" "$RELAY_CARGO")" \
"$(extract_cargo_version "$SHA^" "$RELAY_CARGO")"
fi
if [ -n "$VERSION_ENTRIES" ]; then
VERSIONS_JSON="{${VERSION_ENTRIES#,}}"
fi
# Determine update type from the highest version bump detected
UPDATE_TYPE="patch"
if [ "$VERSIONS_JSON" != "null" ]; then
# Collect all version pairs and find the highest bump level
for entry in $(echo "$VERSIONS_JSON" | jq -r 'to_entries[] | "\(.value.from)|\(.value.to)"'); 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