Repository: renovatebot/renovate
Pull request #35253: fix(bazel): workspace injection
Author: login: Rayahhhmed
Labels: name: auto:no-done-comments
description: Don't say "Done" or "Please review" - request a review instead
Created: 2025-04-08T13:56:07Z | Merged: 2025-05-07T08:00:39Z

<!-- If this is your first pull request: sign the CLA with this GitHub app: https://cla-assistant.io/renovatebot/renovate -->
<!-- Make sure the `Allow edits and access to secrets by maintainers` checkbox is checked on this pull request. -->
<!-- Please read https://github.com/renovatebot/renovate/blob/main/.github/contributing.md before you create your pull request.-->

## Changes

<!-- Describe what behavior is changed by this PR. -->
I have a `.bazelrc` file which contains a custom in house Bazel registry inside my project folder, `common -- registry=file://%workspace%/third_party/registry`. The file registries do not get injected with the %workspace% value. 

There is a [corresponding issue](https://github.com/renovatebot/renovate/discussions/35252) for this PR.
There is a follow up [PR](https://github.com/renovatebot/renovate/pull/35273) branched off this PR. 

## Context

<!-- Describe why you're making these changes if it's not already explained in a corresponding issue. -->
<!-- If you're closing an existing issue with this pull request, use the keyword Closes #issue_number. -->
<!-- If you're referencing an issue with this pull request, put it in a Markdown list like this: - #issue_number. -->

## Documentation (please check one with an [x])

- [ ] I have updated the documentation, or
- [x] No documentation update is required

## How I've tested my work (please select one)

I have verified these changes via:

- [ ] Code inspection only, or
- [ ] Newly added/modified unit tests, or
- [ ] No unit tests but ran on a real repository, or
- [x] Both unit tests + ran on a real repository

<!-- Do you have any suggestions about this PR template? Edit it here: https://github.com/renovatebot/renovate/edit/main/.github/pull_request_template.md -->

<!-- Please do not force push to your PR's branch after you have created your PR, as doing so forces us to review the whole PR again. This makes it harder for us to review your work because we don't know what has changed. -->
<!-- PRs will always be squashed by us when we merge your work. Commit as many times as you need in this branch. -->



Diff:
diff --git a/lib/modules/manager/bazel-module/bazelrc.spec.ts b/lib/modules/manager/bazel-module/bazelrc.spec.ts
index ef6fcc780e0..b20e9314f7f 100644
--- a/lib/modules/manager/bazel-module/bazelrc.spec.ts
+++ b/lib/modules/manager/bazel-module/bazelrc.spec.ts
@@ -1,5 +1,13 @@
 import { codeBlock } from 'common-tags';
-import { BazelOption, CommandEntry, ImportEntry, parse, read } from './bazelrc';
+import {
+  BazelOption,
+  CommandEntry,
+  ImportEntry,
+  expandWorkspacePath,
+  parse,
+  read,
+  sanitizeOptions,
+} from './bazelrc';
 import { fs } from '~test/util';
 
 vi.mock('../../../util/fs');
@@ -243,5 +251,110 @@ describe('modules/manager/bazel-module/bazelrc', () => {
         new CommandEntry('build', [new BazelOption('jobs', '600')]),
       ]);
     });
