Template Versioning System
Learn how to manage, version, and evolve GISE templates to ensure consistency and maintainability across projects while supporting continuous improvement.
Overview
The GISE template versioning system provides a structured approach to managing template evolution, ensuring backward compatibility, and facilitating seamless upgrades across projects.
Versioning Strategy
Semantic Versioning for Templates
Version Number Format: MAJOR.MINOR.PATCH
- MAJOR: Breaking changes that require manual migration
- MINOR: New features that are backward compatible
- PATCH: Bug fixes and minor improvements
Template Categories and Versioning
| Template Category | Versioning Frequency | Stability Level |
|---|---|---|
| Core Templates | Conservative (quarterly) | High |
| Framework Templates | Regular (monthly) | Medium |
| Experimental Templates | Frequent (weekly) | Low |
| Community Templates | Variable | Variable |
Template Metadata Structure
Template Manifest
// template.json
{
"name": "nextjs-typescript-starter",
"version": "2.1.0",
"description": "Next.js TypeScript starter with GISE best practices",
"category": "frontend",
"stability": "stable",
"giseVersion": ">=1.0.0",
"author": {
"name": "GISE Team",
"email": "templates@gise.dev"
},
"repository": {
"type": "git",
"url": "https://github.com/gise-templates/nextjs-typescript-starter"
},
"license": "MIT",
"keywords": ["nextjs", "typescript", "react", "frontend"],
"dependencies": {
"node": ">=18.0.0",
"npm": ">=8.0.0"
},
"features": [
"typescript",
"eslint",
"prettier",
"jest",
"tailwindcss",
"docker"
],
"changelog": "CHANGELOG.md",
"migration": {
"from": ["2.0.x"],
"guide": "docs/migration/v2.0-to-v2.1.md"
},
"compatibility": {
"breaking": false,
"deprecated": [],
"removed": []
},
"customization": {
"variables": "template.vars.json",
"prompts": "template.prompts.json"
}
}
Template Variables Configuration
// template.vars.json
{
"variables": {
"projectName": {
"type": "string",
"description": "Project name",
"default": "my-gise-project",
"validation": "^[a-z][a-z0-9-]*$"
},
"description": {
"type": "string",
"description": "Project description",
"default": "A GISE project"
},
"author": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Author name"
},
"email": {
"type": "string",
"description": "Author email",
"validation": "^[^@]+@[^@]+\\.[^@]+$"
}
}
},
"features": {
"type": "array",
"description": "Features to include",
"items": {
"type": "string",
"enum": ["auth", "database", "api", "testing", "docker"]
},
"default": ["api", "testing"]
},
"database": {
"type": "string",
"description": "Database type",
"enum": ["postgresql", "mysql", "sqlite", "mongodb"],
"default": "postgresql",
"condition": "features.includes('database')"
}
},
"computed": {
"packageName": "projectName.toLowerCase().replace(/[^a-z0-9-]/g, '-')",
"className": "projectName.replace(/-([a-z])/g, (g) => g[1].toUpperCase()).replace(/^[a-z]/, (g) => g.toUpperCase())"
}
}
Template Prompts Configuration
// template.prompts.json
{
"prompts": [
{
"name": "projectName",
"message": "What is your project name?",
"type": "input",
"validate": "^[a-z][a-z0-9-]*$",
"invalidMessage": "Project name must start with a letter and contain only lowercase letters, numbers, and hyphens"
},
{
"name": "description",
"message": "Describe your project:",
"type": "input"
},
{
"name": "features",
"message": "Select features to include:",
"type": "multiselect",
"choices": [
{ "name": "Authentication", "value": "auth" },
{ "name": "Database Integration", "value": "database" },
{ "name": "REST API", "value": "api" },
{ "name": "Testing Setup", "value": "testing" },
{ "name": "Docker Configuration", "value": "docker" }
]
},
{
"name": "database",
"message": "Choose database type:",
"type": "select",
"choices": [
{ "name": "PostgreSQL", "value": "postgresql" },
{ "name": "MySQL", "value": "mysql" },
{ "name": "SQLite", "value": "sqlite" },
{ "name": "MongoDB", "value": "mongodb" }
],
"when": "features.includes('database')"
}
]
}
Template Registry System
Registry Structure
// src/registry/template-registry.ts
export interface TemplateRegistry {
templates: TemplateEntry[];
categories: TemplateCategory[];
tags: string[];
lastUpdated: string;
}
export interface TemplateEntry {
id: string;
name: string;
version: string;
description: string;
category: string;
stability: 'experimental' | 'beta' | 'stable' | 'deprecated';
downloadUrl: string;
documentationUrl: string;
repositoryUrl: string;
author: TemplateAuthor;
license: string;
tags: string[];
features: string[];
dependencies: TemplateDependencies;
compatibility: TemplateCompatibility;
stats: TemplateStats;
createdAt: string;
updatedAt: string;
}
export interface TemplateCategory {
id: string;
name: string;
description: string;
icon: string;
templates: string[];
}
export interface TemplateAuthor {
name: string;
email?: string;
url?: string;
organization?: string;
}
export interface TemplateDependencies {
gise: string;
node?: string;
npm?: string;
docker?: string;
[key: string]: string | undefined;
}
export interface TemplateCompatibility {
giseVersions: string[];
nodeVersions: string[];
platforms: string[];
breaking: boolean;
deprecated: string[];
removed: string[];
}
export interface TemplateStats {
downloads: number;
stars: number;
forks: number;
issues: number;
lastDownload: string;
}
Registry Management
// src/registry/registry-manager.ts
export class TemplateRegistryManager {
private registry: TemplateRegistry;
private cacheTimeout = 3600000; // 1 hour
private lastFetch = 0;
constructor(private registryUrl: string) {
this.registry = { templates: [], categories: [], tags: [], lastUpdated: '' };
}
async getTemplates(filters?: TemplateFilters): Promise<TemplateEntry[]> {
await this.ensureRegistryLoaded();
let templates = this.registry.templates;
if (filters) {
templates = this.applyFilters(templates, filters);
}
return templates.sort(this.sortTemplates);
}
async getTemplate(id: string, version?: string): Promise<TemplateEntry | null> {
await this.ensureRegistryLoaded();
const templates = this.registry.templates.filter(t => t.id === id);
if (templates.length === 0) return null;
if (version) {
return templates.find(t => t.version === version) || null;
}
// Return latest stable version
const stableTemplates = templates.filter(t => t.stability === 'stable');
if (stableTemplates.length > 0) {
return stableTemplates.sort((a, b) => this.compareVersions(b.version, a.version))[0];
}
// Return latest version if no stable version
return templates.sort((a, b) => this.compareVersions(b.version, a.version))[0];
}
async getCategories(): Promise<TemplateCategory[]> {
await this.ensureRegistryLoaded();
return this.registry.categories;
}
async searchTemplates(query: string): Promise<TemplateEntry[]> {
await this.ensureRegistryLoaded();
const searchTerms = query.toLowerCase().split(' ');
return this.registry.templates.filter(template => {
const searchableText = [
template.name,
template.description,
...template.tags,
...template.features,
template.category
].join(' ').toLowerCase();
return searchTerms.every(term => searchableText.includes(term));
});
}
private async ensureRegistryLoaded(): Promise<void> {
const now = Date.now();
if (now - this.lastFetch > this.cacheTimeout) {
await this.fetchRegistry();
this.lastFetch = now;
}
}
private async fetchRegistry(): Promise<void> {
try {
const response = await fetch(this.registryUrl);
if (!response.ok) {
throw new Error(`Failed to fetch registry: ${response.statusText}`);
}
this.registry = await response.json();
} catch (error) {
console.error('Failed to fetch template registry:', error);
// Use cached registry if available
}
}
private applyFilters(templates: TemplateEntry[], filters: TemplateFilters): TemplateEntry[] {
return templates.filter(template => {
if (filters.category && template.category !== filters.category) return false;
if (filters.stability && template.stability !== filters.stability) return false;
if (filters.tags && !filters.tags.every(tag => template.tags.includes(tag))) return false;
if (filters.features && !filters.features.every(feature => template.features.includes(feature))) return false;
if (filters.author && template.author.name !== filters.author) return false;
return true;
});
}
private sortTemplates = (a: TemplateEntry, b: TemplateEntry): number => {
// Sort by stability first (stable > beta > experimental > deprecated)
const stabilityOrder = { stable: 0, beta: 1, experimental: 2, deprecated: 3 };
const stabilityDiff = stabilityOrder[a.stability] - stabilityOrder[b.stability];
if (stabilityDiff !== 0) return stabilityDiff;
// Then by download count
const downloadDiff = b.stats.downloads - a.stats.downloads;
if (downloadDiff !== 0) return downloadDiff;
// Finally by name
return a.name.localeCompare(b.name);
};
private compareVersions(a: string, b: string): number {
const aParts = a.split('.').map(Number);
const bParts = b.split('.').map(Number);
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
const aPart = aParts[i] || 0;
const bPart = bParts[i] || 0;
if (aPart !== bPart) {
return aPart - bPart;
}
}
return 0;
}
}
export interface TemplateFilters {
category?: string;
stability?: 'experimental' | 'beta' | 'stable' | 'deprecated';
tags?: string[];
features?: string[];
author?: string;
}
Version Migration System
Migration Framework
// src/migration/template-migrator.ts
export interface MigrationStep {
id: string;
description: string;
fromVersion: string;
toVersion: string;
breaking: boolean;
automatic: boolean;
execute: (context: MigrationContext) => Promise<MigrationResult>;
}
export interface MigrationContext {
projectPath: string;
currentVersion: string;
targetVersion: string;
templateId: string;
config: any;
dryRun: boolean;
}
export interface MigrationResult {
success: boolean;
changes: FileChange[];
warnings: string[];
errors: string[];
}
export interface FileChange {
type: 'create' | 'update' | 'delete' | 'rename';
path: string;
newPath?: string;
content?: string;
backup?: boolean;
}
export class TemplateMigrator {
private migrations: Map<string, MigrationStep[]> = new Map();
registerMigration(templateId: string, migration: MigrationStep): void {
if (!this.migrations.has(templateId)) {
this.migrations.set(templateId, []);
}
this.migrations.get(templateId)!.push(migration);
}
async migrate(context: MigrationContext): Promise<MigrationResult> {
const templateMigrations = this.migrations.get(context.templateId) || [];
const applicableMigrations = this.findApplicableMigrations(
templateMigrations,
context.currentVersion,
context.targetVersion
);
if (applicableMigrations.length === 0) {
return {
success: true,
changes: [],
warnings: [],
errors: []
};
}
const result: MigrationResult = {
success: true,
changes: [],
warnings: [],
errors: []
};
// Check for breaking changes
const breakingMigrations = applicableMigrations.filter(m => m.breaking);
if (breakingMigrations.length > 0 && !context.dryRun) {
result.warnings.push(
`This migration includes ${breakingMigrations.length} breaking changes. ` +
'Please review the migration guide before proceeding.'
);
}
// Execute migrations in order
for (const migration of applicableMigrations) {
try {
const migrationResult = await migration.execute(context);
result.changes.push(...migrationResult.changes);
result.warnings.push(...migrationResult.warnings);
result.errors.push(...migrationResult.errors);
if (!migrationResult.success) {
result.success = false;
break;
}
} catch (error) {
result.success = false;
result.errors.push(`Migration ${migration.id} failed: ${error.message}`);
break;
}
}
return result;
}
private findApplicableMigrations(
migrations: MigrationStep[],
fromVersion: string,
toVersion: string
): MigrationStep[] {
return migrations
.filter(m => this.isVersionInRange(fromVersion, m.fromVersion, toVersion))
.sort((a, b) => this.compareVersions(a.toVersion, b.toVersion));
}
private isVersionInRange(current: string, from: string, to: string): boolean {
return this.compareVersions(current, from) >= 0 &&
this.compareVersions(to, current) > 0;
}
private compareVersions(a: string, b: string): number {
const aParts = a.split('.').map(Number);
const bParts = b.split('.').map(Number);
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
const aPart = aParts[i] || 0;
const bPart = bParts[i] || 0;
if (aPart !== bPart) {
return aPart - bPart;
}
}
return 0;
}
}
Example Migration Steps
// src/migration/migrations/nextjs-v2.0-to-v2.1.ts
import { MigrationStep, MigrationContext, MigrationResult } from '../template-migrator';
export const nextjsV20ToV21Migration: MigrationStep = {
id: 'nextjs-v2.0-to-v2.1',
description: 'Upgrade Next.js template from v2.0 to v2.1',
fromVersion: '2.0.0',
toVersion: '2.1.0',
breaking: false,
automatic: true,
execute: async (context: MigrationContext): Promise<MigrationResult> => {
const changes: FileChange[] = [];
const warnings: string[] = [];
const errors: string[] = [];
try {
// Update package.json dependencies
const packageJsonPath = path.join(context.projectPath, 'package.json');
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));
// Update Next.js version
if (packageJson.dependencies?.next) {
packageJson.dependencies.next = '^14.0.0';
changes.push({
type: 'update',
path: 'package.json',
content: JSON.stringify(packageJson, null, 2),
backup: true
});
}
// Add new ESLint configuration
const eslintConfigPath = path.join(context.projectPath, '.eslintrc.json');
if (!await fs.pathExists(eslintConfigPath)) {
const eslintConfig = {
extends: ['next/core-web-vitals', '@typescript-eslint/recommended'],
rules: {
'@typescript-eslint/no-unused-vars': 'error'
}
};
changes.push({
type: 'create',
path: '.eslintrc.json',
content: JSON.stringify(eslintConfig, null, 2)
});
}
// Update TypeScript configuration
const tsconfigPath = path.join(context.projectPath, 'tsconfig.json');
if (await fs.pathExists(tsconfigPath)) {
const tsconfig = JSON.parse(await fs.readFile(tsconfigPath, 'utf-8'));
// Add new compiler options
tsconfig.compilerOptions = {
...tsconfig.compilerOptions,
incremental: true,
plugins: [{ name: 'next' }]
};
changes.push({
type: 'update',
path: 'tsconfig.json',
content: JSON.stringify(tsconfig, null, 2),
backup: true
});
}
warnings.push('Please run "npm install" to update dependencies after migration.');
return {
success: true,
changes,
warnings,
errors
};
} catch (error) {
return {
success: false,
changes: [],
warnings,
errors: [error.message]
};
}
}
};
Template Publishing Workflow
Publishing Pipeline
# .github/workflows/template-publish.yml
name: Template Publishing
on:
push:
tags:
- 'v*'
jobs:
validate-template:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate template structure
run: |
node scripts/validate-template.js
- name: Test template generation
run: |
node scripts/test-template.js
- name: Lint template files
run: |
npm run lint:template
publish-template:
needs: validate-template
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Extract version from tag
id: version
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
- name: Update template manifest
run: |
jq '.version = "${{ steps.version.outputs.VERSION }}"' template.json > template.json.tmp
mv template.json.tmp template.json
- name: Create template package
run: |
tar -czf template-${{ steps.version.outputs.VERSION }}.tar.gz \
--exclude='.git' \
--exclude='node_modules' \
--exclude='.github' \
.
- name: Upload to registry
run: |
curl -X POST \
-H "Authorization: Bearer ${{ secrets.REGISTRY_TOKEN }}" \
-F "template=@template-${{ steps.version.outputs.VERSION }}.tar.gz" \
-F "manifest=@template.json" \
"${{ secrets.REGISTRY_URL }}/templates"
- name: Create GitHub release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ steps.version.outputs.VERSION }}
body_path: CHANGELOG.md
draft: false
prerelease: false
Template Validation
// scripts/validate-template.js
const fs = require('fs');
const path = require('path');
const Ajv = require('ajv');
const templateSchema = {
type: 'object',
required: ['name', 'version', 'description', 'category'],
properties: {
name: { type: 'string', pattern: '^[a-z][a-z0-9-]*$' },
version: { type: 'string', pattern: '^\\d+\\.\\d+\\.\\d+$' },
description: { type: 'string', minLength: 10 },
category: { type: 'string', enum: ['frontend', 'backend', 'fullstack', 'mobile', 'desktop'] },
stability: { type: 'string', enum: ['experimental', 'beta', 'stable', 'deprecated'] },
giseVersion: { type: 'string' },
author: {
type: 'object',
required: ['name'],
properties: {
name: { type: 'string' },
email: { type: 'string', format: 'email' }
}
}
}
};
function validateTemplate() {
const ajv = new Ajv();
const validate = ajv.compile(templateSchema);
// Validate template.json
const templatePath = path.join(process.cwd(), 'template.json');
if (!fs.existsSync(templatePath)) {
console.error('template.json not found');
process.exit(1);
}
const template = JSON.parse(fs.readFileSync(templatePath, 'utf-8'));
const valid = validate(template);
if (!valid) {
console.error('Template validation failed:');
console.error(validate.errors);
process.exit(1);
}
// Validate required files
const requiredFiles = [
'README.md',
'CHANGELOG.md',
'template.vars.json',
'template.prompts.json'
];
for (const file of requiredFiles) {
if (!fs.existsSync(path.join(process.cwd(), file))) {
console.error(`Required file missing: ${file}`);
process.exit(1);
}
}
console.log('Template validation passed');
}
validateTemplate();
Version Compatibility Matrix
Compatibility Tracking
// src/compatibility/compatibility-checker.ts
export interface CompatibilityMatrix {
giseVersions: VersionRange[];
nodeVersions: VersionRange[];
npmVersions: VersionRange[];
platforms: Platform[];
frameworks: FrameworkCompatibility[];
}
export interface VersionRange {
min: string;
max?: string;
supported: boolean;
notes?: string;
}
export interface Platform {
name: string;
supported: boolean;
notes?: string;
}
export interface FrameworkCompatibility {
name: string;
versions: VersionRange[];
optional: boolean;
}
export class CompatibilityChecker {
async checkCompatibility(
templateId: string,
templateVersion: string,
environment: Environment
): Promise<CompatibilityResult> {
const template = await this.getTemplate(templateId, templateVersion);
if (!template) {
return {
compatible: false,
issues: [`Template ${templateId}@${templateVersion} not found`],
warnings: []
};
}
const issues: string[] = [];
const warnings: string[] = [];
// Check GISE version compatibility
if (!this.isVersionCompatible(environment.giseVersion, template.compatibility.giseVersions)) {
issues.push(`GISE version ${environment.giseVersion} is not compatible with this template`);
}
// Check Node.js version compatibility
if (!this.isVersionCompatible(environment.nodeVersion, template.compatibility.nodeVersions)) {
issues.push(`Node.js version ${environment.nodeVersion} is not compatible with this template`);
}
// Check platform compatibility
const platformSupported = template.compatibility.platforms.some(p =>
p.name === environment.platform && p.supported
);
if (!platformSupported) {
issues.push(`Platform ${environment.platform} is not supported by this template`);
}
// Check for deprecated features
if (template.compatibility.deprecated.length > 0) {
warnings.push(`This template uses deprecated features: ${template.compatibility.deprecated.join(', ')}`);
}
return {
compatible: issues.length === 0,
issues,
warnings
};
}
private isVersionCompatible(version: string, ranges: VersionRange[]): boolean {
return ranges.some(range => {
const minSatisfied = this.compareVersions(version, range.min) >= 0;
const maxSatisfied = !range.max || this.compareVersions(version, range.max) <= 0;
return minSatisfied && maxSatisfied && range.supported;
});
}
private compareVersions(a: string, b: string): number {
const aParts = a.split('.').map(Number);
const bParts = b.split('.').map(Number);
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
const aPart = aParts[i] || 0;
const bPart = bParts[i] || 0;
if (aPart !== bPart) {
return aPart - bPart;
}
}
return 0;
}
private async getTemplate(id: string, version: string): Promise<TemplateEntry | null> {
// Implementation would fetch from registry
return null;
}
}
export interface Environment {
giseVersion: string;
nodeVersion: string;
npmVersion: string;
platform: string;
}
export interface CompatibilityResult {
compatible: boolean;
issues: string[];
warnings: string[];
}
Template Analytics and Metrics
Usage Analytics
// src/analytics/template-analytics.ts
export interface TemplateAnalytics {
templateId: string;
version: string;
metrics: TemplateMetrics;
trends: TemplateTrends;
feedback: TemplateFeedback;
}
export interface TemplateMetrics {
downloads: {
total: number;
lastMonth: number;
lastWeek: number;
daily: number[];
};
usage: {
activeProjects: number;
successfulGenerations: number;
failedGenerations: number;
averageGenerationTime: number;
};
adoption: {
newUsers: number;
returningUsers: number;
retentionRate: number;
};
}
export interface TemplateTrends {
downloadTrend: 'increasing' | 'decreasing' | 'stable';
popularityRank: number;
categoryRank: number;
growthRate: number;
}
export interface TemplateFeedback {
averageRating: number;
totalRatings: number;
reviews: TemplateReview[];
commonIssues: string[];
featureRequests: string[];
}
export interface TemplateReview {
id: string;
userId: string;
rating: number;
comment: string;
version: string;
createdAt: string;
helpful: number;
}
export class TemplateAnalyticsCollector {
async recordDownload(templateId: string, version: string, userId?: string): Promise<void> {
const event = {
type: 'download',
templateId,
version,
userId,
timestamp: new Date().toISOString(),
userAgent: this.getUserAgent(),
platform: this.getPlatform()
};
await this.sendEvent(event);
}
async recordGeneration(
templateId: string,
version: string,
success: boolean,
duration: number,
userId?: string
): Promise<void> {
const event = {
type: 'generation',
templateId,
version,
success,
duration,
userId,
timestamp: new Date().toISOString()
};
await this.sendEvent(event);
}
async recordFeedback(
templateId: string,
version: string,
rating: number,
comment: string,
userId: string
): Promise<void> {
const event = {
type: 'feedback',
templateId,