Skip to content

Commit 337003f

Browse files
authored
Add npm ci support (#413)
1 parent 83fca30 commit 337003f

File tree

9 files changed

+108
-32
lines changed

9 files changed

+108
-32
lines changed

Diff for: build-info-extractor-npm/src/main/java/org/jfrog/build/extractor/npm/NpmDriver.java

+8
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,14 @@ public String install(File workingDirectory, List<String> extraArgs, Log logger)
4747
}
4848
}
4949

50+
public String ci(File workingDirectory, List<String> extraArgs, Log logger) throws IOException {
51+
try {
52+
return runCommand(workingDirectory, new String[]{"ci"}, extraArgs, logger);
53+
} catch (IOException | InterruptedException e) {
54+
throw new IOException("npm ci failed: " + e.getMessage(), e);
55+
}
56+
}
57+
5058
public String pack(File workingDirectory, List<String> extraArgs) throws IOException {
5159
try {
5260
return runCommand(workingDirectory, new String[]{"pack"}, extraArgs);

Diff for: build-info-extractor-npm/src/main/java/org/jfrog/build/extractor/npm/extractor/NpmBuildInfoExtractor.java

+19-12
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,17 @@ public class NpmBuildInfoExtractor implements BuildInfoExtractor<NpmProject> {
6060
@Override
6161
public Build extract(NpmProject npmProject) throws Exception {
6262
String resolutionRepository = npmProject.getResolutionRepository();
63-
List<String> installationArgs = npmProject.getInstallationArgs();
63+
List<String> commandArgs = npmProject.getCommandArgs();
6464
Path workingDir = npmProject.getWorkingDir();
6565

6666
preparePrerequisites(resolutionRepository, workingDir);
67-
createTempNpmrc(workingDir, installationArgs);
68-
runInstall(workingDir, installationArgs);
67+
createTempNpmrc(workingDir, commandArgs);
68+
if (npmProject.isCiCommand()) {
69+
runCi(workingDir, commandArgs);
70+
} else {
71+
runInstall(workingDir, commandArgs);
72+
}
6973
restoreNpmrc(workingDir);
70-
7174
List<Dependency> dependencies = collectDependencies(workingDir);
7275
String moduleId = StringUtils.isNotBlank(module) ? module : npmPackageInfo.toString();
7376
return createBuild(dependencies, moduleId);
@@ -128,10 +131,10 @@ private void backupProjectNpmrc(Path workingDir) throws IOException {
128131
Files.copy(npmrcPath, npmrcBackupPath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
129132
}
130133

131-
private void createTempNpmrc(Path workingDir, List<String> installationArgs) throws IOException, InterruptedException {
134+
private void createTempNpmrc(Path workingDir, List<String> commandArgs) throws IOException, InterruptedException {
132135
Path npmrcPath = workingDir.resolve(NPMRC_FILE_NAME);
133136
Files.deleteIfExists(npmrcPath); // Delete old npmrc file
134-
final String configList = npmDriver.configList(workingDir.toFile(), installationArgs);
137+
final String configList = npmDriver.configList(workingDir.toFile(), commandArgs);
135138

136139
Properties npmrcProperties = new Properties();
137140

@@ -140,8 +143,8 @@ private void createTempNpmrc(Path workingDir, List<String> installationArgs) thr
140143
JsonNode manifestTree = mapper.readTree(configList);
141144
manifestTree.fields().forEachRemaining(entry -> npmrcProperties.setProperty(entry.getKey(), entry.getValue().asText()));
142145
// Since we run the get config cmd with "--json" flag, we don't want to force the json output on the new npmrc we write.
143-
// We will get json output only if it was explicitly required in the installation arguments.
144-
npmrcProperties.setProperty("json", String.valueOf(isJsonOutputRequired(installationArgs)));
146+
// We will get json output only if it was explicitly required in the command arguments.
147+
npmrcProperties.setProperty("json", String.valueOf(isJsonOutputRequired(commandArgs)));
145148

146149
// Save npm auth
147150
npmrcProperties.putAll(npmAuth);
@@ -172,12 +175,12 @@ private void createTempNpmrc(Path workingDir, List<String> installationArgs) thr
172175
* 2. --arg=value (true/false)
173176
* 3. --arg value (true/false)
174177
*/
175-
static boolean isJsonOutputRequired(List<String> installationArgs) {
176-
int jsonIndex = installationArgs.indexOf("--json");
178+
static boolean isJsonOutputRequired(List<String> commandArgs) {
179+
int jsonIndex = commandArgs.indexOf("--json");
177180
if (jsonIndex > -1) {
178-
return jsonIndex == installationArgs.size() - 1 || !installationArgs.get(jsonIndex + 1).equals("false");
181+
return jsonIndex == commandArgs.size() - 1 || !commandArgs.get(jsonIndex + 1).equals("false");
179182
}
180-
return installationArgs.contains("--json=true");
183+
return commandArgs.contains("--json=true");
181184
}
182185

183186
/**
@@ -198,6 +201,10 @@ private void runInstall(Path workingDir, List<String> installationArgs) throws I
198201
logger.info(npmDriver.install(workingDir.toFile(), installationArgs, logger));
199202
}
200203

204+
private void runCi(Path workingDir, List<String> installationArgs) throws IOException {
205+
logger.info(npmDriver.ci(workingDir.toFile(), installationArgs, logger));
206+
}
207+
201208
private void restoreNpmrc(Path workingDir) throws IOException {
202209
Path npmrcPath = workingDir.resolve(NPMRC_FILE_NAME);
203210
Path npmrcBackupPath = workingDir.resolve(NPMRC_BACKUP_FILE_NAME);

Diff for: build-info-extractor-npm/src/main/java/org/jfrog/build/extractor/npm/extractor/NpmCommand.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import java.util.Map;
1717

1818
/**
19-
* Base class for npm install and npm publish commands.
19+
* Base class for npm commands.
2020
*
2121
* @author Yahav Itzhak
2222
*/

Diff for: build-info-extractor-npm/src/main/java/org/jfrog/build/extractor/npm/extractor/NpmInstall.java renamed to build-info-extractor-npm/src/main/java/org/jfrog/build/extractor/npm/extractor/NpmInstallCi.java

+12-9
Original file line numberDiff line numberDiff line change
@@ -22,25 +22,27 @@
2222
* @author Yahav Itzhak
2323
*/
2424
@SuppressWarnings({"unused", "WeakerAccess"})
25-
public class NpmInstall extends NpmCommand {
25+
public class NpmInstallCi extends NpmCommand {
2626

2727
NpmBuildInfoExtractor buildInfoExtractor;
28-
List<String> installArgs;
28+
List<String> commandArgs;
29+
boolean isCiCommand;
2930

3031
/**
31-
* Install npm package.
32+
* Run npm install or npm ci commands.
3233
*
3334
* @param clientBuilder - Build Info client builder.
3435
* @param resolutionRepository - The repository it'll resolve from.
35-
* @param installArgs - Npm install args.
36+
* @param commandArgs - Npm command args.
3637
* @param logger - The logger.
3738
* @param path - Path to directory contains package.json or path to '.tgz' file.
3839
* @param env - Environment variables to use during npm execution.
3940
*/
40-
public NpmInstall(ArtifactoryDependenciesClientBuilder clientBuilder, String resolutionRepository, String installArgs, Log logger, Path path, Map<String, String> env, String module) {
41+
public NpmInstallCi(ArtifactoryDependenciesClientBuilder clientBuilder, String resolutionRepository, String commandArgs, Log logger, Path path, Map<String, String> env, String module, boolean isCiCommand) {
4142
super(clientBuilder, resolutionRepository, logger, path, env);
4243
buildInfoExtractor = new NpmBuildInfoExtractor(clientBuilder, npmDriver, logger, module);
43-
this.installArgs = StringUtils.isBlank(installArgs) ? new ArrayList<>() : Arrays.asList(installArgs.trim().split("\\s+"));
44+
this.commandArgs = StringUtils.isBlank(commandArgs) ? new ArrayList<>() : Arrays.asList(commandArgs.trim().split("\\s+"));
45+
this.isCiCommand = isCiCommand;
4446
}
4547

4648
@Override
@@ -52,7 +54,7 @@ public Build execute() {
5254
validateNpmVersion();
5355
validateRepoExists(client, repo, "Source repo must be specified");
5456

55-
NpmProject npmProject = new NpmProject(installArgs, repo, workingDir);
57+
NpmProject npmProject = new NpmProject(commandArgs, repo, workingDir, isCiCommand);
5658
return buildInfoExtractor.extract(npmProject);
5759
} catch (Exception e) {
5860
logger.error(e.getMessage(), e);
@@ -69,13 +71,14 @@ public static void main(String[] ignored) {
6971
ArtifactoryClientConfiguration clientConfiguration = createArtifactoryClientConfiguration();
7072
ArtifactoryDependenciesClientBuilder clientBuilder = new ArtifactoryDependenciesClientBuilder().setClientConfiguration(clientConfiguration, clientConfiguration.resolver);
7173
ArtifactoryClientConfiguration.PackageManagerHandler npmHandler = clientConfiguration.packageManagerHandler;
72-
NpmInstall npmInstall = new NpmInstall(clientBuilder,
74+
NpmInstallCi npmInstall = new NpmInstallCi(clientBuilder,
7375
clientConfiguration.resolver.getRepoKey(),
7476
npmHandler.getArgs(),
7577
clientConfiguration.getLog(),
7678
Paths.get(npmHandler.getPath() != null ? npmHandler.getPath() : "."),
7779
clientConfiguration.getAllProperties(),
78-
npmHandler.getModule());
80+
npmHandler.getModule(),
81+
clientConfiguration.npmHandler.isCiCommand());
7982
npmInstall.executeAndSaveBuildInfo(clientConfiguration);
8083
} catch (RuntimeException e) {
8184
ExceptionUtils.printRootCauseStackTrace(e, System.out);

Diff for: build-info-extractor-npm/src/main/java/org/jfrog/build/extractor/npm/types/NpmProject.java

+11-5
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,16 @@
1111
*/
1212
public class NpmProject {
1313

14-
private List<String> installationArgs;
14+
private List<String> commandArgs;
1515
private String resolutionRepository;
1616
private Path workingDir;
17+
private boolean ciCommand;
1718

18-
public NpmProject(List<String> installationArgs, String resolutionRepository, Path workingDir) {
19-
this.installationArgs = installationArgs;
19+
public NpmProject(List<String> commandArgs, String resolutionRepository, Path workingDir, boolean ciCommand) {
20+
this.commandArgs = commandArgs;
2021
this.resolutionRepository = resolutionRepository;
2122
this.workingDir = workingDir;
23+
this.ciCommand = ciCommand;
2224
}
2325

2426
public String getResolutionRepository() {
@@ -29,7 +31,11 @@ public Path getWorkingDir() {
2931
return workingDir;
3032
}
3133

32-
public List<String> getInstallationArgs() {
33-
return installationArgs;
34+
public List<String> getCommandArgs() {
35+
return commandArgs;
36+
}
37+
38+
public boolean isCiCommand() {
39+
return ciCommand;
3440
}
3541
}

Diff for: build-info-extractor-npm/src/test/java/org/jfrog/build/extractor/npm/extractor/NpmExtractorTest.java

+38-4
Original file line numberDiff line numberDiff line change
@@ -151,16 +151,50 @@ private Object[][] npmInstallProvider() {
151151
@SuppressWarnings("unused")
152152
@Test(dataProvider = "npmInstallProvider")
153153
public void npmInstallTest(Project project, Set<String> expectedDependencies, String args, boolean packageJsonPath) {
154+
runNpmTest(project, expectedDependencies, args, packageJsonPath, false);
155+
}
156+
157+
@DataProvider
158+
private Object[][] npmCiProvider() {
159+
return new Object[][]{
160+
{Project.A, Project.A.dependencies, "", true},
161+
{Project.A, Collections.emptySet(), "--only=dev", false},
162+
{Project.B, Project.B.dependencies, "", true},
163+
{Project.B, Collections.emptySet(), "--production", false},
164+
{Project.C, Project.C.dependencies, "", true},
165+
{Project.C, Project.A.dependencies, "--only=production", true}
166+
};
167+
}
168+
169+
@SuppressWarnings("unused")
170+
@Test(dataProvider = "npmCiProvider")
171+
public void npmCiTest(Project project, Set<String> expectedDependencies, String args, boolean packageJsonPath) {
172+
runNpmTest(project, expectedDependencies, args, packageJsonPath, true);
173+
}
174+
175+
private void runNpmTest(Project project, Set<String> expectedDependencies, String args, boolean packageJsonPath, boolean isNpmCi) {
154176
Path projectDir = null;
155177
try {
156-
// Run npm install
178+
// Prepare.
157179
projectDir = createProjectDir(project);
158180
Path path = packageJsonPath ? projectDir.resolve("package.json") : projectDir;
159-
NpmInstall npmInstall = new NpmInstall(dependenciesClientBuilder, virtualRepo, args, log, path, null, null);
160-
Build build = npmInstall.execute();
181+
if (isNpmCi) {
182+
// Run npm install to generate package-lock.json file.
183+
new NpmInstallCi(dependenciesClientBuilder, virtualRepo, args, log, path, null, null, false).execute();
184+
}
185+
186+
// Execute command.
187+
NpmInstallCi buildExecutor;
188+
if (isNpmCi) {
189+
buildExecutor = new NpmInstallCi(dependenciesClientBuilder, virtualRepo, args, log, path, null, null, true);
190+
} else {
191+
buildExecutor = new NpmInstallCi(dependenciesClientBuilder, virtualRepo, args, log, path, null, null, false);
192+
}
193+
Build build = buildExecutor.execute();
194+
195+
// Validate.
161196
assertEquals(build.getModules().size(), 1);
162197
Module module = build.getModules().get(0);
163-
// Check correctness of the module and dependencies
164198
assertEquals(module.getType(), "npm");
165199
assertEquals(module.getId(), project.getModuleId());
166200
Set<String> moduleDependencies = module.getDependencies().stream().map(Dependency::getId).collect(Collectors.toSet());

Diff for: build-info-extractor/src/main/java/org/jfrog/build/extractor/clientConfiguration/ArtifactoryClientConfiguration.java

+16-1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public class ArtifactoryClientConfiguration {
5555
public final BuildInfoHandler info;
5656
public final ProxyHandler proxy;
5757
public final PackageManagerHandler packageManagerHandler;
58+
public final NpmHandler npmHandler;
5859
public final PipHandler pipHandler;
5960
public final DotnetHandler dotnetHandler;
6061
public final DockerHandler dockerHandler;
@@ -72,6 +73,7 @@ public ArtifactoryClientConfiguration(Log log) {
7273
this.info = new BuildInfoHandler();
7374
this.proxy = new ProxyHandler();
7475
this.packageManagerHandler = new PackageManagerHandler();
76+
this.npmHandler = new NpmHandler();
7577
this.pipHandler = new PipHandler();
7678
this.dotnetHandler = new DotnetHandler();
7779
this.dockerHandler = new DockerHandler();
@@ -502,8 +504,21 @@ public void setModule(String packageManagerModule) {
502504
}
503505
}
504506

505-
public class PipHandler extends PrefixPropertyHandler {
507+
public class NpmHandler extends PrefixPropertyHandler {
508+
public NpmHandler() {
509+
super(root, PROP_NPM_PREFIX);
510+
}
511+
512+
public boolean isCiCommand() {
513+
return rootConfig.getBooleanValue(NPM_CI_COMMAND, false);
514+
}
506515

516+
public void setCiCommand(boolean ciCommand) {
517+
rootConfig.setBooleanValue(NPM_CI_COMMAND, ciCommand);
518+
}
519+
}
520+
521+
public class PipHandler extends PrefixPropertyHandler {
507522
public PipHandler() {
508523
super(root, PROP_PIP_PREFIX);
509524
}

Diff for: build-info-extractor/src/main/java/org/jfrog/build/extractor/clientConfiguration/ClientConfigurationFields.java

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public interface ClientConfigurationFields {
3333
String PACKAGE_MANAGER_ARGS = "package.manager.args";
3434
String PACKAGE_MANAGER_PATH = "package.manager.path"; // Path to package-manager execution dir
3535
String PACKAGE_MANAGER_MODULE = "package.manager.module"; // Custom module name for the build-info
36+
String NPM_CI_COMMAND = "npm.ci.command"; // Determines whether the npm build is 'npm install' or 'npm ci' command.
3637
String PIP_ENV_ACTIVATION = "pip.env.activation";
3738
String DOTNET_USE_DOTNET_CORE_CLI = "dotnet.use.dotnet.core.cli";
3839
String DOCKER_IMAGE_TAG = "docker.image.tag";

Diff for: build-info-extractor/src/main/java/org/jfrog/build/extractor/clientConfiguration/ClientProperties.java

+2
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ public interface ClientProperties {
4444

4545
String PROP_PACKAGE_MANAGER_PREFIX = ARTIFACTORY_PREFIX + "package.manager.";
4646

47+
String PROP_NPM_PREFIX = ARTIFACTORY_PREFIX + "npm.";
48+
4749
String PROP_PIP_PREFIX = ARTIFACTORY_PREFIX + "pip.";
4850

4951
String PROP_DOTNET_PREFIX = ARTIFACTORY_PREFIX + "dotnet.";

0 commit comments

Comments
 (0)