Skip to content

Commit 68afb48

Browse files
author
Ronaldo Macapobre
committed
initial attempt for versioning
1 parent 90177ca commit 68afb48

File tree

5 files changed

+257
-6
lines changed

5 files changed

+257
-6
lines changed

.github/workflows/actions/build-api/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ runs:
2727
shell: bash
2828
working-directory: ${{ inputs.working_directory }}
2929

30-
- run: dotnet build --configuration Release --no-restore
30+
- run: dotnet build --configuration Release --no-restore /p:BuildNumber=${{ github.run_number }}
3131
shell: bash
3232
working-directory: ${{ inputs.working_directory }}/api
3333

api/Controllers/ApplicationController.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Reflection;
12
using Microsoft.AspNetCore.Authorization;
23
using Microsoft.AspNetCore.Http;
34
using Microsoft.AspNetCore.Mvc;
@@ -22,11 +23,15 @@ public class ApplicationController(IConfiguration configuration) : ControllerBas
2223
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
2324
public IActionResult GetApplicationInfo()
2425
{
26+
var assembly = Assembly.GetExecutingAssembly();
27+
var version = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.
28+
InformationalVersion ?? assembly.GetName().Version?.ToString() ?? "Unknown";
29+
2530
return Ok(new
2631
{
32+
Version = version,
2733
NutrientFeLicenseKey = _configuration.GetNonEmptyValue("NUTRIENT_FE_LICENSE_KEY"),
2834
Environment = _configuration.GetNonEmptyValue("ASPNETCORE_ENVIRONMENT"),
29-
// Include JASPER version at a later point
3035
});
3136
}
3237
}

api/api.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
<TargetFramework>net10.0</TargetFramework>
55
<RootNamespace>Scv.Api</RootNamespace>
66
<UserSecretsId>de959767-ede6-4f8a-b6b9-d36aed703396</UserSecretsId>
7+
<!-- Version information -->
8+
<VersionPrefix>1.0</VersionPrefix>
9+
<Version Condition="'$(BuildNumber)' != ''">$(VersionPrefix).$(BuildNumber)</Version>
10+
<Version Condition="'$(BuildNumber)' == ''">$(VersionPrefix).0</Version>
711
</PropertyGroup>
812
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
913
<DocumentationFile>bin\$(Configuration)\net10.0\api.xml</DocumentationFile>

