✅ Sistema de Actualización PWA Implementado (v1.0.87)
Frontend (v1.0.87) Service Worker (public/service-worker.js) ✅ Cache versionado dinámico: ayutec-v1.0.87 ✅ Estrategia Network-First con fallback a cache ✅ Auto-limpieza de caches antiguos en activación ✅ Skip waiting para activación inmediata ✅ Soporte para mensaje SKIP_WAITING desde cliente Detección de Actualizaciones (App.jsx) ✅ Registro automático de Service Worker ✅ Listener de updatefound para detectar nuevas versiones ✅ Listener de controllerchange para recarga automática ✅ Estado updateAvailable y waitingWorker Modal de Actualización ✅ Diseño grande y llamativo con animación bounce ✅ Overlay bloqueante (z-index 9999, no se puede cerrar) ✅ Botón enorme: "🚀 ACTUALIZAR AHORA" ✅ Gradiente indigo/purple, responsive ✅ Texto claro: "Nueva versión disponible" ✅ Recarga automática al actualizar PWA Manifest (site.webmanifest) ✅ Agregado start_url y scope ✅ Configurado orientation: portrait ✅ Display standalone para app nativa HTML Metatags (index.html) ✅ theme-color para barra de navegación ✅ apple-mobile-web-app-capable para iOS ✅ mobile-web-app-capable para Android ✅ Viewport con user-scalable=no para PWA Automatización ✅ Script PowerShell update-version.ps1: Incrementa versión automáticamente (patch) Actualiza package.json Actualiza service-worker.js Sincroniza ambos archivos ✅ Guía completa PWA-UPDATE-GUIDE.md Flujo de Actualización Desarrollador ejecuta update-version.ps1 Build y deploy de nueva versión Usuario abre la app Service Worker detecta nueva versión Modal aparece automáticamente bloqueando UI Usuario presiona "ACTUALIZAR AHORA" Service Worker se activa Página se recarga automáticamente Usuario usa nueva versión Backend (v1.0.84) Sin cambios Ahora la PWA se actualiza automáticamente mostrando un modal imposible de ignorar! 🚀📱
This commit is contained in:
168
frontend/PWA-UPDATE-GUIDE.md
Normal file
168
frontend/PWA-UPDATE-GUIDE.md
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
# Sistema de Actualización PWA - AYUTEC
|
||||||
|
|
||||||
|
## 🚀 Características
|
||||||
|
|
||||||
|
- ✅ **Detección automática** de nuevas versiones
|
||||||
|
- ✅ **Modal de actualización** grande y visible
|
||||||
|
- ✅ **Service Worker** con estrategia Network-First
|
||||||
|
- ✅ **Cache inteligente** para funcionamiento offline
|
||||||
|
- ✅ **Actualización forzada** al usuario cuando hay nueva versión
|
||||||
|
|
||||||
|
## 📱 Instalación como PWA
|
||||||
|
|
||||||
|
### En Android/iOS:
|
||||||
|
1. Abre la app en Chrome/Safari
|
||||||
|
2. Toca el menú (⋮)
|
||||||
|
3. Selecciona "Agregar a pantalla de inicio"
|
||||||
|
4. Confirma la instalación
|
||||||
|
|
||||||
|
### En Desktop:
|
||||||
|
1. Abre la app en Chrome/Edge
|
||||||
|
2. Haz clic en el ícono de instalación (➕) en la barra de direcciones
|
||||||
|
3. Confirma "Instalar"
|
||||||
|
|
||||||
|
## 🔄 Proceso de Actualización
|
||||||
|
|
||||||
|
### Para el Usuario:
|
||||||
|
1. Cuando hay una actualización, aparece automáticamente un **modal grande**
|
||||||
|
2. El modal muestra: "¡Nueva Actualización!"
|
||||||
|
3. Botón grande: **"🚀 ACTUALIZAR AHORA"**
|
||||||
|
4. Al presionar, la app se recarga con la nueva versión
|
||||||
|
|
||||||
|
### Para el Desarrollador:
|
||||||
|
|
||||||
|
#### Opción 1: Script Automático (Recomendado)
|
||||||
|
```powershell
|
||||||
|
cd frontend
|
||||||
|
.\update-version.ps1
|
||||||
|
```
|
||||||
|
Este script:
|
||||||
|
- Incrementa automáticamente la versión patch (1.0.87 → 1.0.88)
|
||||||
|
- Actualiza `package.json`
|
||||||
|
- Actualiza `public/service-worker.js`
|
||||||
|
|
||||||
|
#### Opción 2: Manual
|
||||||
|
1. **Actualizar `package.json`:**
|
||||||
|
```json
|
||||||
|
"version": "1.0.88" // Incrementar número
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Actualizar `public/service-worker.js`:**
|
||||||
|
```javascript
|
||||||
|
const CACHE_NAME = 'ayutec-v1.0.88'; // Mismo número
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Hacer build y deploy:**
|
||||||
|
```powershell
|
||||||
|
npm run build
|
||||||
|
docker build -t tu-registry/checklist-frontend:latest .
|
||||||
|
docker push tu-registry/checklist-frontend:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Cómo Funciona
|
||||||
|
|
||||||
|
### 1. Service Worker
|
||||||
|
- Registrado en `App.jsx`
|
||||||
|
- Cache con nombre versionado: `ayutec-v1.0.87`
|
||||||
|
- Estrategia: **Network First, Cache Fallback**
|
||||||
|
- Al cambiar la versión, se crea nuevo cache
|
||||||
|
|
||||||
|
### 2. Detección de Actualización
|
||||||
|
```javascript
|
||||||
|
// En App.jsx
|
||||||
|
registration.addEventListener('updatefound', () => {
|
||||||
|
// Nueva versión detectada
|
||||||
|
setUpdateAvailable(true)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Modal de Actualización
|
||||||
|
- Overlay negro semi-transparente (z-index: 9999)
|
||||||
|
- Modal animado con bounce
|
||||||
|
- Botón grande con gradiente
|
||||||
|
- **No se puede cerrar** - obliga a actualizar
|
||||||
|
|
||||||
|
### 4. Aplicación de Actualización
|
||||||
|
```javascript
|
||||||
|
waitingWorker.postMessage({ type: 'SKIP_WAITING' });
|
||||||
|
// Activa el nuevo service worker
|
||||||
|
// Recarga la página automáticamente
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Versionado
|
||||||
|
|
||||||
|
Seguimos **Semantic Versioning**:
|
||||||
|
- **MAJOR**: Cambios incompatibles (1.0.0 → 2.0.0)
|
||||||
|
- **MINOR**: Nueva funcionalidad compatible (1.0.0 → 1.1.0)
|
||||||
|
- **PATCH**: Correcciones de bugs (1.0.0 → 1.0.1)
|
||||||
|
|
||||||
|
El script `update-version.ps1` incrementa automáticamente **PATCH**.
|
||||||
|
|
||||||
|
## 🧪 Probar Localmente
|
||||||
|
|
||||||
|
1. **Compilar en modo producción:**
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
npm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Simular actualización:**
|
||||||
|
- Abre la app en navegador
|
||||||
|
- Incrementa versión en `service-worker.js`
|
||||||
|
- Recarga la página (Ctrl+F5)
|
||||||
|
- Debe aparecer el modal de actualización
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### El modal no aparece
|
||||||
|
- Verifica que el service worker esté registrado (F12 → Application → Service Workers)
|
||||||
|
- Asegúrate de cambiar el `CACHE_NAME` en `service-worker.js`
|
||||||
|
- Desregistra el SW antiguo: `Application → Service Workers → Unregister`
|
||||||
|
|
||||||
|
### La app no se actualiza
|
||||||
|
- Fuerza actualización: Ctrl+Shift+R (hard reload)
|
||||||
|
- Limpia cache del navegador
|
||||||
|
- Verifica que la nueva versión esté deployada
|
||||||
|
|
||||||
|
### PWA no se instala
|
||||||
|
- Verifica que `site.webmanifest` esté accesible
|
||||||
|
- Requiere HTTPS (excepto localhost)
|
||||||
|
- Verifica íconos en `/public/`
|
||||||
|
|
||||||
|
## 📝 Checklist de Deploy
|
||||||
|
|
||||||
|
- [ ] Incrementar versión con `update-version.ps1`
|
||||||
|
- [ ] Verificar que ambos archivos tengan la misma versión
|
||||||
|
- [ ] Hacer commit: `git commit -m "chore: bump version to X.X.X"`
|
||||||
|
- [ ] Build de producción: `npm run build`
|
||||||
|
- [ ] Build de Docker: `docker build -t frontend:vX.X.X .`
|
||||||
|
- [ ] Push a registry
|
||||||
|
- [ ] Deploy en servidor
|
||||||
|
- [ ] Verificar que usuarios vean el modal de actualización
|
||||||
|
|
||||||
|
## 🎯 Mejores Prácticas
|
||||||
|
|
||||||
|
1. **Siempre** incrementar versión antes de deploy
|
||||||
|
2. **Nunca** reutilizar números de versión
|
||||||
|
3. **Probar** localmente antes de deploy
|
||||||
|
4. **Documentar** cambios en commit message
|
||||||
|
5. **Notificar** a usuarios si es actualización crítica
|
||||||
|
|
||||||
|
## 🔐 Seguridad
|
||||||
|
|
||||||
|
- Service Worker solo funciona en HTTPS
|
||||||
|
- Manifest require `start_url` y `scope` correctos
|
||||||
|
- Cache no almacena datos sensibles (solo assets estáticos)
|
||||||
|
|
||||||
|
## 📱 Compatibilidad
|
||||||
|
|
||||||
|
- ✅ Chrome/Edge (Desktop y Mobile)
|
||||||
|
- ✅ Safari (iOS 11.3+)
|
||||||
|
- ✅ Firefox (Desktop y Mobile)
|
||||||
|
- ✅ Samsung Internet
|
||||||
|
- ⚠️ IE11 no soportado
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Versión actual:** 1.0.87
|
||||||
|
**Última actualización:** 2025-11-30
|
||||||
@@ -7,7 +7,11 @@
|
|||||||
<link rel="shortcut icon" href="/favicon.ico" />
|
<link rel="shortcut icon" href="/favicon.ico" />
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
<link rel="manifest" href="/site.webmanifest" />
|
<link rel="manifest" href="/site.webmanifest" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||||
|
<meta name="theme-color" content="#4f46e5" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
<title>AYUTEC - Sistema Inteligente de Inspecciones</title>
|
<title>AYUTEC - Sistema Inteligente de Inspecciones</title>
|
||||||
<meta name="description" content="AYUTEC: Sistema avanzado de inspecciones vehiculares con inteligencia artificial" />
|
<meta name="description" content="AYUTEC: Sistema avanzado de inspecciones vehiculares con inteligencia artificial" />
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "checklist-frontend",
|
"name": "checklist-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.86",
|
"version": "1.0.87",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
66
frontend/public/service-worker.js
Normal file
66
frontend/public/service-worker.js
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
// Service Worker para PWA con detección de actualizaciones
|
||||||
|
// IMPORTANTE: Actualizar esta versión cada vez que se despliegue una nueva versión
|
||||||
|
const CACHE_NAME = 'ayutec-v1.0.87';
|
||||||
|
const urlsToCache = [
|
||||||
|
'/',
|
||||||
|
'/index.html'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Instalación del service worker
|
||||||
|
self.addEventListener('install', (event) => {
|
||||||
|
console.log('Service Worker: Installing version', CACHE_NAME);
|
||||||
|
event.waitUntil(
|
||||||
|
caches.open(CACHE_NAME)
|
||||||
|
.then((cache) => {
|
||||||
|
console.log('Service Worker: Caching files');
|
||||||
|
return cache.addAll(urlsToCache);
|
||||||
|
})
|
||||||
|
.then(() => self.skipWaiting()) // Forzar activación inmediata
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Activación del service worker
|
||||||
|
self.addEventListener('activate', (event) => {
|
||||||
|
console.log('Service Worker: Activating...');
|
||||||
|
event.waitUntil(
|
||||||
|
caches.keys().then((cacheNames) => {
|
||||||
|
return Promise.all(
|
||||||
|
cacheNames.map((cacheName) => {
|
||||||
|
if (cacheName !== CACHE_NAME) {
|
||||||
|
console.log('Service Worker: Deleting old cache:', cacheName);
|
||||||
|
return caches.delete(cacheName);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}).then(() => self.clients.claim()) // Tomar control de todas las páginas
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Estrategia: Network First, fallback to Cache
|
||||||
|
self.addEventListener('fetch', (event) => {
|
||||||
|
event.respondWith(
|
||||||
|
fetch(event.request)
|
||||||
|
.then((response) => {
|
||||||
|
// Clone la respuesta
|
||||||
|
const responseToCache = response.clone();
|
||||||
|
|
||||||
|
// Actualizar cache con la nueva respuesta
|
||||||
|
caches.open(CACHE_NAME).then((cache) => {
|
||||||
|
cache.put(event.request, responseToCache);
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Si falla la red, usar cache
|
||||||
|
return caches.match(event.request);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mensaje para notificar actualización
|
||||||
|
self.addEventListener('message', (event) => {
|
||||||
|
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||||
|
self.skipWaiting();
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "AYUTEC - Sistema de Inspecciones",
|
"name": "AYUTEC - Sistema de Inspecciones",
|
||||||
"short_name": "AYUTEC",
|
"short_name": "AYUTEC",
|
||||||
|
"start_url": "/",
|
||||||
|
"scope": "/",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/web-app-manifest-192x192.png",
|
"src": "/web-app-manifest-192x192.png",
|
||||||
@@ -17,5 +19,6 @@
|
|||||||
],
|
],
|
||||||
"theme_color": "#4f46e5",
|
"theme_color": "#4f46e5",
|
||||||
"background_color": "#ffffff",
|
"background_color": "#ffffff",
|
||||||
"display": "standalone"
|
"display": "standalone",
|
||||||
|
"orientation": "portrait"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,56 @@ import QuestionAnswerInput from './QuestionAnswerInput'
|
|||||||
function App() {
|
function App() {
|
||||||
const [user, setUser] = useState(null)
|
const [user, setUser] = useState(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [updateAvailable, setUpdateAvailable] = useState(false)
|
||||||
|
const [waitingWorker, setWaitingWorker] = useState(null)
|
||||||
|
|
||||||
|
// Detectar actualizaciones del Service Worker
|
||||||
|
useEffect(() => {
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
// Registrar service worker
|
||||||
|
navigator.serviceWorker.register('/service-worker.js')
|
||||||
|
.then((registration) => {
|
||||||
|
console.log('✅ Service Worker registrado:', registration);
|
||||||
|
|
||||||
|
// Verificar si hay actualización esperando
|
||||||
|
if (registration.waiting) {
|
||||||
|
setWaitingWorker(registration.waiting);
|
||||||
|
setUpdateAvailable(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detectar cuando hay nueva versión instalándose
|
||||||
|
registration.addEventListener('updatefound', () => {
|
||||||
|
const newWorker = registration.installing;
|
||||||
|
console.log('🔄 Nueva versión detectada, instalando...');
|
||||||
|
|
||||||
|
newWorker.addEventListener('statechange', () => {
|
||||||
|
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
||||||
|
// Hay nueva versión disponible
|
||||||
|
console.log('✨ Nueva versión lista!');
|
||||||
|
setWaitingWorker(newWorker);
|
||||||
|
setUpdateAvailable(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('❌ Error al registrar Service Worker:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Escuchar cambios de controlador (cuando se activa nueva versión)
|
||||||
|
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||||
|
console.log('🔄 Controlador cambiado, recargando página...');
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Función para actualizar la app
|
||||||
|
const handleUpdate = () => {
|
||||||
|
if (waitingWorker) {
|
||||||
|
waitingWorker.postMessage({ type: 'SKIP_WAITING' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Verificar si hay token guardado
|
// Verificar si hay token guardado
|
||||||
@@ -31,6 +81,38 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
{/* Modal de actualización disponible */}
|
||||||
|
{updateAvailable && (
|
||||||
|
<div className="fixed inset-0 bg-black/80 flex items-center justify-center z-[9999] p-4">
|
||||||
|
<div className="bg-white rounded-2xl max-w-md w-full p-6 sm:p-8 shadow-2xl animate-bounce">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="mb-4 flex justify-center">
|
||||||
|
<div className="w-20 h-20 bg-gradient-to-r from-green-500 to-emerald-500 rounded-full flex items-center justify-center">
|
||||||
|
<span className="text-4xl">🔄</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl sm:text-3xl font-bold text-gray-900 mb-3">
|
||||||
|
¡Nueva Actualización!
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-600 mb-6 text-sm sm:text-base">
|
||||||
|
Hay una nueva versión disponible con mejoras y correcciones.
|
||||||
|
<br />
|
||||||
|
<strong className="text-indigo-600">Por favor actualiza para continuar.</strong>
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={handleUpdate}
|
||||||
|
className="w-full py-4 px-6 bg-gradient-to-r from-indigo-600 to-purple-600 text-white text-lg sm:text-xl font-bold rounded-xl hover:from-indigo-700 hover:to-purple-700 transition-all transform hover:scale-105 shadow-lg"
|
||||||
|
>
|
||||||
|
🚀 ACTUALIZAR AHORA
|
||||||
|
</button>
|
||||||
|
<p className="text-xs text-gray-400 mt-4">
|
||||||
|
La página se recargará automáticamente
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{!user ? (
|
{!user ? (
|
||||||
<LoginPage setUser={setUser} />
|
<LoginPage setUser={setUser} />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
41
frontend/update-version.ps1
Normal file
41
frontend/update-version.ps1
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Script para actualizar la versión del frontend y service worker automáticamente
|
||||||
|
|
||||||
|
Write-Host "🔄 Actualizando versión del frontend..." -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# Leer package.json
|
||||||
|
$packageJson = Get-Content "package.json" -Raw | ConvertFrom-Json
|
||||||
|
$currentVersion = $packageJson.version
|
||||||
|
Write-Host "📦 Versión actual: $currentVersion" -ForegroundColor Yellow
|
||||||
|
|
||||||
|
# Separar versión en partes (major.minor.patch)
|
||||||
|
$versionParts = $currentVersion -split '\.'
|
||||||
|
$major = [int]$versionParts[0]
|
||||||
|
$minor = [int]$versionParts[1]
|
||||||
|
$patch = [int]$versionParts[2]
|
||||||
|
|
||||||
|
# Incrementar patch
|
||||||
|
$patch++
|
||||||
|
$newVersion = "$major.$minor.$patch"
|
||||||
|
|
||||||
|
Write-Host "✨ Nueva versión: $newVersion" -ForegroundColor Green
|
||||||
|
|
||||||
|
# Actualizar package.json
|
||||||
|
$packageJsonContent = Get-Content "package.json" -Raw
|
||||||
|
$packageJsonContent = $packageJsonContent -replace """version"": ""$currentVersion""", """version"": ""$newVersion"""
|
||||||
|
Set-Content "package.json" -Value $packageJsonContent -NoNewline
|
||||||
|
|
||||||
|
Write-Host "✅ package.json actualizado" -ForegroundColor Green
|
||||||
|
|
||||||
|
# Actualizar service-worker.js
|
||||||
|
$swPath = "public\service-worker.js"
|
||||||
|
$swContent = Get-Content $swPath -Raw
|
||||||
|
$swContent = $swContent -replace "ayutec-v$currentVersion", "ayutec-v$newVersion"
|
||||||
|
Set-Content $swPath -Value $swContent -NoNewline
|
||||||
|
|
||||||
|
Write-Host "✅ service-worker.js actualizado" -ForegroundColor Green
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "🎉 Versión actualizada exitosamente a: $newVersion" -ForegroundColor Magenta
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "📝 Recuerda hacer commit de los cambios:" -ForegroundColor Yellow
|
||||||
|
Write-Host " git add package.json public/service-worker.js" -ForegroundColor Gray
|
||||||
|
Write-Host " git commit -m 'chore: bump version to $newVersion'" -ForegroundColor Gray
|
||||||
Reference in New Issue
Block a user