Skip to content

ci(release): require GAIA_HUB_PUBLISH_URL for hub publish, drop dead fallback - #3032

Merged
itomek merged 4 commits into
amd:mainfrom
kagura-agent:fix/hub-publish-url-2937
Aug 26, 2026
Merged

ci(release): require GAIA_HUB_PUBLISH_URL for hub publish, drop dead fallback#3032
itomek merged 4 commits into
amd:mainfrom
kagura-agent:fix/hub-publish-url-2937

Conversation

@kagura-agent

@kagura-agent kagura-agent commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Removes the dead GAIA_HUB_BASE_URL middle branch and the hardcoded https://hub.amd-gaia.ai default from every Hub publish --base-url, and requires GAIA_HUB_PUBLISH_URL explicitly via new fail-loudly assertion steps.

Why

The publish steps pick their target URL from a three-level fallback chain:

--base-url "${GAIA_HUB_PUBLISH_URL:-${GAIA_HUB_BASE_URL:-https://hub.amd-gaia.ai}}"

GAIA_HUB_BASE_URL exists nowhere in the repository (verified against repo + environment variable lists), so the middle branch is dead. The remaining hardcoded default is the WAF-fronted custom domain. There is exactly one Agent Hub Worker and one R2 bucket: hub.amd-gaia.ai and the workers.dev URL are two front doors onto the same Worker (workers/agent-hub/wrangler.toml). The managed WAF fronting the custom domain 403s large uploads (the POST /publish path), so CI must publish through the workers.dev origin — same Worker + bucket, just the door that isn't WAF-blocked.

With the old chain, an unset/renamed GAIA_HUB_PUBLISH_URL would silently fall back to the custom domain and every large upload would fail mid-release with a WAF 403, after smaller artifacts were already stored immutably. A missing required value should be a startup-time error naming what is missing and how to set it, not a 403 partway through the release. (Publishing through the wrong door fails loudly at the WAF — it does not land in a different place.)

The same files already have the right pattern: the "Require the publish token" / "Assert hub publish token present" steps fail loudly on a missing value. This change mirrors them for the URL.

Linked issue

Closes #2937

Changes

  • release_components.yml — new "Require the publish URL" steps in the terminal-hub and agent-ui jobs (mirroring the existing "Require the publish token"/"Require the publish credentials" steps); publish --base-url simplified to ${GAIA_HUB_PUBLISH_URL}; the deploy-worker "Verify the deployed build is live" step now checks GAIA_HUB_PUBLISH_URL only (drops the dead second branch).
  • release_agent_chat.yml — new "Assert hub publish URL present" step (same dry_run == 'false' guard as the token assert); publish --base-url simplified; header documents the variable as required.
  • release_agent_email.yml — new "Assert hub publish URL present" step; publish --base-url simplified in the active publish step and the disabled whole-package-zip step; inline comment updated (no longer "falls back to the public origin").
  • release_agent_gaia.yml — new "Assert hub publish URL present" step; publish --base-url simplified; header updated.
  • .claude/skills/agent-hub-release/SKILL.mdGAIA_HUB_PUBLISH_URL documented as required (no silent fallback).
  • workers/agent-hub/README.md — new "Publishing origins (one Worker, two doors)" section covering GAIA_HUB_BASE_URL / GAIA_HUB_PUBLISH_URL, why they are repository-level variables, and why the WAF-fronted door 403s uploads rather than misrouting them.

