feat: implement version display with commit hash and date
This commit is contained in:
@@ -2,6 +2,8 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
||||
import org.gradle.api.tasks.testing.logging.TestLogEvent.*
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
import java.time.Instant
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
plugins {
|
||||
java
|
||||
@@ -20,7 +22,7 @@ node {
|
||||
}
|
||||
|
||||
group = "com.example"
|
||||
version = "1.0.0-SNAPSHOT"
|
||||
version = "1.0.0-beta"
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
@@ -189,3 +191,37 @@ tasks.register("countCodeLines") {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("generateVersionFile") {
|
||||
doLast {
|
||||
// Версия из gradle.properties (по умолчанию 'unspecified', если не задана)
|
||||
val version = project.version.takeIf { it.toString() != "unspecified" }?.toString() ?: "0.0.0"
|
||||
|
||||
// Получение короткого хэша коммита (с обработкой ошибки, если git не доступен)
|
||||
val commitHash = try {
|
||||
providers.exec {
|
||||
commandLine("git", "rev-parse", "--short", "HEAD")
|
||||
}.standardOutput.asText.get().trim()
|
||||
} catch (e: Exception) {
|
||||
logger.warn("Не удалось получить хэш коммита: ${e.message}")
|
||||
"unknown"
|
||||
}
|
||||
|
||||
val buildTime = DateTimeFormatter.ISO_INSTANT.format(Instant.now())
|
||||
|
||||
val propertiesContent = """
|
||||
version=$version
|
||||
commit.hash=$commitHash
|
||||
build.time=$buildTime
|
||||
""".trimIndent()
|
||||
|
||||
val resourceDir = file("src/main/resources")
|
||||
resourceDir.mkdirs()
|
||||
file("$resourceDir/version.properties").writeText(propertiesContent)
|
||||
|
||||
logger.lifecycle("✅ Файл version.properties создан: версия=$version, коммит=$commitHash")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.processResources {
|
||||
dependsOn("generateVersionFile")
|
||||
}
|
||||
|
||||
@@ -21,10 +21,12 @@
|
||||
</style>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { watch } from 'vue'
|
||||
import { watch, onMounted } from 'vue'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
import { useVersionStore } from '@/stores/version'
|
||||
|
||||
const settings = useSettingsStore()
|
||||
const versionStore = useVersionStore()
|
||||
|
||||
watch(() => settings.siteDescription, (desc) => {
|
||||
let meta = document.querySelector('meta[name="description"]')
|
||||
@@ -35,4 +37,7 @@ watch(() => settings.siteDescription, (desc) => {
|
||||
}
|
||||
meta.setAttribute('content', desc || '')
|
||||
}, { immediate: true })
|
||||
onMounted(() => {
|
||||
versionStore.fetchVersion()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -167,6 +167,16 @@
|
||||
{{ userInitials }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Версия сборки (всегда внизу) -->
|
||||
<div v-if="!sidebarCollapsed" class="px-4 py-3 border-t border-gray-200 text-xs text-gray-500">
|
||||
{{ versionStore.getFormattedVersion(t) }}
|
||||
</div>
|
||||
<div v-else class="p-2 border-t border-gray-200 flex justify-center">
|
||||
<div class="text-xs text-gray-500 font-mono" :title="versionStore.getFormattedVersion(t)">
|
||||
{{ versionStore.version?.commitHash?.slice(0, 6) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -256,9 +266,11 @@ import { useSettingsStore } from '@/stores/settings'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useNotification } from '@/composables/useNotification'
|
||||
import { useVersionStore } from '@/stores/version'
|
||||
|
||||
const { notification, showNotification } = useNotification()
|
||||
const settings = useSettingsStore()
|
||||
const versionStore = useVersionStore()
|
||||
const userStore = useUserStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
@@ -46,7 +46,9 @@
|
||||
"deleteConfirmation": "Are you sure you want to delete this item? This action cannot be undone.",
|
||||
"operationSuccess": "Operation completed successfully",
|
||||
"operationFailed": "Operation failed",
|
||||
"networkError": "Network error"
|
||||
"networkError": "Network error",
|
||||
"version": "Version",
|
||||
"versionFrom": "from"
|
||||
},
|
||||
"dashboard": {
|
||||
"totalUsers": "Total Users",
|
||||
|
||||
@@ -46,7 +46,9 @@
|
||||
"deleteConfirmation": "Вы уверены, что хотите удалить этот элемент? Это действие необратимо.",
|
||||
"operationSuccess": "Операция выполнена успешно",
|
||||
"operationFailed": "Операция не удалась",
|
||||
"networkError": "Ошибка сети"
|
||||
"networkError": "Ошибка сети",
|
||||
"version": "Версия",
|
||||
"versionFrom": "от"
|
||||
},
|
||||
"dashboard": {
|
||||
"totalUsers": "Всего пользователей",
|
||||
|
||||
55
frontend/src/stores/version.ts
Normal file
55
frontend/src/stores/version.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
export interface BuildVersion {
|
||||
version: string
|
||||
commitHash: string
|
||||
buildTime: string // ISO строка, например "2025-04-03T12:34:56Z"
|
||||
}
|
||||
|
||||
export const useVersionStore = defineStore('version', () => {
|
||||
const version = ref<BuildVersion | null>(null)
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
async function fetchVersion() {
|
||||
if (version.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetch('/api/build-info')
|
||||
if (!res.ok) throw new Error('Failed to fetch version')
|
||||
const data = await res.json()
|
||||
version.value = {
|
||||
version: data.version || '0.0.0',
|
||||
commitHash: data.commitHash || 'unknown',
|
||||
buildTime: data.buildTime || ''
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(err)
|
||||
error.value = err.message
|
||||
version.value = { version: 'dev', commitHash: 'unknown', buildTime: '' }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Отформатированная дата сборки (только дата, без времени)
|
||||
const buildDateFormatted = computed(() => {
|
||||
if (!version.value?.buildTime) return ''
|
||||
const date = new Date(version.value.buildTime)
|
||||
if (isNaN(date.getTime())) return ''
|
||||
// Формат YYYY-MM-DD (универсальный, без локализации)
|
||||
return date.toISOString().split('T')[0]
|
||||
})
|
||||
|
||||
// Полная строка версии: "Версия: 1.2.3 (build abc1234 от 2025-04-03)"
|
||||
// Принимает функцию перевода для слова "от"/"from"
|
||||
const getFormattedVersion = (t: (key: string) => string) => {
|
||||
if (!version.value) return t('common.version') + ': ...'
|
||||
const { version: ver, commitHash } = version.value
|
||||
const datePart = buildDateFormatted.value ? ` ${t('common.versionFrom')} ${buildDateFormatted.value}` : ''
|
||||
return `${t('common.version')}: ${ver} (build ${commitHash}${datePart})`
|
||||
}
|
||||
|
||||
return { version, loading, error, fetchVersion, buildDateFormatted, getFormattedVersion }
|
||||
})
|
||||
@@ -96,6 +96,12 @@
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<!-- Блок версии внизу -->
|
||||
<div class="mt-6 text-center text-xs text-gray-500">
|
||||
{{ versionStore.getFormattedVersion(t) }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -105,12 +111,14 @@ import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useVersionStore } from '@/stores/version'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const settings = useSettingsStore()
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
const { t, locale } = useI18n()
|
||||
const versionStore = useVersionStore()
|
||||
const form = ref({ login: '', password: '' })
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
@@ -35,6 +35,12 @@
|
||||
{{ t('register.alreadyHaveAccount') }} <router-link to="/login" class="text-primary-600">{{ t('login.signin') }}</router-link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Блок версии внизу -->
|
||||
<div class="mt-6 text-center text-xs text-gray-500">
|
||||
{{ versionStore.getFormattedVersion(t) }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -42,7 +48,9 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useVersionStore } from '@/stores/version'
|
||||
const { t } = useI18n()
|
||||
const versionStore = useVersionStore()
|
||||
const form = ref({ login: '', email: '', password: '' })
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
51
src/main/java/su/xserver/iikocon/BuildVersionProvider.java
Normal file
51
src/main/java/su/xserver/iikocon/BuildVersionProvider.java
Normal file
@@ -0,0 +1,51 @@
|
||||
package su.xserver.iikocon;
|
||||
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Properties;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class BuildVersionProvider {
|
||||
private static final Logger LOG = Logger.getLogger(BuildVersionProvider.class.getName());
|
||||
|
||||
private JsonObject cachedVersion;
|
||||
|
||||
public BuildVersionProvider() {
|
||||
this.cachedVersion = loadVersionData();
|
||||
}
|
||||
|
||||
private JsonObject loadVersionData() {
|
||||
Properties props = new Properties();
|
||||
try (InputStream is = getClass().getClassLoader().getResourceAsStream("version.properties")) {
|
||||
if (is == null) {
|
||||
LOG.warning("version.properties not found in classpath");
|
||||
return createFallbackVersion();
|
||||
}
|
||||
props.load(is);
|
||||
} catch (IOException e) {
|
||||
LOG.severe("Failed to read version.properties: " + e.getMessage());
|
||||
return createFallbackVersion();
|
||||
}
|
||||
|
||||
String version = props.getProperty("version", "0.0.0");
|
||||
String commitHash = props.getProperty("commit.hash", "unknown");
|
||||
String buildTime = props.getProperty("build.time", "");
|
||||
|
||||
return new JsonObject()
|
||||
.put("version", version)
|
||||
.put("commitHash", commitHash)
|
||||
.put("buildTime", buildTime);
|
||||
}
|
||||
|
||||
private JsonObject createFallbackVersion() {
|
||||
return new JsonObject()
|
||||
.put("version", "0.0.0-dev")
|
||||
.put("commitHash", "unknown")
|
||||
.put("buildTime", "1970-01-01T00:00:00Z");
|
||||
}
|
||||
|
||||
public JsonObject getVersion() {
|
||||
return cachedVersion;
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,8 @@ import su.xserver.iikocon.iiko.IikoOlapClient;
|
||||
import su.xserver.iikocon.iiko.OlapQueryService;
|
||||
import su.xserver.iikocon.service.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -39,6 +41,7 @@ public class MainVerticle extends AbstractVerticle {
|
||||
|
||||
private final Logger log = LoggerFactory.getLogger("[MainVerticle]");
|
||||
|
||||
private BuildVersionProvider versionProvider;
|
||||
private DataBaseService db;
|
||||
private RedisService redis;
|
||||
private HttpServer httpServer;
|
||||
@@ -52,11 +55,13 @@ public class MainVerticle extends AbstractVerticle {
|
||||
private OlapQueryService olapQueryService;
|
||||
|
||||
@Override
|
||||
public void start(Promise<Void> startPromise) throws ClassNotFoundException {
|
||||
public void start(Promise<Void> startPromise) throws ClassNotFoundException, IOException {
|
||||
|
||||
Class.forName("com.mysql.cj.jdbc.Driver");
|
||||
Class.forName("org.postgresql.Driver");
|
||||
|
||||
versionProvider = new BuildVersionProvider();
|
||||
|
||||
ConfigStoreOptions classpathStore = new ConfigStoreOptions()
|
||||
.setType("file")
|
||||
.setFormat("json")
|
||||
@@ -276,6 +281,13 @@ public class MainVerticle extends AbstractVerticle {
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/api/build-info").handler(rc -> {
|
||||
JsonObject version = versionProvider.getVersion();
|
||||
rc.response()
|
||||
.putHeader("Content-Type", "application/json")
|
||||
.end(version.encode());
|
||||
});
|
||||
|
||||
// Rate Limiter Handler
|
||||
RedisRateLimiter limiter = new RedisRateLimiter(
|
||||
redis.getRedis(), 60, 60_000
|
||||
|
||||
3
src/main/resources/version.properties
Normal file
3
src/main/resources/version.properties
Normal file
@@ -0,0 +1,3 @@
|
||||
version=1.0.0-beta
|
||||
commit.hash=debf1b1
|
||||
build.time=2026-05-09T11:03:39.671956300Z
|
||||
Reference in New Issue
Block a user