Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Fixed

- GitHub Action: a pull request that breaks a budget gate now still gets
its cost comment. The `budget` and `budget-delta` gates, including a
`budget` or `budget_delta` set in the repository's `.c3x.toml`, ran
before the comment step and failed the job first, so the comment was
skipped on exactly the pull requests that needed it. When a comment is
posted, every gate now runs after it; `strict` already did.
- GitHub Action: the `currency` input now applies to the PR comment and
the base-branch baseline, not only to the log output. Comments were
always in USD.
- The budget-delta message puts the sign before the currency symbol:
`+$2781.48/mo`, not `$+2781.48/mo`.

## [0.3.14] - 2026-09-29

### Changed
Expand Down
93 changes: 65 additions & 28 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -214,18 +214,24 @@ runs:
run: |
set -euo pipefail
ARGS=(estimate --path "$IN_PATH" --format "$IN_FORMAT")
# When a PR comment will be posted, --strict is applied by the
# comment step instead, after posting, so reviewers see the caveats
# before the job fails.
# When a PR comment will be posted, every gate (strict, budget,
# budget-delta) is enforced after posting, so the pull request that
# breaks a gate still gets the comment explaining why. --budget 0
# also switches off a budget set in the repo's .c3x.toml here; the
# Budget gate step applies it after the comment.
WILL_COMMENT=false
if [ "$IN_COMMENT" = "true" ] && [ "$EVENT_NAME" = "pull_request" ]; then
WILL_COMMENT=true
fi
if [ "$IN_STRICT" = "true" ] && [ "$WILL_COMMENT" = "false" ]; then
ARGS+=(--strict)
fi
if [ "$IN_BUDGET" != "0" ]; then
ARGS+=(--budget "$IN_BUDGET")
if [ "$WILL_COMMENT" = "true" ]; then
ARGS+=(--budget 0)
else
if [ "$IN_STRICT" = "true" ]; then
ARGS+=(--strict)
fi
if [ "$IN_BUDGET" != "0" ]; then
ARGS+=(--budget "$IN_BUDGET")
fi
fi
if [ -n "$IN_CURRENCY" ]; then
ARGS+=(--currency "$IN_CURRENCY")
Expand Down Expand Up @@ -285,7 +291,8 @@ runs:

# --budget 0: a base branch already over a budget set in its
# .c3x.toml must still produce a baseline.
if c3x estimate --path "$base_path" --budget 0 --save-baseline "$out" >"$log" 2>&1; then
if c3x estimate --path "$base_path" --budget 0 ${IN_CURRENCY:+--currency "$IN_CURRENCY"} \
--save-baseline "$out" >"$log" 2>&1; then
echo "baseline=$out" >> "$GITHUB_OUTPUT"
echo "Computed cost baseline from base $BASE_SHA"
else
Expand All @@ -296,25 +303,6 @@ runs:
echo "::endgroup::"
fi

- name: Budget delta gate
if: inputs.install-only != 'true' && inputs.budget-delta != '0' && github.event_name == 'pull_request'
shell: bash
env:
IN_PATH: ${{ inputs.path }}
BASELINE: ${{ steps.baseline.outputs.baseline }}
IN_BUDGET_DELTA: ${{ inputs.budget-delta }}
run: |
set -euo pipefail
if [ -n "$BASELINE" ]; then
c3x diff --path "$IN_PATH" --baseline "$BASELINE" --budget-delta "$IN_BUDGET_DELTA"
elif [ -f "$IN_PATH" ] && [ "${IN_PATH##*.}" = "json" ]; then
# A plan JSON is generated in CI, so the base branch has no copy
# to estimate; the plan's own prior state is the baseline.
c3x diff --path "$IN_PATH" --budget-delta "$IN_BUDGET_DELTA"
else
echo "::warning::budget-delta was not enforced: no base-branch estimate could be computed"
fi