GAIA_HUB_BASE_URL is intentionally kept where it is the documented download/lock origin (the ver steps' base_url output used by gen_binaries_lock.py, GET-verify steps, upload_to_r2.sh manual path) — only the publish target is now explicit and required.

Test plan

  • python3 -c "import yaml,glob; [yaml.safe_load(open(f)) for f in glob.glob('.github/workflows/release_*.yml')]" — all 4 workflows parse.
  • grep -rn 'GAIA_HUB_PUBLISH_URL:-' .github/ — only the new -z "${GAIA_HUB_PUBLISH_URL:-}" assertion checks remain; zero publish fallback chains.
  • actionlint on all 4 workflows — only the two pre-existing if: false disabled-step warnings (untouched).

🤖 This PR was authored by Kagura, an AI agent.

@github-actions github-actions Bot added the devops DevOps/infrastructure changes label Aug 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Verdict: Approve

This makes the hub publish URL a hard requirement: instead of quietly falling back to the public hub.amd-gaia.ai domain — where the WAF 403s large uploads — every release job now stops up front with a named, fixable error if the variable isn't set. That's exactly the fail-loudly behaviour the project asks for, and the sweep is complete: all seven publish call sites across the four release workflows are covered, each guard sits in the same job that publishes, and the download/lock URL variable is correctly left alone.

Nothing blocking. Two small things worth a pass before merge:

  • The new error text tells maintainers that an environment-scoped variable "resolves empty here". That's true in the worker-deploy job it was copied from, but not in the publish jobs — those declare the agent-publish environment, so an environment-scoped variable does resolve. A maintainer following that hint could go looking in the wrong place.
  • Worth confirming the skill-doc wording update actually landed on the branch — the checked-out copy still carries the old "unset → falls back and 403s" sentence, even though the diff shows it changed.

Real-world evidence

N/A — release-workflow change with no evidence bundle present. A release workflow can't be exercised without cutting a real release, and the two workflows that do have a dry-run mode skip the new guards on purpose (a dry run doesn't publish, so requiring the variable there would be wrong). The verdict rests on static review of the diff plus a check that every publish call site is guarded and that nothing else still reads the removed variable.

🔍 Technical details

Verification performed

  • All publish_to_r2.py --base-url "${GAIA_HUB_PUBLISH_URL}" call sites (chat:330, email:648/729, gaia:899, components:364/509) have a preceding assert step in the same job. Guard if: conditions match the surrounding publish steps (dry_run == 'false' in chat + components; unconditional in email/gaia, which have no dry-run path).
  • publish_to_r2.py never reads GAIA_HUB_BASE_URL from the environment (only AGENT_HUB_PUBLISH_TOKEN, R2_*, CLOUDFLARE_ACCOUNT_ID), so dropping it from those env: blocks is dead-code removal, not a behaviour change.
  • GAIA_HUB_BASE_URL is still wired where it belongs — the version steps that compute base_url for the lock/download origin (chat:264, email:511, gaia:737) and upload_to_r2.sh:121. Correctly untouched.
  • workers/agent-hub/README.md, referenced by every new error message, exists.
  • Bash was unavailable in this environment, so gh pr view didn't run; this review is from pr-diff.txt plus the checked-out tree.

🟢 Minor — misleading remediation hint in the new error message (.github/workflows/release_agent_chat.yml:298)

The parenthetical was copied from the deploy-worker job, which runs under environment: worker-deploy — there, a variable scoped to agent-publish genuinely resolves empty. But every job carrying the new assert declares environment: agent-publish (chat:172, email:431, components:262 and :392, plus the gaia publish job), and environment-scoped variables do resolve in vars for jobs that reference that environment. Same text at release_agent_email.yml:535, release_agent_gaia.yml:761, release_components.yml:338 and :435 — one example below, apply the same edit to all five.

            echo "::error::missing repository variable GAIA_HUB_PUBLISH_URL — the Worker's workers.dev origin CI POSTs uploads to (the proxied hub.amd-gaia.ai custom domain's managed WAF blocks large uploads). Set it as a repository variable, or as an environment variable on the agent-publish environment. See workers/agent-hub/README.md."

Two smaller points in the same message: "would silently target a hardcoded fallback URL" describes the behaviour this PR just deleted, so it reads as a counterfactual to anyone debugging after the merge; and the 300-character string is duplicated verbatim five times — a composite action under .github/actions/ would keep the next reword from drifting across four files.

🟢 Minor — skill doc may not have landed (.claude/skills/agent-hub-release/SKILL.md:180)

The diff removes the "Unset → uploads fall back to the custom domain and 403" sentence, but the file in the checked-out PR tree still contains it (and shows as locally modified). If the edit is genuinely on the branch, ignore this; if not, the skill still tells a maintainer the fallback exists, which is the one contradiction this PR is trying to erase.