docs/versioning.md

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
# JASPER Version Tracking
2+
3+
## Overview
4+
5+
JASPER uses automatic versioning based on GitHub Actions workflow run numbers to track releases and enable version change notifications. The version is exposed through the `/api/application/info` endpoint.
6+
7+
## How It Works
8+
9+
### Backend Implementation
10+
11+
1. **Version in api.csproj**: Major.Minor prefix + GitHub Actions run_number as patch
12+
13+
```xml
14+
<VersionPrefix>1.0</VersionPrefix>
15+
<Version Condition="'$(BuildNumber)' != ''">$(VersionPrefix).$(BuildNumber)</Version>
16+
<Version Condition="'$(BuildNumber)' == ''">$(VersionPrefix).0</Version>
17+
```
18+
19+
2. **GitHub Actions**: Injects the workflow run_number during build
20+
21+
```bash
22+
dotnet build /p:BuildNumber=${{ github.run_number }}
23+
```
24+
25+
3. **API Endpoint**: Returns version in `/api/application/info`
26+
```json
27+
{
28+
"version": "1.0.456",
29+
"nutrientFeLicenseKey": "...",
30+
"environment": "Production"
31+
}
32+
```
33+
34+
### Version Format
35+
36+
- **Format**: `{Major}.{Minor}.{RunNumber}`
37+
- **Example**: `1.0.456`
38+
- **Major.Minor**: Manually set in api.csproj (e.g., `1.0`, `1.1`, `2.0`)
39+
- **RunNumber**: Automatically incremented by GitHub Actions with each workflow run
40+
- **Local Development**: Defaults to `1.0.0` when BuildNumber is not set
41+
42+
## Frontend Integration
43+
44+
### Detecting New Releases
45+
46+
To notify users when a new version is deployed:
47+
48+
```typescript
49+
// Store the version when app loads
50+
let currentVersion: string | null = null;
51+
52+
// Fetch version on app initialization
53+
async function initializeVersion() {
54+
const response = await fetch('/api/application/info');
55+
const data = await response.json();
56+
currentVersion = data.version;
57+
}
58+
59+
// Poll periodically or on visibility change
60+
async function checkForNewVersion() {
61+
const response = await fetch('/api/application/info');
62+
const data = await response.json();
63+
64+
if (currentVersion && data.version !== currentVersion) {
65+
// Show notification to user
66+
showUpdateNotification(
67+
'A new version of JASPER is available. Please refresh.'
68+
);
69+
return true;
70+
}
71+
return false;
72+
}
73+
74+
// Example: Check every 5 minutes
75+
setInterval(checkForNewVersion, 5 * 60 * 1000);
76+
77+
// Example: Check when tab becomes visible
78+
document.addEventListener('visibilitychange', () => {
79+
if (!document.hidden) {
80+
checkForNewVersion();
81+
}
82+
});
83+
```
84+
85+
### Vue.js Example
86+
87+
```typescript
88+
// composables/useVersionCheck.ts
89+
import { ref, onMounted } from 'vue';
90+
91+
export function useVersionCheck() {
92+
const currentVersion = ref<string | null>(null);
93+
const hasNewVersion = ref(false);
94+
95+
async function fetchVersion(): Promise<string> {
96+
const response = await fetch('/api/application/info');
97+
const data = await response.json();
98+
return data.version;
99+
}
100+
101+
async function checkVersion() {
102+
const latestVersion = await fetchVersion();
103+
104+
if (!currentVersion.value) {
105+
currentVersion.value = latestVersion;
106+
} else if (currentVersion.value !== latestVersion) {
107+
hasNewVersion.value = true;
108+
}
109+
}
110+
111+
onMounted(async () => {
112+
await checkVersion();
113+
114+
// Check every 5 minutes
115+
setInterval(checkVersion, 5 * 60 * 1000);
116+
117+
// Check when tab becomes visible
118+
document.addEventListener('visibilitychange', () => {
119+
if (!document.hidden) checkVersion();
120+
});
121+
});
122+
123+
return {
124+
currentVersion,
125+
hasNewVersion,
126+
checkVersion,
127+
};
128+
}
129+
```
130+
131+
## Updating the Version
132+
133+
### For Minor/Patch Releases
134+
135+
No action needed - Git SHA automatically changes with each commit.
136+
137+
### For Major/Minor Releases
138+
139+
Update the semantic version in `api/api.csproj`:
140+
141+
```xml
142+
<PropertyGroup>
143+
<Version>1.1.0</Version> <!-- Update this -->
144+
<InformationalVersion>$(Version)+$(SourceRevisionId)</InformationalVersion>
145+
</PropertyGroup>
146+
```
147+
148+
### Using Git Tags (Optional)
149+
150+
## Updating the Version
151+
152+
### For Most Deployments
153+
154+
No action needed - the patch version (run_number) automatically increments with each GitHub Actions workflow run.
155+
156+
- **Run 100**: `1.0.100`
157+
- **Run 101**: `1.0.101`
158+
- **Run 102**: `1.0.102`
159+
160+
### For Major/Minor Releases
161+
162+
Update the version prefix in `api/api.csproj`:
163+
164+
```xml
165+
<PropertyGroup>
166+
<VersionPrefix>1.1</VersionPrefix> <!-- Changed from 1.0 to 1.1 -->
167+
<Version Condition="'$(BuildNumber)' != ''">$(VersionPrefix).$(BuildNumber)</Version>
168+
<Version Condition="'$(BuildNumber)' == ''">$(VersionPrefix).0</Version>
169+
</PropertyGroup>
170+
```
171+
172+
After updating, the next deployment will be `1.1.{next_run_number}`.
173+
174+
### Using Git Tags (Optional)
175+
176+
For tracking major releases, create Git tags:
177+
178+
```bash
179+
git tag v1.0.0
180+
git push origin v1.0.0
181+
```
182+
183+
## Benefits
184+
185+
1.**Automatic**: Version increments with every deployment
186+
2.**Unique**: Each deployment has a unique, sequential version number
187+
3.**Simple**: Easy to compare versions (1.0.101 > 1.0.100)
188+
4.**No Manual Work**: No need to remember to bump patch numbers
189+
5.**Frontend Detection**: Simple numeric comparison to detect new versions
190+
6.**Traceable**: Run number links directly to GitHub Actions workflow
191+
192+
## Testing Locally
193+
194+
When running locally, the version defaults to `{VersionPrefix}.0`:
195+
196+
- **Local**: `1.0.0`
197+
- **CI/CD Run 456**: `1.0.456`
198+
199+
To test with a specific build number locally:
200+
201+
```bash
202+
dotnet build /p:BuildNumber=999
203+
# Result: 1.0.999
204+
```
205+
206+
## Workflow Run Number
207+
208+
The GitHub Actions `run_number` is:
209+
210+
- Repository-specific sequential counter
211+
- Starts at 1 for each repository
212+
- Increments with each workflow run
213+
- Persists across branches
214+
- Never resets (unlike run_id which is random)
215+
216+
You can view the run number in GitHub Actions:
217+
`https://github.com/{org}/{repo}/actions/runs/{run_number}`

