Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions github-activity-tracker/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ RDS_DATABASE=github_tracker
RDS_USER=postgres
RDS_PASSWORD=your-password

# Single source of truth for lookup tables (seeded into DB on migrate/start).
GITHUB_ORG=mosip,inji
USER_ROLES=Developer,Tech Lead,Architect,Product Owner,Leadership,QA Engineer,DevOps Engineer

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Quote the USER_ROLES value to prevent potential parsing issues.

The value contains spaces (e.g., "Tech Lead", "QA Engineer"). Without quotes, strict dotenv parsers may truncate at the first space, yielding Developer,Tech instead of the full list. The downstream defaultUserRoles.js reads process.env.USER_ROLES directly, so a truncation would silently produce an incomplete role list.

🔧 Proposed fix
-USER_ROLES=Developer,Tech Lead,Architect,Product Owner,Leadership,QA Engineer,DevOps Engineer
+USER_ROLES="Developer,Tech Lead,Architect,Product Owner,Leadership,QA Engineer,DevOps Engineer"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
USER_ROLES=Developer,Tech Lead,Architect,Product Owner,Leadership,QA Engineer,DevOps Engineer
USER_ROLES="Developer,Tech Lead,Architect,Product Owner,Leadership,QA Engineer,DevOps Engineer"
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 15-15: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@github-activity-tracker/.env.example` at line 15, The USER_ROLES environment
entry should be quoted because it contains role names with spaces, which can be
truncated by strict dotenv parsing. Update the USER_ROLES value in the example
env file to use quotes so downstream consumers like defaultUserRoles.js still
read the full comma-separated list from process.env.USER_ROLES.

Source: Linters/SAST tools


# Variables used by the Postgres container itself:
# POSTGRES_DB=github_data
# POSTGRES_USER=github_user
Expand Down
10 changes: 9 additions & 1 deletion github-activity-tracker/backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,12 @@ RDS_PORT=5432
RDS_DATABASE=github_data
RDS_USER=your-rds-user
RDS_PASSWORD=your-rds-password
GITHUB_TOKEN=your-github-token
GITHUB_TOKEN=your-github-token

Comment on lines +6 to +7

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep the dotenv keys in the expected order.

Move GITHUB_TOKEN before RDS_DATABASE; otherwise this example will keep tripping the repo’s env-file lint.

🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 6-6: [UnorderedKey] The GITHUB_TOKEN key should go before the RDS_DATABASE key

(UnorderedKey)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@github-activity-tracker/backend/.env.example` around lines 6 - 7, The dotenv
example keys are out of the expected order, which triggers the env-file lint.
Reorder the entries in the .env.example block so GITHUB_TOKEN appears before
RDS_DATABASE, keeping the rest of the example unchanged and matching the repo’s
required key ordering.

Source: Linters/SAST tools

# Read from .env on backend start and stored in user_roles / organizations tables.
GITHUB_ORG=mosip,inji
USER_ROLES=Developer,Tech Lead,Architect,Product Owner,Leadership,QA Engineer,DevOps Engineer

# Bearer token required to call /admin/* endpoints (sync + role management).
# Generate a strong random value, e.g. `openssl rand -hex 32`.
ADMIN_API_TOKEN=your-admin-api-token
35 changes: 33 additions & 2 deletions github-activity-tracker/backend/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* GitHub Activity Tracker – Backend API
*
* Express server that exposes admin sync endpoints to pull repository, commit,
* PR, and review data from GitHub into PostgreSQL. Run migrations first (npm run migrate).
* PR, and review data from GitHub into PostgreSQL.
*/
require('dotenv').config();
const express = require('express');
Expand All @@ -16,6 +16,11 @@ const orgActivityRoute = require('./routes/orgActivityRoute');
const orgSummaryRoute = require('./routes/orgSummaryRoute');
const orgUsersRoute = require('./routes/orgUsersRoute');
const userDetailsRoute = require('./routes/userDetailsRoute');
const userNameSyncRoute = require('./routes/userNameSyncRoute');
const userRoleRoute = require('./routes/userRoleRoute');
const userRolesRoute = require('./routes/userRolesRoute');
const organizationsRoute = require('./routes/organizationsRoute');
const { ensureLookupTables } = require('./db/initLookupTables');

const app = express();
const PORT = process.env.PORT || 3000;
Expand All @@ -34,6 +39,11 @@ app.get('/', (req, res) => {
'POST /admin/sync/commits': 'Sync commits for all repositories in DB',
'POST /admin/sync/prs': 'Sync PRs for all repositories in DB',
'POST /admin/sync/reviews': 'Sync PR reviews for all repositories in DB',
'POST /admin/sync/user-names': 'Backfill GitHub profile names for users in DB',
'POST /admin/users/role': 'Assign or change job role for a GitHub user',
'GET /admin/users/:login/role': 'Fetch job role for a GitHub user',
'GET /user-roles': 'List assignable user job roles',
'GET /organizations': 'List tracked GitHub organizations',
},
});
});
Expand All @@ -43,12 +53,33 @@ app.use(repoSyncRoute);
app.use(commitSyncRoute);
app.use(prSyncRoute);
app.use(reviewSyncRoute);
app.use(userNameSyncRoute);
app.use(userRoleRoute);
Comment thread
kharodejayesh marked this conversation as resolved.
app.use(userRolesRoute);
app.use(organizationsRoute);
app.use(orgUsersRoute);
app.use(orgSummaryRoute);
app.use(userDetailsRoute);
app.use(orgActivityRoute);
app.use(leaderboardRoute);
Comment on lines +56 to 64

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how admin routes are mounted in app.js
cat -n github-activity-tracker/backend/app.js | sed -n '40,65p'

# Search for adminAuth middleware usage
rg -n 'adminAuth' github-activity-tracker/backend/app.js

# Check route files for any built-in auth guards
rg -n 'adminAuth|requireAuth|middleware' github-activity-tracker/backend/routes/userRoleRoute.js github-activity-tracker/backend/routes/userNameSyncRoute.js

Repository: mosip/mosip-labs

Length of output: 1282


🏁 Script executed:

#!/bin/bash
set -e

cat -n github-activity-tracker/backend/routes/userNameSyncRoute.js | sed -n '1,220p'
printf '\n---\n'
cat -n github-activity-tracker/backend/routes/userRoleRoute.js | sed -n '1,220p'
printf '\n---\n'
cat -n github-activity-tracker/backend/app.js | sed -n '1,140p'

Repository: mosip/mosip-labs

Length of output: 9516


Mount the admin routes behind adminAuth. github-activity-tracker/backend/app.js:56-57 exposes userNameSyncRoute and userRoleRoute directly, and neither router applies its own auth guard, so the privileged /admin/* endpoints are reachable without the required middleware.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@github-activity-tracker/backend/app.js` around lines 56 - 64, Mount the
privileged admin routers behind the existing adminAuth middleware in app.js: the
userNameSyncRoute and userRoleRoute are currently registered directly with
app.use, so update their wiring so requests must pass through adminAuth before
reaching those /admin/* handlers. Keep the other route mounts unchanged and use
the route names userNameSyncRoute and userRoleRoute to locate the affected
registration block.


app.listen(PORT, () => {
app.listen(PORT, async () => {
try {
const result = await ensureLookupTables();

if (result.createdTables.length > 0) {
console.log(`Created tables: ${result.createdTables.join(', ')}`);
}

if (result.rolesAdded > 0 || result.orgsAdded > 0) {
console.log(`Added from .env: ${result.rolesAdded} role(s), ${result.orgsAdded} organization(s)`);
} else if (result.createdTables.length === 0) {
console.log('Lookup tables already exist; no new roles or organizations to add.');
}
} catch (error) {
console.error('Failed to initialize lookup tables:', error.message);
process.exit(1);
}

console.log(`Server running on http://localhost:${PORT}`);
});
16 changes: 16 additions & 0 deletions github-activity-tracker/backend/config/defaultUserRoles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
function parseUserRolesFromEnv() {
const fromEnv = process.env.USER_ROLES;

if (!fromEnv || !fromEnv.trim()) {
return [];
}

return fromEnv
.split(',')
.map((role) => role.trim())
.filter(Boolean);
}

module.exports = {
parseUserRolesFromEnv,
};
15 changes: 15 additions & 0 deletions github-activity-tracker/backend/config/organizations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const {
getAllOrganizations,
getOrganizationSlugs,
getOrganizationNames,
normalizeOrganization,
isValidOrganization,
} = require('../services/organizationsService');

module.exports = {
getAllOrganizations,
getOrganizationSlugs,
getOrganizationNames,
normalizeOrganization,
isValidOrganization,
};
12 changes: 12 additions & 0 deletions github-activity-tracker/backend/config/syncConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@
/** Delay in ms between processing each repo when syncing commits/PRs/reviews for all repos. */
const DELAY_BETWEEN_REPOS_MS = 300;

/** Default batch size for backfilling missing GitHub user display names per request. */
const NAME_BACKFILL_BATCH_SIZE = 50;

/** Delay in ms between GitHub profile lookups when backfilling user names. */
const NAME_FETCH_DELAY_MS = 120;
Comment thread
kharodejayesh marked this conversation as resolved.

/** Maximum batch size allowed for a single user-name backfill request. */
const NAME_BACKFILL_MAX_BATCH_SIZE = 500;

module.exports = {
DELAY_BETWEEN_REPOS_MS,
NAME_BACKFILL_BATCH_SIZE,
NAME_FETCH_DELAY_MS,
NAME_BACKFILL_MAX_BATCH_SIZE,
};
11 changes: 11 additions & 0 deletions github-activity-tracker/backend/config/userRoles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const {
getAllUserRoles,
getUserRoleNames,
isValidUserRole,
} = require('../services/userRolesService');

module.exports = {
getAllUserRoles,
getUserRoleNames,
isValidUserRole,
};
96 changes: 96 additions & 0 deletions github-activity-tracker/backend/db/initLookupTables.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
const pool = require('./dbPool');
const { parseUserRolesFromEnv } = require('../config/defaultUserRoles');

function parseOrganizationsFromEnv(value = process.env.GITHUB_ORG) {
return (value || '')
.split(',')
.map((slug) => slug.trim().toLowerCase())
.filter(Boolean);
}

async function tableExists(tableName) {
const result = await pool.query(
`
SELECT 1
FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = $1
LIMIT 1
`,
[tableName]
);
return result.rowCount > 0;
}

async function ensureLookupTables() {
const roles = parseUserRolesFromEnv();
const orgs = parseOrganizationsFromEnv();
const createdTables = [];

if (!(await tableExists('user_roles'))) {
await pool.query(`
CREATE TABLE user_roles (
id SERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
createdTables.push('user_roles');
}

if (!(await tableExists('organizations'))) {
await pool.query(`
CREATE TABLE organizations (
id SERIAL PRIMARY KEY,
slug VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
createdTables.push('organizations');
}

let rolesAdded = 0;
for (const name of roles) {
const result = await pool.query(
`
INSERT INTO user_roles (name)
VALUES ($1)
ON CONFLICT (name) DO NOTHING
RETURNING id
`,
[name]
);
if (result.rowCount > 0) {
rolesAdded += 1;
}
}

let orgsAdded = 0;
for (const slug of orgs) {
const result = await pool.query(
`
INSERT INTO organizations (slug, name)
VALUES ($1, $2)
ON CONFLICT (slug) DO NOTHING
RETURNING id
`,
[slug, slug.toUpperCase()]
);
if (result.rowCount > 0) {
orgsAdded += 1;
}
}

return {
roles,
orgs,
createdTables,
rolesAdded,
orgsAdded,
};
}

module.exports = {
ensureLookupTables,
parseOrganizationsFromEnv,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- GitHub profile display name (from GET /users/{login} or GraphQL User.name).
ALTER TABLE github_users
ADD COLUMN IF NOT EXISTS name VARCHAR(255);
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- Job role for team members (set manually until admin update API exists).
-- Allowed values: Developer, Tech Lead, Architect, Product Owner, Leadership, QA Engineer, DevOps Engineer
ALTER TABLE github_users
ADD COLUMN IF NOT EXISTS role VARCHAR(50);

CREATE INDEX IF NOT EXISTS idx_github_users_role ON github_users(role);
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
-- User role/organization assignments with history. One active row per user at a time.
-- Lookup tables (user_roles, organizations) are created at app startup from .env.

CREATE TABLE IF NOT EXISTS user_details (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES github_users(id) ON DELETE CASCADE,
role_id INTEGER REFERENCES user_roles(id),
organization_id INTEGER REFERENCES organizations(id),
active BOOLEAN NOT NULL DEFAULT true,
active_from TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
active_to TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

ALTER TABLE user_details
ADD COLUMN IF NOT EXISTS role_id INTEGER REFERENCES user_roles(id),
ADD COLUMN IF NOT EXISTS organization_id INTEGER REFERENCES organizations(id);

ALTER TABLE user_details
DROP COLUMN IF EXISTS login,
DROP COLUMN IF EXISTS name,
DROP COLUMN IF EXISTS github_user_id;

CREATE INDEX IF NOT EXISTS idx_user_details_user_id ON user_details(user_id);
CREATE INDEX IF NOT EXISTS idx_user_details_role_id ON user_details(role_id);
CREATE INDEX IF NOT EXISTS idx_user_details_organization_id ON user_details(organization_id);
CREATE INDEX IF NOT EXISTS idx_user_details_active_from ON user_details(active_from);
CREATE INDEX IF NOT EXISTS idx_user_details_active_to ON user_details(active_to);

CREATE UNIQUE INDEX IF NOT EXISTS idx_user_details_one_active_per_user
ON user_details(user_id)
WHERE active = true;

-- GitHub profile name lives on github_users; assignments live in user_details.
ALTER TABLE github_users
ADD COLUMN IF NOT EXISTS name VARCHAR(255);

DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'user_details'
AND column_name = 'name'
) THEN
UPDATE github_users u
SET
name = ud.name,
updated_at = CURRENT_TIMESTAMP
FROM user_details ud
WHERE ud.user_id = u.id
AND ud.active = true
AND ud.name IS NOT NULL
AND (u.name IS NULL OR u.name = '');
END IF;
END $$;
Comment on lines +20 to +58

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Critical: user_details.name column is dropped before the data migration that uses it, causing silent data loss on upgrades.

Lines 20-23 execute DROP COLUMN IF EXISTS name on user_details, then lines 39-58 contain a DO $$ block that checks information_schema.columns for user_details.name to migrate it to github_users.name. Since the column was already dropped, the IF EXISTS check will always be false and the migration never runs. Any existing display names stored in user_details.name are permanently lost.

For fresh installs this is harmless (no data to migrate), but for upgrades from a schema where user_details had a name column, this is a data-loss bug.

Note: the github_users.role migration at lines 60-83 is correctly ordered — the DROP COLUMN IF EXISTS role at line 87 runs after the DO $$ block that uses it.

🐛 Proposed fix: move DROP COLUMN after the data migration
 ALTER TABLE user_details
-  DROP COLUMN IF EXISTS login,
-  DROP COLUMN IF EXISTS name,
-  DROP COLUMN IF EXISTS github_user_id;
+  DROP COLUMN IF EXISTS login,
+  DROP COLUMN IF EXISTS github_user_id;

 CREATE INDEX IF NOT EXISTS idx_user_details_user_id ON user_details(user_id);
 CREATE INDEX IF NOT EXISTS idx_user_details_role_id ON user_details(role_id);
 CREATE INDEX IF NOT EXISTS idx_user_details_organization_id ON user_details(organization_id);
 CREATE INDEX IF NOT EXISTS idx_user_details_active_from ON user_details(active_from);
 CREATE INDEX IF NOT EXISTS idx_user_details_active_to ON user_details(active_to);

 CREATE UNIQUE INDEX IF NOT EXISTS idx_user_details_one_active_per_user
   ON user_details(user_id)
   WHERE active = true;

 -- GitHub profile name lives on github_users; assignments live in user_details.
 ALTER TABLE github_users
   ADD COLUMN IF NOT EXISTS name VARCHAR(255);

 DO $$
 BEGIN
   IF EXISTS (
     SELECT 1
     FROM information_schema.columns
     WHERE table_schema = 'public'
       AND table_name = 'user_details'
       AND column_name = 'name'
   ) THEN
     UPDATE github_users u
     SET
       name = ud.name,
       updated_at = CURRENT_TIMESTAMP
     FROM user_details ud
     WHERE ud.user_id = u.id
       AND ud.active = true
       AND ud.name IS NOT NULL
       AND (u.name IS NULL OR u.name = '');
   END IF;
 END $$;

+-- Now safe to drop the name column after migration
+ALTER TABLE user_details
+  DROP COLUMN IF EXISTS name;
+
 DO $$
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ALTER TABLE user_details
DROP COLUMN IF EXISTS login,
DROP COLUMN IF EXISTS name,
DROP COLUMN IF EXISTS github_user_id;
CREATE INDEX IF NOT EXISTS idx_user_details_user_id ON user_details(user_id);
CREATE INDEX IF NOT EXISTS idx_user_details_role_id ON user_details(role_id);
CREATE INDEX IF NOT EXISTS idx_user_details_organization_id ON user_details(organization_id);
CREATE INDEX IF NOT EXISTS idx_user_details_active_from ON user_details(active_from);
CREATE INDEX IF NOT EXISTS idx_user_details_active_to ON user_details(active_to);
CREATE UNIQUE INDEX IF NOT EXISTS idx_user_details_one_active_per_user
ON user_details(user_id)
WHERE active = true;
-- GitHub profile name lives on github_users; assignments live in user_details.
ALTER TABLE github_users
ADD COLUMN IF NOT EXISTS name VARCHAR(255);
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'user_details'
AND column_name = 'name'
) THEN
UPDATE github_users u
SET
name = ud.name,
updated_at = CURRENT_TIMESTAMP
FROM user_details ud
WHERE ud.user_id = u.id
AND ud.active = true
AND ud.name IS NOT NULL
AND (u.name IS NULL OR u.name = '');
END IF;
END $$;
ALTER TABLE user_details
DROP COLUMN IF EXISTS login,
DROP COLUMN IF EXISTS github_user_id;
CREATE INDEX IF NOT EXISTS idx_user_details_user_id ON user_details(user_id);
CREATE INDEX IF NOT EXISTS idx_user_details_role_id ON user_details(role_id);
CREATE INDEX IF NOT EXISTS idx_user_details_organization_id ON user_details(organization_id);
CREATE INDEX IF NOT EXISTS idx_user_details_active_from ON user_details(active_from);
CREATE INDEX IF NOT EXISTS idx_user_details_active_to ON user_details(active_to);
CREATE UNIQUE INDEX IF NOT EXISTS idx_user_details_one_active_per_user
ON user_details(user_id)
WHERE active = true;
-- GitHub profile name lives on github_users; assignments live in user_details.
ALTER TABLE github_users
ADD COLUMN IF NOT EXISTS name VARCHAR(255);
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'user_details'
AND column_name = 'name'
) THEN
UPDATE github_users u
SET
name = ud.name,
updated_at = CURRENT_TIMESTAMP
FROM user_details ud
WHERE ud.user_id = u.id
AND ud.active = true
AND ud.name IS NOT NULL
AND (u.name IS NULL OR u.name = '');
END IF;
END $$;
-- Now safe to drop the name column after migration
ALTER TABLE user_details
DROP COLUMN IF EXISTS name;
🧰 Tools
🪛 Squawk (2.59.0)

[warning] 21-21: Dropping a column may break existing clients.

(ban-drop-column)


[warning] 22-22: Dropping a column may break existing clients.

(ban-drop-column)


[warning] 23-23: Dropping a column may break existing clients.

(ban-drop-column)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@github-activity-tracker/backend/migrations/007_create_user_details_table.sql`
around lines 20 - 58, The migration in `007_create_user_details_table.sql` drops
`user_details.name` before the backfill block that reads it, so the data
migration in the `DO $$` section never runs. Reorder the statements so the
`UPDATE github_users ... FROM user_details` logic executes before any `DROP
COLUMN IF EXISTS name` on `user_details`, keeping the existing
`github_users`/`user_details` migration flow intact and preserving upgrade data.


DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'github_users'
AND column_name = 'role'
) THEN
INSERT INTO user_details (user_id, role_id, active, active_from, active_to)
SELECT
u.id,
ur.id,
true,
COALESCE(u.updated_at, u.inserted_at, CURRENT_TIMESTAMP),
NULL
FROM github_users u
JOIN user_roles ur ON ur.name = u.role
WHERE u.role IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM user_details ud WHERE ud.user_id = u.id AND ud.active = true
);
END IF;
END $$;

DROP INDEX IF EXISTS idx_github_users_role;
ALTER TABLE github_users
DROP COLUMN IF EXISTS role;
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
-- Backdate user_details.active_from so historical activity before user sync
-- is included when a role is first assigned.
UPDATE user_details ud
SET
active_from = LEAST(
ud.active_from,
COALESCE(
(SELECT MIN(e.created_at) FROM activity_events e WHERE e.user_id = ud.user_id),
ud.active_from
)
),
updated_at = CURRENT_TIMESTAMP
WHERE EXISTS (
SELECT 1 FROM activity_events e WHERE e.user_id = ud.user_id
);

-- First role assignment rows should cover all prior activity, not only from assignment time.
UPDATE user_details current_ud
SET
active_from = LEAST(
current_ud.active_from,
COALESCE(
(SELECT MIN(e.created_at) FROM activity_events e WHERE e.user_id = current_ud.user_id),
current_ud.active_from
),
COALESCE(
(SELECT MIN(h.active_from) FROM user_details h WHERE h.user_id = current_ud.user_id),
current_ud.active_from
)
),
updated_at = CURRENT_TIMESTAMP
WHERE current_ud.role_id IS NOT NULL
AND NOT EXISTS (
SELECT 1
FROM user_details prior_role
WHERE prior_role.user_id = current_ud.user_id
AND prior_role.role_id IS NOT NULL
AND prior_role.id <> current_ud.id
AND prior_role.active_to IS NOT NULL
AND prior_role.active_to <= current_ud.active_from
);
Comment on lines +3 to +41

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift

Only backdate the first role assignment per user.

Line 3 backdates every user_details row before Line 18 tries to identify first role assignments. For users with multiple role-history rows, later assignments can be moved back to the earliest activity date, overlapping prior windows and causing role-filtered activity to be double-counted or misattributed. Restrict the migration to the earliest role assignment row per user, and leave later assignment windows unchanged.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@github-activity-tracker/backend/migrations/008_backfill_user_details_active_from.sql`
around lines 3 - 41, The migration backdates too many user_details rows, which
can shift later role windows and overlap prior history. Update the backfill
logic in the SQL migration so only the earliest role assignment row per user is
backdated, using the existing user_details and activity_events lookups to
identify that single first role row. Keep later role assignment rows unchanged,
and ensure the updated_at touch only applies to the targeted earliest assignment
row.

Loading
Loading