Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f697641
feat(convctl): CI-native output formats — github, sarif, markdown
vrabbi Sep 15, 2026
9730682
feat(convctl): lint, one run over a whole config tree
vrabbi Sep 15, 2026
892be16
feat(convctl): bounded sampling for --live on large clusters
vrabbi Sep 15, 2026
83f3586
ci: publish the convctl container image
vrabbi Sep 15, 2026
51dd990
feat(release): Homebrew, Scoop, deb/rpm, and an honest version command
vrabbi Sep 15, 2026
bef3a15
feat(convctl): --package, a Crossplane package as a schema source
vrabbi Sep 15, 2026
2a8f364
feat(actions): setup-convctl, a composite Action that verifies by def…
vrabbi Sep 15, 2026
e599919
feat(actions): convctl-test, with JUnit, a job summary and annotations
vrabbi Sep 15, 2026
e9e336f
feat(actions): convctl-diff, a sticky pull-request comment with the d…
vrabbi Sep 15, 2026
738c0a3
feat(actions): convctl-fleet, and a reference workflow that runs
vrabbi Sep 15, 2026
2e6a71d
test(actions): a workflow that exercises every Action, including the …
vrabbi Sep 15, 2026
7d50103
docs: record phase 13 as shipped, with its deviations and limits
vrabbi Sep 15, 2026
af1bb6c
fix(actions): pass step outputs through env, and shellcheck the Actions
vrabbi Sep 15, 2026
f742c47
fix(ci): pin actions to tags that exist, and check that they do
vrabbi Sep 15, 2026
2b95f68
fix(ci): do not report an API failure as a missing action reference
vrabbi Sep 15, 2026
78f8933
fix(release): the published verification command could never succeed
vrabbi Sep 15, 2026
5054cee
fix: make the binary's version match the tag people install
vrabbi Sep 15, 2026
f6b9243
test(actions): exercise the Actions against the build under review
vrabbi Sep 15, 2026
42ba623
test(actions): test the cache the way the cache actually behaves
vrabbi Sep 15, 2026
3f5f129
fix: address the review findings on the Actions, sampling and packaging
vrabbi Sep 15, 2026
0640290
test(actions): diff two configs that share a hub
vrabbi Sep 15, 2026
abfdaae
fix: expose a supplied binary as convctl, and scope sampling validation
vrabbi Sep 15, 2026
148db41
test(convctl): cover the early-stop sampling report at the live call …
vrabbi Sep 15, 2026
0d9118b
fix(test): do not shadow the cap builtin
vrabbi Sep 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions .github/actions/convctl-diff/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# `convctl-diff`

Runs `convctl diff` and upserts the delta as a **sticky** pull-request
comment — updated in place as the branch changes, rather than appended to on
every push.

```yaml
permissions:
contents: read
pull-requests: write

steps:
# convctl comes from setup-convctl, once per job.
- uses: terasky-oss/declarative-conversion-operator/.github/actions/setup-convctl@v1
- uses: terasky-oss/declarative-conversion-operator/.github/actions/convctl-diff@v1
with:
config: apis/widgets/conversion.yaml
xrd: apis/widgets/xrd.yaml
live: "true"
kubeconfig: ${{ secrets.KUBECONFIG_PROD }}
```

## Exit codes are not all failures

`convctl diff` exits 1 when it finds a delta. That is the thing being
reported, not a failure, so **the job stays green by default**. Exit 2 — a
usage error, or a cluster it could not reach — always fails, because a gate
that reported "no deltas" when it could not run would be silently useless.

Set `fail-on-delta: true` to make any delta fail the job.

## A comment that says "no deltas" rather than vanishing

When there is nothing to report, the comment is edited to say so. A comment
that disappears reads as *"the check stopped running"*, which is the wrong
message to send about a check that ran and passed.

## Multiple configs in one pull request

`comment-tag` identifies the comment to update, and defaults to the config
path — so two configs in one pull request get one comment each with no
configuration. Set it explicitly if you want something else.

## Forks

A `pull_request` event from a fork has a read-only token. The Action emits a
**notice** and leaves the delta in the job summary, rather than failing: a red
check a contributor cannot fix teaches them to ignore red checks.

Needs `pull-requests: write` to comment.

## Requires `setup-convctl`

This Action consumes `convctl` from `PATH` and does not install it. Run
[`setup-convctl`](../setup-convctl) first — once per job, however many of
these Actions follow.