- name: PR comment
if: inputs.install-only != 'true' && inputs.comment == 'true' && github.event_name == 'pull_request'
shell: bash
Expand All @@ -326,8 +314,13 @@ runs:
IN_PATH: ${{ inputs.path }}
BASELINE: ${{ steps.baseline.outputs.baseline }}
IN_STRICT: ${{ inputs.strict }}
IN_CURRENCY: ${{ inputs.currency }}
run: |
set -euo pipefail
# c3x comment has no --currency flag; the config layer reads it
# from C3X_CURRENCY. Exported only when set, so the repo's
# .c3x.toml currency still applies otherwise.
if [ -n "$IN_CURRENCY" ]; then export C3X_CURRENCY="$IN_CURRENCY"; fi
TOKEN="$GH_DEFAULT_TOKEN"
# Branded comments ask token.c3x.dev for a short-lived token of the
# C3X Cloud app, scoped to this one repository. The request carries
Expand Down Expand Up @@ -382,3 +375,47 @@ runs:
ARGS+=(--baseline "$BASELINE")
fi
GITHUB_TOKEN="$TOKEN" c3x "${ARGS[@]}"

# Gates run after the comment, so a pull request that breaks one still
# gets the comment that explains why. (strict is applied by the comment
# step itself, after posting.)
- name: Budget delta gate
if: inputs.install-only != 'true' && github.event_name == 'pull_request'
shell: bash
env:
IN_PATH: ${{ inputs.path }}
BASELINE: ${{ steps.baseline.outputs.baseline }}
IN_BUDGET_DELTA: ${{ inputs.budget-delta }}
IN_CURRENCY: ${{ inputs.currency }}
run: |
set -euo pipefail
if [ -n "$IN_CURRENCY" ]; then export C3X_CURRENCY="$IN_CURRENCY"; fi
# Without the input, c3x diff still applies budget_delta from the
# repo's .c3x.toml, if any; passing 0 would switch that off.
GATE=()
if [ "$IN_BUDGET_DELTA" != "0" ]; then GATE=(--budget-delta "$IN_BUDGET_DELTA"); fi
if [ -n "$BASELINE" ]; then
c3x diff --path "$IN_PATH" --baseline "$BASELINE" ${GATE[@]+"${GATE[@]}"} >/dev/null
elif [ -f "$IN_PATH" ] && [ "${IN_PATH##*.}" = "json" ]; then
# A plan JSON is generated in CI, so the base branch has no copy
# to estimate; the plan's own prior state is the baseline.
c3x diff --path "$IN_PATH" ${GATE[@]+"${GATE[@]}"} >/dev/null
elif [ "$IN_BUDGET_DELTA" != "0" ]; then
echo "::warning::budget-delta was not enforced: no base-branch estimate could be computed"
fi

- name: Budget gate
# Only when the Estimate step skipped it to let the comment through.
if: inputs.install-only != 'true' && inputs.comment == 'true' && github.event_name == 'pull_request'
shell: bash
env:
IN_PATH: ${{ inputs.path }}
IN_BUDGET: ${{ inputs.budget }}
IN_CURRENCY: ${{ inputs.currency }}
run: |
set -euo pipefail
ARGS=(estimate --path "$IN_PATH" --format json)
if [ "$IN_BUDGET" != "0" ]; then ARGS+=(--budget "$IN_BUDGET"); fi
if [ -n "$IN_CURRENCY" ]; then ARGS+=(--currency "$IN_CURRENCY"); fi
# Without the input, a budget in the repo's .c3x.toml applies.
c3x "${ARGS[@]}" >/dev/null
2 changes: 1 addition & 1 deletion cmd/c3x/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ func enforceBudgetDelta(cmd *cobra.Command, d domain.Diff, limit float64) error
return nil
}
fmt.Fprintf(cmd.ErrOrStderr(),
"c3x: delta exceeds gate — %s%+.2f/mo > %s%+.2f/mo\n",
"c3x: delta exceeds gate — +%s%.2f/mo > +%s%.2f/mo\n",
d.Currency.Symbol(), delta, d.Currency.Symbol(), limit)
return errBudgetDeltaExceeded
}
Expand Down
Loading