+
+    it('when bazelrc has %workspace% paths in options', async () => {
+      const workspaceDir = '/tmp/workspace';
+      mockReadLocalFile({
+        '/tmp/workspace/.bazelrc': codeBlock`
+          build --output_base=%workspace%/bazel-out
+          `,
+      });
+      mockIsValidLocalPath({
+        '/tmp/workspace/.bazelrc': true,
+        '/tmp/workspace/bazel-out': true,
+      });
+      const result = await read(workspaceDir);
+      expect(result).toEqual([
+        new CommandEntry('build', [
+          new BazelOption('output_base', '/tmp/workspace/bazel-out'),
+        ]),
+      ]);
+    });
+
+    it('when bazelrc has %workspace% paths in imported files', async () => {
+      const workspaceDir = '/tmp/workspace';
+      mockReadLocalFile({
+        '/tmp/workspace/.bazelrc': codeBlock`
+          import %workspace%/shared.bazelrc
+          `,
+        '/tmp/workspace/shared.bazelrc': codeBlock`
+          build --output_base=%workspace%/bazel-out
+          build --test_output=%workspace%/test-results
+          `,
+      });
+      mockIsValidLocalPath({
+        '/tmp/workspace/.bazelrc': true,
+        '/tmp/workspace/shared.bazelrc': true,
+        '/tmp/workspace/bazel-out': true,
+        '/tmp/workspace/test-results': true,
+      });
+      const result = await read(workspaceDir);
+      expect(result).toEqual([
+        new CommandEntry('build', [
+          new BazelOption('output_base', '/tmp/workspace/bazel-out'),
+        ]),
+        new CommandEntry('build', [
+          new BazelOption('test_output', '/tmp/workspace/test-results'),
+        ]),
+      ]);
+    });
+  });
+
+  describe('expandWorkspacePath', () => {
+    it('should return original value if no workspace path', () => {
+      expect(expandWorkspacePath('--some-option', '/workspace')).toBe(
+        '--some-option',
+      );
+    });
+
+    it('should expand valid workspace path', () => {
+      fs.isValidLocalPath.mockImplementation((path: string) => {
+        return path === '/workspace' || path === '/workspace/some/path';
+      });
+      const workspaceDir = '/workspace';
+      const value = '%workspace%/some/path';
+      const expected = '/workspace/some/path';
+      expect(expandWorkspacePath(value, workspaceDir)).toBe(expected);
+    });
+
+    it('should throw error for invalid workspace path', () => {
+      const workspaceDir = '/workspace';
+      const value = '%workspace%/../../outside';
+      expect(expandWorkspacePath(value, workspaceDir)).toBeNull();
+    });
+  });
+
+  describe('sanitizeOptions', () => {
+    it('should handle options without values', () => {
+      const options = [new BazelOption('build')];
+      expect(sanitizeOptions(options, '/workspace')).toEqual(options);
+    });
+
+    it('should expand valid workspace paths', () => {
+      fs.isValidLocalPath.mockImplementation((path: string) => {
+        return (
+          path === '/workspace' ||
+          path === '/workspace/some/path' ||
+          path === '/workspace/other/path'
+        );
+      });
+      const options = [
+        new BazelOption('build', '%workspace%/some/path'),
+        new BazelOption('test', '%workspace%/other/path'),
+      ];
+      const result = sanitizeOptions(options, '/workspace');
+      expect(result).toEqual([
+        new BazelOption('build', '/workspace/some/path'),
+        new BazelOption('test', '/workspace/other/path'),
+      ]);
+    });
+
+    it('should throw error for invalid workspace paths', () => {
+      const options = [
+        new BazelOption('build', '%workspace%/valid/path'),
+        new BazelOption('test', '%workspace%/../../invalid'),
+      ];
+      expect(sanitizeOptions(options, '/workspace')).toEqual([]);
+    });
   });
 });
diff --git a/lib/modules/manager/bazel-module/bazelrc.ts b/lib/modules/manager/bazel-module/bazelrc.ts
index 39de1fab860..f093e98fd9a 100644
--- a/lib/modules/manager/bazel-module/bazelrc.ts
+++ b/lib/modules/manager/bazel-module/bazelrc.ts
@@ -96,6 +96,42 @@ function createEntry(line: string): BazelrcEntries | undefined {
   return undefined;
 }
 
