Conversation
* Allow images to be pulled from azure-api.net * Fallback to pulling images from Azure api * Tests
There was a problem hiding this comment.
Pull request overview
Adds an Azure Container Registry (via *.azure-api.net) as a fallback source for updater/proxy images when the primary pull fails, to improve reliability of image availability for this action.
Changes:
- Add fallback image-pull behavior in
run()to retry pulls from an Azure registry when the primary pull fails. - Expand
ImageService.pull()repository allowlist to includeazure-api.net. - Update documentation and tests to reflect the new fallback registry behavior.
Reviewed changes
Copilot reviewed 5 out of 8 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/main.ts | Implements fallback logic for pulling updater/proxy images from Azure and passes the successful image refs to Updater. |
| src/image-service.ts | Updates image allowlist validation to permit azure-api.net registries. |
| tests/main.test.ts | Adjusts workflow tests for image pull failure/fallback scenarios. |
| tests/image-service.test.ts | Updates error-message assertion and adds an Azure-related test case. |
| docker/README.md | Documents existence of Azure fallback registry. |
| docker/containers.json | Formatting-only change (newline/EOF). |
| dist/main/index.js | Updates the bundled Action output to include the new fallback + allowlist logic. |
Comments suppressed due to low confidence (1)
src/image-service.ts:8
AZURE_REGISTRY_REis effectively never going to match: the pattern starts with$(end-of-string anchor), sovalidImageRepository()will reject all*.azure-api.net/...images. Update this to a start-anchored domain/host check (and preferably use a non-global regex with.test()), otherwise the Azure fallback pulls will always fail validation.
const MAX_RETRIES = 5 // Maximum number of retries
const INITIAL_DELAY_MS = 5000 // Initial delay in milliseconds for backoff
const AZURE_REGISTRY_RE = /$\w*\.azure-api\.net/g
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| core.startGroup('Pulling updater images') | ||
| let imagesPulled = false | ||
|
|
||
| try { | ||
| // Using sendMetricsWithPackageManager wrapper to inject package manager tag ti | ||
| // avoid passing additional parameters to ImageService.pull method | ||
| await ImageService.pull(updaterImage, sendMetricsWithPackageManager) | ||
| await ImageService.pull(PROXY_IMAGE_NAME, sendMetricsWithPackageManager) | ||
| } catch (error: unknown) { | ||
| if (error instanceof Error) { | ||
| await failJob( | ||
| apiClient, | ||
| 'Error fetching updater images', | ||
| error, | ||
| DependabotErrorType.Image | ||
| ) | ||
| return | ||
| await ImageService.pull(proxyImage, sendMetricsWithPackageManager) | ||
| imagesPulled = true | ||
| } catch { | ||
| core.warning('Primary image pull failed, attempting fallback') | ||
| } | ||
|
|
||
| if (!imagesPulled) { | ||
| updaterImage = `${FALLBACK_CONTAINER_REGISTRY}/${updaterImage}` | ||
| proxyImage = `${FALLBACK_CONTAINER_REGISTRY}/${proxyImage}` | ||
| try { | ||
| await ImageService.pull(updaterImage, sendMetricsWithPackageManager) | ||
| await ImageService.pull(proxyImage, sendMetricsWithPackageManager) | ||
| } catch (error: unknown) { |
There was a problem hiding this comment.
The fallback logic treats the updater+proxy pulls as all-or-nothing via a single imagesPulled flag. If the updater image pulls successfully from GHCR but the proxy image pull fails, the code will still rewrite both image names to the Azure registry and require both Azure pulls to succeed, potentially failing even though one image was already available locally. Track success per image and only fall back (and rewrite the image name passed to Updater) for the specific image(s) that failed to pull.
There was a problem hiding this comment.
I'm okay with the all-or-nothing approach. It's very unlikely one will fail and not the other
| try { | ||
| // Using sendMetricsWithPackageManager wrapper to inject package manager tag ti | ||
| // avoid passing additional parameters to ImageService.pull method | ||
| await ImageService.pull(updaterImage, sendMetricsWithPackageManager) | ||
| await ImageService.pull(PROXY_IMAGE_NAME, sendMetricsWithPackageManager) | ||
| } catch (error: unknown) { | ||
| if (error instanceof Error) { | ||
| await failJob( | ||
| apiClient, | ||
| 'Error fetching updater images', | ||
| error, | ||
| DependabotErrorType.Image | ||
| ) | ||
| return | ||
| await ImageService.pull(proxyImage, sendMetricsWithPackageManager) | ||
| imagesPulled = true | ||
| } catch { | ||
| core.warning('Primary image pull failed, attempting fallback') | ||
| } |
There was a problem hiding this comment.
catch { core.warning('Primary image pull failed...') } discards the underlying error, which makes diagnosing registry/auth/network issues difficult. Capture the error and include its message (and ideally which image failed) in the warning log.
| core.startGroup('Pulling updater images') | ||
| let imagesPulled = false | ||
|
|
||
| try { | ||
| // Using sendMetricsWithPackageManager wrapper to inject package manager tag ti | ||
| // avoid passing additional parameters to ImageService.pull method | ||
| await ImageService.pull(updaterImage, sendMetricsWithPackageManager) | ||
| await ImageService.pull(PROXY_IMAGE_NAME, sendMetricsWithPackageManager) | ||
| } catch (error: unknown) { | ||
| if (error instanceof Error) { | ||
| await failJob( | ||
| apiClient, | ||
| 'Error fetching updater images', | ||
| error, | ||
| DependabotErrorType.Image | ||
| ) | ||
| return | ||
| await ImageService.pull(proxyImage, sendMetricsWithPackageManager) | ||
| imagesPulled = true | ||
| } catch { | ||
| core.warning('Primary image pull failed, attempting fallback') | ||
| } | ||
|
|
||
| if (!imagesPulled) { | ||
| updaterImage = `${FALLBACK_CONTAINER_REGISTRY}/${updaterImage}` | ||
| proxyImage = `${FALLBACK_CONTAINER_REGISTRY}/${proxyImage}` | ||
| try { | ||
| await ImageService.pull(updaterImage, sendMetricsWithPackageManager) | ||
| await ImageService.pull(proxyImage, sendMetricsWithPackageManager) | ||
| } catch (error: unknown) { | ||
| if (error instanceof Error) { | ||
| await failJob( | ||
| apiClient, | ||
| 'Error fetching updater images', | ||
| error, | ||
| DependabotErrorType.Image | ||
| ) | ||
| return | ||
| } | ||
| } | ||
| } | ||
| core.endGroup() |
There was a problem hiding this comment.
If failJob(...) returns early inside the image-pull group, core.endGroup() is skipped, which can leave the GitHub Actions log grouping unbalanced. Consider wrapping the pull logic in a try/finally that always calls core.endGroup().
See below for a potential fix:
try {
// Using sendMetricsWithPackageManager wrapper to inject package manager tag ti
// avoid passing additional parameters to ImageService.pull method
await ImageService.pull(updaterImage, sendMetricsWithPackageManager)
await ImageService.pull(proxyImage, sendMetricsWithPackageManager)
imagesPulled = true
} catch {
core.warning('Primary image pull failed, attempting fallback')
}
if (!imagesPulled) {
updaterImage = `${FALLBACK_CONTAINER_REGISTRY}/${updaterImage}`
proxyImage = `${FALLBACK_CONTAINER_REGISTRY}/${proxyImage}`
try {
await ImageService.pull(updaterImage, sendMetricsWithPackageManager)
await ImageService.pull(proxyImage, sendMetricsWithPackageManager)
} catch (error: unknown) {
if (error instanceof Error) {
await failJob(
apiClient,
'Error fetching updater images',
error,
DependabotErrorType.Image
)
return
}
}
}
} finally {
core.endGroup()
}
| test('can pull images from azure-api.net', async () => { | ||
| pullMock.mockResolvedValue( | ||
| new Readable({ | ||
| read() {} | ||
| }) | ||
| ) | ||
|
|
||
| await expect( | ||
| ImageService.fetchImageWithRetry( | ||
| 'test.azure-api.net/ghcr.io/dependabot/dependabot-updater-npm', | ||
| {}, | ||
| docker, | ||
| undefined, // explicitly pass undefined for sendMetrics | ||
| 'dependabot' | ||
| ) | ||
| ).resolves.not.toThrow() | ||
| }) |
There was a problem hiding this comment.
This test exercises fetchImageWithRetry with an azure-api.net image name, but the new repository allowlist behavior is enforced in ImageService.pull() via validImageRepository(). Add a unit test that calls ImageService.pull() with an *.azure-api.net/... image and asserts it does not throw (mocking Docker as needed), so the allowlist change is actually covered.
There was a problem hiding this comment.
I don't think that's necessary.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
fcc0b8e to
e5faae2
Compare
Summary
We have added a fallback container registry in Azure that hosts a copy of all the container images we pull in this repo. This PR makes use of this fallback registry by,
azure-api.netUpdatercode with the image that successfully pulled