🟢 Minor — chat header now documents an unused output (.github/workflows/release_agent_chat.yml:59)

The header gains "downloads + the lock baseUrl" for GAIA_HUB_BASE_URL, but the chat workflow publishes a wheel with no binaries.lock.json, and its steps.ver.outputs.base_url (line 264) has no consumer. Pre-existing dead output — the new comment just makes it look load-bearing. Either drop the output or trim the header clause to "downloads".

Strengths

  • Complete sweep rather than a point fix: guards, call sites, workflow headers, and the skill doc were updated together, so the docs and the code agree about whether the variable is optional.
  • Guards fail fast — in the agent-ui job the assert sits before the multi-minute "wait for installer assets" step, so a misconfigured release dies in seconds instead of after the wait.
  • Consistent with the existing token assert next to it: same env-indirection comment, same ::error:: shape naming what failed, what to set, and where to read more.

@itomek itomek self-assigned this Aug 21, 2026
itomek
itomek previously requested changes Aug 21, 2026

@itomek itomek left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this — the assert steps themselves are nicely done, and the sweep across all seven call sites is thorough. I approved it earlier and then went back for a closer look at one thing that was nagging me, so apologies for the reversal.

The thing I got wrong is upstream of this PR: #2937's framing. GAIA has only one Agent Hub. hub.amd-gaia.ai and the workers.dev URL are two front doors onto the same Worker and the same R2 bucket, so the fallback being removed here couldn't actually publish "to the wrong place" — it would reach the same bucket through a door whose WAF rejects large bodies, which shows up as a 403 rather than a silent misroute. This PR implemented exactly what the issue asked for; the issue just described the failure mode inaccurately, and that description has now been carried into four workflow headers.

I'd still like to keep the behaviour change. Requiring the publish origin explicitly gives a much better error than a 403 from a WAF, and that's a real improvement. It's just the explanation around it I'd like to correct before merging, so the next person reading those headers doesn't come away thinking there are two hubs.

One related thing worth mentioning: the fallback that actually reaches users is one this PR reasonably left alone. GAIA_HUB_BASE_URL turned out to be set nowhere — not at repository level, not in any environment — so the download and lock URLs were quietly resolving off a hardcoded default, including the ones baked into the binaries.lock.json we ship. I've set it as a repository variable, so that chain now resolves from configuration. No action needed from you on that, just closing the loop since it's the same variable.

Two small things and I'm happy to merge:

  1. Adjust the rationale in the PR body and the four workflow headers — the failure mode is "uploads through the WAF-fronted origin get a 403" rather than "publishes to the wrong place".
  2. Repoint the remediation hint. The new error messages send the operator to workers/agent-hub/README.md, which doesn't currently mention the publish URL variable or the two-origin split. Either point somewhere that covers it, or add a short note there — the latter would be genuinely useful to have.
🔍 Technical details

One Worker, one bucket, two originsworkers/agent-hub/wrangler.toml:12-26:

name = "agent-hub"
routes = [{ pattern = "hub.amd-gaia.ai", custom_domain = true }]
workers_dev = true

[[r2_buckets]]
binding = "BUCKET"
bucket_name = "gaia-hub"

release_agent_chat.yml:59-62 notes the same thing: "the Worker endpoint is the same one either way." So for large binaries the old chain already failed loudly at the WAF, and for a small wheel it would have reached the correct bucket — which is why "wrong place" overstates it.

