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
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ Gradle plugin for [Embed Code][embed-code], an application that keeps code
examples in Markdown and HTML synchronized with their source files.

The plugin downloads the released Embed Code executable for the current
platform, so developers and CI jobs do not have to install it manually. It
adds two tasks:
platform, so developers and CI jobs do not have to install it manually.
It adds two tasks:

- `checkEmbedding` checks that embedded code is up to date.
- `embedCode` updates embedded code in place.
Expand Down Expand Up @@ -99,6 +99,26 @@ embedCode {
}
```

Before installing an executable, the plugin verifies the release asset's SHA-256
digest from the GitHub Releases API. Only these metadata requests use the
optional token; release asset downloads remain unauthenticated. API requests are
unauthenticated unless a token provider is configured explicitly:

```kotlin
embedCode {
githubToken.set(providers.environmentVariable("EMBED_CODE_GITHUB_TOKEN"))
}
```

For CI, configure `githubToken` to avoid GitHub's unauthenticated API rate limit,
or pin both `version` and `sha256`. Pairing the digest with a fixed version keeps
the pin valid when GitHub publishes a newer release.

The digest applies to the downloaded release asset. For macOS, this means the
ZIP archive rather than the extracted executable. Verified cache metadata is
stored under `build/embed-code`; offline mode reuses the executable only when
its current digest, release source, and platform asset still match that metadata.

Check that documentation is up to date:

```bash
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import java.net.InetSocketAddress
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import java.nio.file.StandardOpenOption
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference
import java.util.zip.ZipEntry
Expand Down Expand Up @@ -145,12 +146,40 @@ internal class EmbedCodePluginSpec {
result.task(":installEmbedCode")?.outcome shouldBe TaskOutcome.SUCCESS
versionChecks.get() shouldBe 2
downloads.get() shouldBe 1
result.output shouldContain "Reusing Embed Code v$TEST_RELEASE_VERSION"
result.output shouldContain "Reusing verified Embed Code v$TEST_RELEASE_VERSION"
} finally {
server.stop(0)
}
}

@Test
fun `ignore an ambient GitHub token unless explicitly configured`() {
Files.writeString(
projectDirectory.resolve("build.gradle.kts"),
"""

tasks.named("installEmbedCode") {
doFirst {
val token = javaClass.getMethod("getGithubToken").invoke(this)
as org.gradle.api.provider.Property<*>
check(!token.isPresent) {
"The plugin must not use GITHUB_TOKEN implicitly."
}
}
}
""".trimIndent(),
StandardOpenOption.APPEND,
)
val environment = System.getenv().toMutableMap()
environment["GITHUB_TOKEN"] = "ambient-token"

val result = runner(":installEmbedCode")
.withEnvironment(environment)
.build()

result.task(":installEmbedCode")?.outcome shouldBe TaskOutcome.SUCCESS
}