tests/api/Controllers/ApplicationControllerTests.cs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ private static ApplicationController CreateController(Dictionary<string, string>
3131
public void GetApplicationInfo_ThrowsConfigurationException_WhenNutrientFeLicenseKeyIsNull()
3232
{
3333
var expectedEnvironment = _faker.PickRandom("Development", "Staging");
34-
34+
3535
var configValues = new Dictionary<string, string>
3636
{
3737
// NUTRIENT_FE_LICENSE_KEY is intentionally missing
@@ -48,7 +48,7 @@ public void GetApplicationInfo_ThrowsConfigurationException_WhenNutrientFeLicens
4848
public void GetApplicationInfo_ThrowsConfigurationException_WhenNutrientFeLicenseKeyIsEmpty()
4949
{
5050
var expectedEnvironment = _faker.PickRandom("Development", "Staging");
51-
51+
5252
var configValues = new Dictionary<string, string>
5353
{
5454
["NUTRIENT_FE_LICENSE_KEY"] = string.Empty,
@@ -65,7 +65,7 @@ public void GetApplicationInfo_ThrowsConfigurationException_WhenNutrientFeLicens
6565
public void GetApplicationInfo_ThrowsConfigurationException_WhenEnvironmentIsNull()
6666
{
6767
var expectedLicenseKey = _faker.Random.AlphaNumeric(32);
68-
68+
6969
var configValues = new Dictionary<string, string>
7070
{
7171
["NUTRIENT_FE_LICENSE_KEY"] = expectedLicenseKey
@@ -82,7 +82,7 @@ public void GetApplicationInfo_ThrowsConfigurationException_WhenEnvironmentIsNul
8282
public void GetApplicationInfo_ThrowsConfigurationException_WhenEnvironmentIsEmpty()
8383
{
8484
var expectedLicenseKey = _faker.Random.AlphaNumeric(32);
85-
85+
8686
var configValues = new Dictionary<string, string>
8787
{
8888
["NUTRIENT_FE_LICENSE_KEY"] = expectedLicenseKey,
@@ -95,6 +95,31 @@ public void GetApplicationInfo_ThrowsConfigurationException_WhenEnvironmentIsEmp
9595
Assert.Equal("Configuration 'ASPNETCORE_ENVIRONMENT' is invalid or missing.", exception.Message);
9696
}
9797

98+
[Fact]
99+
public void GetApplicationInfo_ReturnsVersionInResponse()
100+
{
101+
var expectedLicenseKey = _faker.Random.AlphaNumeric(32);
102+
var expectedEnvironment = _faker.PickRandom("Development", "Staging", "Production");
103+
104+
var configValues = new Dictionary<string, string>
105+
{
106+
["NUTRIENT_FE_LICENSE_KEY"] = expectedLicenseKey,
107+
["ASPNETCORE_ENVIRONMENT"] = expectedEnvironment
108+
};
109+
110+
var controller = CreateController(configValues);
111+
var result = controller.GetApplicationInfo() as OkObjectResult;
112+
113+
Assert.NotNull(result);
114+
var value = result.Value;
115+
var versionProperty = value.GetType().GetProperty("Version");
116+
Assert.NotNull(versionProperty);
117+
118+
var version = versionProperty.GetValue(value) as string;
119+
Assert.NotNull(version);
120+
Assert.NotEmpty(version);
121+
}
122+
98123
#endregion
99124

100125
#region Controller Attribute Tests

0 commit comments

Comments
 (0)