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
79 changes: 79 additions & 0 deletions .github/RELEASE_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<!--
# Release Notes Instructions

Please follow this format when drafting the release notes.
- The sync process validates the release body and will fail if the required format is not followed.
- If this release changes the hero section of landing page, please put the relevant screenshot in /packages/ui/src/assets/release


IMPORTANT:
- The release introduction is optional.
- Keep the category headings exactly as shown, including the πŸ“ emoji.
- Use only the categories that are relevant to this release.
- Remove any category that has no entries. Do not leave unused categories in the release body.
- Each entry must be placed under the appropriate category.
- Do not change the heading levels:
### for categories
#### for entries
- Each entry must follow this format:
#### #<PR_NUMBER> | <TITLE>

<DESCRIPTION>

> Contributors: @<USERNAME> [@<USERNAME> ...]

- PR_NUMBER must be the actual GitHub Pull Request number.
- TITLE must be the title of the entry.
- DESCRIPTION should briefly describe the change.
- Every entry must include a Contributors line.
- Contributors must be GitHub usernames beginning with @.
- Multiple contributors should be separated by spaces.
- Do not use commas between contributor usernames.
- Do not add text or entries outside the defined structure.
- Replace the `Entry` placeholder under each category with the actual release entry, or remove the entire category if it is not used.

Example Entry:

#### #1042 | Revamp USER Dashboard

Full redesign of the user interface according to the latest design specifications for improved usability.

> Contributors: @personA @personB

-->

[OPTIONAL RELEASE INTRODUCTION]

### πŸ“ Features

#### #104 | Example Entry Title 1

Description for example entry 1

> Contributors: @personA @personB

#### #102 | Revamp User Dashboard

Full redesign of the user interface according to the latest design specifications for improved usability.

> Contributors: @personC


### πŸ“ Enhancements

Entry


### πŸ“ Bug Fixes

Entry


### πŸ“ Security

Entry


### πŸ“ Others

Entry
24 changes: 24 additions & 0 deletions .github/workflows/sync-historical-releases.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Seed Historical Releases

on:
workflow_dispatch:

jobs:
seed:
name: Seed historical releases to MongoDB
runs-on: ubuntu-latest
timeout-minutes: 5

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install app dependencies
working-directory: packages/app
run: npm install --legacy-peer-deps

Check warning on line 18 in .github/workflows/sync-historical-releases.yaml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Using dependencies without locking resolved versions is security-sensitive.

See more on https://sonarcloud.io/project/issues?id=TeamShiksha_openlogo&issues=AaDFN1RoDoFd0vvsimLJ&open=AaDFN1RoDoFd0vvsimLJ&pullRequest=1113

Check warning on line 18 in .github/workflows/sync-historical-releases.yaml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Omitting "--ignore-scripts" allows lifecycle scripts to run during package installation.

See more on https://sonarcloud.io/project/issues?id=TeamShiksha_openlogo&issues=AaDFN1RoDoFd0vvsimLI&open=AaDFN1RoDoFd0vvsimLI&pullRequest=1113

- name: Run release seed script
working-directory: packages/app
env:
MONGO_URL: ${{ secrets.MONGO_URL }}
run: npm run seed:releases
27 changes: 27 additions & 0 deletions .github/workflows/sync-releases.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Sync GitHub Release to MongoDB

on:
release:
types: [published, edited]

jobs:
sync:
name: Sync release ${{ github.event.release.tag_name }} to MongoDB
runs-on: ubuntu-latest
timeout-minutes: 5

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install dependencies
run: npm install mongoose --legacy-peer-deps

Check warning on line 18 in .github/workflows/sync-releases.yaml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Omitting "--ignore-scripts" allows lifecycle scripts to run during package installation.

See more on https://sonarcloud.io/project/issues?id=TeamShiksha_openlogo&issues=AaDFN1RgDoFd0vvsimLG&open=AaDFN1RgDoFd0vvsimLG&pullRequest=1113

Check warning on line 18 in .github/workflows/sync-releases.yaml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Using dependencies without locking resolved versions is security-sensitive.

See more on https://sonarcloud.io/project/issues?id=TeamShiksha_openlogo&issues=AaDFN1RgDoFd0vvsimLH&open=AaDFN1RgDoFd0vvsimLH&pullRequest=1113

