1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Git tag validation utility
5
+ * Validates that provided version strings are actual git tags in the repository
6
+ */
7
+
8
+ import { spawn } from 'child_process' ;
9
+ import { dirname , join } from 'path' ;
10
+ import { fileURLToPath } from 'url' ;
11
+
12
+ const __filename = fileURLToPath ( import . meta. url ) ;
13
+ const __dirname = dirname ( __filename ) ;
14
+
15
+ /**
16
+ * Execute a command and return the output
17
+ * @param {string } command - Command to execute
18
+ * @param {string[] } args - Command arguments
19
+ * @param {string } cwd - Working directory
20
+ * @returns {Promise<string> } Command output
21
+ */
22
+ function execCommand ( command , args = [ ] , cwd = process . cwd ( ) ) {
23
+ return new Promise ( ( resolve , reject ) => {
24
+ const child = spawn ( command , args , { cwd, stdio : 'pipe' } ) ;
25
+ let stdout = '' ;
26
+ let stderr = '' ;
27
+
28
+ child . stdout . on ( 'data' , ( data ) => {
29
+ stdout += data . toString ( ) ;
30
+ } ) ;
31
+
32
+ child . stderr . on ( 'data' , ( data ) => {
33
+ stderr += data . toString ( ) ;
34
+ } ) ;
35
+
36
+ child . on ( 'close' , ( code ) => {
37
+ if ( code === 0 ) {
38
+ resolve ( stdout . trim ( ) ) ;
39
+ } else {
40
+ reject ( new Error ( `Command failed: ${ command } ${ args . join ( ' ' ) } \n${ stderr } ` ) ) ;
41
+ }
42
+ } ) ;
43
+ } ) ;
44
+ }
45
+
46
+ /**
47
+ * Get all git tags from the repository
48
+ * @param {string } repoPath - Path to the git repository
49
+ * @returns {Promise<string[]> } Array of tag names
50
+ */
51
+ async function getGitTags ( repoPath = process . cwd ( ) ) {
52
+ try {
53
+ const output = await execCommand ( 'git' , [ 'tag' , '--list' , '--sort=-version:refname' ] , repoPath ) ;
54
+ return output ? output . split ( '\n' ) . filter ( tag => tag . trim ( ) ) : [ ] ;
55
+ } catch ( error ) {
56
+ throw new Error ( `Failed to get git tags: ${ error . message } ` ) ;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Validate that a version string is an existing git tag
62
+ * @param {string } version - Version string to validate
63
+ * @param {string } repoPath - Path to the git repository
64
+ * @returns {Promise<boolean> } True if version is a valid tag
65
+ */
66
+ async function isValidTag ( version , repoPath = process . cwd ( ) ) {
67
+ if ( ! version || version === 'local' ) {
68
+ return true ; // 'local' is a special case for development
69
+ }
70
+
71
+ const tags = await getGitTags ( repoPath ) ;
72
+ return tags . includes ( version ) ;
73
+ }
74
+
75
+ /**
76
+ * Validate multiple version tags
77
+ * @param {string[] } versions - Array of version strings to validate
78
+ * @param {string } repoPath - Path to the git repository
79
+ * @returns {Promise<{valid: boolean, errors: string[], availableTags: string[]}> }
80
+ */
81
+ async function validateTags ( versions , repoPath = process . cwd ( ) ) {
82
+ const errors = [ ] ;
83
+ const availableTags = await getGitTags ( repoPath ) ;
84
+
85
+ for ( const version of versions ) {
86
+ if ( version && version !== 'local' && ! availableTags . includes ( version ) ) {
87
+ errors . push ( `Version '${ version } ' is not a valid git tag` ) ;
88
+ }
89
+ }
90
+
91
+ return {
92
+ valid : errors . length === 0 ,
93
+ errors,
94
+ availableTags : availableTags . slice ( 0 , 10 ) // Return top 10 most recent tags
95
+ } ;
96
+ }
97
+
98
+ /**
99
+ * Validate version inputs and exit with error if invalid
100
+ * @param {string } version - Current version
101
+ * @param {string } previousVersion - Previous version (optional)
102
+ * @param {string } repoPath - Path to the git repository
103
+ */
104
+ async function validateVersionInputs ( version , previousVersion = null , repoPath = process . cwd ( ) ) {
105
+ const versionsToCheck = [ version ] ;
106
+ if ( previousVersion ) {
107
+ versionsToCheck . push ( previousVersion ) ;
108
+ }
109
+
110
+ const validation = await validateTags ( versionsToCheck , repoPath ) ;
111
+
112
+ if ( ! validation . valid ) {
113
+ console . error ( '\n❌ Version validation failed:' ) ;
114
+ validation . errors . forEach ( error => console . error ( ` - ${ error } ` ) ) ;
115
+
116
+ if ( validation . availableTags . length > 0 ) {
117
+ console . error ( '\n📋 Available tags (most recent first):' ) ;
118
+ validation . availableTags . forEach ( tag => console . error ( ` - ${ tag } ` ) ) ;
119
+ } else {
120
+ console . error ( '\n📋 No git tags found in repository' ) ;
121
+ }
122
+
123
+ console . error ( '\n💡 Tip: Use "local" for development/testing with local containers' ) ;
124
+ process . exit ( 1 ) ;
125
+ }
126
+
127
+ console . log ( '✅ Version tags validated successfully' ) ;
128
+ }
129
+
130
+ /**
131
+ * Get the repository root path (where .git directory is located)
132
+ * @param {string } startPath - Starting path to search from
133
+ * @returns {Promise<string> } Path to repository root
134
+ */
135
+ async function getRepositoryRoot ( startPath = process . cwd ( ) ) {
136
+ try {
137
+ const output = await execCommand ( 'git' , [ 'rev-parse' , '--show-toplevel' ] , startPath ) ;
138
+ return output ;
139
+ } catch ( error ) {
140
+ throw new Error ( `Not a git repository or git not available: ${ error . message } ` ) ;
141
+ }
142
+ }
143
+
144
+ export {
145
+ getGitTags ,
146
+ isValidTag ,
147
+ validateTags ,
148
+ validateVersionInputs ,
149
+ getRepositoryRoot
150
+ } ;
151
+
152
+ // CLI usage when run directly
153
+ if ( import . meta. url === `file://${ process . argv [ 1 ] } ` ) {
154
+ const args = process . argv . slice ( 2 ) ;
155
+
156
+ if ( args . length === 0 ) {
157
+ console . log ( 'Usage: node validate-tags.js <version> [previous-version]' ) ;
158
+ console . log ( 'Examples:' ) ;
159
+ console . log ( ' node validate-tags.js v3.0.0' ) ;
160
+ console . log ( ' node validate-tags.js v3.0.0 v2.9.0' ) ;
161
+ console . log ( ' node validate-tags.js local # Special case for development' ) ;
162
+ process . exit ( 1 ) ;
163
+ }
164
+
165
+ const [ version , previousVersion ] = args ;
166
+
167
+ try {
168
+ const repoRoot = await getRepositoryRoot ( ) ;
169
+ await validateVersionInputs ( version , previousVersion , repoRoot ) ;
170
+ console . log ( 'All versions are valid git tags' ) ;
171
+ } catch ( error ) {
172
+ console . error ( `Error: ${ error . message } ` ) ;
173
+ process . exit ( 1 ) ;
174
+ }
175
+ }
0 commit comments