Skip to content
Open
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
32 changes: 24 additions & 8 deletions packages/nx/src/command-line/release/utils/release-graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -820,17 +820,33 @@ Valid values are: ${validReleaseVersionPrefixes

/**
* Merge docker options configured in project with release group config,
* project level configuration should take precedence
* project level configuration should take precedence.
*
* Only apply docker options if:
* 1. Project has explicit project-level docker config, OR
* 2. Project has the nx-release-publish target with @nx/docker:release-publish executor
*
* This prevents all projects in a group from inheriting docker options
* when only some projects should be dockerized. We specifically check for
* the nx-release-publish target as this is what the @nx/docker plugin creates
* for all docker projects.
*/
const projectHasDockerReleaseTarget =
projectGraphNode.data.targets?.['nx-release-publish']?.executor ===
'@nx/docker:release-publish';

const shouldApplyDockerOptions =
projectHasDockerReleaseTarget || projectDockerConfig;

const dockerOptions: NxReleaseDockerConfiguration & {
groupPreVersionCommand?: string;
} = Object.assign(
{},
releaseGroup.docker || {},
projectDockerConfig || {}
) as NxReleaseDockerConfiguration & {
groupPreVersionCommand?: string;
};
} = shouldApplyDockerOptions
? Object.assign(
{},
releaseGroup.docker || {},
projectDockerConfig || {}
)
: {};