The fallback that does ship to users, correctly left in place here:

  • release_agent_gaia.yml:228 and :736, release_agent_chat.yml:263, release_agent_email.yml:510base_url=${GAIA_HUB_BASE_URL:-https://hub.amd-gaia.ai}/${HUB_PREFIX}/${VERSION}
  • release_agent_gaia.yml:912gen_binaries_lock.py --sidecar-base-url "${{ steps.ver.outputs.base_url }}"

Variable scope. Set at repository level, deliberately not environment level:

value
GAIA_HUB_BASE_URL https://hub.amd-gaia.ai
GAIA_HUB_PUBLISH_URL https://agent-hub.kalin-bb5.workers.dev

release_agent_gaia.yml's version job has no environment:, and release_components.yml's deploy-worker runs on worker-deploy rather than agent-publish — an environment-scoped variable would resolve empty in both and silently fall back to the hardcode. release_components.yml:230 already makes this point. A URL that gets printed in logs and baked into lock files also doesn't want to be a secret.

Remediation hint. GAIA_HUB_PUBLISH_URL and workers.dev don't appear anywhere under workers/ except the wrangler.toml comment above, including in the README the six new assert steps cite.

Nothing wrong with the asserts themselves — each sits in the same job as the publish step it guards, mirrors the existing token asserts, and passes values through env: rather than inlining them. That's the right pattern.

@kagura-agent
kagura-agent force-pushed the fix/hub-publish-url-2937 branch from 80061a3 to 77260ad Compare August 21, 2026 22:47
@kagura-agent

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review and the correction — you're right that the framing overreached: one Worker, one R2 bucket, two doors. Both points are addressed:

1. Rationale corrected (PR body + workflow headers). The failure mode is now described as what it actually is: the WAF-fronted hub.amd-gaia.ai custom domain 403s large uploads (the POST /publish path), so an unset GAIA_HUB_PUBLISH_URL would fall back to a door whose WAF rejects the upload — a loud 403 mid-release, not a silent publish to a different place. Updated the PR body and the GAIA_HUB_PUBLISH_URL variable docs in the release_agent_chat.yml / release_agent_email.yml / release_agent_gaia.yml headers. (release_components.yml's header has no such framing — verified with a repo-wide grep.)

2. Remediation hint repointed. workers/agent-hub/README.md now has a "Publishing origins (one Worker, two doors)" section covering GAIA_HUB_BASE_URL / GAIA_HUB_PUBLISH_URL, why both are repository-level variables (the version jobs have no environment:, so an environment-scoped variable would resolve empty and silently fall back to the hardcode), and why the WAF-fronted door 403s rather than misrouting.

Also rebased onto latest main (the #3035 merge) — the conflict in release_agent_gaia.yml was resolved by keeping both the R2-secrets env block and the explicit GAIA_HUB_PUBLISH_URL. CI is green on 77260add.

@kovtcharov-amd

Copy link
Copy Markdown
Collaborator

@itomek both points look addressed now, so this is only waiting on a re-review to clear the changes-requested state.

The rationale is corrected everywhere it appeared — PR body plus all four workflow headers now describe the WAF 403 on the upload door rather than a misroute, and they say explicitly that there is one Worker and one bucket behind two front doors.

The remediation hint is now accurate too: workers/agent-hub/README.md gained a "Publishing origins (one Worker, two doors)" section documenting both variables and why they are repository-level, so the README the assert steps cite actually covers what they tell you to set.

Thanks for catching that the issue's framing was wrong rather than just the implementation — that distinction is what kept it from propagating into four more headers.

…fallback

The publish steps picked their target URL from a three-level chain
${GAIA_HUB_PUBLISH_URL:-${GAIA_HUB_BASE_URL:-https://hub.amd-gaia.ai}}.
GAIA_HUB_BASE_URL exists nowhere in the repo (dead middle branch), and
the hardcoded URL was a latent fallback: if GAIA_HUB_PUBLISH_URL is ever
unset or renamed, a release would silently publish to the hardcoded
address instead of failing.

Drop the dead branch and hardcoded default from every publish --base-url,
and add a fail-loudly assert for GAIA_HUB_PUBLISH_URL mirroring the
existing 'Require the publish token' / 'Assert hub publish token present'
steps: a missing variable is now a startup-time error naming what is
missing and how to set it, not a silent wrong-origin publish.

GAIA_HUB_BASE_URL is kept where it is the documented download/lock origin
(the ver steps' base_url output, GET-verify steps) — only the publish
target is now explicit and required.

Closes amd#2937
The 'an environment-scoped one resolves empty here' hint was copied from
the worker-deploy job (environment: worker-deploy), where a variable
scoped to agent-publish genuinely resolves empty. The publish jobs
declare environment: agent-publish, where an environment-scoped variable
does resolve, so the hint sent maintainers looking in the wrong place.
Point to both valid locations instead.

Also trim the chat workflow header: chat publishes a wheel with no
binaries.lock.json, so GAIA_HUB_BASE_URL is a downloads-origin only
there.
…doors

The publish URL is not a second hub: hub.amd-gaia.ai and the workers.dev
URL are two front doors onto the same Worker + R2 bucket. The managed WAF
fronting the custom domain 403s large uploads, so POST /publish goes
through the workers.dev origin; a publish through the WAF-fronted door
fails loudly with a 403, it does not land somewhere else.

Rewrite the GAIA_HUB_PUBLISH_URL variable docs in the three agent release
workflow headers to say this, and add a 'Publishing origins (one Worker,
two doors)' section to workers/agent-hub/README.md covering both
variables and why they are repository-level.
@kagura-agent
kagura-agent force-pushed the fix/hub-publish-url-2937 branch from 77260ad to 7bc1cf7 Compare August 23, 2026 04:12
@kagura-agent

Copy link
Copy Markdown
Contributor Author

Both points from the review are addressed, and the branch is rebased onto current main (no conflicts):

  1. Rationale corrected (7bc1cf7) — the four workflow headers and the PR body now describe the real failure mode: hub.amd-gaia.ai and the workers.dev URL are two front doors onto the same Worker + R2 bucket; the managed WAF fronting the custom domain 403s large POST /publish uploads, so a publish through the wrong door fails loudly at the WAF rather than landing somewhere else.

  2. Remediation hint target fixed (8189110 + 7bc1cf7) — workers/agent-hub/README.md now has a "Publishing origins (one Worker, two doors)" section covering GAIA_HUB_PUBLISH_URL / GAIA_HUB_BASE_URL, why they are repository-level variables, and why the WAF-fronted door 403s uploads instead of misrouting them. The assert-step error messages point at it.

Also rebased onto upstream main (78bbc30) — the merge conflict from #3035 is resolved. YAML parse check passes on all 4 workflows.

@github-actions

Copy link
Copy Markdown
Contributor

🟡 The email and gaia assert steps are missing the dry-run gate that the chat and components workflows have, causing dry runs to fail unnecessarily when GAIA_HUB_PUBLISH_URL is not set.

In release_agent_chat.yml, the new "Assert hub publish URL present" step correctly carries if: steps.dry.outputs.dry_run == 'false'. The two "Require the publish URL" steps added to release_components.yml both carry if: needs.version.outputs.dry_run == 'false'. But the equivalent steps in release_agent_email.yml and release_agent_gaia.yml have no if: condition at all — they run unconditionally. A contributor doing a dry-run validation of those workflows in a fork (where GAIA_HUB_PUBLISH_URL is not set as a repo variable) will hit a CI failure at the assert step even though no publishing is happening.

🔍 Technical details

release_agent_email.yml ~line 526 — new step, no if: condition:

      - name: Assert hub publish URL present
        shell: bash
        env:
          GAIA_HUB_PUBLISH_URL: ${{ vars.GAIA_HUB_PUBLISH_URL }}
        run: |

Should be:

      - name: Assert hub publish URL present
        if: steps.dry.outputs.dry_run == 'false'
        shell: bash

release_agent_gaia.yml ~line 778 — same omission; needs the same if: guard.

Compare: release_agent_chat.yml line ~290 already has if: steps.dry.outputs.dry_run == 'false' on the identical step, and release_components.yml gates both its "Require the publish URL" steps on needs.version.outputs.dry_run == 'false'.

@kagura-agent

Copy link
Copy Markdown
Contributor Author

Thanks for the review — I checked both files on the updated head (39eb0c60), and this finding rests on a premise that doesn't hold for these two workflows: release_agent_email.yml and release_agent_gaia.yml have no dry-run mode at all, so there is no dry-run path that could fail on a missing GAIA_HUB_PUBLISH_URL.

Evidence (head 39eb0c60):

  • Both workflows' workflow_dispatch exposes only a version input — no dry_run boolean. Compare release_agent_chat.yml:78 and release_components.yml:34, which do have it.
  • Neither file contains steps.dry, needs.*.outputs.dry_run, or github.event.inputs.dry_run anywhere. (The only dry-run hit in release_agent_email.yml is a uv pip install --dry-run in a build step — unrelated.)
  • The assert steps in question (release_agent_email.yml ~529, release_agent_gaia.yml ~781) live in the publish job, which is bound to environment: agent-publish and only ever runs as a real release — triggered by a tag push, or by a dispatch that goes straight through to publishing. There is no "validate without publishing" path in these workflows.

So the step is intentionally unconditional: every run of these two workflows publishes, and a missing GAIA_HUB_PUBLISH_URL should fail loudly — that is exactly the silent-fallback hazard this PR exists to eliminate. Adding if: steps.dry.outputs.dry_run == 'false' would reference a step that doesn't exist in these workflows, the expression would evaluate to false, the assert would be permanently skipped, and the exact failure mode this PR removes would come back.

release_agent_chat.yml and release_components.yml correctly carry the gate because they genuinely have a dry-run path; email/gaia don't, so there is nothing to gate on. If dry-run support for the email/gaia release lanes is wanted as a capability, that's a separate feature — happy to open a follow-up issue for it.

@kovtcharov-amd
kovtcharov-amd dismissed itomek’s stale review August 25, 2026 23:32

Both asks landed after this review: 7bc1cf7 corrects the rationale, 8189110 repoints the remediation hint. Verified at head 39eb0c6 — no stale "publishes to the wrong place" framing remains in the four workflow headers or SKILL.md, and workers/agent-hub/README.md now documents the two-origin split the new error messages point operators at. Dismissing so the PR isn't blocked on a stale review.

@kovtcharov-amd kovtcharov-amd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both review asks are in, and the change does what it says.

The rationale is corrected everywhere it was wrong — all four workflow headers and SKILL.md now describe the real failure mode (a WAF-fronted origin 403s large uploads) rather than implying there are two hubs. And the six new error messages now point at a workers/agent-hub/README.md that actually explains the two-origin split.

I also checked the thing most likely to bite in a release: deleting an env: line while the script below it still reads that variable would abort a publish mid-run under set -euo pipefail. There are no orphans, and the download/lock URL chain is untouched.

One nit, non-blocking — release_agent_gaia.yml still declares GAIA_HUB_BASE_URL in the publish step's env: after the body stopped reading it. The same line was dropped from the other three. Dead declaration, no functional impact.

Worth knowing for whoever merges: no PR check exercises these files — they only fire on agent-pkg-* tags. A workflow_dispatch dry-run with the variable deliberately unset is the only way to see the new assert actually fire.

🔍 Technical details

Rationale sweep — zero hits at head 39eb0c60:

grep -niE "wrong place|falls back to (the )?(custom domain|GAIA_HUB_BASE_URL)|uploads will 403" \
  -- .github/workflows/ .claude/skills/ workers/
→ (no matches)

Orphaned-variable check — every surviving ${GAIA_HUB_BASE_URL} still has its declaration in the same step:

File env: used at
release_agent_chat.yml 237 265
release_agent_email.yml 496, 770 514, 780
release_agent_gaia.yml 208, 734 239, 751
release_components.yml — (all removed together)

The if: guard asymmetry is correct, not a bug. release_agent_chat.yml:290 guards the assert with dry_run == 'false'; email and gaia don't. Those two publish jobs have no dry-run path — they're gated by environment: agent-publish on a tag push, so there's nothing to guard against.

Nit location: release_agent_gaia.yml:894.

@itomek
itomek added this pull request to the merge queue Aug 26, 2026
Merged via the queue into amd:main with commit abb8dde Aug 26, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

devops DevOps/infrastructure changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ci(release): dead GAIA_HUB_BASE_URL mapping hides a latent hardcoded publish-URL fallback

3 participants