feat: EPv2 reconcile-content-branches reusable workflow - #23
Conversation
Automates Enterprise Portal v2 content-branch creation for any Replicated vendor, generalized from FirstResponse's implementation. EPv2 links docs content to a release only when a git branch in the docs repo is named exactly the bare release version label (e.g. 0.3.312, not v0.3.312). With "Require matching content for release versions" on, a release with no matching branch 404s. This workflow reconciles those branches on a schedule. Rule: main tracks the latest release; each new release gets its own branch off main; branches are created once and never force-updated (vendors own them after creation and can edit/sync themselves). Idempotent and bounded. - workflow_call reusable workflow with inputs + replicated_api_token secret - generalized stdlib-only Python script + unit tests (19 tests, green) - workflow fetches the script from this repo at the pinned ref so callers only reference the workflow - vendor-facing README + root README table row Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Round-2 review revisions to the EPv2 reconcile-content-branches workflow. Model redesign: drop the wrong "main tracks the latest release" framing. main is the vendor's canonical working docs branch that release branches are cut from; the job is coverage (a branch for every release across configured channels), not mirroring a single "latest". Rewritten README + docstrings. Base-branch drift: reframe branches as cut from main when reconcile runs (not a point-in-time snapshot of what shipped). Add a timing section recommending a release-published trigger, modeled on the sibling notify-release workflow, with cron as the fallback. Security: add least-privilege `permissions: contents: write` to the reconcile job. Eliminate workflow_ref version skew by making workflow_ref required (no default) and hard-failing when the caller pins `uses:` to a SHA/tag while leaving workflow_ref at main. Observability / fail-loudly: write a per-channel run summary (seen/selected/ non-bare/created/existed, dry-run plan folded in) to $GITHUB_STEP_SUMMARY; emit a ::warning:: when a channel has releases but zero bare matches; SystemExit when no configured channel resolves at all. Polish: default channels Stable,Beta (Unstable opt-in); document no-cleanup story; guard RELEASE_LIMIT with a clean SystemExit; correct the GetVersionByRepoAndBranch(ctx, repoID, branch) signature; add unit tests for the new pure logic. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
A thought: github release triggers are a fairly decent way to trigger this workflow, but not perfect. |
B1: the skew guard branched on github.workflow_ref (the caller's trigger ref), which is orthogonal to how the caller pinned uses:. That produced a false positive that hard-failed the README's own release:[published] usage (a release from a tag makes workflow_ref end in @refs/tags/vX.Y.Z) and a false negative (uses:@shaa with workflow_ref:shaB slipped through). Drop the trigger-ref heuristic: GitHub cannot expose a reusable workflow's own uses: ref to itself, so an in-workflow guard cannot prove skew. Keep workflow_ref required + the empty-string error, and replace the hard ::error:: with an informational ::notice:: echoing both refs for human review. Update README pinning/how-it-works sections accordingly. LOW-2: README caveat that release_limit must exceed live releases per channel, else older-but-still-live releases silently miss coverage. LOW-3: render_summary footnote when the per-channel "To create" column sums above the deduped branch total (a version live on two channels is counted per-channel but created once). Adds two unit tests. Tests: 33 green. actionlint clean. (rw-3zz) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
[reusable_workflows/reviewer — product-owner] Round 3 — VERDICT: SHIP. Worth it. Round-1/2 product blockers stay resolved and this round lands clean.
LOW / non-blocking: the residual "can't detect ref skew" gap is inherent to GitHub, and the README is upfront about it — no action. Future: your own note about a Replicated release webhook being the ideal trigger is a good follow-up (worth its own issue), not a blocker for this PR. Tests: 33 green at |
|
[replicated_docs/obsidian — implementor] Docs half is up: replicatedhq/replicated-docs#4431 (draft). New "Automating content branch creation" section on the EPv2 versioned-docs page, with the branch-name contract, a caller snippet mirroring this PR's README, timing/drift, the bare-version requirement, and cleanup. Links in it point at |
Restructure the reconcile-content-branches README so a vendor adopting cold hits what/why, then the copy-paste quick start, then the inputs and secrets tables, then the pinning note, then caveats. Consolidate the overlapping intro/what-it-does/model prose and group timing, version-label, release_limit, unstable, and cleanup caveats under one section. No workflow or script changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
In the future I might suggest we work the python logic here into replicated cli instead, then instead of that python script we can just pin a version of replicated cli. |
|
|
||
| # The branch-name rule is the EP v2 contract; prove it holds before we touch | ||
| # any refs. Standard-library only, so no pip install. | ||
| - name: Run unit tests |
There was a problem hiding this comment.
Im iffy about running tests during a reusable workflow, unless this becomes a prelight that does a dry run with users values/secrets to make sure everything is ready before we actually run it.
But even then it should be optional, it might not make sense to do this everytime.
What this is
A reusable GitHub Actions workflow (
workflow_call) that auto-creates the per-releasedocs content branches Enterprise Portal v2 needs. A vendor drops one small caller into
their docs repo, hands it an app slug plus a vendor API token, and it keeps a content
branch alive for every release across their configured channels. Generalized from
FirstResponse's implementation so any Replicated vendor can adopt it.
Why it's worth adopting
EPv2 with "Require matching content for release versions" turned on resolves a release's
docs by looking up a git branch in the docs repo named exactly the bare version label:
0.3.312, notv0.3.312, notrelease/v0.3.312. A release with no matching branch is acustomer-visible 404 in the portal. That rule comes straight from vandoor
(
handlers/vendor-api/replv3/enterprise_portal/content_version_pin.go->GetVersionByRepoAndBranch).Today that branch is a manual step, one per release, and forgetting it breaks the portal
quietly. This workflow removes that whole class of failure. It's about as cheap as a
dependency gets: stdlib-only Python, a ~90-line workflow, 33 unit tests over the pure
decision logic, and no
pip install. The branch-name rule is the single load-bearingcontract, and it's unit-tested, so the branches we create always match what the portal
looks for.
How it works
Coverage, not mirroring.
mainis the vendor's canonical working docs branch and thebase new release branches are cut from. It does not track or represent any one release or
channel. The job is coverage: make sure a content branch exists for every release across
the configured channels, because in EPv2 each customer sees the docs for the release their
license is pinned to. The workflow only ever fills in missing branches. It never
force-updates and it never deletes.
Honest timing. Each release branch is cut from
mainas it exists when reconcileruns, not as a point-in-time snapshot of what shipped. If
mainmoved on before reconcileran, the new branch captures the newer
main. So the closer reconcile runs to therelease, the closer the branch tracks what actually shipped. Because of that, the README
recommends firing on the GitHub
release: [published]event, with a cron fallback sonothing slips through. Event-driven keeps branches aligned with what shipped; the timer is
the safety net.
Script delivery. The caller won't have the Python file, so the workflow does a second
actions/checkoutofreplicatedhq/reusable-workflowsat a pinned ref and runs thescript from there. Callers only ever reference the workflow. A required
workflow_refinput (no default) lets a caller pinning
uses:to a SHA fetch the script from the sameSHA. The alternative (a composite action, or heredoc-ing the script inline) would fragment
the interface or make the script unreadable and untestable, so checkout-of-self won.
Design calls worth a look
app_slugis an input, not a secret. Secrets aren't forwarded to reusable workflowsunless the caller redeclares them, and the app slug isn't sensitive (it's in the vendor
portal URL). The vendor API token stays a required secret.
GITHUB_TOKENfor branchcreation, so the caller grants
permissions: contents: writeand supplies no GitHubtoken. The script checkout uses
persist-credentials: false.workflow_refguard is honest about what it can prove. It rejects an empty valueand prints both the
workflow_refand the caller's trigger ref as a::notice::for ahuman to eyeball. It does not infer skew from
github.workflow_ref: that's the caller'stop-level trigger ref, orthogonal to how they pinned
uses:, so a heuristic on itproduces false positives (a
release: [published]run from a tag looks "pinned") andfalse negatives (
uses:@shaAwithworkflow_ref: shaBslips through). GitHub can'texpose a reusable workflow's own ref to itself, so keeping the two in sync stays on the
caller, and the README says so plainly.
env:and are quoted in the script; thetoken is never echoed.
runs-onisubuntu-22.04per the repo's guidance to avoidubuntu-latest.Files
.github/workflows/epv2-reconcile-content-branches.yaml: the reusableworkflow_callworkflowscripts/epv2_reconcile_content_branches.py: generalized, stdlib-only reconcile scriptscripts/test_epv2_reconcile_content_branches.py: 33 unit tests over the pure decision logic.github/workflows/epv2-reconcile-content-branches/README.md: vendor-facing docsREADME.md: new row in the Available Workflows tableVerification
python3 -m unittest discover -s scripts -p 'test_*.py'-> 33 tests, all green.actionlintpasses on the workflow. YAML parses.Draft on purpose. Not for merge yet.
🤖 Generated with Claude Code