Merge pull request #4755 from okxlin/fix/renovate-dockerhub-preflight

Add a Docker Hub credential preflight
This commit is contained in:
okxlin
2026-07-12 11:10:54 +08:00
committed by GitHub
5 changed files with 156 additions and 6 deletions
+6
View File
@@ -61,4 +61,10 @@ After changing either controller:
python3 .github/scripts/test_renovate_app_version.py
```
Before a full scan, verify that Docker Hub accepts the configured repository secrets:
```bash
gh workflow run renovate-dockerhub-preflight.yml --ref localApps
```
Validate both JSON configurations with the Renovate version used by `.github/workflows/renovate.yml`, then run the self-hosted workflow manually. Confirm that its log lists only `docker-compose`, its branches start with `selfhosted-renovate/`, and the hosted App does not close them before retiring an older duplicate dashboard.
@@ -0,0 +1,76 @@
const AUTH_URL =
"https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/alpine:pull";
const MANIFEST_URL =
"https://registry-1.docker.io/v2/library/alpine/manifests/latest";
export async function checkDockerHubCredentials({
username,
password,
fetchImpl = fetch,
}) {
if (!username || !password) {
throw new Error(
"Configure DOCKERHUB_USERNAME and DOCKERHUB_TOKEN repository secrets.",
);
}
const basicAuth = Buffer.from(`${username}:${password}`).toString("base64");
const authResponse = await fetchImpl(AUTH_URL, {
headers: { authorization: `Basic ${basicAuth}` },
});
if (!authResponse.ok) {
throw new Error(
`Docker Hub rejected the configured credentials (HTTP ${authResponse.status}).`,
);
}
const authPayload = await authResponse.json();
const bearer = authPayload.token ?? authPayload.access_token;
if (!bearer) {
throw new Error("Docker Hub did not return a registry bearer token.");
}
const manifestResponse = await fetchImpl(MANIFEST_URL, {
headers: {
accept:
"application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.index.v1+json",
authorization: `Bearer ${bearer}`,
},
});
if (!manifestResponse.ok) {
throw new Error(
`Docker Hub authenticated manifest lookup failed (HTTP ${manifestResponse.status}).`,
);
}
return {
limit:
manifestResponse.headers.get("ratelimit-limit") ??
manifestResponse.headers.get("x-ratelimit-limit") ??
"not reported",
remaining:
manifestResponse.headers.get("ratelimit-remaining") ??
manifestResponse.headers.get("x-ratelimit-remaining") ??
"not reported",
};
}
if (
process.argv[1] &&
import.meta.url === pathToFileURL(resolve(process.argv[1])).href
) {
try {
const result = await checkDockerHubCredentials({
username: process.env.RENOVATE_DOCKERHUB_USERNAME,
password: process.env.RENOVATE_DOCKERHUB_TOKEN,
});
console.log(
`Docker Hub credential preflight passed (limit=${result.limit}, remaining=${result.remaining}).`,
);
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}
}
import { resolve } from "node:path";
import { pathToFileURL } from "node:url";
+53 -1
View File
@@ -211,6 +211,12 @@ class RenovateAppVersionTests(unittest.TestCase):
def test_self_hosted_renovate_requires_docker_hub_credentials(self):
workflow = (REPO_ROOT / ".github" / "workflows" / "renovate.yml").read_text(encoding="utf-8")
preflight = (
REPO_ROOT / ".github" / "workflows" / "renovate-dockerhub-preflight.yml"
).read_text(encoding="utf-8")
checker = (
REPO_ROOT / ".github" / "scripts" / "check_dockerhub_credentials.mjs"
).read_text(encoding="utf-8")
self.assertIn("configurationFile: .github/renovate-global.js", workflow)
self.assertIn(
@@ -220,7 +226,53 @@ class RenovateAppVersionTests(unittest.TestCase):
self.assertIn("RENOVATE_DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}", workflow)
self.assertIn("RENOVATE_DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}", workflow)
self.assertIn("name: Check Docker Hub credentials", workflow)
self.assertIn("Configure DOCKERHUB_USERNAME and DOCKERHUB_TOKEN repository secrets", workflow)
self.assertIn("node .github/scripts/check_dockerhub_credentials.mjs", workflow)
self.assertIn("workflow_dispatch:", preflight)
self.assertIn("node .github/scripts/check_dockerhub_credentials.mjs", preflight)
self.assertIn("https://auth.docker.io/token", checker)
self.assertIn("https://registry-1.docker.io/v2/library/alpine/manifests/latest", checker)
self.assertNotIn("console.log(token", checker)
self.assertNotIn("console.log(password", checker)
def test_docker_hub_preflight_validates_auth_and_manifest_without_logging_token(self):
checker = REPO_ROOT / ".github" / "scripts" / "check_dockerhub_credentials.mjs"
expression = f"""
import {{ checkDockerHubCredentials }} from {json.dumps(checker.as_uri())};
const requests = [];
const responses = [
{{ ok: true, status: 200, json: async () => ({{ token: 'bearer-secret' }}) }},
{{
ok: true,
status: 200,
headers: new Headers({{
'ratelimit-limit': '200;w=21600',
'ratelimit-remaining': '199;w=21600',
}}),
}},
];
const result = await checkDockerHubCredentials({{
username: 'test-user',
password: 'test-password',
fetchImpl: async (url, options) => {{
requests.push({{ url, authorization: options.headers.authorization }});
return responses.shift();
}},
}});
console.log(JSON.stringify({{ result, requests }}));
"""
result = subprocess.run(
["node", "--input-type=module", "-e", expression],
text=True,
capture_output=True,
check=False,
)
payload = json.loads(result.stdout)
self.assertEqual(0, result.returncode)
self.assertEqual("199;w=21600", payload["result"]["remaining"])
self.assertTrue(payload["requests"][0]["authorization"].startswith("Basic "))
self.assertEqual("Bearer bearer-secret", payload["requests"][1]["authorization"])
def test_self_hosted_renovate_uses_a_pinned_persistent_cache(self):
workflow = (REPO_ROOT / ".github" / "workflows" / "renovate.yml").read_text(
@@ -0,0 +1,20 @@
name: Renovate Docker Hub credential preflight
on:
workflow_dispatch:
permissions:
contents: read
jobs:
preflight:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Check Docker Hub credentials
env:
RENOVATE_DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
RENOVATE_DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
run: node .github/scripts/check_dockerhub_credentials.mjs
+1 -5
View File
@@ -40,11 +40,7 @@ jobs:
env:
RENOVATE_DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
RENOVATE_DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
run: |
if [[ -z "${RENOVATE_DOCKERHUB_USERNAME:-}" || -z "${RENOVATE_DOCKERHUB_TOKEN:-}" ]]; then
echo "::error::Configure DOCKERHUB_USERNAME and DOCKERHUB_TOKEN repository secrets before running Renovate."
exit 1
fi
run: node .github/scripts/check_dockerhub_credentials.mjs
- name: Run Renovate
uses: renovatebot/github-action@b50d2ba2bd928235abdcc14d06dfafc217f1c565 # v46.1.18
with: