mirror of
				https://github.com/actions/cache.git
				synced 2025-10-31 20:08:36 +08:00 
			
		
		
		
	Add fail-on-cache-miss option
This commit is contained in:
		
							parent
							
								
									6fd2d4538c
								
							
						
					
					
						commit
						a5631aba37
					
				|  | @ -205,3 +205,72 @@ test("restore with cache found for restore key", async () => { | ||||||
|     ); |     ); | ||||||
|     expect(failedMock).toHaveBeenCalledTimes(0); |     expect(failedMock).toHaveBeenCalledTimes(0); | ||||||
| }); | }); | ||||||
|  | 
 | ||||||
|  | test("Fail restore when fail on cache miss is enabled and primary key not found", async () => { | ||||||
|  |     const path = "node_modules"; | ||||||
|  |     const key = "node-test"; | ||||||
|  |     const restoreKey = "node-"; | ||||||
|  |     testUtils.setInputs({ | ||||||
|  |         path: path, | ||||||
|  |         key, | ||||||
|  |         restoreKeys: [restoreKey], | ||||||
|  |         failOnCacheMiss: true | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const failedMock = jest.spyOn(core, "setFailed"); | ||||||
|  |     const stateMock = jest.spyOn(core, "saveState"); | ||||||
|  |     const setCacheHitOutputMock = jest.spyOn(core, "setOutput"); | ||||||
|  |     const restoreCacheMock = jest | ||||||
|  |         .spyOn(cache, "restoreCache") | ||||||
|  |         .mockImplementationOnce(() => { | ||||||
|  |             return Promise.resolve(undefined); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |     await run(); | ||||||
|  | 
 | ||||||
|  |     expect(restoreCacheMock).toHaveBeenCalledTimes(1); | ||||||
|  |     expect(restoreCacheMock).toHaveBeenCalledWith([path], key, [restoreKey]); | ||||||
|  | 
 | ||||||
|  |     expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key); | ||||||
|  |     expect(setCacheHitOutputMock).toHaveBeenCalledTimes(0); | ||||||
|  | 
 | ||||||
|  |     expect(failedMock).toHaveBeenCalledWith( | ||||||
|  |         `Failed to restore cache entry. Exiting as fail-on-cache-miss is set. Input key: ${key}` | ||||||
|  |     ); | ||||||
|  |     expect(failedMock).toHaveBeenCalledTimes(1); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test("Fail restore when fail on cache miss is enabled and primary key doesn't match restored key", async () => { | ||||||
|  |     const path = "node_modules"; | ||||||
|  |     const key = "node-test"; | ||||||
|  |     const restoreKey = "node-"; | ||||||
|  |     testUtils.setInputs({ | ||||||
|  |         path: path, | ||||||
|  |         key, | ||||||
|  |         restoreKeys: [restoreKey], | ||||||
|  |         failOnCacheMiss: true | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const failedMock = jest.spyOn(core, "setFailed"); | ||||||
|  |     const stateMock = jest.spyOn(core, "saveState"); | ||||||
|  |     const setCacheHitOutputMock = jest.spyOn(core, "setOutput"); | ||||||
|  |     const restoreCacheMock = jest | ||||||
|  |         .spyOn(cache, "restoreCache") | ||||||
|  |         .mockImplementationOnce(() => { | ||||||
|  |             return Promise.resolve(restoreKey); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |     await run(); | ||||||
|  | 
 | ||||||
|  |     expect(restoreCacheMock).toHaveBeenCalledTimes(1); | ||||||
|  |     expect(restoreCacheMock).toHaveBeenCalledWith([path], key, [restoreKey]); | ||||||
|  | 
 | ||||||
|  |     expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key); | ||||||
|  |     expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1); | ||||||
|  |     expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "false"); | ||||||
|  | 
 | ||||||
|  |     expect(failedMock).toHaveBeenCalledWith( | ||||||
|  |         `Restored cache key doesn't match the given input key. Exiting as fail-on-cache-miss is set. Input key: ${key}` | ||||||
|  |     ); | ||||||
|  |     expect(failedMock).toHaveBeenCalledTimes(1); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | @ -11,6 +11,10 @@ inputs: | ||||||
|   restore-keys: |   restore-keys: | ||||||
|     description: 'An ordered list of keys to use for restoring stale cache if no cache hit occurred for key. Note `cache-hit` returns false in this case.' |     description: 'An ordered list of keys to use for restoring stale cache if no cache hit occurred for key. Note `cache-hit` returns false in this case.' | ||||||
|     required: false |     required: false | ||||||
|  |   fail-on-cache-miss: | ||||||
|  |     description: 'Fail the workflow if the cache is not found for the primary key' | ||||||
|  |     required: false | ||||||
|  |     default: "false" | ||||||
|   upload-chunk-size: |   upload-chunk-size: | ||||||
|     description: 'The chunk size used to split up large files during upload, in bytes' |     description: 'The chunk size used to split up large files during upload, in bytes' | ||||||
|     required: false |     required: false | ||||||
|  |  | ||||||
							
								
								
									
										10
									
								
								dist/restore-only/index.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								dist/restore-only/index.js
									
									
									
									
										vendored
									
									
								
							|  | @ -4978,7 +4978,8 @@ var Inputs; | ||||||
|     Inputs["Path"] = "path"; |     Inputs["Path"] = "path"; | ||||||
|     Inputs["RestoreKeys"] = "restore-keys"; |     Inputs["RestoreKeys"] = "restore-keys"; | ||||||
|     Inputs["UploadChunkSize"] = "upload-chunk-size"; |     Inputs["UploadChunkSize"] = "upload-chunk-size"; | ||||||
|     Inputs["EnableCrossOsArchive"] = "enableCrossOsArchive"; // Input for cache, restore, save action
 |     Inputs["EnableCrossOsArchive"] = "enableCrossOsArchive"; | ||||||
|  |     Inputs["FailOnCacheMiss"] = "fail-on-cache-miss"; // Input for cache, restore action
 | ||||||
| })(Inputs = exports.Inputs || (exports.Inputs = {})); | })(Inputs = exports.Inputs || (exports.Inputs = {})); | ||||||
| var Outputs; | var Outputs; | ||||||
| (function (Outputs) { | (function (Outputs) { | ||||||
|  | @ -50497,6 +50498,9 @@ function restoreImpl(stateProvider) { | ||||||
|             const enableCrossOsArchive = utils.getInputAsBool(constants_1.Inputs.EnableCrossOsArchive); |             const enableCrossOsArchive = utils.getInputAsBool(constants_1.Inputs.EnableCrossOsArchive); | ||||||
|             const cacheKey = yield cache.restoreCache(cachePaths, primaryKey, restoreKeys, {}, enableCrossOsArchive); |             const cacheKey = yield cache.restoreCache(cachePaths, primaryKey, restoreKeys, {}, enableCrossOsArchive); | ||||||
|             if (!cacheKey) { |             if (!cacheKey) { | ||||||
|  |                 if (core.getBooleanInput(constants_1.Inputs.FailOnCacheMiss) == true) { | ||||||
|  |                     throw new Error(`Failed to restore cache entry. Exiting as fail-on-cache-miss is set. Input key: ${primaryKey}`); | ||||||
|  |                 } | ||||||
|                 core.info(`Cache not found for input keys: ${[ |                 core.info(`Cache not found for input keys: ${[ | ||||||
|                     primaryKey, |                     primaryKey, | ||||||
|                     ...restoreKeys |                     ...restoreKeys | ||||||
|  | @ -50507,6 +50511,10 @@ function restoreImpl(stateProvider) { | ||||||
|             stateProvider.setState(constants_1.State.CacheMatchedKey, cacheKey); |             stateProvider.setState(constants_1.State.CacheMatchedKey, cacheKey); | ||||||
|             const isExactKeyMatch = utils.isExactKeyMatch(core.getInput(constants_1.Inputs.Key, { required: true }), cacheKey); |             const isExactKeyMatch = utils.isExactKeyMatch(core.getInput(constants_1.Inputs.Key, { required: true }), cacheKey); | ||||||
|             core.setOutput(constants_1.Outputs.CacheHit, isExactKeyMatch.toString()); |             core.setOutput(constants_1.Outputs.CacheHit, isExactKeyMatch.toString()); | ||||||
|  |             if (!isExactKeyMatch && | ||||||
|  |                 core.getBooleanInput(constants_1.Inputs.FailOnCacheMiss) == true) { | ||||||
|  |                 throw new Error(`Restored cache key doesn't match the given input key. Exiting as fail-on-cache-miss is set. Input key: ${primaryKey}`); | ||||||
|  |             } | ||||||
|             core.info(`Cache restored from key: ${cacheKey}`); |             core.info(`Cache restored from key: ${cacheKey}`); | ||||||
|             return cacheKey; |             return cacheKey; | ||||||
|         } |         } | ||||||
|  |  | ||||||
							
								
								
									
										10
									
								
								dist/restore/index.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								dist/restore/index.js
									
									
									
									
										vendored
									
									
								
							|  | @ -4978,7 +4978,8 @@ var Inputs; | ||||||
|     Inputs["Path"] = "path"; |     Inputs["Path"] = "path"; | ||||||
|     Inputs["RestoreKeys"] = "restore-keys"; |     Inputs["RestoreKeys"] = "restore-keys"; | ||||||
|     Inputs["UploadChunkSize"] = "upload-chunk-size"; |     Inputs["UploadChunkSize"] = "upload-chunk-size"; | ||||||
|     Inputs["EnableCrossOsArchive"] = "enableCrossOsArchive"; // Input for cache, restore, save action
 |     Inputs["EnableCrossOsArchive"] = "enableCrossOsArchive"; | ||||||
|  |     Inputs["FailOnCacheMiss"] = "fail-on-cache-miss"; // Input for cache, restore action
 | ||||||
| })(Inputs = exports.Inputs || (exports.Inputs = {})); | })(Inputs = exports.Inputs || (exports.Inputs = {})); | ||||||
| var Outputs; | var Outputs; | ||||||
| (function (Outputs) { | (function (Outputs) { | ||||||
|  | @ -50497,6 +50498,9 @@ function restoreImpl(stateProvider) { | ||||||
|             const enableCrossOsArchive = utils.getInputAsBool(constants_1.Inputs.EnableCrossOsArchive); |             const enableCrossOsArchive = utils.getInputAsBool(constants_1.Inputs.EnableCrossOsArchive); | ||||||
|             const cacheKey = yield cache.restoreCache(cachePaths, primaryKey, restoreKeys, {}, enableCrossOsArchive); |             const cacheKey = yield cache.restoreCache(cachePaths, primaryKey, restoreKeys, {}, enableCrossOsArchive); | ||||||
|             if (!cacheKey) { |             if (!cacheKey) { | ||||||
|  |                 if (core.getBooleanInput(constants_1.Inputs.FailOnCacheMiss) == true) { | ||||||
|  |                     throw new Error(`Failed to restore cache entry. Exiting as fail-on-cache-miss is set. Input key: ${primaryKey}`); | ||||||
|  |                 } | ||||||
|                 core.info(`Cache not found for input keys: ${[ |                 core.info(`Cache not found for input keys: ${[ | ||||||
|                     primaryKey, |                     primaryKey, | ||||||
|                     ...restoreKeys |                     ...restoreKeys | ||||||
|  | @ -50507,6 +50511,10 @@ function restoreImpl(stateProvider) { | ||||||
|             stateProvider.setState(constants_1.State.CacheMatchedKey, cacheKey); |             stateProvider.setState(constants_1.State.CacheMatchedKey, cacheKey); | ||||||
|             const isExactKeyMatch = utils.isExactKeyMatch(core.getInput(constants_1.Inputs.Key, { required: true }), cacheKey); |             const isExactKeyMatch = utils.isExactKeyMatch(core.getInput(constants_1.Inputs.Key, { required: true }), cacheKey); | ||||||
|             core.setOutput(constants_1.Outputs.CacheHit, isExactKeyMatch.toString()); |             core.setOutput(constants_1.Outputs.CacheHit, isExactKeyMatch.toString()); | ||||||
|  |             if (!isExactKeyMatch && | ||||||
|  |                 core.getBooleanInput(constants_1.Inputs.FailOnCacheMiss) == true) { | ||||||
|  |                 throw new Error(`Restored cache key doesn't match the given input key. Exiting as fail-on-cache-miss is set. Input key: ${primaryKey}`); | ||||||
|  |             } | ||||||
|             core.info(`Cache restored from key: ${cacheKey}`); |             core.info(`Cache restored from key: ${cacheKey}`); | ||||||
|             return cacheKey; |             return cacheKey; | ||||||
|         } |         } | ||||||
|  |  | ||||||
							
								
								
									
										3
									
								
								dist/save-only/index.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								dist/save-only/index.js
									
									
									
									
										vendored
									
									
								
							|  | @ -5034,7 +5034,8 @@ var Inputs; | ||||||
|     Inputs["Path"] = "path"; |     Inputs["Path"] = "path"; | ||||||
|     Inputs["RestoreKeys"] = "restore-keys"; |     Inputs["RestoreKeys"] = "restore-keys"; | ||||||
|     Inputs["UploadChunkSize"] = "upload-chunk-size"; |     Inputs["UploadChunkSize"] = "upload-chunk-size"; | ||||||
|     Inputs["EnableCrossOsArchive"] = "enableCrossOsArchive"; // Input for cache, restore, save action
 |     Inputs["EnableCrossOsArchive"] = "enableCrossOsArchive"; | ||||||
|  |     Inputs["FailOnCacheMiss"] = "fail-on-cache-miss"; // Input for cache, restore action
 | ||||||
| })(Inputs = exports.Inputs || (exports.Inputs = {})); | })(Inputs = exports.Inputs || (exports.Inputs = {})); | ||||||
| var Outputs; | var Outputs; | ||||||
| (function (Outputs) { | (function (Outputs) { | ||||||
|  |  | ||||||
							
								
								
									
										3
									
								
								dist/save/index.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								dist/save/index.js
									
									
									
									
										vendored
									
									
								
							|  | @ -4978,7 +4978,8 @@ var Inputs; | ||||||
|     Inputs["Path"] = "path"; |     Inputs["Path"] = "path"; | ||||||
|     Inputs["RestoreKeys"] = "restore-keys"; |     Inputs["RestoreKeys"] = "restore-keys"; | ||||||
|     Inputs["UploadChunkSize"] = "upload-chunk-size"; |     Inputs["UploadChunkSize"] = "upload-chunk-size"; | ||||||
|     Inputs["EnableCrossOsArchive"] = "enableCrossOsArchive"; // Input for cache, restore, save action
 |     Inputs["EnableCrossOsArchive"] = "enableCrossOsArchive"; | ||||||
|  |     Inputs["FailOnCacheMiss"] = "fail-on-cache-miss"; // Input for cache, restore action
 | ||||||
| })(Inputs = exports.Inputs || (exports.Inputs = {})); | })(Inputs = exports.Inputs || (exports.Inputs = {})); | ||||||
| var Outputs; | var Outputs; | ||||||
| (function (Outputs) { | (function (Outputs) { | ||||||
|  |  | ||||||
|  | @ -7,6 +7,7 @@ The restore action, as the name suggest, restores a cache. It acts similar to th | ||||||
| * `path` - A list of files, directories, and wildcard patterns to cache and restore. See [`@actions/glob`](https://github.com/actions/toolkit/tree/main/packages/glob) for supported patterns. | * `path` - A list of files, directories, and wildcard patterns to cache and restore. See [`@actions/glob`](https://github.com/actions/toolkit/tree/main/packages/glob) for supported patterns. | ||||||
| * `key` - String used while saving cache for restoring the cache | * `key` - String used while saving cache for restoring the cache | ||||||
| * `restore-keys` - An ordered list of prefix-matched keys to use for restoring stale cache if no cache hit occurred for key. | * `restore-keys` - An ordered list of prefix-matched keys to use for restoring stale cache if no cache hit occurred for key. | ||||||
|  | * `fail-on-cache-miss` - Fail the workflow if the cache is not found for the primary key | ||||||
| 
 | 
 | ||||||
| ## Outputs | ## Outputs | ||||||
| 
 | 
 | ||||||
|  | @ -95,7 +96,7 @@ steps: | ||||||
| 
 | 
 | ||||||
| ### Exit workflow on cache miss | ### Exit workflow on cache miss | ||||||
| 
 | 
 | ||||||
| You can use the output of this action to exit the workflow on cache miss. This way you can restrict your workflow to only initiate the build when `cache-hit` occurs, in other words, cache with exact key is found. | You can use `fail-on-cache-miss: true` to exit the workflow on a cache miss. This way you can restrict your workflow to only initiate the build when a cache with the exact key is found. | ||||||
| 
 | 
 | ||||||
| ```yaml | ```yaml | ||||||
| steps: | steps: | ||||||
|  | @ -106,10 +107,7 @@ steps: | ||||||
|     with: |     with: | ||||||
|       path: path/to/dependencies |       path: path/to/dependencies | ||||||
|       key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} |       key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} | ||||||
| 
 |       fail-on-cache-miss: true | ||||||
|   - name: Check cache hit |  | ||||||
|     if: steps.cache.outputs.cache-hit != 'true' |  | ||||||
|     run: exit 1 |  | ||||||
| 
 | 
 | ||||||
|   - name: Build |   - name: Build | ||||||
|     run: /build.sh |     run: /build.sh | ||||||
|  |  | ||||||
|  | @ -15,6 +15,10 @@ inputs: | ||||||
|     description: 'An optional boolean when enabled, allows windows runners to restore caches that were saved on other platforms' |     description: 'An optional boolean when enabled, allows windows runners to restore caches that were saved on other platforms' | ||||||
|     default: 'false' |     default: 'false' | ||||||
|     required: false |     required: false | ||||||
|  |   fail-on-cache-miss: | ||||||
|  |     description: 'Fail the workflow if the cache is not found for the primary key' | ||||||
|  |     required: false | ||||||
|  |     default: "false" | ||||||
| outputs: | outputs: | ||||||
|   cache-hit: |   cache-hit: | ||||||
|     description: 'A boolean value to indicate an exact match was found for the primary key' |     description: 'A boolean value to indicate an exact match was found for the primary key' | ||||||
|  |  | ||||||
|  | @ -3,7 +3,8 @@ export enum Inputs { | ||||||
|     Path = "path", // Input for cache, restore, save action
 |     Path = "path", // Input for cache, restore, save action
 | ||||||
|     RestoreKeys = "restore-keys", // Input for cache, restore action
 |     RestoreKeys = "restore-keys", // Input for cache, restore action
 | ||||||
|     UploadChunkSize = "upload-chunk-size", // Input for cache, save action
 |     UploadChunkSize = "upload-chunk-size", // Input for cache, save action
 | ||||||
|     EnableCrossOsArchive = "enableCrossOsArchive" // Input for cache, restore, save action
 |     EnableCrossOsArchive = "enableCrossOsArchive", // Input for cache, restore, save action
 | ||||||
|  |     FailOnCacheMiss = "fail-on-cache-miss" // Input for cache, restore action
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export enum Outputs { | export enum Outputs { | ||||||
|  |  | ||||||
|  | @ -44,6 +44,11 @@ async function restoreImpl( | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|         if (!cacheKey) { |         if (!cacheKey) { | ||||||
|  |             if (core.getBooleanInput(Inputs.FailOnCacheMiss) == true) { | ||||||
|  |                 throw new Error( | ||||||
|  |                     `Failed to restore cache entry. Exiting as fail-on-cache-miss is set. Input key: ${primaryKey}` | ||||||
|  |                 ); | ||||||
|  |             } | ||||||
|             core.info( |             core.info( | ||||||
|                 `Cache not found for input keys: ${[ |                 `Cache not found for input keys: ${[ | ||||||
|                     primaryKey, |                     primaryKey, | ||||||
|  | @ -63,6 +68,15 @@ async function restoreImpl( | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|         core.setOutput(Outputs.CacheHit, isExactKeyMatch.toString()); |         core.setOutput(Outputs.CacheHit, isExactKeyMatch.toString()); | ||||||
|  |         if ( | ||||||
|  |             !isExactKeyMatch && | ||||||
|  |             core.getBooleanInput(Inputs.FailOnCacheMiss) == true | ||||||
|  |         ) { | ||||||
|  |             throw new Error( | ||||||
|  |                 `Restored cache key doesn't match the given input key. Exiting as fail-on-cache-miss is set. Input key: ${primaryKey}` | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         core.info(`Cache restored from key: ${cacheKey}`); |         core.info(`Cache restored from key: ${cacheKey}`); | ||||||
| 
 | 
 | ||||||
|         return cacheKey; |         return cacheKey; | ||||||
|  |  | ||||||
|  | @ -14,11 +14,13 @@ interface CacheInput { | ||||||
|     key: string; |     key: string; | ||||||
|     restoreKeys?: string[]; |     restoreKeys?: string[]; | ||||||
|     enableCrossOsArchive?: boolean; |     enableCrossOsArchive?: boolean; | ||||||
|  |     failOnCacheMiss?: boolean; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function setInputs(input: CacheInput): void { | export function setInputs(input: CacheInput): void { | ||||||
|     setInput(Inputs.Path, input.path); |     setInput(Inputs.Path, input.path); | ||||||
|     setInput(Inputs.Key, input.key); |     setInput(Inputs.Key, input.key); | ||||||
|  |     setInput(Inputs.FailOnCacheMiss, "false"); | ||||||
|     input.restoreKeys && |     input.restoreKeys && | ||||||
|         setInput(Inputs.RestoreKeys, input.restoreKeys.join("\n")); |         setInput(Inputs.RestoreKeys, input.restoreKeys.join("\n")); | ||||||
|     input.enableCrossOsArchive !== undefined && |     input.enableCrossOsArchive !== undefined && | ||||||
|  | @ -26,12 +28,15 @@ export function setInputs(input: CacheInput): void { | ||||||
|             Inputs.EnableCrossOsArchive, |             Inputs.EnableCrossOsArchive, | ||||||
|             input.enableCrossOsArchive.toString() |             input.enableCrossOsArchive.toString() | ||||||
|         ); |         ); | ||||||
|  |     input.failOnCacheMiss && | ||||||
|  |         setInput(Inputs.FailOnCacheMiss, String(input.failOnCacheMiss)); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function clearInputs(): void { | export function clearInputs(): void { | ||||||
|     delete process.env[getInputName(Inputs.Path)]; |     delete process.env[getInputName(Inputs.Path)]; | ||||||
|     delete process.env[getInputName(Inputs.Key)]; |     delete process.env[getInputName(Inputs.Key)]; | ||||||
|     delete process.env[getInputName(Inputs.RestoreKeys)]; |     delete process.env[getInputName(Inputs.RestoreKeys)]; | ||||||
|  |     delete process.env[getInputName(Inputs.FailOnCacheMiss)]; | ||||||
|     delete process.env[getInputName(Inputs.UploadChunkSize)]; |     delete process.env[getInputName(Inputs.UploadChunkSize)]; | ||||||
|     delete process.env[getInputName(Inputs.EnableCrossOsArchive)]; |     delete process.env[getInputName(Inputs.EnableCrossOsArchive)]; | ||||||
| } | } | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 Marc Mueller
						Marc Mueller