Skip to content

Commit 137c509

Browse files
authored
Merge pull request #1044 from JetBrains/rival/fix-tools-feed-downloading-243
Download Functions tooling feed to a json file
2 parents a2629b5 + bd6b6d1 commit 137c509

File tree

4 files changed

+149
-113
lines changed

4 files changed

+149
-113
lines changed

.github/workflows/release.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,13 @@ jobs:
145145
run: |
146146
./gradlew patchChangelog --release-note="$CHANGELOG"
147147
148+
# Upload artifact as a release asset
149+
- name: Upload Release Asset
150+
working-directory: ${{ github.workspace }}/PluginsAndFeatures/azure-toolkit-for-rider
151+
env:
152+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
153+
run: gh release upload ${{ github.event.release.tag_name }} ./build/distributions/*
154+
148155
# Publish the plugin to JetBrains Marketplace
149156
- name: Publish Plugin
150157
working-directory: ${{ github.workspace }}/PluginsAndFeatures/azure-toolkit-for-rider
@@ -155,13 +162,6 @@ jobs:
155162
PRIVATE_KEY_PASSWORD: ${{ secrets.PRIVATE_KEY_PASSWORD }}
156163
run: ./gradlew publishPlugin
157164

158-
# Upload artifact as a release asset
159-
- name: Upload Release Asset
160-
working-directory: ${{ github.workspace }}/PluginsAndFeatures/azure-toolkit-for-rider
161-
env:
162-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
163-
run: gh release upload ${{ github.event.release.tag_name }} ./build/distributions/*
164-
165165
# Create a pull request
166166
- name: Create Pull Request
167167
if: ${{ steps.properties.outputs.changelog != '' }}

PluginsAndFeatures/azure-toolkit-for-rider/CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44

55
## [Unreleased]
66

7+
### Fixed
8+
9+
- Unable to download Azure Functions Core Tools because of the `NoTransformationFoundException` ([#1042](https://github.com/JetBrains/azure-tools-for-intellij/issues/1042))
10+
11+
## [4.3.9] - 2025-02-07
12+
713
### Changed
814

915
- Update domain for Azure Functions CDN
@@ -248,7 +254,8 @@
248254
- Reimplement Azure Functions Core Tools integration
249255
- Reimplement Azure Functions templates
250256

251-
[Unreleased]: https://github.com/JetBrains/azure-tools-for-intellij/compare/v4.3.8...HEAD
257+
[Unreleased]: https://github.com/JetBrains/azure-tools-for-intellij/compare/v4.3.9...HEAD
258+
[4.3.9]: https://github.com/JetBrains/azure-tools-for-intellij/compare/v4.3.8...v4.3.9
252259
[4.3.8]: https://github.com/JetBrains/azure-tools-for-intellij/compare/v4.3.7...v4.3.8
253260
[4.3.7]: https://github.com/JetBrains/azure-tools-for-intellij/compare/v4.3.6...v4.3.7
254261
[4.3.6]: https://github.com/JetBrains/azure-tools-for-intellij/compare/v4.3.5...v4.3.6

PluginsAndFeatures/azure-toolkit-for-rider/azure-intellij-plugin-appservice-dotnet/src/main/kotlin/com/microsoft/azure/toolkit/intellij/legacy/function/toolingFeed/FunctionsToolingFeedService.kt

Lines changed: 133 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -22,28 +22,21 @@ import com.microsoft.azure.toolkit.intellij.legacy.function.settings.AzureFuncti
2222
import io.ktor.client.*
2323
import io.ktor.client.call.*
2424
import io.ktor.client.engine.cio.*
25-
import io.ktor.client.plugins.contentnegotiation.*
25+
import io.ktor.client.plugins.HttpTimeout
2626
import io.ktor.client.request.*
27-
import io.ktor.serialization.kotlinx.json.*
28-
import io.ktor.utils.io.ByteReadChannel
29-
import io.ktor.utils.io.core.isEmpty
30-
import io.ktor.utils.io.core.readBytes
27+
import io.ktor.util.cio.*
28+
import io.ktor.utils.io.*
3129
import kotlinx.coroutines.Dispatchers
3230
import kotlinx.coroutines.sync.Mutex
3331
import kotlinx.coroutines.sync.withLock
3432
import kotlinx.coroutines.withContext
3533
import kotlinx.serialization.ExperimentalSerializationApi
3634
import kotlinx.serialization.json.Json
35+
import kotlinx.serialization.json.decodeFromStream
3736
import java.io.File
3837
import java.nio.file.Path
3938
import java.nio.file.attribute.PosixFilePermission
40-
import kotlin.io.path.ExperimentalPathApi
41-
import kotlin.io.path.Path
42-
import kotlin.io.path.createDirectories
43-
import kotlin.io.path.deleteRecursively
44-
import kotlin.io.path.exists
45-
import kotlin.io.path.isExecutable
46-
import kotlin.io.path.setPosixFilePermissions
39+
import kotlin.io.path.*
4740

4841
@Service(Service.Level.APP)
4942
class FunctionsToolingFeedService : Disposable {
@@ -64,29 +57,86 @@ class FunctionsToolingFeedService : Disposable {
6457
trustManager = CertificateManager.getInstance().trustManager
6558
}
6659
}
67-
install(ContentNegotiation) {
68-
json(Json {
69-
explicitNulls = false
70-
ignoreUnknownKeys = true
71-
allowTrailingComma = true
72-
})
60+
install(HttpTimeout) {
61+
requestTimeoutMillis = 300000
7362
}
7463
}
7564

65+
@OptIn(ExperimentalSerializationApi::class)
66+
private val json = Json {
67+
explicitNulls = false
68+
ignoreUnknownKeys = true
69+
allowTrailingComma = true
70+
allowComments = true
71+
}
72+
73+
/**
74+
* Downloads the latest Azure Functions tooling release for the specified Azure Functions runtime version.
75+
*
76+
* This method fetches the release information, determines if the release has already been downloaded,
77+
* and if not, it downloads the release, extracts it to the appropriate directory and cleans up any temporary files.
78+
*
79+
* @param functionsRuntimeVersion The version of Azure Functions runtime for which to download the latest tooling release.
80+
* @return A Result wrapping the path to the latest Azure Functions tooling release.
81+
*/
82+
suspend fun downloadLatestFunctionsToolingRelease(functionsRuntimeVersion: String): Result<Path> {
83+
downloadAndSaveReleaseFeed().onFailure { error ->
84+
LOG.warn("Unable to download Function tooling release feed", error)
85+
return Result.failure(error)
86+
}
87+
88+
val toolingRelease = getLatestFunctionsToolingRelease(functionsRuntimeVersion)
89+
?: return Result.failure(IllegalStateException("Unable to obtain latest function tooling release"))
90+
val toolingReleasePath = getPathForLatestFunctionsToolingRelease(toolingRelease)
91+
?: return Result.failure(IllegalStateException("Unable to path to download function tooling release"))
92+
93+
val coreToolsExecutablePath = toolingReleasePath.resolveFunctionCoreToolsExecutable()
94+
if (coreToolsExecutablePath.exists()) {
95+
LOG.trace { "The release $toolingRelease is already downloaded" }
96+
return Result.success(toolingReleasePath)
97+
}
98+
99+
return downloadAndExtractFunctionsToolingRelease(
100+
toolingRelease,
101+
toolingReleasePath,
102+
coreToolsExecutablePath
103+
)
104+
}
105+
106+
/**
107+
* Retrieves a list of Azure Functions tooling releases for the specified Azure Functions runtime versions.
108+
*
109+
* @param functionsRuntimeVersions List of Azure Functions runtime versions.
110+
* @return List of tooling releases corresponding to the given versions, or null if the release feed could not be downloaded.
111+
*/
112+
suspend fun getFunctionsToolingReleaseForVersions(functionsRuntimeVersions: List<String>): List<FunctionsToolingRelease>? {
113+
downloadAndSaveReleaseFeed().onFailure { exception ->
114+
LOG.warn("Unable to download Function tooling release feed", exception)
115+
return null
116+
}
117+
118+
return functionsRuntimeVersions.mapNotNull { getLatestFunctionsToolingRelease(it) }
119+
}
120+
76121
/**
77122
* Downloads and saves the Azure Functions tooling release feed if the release cache is empty.
78123
*
79124
* @return Result wrapping any exception encountered during the execution.
80125
*/
81-
suspend fun downloadAndSaveReleaseFeed() = kotlin.runCatching {
82-
if (releaseCache.isNotEmpty()) return@runCatching
126+
private suspend fun downloadAndSaveReleaseFeed(): Result<Unit> {
127+
if (releaseCache.isNotEmpty()) return Result.success(Unit)
83128

84129
releaseCacheMutex.withLock {
85-
if (releaseCache.isNotEmpty()) return@withLock
130+
if (releaseCache.isNotEmpty()) return Result.success(Unit)
86131

87132
LOG.trace("Downloading Functions tooling release feed")
88133

89-
val feed = getReleaseFeed()
134+
val feedResult = downloadFunctionsToolingReleaseFeed().onFailure { error ->
135+
return Result.failure(error)
136+
}
137+
138+
val feed = feedResult.getOrNull()
139+
?: return Result.failure(IllegalStateException("Unable to download Function tooling release feed"))
90140
val releaseTags = feed.tags
91141
.toSortedMap()
92142
.filterValues { !it.releaseQuality.isNullOrEmpty() && !it.release.isNullOrEmpty() && !it.hidden }
@@ -105,93 +155,74 @@ class FunctionsToolingFeedService : Disposable {
105155
FunctionsToolingRelease(releaseKey, releaseFromTag, coreToolsRelease.downloadLink ?: "")
106156
)
107157
}
158+
159+
return Result.success(Unit)
108160
}
109161
}
110162

