diff --git a/.github/workflows/xvm-verify-push.yml b/.github/workflows/push.yml similarity index 100% rename from .github/workflows/xvm-verify-push.yml rename to .github/workflows/push.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6fbbe72a2a..fa6ddc288c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,5 +1,11 @@ name: Build, Release, and Publish +# +# Perhaps: The release flow is manually triggered, and will release a snapshot version. +# If a release already exists, it will either fail or delete it. If it's a draft, it will redo it. +# Bump the SNAPSHOT version? Or just release current version. It may be confusing to react specially +# and perform a release action just by changing to a non-SNAPSHOT. +# on: workflow_dispatch: # Allows manual triggering from GitHub Actions UIon: inputs: @@ -11,13 +17,13 @@ on: - 'true' - 'false' description: Trigger a manual release - push: + #push: # TODO let the xvm-build-verify action ensure release tags too. It should already be doing that. #tags: # - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10' - branches: - # TODO: MASTER - - simplify-tasks + #branches: + # TODO: MASTER + # - simplify-tasks env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -117,7 +123,6 @@ jobs: create-release: name: Create Release - if: env.CURRENT_RELEASE != '' runs-on: ubuntu-latest needs: build-release-artifacts # This job will wait for all builds to complete steps: @@ -131,6 +136,7 @@ jobs: with: path: ./artifacts - name: Create Release + if: env.CURRENT_RELEASE != '' run: | gh release create ${{ env.VERSION }} \ --title "XDK ${{ env.VERSION }}" \ @@ -138,6 +144,7 @@ jobs: --prerelease false \ --draft - name: Upload Release Assets + if: env.CURRENT_RELEASE != '' run: | gh release upload ${{ env.VERSION }} \ ./artifacts/xdk-${{ env.VERSION }}-*.${{ env.ARTIFACT_SUFFIX }} diff --git a/bin/git-rename-branch.sh b/bin/git-rename-branch.sh new file mode 100755 index 0000000000..b7b984d65c --- /dev/null +++ b/bin/git-rename-branch.sh @@ -0,0 +1,71 @@ +#!/bin/bash + +resolve_branch() { + local _branch + if ! _branch=$(git branch --show-current); then + echo "ERROR: Failed to resolve current branch." + exit 1 + fi + echo "$_branch" + return 0 +} + +rename_branch() { + branch=$1 + branch_new=$2 + echo "Renaming $branch to $branch_new" + echo git branch -m "$branch" "$branch_new" + echo git push origin "$branch_new" + echo git push origin --delete "$branch" + echo git push --set-upstream origin "$branch_new" +} + +nargs=$# +if [ "$nargs" -eq 1 ]; then + branch="$(resolve_branch)" + branch_new="$1" + echo "WARNING: No original branch name was given, will assume current branch." +elif [ "$nargs" -eq 2 ]; then + branch="$1" + branch_new="$2" +else + echo "ERROR: Invalid number of arguments. Usage: git-rename-branch.sh [branch] " + exit 1 +fi + +echo "HELLO $branch $branch_new" +if [ -z "$branch" ] || [ -z "$branch_new" ]; then + echo "ERROR: Branch name and new branch name cannot be resolved." + exit 1 +fi + +# Function to prompt for y/n input with default 'n' +ask_yn() { + local prompt="$1" + local response + read -r -p "$prompt [y/N]: " response + case "$response" in + [yY][eE][sS]|[yY]) + return 0 # User input is 'yes' + ;; + *) + return 1 # Default or user input is 'no' + ;; + esac +} + + +branch_current=$(resolve_branch) +if [ "$branch_current" != "$branch" ]; then + if ! git checkout "$branch"; then + echo "ERROR: Tried to check out branch $branch, but failed. Make sure to stash or commit any changes in $branch_current." + exit 1 + fi + branch_current="$branch" +fi + +if ask_yn "Are you sure you want to rename branch: $branch -> $branch_new"; then + rename_branch "$branch" "$branch_new" +fi + +echo "Finished. Current branch is now: $" diff --git a/build-logic/common-plugins/src/main/kotlin/GitHubProtocol.kt b/build-logic/common-plugins/src/main/kotlin/GitHubProtocol.kt index 84c4087e35..261ecaafec 100644 --- a/build-logic/common-plugins/src/main/kotlin/GitHubProtocol.kt +++ b/build-logic/common-plugins/src/main/kotlin/GitHubProtocol.kt @@ -32,6 +32,7 @@ data class GitHubProtocol(private val project: Project) { } private val semanticVersion = project.property("semanticVersion") as SemanticVersion + private val releaseVersion = project.releaseVersion() private val artifactBaseVersion = semanticVersion.artifactVersion.removeSuffix("-SNAPSHOT") private val tagPrefix = if (semanticVersion.isSnapshot()) "snapshot/" else "release/" private val localTag = "${tagPrefix}v$artifactBaseVersion" @@ -84,7 +85,7 @@ data class GitHubProtocol(private val project: Project) { /** * Make sure any local tags out of sync with remote are clobbered and/or updated. */ - private fun fetch(): GitTagInfo = project.run { + private fun fetchTags(): GitTagInfo = project.run { git("fetch", "--tags", "--prune-tags", "--force", "--verbose", logger = logger) return resolveTags() } @@ -114,7 +115,7 @@ data class GitHubProtocol(private val project: Project) { */ fun ensureTags(snapshotOnly: Boolean = false): Pair = project.run { // Fetch and prune remote tags, making sure that we have all remote tags locally, and no local tags that aren't on the remote. - val tags = fetch() + val tags = fetchTags() assert(tags.verifySync()) val isSnapshot = semanticVersion.isSnapshot() val isRelease = !isSnapshot @@ -135,10 +136,15 @@ data class GitHubProtocol(private val project: Project) { if (isSnapshot) { git("tag", "-d", localTag, throwOnError = false, logger = logger) + logger.info("$prefix Deleted tag: $localTag (locally), if it existed.") git("push", "--delete", "origin", localTag, throwOnError = false, logger = logger) + logger.info("$prefix Deleted tag: $localTag (remotely).") } git("tag", localTag, logger = logger) + logger.info("$prefix Created $localTag (locally).") + git("push", "origin", localTag, logger = logger) + logger.info("$prefix Pushed $localTag to remote.") return localTag to tags.localLastCommit } @@ -162,6 +168,51 @@ data class GitHubProtocol(private val project: Project) { return false } + fun isReleased(): Boolean = project.run { + // gh release view return if release already exists. + //currentVersion = senamticVersion.ar + var currentVersion = semanticVersion.artifactVersion + val releaseVersion = project.releaseVersion() + val desc = if (releaseVersion != currentVersion) "'$releaseVersion'" else "the same." + logger.lifecycle("$prefix Current project version is '$currentVersion', release version would be $desc") + val result = gh("release", "view", releaseVersion, throwOnError = false) + if (result.isSuccessful()) { + logger.error("$prefix Release '$releaseVersion' already exists. Aborting release.") + return true + } + logger.lifecycle("$prefix Release '$releaseVersion' has not been released.") + return false + } + + fun triggerRelease(): Boolean = project.run { + // TODO: Will trigger a remote workflow on GitHub so we can build all our platforms regardless of what machine we are on. + val info = fetchTags() + logger.lifecycle("$prefix Triggering Release GitHub Actions workflow for $semanticVersion.") + val inputs = mapOf( + "semanticVersion" to semanticVersion, + "releaseVersion" to releaseVersion() + ) + // Command to trigger GitHub Actions workflow using gh CLI + val workflowId = "release.yml" + val branch = info.localBranchName + val cmdLineArgs = buildList { + add("gh") + add("workflow") + add("run") + add(workflowId) + add("--ref") + add(info.localBranchName) + inputs.forEach { (key, value) -> + add("--field") + add("$key=$value") + } + } + print(cmdLineArgs) + gh(*cmdLineArgs.toTypedArray(), logger = logger) + logger.info("Triggered GitHub workflow $workflowId on branch $branch.") + return true + } + /** * Delete all versions of a package with a certain name, and or certain versions. * If no arguments are given, everything is deleted. diff --git a/build-logic/common-plugins/src/main/kotlin/Processes.kt b/build-logic/common-plugins/src/main/kotlin/Processes.kt index 63390f0c35..f6a255bfd3 100644 --- a/build-logic/common-plugins/src/main/kotlin/Processes.kt +++ b/build-logic/common-plugins/src/main/kotlin/Processes.kt @@ -22,6 +22,7 @@ data class ProcessResult(val execResult: Pair, val failure: Throwab val output: String = execResult.second fun lines(): List = output.lines() fun isSuccessful(): Boolean = exitValue == 0 + fun isFailure(): Boolean = !isSuccessful() fun rethrowFailure() { if (failure != null) { throw failure @@ -52,11 +53,13 @@ fun spawn(command: String, vararg args: String, throwOnError: Boolean = true, lo val commandLine = listOf(commandPath, *args) val builder = ProcessBuilder(commandLine).redirectErrorStream(true) + System.err.println("[processes] Spawning process: '${commandLine.joinToString(" ")}'") logger?.info("[processes] Spawning process: '${commandLine.joinToString(" ")}'") val processResult = try { val process = builder.start() // can throw IOException val exitValue = process.waitFor() + System.err.println("Spawn finished: exitValue=$exitValue") val result = process.inputStream.readTextAndClose() var failure: IllegalStateException? = null if (exitValue != 0) { diff --git a/build-logic/common-plugins/src/main/kotlin/XdkBuildLogic.kt b/build-logic/common-plugins/src/main/kotlin/XdkBuildLogic.kt index ff799db86c..d4ca351b48 100644 --- a/build-logic/common-plugins/src/main/kotlin/XdkBuildLogic.kt +++ b/build-logic/common-plugins/src/main/kotlin/XdkBuildLogic.kt @@ -185,6 +185,10 @@ fun Task.considerAlwaysUpToDate() { */ fun Project.isDryRun() = project.findProperty("dryRun")?.toString()?.toBoolean() ?: false +fun Project.releaseVersion(): String { + return project.version.toString().replace("-SNAPSHOT", "") +} + fun Project.isSnapshot(): Boolean { return project.version.toString().endsWith("-SNAPSHOT") } diff --git a/build-logic/common-plugins/src/main/kotlin/org.xtclang.build.publish.gradle.kts b/build-logic/common-plugins/src/main/kotlin/org.xtclang.build.publish.gradle.kts index 96c7d93cb2..c4f4034dea 100644 --- a/build-logic/common-plugins/src/main/kotlin/org.xtclang.build.publish.gradle.kts +++ b/build-logic/common-plugins/src/main/kotlin/org.xtclang.build.publish.gradle.kts @@ -25,7 +25,7 @@ tasks.withType().configureEach { allowPublication() } if (!allowPublication()) { - logger.warn("$prefix Skipping publication task, snapshotOnly=${snapshotOnly()} for version: '$semanticVersion'") + logger.warn("$prefix Skipping publication task, snapshotOnly=${snapshotOnly()} for version: '$semanticVersion')") } } diff --git a/xdk/build.gradle.kts b/xdk/build.gradle.kts index f742a7bcc8..d364b18fa3 100644 --- a/xdk/build.gradle.kts +++ b/xdk/build.gradle.kts @@ -1,4 +1,5 @@ import XdkBuildLogic.Companion.XDK_ARTIFACT_NAME_DISTRIBUTION_ARCHIVE +import XdkDistribution.Companion.DISTRIBUTION_TASK_GROUP import XdkDistribution.Companion.JAVATOOLS_INSTALLATION_NAME import XdkDistribution.Companion.JAVATOOLS_PREFIX_PATTERN import org.gradle.api.attributes.Category.CATEGORY_ATTRIBUTE @@ -202,6 +203,46 @@ val ensureTags by tasks.registering { } } +// If we have a snapshot version, create the release for the verison with the snapshot stripped. +// +// If the release already exists, this will fail. If the release exists but is draft, or with an override, we +// may allow overwriting it. +// +// We trigger a GitHub workflow that uploads release artifacts for all our platforms, and then collect them +// and call GH release from here. The default is that the release is in draft mode. +// We can also have a mode where we create a separate pull request after the release, that contains +// e.g. a new entry about the release in some REAADME.md or something, as well as comitting a new VERSION +// file with the next snapshot. +// +// TODO: Figure out the best place to do the tagging. It may be enough and simplest just to do publishRemote +// when the release has been created, and let the existing ensureTagging logic just solve it with 100% existing +// code. Should be safe enough given enough state checks like the checkReleased tasks and so on before we start +// weaving the actual artifacts and release specificaiton. +// +// TODO: Figure out where to hook up publishing the plugin and the XDK artifact to gradlePluinPortal and +// mavenCentral. Verify our mavenCentral credentials again, by publishing a removable normal Java-Maven artifact +// so that we confirm that the XDK artifact is equally legal, even if we don't look 100% like a Java artifact +// For mavenCentral this may still involve faking a jar file, or shipping XDK as a jar file with all other +// stuff as reasources. It should not be a big problem and can be wrapped in abstraction. +val relaseXvm by tasks.registering { + description = "Trigger a GitHub workflow that builds and releases the current branch at the last commit." + doLast { + xdkBuildLogic.gitHubProtocol().triggerRelease() + } +} + +val checkUnreleased by tasks.registering { + group = DISTRIBUTION_TASK_GROUP + description = "Fail if we already have a release with the current release version." + doLast { + val releaseVersion = releaseVersion() + if (xdkBuildLogic.gitHubProtocol().isReleased()) { + throw buildException("Release already exists for version: '$releaseVersion'") + } + logger.info("$prefix Successfully resolved new release version: $releaseVersion") + } +} + /** * Creates distribution contents based on a distribution name, version and classifier. * This logic is used for the nain distribution artifact (named "xdk"), and the contents