Convert and Update Tool #171
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: Convert and Update Tool | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| tool_id: | |
| description: "Tool ID (npm package name)" | |
| required: true | |
| type: string | |
| version: | |
| description: "Version to convert" | |
| required: true | |
| type: string | |
| authors: | |
| description: "Authors of the tool" | |
| required: true | |
| type: string | |
| repository: | |
| description: "Repository URL" | |
| required: false | |
| type: string | |
| website: | |
| description: "Website URL" | |
| required: false | |
| type: string | |
| csp_exceptions: | |
| description: "CSP Exceptions" | |
| required: false | |
| type: string | |
| features: | |
| description: "Features" | |
| required: false | |
| type: string | |
| branch: | |
| description: "Branch to commit to (optional, defaults to current branch)" | |
| required: false | |
| type: string | |
| jobs: | |
| convert: | |
| runs-on: ubuntu-latest | |
| permissions: write-all | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| ref: ${{ inputs.branch || github.ref }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20" | |
| - name: Get tool metadata from npm package | |
| id: metadata | |
| run: | | |
| TOOL_ID=$(echo "${{ inputs.tool_id }}" | sed 's/@//g' | sed 's/\//-/g') | |
| TOOL_PACKAGE_NAME="${{ inputs.tool_id }}" | |
| # Fetch metadata from npm registry | |
| NPM_INFO=$(npm view "${{ inputs.tool_id }}@${{ inputs.version }}" --json || echo '{}') | |
| TOOL_NAME=$(echo "$NPM_INFO" | jq -r '.displayName // .name // "${{ inputs.tool_id }}"') | |
| TOOL_DESC=$(echo "$NPM_INFO" | jq -r '.description // ""') | |
| # Extract icon and readmeUrl from configurations section | |
| ICON=$(echo "$NPM_INFO" | jq -r '.icon // empty') | |
| README_URL=$(echo "$NPM_INFO" | jq -r '.configurations.readmeUrl // empty') | |
| LICENSE=$(echo "$NPM_INFO" | jq -r '(.license // .licenses // "") | (if type=="string" then . else (.[0].type // "") end) // ""') | |
| REPOSITORY=$(echo "$NPM_INFO" | jq -r '.configurations.repository // empty') | |
| WEBSITE=$(echo "$NPM_INFO" | jq -r '.configurations.website // empty') | |
| CSP_EXCEPTIONS=$(echo "$NPM_INFO" | jq -c '.csp_exceptions // .cspexception // .configurations.csp_exceptions // null') | |
| FEATURES=$(echo "$NPM_INFO" | jq -c '.features // .configurations.features // null') | |
| echo "id=$TOOL_ID" >> $GITHUB_OUTPUT | |
| echo "package_name=$TOOL_PACKAGE_NAME" >> $GITHUB_OUTPUT | |
| echo "name=$TOOL_NAME" >> $GITHUB_OUTPUT | |
| echo "description=$TOOL_DESC" >> $GITHUB_OUTPUT | |
| echo "readme_url=$README_URL" >> $GITHUB_OUTPUT | |
| echo "icon=$ICON" >> $GITHUB_OUTPUT | |
| echo "license=$LICENSE" >> $GITHUB_OUTPUT | |
| echo "repository=$REPOSITORY" >> $GITHUB_OUTPUT | |
| echo "website=$WEBSITE" >> $GITHUB_OUTPUT | |
| echo "csp_exceptions=$CSP_EXCEPTIONS" >> $GITHUB_OUTPUT | |
| echo "features=$FEATURES" >> $GITHUB_OUTPUT | |
| - name: Download and build tool | |
| id: build | |
| run: | | |
| # Download specific version from npm | |
| npm pack ${{ inputs.tool_id }}@${{ inputs.version }} | |
| # Extract | |
| tar -xzf *.tgz | |
| cd package | |
| # Verify version | |
| VERSION=$(node -p "require('./package.json').version") | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| # Install production dependencies (skip lifecycle scripts to avoid husky issues) | |
| npm install --production --no-optional --ignore-scripts | |
| # Run build if script exists | |
| npm run --if-present build || echo "No build script found or build failed" | |
| cd .. | |
| - name: Set tool environment | |
| run: | | |
| echo "TOOL_ID=${{ steps.metadata.outputs.id }}" >> $GITHUB_ENV | |
| echo "TOOL_VERSION=${{ inputs.version }}" >> $GITHUB_ENV | |
| - name: Create distribution archive | |
| run: | | |
| cd package | |
| # Remove unnecessary files | |
| rm -rf .git .github node_modules/*/test node_modules/*/tests | |
| rm -rf node_modules/*/*.md node_modules/*/.npmignore | |
| find . -name "*.map" -delete | |
| find . -name "*.ts" ! -name "*.d.ts" -delete | |
| # Create archive | |
| tar -czf ../${{ steps.metadata.outputs.id }}-${{ inputs.version }}.tar.gz \ | |
| --exclude=node_modules/.bin \ | |
| . | |
| cd .. | |
| - name: Generate checksum | |
| id: checksum | |
| run: | | |
| CHECKSUM=$(sha256sum ${{ steps.metadata.outputs.id }}-${{ inputs.version }}.tar.gz | awk '{print $1}') | |
| SIZE=$(stat -f%z ${{ steps.metadata.outputs.id }}-${{ inputs.version }}.tar.gz 2>/dev/null || stat -c%s ${{ steps.metadata.outputs.id }}-${{ inputs.version }}.tar.gz) | |
| echo "checksum=$CHECKSUM" >> $GITHUB_OUTPUT | |
| echo "size=$SIZE" >> $GITHUB_OUTPUT | |
| - name: Extract or download icon | |
| id: icon | |
| if: ${{ steps.metadata.outputs.icon != '' }} | |
| run: | | |
| ICON="${{ steps.metadata.outputs.icon }}" | |
| if [ -n "$ICON" ]; then | |
| echo "Using bundled icon: $ICON" | |
| # Check if icon exists in package/dist | |
| if [ -f "package/dist/$ICON" ]; then | |
| # Extract extension | |
| EXTENSION="${ICON##*.}" | |
| EXTENSION=$(echo "$EXTENSION" | tr '[:upper:]' '[:lower:]') | |
| # Copy icon to release directory | |
| ICON_FILENAME="${{ steps.metadata.outputs.id }}-${{ inputs.version }}-icon.${EXTENSION}" | |
| cp "package/dist/$ICON" "$ICON_FILENAME" | |
| echo "✅ Bundled icon extracted successfully" | |
| echo "filename=$ICON_FILENAME" >> $GITHUB_OUTPUT | |
| echo "has_icon=true" >> $GITHUB_OUTPUT | |
| echo "icon_path=$ICON" >> $GITHUB_OUTPUT | |
| else | |
| echo "⚠️ Bundled icon not found at package/dist/$ICON" | |
| echo "has_icon=false" >> $GITHUB_OUTPUT | |
| fi | |
| fi | |
| - name: Login to Azure | |
| uses: azure/login@v2 | |
| with: | |
| creds: ${{ secrets.AZURE_CREDENTIALS }} | |
| - name: Upload tool package and icon to Azure Blob | |
| run: | | |
| set -euo pipefail | |
| FOLDER="${TOOL_ID}-${TOOL_VERSION}" | |
| az storage blob upload \ | |
| --account-name ${{ secrets.AZURE_STORAGE_ACCOUNT }} \ | |
| --container-name ${{ secrets.AZURE_STORAGE_CONTAINER }} \ | |
| --name "packages/${FOLDER}/${FOLDER}.tar.gz" \ | |
| --file "${{ steps.metadata.outputs.id }}-${{ inputs.version }}.tar.gz" \ | |
| --auth-mode login \ | |
| --overwrite true | |
| if [ "${{ steps.icon.outputs.has_icon }}" = "true" ]; then | |
| az storage blob upload \ | |
| --account-name ${{ secrets.AZURE_STORAGE_ACCOUNT }} \ | |
| --container-name ${{ secrets.AZURE_STORAGE_CONTAINER }} \ | |
| --name "packages/${FOLDER}/${{ steps.icon.outputs.filename }}" \ | |
| --file "${{ steps.icon.outputs.filename }}" \ | |
| --auth-mode login \ | |
| --overwrite true | |
| fi | |
| - name: Prepare release files | |
| id: release_files | |
| run: | | |
| # Build files list | |
| FILES="${{ steps.metadata.outputs.id }}-${{ inputs.version }}.tar.gz" | |
| if [ "${{ steps.icon.outputs.has_icon }}" = "true" ]; then | |
| FILES="${FILES}"$'\n'"${{ steps.icon.outputs.filename }}" | |
| fi | |
| # Output as multiline string | |
| echo "files<<EOF" >> $GITHUB_OUTPUT | |
| echo "$FILES" >> $GITHUB_OUTPUT | |
| echo "EOF" >> $GITHUB_OUTPUT | |
| - name: Build tool metadata JSON | |
| env: | |
| INPUT_FEATURES: ${{ inputs.features }} | |
| INPUT_CSP_EXCEPTIONS: ${{ inputs.csp_exceptions }} | |
| METADATA_FEATURES: ${{ steps.metadata.outputs.features }} | |
| METADATA_CSP: ${{ steps.metadata.outputs.csp_exceptions }} | |
| run: | | |
| set -euo pipefail | |
| # Normalize JSON-like input safely. Accepts full JSON and object fragments like "key":"value". | |
| normalize_json_input() { | |
| local raw="$1" | |
| local field_name="$2" | |
| local trimmed | |
| local candidate | |
| trimmed=$(printf '%s' "$raw" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') | |
| if [ -z "$trimmed" ] || [ "$trimmed" = "null" ]; then | |
| echo "null" | |
| return 0 | |
| fi | |
| if printf '%s' "$trimmed" | jq -e . >/dev/null 2>&1; then | |
| printf '%s' "$trimmed" | jq -c . | |
| return 0 | |
| fi | |
| # Some callers send object members without surrounding braces. | |
| if [[ "$trimmed" == *:* ]] && [[ "$trimmed" != \{* ]] && [[ "$trimmed" != \[* ]]; then | |
| candidate="{$trimmed}" | |
| if printf '%s' "$candidate" | jq -e . >/dev/null 2>&1; then | |
| printf '%s' "$candidate" | jq -c . | |
| return 0 | |
| fi | |
| fi | |
| echo "Invalid JSON for ${field_name} workflow input: $trimmed" >&2 | |
| return 1 | |
| } | |
| # Prefer caller-provided values; otherwise fall back to npm package metadata. | |
| # Use env vars for JSON values to avoid bash brace-expansion issues with literal JSON defaults. | |
| FEATURES_SOURCE="${INPUT_FEATURES:-}" | |
| if [ -z "$FEATURES_SOURCE" ] || [ "$FEATURES_SOURCE" = "null" ]; then | |
| FEATURES_SOURCE="$METADATA_FEATURES" | |
| fi | |
| CSP_SOURCE="${INPUT_CSP_EXCEPTIONS:-}" | |
| if [ -z "$CSP_SOURCE" ] || [ "$CSP_SOURCE" = "null" ]; then | |
| CSP_SOURCE="$METADATA_CSP" | |
| fi | |
| FEATURES_JSON=$(normalize_json_input "${FEATURES_SOURCE}" "features") | |
| CSP_JSON=$(normalize_json_input "${CSP_SOURCE}" "csp_exceptions") | |
| FOLDER="${TOOL_ID}-${TOOL_VERSION}" | |
| DOWNLOAD_URL="https://${{ secrets.AZURE_STORAGE_ACCOUNT }}.blob.core.windows.net/${{ secrets.AZURE_STORAGE_CONTAINER }}/packages/${FOLDER}/${FOLDER}.tar.gz" | |
| ICON_URL="" | |
| if [ "${{ steps.icon.outputs.has_icon }}" = "true" ]; then | |
| ICON_URL="https://${{ secrets.AZURE_STORAGE_ACCOUNT }}.blob.core.windows.net/${{ secrets.AZURE_STORAGE_CONTAINER }}/packages/${FOLDER}/${{ steps.icon.outputs.filename }}" | |
| fi | |
| TOOL_METADATA_JSON=$(jq -c -n \ | |
| --arg tool_id "${TOOL_ID}" \ | |
| --arg tool_version "${TOOL_VERSION}" \ | |
| --arg packageName "${{ steps.metadata.outputs.package_name }}" \ | |
| --arg name "${{ steps.metadata.outputs.name }}" \ | |
| --arg description "${{ steps.metadata.outputs.description }}" \ | |
| --arg authors "${{ inputs.authors }}" \ | |
| --arg readme_url "${{ steps.metadata.outputs.readme_url }}" \ | |
| --arg icon "$ICON_URL" \ | |
| --arg downloadurl "$DOWNLOAD_URL" \ | |
| --arg checksum "${{ steps.checksum.outputs.checksum }}" \ | |
| --arg size "${{ steps.checksum.outputs.size }}" \ | |
| --arg repository "${{ inputs.repository }}" \ | |
| --arg website "${{ inputs.website }}" \ | |
| --argjson csp_exceptions "$CSP_JSON" \ | |
| --argjson features "$FEATURES_JSON" \ | |
| --arg published "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ | |
| '{ | |
| tool_id: $tool_id, | |
| tool_version: $tool_version, | |
| packagename: $packageName, | |
| name: $name, | |
| description: $description, | |
| authors: $authors, | |
| readmeurl: $readme_url, | |
| icon: (if $icon == "" then null else $icon end), | |
| downloadurl: $downloadurl, | |
| checksum: $checksum, | |
| size: ($size | tonumber), | |
| repository: (if $repository == "" then null else $repository end), | |
| website: (if $website == "" then null else $website end), | |
| csp_exceptions: $csp_exceptions, | |
| features: $features, | |
| published_at: $published | |
| }') | |
| echo "TOOL_METADATA_JSON=$TOOL_METADATA_JSON" >> $GITHUB_ENV | |
| - name: Regenerate Azure Blob registry.json | |
| run: | | |
| set -euo pipefail | |
| az storage blob download \ | |
| --account-name ${{ secrets.AZURE_STORAGE_ACCOUNT }} \ | |
| --container-name ${{ secrets.AZURE_STORAGE_CONTAINER }} \ | |
| --name registry.json \ | |
| --file registry.json \ | |
| --auth-mode login \ | |
| || echo '{"version":"1.0","tools":[]}' > registry.json | |
| node buildScripts/updateRegistry.js "${TOOL_ID}" "${TOOL_VERSION}" "$TOOL_METADATA_JSON" | |
| az storage blob upload \ | |
| --account-name ${{ secrets.AZURE_STORAGE_ACCOUNT }} \ | |
| --container-name ${{ secrets.AZURE_STORAGE_CONTAINER }} \ | |
| --name registry.json \ | |
| --file registry.json \ | |
| --auth-mode login \ | |
| --overwrite true | |
| - name: Update Supabase database | |
| env: | |
| SUPABASE_URL: ${{ secrets.SUPABASE_URL }} | |
| SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }} | |
| INPUT_FEATURES: ${{ inputs.features }} | |
| INPUT_CSP_EXCEPTIONS: ${{ inputs.csp_exceptions }} | |
| METADATA_FEATURES: ${{ steps.metadata.outputs.features }} | |
| METADATA_CSP: ${{ steps.metadata.outputs.csp_exceptions }} | |
| if: env.SUPABASE_URL != '' && env.SUPABASE_SERVICE_ROLE_KEY != '' | |
| run: | | |
| set -euo pipefail | |
| # Ensure required tools exist (exit 127 indicates command-not-found) | |
| command -v curl >/dev/null 2>&1 || (sudo apt-get update && sudo apt-get install -y curl) | |
| command -v jq >/dev/null 2>&1 || (sudo apt-get update && sudo apt-get install -y jq) | |
| FOLDER="${TOOL_ID}-${TOOL_VERSION}" | |
| # Prepare icon and download URLs | |
| ICON_URL="" | |
| if [ "${{ steps.icon.outputs.has_icon }}" = "true" ]; then | |
| ICON_URL="https://${{ secrets.AZURE_STORAGE_ACCOUNT }}.blob.core.windows.net/${{ secrets.AZURE_STORAGE_CONTAINER }}/packages/${FOLDER}/${{ steps.icon.outputs.filename }}" | |
| fi | |
| # Azure Blob (preferred distribution) | |
| BLOB_DOWNLOAD_URL="https://${{ secrets.AZURE_STORAGE_ACCOUNT }}.blob.core.windows.net/${{ secrets.AZURE_STORAGE_CONTAINER }}/packages/${FOLDER}/${FOLDER}.tar.gz" | |
| # Normalize JSON-like input safely. Accepts full JSON and object fragments like "key":"value". | |
| normalize_json_input() { | |
| local raw="$1" | |
| local field_name="$2" | |
| local trimmed | |
| local candidate | |
| trimmed=$(printf '%s' "$raw" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') | |
| if [ -z "$trimmed" ] || [ "$trimmed" = "null" ]; then | |
| echo "null" | |
| return 0 | |
| fi | |
| if printf '%s' "$trimmed" | jq -e . >/dev/null 2>&1; then | |
| printf '%s' "$trimmed" | jq -c . | |
| return 0 | |
| fi | |
| # Some callers send object members without surrounding braces. | |
| if [[ "$trimmed" == *:* ]] && [[ "$trimmed" != \{* ]] && [[ "$trimmed" != \[* ]]; then | |
| candidate="{$trimmed}" | |
| if printf '%s' "$candidate" | jq -e . >/dev/null 2>&1; then | |
| printf '%s' "$candidate" | jq -c . | |
| return 0 | |
| fi | |
| fi | |
| echo "Invalid JSON for ${field_name} workflow input: $trimmed" >&2 | |
| return 1 | |
| } | |
| # Prefer caller-provided values; otherwise fall back to npm package metadata. | |
| # Use env vars for JSON values to avoid bash brace-expansion issues with literal JSON defaults. | |
| FEATURES_SOURCE="${INPUT_FEATURES:-}" | |
| if [ -z "$FEATURES_SOURCE" ] || [ "$FEATURES_SOURCE" = "null" ]; then | |
| FEATURES_SOURCE="$METADATA_FEATURES" | |
| fi | |
| CSP_SOURCE="${INPUT_CSP_EXCEPTIONS:-}" | |
| if [ -z "$CSP_SOURCE" ] || [ "$CSP_SOURCE" = "null" ]; then | |
| CSP_SOURCE="$METADATA_CSP" | |
| fi | |
| FEATURES_JSON=$(normalize_json_input "${FEATURES_SOURCE}" "features") | |
| CSP_JSON=$(normalize_json_input "${CSP_SOURCE}" "csp_exceptions") | |
| # Prepare SQL payload for tools table | |
| TOOL_PAYLOAD=$(jq -n \ | |
| --arg packagename "${{ steps.metadata.outputs.package_name }}" \ | |
| --arg version "${{ inputs.version }}" \ | |
| --arg name "${{ steps.metadata.outputs.name }}" \ | |
| --arg description "${{ steps.metadata.outputs.description }}" \ | |
| --arg readmeurl "${{ steps.metadata.outputs.readme_url }}" \ | |
| --arg license "${{ steps.metadata.outputs.license }}" \ | |
| --arg download "$BLOB_DOWNLOAD_URL" \ | |
| --arg icon "$ICON_URL" \ | |
| --arg published "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ | |
| --arg checksum "${{ steps.checksum.outputs.checksum }}" \ | |
| --arg size "${{ steps.checksum.outputs.size }}" \ | |
| --arg repository "${{ inputs.repository }}" \ | |
| --arg website "${{ inputs.website }}" \ | |
| --argjson csp_exceptions "$CSP_JSON" \ | |
| --argjson features "$FEATURES_JSON" \ | |
| '{ | |
| packagename: $packagename, | |
| version: $version, | |
| name: $name, | |
| description: $description, | |
| readmeurl: $readmeurl, | |
| license: $license, | |
| download: $download, | |
| icon: (if $icon == "" then null else $icon end), | |
| published_at: $published, | |
| checksum: $checksum, | |
| size: ($size|tonumber), | |
| repository: (if $repository == "" then null else $repository end), | |
| website: (if $website == "" then null else $website end), | |
| csp_exceptions: $csp_exceptions, | |
| features: $features | |
| }') | |
| # Update existing tool metadata in Supabase (falls back to upsert if missing) | |
| ENCODED_PACKAGE=$(echo "${{ steps.metadata.outputs.package_name }}" | jq -sRr @uri) | |
| PATCH_RESPONSE=$(curl -s -w "\n%{http_code}" -X PATCH "${{ secrets.SUPABASE_URL }}/rest/v1/tools?packagename=eq.$ENCODED_PACKAGE&select=id" \ | |
| -H "apikey: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}" \ | |
| -H "Authorization: Bearer ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}" \ | |
| -H "Content-Type: application/json" \ | |
| -H "Prefer: return=representation" \ | |
| -d "$TOOL_PAYLOAD") | |
| PATCH_BODY=$(echo "$PATCH_RESPONSE" | head -n1) | |
| PATCH_STATUS=$(echo "$PATCH_RESPONSE" | tail -n1) | |
| echo "Supabase tools patch status: $PATCH_STATUS" | |
| echo "Supabase tools patch body: $PATCH_BODY" | |
| TOOL_ID=$(echo "$PATCH_BODY" | jq -r '.[0].id // empty') | |
| # If PATCH returned nothing, try upsert | |
| if [ -z "$TOOL_ID" ]; then | |
| UPSERT_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "${{ secrets.SUPABASE_URL }}/rest/v1/tools?on_conflict=packagename&select=id" \ | |
| -H "apikey: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}" \ | |
| -H "Authorization: Bearer ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}" \ | |
| -H "Content-Type: application/json" \ | |
| -H "Prefer: resolution=merge-duplicates,return=representation" \ | |
| -d "$TOOL_PAYLOAD") | |
| UPSERT_BODY=$(echo "$UPSERT_RESPONSE" | head -n1) | |
| UPSERT_STATUS=$(echo "$UPSERT_RESPONSE" | tail -n1) | |
| echo "Supabase tools upsert status: $UPSERT_STATUS" | |
| echo "Supabase tools upsert body: $UPSERT_BODY" | |
| TOOL_ID=$(echo "$UPSERT_BODY" | jq -r '.[0].id // empty') | |
| fi | |
| if [ -n "$TOOL_ID" ]; then | |
| echo "Tool metadata updated with ID: $TOOL_ID" | |
| echo "Analytics data preserved from previous version" | |
| AUTHOR_LIST="${{ inputs.authors }}" | |
| NEW_CONTRIBUTOR_IDS="" | |
| if [ -n "$AUTHOR_LIST" ]; then | |
| IFS=',' read -ra AUTHORS <<< "$AUTHOR_LIST" | |
| for RAW_AUTHOR in "${AUTHORS[@]}"; do | |
| AUTHOR_NAME=$(echo "$RAW_AUTHOR" | xargs) | |
| if [ -z "$AUTHOR_NAME" ]; then | |
| continue | |
| fi | |
| ENCODED_NAME=$(jq -rn --arg v "$AUTHOR_NAME" '$v|@uri') | |
| CONTRIBUTOR_LOOKUP=$(curl -s "${{ secrets.SUPABASE_URL }}/rest/v1/contributors?name=eq.${ENCODED_NAME}&select=id&limit=1" \ | |
| -H "apikey: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}" \ | |
| -H "Authorization: Bearer ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}" \ | |
| -H "Content-Type: application/json") | |
| CONTRIBUTOR_ID=$(echo "$CONTRIBUTOR_LOOKUP" | jq -r '.[0].id // empty') | |
| if [ -z "$CONTRIBUTOR_ID" ]; then | |
| CONTRIBUTOR_PAYLOAD=$(jq -n --arg name "$AUTHOR_NAME" '{name: $name}') | |
| CONTRIBUTOR_RESPONSE=$(curl -s -X POST "${{ secrets.SUPABASE_URL }}/rest/v1/contributors?select=id" \ | |
| -H "apikey: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}" \ | |
| -H "Authorization: Bearer ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}" \ | |
| -H "Content-Type: application/json" \ | |
| -H "Prefer: return=representation" \ | |
| -d "$CONTRIBUTOR_PAYLOAD") | |
| CONTRIBUTOR_ID=$(echo "$CONTRIBUTOR_RESPONSE" | jq -r '.[0].id // empty') | |
| fi | |
| if [ -n "$CONTRIBUTOR_ID" ]; then | |
| NEW_CONTRIBUTOR_IDS="${NEW_CONTRIBUTOR_IDS:+$NEW_CONTRIBUTOR_IDS,}$CONTRIBUTOR_ID" | |
| TOOL_CONTRIBUTOR_PAYLOAD=$(jq -n \ | |
| --arg tool_id "$TOOL_ID" \ | |
| --arg contributor_id "$CONTRIBUTOR_ID" \ | |
| '{tool_id: $tool_id, contributor_id: ($contributor_id|tonumber)}') | |
| curl -s -X POST "${{ secrets.SUPABASE_URL }}/rest/v1/tool_contributors" \ | |
| -H "apikey: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}" \ | |
| -H "Authorization: Bearer ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}" \ | |
| -H "Content-Type: application/json" \ | |
| -H "Prefer: resolution=ignore-duplicates" \ | |
| -d "$TOOL_CONTRIBUTOR_PAYLOAD" >/dev/null || true | |
| echo "Contributor linked: $AUTHOR_NAME" | |
| else | |
| echo "Warning: Failed to upsert contributor for author '$AUTHOR_NAME'" | |
| fi | |
| done | |
| fi | |
| # Remove contributors that are no longer in the authors list | |
| EXISTING_CONTRIBUTORS=$(curl -s "${{ secrets.SUPABASE_URL }}/rest/v1/tool_contributors?tool_id=eq.$TOOL_ID&select=contributor_id" \ | |
| -H "apikey: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}" \ | |
| -H "Authorization: Bearer ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}") | |
| for EXISTING_ID in $(echo "$EXISTING_CONTRIBUTORS" | jq -r '.[].contributor_id'); do | |
| if ! echo ",$NEW_CONTRIBUTOR_IDS," | grep -q ",$EXISTING_ID,"; then | |
| curl -s -X DELETE "${{ secrets.SUPABASE_URL }}/rest/v1/tool_contributors?tool_id=eq.$TOOL_ID&contributor_id=eq.$EXISTING_ID" \ | |
| -H "apikey: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}" \ | |
| -H "Authorization: Bearer ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}" >/dev/null || true | |
| echo "Contributor removed: $EXISTING_ID" | |
| fi | |
| done | |
| else | |
| echo "Warning: Could not extract tool ID from Supabase response" | |
| fi |