+export function expandWorkspacePath(
+  value: string,
+  workspaceDir: string,
+): string | null {
+  if (!value.includes('%workspace%')) {
+    return value;
+  }
+  const absolutePath = upath.resolve(workspaceDir);
+  const expandedPath = value.replace('%workspace%', absolutePath);
+  if (!fs.isValidLocalPath(expandedPath)) {
+    return null;
+  }
+  return expandedPath;
+}
+
+export function sanitizeOptions(
+  options: BazelOption[],
+  workspaceDir: string,
+): BazelOption[] {
+  return options
+    .map((option) => {
+      if (!option.value) {
+        return option;
+      }
+      const expandedPath = expandWorkspacePath(option.value, workspaceDir);
+      if (!expandedPath) {
+        logger.debug(
+          `Skipping invalid workspace path: ${option.value} in ${workspaceDir}`,
+        );
+        return null;
+      }
+      return new BazelOption(option.name, expandedPath);
+    })
+    .filter(isNotNullOrUndefined);
+}
+
 export function parse(contents: string): BazelrcEntries[] {
   return contents
     .split('\n')
@@ -124,7 +160,10 @@ async function readFile(
   const results: CommandEntry[] = [];
   for (const entry of entries) {
     if (entry.entryType === 'command') {
-      results.push(entry);
+      const sanitizedOptions = sanitizeOptions(entry.options, workspaceDir);
+      results.push(
+        new CommandEntry(entry.command, sanitizedOptions, entry.config),
+      );
       continue;
     }
 


Reviews:
user: login: rarkins
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T05:00:44Z
user: login: Rayahhhmed
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T05:28:45Z
user: login: rarkins
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T05:55:14Z
user: login: Rayahhhmed
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T06:11:20Z
user: login: rarkins
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T06:17:28Z
user: login: rarkins
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T07:52:21Z
user: login: rarkins
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T04:34:24Z
user: login: viceice
body: otherwise lgtm
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T07:07:27Z
user: login: viceice
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T07:08:10Z
user: login: viceice
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-07T07:29:18Z

Review comments:
diff_hunk: @@ -96,6 +96,36 @@ function createEntry(line: string): BazelrcEntries | undefined {
   return undefined;
 }
 
+export function expandWorkspacePath(
+  value: string,
+  workspaceDir: string,
+): string {
+  if (!value.includes('%workspace%')) {
+    return value;
+  }
+  const absolutePath = upath.resolve(workspaceDir);
path: lib/modules/manager/bazel-module/bazelrc.ts
user: login: rarkins
body: What is a typical value for workspaceDir? Eg is it absolute like /tmp/foo or relative like some/path/? 
created_at: 2025-04-23T05:00:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -96,6 +96,36 @@ function createEntry(line: string): BazelrcEntries | undefined {
   return undefined;
 }
 
+export function expandWorkspacePath(
+  value: string,
+  workspaceDir: string,
+): string {
+  if (!value.includes('%workspace%')) {
+    return value;
+  }
+  const absolutePath = upath.resolve(workspaceDir);
path: lib/modules/manager/bazel-module/bazelrc.ts
user: login: Rayahhhmed
body: resolved as absolute path
created_at: 2025-04-23T05:28:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -96,6 +96,36 @@ function createEntry(line: string): BazelrcEntries | undefined {
   return undefined;
 }
 
+export function expandWorkspacePath(
+  value: string,
+  workspaceDir: string,
+): string {
+  if (!value.includes('%workspace%')) {
+    return value;
+  }
+  const absolutePath = upath.resolve(workspaceDir);
+  const expandedPath = value.replace('%workspace%', absolutePath);
+  if (!fs.isValidLocalPath(expandedPath)) {
+    throw new Error(
path: lib/modules/manager/bazel-module/bazelrc.ts
user: login: rarkins
body: Should this really be a thrown error? Will it abort the entire renovate run?
created_at: 2025-04-28T05:55:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -96,6 +96,36 @@ function createEntry(line: string): BazelrcEntries | undefined {
   return undefined;
 }
 
+export function expandWorkspacePath(
+  value: string,
+  workspaceDir: string,
+): string {
+  if (!value.includes('%workspace%')) {
+    return value;
+  }
+  const absolutePath = upath.resolve(workspaceDir);
+  const expandedPath = value.replace('%workspace%', absolutePath);
+  if (!fs.isValidLocalPath(expandedPath)) {
+    throw new Error(
path: lib/modules/manager/bazel-module/bazelrc.ts
user: login: Rayahhhmed
body: Being a bit defensive here in coding, if file doesnt exist it should be a problem. Would you recommend removing this?
created_at: 2025-04-29T06:11:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -96,6 +96,36 @@ function createEntry(line: string): BazelrcEntries | undefined {
   return undefined;
 }
 
+export function expandWorkspacePath(
+  value: string,
+  workspaceDir: string,
+): string {
+  if (!value.includes('%workspace%')) {
+    return value;
+  }
+  const absolutePath = upath.resolve(workspaceDir);
+  const expandedPath = value.replace('%workspace%', absolutePath);
+  if (!fs.isValidLocalPath(expandedPath)) {
+    throw new Error(
path: lib/modules/manager/bazel-module/bazelrc.ts
user: login: rarkins
body: I think the dependency should just be skipped, instead of ruining the whole run 
created_at: 2025-04-29T06:17:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -96,6 +96,42 @@ function createEntry(line: string): BazelrcEntries | undefined {
   return undefined;
 }
 
+export function expandWorkspacePath(
+  value: string,
+  workspaceDir: string,
+): string | undefined {
+  if (!value.includes('%workspace%')) {
+    return value;
+  }
+  const absolutePath = upath.resolve(workspaceDir);
+  const expandedPath = value.replace('%workspace%', absolutePath);
+  if (!fs.isValidLocalPath(expandedPath)) {
+    return undefined;
+  }
+  return expandedPath;
+}
+
+export function sanitizeOptions(
+  options: BazelOption[],
+  workspaceDir: string,
+): BazelOption[] {
+  return options
+    .map((option) => {
+      if (!option.value) {
+        return option;
+      }
+      const expandedPath = expandWorkspacePath(option.value, workspaceDir);
+      if (!expandedPath) {
+        logger.debug(
+          `Skipping invalid workspace path: ${option.value} in ${workspaceDir}`,
+        );
+        return undefined;
path: lib/modules/manager/bazel-module/bazelrc.ts
user: login: viceice
body: ```suggestion
        return null;
```

prefer null over undefined
created_at: 2025-05-07T07:06:51Z
author_association: MEMBER
diff_hunk: @@ -96,6 +96,42 @@ function createEntry(line: string): BazelrcEntries | undefined {
   return undefined;
 }
 
+export function expandWorkspacePath(
+  value: string,
+  workspaceDir: string,
+): string | undefined {
path: lib/modules/manager/bazel-module/bazelrc.ts
user: login: viceice
body: ```suggestion
): string | null {
```
created_at: 2025-05-07T07:07:58Z
author_association: MEMBER
diff_hunk: @@ -96,6 +96,42 @@ function createEntry(line: string): BazelrcEntries | undefined {
   return undefined;
 }
 
+export function expandWorkspacePath(
+  value: string,
+  workspaceDir: string,
+): string | undefined {
+  if (!value.includes('%workspace%')) {
+    return value;
+  }
+  const absolutePath = upath.resolve(workspaceDir);
+  const expandedPath = value.replace('%workspace%', absolutePath);
+  if (!fs.isValidLocalPath(expandedPath)) {
+    return undefined;
path: lib/modules/manager/bazel-module/bazelrc.ts
user: login: viceice
body: ```suggestion
    return null;
```
created_at: 2025-05-07T07:08:05Z
author_association: MEMBER

PR comments:
user: login: KoltesDigital
created_at: 2025-04-11T08:46:00Z
body: @Rayahhhmed thanks for the fix, I'm having the issue too.

FWIW, I'm on Windows and I have to use three slashes after `file:`.
author_association: NONE
user: login: Rayahhhmed
created_at: 2025-04-11T10:14:45Z
body: > @Rayahhhmed thanks for the fix, I'm having the issue too.
> 
> FWIW, I'm on Windows and I have to use three slashes after `file:`.

no worries! I have another PR opened [here](https://github.com/renovatebot/renovate/pull/35273), that also fixes the url bug if ur interested in following along 
author_association: CONTRIBUTOR
user: login: Rayahhhmed
created_at: 2025-04-20T00:42:51Z
body: @rarkins  could u please run the checks on this PR as well 🥺 
author_association: CONTRIBUTOR
user: login: Rayahhhmed
created_at: 2025-04-20T06:59:39Z
body: thank u so much @rarkins , could u also give the PR a 👀? Much appreciated in advanced! 
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-20T08:30:24Z
body: Hi there,

You are using `done` comments which cause a lot of noise/notifications. Instead, please use GitHub's web interface to request another review. Please read our [contributing guidelines](https://github.com/renovatebot/renovate/blob/main/.github/contributing.md#resolve-review-comments-instead-of-commenting) to reduce noise.

Good luck,

The Renovate team
author_association: CONTRIBUTOR
user: login: Rayahhhmed
created_at: 2025-04-23T00:20:13Z
body: @rarkins  hey! Would it be possible to assign reviewers (It does not allow me to assign it in the web dashboard), I am sorry if there is another approach to this. Would love some redirection if needed!
author_association: CONTRIBUTOR
user: login: rarkins
created_at: 2025-04-23T04:58:09Z
body: You should be able to request reviews yourself? Can you screenshot what you see?
author_association: CONTRIBUTOR
user: login: Rayahhhmed
created_at: 2025-04-23T05:28:33Z
body: > You should be able to request reviews yourself? Can you screenshot what you see?

In this image I dont see the cogs icon to assign reviewers. Since you have commented, I can only ask u for review and not someone else :O 
<img width="389" alt="Screenshot 2025-04-23 at 3 09 44 pm" src="https://github.com/user-attachments/assets/1b32cd01-f501-427b-b461-e2f5559b4944" />

author_association: CONTRIBUTOR
user: login: rarkins
created_at: 2025-04-23T06:06:54Z
body: You should use the circle/retry button in your screenshot and never @ mention that person 
author_association: CONTRIBUTOR
user: login: Rayahhhmed
created_at: 2025-04-28T04:21:19Z
body: Anyway I can speed the 👀 on this
author_association: CONTRIBUTOR
user: login: Rayahhhmed
created_at: 2025-05-01T00:01:41Z
body: if file contents are empty it shouldnt throw error logs causing Renovate to return non success status code
author_association: CONTRIBUTOR
user: login: Rayahhhmed
created_at: 2025-05-04T06:52:05Z
body: Is there anything else I can do to speed up the PR review process 🙏🏾 🥺 
author_association: CONTRIBUTOR
user: login: renovate-release
created_at: 2025-05-07T08:17:06Z
body: :tada: This PR is included in version 40.7.1 :tada:

The release is available on:
- [GitHub release](https://github.com/renovatebot/renovate/releases/tag/40.7.1)
- [npm package (@latest dist-tag)](https://www.npmjs.com/package/renovate/v/40.7.1)
- `40.7.1`

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: NONE