@Test
fun `use a resolved latest release tag without modification`() {
val releaseTag = "release-$TEST_RELEASE_VERSION"
Expand Down Expand Up @@ -184,10 +213,17 @@ internal class EmbedCodePluginSpec {
val downloads = AtomicInteger()
val server = startReleaseServer(latestTag, versionChecks, downloads)
try {
writeBuildFile(downloadBaseUrl = server.releaseBaseUrl)
writeBuildFile(
downloadBaseUrl = server.releaseBaseUrl,
sha256 = releaseAssetSha256(version = TEST_RELEASE_VERSION),
)
runner(":installEmbedCode").build()

latestTag.set("v$nextVersion")
writeBuildFile(
downloadBaseUrl = server.releaseBaseUrl,
sha256 = releaseAssetSha256(version = nextVersion),
)
runner(":installEmbedCode").build()

versionChecks.get() shouldBe 2
Expand Down Expand Up @@ -218,7 +254,7 @@ internal class EmbedCodePluginSpec {
val result = runner(":installEmbedCode", "--offline").build()

result.task(":installEmbedCode")?.outcome shouldBe TaskOutcome.SUCCESS
result.output shouldContain "Reusing cached Embed Code executable"
result.output shouldContain "Reusing verified cached Embed Code executable"
}

@Test
Expand Down Expand Up @@ -260,10 +296,57 @@ internal class EmbedCodePluginSpec {
val result = runner(":installEmbedCode", "--offline").buildAndFail()

result.output shouldContain
"Cannot install the latest Embed Code release in offline mode because " +
"Cannot install Embed Code in offline mode because " +
"no cached executable exists"
}

@Test
fun `keep explicit versions offline when no verified executable is cached`() {
writeBuildFile(version = TEST_RELEASE_VERSION)

val result = runner(":installEmbedCode", "--offline").buildAndFail()

result.output shouldContain "Cannot install Embed Code in offline mode"
result.output shouldNotContain "Downloading Embed Code"
}

@Test
fun `reject a release asset whose SHA-256 digest does not match`() {
writeBuildFile(sha256 = "0".repeat(64))

val result = runner(":installEmbedCode").buildAndFail()

result.output shouldContain "SHA-256 verification failed for Embed Code asset"
}

@Test
fun `reject a modified cached executable in offline mode`() {
runner(":installEmbedCode").build()
val executableName = EmbedCodePlatform.installedExecutableName(
System.getProperty("os.name"),
)
Files.writeString(
projectDirectory.resolve("build/embed-code/latest/$executableName"),
"modified after verification",
)

val result = runner(":installEmbedCode", "--offline").buildAndFail()

result.output shouldContain "SHA-256 integrity metadata is missing or does not match"
}

@Test
fun `accept an explicitly pinned release asset`() {
writeBuildFile(
version = TEST_RELEASE_VERSION,
sha256 = releaseAssetSha256(version = TEST_RELEASE_VERSION),
)

val result = runner(":installEmbedCode").build()

result.task(":installEmbedCode")?.outcome shouldBe TaskOutcome.SUCCESS
}

@Test
@EnabledOnOs(OS.LINUX, OS.MAC)
fun `trim an overridden Embed Code version`() {
Expand Down Expand Up @@ -360,7 +443,8 @@ internal class EmbedCodePluginSpec {

val result = runner(":embedCode").build()

result.task(":installEmbedCode")?.outcome shouldBe TaskOutcome.UP_TO_DATE
result.task(":installEmbedCode")?.outcome shouldBe TaskOutcome.SUCCESS
result.output shouldContain "Reusing verified Embed Code $TEST_RELEASE_VERSION"
result.task(":embedCode")?.outcome shouldBe TaskOutcome.SUCCESS
Files.readString(projectDirectory.resolve("mode.txt")).trim() shouldBe "embed"
}
Expand Down Expand Up @@ -538,8 +622,10 @@ internal class EmbedCodePluginSpec {
private fun writeBuildFile(
version: String? = null,
downloadBaseUrl: String = releaseDirectory.toUri().toString().trimEnd('/'),
sha256: String? = null,
) {
val versionConfiguration = version?.let { "version.set(\"$it\")" }.orEmpty()
val configuredSha256 = sha256 ?: releaseAssetSha256(version = version)
Files.writeString(
projectDirectory.resolve("build.gradle.kts"),
"""
Expand All @@ -549,6 +635,7 @@ internal class EmbedCodePluginSpec {

embedCode {
$versionConfiguration
sha256.set("$configuredSha256")
downloadBaseUrl.set("$downloadBaseUrl")
codePath.set(layout.projectDirectory.dir("code"))
docsPath.set(layout.projectDirectory.dir("docs"))
Expand Down Expand Up @@ -591,6 +678,7 @@ internal class EmbedCodePluginSpec {

embedCode {
downloadBaseUrl.set("$baseUrl")
sha256.set("${releaseAssetSha256()}")
$directSource
namedSource("$firstSourceName", layout.projectDirectory.dir("company-site"))
$secondSource
Expand Down Expand Up @@ -621,6 +709,7 @@ internal class EmbedCodePluginSpec {
executable,
"""
#!/bin/sh
# release-marker: $version
: > arguments.txt
for argument in "${'$'}@"; do
printf '%s\n' "${'$'}argument" >> arguments.txt
Expand All @@ -643,13 +732,39 @@ internal class EmbedCodePluginSpec {
} else {
Files.copy(executable, asset)
}
val latestAsset = latestDirectory.resolve(platform.assetName)
Files.copy(
asset,
latestDirectory.resolve(platform.assetName),
latestAsset,
StandardCopyOption.REPLACE_EXISTING,
)
}

/**
* Returns the digest of a fake release asset.
*/
private fun releaseAssetSha256(
root: Path = releaseDirectory,
version: String? = null,
): String {
val platform = EmbedCodePlatform.detect(
System.getProperty("os.name"),
System.getProperty("os.arch"),
)
val asset = if (version == null) {
root.resolve("latest/download/${platform.assetName}")
} else {
val normalizedVersion = version.trim()
val tag = if (normalizedVersion.startsWith('v')) {
normalizedVersion
} else {
"v$normalizedVersion"
}
root.resolve("download/$tag/${platform.assetName}")
}
return sha256(asset)
}

/**
* Starts a release server whose latest endpoint redirects to a mutable tag.
*/
Expand All @@ -673,8 +788,8 @@ internal class EmbedCodePluginSpec {
exchange.close()
}
server.createContext("/releases/download/") { exchange ->
downloads.incrementAndGet()
val relativePath = exchange.requestURI.path.removePrefix("/releases/download/")
downloads.incrementAndGet()
val asset = releaseDirectory.resolve("download").resolve(relativePath).normalize()
if (!asset.startsWith(releaseDirectory.resolve("download")) || !Files.isRegularFile(asset)) {
exchange.sendResponseHeaders(404, -1)
Expand Down
Loading