Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ jobs:
- name: Build
shell: bash
run: ./gradlew build
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
44 changes: 44 additions & 0 deletions .github/workflows/publish-plugin.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Publish Plugin

on:
workflow_dispatch:
inputs:
version:
description: 'Version to publish (e.g., 1.0.0)'
required: true
default: '1.0.0'

jobs:
publish:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4

- name: Set up Java
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'

- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4

- name: Validate Plugin
id: validate
shell: bash
run: |
echo "Validating version ${{ github.event.inputs.version }}"

./gradlew publishPlugins --validate-only

- name: Publish Plugin
shell: bash
run: |
echo "Publishing version ${{ github.event.inputs.version }}"

./gradlew publishPlugins
env:
GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }}
GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }}
GRADLE_PUBLISH_VERSION: ${{ github.event.inputs.version }}
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ This plugin is split into two: one for `settings` and the other for `project`. D
}

plugins {
id 'io.github.adelinosousa.gradle.plugins.settings.gh-cli-auth' version '1.0.1'
id 'io.github.adelinosousa.gradle.plugins.settings.gh-cli-auth' version '1.0.4'
}
```

Expand All @@ -54,7 +54,7 @@ This plugin is split into two: one for `settings` and the other for `project`. D
# build.gradle

plugins {
id 'io.github.adelinosousa.gradle.plugins.project.gh-cli-auth' version '1.0.1'
id 'io.github.adelinosousa.gradle.plugins.project.gh-cli-auth' version '1.0.4'
}
```

Expand All @@ -68,6 +68,7 @@ This plugin is split into two: one for `settings` and the other for `project`. D

**NOTE**: When using both plugins, ensure that you **only** apply the plugin version to <u>settings</u> plugin block and not to the <u>project</u> plugin block, as it will lead to a conflict.
You also won't be able to obtain GitHub token from the `ghCliAuth` extension if you're setting `RepositoriesMode` as `FAIL_ON_PROJECT_REPOS`, as it is only _currently_ available in the `project` plugin.
This plugin is also **not** compatible with locally precompiled plugin scripts in your `settings.gradle.kts` file, due to the way Gradle handles plugin resolution during the initialization phase.

### Configuration

Expand Down
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
org.gradle.configuration-cache=true
gradle.publish.version=1.0.4
2 changes: 1 addition & 1 deletion plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ plugins {
}

group = "io.github.adelinosousa"
version = "1.0.1"
version = System.getenv("GRADLE_PUBLISH_VERSION") ?: project.findProperty("gradle.publish.version") ?: "1.0.1"

