import * as path from 'path' ; import * as fs from 'fs-extra' ; import * as git from '../help/git' ; import * as os from '../help/os' ; import * as shell from '../help/shell' ; /**
 * Encapsulates some information about the release.
 */ export interface GoRelease { /**
     * The tags the release created.
     */ readonly tags ? : string [ ] ; /**
     * The commit message of the release.
     */ readonly commitMessage ? : string ; /**
     * The directory where the repository was released from.
     */ readonly repoDir ? : string ; } /**
 * Properties for `GoReleaser`.
 */ export interface GoReleaserProps { default readonly dir ? : string ; default readonly dryRun ? : boolean ; default readonly branch ? : string ; default readonly username ? : string ; default readonly email ? : string ; default readonly version ? : string ; /**
     * The message to use for the commit marking the release.
     */ readonly message ? : string ; } /**
 * Information about a specific module.
 */ export interface GoModule { /**
     * Path to the mod file.
     */ readonly modFile : string ; /**
     * The version of the module.
     */ readonly version : string ; /**
     * The cannonical name of the module. (e.g 'github.com/aws/constructs-go/constructs/v3`)
     */ readonly cannonicalName : string ; /**
     * The path inside the repository the module is located in. (e.g 'constructs')
     */ readonly repoPath : string ; /**
     * The repository URL. (e.g 'github.com/aws/constructs')
     */ readonly repoURL : string ; } /**
 * Release a set of Golang modules.
 */ export class GoReleaser { private readonly version ? : string ; private readonly gitCommitMessage ? : string ; private readonly dir : string ; private readonly dryRun : boolean ; private readonly gitBranch : string ; private readonly gitUsername : string ; private readonly gitUseremail : string ; constructor ( props ) { if ( ! shell . which ( 'git' ) ) { throw new Error ( 'git must be available to in order to be able to push Go code to GitHub' ) ; } this . version = props . version ; this . gitCommitMessage = props . message ; this . dir = path . resolve ( props . dir ?? path . join ( process . cwd ( ) , 'dist' , 'go' ) ) ; this . gitBranch = props . branch ?? 'main' ; this . dryRun = props . dryRun ?? false ; const gitUsername = props . username ?? git . username ( ) ; const gitUseremail = props . email ?? git . email ( ) ; if ( ! gitUsername ) { throw new Error ( 'Unable to detect username. either configure a git user.name or pass GIT_USER_NAME env variable' ) ; } if ( ! gitUseremail ) { throw new Error ( 'Unable to detect user email. either configure a git user.email or pass GIT_USER_EMAIL env variable' ) ; } this . gitUseremail = gitUseremail ; this . gitUsername = gitUsername ; } returns public release ( ) { const modules = this . collectModules ( this . dir ) ; if ( modules . length === 0 ) { console . log ( 'No modules detected. Skipping' ) ; return { } ; } console . log ( 'Detected modules:' ) ; modules . forEach ( m => console . log ( ` - ${ m . modFile }` ) ) ; const repoURL = this . extractRepoURL ( modules ) ; const repoDir = path . join ( os . mkdtempSync ( ) , 'repo' ) ; git . clone ( repoURL , repoDir ) ; const cwd = process . cwd ( ) ; try { process . chdir ( repoDir ) ; return this . doRelease ( modules , repoDir ) ; } finally { process . chdir ( cwd ) ; } } private doRelease ( modules , repoDir ) { git . identify ( this . gitUsername , this . gitUseremail ) ; git . checkout ( this . gitBranch , { createIfMissing : true } ) ; this . syncRepo ( repoDir ) ; git . add ( '.' ) ; if ( ! git . diffIndex ( ) ) { console . log ( 'No changes. Skipping release' ) ; return { } ; } const commitMessage = this . gitCommitMessage ?? this . buildReleaseMessage ( modules ) ; git . commit ( commitMessage ) ; const tags = [ ] ; for ( const module of modules ) { const name = this . buildTagName ( module ) ; const created = git . tag ( name ) ; if ( created ) { tags . push ( name ) ; } } if ( tags . length === 0 ) { console . log ( 'All tags already exist. Skipping release' ) ; return { } ; } const refs = [ ... tags , this . gitBranch ] ; if ( this . dryRun ) { console . log ( '===========================================' ) ; console . log ( '            🏜️ DRY-RUN MODE 🏜️' ) ; console . log ( '===========================================' ) ; refs . forEach ( t => console . log ( `Remote ref will be updated: ${ t }` ) ) ; } else { refs . forEach ( t => git . push ( t ) ) ; } return { tags , commitMessage , repoDir } ; } private collectModules ( dir ) { const modules = [ ] ; for ( const p of [ ... fs . readdirSync ( dir ) , '.' ] ) { const modFile = path . join ( dir , p , 'go.mod' ) ; if ( fs . existsSync ( modFile ) ) { modules . push ( this . parseModule ( modFile ) ) ; } } return modules ; } private parseModule ( modFile ) { const version = this . extractVersion ( path . dirname ( modFile ) ) ; const majorVersion = Number ( version . split ( '.' ) [ 0 ] ) ; const match = fs . readFileSync ( modFile ) . toString ( ) . match ( /module (.*)/ ) ; if ( ! match || ! match [ 1 ] ) { throw new Error ( `Unable to detected module declaration in ${ modFile }` ) ; } const cannonicalName = match [ 1 ] ; const cannonicalNameParts = cannonicalName . split ( '/' ) ; const repoURL = cannonicalNameParts . slice ( 0 , 3 ) . join ( '/' ) ; const majorVersionSuffix = majorVersion > 1 ? `/v${ majorVersion }` : '' ; if ( ! cannonicalName . endsWith ( majorVersionSuffix ) ) { throw new Error ( `Module declaration in '${ modFile }' expected to end with '${ majorVersionSuffix }' since its major version is larger than 1` ) ; } if ( ! repoURL . startsWith ( 'github.com' ) ) { throw new Error ( `Repository must be hosted on github.com. Found: '${ repoURL }' in ${ modFile }` ) ; } let repoPath = cannonicalNameParts . slice ( 3 ) . join ( '/' ) ; const split = repoPath . split ( '/' ) ; if ( split [ split . length - 1 ] === `v${ majorVersion }` ) { split . pop ( ) ; repoPath = split . join ( '/' ) ; } repoPath = repoPath . endsWith ( '/' ) ? repoPath . substr ( 0 , repoPath . length - 1 ) : repoPath ; return { modFile , version , cannonicalName , repoPath , repoURL } ; } private buildTagName ( m ) { return m . repoPath === '' ? `v${ m . version }` : `${ m . repoPath }/v${ m . version }` ; } private buildReleaseMessage ( modules ) { const semantic = 'chore(release)' ; const versions = new Set ( modules . map ( m => m . version ) ) ; if ( versions . size === 1 ) { return `${ semantic }: v${ versions . values ( ) . next ( ) . value }` ; } else { return `${ semantic }: ${ modules . map ( m => `${ m . repoPath ? `${ m . repoPath }@` : '' }v${ m . version }` ) . join ( ' ' ) }` ; } } private syncRepo ( repoDir ) { const topLevel = path . join ( repoDir , 'go.mod' ) ; if ( fs . existsSync ( topLevel ) ) { fs . readdirSync ( repoDir ) . filter ( f => f !== '.git' ) . forEach ( f => fs . removeSync ( path . join ( repoDir , f ) ) ) ; } else { for ( const p of fs . readdirSync ( repoDir ) ) { const submodule = path . join ( repoDir , p , 'go.mod' ) ; if ( fs . existsSync ( submodule ) ) { fs . removeSync ( path . join ( repoDir , p ) ) ; } } } fs . copySync ( this . dir , repoDir , { recursive : true } ) ; } private extractRepoURL ( modules ) { const repos = new Set < string > ( modules . map ( m => m . repoURL ) ) ; if ( repos . size === 0 ) { throw new Error ( 'Unable to detect repository from module files.' ) ; } if ( repos . size > 1 ) { throw new Error ( 'Multiple repositories found in module files' ) ; } return repos . values ( ) . next ( ) . value ; } private extractVersion ( moduleDirectory ) { const versionFile = path . join ( moduleDirectory , 'version' ) ; const repoVersion = this . version ; const moduleVersion = fs . existsSync ( versionFile ) ? fs . readFileSync ( versionFile ) . toString ( ) : undefined ; if ( repoVersion && moduleVersion && repoVersion !== moduleVersion ) { throw new Error ( `Repo version (${ repoVersion }) conflicts with module version (${ moduleVersion }) for module in ${ moduleDirectory }` ) ; } const version = moduleVersion ? moduleVersion : repoVersion ; if ( ! version ) { throw new Error ( `Unable to determine version of module ${ moduleDirectory }. ` + "Either include a 'version' file, or specify a global version using the VERSION environment variable." ) ; } return version ; } }