That is not an ergonomic preference. A composite action cannot reference a
local action by path once published: `./…` resolves against the **consumer's**
workspace, so a nested setup step would work in this repository's own tests
and fail for everyone else.
206 changes: 206 additions & 0 deletions .github/actions/convctl-diff/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
name: convctl diff
description: >-
Run convctl diff and upsert the coverage delta as a sticky pull-request
comment, updated in place as the branch changes.

inputs:
config:
description: >-
Path to a conversion config. Pass twice (newline-separated) to compare
two files, or once with live to compare against the cluster.
required: true
xrd:
description: Path to the XRD.
required: false
default: ""
crd:
description: Path to the CRD.
required: false
default: ""
live:
description: Compare the config against what the cluster has.
required: false
default: "false"
kubeconfig:
description: Kubeconfig contents for a live comparison.
required: false
default: ""
context:
description: Kubeconfig context for a live comparison.
required: false
default: ""
comment:
description: Upsert the delta as a pull-request comment.
required: false
default: "true"
comment-tag:
description: >-
Identifies the comment to update. Defaults to the config path, so two
configs in one pull request get one comment each without configuration.
required: false
default: ""
fail-on-delta:
description: >-
Fail the job when a delta is found. Off by default — a coverage delta
is a review artifact, not automatically a failure. A usage or cluster
error (exit 2) always fails.
required: false
default: "false"
token:
description: Token used to upsert the comment.
required: false
default: ${{ github.token }}

outputs:
exit-code:
description: convctl's own exit code (1 means deltas were found).
value: ${{ steps.run.outputs.exit-code }}
has-deltas:
description: Whether any delta was found.
value: ${{ steps.run.outputs.has-deltas }}
markdown-path:
description: Path of the rendered markdown.
value: ${{ steps.run.outputs.markdown-path }}

runs:
using: composite
steps:
# convctl is not installed here. A composite action cannot reference a
# local action by path once it is published: `./…` resolves against the
# CONSUMER's workspace, not this repository, so a nested setup step
# works in these tests and fails for everyone else. Run setup-convctl
# first — which also means installing once per job rather than once per
# Action.
- name: Check convctl is on PATH
shell: bash
run: |
set -euo pipefail
if ! command -v convctl >/dev/null 2>&1; then
echo "::error::convctl is not on PATH. Run the setup-convctl Action before this one:" >&2
echo " - uses: terasky-oss/declarative-conversion-operator/.github/actions/setup-convctl@v1" >&2
exit 1
fi
convctl version

- name: Write kubeconfig
if: inputs.kubeconfig != ''
shell: bash
env:
KUBECONFIG_CONTENTS: ${{ inputs.kubeconfig }}
run: |
set -euo pipefail
umask 077
printf '%s' "$KUBECONFIG_CONTENTS" > "$RUNNER_TEMP/kubeconfig"
echo "KUBECONFIG=$RUNNER_TEMP/kubeconfig" >> "$GITHUB_ENV"

- name: Run convctl diff
id: run
shell: bash
env:
CONFIGS: ${{ inputs.config }}
XRD: ${{ inputs.xrd }}
CRD: ${{ inputs.crd }}
LIVE: ${{ inputs.live }}
CONTEXT: ${{ inputs.context }}
FAIL_ON_DELTA: ${{ inputs.fail-on-delta }}
run: |
set -euo pipefail

md="$RUNNER_TEMP/convctl-diff.md"
args=()
while IFS= read -r cfg; do
if [ -n "$cfg" ]; then args+=(--config "$cfg"); fi
done <<< "$CONFIGS"
if [ -n "$XRD" ]; then args+=(--xrd "$XRD"); fi
if [ -n "$CRD" ]; then args+=(--crd "$CRD"); fi
if [ "$LIVE" = "true" ]; then args+=(--live); fi
if [ -n "$CONTEXT" ]; then args+=(--context "$CONTEXT"); fi

set +e
convctl diff "${args[@]}" --output markdown > "$md"
code=$?
set -e

# Exit 1 means deltas were found, which is the thing being reported
# rather than a failure. Exit 2 means the command could not run —
# a usage error, or a cluster it could not reach — and a gate that
# treated that as "no deltas" would be silently useless.
case "$code" in
0) has_deltas=false ;;
1) has_deltas=true ;;
*)
echo "::error::convctl diff failed to run (exit $code)" >&2
cat "$md" >&2 || true
exit "$code"
;;
esac