- name: Run release sync script
working-directory: packages/app
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MONGO_URL: ${{ secrets.MONGO_URL }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: node scripts/releases/syncNewReleases.js
155 changes: 155 additions & 0 deletions packages/app/__tests__/controllers/release.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
"use strict";

const request = require("supertest");
const app = require("../../server");
const ReleaseService = require("../../services/release");

jest.mock("../../services/release");

describe("Releases Controller & Routes (/api/releases)", () => {
let mockGetAllReleases;
let mockGetReleaseByVersion;
let mockGetReleaseVersions;

beforeEach(() => {
jest.clearAllMocks();
mockGetAllReleases = jest.fn();
mockGetReleaseByVersion = jest.fn();
mockGetReleaseVersions = jest.fn();

ReleaseService.prototype.getAllReleases = mockGetAllReleases;
ReleaseService.prototype.getReleaseByVersion = mockGetReleaseByVersion;
ReleaseService.prototype.getReleaseVersions = mockGetReleaseVersions;
});

describe("GET /api/releases", () => {
it("returns 200 with paginated releases data", async () => {
const mockResult = {
data: [
{
version: "v1.0.0",
releaseDate: "2026-09-01T00:00:00.000Z",
githubReleaseId: 101,
githubReleaseUrl: "https://github.com/org/repo/releases/tag/v1.0.0",
entries: [],
},
],
total: 1,
currentPage: 1,
totalPages: 1,
};

mockGetAllReleases.mockResolvedValue(mockResult);

const res = await request(app).get("/api/releases");

expect(res.status).toBe(200);
expect(res.body.statusCode).toBe(200);
expect(res.body.data).toEqual(mockResult.data);
expect(res.body.pagination).toEqual({
total: 1,
currentPage: 1,
totalPages: 1,
limit: 10,
});
expect(res.body.archivedReleases).toBeUndefined();
expect(mockGetAllReleases).toHaveBeenCalledWith(1, 10);
});

it("parses page and limit query parameters", async () => {
mockGetAllReleases.mockResolvedValue({
data: [],
total: 20,
currentPage: 2,
totalPages: 4,
});

const res = await request(app).get("/api/releases?page=2&limit=5");

expect(res.status).toBe(200);
expect(mockGetAllReleases).toHaveBeenCalledWith(2, 5);
expect(res.body.pagination.limit).toBe(5);
});

it("clamps page to minimum 1 and limit between 1 and 100", async () => {
mockGetAllReleases.mockResolvedValue({
data: [],
total: 0,
currentPage: 1,
totalPages: 0,
});

await request(app).get("/api/releases?page=-5&limit=500");

expect(mockGetAllReleases).toHaveBeenCalledWith(1, 100);
});
});

describe("GET /api/releases/:version", () => {
it("returns 200 with single release data when version exists", async () => {
const releaseData = {
version: "v1.2.0",
releaseDate: "2026-09-10T00:00:00.000Z",
githubReleaseId: 202,
githubReleaseUrl: "https://github.com/org/repo/releases/tag/v1.2.0",
entries: [
{
category: "Feature",
prNumber: 1042,
title: "Revamp USER Dashboard",
description: "Redesign UI",
contributors: [{ username: "personA" }],
},
],
};

mockGetReleaseByVersion.mockResolvedValue(releaseData);

const res = await request(app).get("/api/releases/v1.2.0");

expect(res.status).toBe(200);
expect(res.body.statusCode).toBe(200);
expect(res.body.data).toEqual(releaseData);
expect(mockGetReleaseByVersion).toHaveBeenCalledWith("v1.2.0");
});

it("returns 404 when release version is not found", async () => {
mockGetReleaseByVersion.mockResolvedValue(null);

const res = await request(app).get("/api/releases/v9.9.9");

expect(res.status).toBe(404);
expect(res.body).toEqual({
statusCode: 404,
error: "Not Found",
message: "Release v9.9.9 not found.",
});
});
});

describe("GET /api/releases/versions", () => {
it("returns 200 with lightweight list of versions", async () => {
const versionsData = [
{
version: "0.8.0",
releaseDate: "2026-05-15T00:00:00.000Z",
},
{
version: "0.7.0",
releaseDate: "2026-03-20T00:00:00.000Z",
},
];

mockGetReleaseVersions.mockResolvedValue(versionsData);

const res = await request(app).get("/api/releases/versions");

expect(res.status).toBe(200);
expect(res.body).toEqual({
statusCode: 200,
data: versionsData,
});
expect(mockGetReleaseVersions).toHaveBeenCalled();
});
});
});
Loading
Loading