repositories {
mavenCentral()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class GhCliAuthProjectPluginFunctionalTest {

private val buildFile by lazy { projectDir.resolve("build.gradle") }
private val settingsFile by lazy { projectDir.resolve("settings.gradle") }
private val propertiesFile by lazy { projectDir.resolve("gradle.properties") }

@Test fun `can run plugin`() {
settingsFile.writeText("")
Expand All @@ -21,6 +22,7 @@ class GhCliAuthProjectPluginFunctionalTest {
id('io.github.adelinosousa.gradle.plugins.project.gh-cli-auth')
}
""".trimIndent())
propertiesFile.writeText("gh.cli.auth.github.org=test-org")

val result = GradleRunner.create()
.forwardOutput()
Expand All @@ -31,6 +33,6 @@ class GhCliAuthProjectPluginFunctionalTest {
.buildAndFail()

// Verify the result
assertTrue(result.output.contains("Applying GitHubAuthPlugin to project"))
assertTrue(result.output.contains("GitHub CLI is not authenticated or does not have the required scopes"))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class GhCliAuthSettingsPluginFunctionalTest {

private val buildFile by lazy { projectDir.resolve("build.gradle") }
private val settingsFile by lazy { projectDir.resolve("settings.gradle") }
private val propertiesFile by lazy { projectDir.resolve("gradle.properties") }

@Test fun `can run plugin`() {
settingsFile.writeText("""
Expand All @@ -21,6 +22,7 @@ class GhCliAuthSettingsPluginFunctionalTest {
}
""".trimIndent())
buildFile.writeText("")
propertiesFile.writeText("gh.cli.auth.github.org=test-org")

val result = GradleRunner.create()
.forwardOutput()
Expand All @@ -31,6 +33,6 @@ class GhCliAuthSettingsPluginFunctionalTest {
.buildAndFail()

// Verify the result
assertTrue(result.output.contains("Applying GitHubAuthPlugin to settings"))
assertTrue(result.output.contains("GitHub CLI is not authenticated or does not have the required scopes"))
}
}
Original file line number Diff line number Diff line change
@@ -1,37 +1,23 @@
package io.github.adelinosousa.gradle.plugins

import org.gradle.api.GradleException
import org.gradle.api.provider.Provider

object GhCliAuth {
val requiredScopes: Set<String> = setOf("read:packages", "repo", "read:org")
var ghCLIProcess: GitHubCLIProcessProvider = GitHubCLIProcess()

private fun checkGhCliInstalled() {
try {
ghCLIProcess.isGhCliInstalled()
} catch (_: Exception) {
throw GradleException("GitHub CLI is not installed or not found in PATH. Please install it before using this plugin.")
}
}

fun checkGhCliAuthenticatedWithCorrectScopes(): String {
try {
checkGhCliInstalled()

val output = ghCLIProcess.getGhCliAuthStatus()
fun checkGhCliAuthenticatedWithCorrectScopes(authStatusProvider: Provider<String>): Provider<String> {
return authStatusProvider.map { output ->
if (output.contains("Token:")) {
val scopesLine = output.lines().firstOrNull { it.contains("Token scopes:") }
val scopes = scopesLine?.substringAfter(":")?.trim()?.split(",")?.map { it.replace("'", "").trim() }
?: emptyList()

if (scopes.containsAll(requiredScopes)) {
return output
return@map output
}
}

throw GradleException("GitHub CLI is not authenticated or does not have the required scopes $requiredScopes")
} catch (e: Exception) {
throw GradleException("Failed to authenticate: ${e.message}")
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,17 @@ class GhCliAuthProjectPlugin : Plugin<Project> {
override fun apply(project: Project) {
val extension = project.extensions.create("ghCliAuth", GhCliAuthExtension::class.java)

project.logger.info("Applying GitHubAuthPlugin to project")

val githubOrg = getGradleProperty(project, Config.GITHUB_ORG)
val gitEnvTokenName = getGradleProperty(project, Config.ENV_PROPERTY_NAME) ?: "GITHUB_TOKEN"

if (githubOrg.isNullOrEmpty()) {
throw IllegalStateException("GitHub organization not specified. Please set the '${Config.GITHUB_ORG}' in your gradle.properties file.")
}

val repoCredentials = Environment.getEnvCredentials(gitEnvTokenName) ?: getGhCliCredentials()
val repoCredentials = Environment.getEnvCredentials(gitEnvTokenName) ?: getGhCliCredentials(project)
if (repoCredentials.isValid()) {
// Set the extension token to share with other tasks
println("Registering Maven GitHub repository for organization: $githubOrg")
extension.token.set(repoCredentials.token)
project.repositories.maven {
name = "GitHubPackages"
Expand All @@ -35,16 +34,14 @@ class GhCliAuthProjectPlugin : Plugin<Project> {
}
}

private fun getGhCliCredentials(): RepositoryCredentials {
println("No GitHub credentials found in environment variables. Attempting to use 'gh' CLI.")
val output = GhCliAuth.checkGhCliAuthenticatedWithCorrectScopes()
return GhCliAuth.getGitHubCredentials(output)
private fun getGhCliCredentials(project: Project): RepositoryCredentials {
val authStatusProvider = project.providers.of(GitHubCLIProcess::class.java) {}
val output = GhCliAuth.checkGhCliAuthenticatedWithCorrectScopes(authStatusProvider)
return GhCliAuth.getGitHubCredentials(output.get())
}

private fun getGradleProperty(project: Project, propertyName: String): String? {
return project.providers.gradleProperty(propertyName).let {
if (it.isPresent) it.get() else null
}
return project.providers.gradleProperty(propertyName).orNull
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,14 @@ import java.net.URI
@Suppress("unused")
class GhCliAuthSettingsPlugin : Plugin<Settings> {
override fun apply(settings: Settings) {
println("Applying GitHubAuthPlugin to settings")

val githubOrg = getGradleProperty(settings, Config.GITHUB_ORG)
val gitEnvTokenName = getGradleProperty(settings, Config.ENV_PROPERTY_NAME) ?: "GITHUB_TOKEN"

if (githubOrg.isNullOrEmpty()) {
throw IllegalStateException("GitHub organization not specified. Please set the '${Config.GITHUB_ORG}' in your gradle.properties file.")
}

val repoCredentials = Environment.getEnvCredentials(gitEnvTokenName) ?: getGhCliCredentials()
val repoCredentials = Environment.getEnvCredentials(gitEnvTokenName) ?: getGhCliCredentials(settings)
if (repoCredentials.isValid()) {
val githubMavenRepository = { repo: MavenArtifactRepository ->
repo.name = "GitHubPackages"
Expand All @@ -27,23 +25,21 @@ class GhCliAuthSettingsPlugin : Plugin<Settings> {
this.password = repoCredentials.token
}
}

println("Registering Maven GitHub repository for organization: $githubOrg")
settings.pluginManagement.repositories.maven(githubMavenRepository)
settings.dependencyResolutionManagement.repositories.maven(githubMavenRepository)
} else {
throw IllegalStateException("Token not found in environment variable '${gitEnvTokenName}' or 'gh' CLI. Unable to configure GitHub Packages repository.")
}
}

private fun getGhCliCredentials(): RepositoryCredentials {
println("No GitHub credentials found in environment variables. Attempting to use 'gh' CLI.")
val output = GhCliAuth.checkGhCliAuthenticatedWithCorrectScopes()
return GhCliAuth.getGitHubCredentials(output)
private fun getGhCliCredentials(settings: Settings): RepositoryCredentials {
val authStatusProvider = settings.providers.of(GitHubCLIProcess::class.java) {}
val output = GhCliAuth.checkGhCliAuthenticatedWithCorrectScopes(authStatusProvider)
return GhCliAuth.getGitHubCredentials(output.get())
}

private fun getGradleProperty(settings: Settings, propertyName: String): String? {
return settings.providers.gradleProperty(propertyName).let {
if (it.isPresent) it.get() else null
}
return settings.providers.gradleProperty(propertyName).orNull
}
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,54 @@
package io.github.adelinosousa.gradle.plugins

import org.gradle.api.provider.ValueSource
import org.gradle.api.provider.ValueSourceParameters
import org.gradle.process.ExecOperations
import java.io.ByteArrayOutputStream
import java.io.File
import javax.inject.Inject

class GitHubCLIProcess : GitHubCLIProcessProvider {
override fun isGhCliInstalled() = ProcessBuilder("gh", "--version").start().waitFor() == 0
abstract class GitHubCLIProcess : ValueSource<String, ValueSourceParameters.None> {

override fun getGhCliAuthStatus(): String {
val process = ProcessBuilder("gh", "auth", "status", "--show-token").start()
val outputStream = ByteArrayOutputStream()
process.inputStream.copyTo(outputStream)
val output = outputStream.toString().trim()
@get:Inject
abstract val execOperations: ExecOperations

if (process.waitFor() == 0) {
return output
}
override fun obtain(): String? {
return try {
val ghPath = findGhPath()
val outputStream = ByteArrayOutputStream()
val process = execOperations.exec {
commandLine(ghPath, "auth", "status", "--show-token")
standardOutput = outputStream
isIgnoreExitValue = true
}

if (process.exitValue == 0) {
outputStream.toString().trim()
} else {
val checkInstall = execOperations.exec {
commandLine("gh", "--version")
isIgnoreExitValue = true
}

throw IllegalStateException("Failed to execute 'gh auth status'")
if (checkInstall.exitValue != 0) {
throw IllegalStateException("GitHub CLI is not installed or not found in PATH. Please install it before using this plugin.")
}

throw IllegalStateException("Failed to execute 'gh auth status'")
}
} catch (e: Exception) {
throw IllegalStateException("Failed to authenticate: ${e.message}", e)
}
}
}

interface GitHubCLIProcessProvider {
fun isGhCliInstalled(): Boolean
fun getGhCliAuthStatus(): String
private fun findGhPath(): String {
// Path fix for macOS
if ("mac" in System.getProperty("os.name").lowercase()) {
val homebrewPaths = listOf("/opt/homebrew/bin/gh", "/usr/local/bin/gh")
return homebrewPaths.firstOrNull { File(it).exists() } ?: "gh"
}

return "gh"
}
}

Loading