{
echo "exit-code=$code"
echo "has-deltas=$has_deltas"
echo "markdown-path=$md"
} >> "$GITHUB_OUTPUT"

cat "$md" >> "$GITHUB_STEP_SUMMARY"

if [ "$has_deltas" = "true" ] && [ "$FAIL_ON_DELTA" = "true" ]; then
exit 1
fi

- name: Upsert the pull-request comment
if: always() && inputs.comment == 'true' && github.event_name == 'pull_request'
shell: bash
env:
GH_TOKEN: ${{ inputs.token }}
TAG: ${{ inputs.comment-tag != '' && inputs.comment-tag || inputs.config }}
MD: ${{ steps.run.outputs.markdown-path }}
PR: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
FORK: ${{ github.event.pull_request.head.repo.fork }}
run: |
set -euo pipefail

# A pull_request event from a fork has a read-only token. That is a
# notice, not a failure: the delta is already in the job summary, and
# failing here would make every fork contribution red for a reason
# the contributor cannot fix.
if [ "$FORK" = "true" ]; then
echo "::notice::pull request is from a fork, so the token cannot comment; the delta is in the job summary"
exit 0
fi
if [ ! -f "$MD" ]; then
echo "::notice::no diff output to comment"
exit 0
fi

# Collapse the tag onto one line: it defaults to the config input,
# which is multiline when two configs are being compared, and a
# marker containing a newline would never match itself.
tag="$(tr '\n' '_' <<< "$TAG" | sed 's/_$//')"
marker="<!-- convctl-diff:$tag -->"
body="$RUNNER_TEMP/convctl-diff-comment.md"
{
echo "$marker"
cat "$MD"
} > "$body"

# Find this tag's comment and edit it, so repeated runs update one
# comment rather than appending a new one each push.
#
# The marker goes to jq as data, not spliced into the filter. The
# default tag is the config input, which is multiline for the
# two-config form — and a newline inside a jq string literal is a
# syntax error, so the lookup would fail and every run would post a
# new comment. A quote or backslash in an explicit tag does the
# same.
existing="$(gh api "repos/$REPO/issues/$PR/comments" --paginate \
| jq -r --arg marker "$marker" \
'[.[] | select(.body | contains($marker))] | .[0].id // empty')"

if [ -n "$existing" ]; then
gh api --method PATCH "repos/$REPO/issues/comments/$existing" \
-F body=@"$body" --silent
else
gh api --method POST "repos/$REPO/issues/$PR/comments" \
-F body=@"$body" --silent
fi
48 changes: 48 additions & 0 deletions .github/actions/convctl-fleet/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# `convctl-fleet`

Runs `convctl test --live` against every cluster in a fleet and aggregates
the result into one JUnit report, with one `<testsuite>` per cluster.

```yaml
# convctl comes from setup-convctl, once per job.
- uses: terasky-oss/declarative-conversion-operator/.github/actions/setup-convctl@v1
- uses: terasky-oss/declarative-conversion-operator/.github/actions/convctl-fleet@v1
with:
config: apis/widgets/conversion.yaml
xrd: apis/widgets/xrd.yaml
contexts: prod-us,prod-eu
kubeconfig: ${{ secrets.FLEET_KUBECONFIG }}
```

Either `contexts` (comma-separated, from one kubeconfig) or `kubeconfig-dir`
(one file per cluster).

## An unreachable cluster is a failed suite

Not a skip. `convctl` already behaves this way; the Action surfaces it, in
the report and in the `failed-clusters` output. A fleet check that quietly
covered four of five clusters and reported green is worse than one that did
not run at all.

## Alternative: a matrix

`convctl-fleet` runs the clusters in one job. A per-cluster matrix using
[`convctl-test`](../convctl-test) gives you one job per cluster — slower to
set up, but a clearer failure surface and parallel execution. Use
`fail-fast: false` either way. Both shapes are in
[`convctl-fleet.gha.yml`](../../../docs/gitops/convctl-fleet.gha.yml).

## Outputs

`exit-code`, `report-path`, `clusters`, `failed-clusters`.

## Requires `setup-convctl`

This Action consumes `convctl` from `PATH` and does not install it. Run
[`setup-convctl`](../setup-convctl) first — once per job, however many of
these Actions follow.

That is not an ergonomic preference. A composite action cannot reference a
local action by path once published: `./…` resolves against the **consumer's**
workspace, so a nested setup step would work in this repository's own tests
and fail for everyone else.
Loading
Loading