From 8bfbb50bc1027d6990317f0732aaf1d30ba062b0 Mon Sep 17 00:00:00 2001 From: FelineFantasy Date: Wed, 8 Jul 2026 00:38:10 +0500 Subject: [PATCH 01/10] feat: add GitHub Action for source maps upload --- .github/action.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/action.yml diff --git a/.github/action.yml b/.github/action.yml new file mode 100644 index 00000000..eb5f6d86 --- /dev/null +++ b/.github/action.yml @@ -0,0 +1,32 @@ +name: 'Upload Source Maps' +description: 'Uploads source maps to Telemetry-Tracker on release' +inputs: + api_key: + description: 'Telemetry-Tracker API Key' + required: true + release: + description: 'Release version/tag' + required: true + app: + description: 'Application name' + required: true + artifact_path: + description: 'Path to source map files' + required: false + default: './dist' + +runs: + using: 'composite' + steps: + - name: Upload maps via API + shell: bash + run: | + for file in ${{ inputs.artifact_path }}/*.map; do + if [ -f "$file" ]; then + curl -X POST https://telemetry-tracker.com/api/project/source-maps \ + -H "X-API-Key: ${{ inputs.api_key }}" \ + -F "release=${{ inputs.release }}" \ + -F "app=${{ inputs.app }}" \ + -F "file=@$file" + fi + done From e5e6e1807635f99b44eb381e627959f825a9aad7 Mon Sep 17 00:00:00 2001 From: FelineFantasy Date: Wed, 8 Jul 2026 00:41:46 +0500 Subject: [PATCH 02/10] docs: add GitHub Action example to source-maps documentation --- docs/source-maps.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/source-maps.md b/docs/source-maps.md index aff6138c..355c0119 100644 --- a/docs/source-maps.md +++ b/docs/source-maps.md @@ -145,3 +145,27 @@ Symbolication is display-only; grouping fingerprints stay on raw minified stacks - [sdk-core.md](./sdk-core.md) — `trackError`, `release` in payloads - [ARCHITECTURE.md](./ARCHITECTURE.md) — ingest pipeline - [ENTITLEMENTS.md](./ENTITLEMENTS.md) — retention by plan tier + +## GitHub Action Workflow Example + +You can automatically upload source maps on every release using our GitHub Action: + +```yaml +name: Release Configuration +on: + release: + types: [published] + +jobs: + upload-maps: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Upload Source Maps + uses: ./ + with: + api_key: ${{ secrets.TT_API_KEY }} + release: ${{ github.event.release.tag_name }} + app: "my-telemetry-app" + artifact_path: "./build" +``` From 637ddd5ad5461e1cca50ae45eafbdb6770dc751e Mon Sep 17 00:00:00 2001 From: FelineFantasy Date: Wed, 8 Jul 2026 01:37:23 +0500 Subject: [PATCH 03/10] refactor: move action to .github/actions/ and rewrite to json api --- .github/actions/upload-source-maps/action.yml | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 .github/actions/upload-source-maps/action.yml diff --git a/.github/actions/upload-source-maps/action.yml b/.github/actions/upload-source-maps/action.yml new file mode 100644 index 00000000..03ac19d8 --- /dev/null +++ b/.github/actions/upload-source-maps/action.yml @@ -0,0 +1,108 @@ +name: 'Upload Source Maps' +description: 'Uploads source maps to Telemetry-Tracker on release' +inputs: + session_cookie: + description: 'Dashboard session cookie for authentication' + required: true + project_id: + description: 'Telemetry-Tracker Project ID (X-Project-Id)' + required: true + release: + description: 'Release version/tag' + required: true + app: + description: 'Application name' + required: true + artifact_path: + description: 'Path to look for source map files recursively' + required: false + default: './dist' + base_url: + description: 'Base URL for your deployed JS bundles (e.g., https://example.com)' + required: true + +runs: + using: 'composite' + steps: + - name: Upload maps via JSON API + shell: bash + run: | + node -e " + const fs = require('fs'); + const path = require('path'); + const http = require('https'); + + function findFiles(dir) { + let results = []; + if (!fs.existsSync(dir)) return results; + const list = fs.readdirSync(dir); + list.forEach(file => { + file = path.join(dir, file); + const stat = fs.statSync(file); + if (stat && stat.isDirectory()) results = results.concat(findFiles(file)); + else if (file.endsWith('.map')) results.push(file); + }); + return results; + } + + function uploadFile(filePath) { + return new Promise((resolve, reject) => { + const content = fs.readFileSync(filePath, 'utf8'); + const fileName = path.basename(filePath); + const jsName = fileName.replace('.map', ''); + const bundleUrl = \`\${'${{ inputs.base_url }}'}/\${jsName}\`; + + const payload = JSON.stringify({ + app: '${{ inputs.app }}', + release: '${{ inputs.release }}', + bundle_url: bundleUrl, + content: JSON.parse(content) + }); + + const options = { + hostname: 'api.telemetry-tracker.com', + path: '/api/project/source-maps', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Project-Id': '${{ inputs.project_id }}', + 'Cookie': '${{ inputs.session_cookie }}', + 'Content-Length': Buffer.byteLength(payload) + } + }; + + const req = http.request(options, (res) => { + let body = ''; + res.on('data', chunk => body += chunk); + res.on('end', () => { + if (res.statusCode >= 400) { + reject(new Error(\`Failed to upload \${fileName}: \${res.statusCode} - \${body}\`)); + } else { + console.log(\`Successfully uploaded \${fileName}\`); + resolve(); + } + }); + }); + + req.on('error', (e) => reject(e)); + req.write(payload); + req.end(); + }); + } + + (async () => { + try { + const files = findFiles('${{ inputs.artifact_path }}'); + if (files.length === 0) { + console.log('No source maps found to upload.'); + return; + } + for (const file of files) { + await uploadFile(file); + } + } catch (err) { + console.error(err.message); + process.exit(1); + } + })(); + " From ea8e98988a030a79336860108e330d419aabc8ec Mon Sep 17 00:00:00 2001 From: FelineFantasy Date: Wed, 8 Jul 2026 01:38:01 +0500 Subject: [PATCH 04/10] style: remove deprecated root action.yml file --- .github/action.yml | 32 -------------------------------- 1 file changed, 32 deletions(-) delete mode 100644 .github/action.yml diff --git a/.github/action.yml b/.github/action.yml deleted file mode 100644 index eb5f6d86..00000000 --- a/.github/action.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: 'Upload Source Maps' -description: 'Uploads source maps to Telemetry-Tracker on release' -inputs: - api_key: - description: 'Telemetry-Tracker API Key' - required: true - release: - description: 'Release version/tag' - required: true - app: - description: 'Application name' - required: true - artifact_path: - description: 'Path to source map files' - required: false - default: './dist' - -runs: - using: 'composite' - steps: - - name: Upload maps via API - shell: bash - run: | - for file in ${{ inputs.artifact_path }}/*.map; do - if [ -f "$file" ]; then - curl -X POST https://telemetry-tracker.com/api/project/source-maps \ - -H "X-API-Key: ${{ inputs.api_key }}" \ - -F "release=${{ inputs.release }}" \ - -F "app=${{ inputs.app }}" \ - -F "file=@$file" - fi - done From 1bc0d9061bc07136b86cc2ef6e64251c83ff637d Mon Sep 17 00:00:00 2001 From: FelineFantasy Date: Wed, 8 Jul 2026 01:39:37 +0500 Subject: [PATCH 05/10] docs: update workflow example with new action path and inputs --- docs/source-maps.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/source-maps.md b/docs/source-maps.md index 355c0119..22eebd9a 100644 --- a/docs/source-maps.md +++ b/docs/source-maps.md @@ -162,10 +162,12 @@ jobs: steps: - uses: actions/checkout@v4 - name: Upload Source Maps - uses: ./ + uses: ./.github/actions/upload-source-maps with: - api_key: ${{ secrets.TT_API_KEY }} - release: ${{ github.event.release.tag_name }} + session_cookie: \${{ secrets.TT_SESSION_COOKIE }} + project_id: "your-project-uuid-here" + release: \${{ github.event.release.tag_name }} app: "my-telemetry-app" - artifact_path: "./build" + artifact_path: "./dist" + base_url: "https://example.com" ``` From 9503869553208179c0f51c6fefe37396fed71ff8 Mon Sep 17 00:00:00 2001 From: FelineFantasy Date: Wed, 8 Jul 2026 23:56:07 +0500 Subject: [PATCH 06/10] fix: correct bundle_url for nested files in upload action --- .github/actions/upload-source-maps/action.yml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/actions/upload-source-maps/action.yml b/.github/actions/upload-source-maps/action.yml index 03ac19d8..4ffe46b9 100644 --- a/.github/actions/upload-source-maps/action.yml +++ b/.github/actions/upload-source-maps/action.yml @@ -48,9 +48,13 @@ runs: function uploadFile(filePath) { return new Promise((resolve, reject) => { const content = fs.readFileSync(filePath, 'utf8'); - const fileName = path.basename(filePath); - const jsName = fileName.replace('.map', ''); - const bundleUrl = \`\${'${{ inputs.base_url }}'}/\${jsName}\`; + const artifactPath = '${{ inputs.artifact_path }}'; + // 1. Получаем путь, относительный к artifact_path + const relativePath = filePath.replace(artifactPath, '').replace(/^\/+/, ''); + // 2. Убираем .map, чтобы получить путь к исходному JS + const jsPath = relativePath.replace(/\.map$/, ''); + // 3. Собираем полный URL + const bundleUrl = \`\${{ inputs.base_url }}/\${jsPath}\`; const payload = JSON.stringify({ app: '${{ inputs.app }}', @@ -76,9 +80,9 @@ runs: res.on('data', chunk => body += chunk); res.on('end', () => { if (res.statusCode >= 400) { - reject(new Error(\`Failed to upload \${fileName}: \${res.statusCode} - \${body}\`)); + reject(new Error(\`Failed to upload \${path.basename(filePath)}: \${res.statusCode} - \${body}\`)); } else { - console.log(\`Successfully uploaded \${fileName}\`); + console.log(\`Successfully uploaded \${path.basename(filePath)}\`); resolve(); } }); From a90ef909c9c8977d3307b85f3f7c65e226fa63da Mon Sep 17 00:00:00 2001 From: FelineFantasy Date: Wed, 8 Jul 2026 23:59:11 +0500 Subject: [PATCH 07/10] docs: fix session cookie expression escaping in source-maps.md --- docs/source-maps.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source-maps.md b/docs/source-maps.md index 22eebd9a..4ba94d77 100644 --- a/docs/source-maps.md +++ b/docs/source-maps.md @@ -164,9 +164,9 @@ jobs: - name: Upload Source Maps uses: ./.github/actions/upload-source-maps with: - session_cookie: \${{ secrets.TT_SESSION_COOKIE }} + session_cookie: ${{ secrets.TT_SESSION_COOKIE }} project_id: "your-project-uuid-here" - release: \${{ github.event.release.tag_name }} + release: ${{ github.event.release.tag_name }} app: "my-telemetry-app" artifact_path: "./dist" base_url: "https://example.com" From d559103e33e439533bff64e314756247f8d99beb Mon Sep 17 00:00:00 2001 From: FelineFantasy Date: Thu, 9 Jul 2026 17:08:52 +0500 Subject: [PATCH 08/10] fix: use path.relative for bundle_url in action --- .github/actions/upload-source-maps/action.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/actions/upload-source-maps/action.yml b/.github/actions/upload-source-maps/action.yml index 4ffe46b9..0f37dbf5 100644 --- a/.github/actions/upload-source-maps/action.yml +++ b/.github/actions/upload-source-maps/action.yml @@ -50,7 +50,9 @@ runs: const content = fs.readFileSync(filePath, 'utf8'); const artifactPath = '${{ inputs.artifact_path }}'; // 1. Получаем путь, относительный к artifact_path - const relativePath = filePath.replace(artifactPath, '').replace(/^\/+/, ''); + const resolvedArtifactPath = path.resolve(artifactPath); + const resolvedFilePath = path.resolve(filePath); + const relativePath = path.relative(resolvedArtifactPath, resolvedFilePath); // 2. Убираем .map, чтобы получить путь к исходному JS const jsPath = relativePath.replace(/\.map$/, ''); // 3. Собираем полный URL From 980d099638a1d51d26a12a453017b286a5b98f7a Mon Sep 17 00:00:00 2001 From: FelineFantasy Date: Thu, 9 Jul 2026 17:10:37 +0500 Subject: [PATCH 09/10] docs: translate comments to English in action --- .github/actions/upload-source-maps/action.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actions/upload-source-maps/action.yml b/.github/actions/upload-source-maps/action.yml index 0f37dbf5..73da05c4 100644 --- a/.github/actions/upload-source-maps/action.yml +++ b/.github/actions/upload-source-maps/action.yml @@ -49,13 +49,13 @@ runs: return new Promise((resolve, reject) => { const content = fs.readFileSync(filePath, 'utf8'); const artifactPath = '${{ inputs.artifact_path }}'; - // 1. Получаем путь, относительный к artifact_path + // 1. Get path relative to artifact_path const resolvedArtifactPath = path.resolve(artifactPath); const resolvedFilePath = path.resolve(filePath); const relativePath = path.relative(resolvedArtifactPath, resolvedFilePath); - // 2. Убираем .map, чтобы получить путь к исходному JS + // 2. Remove .map to get the original JS path const jsPath = relativePath.replace(/\.map$/, ''); - // 3. Собираем полный URL + // 3. Build the full URL const bundleUrl = \`\${{ inputs.base_url }}/\${jsPath}\`; const payload = JSON.stringify({ From b39757e2d3492421ba1595a57e2ae3cb25d2f27f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sanja=20Malovi=C4=87?= Date: Thu, 9 Jul 2026 14:43:47 +0200 Subject: [PATCH 10/10] fix(ci): drop migrate from release email workflow GitHub Actions cannot reach Railway postgres.railway.internal; migrations stay a pre-tag manual step. Document public DATABASE_URL for the send ledger. Co-authored-by: Cursor --- .github/workflows/release-email.yml | 6 ------ docs/MARKETING-EMAIL.md | 11 ++++++----- docs/RELEASE.md | 4 ++-- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/.github/workflows/release-email.yml b/.github/workflows/release-email.yml index 267e2aae..e2d4cdbd 100644 --- a/.github/workflows/release-email.yml +++ b/.github/workflows/release-email.yml @@ -81,12 +81,6 @@ jobs: echo "reason=$(node -e "console.log(JSON.parse(process.argv[1]).reason)" "$BUMP")" >> "$GITHUB_OUTPUT" echo "Bump decision: $BUMP" - - name: Apply database migrations - if: steps.bump.outputs.should_send == 'true' - run: pnpm --filter api exec prisma migrate deploy - env: - DATABASE_URL: ${{ secrets.DATABASE_URL }} - - name: Send product update email if: steps.bump.outputs.should_send == 'true' working-directory: apps/api diff --git a/docs/MARKETING-EMAIL.md b/docs/MARKETING-EMAIL.md index 1343cc22..69651793 100644 --- a/docs/MARKETING-EMAIL.md +++ b/docs/MARKETING-EMAIL.md @@ -56,19 +56,20 @@ Delivery is recorded in `MarketingReleaseEmailSend` (one row per subscriber per Pushing a semver tag `vX.Y.Z` to GitHub triggers **Release product email** when **X** (major) or **Y** (minor) increases vs the previous tag; **Z**-only (patch/hotfix) tags are skipped: 1. Resolves the previous semver tag and compares **X** and **Y** (Z-only → skip). -2. Runs `prisma migrate deploy` against production (applies pending migrations from the tagged commit, including `MarketingReleaseEmailSend`). -3. Runs `send-release-email.ts --version=X.Y.Z --previous-version=…` with repository secrets. +2. Runs `send-release-email.ts --version=X.Y.Z --previous-version=…` with repository secrets. -Required GitHub repository secrets (production): +Apply production migrations **before tagging** ([RELEASE.md](./RELEASE.md#3-database-migrations-production)); this workflow does not run `prisma migrate deploy` (GitHub Actions cannot reach Railway private `*.railway.internal` URLs). + +Required GitHub secrets in the **`production`** environment: | Secret | Purpose | |--------|---------| -| `DATABASE_URL` | Production Postgres (`MarketingSubscriber`) | +| `DATABASE_URL` | Production Postgres **public** URL (Railway TCP proxy / public host — not `postgres.railway.internal`) | | `RESEND_API_KEY` | Resend API | | `TELEMETRY_EMAIL_FROM` | From address | | `TELEMETRY_DASHBOARD_ORIGIN` | Unsubscribe / docs links (e.g. `https://telemetry-tracker.com`) | -Store these in the GitHub **`production`** environment (the workflow job uses `environment: production`). To retry a send without re-tagging, use **Actions → Release product email → Run workflow** with `version` and `previous_version`. +Store these in the GitHub **`production`** environment. To retry a send without re-tagging, use **Actions → Release product email → Run workflow** with `version` and `previous_version`. The workflow runs **after** the tag is pushed — finalize `CHANGELOG.md` on `main` **before** tagging so the email body matches the release. diff --git a/docs/RELEASE.md b/docs/RELEASE.md index 6bf97661..f8b4fbd8 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -138,7 +138,7 @@ Always document **migrations**, **new env vars**, and **breaking changes** in CH 1. **Finalize CHANGELOG** — rename `[Unreleased]` to `[X.Y.Z] - YYYY-MM-DD`. Prefer doing this in the **`develop` → `main`** release PR so `develop` and `main` stay aligned; if you commit on `main` after promotion, you **must** sync `develop` in step 9. 2. **Deploy** — Railway rebuilds `main` automatically when the release PR merges; see [Deploy runbook](#deploy-runbook-railway). -3. **Production DB** — run [migrations](#3-database-migrations-production) **before tagging** on MINOR (Y) / MAJOR (X) releases (the [Release product email](../.github/workflows/release-email.yml) workflow also runs `prisma migrate deploy` immediately before send, but apply migrations here as part of the normal release gate). +3. **Production DB** — run [migrations](#3-database-migrations-production) **before tagging** on MINOR (Y) / MAJOR (X) releases. The release email workflow reads production Postgres but does not migrate (use the public `DATABASE_URL` in GitHub secrets — see [MARKETING-EMAIL.md](./MARKETING-EMAIL.md#automated-send-minor--major-tags)). 4. **Post-deploy** — [verification](#post-deploy-verification). 5. **Tag** — after deploy and migrations are green (tag push triggers the product update email workflow for MINOR/MAJOR — **X** or **Y** bump, not Z-only hotfixes): ```bash @@ -198,7 +198,7 @@ On push and pull requests to **`develop`** and **`main`**, CI runs ([`.github/wo ### 3. Database migrations (production) -CI does **not** migrate your production database during normal builds. Apply migrations after API deploy and **before pushing a MINOR (Y) / MAJOR (X) tag** (see [On `main` after promotion](#on-main-after-promotion)). The [Release product email](../.github/workflows/release-email.yml) workflow also runs `prisma migrate deploy` immediately before sending so delivery records can be written. +CI does **not** migrate your production database during normal builds. Apply migrations after API deploy and **before pushing a MINOR (Y) / MAJOR (X) tag** (see [On `main` after promotion](#on-main-after-promotion)). The [Release product email](../.github/workflows/release-email.yml) workflow connects to production Postgres for the send ledger only — use Railway’s **public** database URL in GitHub secrets, not `postgres.railway.internal`. ```bash DATABASE_URL="postgresql://..." pnpm --filter api exec prisma migrate deploy