From ff543ef9db69961c7b24c81ba34a008a0dd6e01e Mon Sep 17 00:00:00 2001 From: fahmidareem3 Date: Mon, 4 May 2026 21:15:22 +0600 Subject: [PATCH 1/5] chore: add crowdsplit staging sync workflow --- .github/workflows/crowdsplit-sync.yml | 108 ++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 .github/workflows/crowdsplit-sync.yml diff --git a/.github/workflows/crowdsplit-sync.yml b/.github/workflows/crowdsplit-sync.yml new file mode 100644 index 00000000..6fa64768 --- /dev/null +++ b/.github/workflows/crowdsplit-sync.yml @@ -0,0 +1,108 @@ +name: Crowdsplit Staging Sync + +on: + repository_dispatch: + types: [crowdsplit-staging-pushed] + +permissions: + issues: write + +concurrency: + group: crowdsplit-sync + cancel-in-progress: false + +jobs: + create-sync-issue: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Analyze changes and create issue + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const payload = context.payload.client_payload ?? {}; + const changedFiles = Array.isArray(payload.changed_files) + ? payload.changed_files.filter(f => typeof f === 'string' && !f.includes('`')) + : []; + const truncated = Boolean(payload.truncated); + const fileCount = Number(payload.file_count) || changedFiles.length; + const sha = typeof payload.sha === 'string' ? payload.sha : ''; + const compareUrl = typeof payload.compare_url === 'string' ? payload.compare_url : ''; + const escapeMd = (s) => String(s).replace(/[`*_\[\]\\]/g, '\\$&'); + const commitMessage = escapeMd(payload.commit_message ?? '(no message)').slice(0, 500); + + if (changedFiles.length === 0) { + console.log('No changed files in payload. Skipping issue creation.'); + return; + } + + // Deduplicate: if an open api-sync issue was recently updated, append a comment instead + const since = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); + const existingIssues = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + labels: 'api-sync', + state: 'open', + sort: 'created', + direction: 'desc', + since, + per_page: 5, + }); + + const fileList = changedFiles.map(f => `- \`${f}\``).join('\n'); + const shortSha = sha ? sha.substring(0, 7) : 'unknown'; + const commitLink = compareUrl ? `[\`${shortSha}\`](${compareUrl})` : `\`${shortSha}\``; + const truncationNote = truncated + ? `\n\n> **Note:** ${fileCount} files changed in total, showing first ${changedFiles.length}.` + : ''; + const runLink = `_[Workflow run](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})_`; + + if (existingIssues.data.length > 0) { + const latestIssue = existingIssues.data[0]; + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: latestIssue.number, + body: [ + `### Additional staging push detected`, + ``, + `**Commit:** ${commitLink}`, + `**Message:** ${commitMessage}`, + ``, + `### Changed files`, + fileList, + truncationNote, + ``, + runLink, + ].join('\n'), + }); + console.log(`Appended comment to existing issue #${latestIssue.number}`); + return; + } + + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: `[API Sync] Crowdsplit staging updated — ${fileCount} file(s) changed`, + body: [ + `## Crowdsplit Staging Change Detected`, + ``, + `A push to Crowdsplit \`staging\` included changes that may require SDK updates.`, + ``, + `**Commit:** ${commitLink}`, + `**Message:** ${commitMessage}`, + ``, + `### Changed files`, + fileList, + truncationNote, + ``, + `### Action required`, + `1. Review the changed files in Crowdsplit`, + `2. Determine if SDK types, services, or methods need updating`, + `3. Follow the patterns in \`CLAUDE.md\` (Result, factory functions, withAuth)`, + ``, + `---`, + `${runLink} · _Auto-generated by Crowdsplit Staging Sync workflow_`, + ].join('\n'), + labels: ['api-sync', 'crowdsplit'], + }); From 0a86d8099d470c8f6dde90231a50d06d77305a17 Mon Sep 17 00:00:00 2001 From: fahmidareem3 Date: Tue, 5 May 2026 16:47:38 +0600 Subject: [PATCH 2/5] fix: handle edge cases in crowdsplit sync workflow --- .github/workflows/crowdsplit-sync.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/crowdsplit-sync.yml b/.github/workflows/crowdsplit-sync.yml index 6fa64768..f350589d 100644 --- a/.github/workflows/crowdsplit-sync.yml +++ b/.github/workflows/crowdsplit-sync.yml @@ -31,7 +31,7 @@ jobs: const escapeMd = (s) => String(s).replace(/[`*_\[\]\\]/g, '\\$&'); const commitMessage = escapeMd(payload.commit_message ?? '(no message)').slice(0, 500); - if (changedFiles.length === 0) { + if (changedFiles.length === 0 && fileCount === 0) { console.log('No changed files in payload. Skipping issue creation.'); return; } @@ -49,7 +49,9 @@ jobs: per_page: 5, }); - const fileList = changedFiles.map(f => `- \`${f}\``).join('\n'); + const fileList = changedFiles.length > 0 + ? changedFiles.map(f => `- \`${f}\``).join('\n') + : `_File list not available — ${fileCount} file(s) changed (see commit for details)._`; const shortSha = sha ? sha.substring(0, 7) : 'unknown'; const commitLink = compareUrl ? `[\`${shortSha}\`](${compareUrl})` : `\`${shortSha}\``; const truncationNote = truncated @@ -57,8 +59,11 @@ jobs: : ''; const runLink = `_[Workflow run](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})_`; - if (existingIssues.data.length > 0) { - const latestIssue = existingIssues.data[0]; + // Filter out PRs — listForRepo returns both issues and pull requests + const issuesOnly = existingIssues.data.filter(i => !i.pull_request); + + if (issuesOnly.length > 0) { + const latestIssue = issuesOnly[0]; await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, From c154bd5c84c17256251eb854c035e9da632a63f5 Mon Sep 17 00:00:00 2001 From: fahmidareem3 Date: Tue, 5 May 2026 17:05:51 +0600 Subject: [PATCH 3/5] fix: harden crowdsplit sync workflow input validation and display --- .github/workflows/crowdsplit-sync.yml | 60 ++++++++++++++++++--------- 1 file changed, 41 insertions(+), 19 deletions(-) diff --git a/.github/workflows/crowdsplit-sync.yml b/.github/workflows/crowdsplit-sync.yml index f350589d..429a872d 100644 --- a/.github/workflows/crowdsplit-sync.yml +++ b/.github/workflows/crowdsplit-sync.yml @@ -20,24 +20,55 @@ jobs: uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: script: | + // --- Constants --- + const MAX_COMMIT_MSG_LENGTH = 500; + const MAX_DISPLAYED_FILES = 50; + const MAX_SHA_LENGTH = 7; + const DEDUP_WINDOW_MS = 24 * 60 * 60 * 1000; + const DEDUP_QUERY_LIMIT = 5; + const GITHUB_ORIGIN = 'https://github.com/'; + const SAFE_FILENAME = /^[\w\-./]+$/; + + // --- Parse and validate payload --- const payload = context.payload.client_payload ?? {}; + const changedFiles = Array.isArray(payload.changed_files) - ? payload.changed_files.filter(f => typeof f === 'string' && !f.includes('`')) + ? payload.changed_files.filter(f => typeof f === 'string' && SAFE_FILENAME.test(f)) : []; const truncated = Boolean(payload.truncated); - const fileCount = Number(payload.file_count) || changedFiles.length; + const fileCount = Number.isFinite(payload.file_count) ? payload.file_count : changedFiles.length; const sha = typeof payload.sha === 'string' ? payload.sha : ''; - const compareUrl = typeof payload.compare_url === 'string' ? payload.compare_url : ''; - const escapeMd = (s) => String(s).replace(/[`*_\[\]\\]/g, '\\$&'); - const commitMessage = escapeMd(payload.commit_message ?? '(no message)').slice(0, 500); + const compareUrl = typeof payload.compare_url === 'string' && payload.compare_url.startsWith(GITHUB_ORIGIN) + ? payload.compare_url + : ''; + + const escapeMd = (s) => String(s).replace(/[`*_\[\]\\#!()~<>|]/g, '\\$&'); + const commitMessage = escapeMd((payload.commit_message ?? '(no message)').slice(0, MAX_COMMIT_MSG_LENGTH)); if (changedFiles.length === 0 && fileCount === 0) { console.log('No changed files in payload. Skipping issue creation.'); return; } - // Deduplicate: if an open api-sync issue was recently updated, append a comment instead - const since = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); + // --- Build display elements --- + const displayedFiles = changedFiles.slice(0, MAX_DISPLAYED_FILES); + const fileList = displayedFiles.length > 0 + ? displayedFiles.map(f => `- \`${f}\``).join('\n') + : `_File list not available — ${fileCount} file(s) changed (see commit for details)._`; + + const shortSha = sha ? sha.substring(0, MAX_SHA_LENGTH) : 'unknown'; + const commitLink = compareUrl ? `[\`${shortSha}\`](${compareUrl})` : `\`${shortSha}\``; + + const truncationParts = []; + if (truncated || changedFiles.length > MAX_DISPLAYED_FILES) { + truncationParts.push(`> **Note:** ${fileCount} files changed in total, showing first ${displayedFiles.length}.`); + } + const truncationNote = truncationParts.length > 0 ? '\n\n' + truncationParts.join('\n') : ''; + + const runLink = `_[Workflow run](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})_`; + + // --- Deduplicate: append to recent open issue if one exists --- + const since = new Date(Date.now() - DEDUP_WINDOW_MS).toISOString(); const existingIssues = await github.rest.issues.listForRepo({ owner: context.repo.owner, repo: context.repo.repo, @@ -46,19 +77,9 @@ jobs: sort: 'created', direction: 'desc', since, - per_page: 5, + per_page: DEDUP_QUERY_LIMIT, }); - const fileList = changedFiles.length > 0 - ? changedFiles.map(f => `- \`${f}\``).join('\n') - : `_File list not available — ${fileCount} file(s) changed (see commit for details)._`; - const shortSha = sha ? sha.substring(0, 7) : 'unknown'; - const commitLink = compareUrl ? `[\`${shortSha}\`](${compareUrl})` : `\`${shortSha}\``; - const truncationNote = truncated - ? `\n\n> **Note:** ${fileCount} files changed in total, showing first ${changedFiles.length}.` - : ''; - const runLink = `_[Workflow run](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})_`; - // Filter out PRs — listForRepo returns both issues and pull requests const issuesOnly = existingIssues.data.filter(i => !i.pull_request); @@ -85,6 +106,7 @@ jobs: return; } + // --- Create new issue --- await github.rest.issues.create({ owner: context.repo.owner, repo: context.repo.repo, @@ -104,7 +126,7 @@ jobs: `### Action required`, `1. Review the changed files in Crowdsplit`, `2. Determine if SDK types, services, or methods need updating`, - `3. Follow the patterns in \`CLAUDE.md\` (Result, factory functions, withAuth)`, + `3. Follow SDK patterns (Result, factory functions, withAuth)`, ``, `---`, `${runLink} · _Auto-generated by Crowdsplit Staging Sync workflow_`, From a02d0b50b055f5f84c33c22836696baa93d5cc64 Mon Sep 17 00:00:00 2001 From: fahmidareem3 Date: Tue, 5 May 2026 17:10:49 +0600 Subject: [PATCH 4/5] fix: correct file_count parsing and commit message coercion --- .github/workflows/crowdsplit-sync.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/crowdsplit-sync.yml b/.github/workflows/crowdsplit-sync.yml index 429a872d..3bbbce4a 100644 --- a/.github/workflows/crowdsplit-sync.yml +++ b/.github/workflows/crowdsplit-sync.yml @@ -36,14 +36,15 @@ jobs: ? payload.changed_files.filter(f => typeof f === 'string' && SAFE_FILENAME.test(f)) : []; const truncated = Boolean(payload.truncated); - const fileCount = Number.isFinite(payload.file_count) ? payload.file_count : changedFiles.length; + const rawCount = Number(payload.file_count); + const fileCount = Number.isFinite(rawCount) ? rawCount : changedFiles.length; const sha = typeof payload.sha === 'string' ? payload.sha : ''; const compareUrl = typeof payload.compare_url === 'string' && payload.compare_url.startsWith(GITHUB_ORIGIN) ? payload.compare_url : ''; const escapeMd = (s) => String(s).replace(/[`*_\[\]\\#!()~<>|]/g, '\\$&'); - const commitMessage = escapeMd((payload.commit_message ?? '(no message)').slice(0, MAX_COMMIT_MSG_LENGTH)); + const commitMessage = escapeMd(String(payload.commit_message ?? '(no message)').slice(0, MAX_COMMIT_MSG_LENGTH)); if (changedFiles.length === 0 && fileCount === 0) { console.log('No changed files in payload. Skipping issue creation.'); From 91211481b5cdcdabeb687a39418e5e3385df59b1 Mon Sep 17 00:00:00 2001 From: fahmidareem3 Date: Tue, 5 May 2026 17:28:39 +0600 Subject: [PATCH 5/5] chore: rename workflow file and workflow content --- .../{crowdsplit-sync.yml => backend-sync.yml} | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) rename .github/workflows/{crowdsplit-sync.yml => backend-sync.yml} (90%) diff --git a/.github/workflows/crowdsplit-sync.yml b/.github/workflows/backend-sync.yml similarity index 90% rename from .github/workflows/crowdsplit-sync.yml rename to .github/workflows/backend-sync.yml index 3bbbce4a..4bcf1375 100644 --- a/.github/workflows/crowdsplit-sync.yml +++ b/.github/workflows/backend-sync.yml @@ -1,14 +1,14 @@ -name: Crowdsplit Staging Sync +name: Backend Staging Sync on: repository_dispatch: - types: [crowdsplit-staging-pushed] + types: [backend-staging-pushed] permissions: issues: write concurrency: - group: crowdsplit-sync + group: backend-sync cancel-in-progress: false jobs: @@ -111,11 +111,11 @@ jobs: await github.rest.issues.create({ owner: context.repo.owner, repo: context.repo.repo, - title: `[API Sync] Crowdsplit staging updated — ${fileCount} file(s) changed`, + title: `[API Sync] Backend staging updated — ${fileCount} file(s) changed`, body: [ - `## Crowdsplit Staging Change Detected`, + `## Backend Staging Change Detected`, ``, - `A push to Crowdsplit \`staging\` included changes that may require SDK updates.`, + `A push to the backend \`staging\` branch included changes that may require SDK updates.`, ``, `**Commit:** ${commitLink}`, `**Message:** ${commitMessage}`, @@ -125,12 +125,12 @@ jobs: truncationNote, ``, `### Action required`, - `1. Review the changed files in Crowdsplit`, + `1. Review the changed files in the backend`, `2. Determine if SDK types, services, or methods need updating`, `3. Follow SDK patterns (Result, factory functions, withAuth)`, ``, `---`, - `${runLink} · _Auto-generated by Crowdsplit Staging Sync workflow_`, + `${runLink} · _Auto-generated by Backend Staging Sync workflow_`, ].join('\n'), - labels: ['api-sync', 'crowdsplit'], + labels: ['api-sync', 'backend'], });