Feat: Publish to multiple registries via matrix jobs - #87
Merged
tykeal merged 1 commit intoSep 9, 2026
Conversation
ModeSevenIndustrialSolutions
requested review from
a team
and
a balanced review from Copilot
September 2, 2026 14:48
Copilot started reviewing on behalf of
ModeSevenIndustrialSolutions
September 2, 2026 14:49
View session
ModeSevenIndustrialSolutions
force-pushed
the
feat/multi-registry-publish-targets
branch
from
September 2, 2026 15:01
92449e6 to
3a6b7dc
Compare
Copilot started reviewing on behalf of
ModeSevenIndustrialSolutions
September 2, 2026 15:02
View session
ModeSevenIndustrialSolutions
force-pushed
the
feat/multi-registry-publish-targets
branch
from
September 2, 2026 15:14
3a6b7dc to
6d95765
Compare
Copilot started reviewing on behalf of
ModeSevenIndustrialSolutions
September 2, 2026 15:15
View session
ModeSevenIndustrialSolutions
force-pushed
the
feat/multi-registry-publish-targets
branch
from
September 2, 2026 15:34
6d95765 to
95cfc98
Compare
Copilot started reviewing on behalf of
ModeSevenIndustrialSolutions
September 2, 2026 15:35
View session
ModeSevenIndustrialSolutions
force-pushed
the
feat/multi-registry-publish-targets
branch
2 times, most recently
from
September 2, 2026 15:46
95cfc98 to
7832dde
Compare
Copilot started reviewing on behalf of
ModeSevenIndustrialSolutions
September 2, 2026 15:47
View session
ModeSevenIndustrialSolutions
force-pushed
the
feat/multi-registry-publish-targets
branch
2 times, most recently
from
September 2, 2026 16:16
0a38289 to
29ddaa8
Compare
ModeSevenIndustrialSolutions
requested
a balanced review from Copilot
and removed request for
Copilot
September 2, 2026 16:46
Copilot started reviewing on behalf of
ModeSevenIndustrialSolutions
September 2, 2026 16:47
View session
ModeSevenIndustrialSolutions
force-pushed
the
feat/multi-registry-publish-targets
branch
from
September 2, 2026 17:01
29ddaa8 to
d5c4eb5
Compare
Copilot started reviewing on behalf of
ModeSevenIndustrialSolutions
September 2, 2026 17:02
View session
Both publish lanes in merge.yaml assumed exactly one registry, so there was no way to send a release to Nexus and registry.npmjs.org. Adding a second scalar input would not scale and would duplicate the credential and publish plumbing again. Take a JSON array of targets instead, fanned out as a matrix with fail-fast disabled: the legs are independent destinations for one artefact, and completing a partial publish is easier than redoing a whole one. Each leg names the registry in its job title, so a failed check identifies which registry failed. Per-target credentials work through credential-load-action's credential_name input, which selects the 1Password item. That is what makes several registries workable at all, since their credentials are unrelated. Authorisation is unchanged: the administrator-managed grants list still gates which items a repository may load, so naming an item cannot widen access. The target's auth mode routes the loaded credential to nexus_password or auth_token, which node-publish-action treats as mutually exclusive. Resolution lives in a reusable workflow rather than a job inside merge.yaml. merge.yaml is Gerrit-driven and cannot run in CI, so validation rules embedded there would have no coverage; as a separate workflow, testing.yaml calls it directly. build-test-release.yaml needs the same rules and can reuse it rather than carry a copy. Validation rejects malformed JSON, non-arrays, empty arrays, missing or malformed fields, non-https URLs, URLs without a trailing slash, unknown auth modes, and duplicate names or registry URLs. Duplicates matter because two legs publishing one version to one registry cannot both succeed, and the loser fails with EPUBLISHCONFLICT on a version that did publish. A bad list fails once, before any leg starts, and names the offending entry. The scalar registry inputs keep working, resolving to a one-element list with nexus auth and a deprecation notice. They change from required to optional, which existing callers do not notice. OIDC is deliberately absent as an auth mode. It needs 'id-token: write', which cannot be granted per matrix leg, so it belongs with lfreleng-actions#54 rather than here. Refs lfreleng-actions#52 Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Matthew Watkins <mwatkins@linuxfoundation.org>
ModeSevenIndustrialSolutions
force-pushed
the
feat/multi-registry-publish-targets
branch
from
September 2, 2026 17:15
d5c4eb5 to
52307e4
Compare
Copilot started reviewing on behalf of
ModeSevenIndustrialSolutions
September 2, 2026 17:16
View session
There was a problem hiding this comment.
🔵 Needs a closer look
Release publishing and credential routing are operationally sensitive, while CI exercises the resolver but not the complete publishing paths.
Review details
- Files reviewed: 7/7 changed files
- Comments generated: 0 new
- Review effort level: Balanced
tykeal
approved these changes
Sep 9, 2026
ModeSevenIndustrialSolutions
deleted the
feat/multi-registry-publish-targets
branch
September 9, 2026 15:18
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #52. Delivers the
merge.yamlhalf;build-test-release.yamland the OIDC auth mode follow separately (see Scope).Problem
Both publish lanes assumed exactly one registry, so there was no way to send a release to Nexus and
registry.npmjs.org. Adding a second scalar input would not scale and would duplicate the credential and publish plumbing a third time.Approach
A JSON array of targets, fanned out as a matrix:
fail-fast: false, because the legs are independent destinations for one artefact and completing a partial publish is easier than redoing a whole one. Each leg names its registry in the job title, so a failed check identifies which registry failed without opening it.Per-target credentials
This was the design crux — the lanes take no registry secrets, only 1Password access, so two registries with unrelated credentials looked impossible.
credential-load-actionalready solves it:credential_nameselects the 1Password item, and the admin-managedCREDENTIAL_LOAD_GRANTSallow-list still gates which items a repository may load. So naming an item here cannot widen access, and no new secrets are introduced.The target's
authmode then routes the loaded credential tonexus_passwordorauth_token, whichnode-publish-actiontreats as mutually exclusive.Why resolution is a reusable workflow
merge.yamlis Gerrit-driven and cannot run in CI —testing.yamlself-tests onlybuild-test.yaml, and its header explains why the other two are excluded. Validation rules embedded inmerge.yamlwould therefore have had no test coverage at all.As a separate workflow,
testing.yamlcalls it directly on every pull request.build-test-release.yamlneeds identical rules and can reuse it rather than carry a second copy that drifts.Validation
36 cases run in CI — 32 rejections and 4 acceptances — plus six standalone assertions. The suite extracts the shipped validator and drives it directly, because a called reusable workflow that fails marks the whole run red and so cannot be used to assert a rejection.
Rejected: malformed JSON, non-array, empty array, non-object entry, each missing field,
http, uppercase scheme, no host, no trailing slash,./..path segments, trailing-dot host, userinfo, bad port (non-numeric and out-of-range), unsupported auth,oidc, non-string auth, newline in name, non-string and unsafecredential_name, duplicate name, duplicate registry, duplicate by host case, duplicate by explicit:443, trailing-dot aliasing, more than 256 targets, and a multilinelabel.Accepted, so the rules cannot over-reject: a valid list, a different port, differing path case, and exactly 256 targets.
Beyond outcomes, the suite asserts properties:
::error::naming the target — a non-zero exit alone would let a Python traceback count as a correct rejectionlabelcannot forge a second workflow commandcountagrees with the matrix lengthDuplicates matter beyond tidiness: two legs publishing one version to one registry cannot both succeed, and the loser fails with
EPUBLISHCONFLICTon a version that did publish — the same confusing post-publish failure shape as #58 and #85.Backward compatibility
snapshot_registry_urlandrelease_registry_urlkeep working, resolving to a one-element list withnexusauth and emitting a deprecation notice. They change fromrequired: trueto optional, which existing callers do not notice.Each lane needs one of its two forms; leaving both empty fails, which the input table and the Publish Targets section both state.
Scope
merge.yamlsnapshot + releasenexusandtokenauthbuild-test-release.yamloidcauth modenode-publish-action#28; see below#52 lists
node-publish-action#28as a dependency covering both token and OIDC. Token shipped in v0.1.0 and is already pinned here, so the ONAP Nexus + npmjs.org case is deliverable today. OIDC additionally needsid-token: write, which GitHub cannot grant per matrix leg — the issue flags this itself — so it belongs with #54. The resolver rejectsauth: oidcwith a message naming it.Known limitation, called out rather than implied away
CI exercises the resolver, not the complete publish paths.
merge.yamlneeds a merged-commit context and real Nexus credentials, so neither lane can run on a pull request; the same is true ofbuild-test-release.yamland its tag-push context. That is precisely why the resolver was extracted — it is the part that can be tested — but the credential routing and matrix expansion themselves are first exercised by an instantiating repository's merge cycle.A tooling conflict worth flagging
Adding local
uses:references raised zizmor'sself-repositorycount from 1 to 5. zizmor recommends GitHub's$/...syntax, and it is right to — per its docs the$/form "is not subject to runtime filesystem state, meaning that it can't load an action that was cloned at runtime in a previous step."I applied it, and actionlint rejected all five as malformed reusable-workflow calls. Support is requested upstream in rhysd/actionlint#711 and #732, both open — so no version bump resolves it.
Silencing actionlint would be the worse trade: it validates whether a workflow call is well formed at all, where
self-repositoryis advisory. So the./form stays, with a line-scoped ignore naming the five calls, and the exact follow-up recorded.Line scoping rather than file scoping matters: a file entry would also cover step-level
uses: ./..., and a step-local action resolved after an attacker-controlled checkout is the case this rule most needs to catch.Net result: zizmor reports 0 findings, down from 1 on
main.Verification
prekacross all workflows and examples: cleanzizmor --persona auditor: 0 findings (was 1 onmain)allowvsaccept) that resolver-only testing could not have caught