/**
* currentVersionResolver, defaults to disk
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1866,4 +1866,205 @@ describe('ReleaseGroupProcessor', () => {
`);
});
});

describe('docker project filtering', () => {
beforeEach(() => {
// Set dry-run mode to avoid actual docker command execution in tests
process.env.NX_DRY_RUN = 'true';
});

afterEach(() => {
// Clean up dry-run mode
delete process.env.NX_DRY_RUN;
});

it('should only process projects with docker targets when group has docker config', async () => {
const { nxReleaseConfig, projectGraph, filters } =
await createNxReleaseConfigAndPopulateWorkspace(
tree,
`
__default__ ({ "projectsRelationship": "independent", "docker": { "preVersionCommand": "npx nx run-many -t docker:build", "versionSchemes": { "production": "{currentDate|YYMM.DD}.{shortCommitSha}" } } }):
- [email protected] [js] ({ "targets": { "nx-release-publish": { "executor": "@nx/docker:release-publish" } } })
- [email protected] [js] ({ "targets": { "nx-release-publish": { "executor": "@nx/docker:release-publish" } } })
- [email protected] [js]
`,
{
version: {
conventionalCommits: true,
},
},
mockResolveCurrentVersion
);

const processor = await createTestReleaseGroupProcessor(
tree,
projectGraph,
nxReleaseConfig,
filters
);

// Check that only docker projects got dockerOptions
const apiConfig =
processor['releaseGraph'].finalConfigsByProject.get('api');
const webAppConfig =
processor['releaseGraph'].finalConfigsByProject.get('web-app');
const sharedLibConfig =
processor['releaseGraph'].finalConfigsByProject.get('shared-lib');

// api and web-app should have docker options
expect(apiConfig).toBeDefined();
expect(webAppConfig).toBeDefined();
expect(Object.keys(apiConfig!.dockerOptions).length).toBeGreaterThan(0);
expect(Object.keys(webAppConfig!.dockerOptions).length).toBeGreaterThan(
0
);

// shared-lib should NOT have docker options (empty object)
expect(sharedLibConfig).toBeDefined();
expect(Object.keys(sharedLibConfig!.dockerOptions).length).toBe(0);
});

it('should not apply docker options to projects without docker targets', async () => {
const { nxReleaseConfig, projectGraph, filters } =
await createNxReleaseConfigAndPopulateWorkspace(
tree,
`
__default__ ({ "projectsRelationship": "independent", "docker": { "preVersionCommand": "npx nx run-many -t docker:build", "skipVersionActions": ["docker-app"] } }):
- [email protected] [js] ({ "targets": { "docker:build": { "executor": "@nx/docker:build" }, "nx-release-publish": { "executor": "@nx/docker:release-publish" } } })
- [email protected] [js]
`,
{
version: {
conventionalCommits: true,
},
},
mockResolveCurrentVersion
);

const processor = await createTestReleaseGroupProcessor(
tree,
projectGraph,
nxReleaseConfig,
filters
);

// Check finalConfigsByProject to ensure npm-package doesn't have docker options
const dockerAppConfig =
processor['releaseGraph'].finalConfigsByProject.get('docker-app');
const npmPackageConfig =
processor['releaseGraph'].finalConfigsByProject.get('npm-package');

// docker-app should have docker options
expect(dockerAppConfig).toBeDefined();
expect(
Object.keys(dockerAppConfig!.dockerOptions).length
).toBeGreaterThan(0);

// npm-package should NOT have docker options (empty object)
expect(npmPackageConfig).toBeDefined();
expect(Object.keys(npmPackageConfig!.dockerOptions).length).toBe(0);
});

it('should handle mixed release group with npm and docker projects correctly', async () => {
const { nxReleaseConfig, projectGraph, filters } =
await createNxReleaseConfigAndPopulateWorkspace(
tree,
`
backend-apps ({ "projectsRelationship": "fixed", "docker": { "preVersionCommand": "npx nx affected -t docker:build", "versionSchemes": { "production": "{currentDate|YYMM.DD}.{shortCommitSha}" } } }):
- [email protected] [js] ({ "targets": { "docker:build": { "executor": "@nx/docker:build" }, "nx-release-publish": { "executor": "@nx/docker:release-publish" } } })
- [email protected] [js] ({ "targets": { "docker:build": { "executor": "@nx/docker:build" }, "nx-release-publish": { "executor": "@nx/docker:release-publish" } } })
- [email protected] [js]
`,
{
version: {
conventionalCommits: true,
},
},
mockResolveCurrentVersion
);

const processor = await createTestReleaseGroupProcessor(
tree,
projectGraph,
nxReleaseConfig,
filters
);

// Verify that only docker projects got dockerOptions
const apiGatewayConfig =
processor['releaseGraph'].finalConfigsByProject.get('api-gateway');
const authServiceConfig =
processor['releaseGraph'].finalConfigsByProject.get('auth-service');
const sharedUtilsConfig =
processor['releaseGraph'].finalConfigsByProject.get('shared-utils');

expect(apiGatewayConfig).toBeDefined();
expect(authServiceConfig).toBeDefined();
expect(sharedUtilsConfig).toBeDefined();

// Docker projects should have docker options
expect(
Object.keys(apiGatewayConfig!.dockerOptions).length
).toBeGreaterThan(0);
expect(
Object.keys(authServiceConfig!.dockerOptions).length
).toBeGreaterThan(0);

// shared-utils should NOT have docker options (empty object)
expect(Object.keys(sharedUtilsConfig!.dockerOptions).length).toBe(0);
});

it('should skip docker projects that have no new version (no changes)', async () => {
const { nxReleaseConfig, projectGraph, filters } =
await createNxReleaseConfigAndPopulateWorkspace(
tree,
`
__default__ ({ "projectsRelationship": "independent", "docker": { "preVersionCommand": "npx nx run-many -t docker:build", "versionSchemes": { "production": "{currentDate|YYMM.DD}.{shortCommitSha}" } } }):
- [email protected] [js] ({ "targets": { "nx-release-publish": { "executor": "@nx/docker:release-publish" } } })
- [email protected] [js] ({ "targets": { "nx-release-publish": { "executor": "@nx/docker:release-publish" } } })
`,
{
version: {
conventionalCommits: true,
},
},
mockResolveCurrentVersion
);

const processor = await createTestReleaseGroupProcessor(
tree,
projectGraph,
nxReleaseConfig,
filters
);

// Simulate that only app-with-changes has a new version
// Set up mock BEFORE calling processGroups
mockDeriveSpecifierFromConventionalCommits.mockImplementation(
(_, __, ___, ____, { name: projectName }) => {
if (projectName === 'app-with-changes') {
return 'patch';
}
return 'none'; // app-no-changes has no changes
}
);

await processor.processGroups();

// Verify that processDockerProjects doesn't throw when a project has no new version
await processor.processDockerProjects('production', '2024.10.test');

const versionData = processor.getVersionData();

// app-with-changes should have both semver and docker version
expect(versionData['app-with-changes'].newVersion).toBe('1.0.1');
expect(versionData['app-with-changes'].dockerVersion).toBe(
'2024.10.test'
);

// app-no-changes should have neither (newVersion is null, dockerVersion stays null)
expect(versionData['app-no-changes'].newVersion).toBeNull();
expect(versionData['app-no-changes'].dockerVersion).toBeNull();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,28 @@ export class ReleaseGroupProcessor {
if (Object.keys(finalConfigForProject.dockerOptions).length === 0) {
continue;
}

// Additionally verify that the project actually has the docker release target
// This prevents projects without docker capability from being processed
// even if they inherited group-level docker config.
// We specifically check for nx-release-publish with @nx/docker:release-publish
// executor as this is what the @nx/docker plugin creates for docker projects.
const projectNode = this.projectGraph.nodes[project];
const hasDockerReleaseTarget =
projectNode.data.targets?.['nx-release-publish']?.executor ===
'@nx/docker:release-publish';

if (!hasDockerReleaseTarget) {
continue;
}

// Skip projects that don't have a new version (no changes)
// This prevents docker tag failures when the version is empty
const projectVersionData = this.versionData.get(project);
if (!projectVersionData.newVersion) {
continue;
}
Comment on lines +284 to +289
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't this break the current behavior for projects that have skipVersionActions set to true?


dockerProjects.set(project, finalConfigForProject);
}
// If no docker projects to process, exit early to avoid unnecessary loading of docker handling
Expand Down
15 changes: 15 additions & 0 deletions packages/nx/src/command-line/release/version/test-utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ describe('parseGraphDefinition', () => {
);
expect(testGraph).toMatchInlineSnapshot(`
{
"groupConfigs": {
"__default__": {
"projectsRelationship": "fixed",
},
},
"projects": {
"projectD": {
"alternateNameInManifest": undefined,
Expand Down Expand Up @@ -81,6 +86,11 @@ describe('parseGraphDefinition', () => {
);
expect(testGraph).toMatchInlineSnapshot(`
{
"groupConfigs": {
"__default__": {
"projectsRelationship": "independent",
},
},
"projects": {
"projectA": {
"alternateNameInManifest": undefined,
Expand Down Expand Up @@ -123,6 +133,11 @@ describe('parseGraphDefinition', () => {
);
expect(testGraph).toMatchInlineSnapshot(`
{
"groupConfigs": {
"__default__": {
"projectsRelationship": "independent",
},
},
"projects": {
"projectA": {
"alternateNameInManifest": undefined,
Expand Down
9 changes: 7 additions & 2 deletions packages/nx/src/command-line/release/version/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ export class ExampleNonSemverVersionActions extends VersionActions {
}

export function parseGraphDefinition(definition: string) {
const graph = { projects: {} as any };
const graph = { projects: {} as any, groupConfigs: {} as any };
const lines = definition.trim().split('\n');
let currentGroup = '';
let groupConfig = {};
Expand All @@ -340,11 +340,13 @@ export function parseGraphDefinition(definition: string) {
}

// Match group definitions with JSON config
const groupMatch = line.match(/^(\w+)\s*\(\s*(\{.*?\})\s*\):$/);
const groupMatch = line.match(/^([\w-]+)\s*\(\s*(\{.*\})\s*\):$/);
if (groupMatch) {
currentGroup = groupMatch[1];
groupConfig = JSON.parse(groupMatch[2]);
groupRelationship = groupConfig['projectsRelationship'] || 'independent';
// Store the full group config
graph.groupConfigs[currentGroup] = groupConfig;
return;
}

Expand Down Expand Up @@ -592,7 +594,10 @@ function setupGraph(tree: any, graph: any) {

// Add to releaseGroups
if (!groups[group]) {
// Use stored groupConfig if available, otherwise create default config
const storedGroupConfig = graph.groupConfigs?.[group] || {};
groups[group] = {
...storedGroupConfig,
projectsRelationship: relationship,
projects: [],
} as any;
Expand Down