Skip to main content

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 CategoryVersioning FrequencyStability Level
Core TemplatesConservative (quarterly)High
Framework TemplatesRegular (monthly)Medium
Experimental TemplatesFrequent (weekly)Low
Community TemplatesVariableVariable

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,