Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add task for purging npm alpha versions #130

Merged
merged 3 commits into from
Jul 27, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ on how to do that, including how to develop and test locally and the versioning

_Note: 1.28.0 and later require Gradle 7_

### TBD
*Released*: TBD
(Earliest compatible LabKey version: 21.3)
* Add new PurgeNpmAlphaVersion task to make it easier to remove obsolete alpha package versions

### 1.29.0
*Released*: 7 July 2021
(Earliest compatible LabKey version: 21.3)
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ dependencies {
}

group 'org.labkey.build'
project.version = "1.30.0-SNAPSHOT"
project.version = "1.30.0-purgeAlphaVersions-SNAPSHOT"

gradlePlugin {
// TODO after transitioning to using these plugin ids, remove the properties files from resources/META-INF.gradle-plugins
Expand Down
128 changes: 128 additions & 0 deletions src/main/groovy/org/labkey/gradle/task/PurgeNpmAlphaVersions.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package org.labkey.gradle.task

import groovy.json.JsonSlurper
import org.apache.http.HttpStatus
import org.apache.http.client.methods.CloseableHttpResponse
import org.apache.http.client.methods.HttpDelete
import org.apache.http.impl.client.CloseableHttpClient
import org.apache.http.impl.client.HttpClients
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.tasks.TaskAction

import java.util.stream.Collectors

class PurgeNpmAlphaVersions extends DefaultTask
{
private static final String REPOSITORY_NAME = 'libs-client-local'
public static final String ALPHA_PREFIX_PROPERTY = 'alphaPrefix'
public static final String[] PACKAGE_NAMES = [
'@labkey/api',
'@labkey/assayreport',
'@labkey/build',
'@labkey/components',
'@labkey/freezermanager',
'@labkey/test',
'@labkey/themes',
'@labkey/workflow'
]

@TaskAction
void purgeVersions()
{
String alphaPrefix;
if (!project.hasProperty(ALPHA_PREFIX_PROPERTY))
throw new GradleException("No value provided for alphaPrefix.")
alphaPrefix = project.property(ALPHA_PREFIX_PROPERTY)
String[] undeletedVersions = []
for (String packageName : PACKAGE_NAMES)
{
project.logger.quiet("Considering ${packageName}...")
List<String> alphaVersions = getNpmAlphaVersions(packageName, alphaPrefix)
project.logger.quiet("Found ${alphaVersions.size()} versions with alpha prefix ${alphaPrefix} in package ${packageName}")
if (!alphaVersions.isEmpty())
{
alphaVersions.forEach(version -> {
if (project.hasProperty("dryRun"))
project.logger.quiet("Removing version ${version} of package ${packageName} -- Skipped for dry run")
else {
project.logger.quiet("Removing version ${version} of package ${packageName}")
if (!makeDeleteRequest(packageName, version)) {
undeletedVersions += "${packageName}: ${version}"
}
}
})
}
}
if (undeletedVersions.size() > 0)
throw new GradleException("The following versions were not deleted.\n${undeletedVersions}\nCheck the log for more information.")
}


private static List<String> getNpmAlphaVersions(String packageName, String alphaPrefix)
{
String output = "npm view ${packageName} versions --json".execute().text
def parsedJson = new JsonSlurper().parseText(output)
if (parsedJson instanceof ArrayList) {
return parsedJson.stream().filter(version -> {
version.matches(".+-" + alphaPrefix + "\\.\\d+")
}).collect(Collectors.toList())
}
else
throw new GradleException("Error retrieving versions for package ${packageName}: ${parsedJson.error}")
}

/**
* This uses the Artifactory REST Api to request a deletion of a particular package and version. There does
* not appear to be a way to request deletion of multiple versions at once. Also, though it might seem natural
* to use "npm unpublish" for this deletion, this does not work with artifactory, possibly due to this long-standing
* issue: https://github.com/npm/npm-registry-client/issues/41
* The command appears to work, returning a 200 status code when you use --verbose logging, but the artifact doesn't
* go anywhere.
*
* Another possibility here would be to use the same action as is used in the Web UI. There, Artifactory sends
* a POST request to:
* Request URL: https://artifactory.labkey.com/artifactory/ui/artifactactions/delete
* with parameters
* repoKey: libs-client-local
* path: "@labkey/components/-/@labkey/components-2.14.2-fb-update-react-select.1.tgz"
* The REST API seems a better approach, though.
* @param packageName
* @param version
* @return true if deletion was successful, false otherwise
* @throws IOException
*/
boolean makeDeleteRequest(String packageName, String version) throws IOException
{
CloseableHttpClient httpClient = HttpClients.createDefault();
String endpoint = project.property('artifactory_contextUrl')
if (!endpoint.endsWith("/"))
endpoint += "/"

// The coordinates of the packages look like this: "@labkey/components/-/@labkey/components-2.14.2-fb-update-react-select.1.tgz"
endpoint += REPOSITORY_NAME + "/" + packageName + "/-/" + packageName + "-" + version + ".tgz"
project.logger.debug("Making delete request for package ${packageName} and version ${version} via endpoint ${endpoint}")
try
{
HttpDelete httpDelete = new HttpDelete(endpoint)
// N.B. Using Authorization Bearer with an API token does not currently work
httpDelete.setHeader("Authorization", "Basic " + Base64.getEncoder().encodeToString("${project.property('artifactory_user')}:${project.property('artifactory_password')}".getBytes()))
CloseableHttpResponse response = httpClient.execute(httpDelete)
int statusCode = response.getStatusLine().getStatusCode()
if (statusCode != HttpStatus.SC_OK && statusCode != HttpStatus.SC_NO_CONTENT) {
project.logger.error("Unable to delete using ${endpoint}: ${response.getStatusLine()}")
return false
}
response.close()
return true
}
catch (Exception e)
{
throw new GradleException("Problem executing delete request with url ${endpoint}", e)
}
finally
{
httpClient.close()
}
}
}