mirror of
https://github.com/okxlin/appstore.git
synced 2026-09-25 00:01:02 +00:00
Merge pull request #4755 from okxlin/fix/renovate-dockerhub-preflight
Add a Docker Hub credential preflight
This commit is contained in:
@@ -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";
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user