Skip to content

feat(publish): hidden kosli publish rego-policy, publish a versioned Rego policy - #1256

Merged
jumboduck merged 26 commits into
mainfrom
feat/create-rego-policy
Oct 12, 2026
Merged

jumboduck merged 26 commits into
mainfrom
feat/create-rego-policy

Conversation

@jumboduck

@jumboduck jumboduck commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Implements the CLI side of kosli-dev/server#7079. The server side is merged (kosli-dev/server#7269, #7296, #7307, #7311, #7320).

A new command publishes a versioned Rego policy under an immutable identifier, then waits for the server to validate the new version.

kosli publish rego-policy pr-approved ./policies/pr-approved/ \
    --input-schema trail \
    --name "Pull request approved" \
    --description "Every change reached main through an approved PR" \
    --comment "Initial version"

What it does

  • PUT /api/v2/rego-policies/{org}/{identifier} with the bundle, then polls GET .../versions/{n} until the version leaves pending (30 s by default).
  • PATH is a file or a directory, collected by the same policyBundle() as kosli evaluate policy --policy, so the same directory gives the same files and digest on both paths. The 100-file and 1 MiB caps are refused before sending.
  • Outcomes, each worded apart:
    • valid: prints rego policy 'pr-approved' version 1 is valid (sha256:...), exits 0.
    • rejected: the policy is at fault; prints the kind and message from the evaluator, exits 1.
    • failed: Kosli is at fault; says so and that publishing again retries, exits 1.
    • Wait expired: says the version is still pending and the full URL to read it later, exits 1.
  • Publishing the files and input schema of the newest version stores nothing and answers with that version. When it is already valid, it is not polled, and the table output says that nothing new was stored (and that --comment was not kept, when one was given).
  • --input-schema (trail | trails) is required. --name is needed on the first publish only. --output json always prints the version: with error.kind and error.message for a rejected or failed one, and as pending when the wait expires; the exit code is unchanged. --dry-run is supported.

Decisions worth a reviewer's attention

  • Named publish rego-policy, not create rego-policy as the issue's designed surface has it. Every call publishes a version, the first one included, which is what the server API ("Publish a version of a rego policy") and the help call it. It adds a publish verb; the CLI's other create commands are create-or-update. The issue and the server docs still say create rego-policy.
  • Hidden and beta, with no docs page, like evaluate policy, while the endpoint is behind is-server-side-evaluation-enabled. The publish verb has no other subcommand, so it is hidden too. A lifecycle test pins this, including that kosli docs generates no page; unhiding is the Hidden line plus that test.
  • requests.Do now accepts 202 as well as 200 and 201, because the publish returns 202 Accepted. Only those three: every caller reads the body as the whole answer, which a 204, 206 or 207 is not. A side effect: the Bitbucket case 202 ("still being indexed, please retry") can now run; before, a 202 was an error.
  • The identifier is escaped as one path segment, and "", . and .. are refused before sending. Before this, ../other/x left the org's route.
  • A 409 on the publish is not retried (DisableConflictRetry): the server re-runs a publish that loses a race itself, so a 409 is a refusal to report.
  • --description "" does not clear a description. Every flag refuses an empty value, as hack/empty-flag-audit/docs/2026-08-13-empty-value-decision.md decided.
  • A policy URL is refused as PATH. Decisions cite a published version, so its source must be on disk.
  • The poll loop is generic (internal/poll), shared with evaluations. Both packages use one options type, poll.Options, with their own defaults filled by Options.Or.
  • The evaluate still-pending message changes too: it now gives the full URL with the host, as the new command does, so a user on a non-default --host can use it as it is.

Not in this PR

  • Evaluation by identifier (kosli-dev/server#7081). The help says an evaluation runs the highest-numbered valid version, which is the server design that #7081 completes.
  • The empty-flag audit's own coverage check (audit.py) fails on main for three snapshot s3 flags; not touched here. The new command is in spec.json and coverage.json.

Testing

  • New: internal/poll (6 tests), internal/regopolicies (23 tests), and the command suite (24 tests), all against stubbed servers, because the local test server cannot validate a version.
  • make test_integration passed before the last 5 commits (second review fixes and the rename); those have offline tests only. make lint: 0 issues.
  • Not yet done: a manual publish against staging with an org that has is-server-side-evaluation-enabled.

🤖 Generated with Claude Code

jumboduck and others added 13 commits October 12, 2026 09:49
The rego-policy publish needs the same backoff wait as evaluations.
No behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rego-policy publish returns 202 Accepted, which Do reported as an
error. Only 200, 201 and 202 are accepted: callers read the body as the
whole answer, which a 204 (no body) or a 206/207 (part of one) is not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds the client for PUT /api/v2/rego-policies/{org}/{identifier},
which stores the next version as pending.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… valid version

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sions apart

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…publishing

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o-policy help

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A slash in an identifier was read as a path separator, and "../x" left
the org's route. The identifier and org are now escaped, and "", "." and
"..", which escaping cannot keep in place, are refused before sending.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rminal

Publishing content the newest valid version already holds answers with
that version, so the extra read was wasted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The server re-runs a publish that loses a race, so a 409 is a refusal.
Retrying it as a lock delayed the answer and lost the server's message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep only the fact each comment states that the code cannot show, and
drop the comments that restate a test name or what a helper does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added go Pull requests that update go code feat labels Oct 12, 2026
@claude

claude Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @jumboduck's task in 2m 8s —— View job


Review complete ✅

Scope: git diff f273e217..f3da36f8 (commits c711ef49, 67e6240b, 14bcab29, f3da36f8), touching only cmd/kosli/publishRegoPolicy.go and its test file.

  • Fetch previous review comments
  • Diff BEFORE..AFTER commits
  • Evaluate status of each previous finding
  • Check changed lines for new issues
  • Post summary comment

Previous findings

Both of my earlier findings are resolved (threads already marked resolved on GitHub) and neither location was touched by this batch of commits:

  1. JSON printed on rejected/failed outcomes (publishRegoPolicy.go:189) — fixed in 9cafca37, before this diff range. The notValidError block is unchanged context in the new diff.
  2. Still-pending hint missing the host (internal/regopolicies/wait.go) — fixed in 08ee1f44/96beeb94, before this diff range. wait.go isn't touched by c711ef49..f3da36f8.

New commits in this range

  • c711ef49 — names the published version in the error when WaitForTerminal fails for a reason other than still-pending (e.g. a 404 on read-back), and now prints the JSON version for any read error, not just still-pending.
  • 67e6240b — a pending (not just terminal) publish answer whose comment differs from --comment is now also reported as "nothing new was stored... so --comment was not kept".
  • 14bcab29 — doc-comment correction only.
  • f3da36f8 — guards against published.Version < 1 before attempting to read/poll a version that doesn't exist.

I traced the new logic (run() in publishRegoPolicy.go) against the new/updated tests (TestAnAnswerWithNoVersionIsNotReadBack, TestAFailedReadNamesTheVersionThatWasPublished, TestItSaysWhenAPublishStoredNothingNew) and the server-side Version/WaitForTerminal types in internal/regopolicies. The behavior is consistent and I found no new bugs, security issues, or regressions in the changed lines. No inline comments posted.

Note: I could not execute go build/go test in this sandboxed environment (bash approval required for build commands), so this is a static review of the diff against the existing test expectations, not a confirmed green test run.

Comment thread cmd/kosli/publishRegoPolicy.go
Comment thread internal/regopolicies/wait.go Outdated
jumboduck and others added 4 commits October 12, 2026 11:40
…alid one

With --output json, a rejected or failed version left stdout empty, so a
script could not read error.kind or error.message. The exit code is still 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ding message

A route without a host left a user on a non-default --host to fill it in.
The message and GetVersion now build the URL in one place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing message

The same message format as create rego-policy, so a user on a
non-default --host can use the URL as it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jumboduck

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Triage of the summary points (1 and 2 are answered on their threads):

  1. JSON output for a rejected or failed version: fixed in 9cafca3.
  2. Host in the still-pending hint: fixed in 08ee1f4, and the evaluate message the same way in 96beeb9.
  3. Effect of accepting 202 on other callers: skipped. No Kosli endpoint the CLI calls returns 202 other than this publish, and only 200, 201 and 202 are accepted. A 202 with an empty or non-JSON body was an error before too, with the same JSON parse message, so nothing that worked now fails.
  4. A publish answered with a version that is already rejected: skipped. The server returns an existing version only when it is pending or valid (the no-op rule); republishing rejected content stores a new pending version. The command would still handle it: the answer goes through the same notValidError check, without polling.
  5. A 409 at the command level: skipped. TestPublishDoesNotRetryAConflict covers the client, and the command returns the client's error on the same path that the 400, 403 and 413 tests cover.
  6. Coupling between the options types: fixed in 732f7d7. There is now one type: WaitOptions is an alias of poll.Options in both packages, and Options.Or fills the defaults, so the conversion is gone.

jumboduck and others added 5 commits October 12, 2026 11:58
A script reading stdout had no version number to read the version later.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Publishing the files and input schema of the newest valid version
answers with that version. The table output now says that nothing new
was stored, and that --comment was not kept when one was given.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both StillPendingError types rebuilt the URL in Error() and carried a
fallback branch that could not run. They now hold the URL itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The docHidden annotation still generates a page, with hidden: true;
like evaluate policy, this command gets none until it is unhidden.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each call publishes a version, the first one included, which is what the
server API and the help already call it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jumboduck jumboduck changed the title feat(create): hidden kosli create rego-policy, publish a versioned Rego policy feat(publish): hidden kosli publish rego-policy, publish a versioned Rego policy Oct 12, 2026
The version is already stored when the read fails, so the error now says
which one, and --output json still prints it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jumboduck and others added 3 commits October 12, 2026 12:37
…as dropped

A new version keeps the comment it was sent with, so an answer with a
different comment is an existing version, pending or not. The comment
clause now appears only when the comment really was not kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The JSON output is built from the Version struct, not the server's body
as sent, so it holds only the fields Version declares.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A 2xx without a version decoded to 0, and the command read versions/0
and reported whatever came back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jumboduck
jumboduck merged commit 481f600 into main Oct 12, 2026
23 checks passed
@jumboduck
jumboduck deleted the feat/create-rego-policy branch October 12, 2026 12:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat go Pull requests that update go code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants