chore(ci): Validate git tags for workflows: - Reusable utility for validating git tags across all

scripts
  - Supports special case for local development mode
  - Provides helpful error messages with available tags
  - Can be used as CLI tool or imported module
pull/6190/head
Jason Stirnaman 2025-07-05 19:18:52 -05:00
parent fa069a77ea
commit 1cb33bfb13
5 changed files with 225 additions and 3 deletions

View File

@ -4,7 +4,7 @@ on:
workflow_dispatch:
inputs:
version:
description: 'Version to audit (use "local" for running containers)'
description: 'Version to audit (must exist in git tags, e.g., v3.0.0 or "local" for dev containers)'
required: false
default: 'local'
create_issue:

View File

@ -14,11 +14,11 @@ on:
- cloud-dedicated
- cloud-serverless
version:
description: 'Version being released (e.g., 3.0.0)'
description: 'Release tag name (must exist in git tags, e.g., v3.0.0 or "local" for dev)'
required: true
type: string
previous_version:
description: 'Previous version for comparison (e.g., 2.9.0)'
description: 'Previous release tag name (must exist in git tags, e.g., v2.9.0)'
required: true
type: string
dry_run:

View File

@ -43,6 +43,30 @@ FROM_VERSION="${1:-v3.1.0}"
TO_VERSION="${2:-v3.2.0}"
PRIMARY_REPO="${3:-${HOME}/Documents/github/influxdb}"
# Function to validate git tag
validate_git_tag() {
local version="$1"
local repo_path="$2"
if [ "$version" = "local" ]; then
return 0 # Special case for development
fi
if [ ! -d "$repo_path" ]; then
echo -e "${RED}Error: Repository not found: $repo_path${NC}"
return 1
fi
if ! git -C "$repo_path" tag --list | grep -q "^${version}$"; then
echo -e "${RED}Error: Version tag '$version' does not exist in repository $repo_path${NC}"
echo -e "${YELLOW}Available tags (most recent first):${NC}"
git -C "$repo_path" tag --list --sort=-version:refname | head -10 | sed 's/^/ /'
return 1
fi
return 0
}
# Collect additional repositories (all arguments after the third)
ADDITIONAL_REPOS=()
shift 3 2>/dev/null || true
@ -58,6 +82,19 @@ YELLOW='\033[0;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Validate version tags
echo -e "${YELLOW}Validating version tags...${NC}"
if ! validate_git_tag "$FROM_VERSION" "$PRIMARY_REPO"; then
echo -e "${RED}From version validation failed${NC}"
exit 1
fi
if ! validate_git_tag "$TO_VERSION" "$PRIMARY_REPO"; then
echo -e "${RED}To version validation failed${NC}"
exit 1
fi
echo -e "${GREEN}✓ Version tags validated successfully${NC}\n"
echo -e "${BLUE}Generating release notes for ${TO_VERSION}${NC}"
echo -e "Primary Repository: ${PRIMARY_REPO}"
if [ ${#ADDITIONAL_REPOS[@]} -gt 0 ]; then

View File

@ -0,0 +1,175 @@
#!/usr/bin/env node
/**
* Git tag validation utility
* Validates that provided version strings are actual git tags in the repository
*/
import { spawn } from 'child_process';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
/**
* Execute a command and return the output
* @param {string} command - Command to execute
* @param {string[]} args - Command arguments
* @param {string} cwd - Working directory
* @returns {Promise<string>} Command output
*/
function execCommand(command, args = [], cwd = process.cwd()) {
return new Promise((resolve, reject) => {
const child = spawn(command, args, { cwd, stdio: 'pipe' });
let stdout = '';
let stderr = '';
child.stdout.on('data', (data) => {
stdout += data.toString();
});
child.stderr.on('data', (data) => {
stderr += data.toString();
});
child.on('close', (code) => {
if (code === 0) {
resolve(stdout.trim());
} else {
reject(new Error(`Command failed: ${command} ${args.join(' ')}\n${stderr}`));
}
});
});
}
/**
* Get all git tags from the repository
* @param {string} repoPath - Path to the git repository
* @returns {Promise<string[]>} Array of tag names
*/
async function getGitTags(repoPath = process.cwd()) {
try {
const output = await execCommand('git', ['tag', '--list', '--sort=-version:refname'], repoPath);
return output ? output.split('\n').filter(tag => tag.trim()) : [];
} catch (error) {
throw new Error(`Failed to get git tags: ${error.message}`);
}
}
/**
* Validate that a version string is an existing git tag
* @param {string} version - Version string to validate
* @param {string} repoPath - Path to the git repository
* @returns {Promise<boolean>} True if version is a valid tag
*/
async function isValidTag(version, repoPath = process.cwd()) {
if (!version || version === 'local') {
return true; // 'local' is a special case for development
}
const tags = await getGitTags(repoPath);
return tags.includes(version);
}
/**
* Validate multiple version tags
* @param {string[]} versions - Array of version strings to validate
* @param {string} repoPath - Path to the git repository
* @returns {Promise<{valid: boolean, errors: string[], availableTags: string[]}>}
*/
async function validateTags(versions, repoPath = process.cwd()) {
const errors = [];
const availableTags = await getGitTags(repoPath);
for (const version of versions) {
if (version && version !== 'local' && !availableTags.includes(version)) {
errors.push(`Version '${version}' is not a valid git tag`);
}
}
return {
valid: errors.length === 0,
errors,
availableTags: availableTags.slice(0, 10) // Return top 10 most recent tags
};
}
/**
* Validate version inputs and exit with error if invalid
* @param {string} version - Current version
* @param {string} previousVersion - Previous version (optional)
* @param {string} repoPath - Path to the git repository
*/
async function validateVersionInputs(version, previousVersion = null, repoPath = process.cwd()) {
const versionsToCheck = [version];
if (previousVersion) {
versionsToCheck.push(previousVersion);
}
const validation = await validateTags(versionsToCheck, repoPath);
if (!validation.valid) {
console.error('\n❌ Version validation failed:');
validation.errors.forEach(error => console.error(` - ${error}`));
if (validation.availableTags.length > 0) {
console.error('\n📋 Available tags (most recent first):');
validation.availableTags.forEach(tag => console.error(` - ${tag}`));
} else {
console.error('\n📋 No git tags found in repository');
}
console.error('\n💡 Tip: Use "local" for development/testing with local containers');
process.exit(1);
}
console.log('✅ Version tags validated successfully');
}
/**
* Get the repository root path (where .git directory is located)
* @param {string} startPath - Starting path to search from
* @returns {Promise<string>} Path to repository root
*/
async function getRepositoryRoot(startPath = process.cwd()) {
try {
const output = await execCommand('git', ['rev-parse', '--show-toplevel'], startPath);
return output;
} catch (error) {
throw new Error(`Not a git repository or git not available: ${error.message}`);
}
}
export {
getGitTags,
isValidTag,
validateTags,
validateVersionInputs,
getRepositoryRoot
};
// CLI usage when run directly
if (import.meta.url === `file://${process.argv[1]}`) {
const args = process.argv.slice(2);
if (args.length === 0) {
console.log('Usage: node validate-tags.js <version> [previous-version]');
console.log('Examples:');
console.log(' node validate-tags.js v3.0.0');
console.log(' node validate-tags.js v3.0.0 v2.9.0');
console.log(' node validate-tags.js local # Special case for development');
process.exit(1);
}
const [version, previousVersion] = args;
try {
const repoRoot = await getRepositoryRoot();
await validateVersionInputs(version, previousVersion, repoRoot);
console.log('All versions are valid git tags');
} catch (error) {
console.error(`Error: ${error.message}`);
process.exit(1);
}
}

View File

@ -11,6 +11,7 @@ import { promises as fs } from 'fs';
import { homedir } from 'os';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { validateVersionInputs, getRepositoryRoot } from '../common/validate-tags.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@ -945,6 +946,15 @@ async function main() {
process.exit(1);
}
// Validate version tag
try {
const repoRoot = await getRepositoryRoot();
await validateVersionInputs(version, null, repoRoot);
} catch (error) {
console.error(`Version validation failed: ${error.message}`);
process.exit(1);
}
const auditor = new CLIDocAuditor(product, version);
await auditor.run();
}