111-
/**
112-
* Downloads the latest Azure Functions tooling release for the specified Azure Functions runtime version.
113-
*
114-
* This method fetches the release information, determines if the release has already been downloaded,
115-
* and if not, it downloads the release, extracts it to the appropriate directory and cleans up any temporary files.
116-
*
117-
* @param functionsRuntimeVersion The version of Azure Functions runtime for which to download the latest tooling release.
118-
* @return A Result wrapping the path to the latest Azure Functions tooling release.
119-
*/
120-
suspend fun downloadLatestFunctionsToolingRelease(functionsRuntimeVersion: String): Result<Path> {
121-
downloadAndSaveReleaseFeed().onFailure { exception ->
122-
LOG.warn("Unable to download Function tooling release feed", exception)
123-
return Result.failure(exception)
124-
}
163+
private suspend fun downloadFunctionsToolingReleaseFeed(): Result<ReleaseFeed> = runCatching {
164+
val feedUrl = Registry.get("azure.function_app.core_tools.feed.url").asString()
165+
LOG.trace { "Functions tooling release feed: $feedUrl" }
125166

126-
val toolingRelease = getLatestFunctionsToolingRelease(functionsRuntimeVersion)
127-
if (toolingRelease == null) {
128-
return Result.failure(IllegalStateException("Unable to obtain latest function tooling release"))
129-
}
130-
val toolingReleasePath = getPathForLatestFunctionsToolingRelease(toolingRelease)
131-
if (toolingReleasePath == null) {
132-
return Result.failure(IllegalStateException("Unable to path to download function tooling release"))
167+
val temporaryFeedFile = FileUtil.createTempFile(
168+
File(FileUtil.getTempDirectory()),
169+
"AzureFunctionsToolingFeed",
170+
".json",
171+
true,
172+
false
173+
)
174+
val temporaryFeedPath = temporaryFeedFile.toPath()
175+
176+
LOG.trace { "Created a temporary feed file: ${temporaryFeedPath.absolutePathString()}" }
177+
178+
withContext(Dispatchers.IO) {
179+
client.prepareGet(feedUrl).execute { httpResponse ->
180+
val channel: ByteReadChannel = httpResponse.body()
181+
channel.copyAndClose(temporaryFeedFile.writeChannel())
182+
}
133183
}
134-
val coreToolsExecutablePath = toolingReleasePath.resolveFunctionCoreToolsExecutable()
135-
if (coreToolsExecutablePath.exists()) {
136-
LOG.trace { "The release $toolingRelease is already downloaded" }
137-
return Result.success(toolingReleasePath)
184+
185+
LOG.trace { "Downloaded Functions tooling feed to the ${temporaryFeedPath.absolutePathString()}" }
186+
187+
val feed = withContext(Dispatchers.IO) {
188+
json.decodeFromStream<ReleaseFeed>(temporaryFeedPath.inputStream())
138189
}
139190

140-
return downloadAndExtractFunctionsToolingRelease(
141-
toolingRelease,
142-
toolingReleasePath,
143-
coreToolsExecutablePath
144-
)
191+
temporaryFeedPath.deleteIfExists()
192+
193+
return Result.success(feed)
145194
}
146195

196+
147197
private suspend fun downloadAndExtractFunctionsToolingRelease(
148198
toolingRelease: FunctionsToolingRelease,
149199
toolingReleasePath: Path,
150200
coreToolsExecutablePath: Path,
151201
): Result<Path> {
202+
if (coreToolsExecutablePath.exists()) {
203+
LOG.trace { "The release $toolingRelease is already downloaded" }
204+
return Result.success(toolingReleasePath)
205+
}
206+
152207
functionsToolingReleaseMutex.withLock {
153208
if (coreToolsExecutablePath.exists()) {
154209
LOG.trace { "The release $toolingRelease is already downloaded" }
155210
return Result.success(toolingReleasePath)
156211
}
157212

158213
try {
159-
val tempFile = FileUtil.createTempFile(
160-
File(FileUtil.getTempDirectory()),
161-
"AzureFunctions-${toolingRelease.functionsVersion}-${toolingRelease.releaseTag}",
162-
".zip",
163-
true,
164-
true
165-
)
166-
167-
LOG.trace { "Created a temporary file: ${tempFile.absolutePath}" }
168-
169-
withContext(Dispatchers.IO) {
170-
client.prepareGet(toolingRelease.artifactUrl).execute { httpResponse ->
171-
val channel: ByteReadChannel = httpResponse.body()
172-
while (!channel.isClosedForRead) {
173-
val packet = channel.readRemaining(DEFAULT_BUFFER_SIZE.toLong())
174-
while (!packet.isEmpty) {
175-
val bytes = packet.readBytes()
176-
tempFile.appendBytes(bytes)
177-
}
178-
}
179-
}
180-
}
214+
val temporaryArchive = downloadFunctionsToolingArchive(toolingRelease)
181215

182-
LOG.trace { "Downloaded core tooling archive to the ${tempFile.absolutePath}" }
216+
if (!toolingReleasePath.exists()) toolingReleasePath.createDirectories()
183217

184-
if (!toolingReleasePath.exists())
185-
toolingReleasePath.createDirectories()
218+
LOG.trace { "Extracting from ${temporaryArchive.absolutePathString()} to $toolingReleasePath" }
219+
ZipUtil.extract(temporaryArchive, toolingReleasePath, null, true)
186220

187-
LOG.trace { "Extracting from ${tempFile.absolutePath} to $toolingReleasePath" }
188-
ZipUtil.extract(tempFile.toPath(), toolingReleasePath, null, true)
221+
temporaryArchive.deleteIfExists()
189222

190-
if (tempFile.exists())
191-
tempFile.delete()
192-
193-
if (!coreToolsExecutablePath.isExecutable() && !SystemInfo.isWindows)
223+
if (!coreToolsExecutablePath.isExecutable() && !SystemInfo.isWindows) {
194224
setExecutablePermissionsForCoreTools(coreToolsExecutablePath)
225+
}
195226

196227
return Result.success(toolingReleasePath)
197228
} catch (e: Exception) {
@@ -202,30 +233,28 @@ class FunctionsToolingFeedService : Disposable {
202233
}
203234
}
204235

205-
/**
206-
* Retrieves a list of Azure Functions tooling releases for the specified Azure Functions runtime versions.
207-
*
208-
* @param functionsRuntimeVersions List of Azure Functions runtime versions.
209-
* @return List of tooling releases corresponding to the given versions, or null if the release feed could not be downloaded.
210-
*/
211-
suspend fun getFunctionsToolingReleaseForVersions(functionsRuntimeVersions: List<String>): List<FunctionsToolingRelease>? {
212-
downloadAndSaveReleaseFeed().onFailure { exception ->
213-
LOG.warn("Unable to download Function tooling release feed", exception)
214-
return null
215-
}
216-
217-
return functionsRuntimeVersions.mapNotNull { getLatestFunctionsToolingRelease(it) }
218-
}
236+
private suspend fun downloadFunctionsToolingArchive(toolingRelease: FunctionsToolingRelease): Path {
237+
val temporaryArchive = FileUtil.createTempFile(
238+
File(FileUtil.getTempDirectory()),
239+
"AzureFunctions-${toolingRelease.functionsVersion}-${toolingRelease.releaseTag}",
240+
".zip",
241+
true,
242+
false
243+
)
244+
val temporaryArchivePath = temporaryArchive.toPath()
219245

220-
private suspend fun getReleaseFeed(): ReleaseFeed {
221-
val feedUrl = Registry.get("azure.function_app.core_tools.feed.url").asString()
222-
LOG.trace { "Functions tooling release feed: $feedUrl" }
246+
LOG.trace { "Created a temporary file: ${temporaryArchivePath.absolutePathString()}" }
223247

224-
val response = withContext(Dispatchers.IO) {
225-
client.get(feedUrl)
248+
withContext(Dispatchers.IO) {
249+
client.prepareGet(toolingRelease.artifactUrl).execute { httpResponse ->
250+
val channel: ByteReadChannel = httpResponse.body()
251+
channel.copyAndClose(temporaryArchive.writeChannel())
252+
}
226253
}
227254

228-
return response.body<ReleaseFeed>()
255+
LOG.trace { "Downloaded Functions tooling archive to the ${temporaryArchivePath.absolutePathString()}" }
256+
257+
return temporaryArchivePath
229258
}
230259

231260
private fun getLatestFunctionsToolingRelease(functionsRuntimeVersion: String): FunctionsToolingRelease? {
@@ -284,4 +313,4 @@ class FunctionsToolingFeedService : Disposable {
284313
}
285314

286315
override fun dispose() = client.close()
287-
}
316+
}

PluginsAndFeatures/azure-toolkit-for-rider/gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ pluginGroup = com.jetbrains
44
pluginName = azure-toolkit-for-rider
55
pluginRepositoryUrl = https://github.com/JetBrains/azure-tools-for-intellij
66
# SemVer format -> https://semver.org
7-
pluginVersion = 4.3.9
7+
pluginVersion = 4.3.10
88

99
# Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
1010
pluginSinceBuild = 243

0 commit comments

Comments
 (0)