Skip to content

fix(ci): install protoc for the release build, and allow republishing a tag - #4

Merged
jlucaso1 merged 9 commits into
mainfrom
fix/release-protoc
Aug 7, 2026
Merged

fix(ci): install protoc for the release build, and allow republishing a tag#4
jlucaso1 merged 9 commits into
mainfrom
fix/release-protoc

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

The v0.6.1 release job failed at Build the wasm package with Executable not found in $PATH: "protoc".

bun run build runs gengen:proto-codec, which shells out to protoc. ci.yml never caught this because it calls wasm-pack build and bun run build:ts directly and skips gen entirely — so the release job was the first thing in CI to run the package's actual build. Installed through the taiki-e/install-action already pinned in the workflow, so no new dependency.

The gate held. The tag v0.6.1 and the GitHub release exist; npm is still on 0.6.0. Nothing broken was published, which is what the verify/publish split is for.

Recovering the tag

Re-running the failed job does not work here: only the run that creates a tag carries release_created, and a re-run replays the workflow file of that commit — the broken one. So this adds a workflow_dispatch trigger taking an existing tag:

Actions → Release → Run workflow → tag: v0.6.1

It skips release-please and puts the tag through verify then publish with the workflow as it currently stands. Useful beyond today: a publish can fail for reasons that have nothing to do with the tag — a missing tool, a registry outage — and without this the only way out is a version bump.

After merging, dispatch with v0.6.1 to publish it.

Note on the gap this exposed

ci.yml and the release build are not running the same thing. CI skips gen, so codegen breakage only surfaces at release time, which is the worst moment to find it. Worth closing separately — either CI runs bun run build, or the release stops regenerating and trusts the committed output. I did not fold that into a fix for a red release.


Summary by cubic

Pins protoc for the release build, hardens provenance by using immutable checkouts, and adds a safe tag republish flow restricted to release tags on main. Fixes the v0.6.1 build.

  • Bug Fixes

    • Install protoc via taiki-e/install-action, pinned to protoc@35.1, so bun run build can regenerate the proto codec.
    • Verify/publish always check out by sha; both build the same immutable commit.
    • On push, refuse a release if the tag’s commit differs from the event’s; instruct to dispatch on the tag so tree and npm provenance match.
    • On dispatch, require the tag’s commit is reachable from main, the tag has a GitHub release, and its version matches package.json; fail fast if npm already has a higher version.
    • Serialize all release runs under one concurrency group.
    • Publish runs only if verify succeeded and the run wasn’t cancelled.
  • New Features

    • Replace free-form input with workflow_dispatch on a tag ref; select an existing tag to rebuild/publish it.
    • Recovery works only for tags created after this change, since dispatch runs the workflow stored in the selected tag.

Written for commit be326db. Summary will update on new commits.

Review in cubic

… a tag

`bun run build` regenerates the proto codec through protoc, which is not
on the runner. ci.yml never caught it because it calls wasm-pack and
build:ts directly and skips `gen`, so the release job was the first thing
to run the package's real build. It failed on v0.6.1 at that step.

The gate held: the tag and the GitHub release exist, npm is untouched.
Recovering from that is the second half of this change. Only the run that
creates a tag carries `release_created`, and re-running it replays the
workflow file of that same commit, which is the broken one. A dispatch
trigger takes an existing tag and puts it through verify and publish with
the workflow as it stands.
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The release workflow now supports manual execution from existing tags. Verification validates and outputs the exact commit SHA. Publishing runs only after successful verification and uses that SHA. Both jobs install protoc with wasm-pack.

Changes

Release workflow

Layer / File(s) Summary
Dispatch and verification flow
.github/workflows/release.yml
Manual dispatch uses tag refs. Release preparation remains push-only. Verification selects and records the validated commit SHA and installs protoc with wasm-pack.
Verified publishing flow
.github/workflows/release.yml
Publishing requires successful verification, checks out the emitted SHA, and installs protoc with wasm-pack.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ReleaseEvent
  participant VerificationJob
  participant PublishingJob
  ReleaseEvent->>VerificationJob: provide release tag or dispatched tag ref
  VerificationJob->>VerificationJob: check out and record validated commit SHA
  VerificationJob->>PublishingJob: emit validated commit SHA
  PublishingJob->>PublishingJob: check out SHA and install protoc with wasm-pack
  PublishingJob->>PublishingJob: publish after successful verification
Loading

Possibly related PRs

Poem

A rabbit checks the tagged release trail,
Records the SHA without fail.
protoc joins the tool parade,
Verification leads the grade.
Then publishing hops along—
With the exact commit, strong and long.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes both primary changes: installing protoc and enabling tag republishing.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ad9796e761

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/release.yml Outdated
Comment thread .github/workflows/release.yml Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 1 file

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread .github/workflows/release.yml Outdated
Comment thread .github/workflows/release.yml Outdated
Taking the tag as a free-form input let a dispatch point the privileged
publish job at any branch or commit, and a mutable ref could advance
between the verify checkout and the publish one, shipping code that was
never verified. npm's provenance compounded it: it records the event's
ref and sha, not the workspace, so a tarball built from a tag while the
event pointed at a branch would be attested to the wrong commit.

The dispatch now carries no input. Select the tag as the ref instead: the
picker only offers refs that exist, `ref_type` rejects anything that is
not a tag, and the event then points at the same commit the provenance
will name. Publish checks out the sha verify resolved rather than the ref
again, so there is no window between the two.

Publish also gated on `!cancelled()` rather than `always()`, which ran
through cancellation as long as verify had already succeeded.
@jlucaso1

jlucaso1 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

All four applied in 4cd4190. Three were about the recovery path I added, and they were right that it was worse than the problem it solved.

Arbitrary ref through the privileged job (codex + cubic P1). A free-form tag input is not a tag — a dispatch could point the OIDC publish job at any branch or commit. And a mutable ref can advance between the verify checkout and the publish one, so what got tested and what got shipped were not guaranteed to be the same tree.

Provenance attested to the wrong commit (codex). This is the one I would not have found: npm reads the event's ref and sha, not the workspace, so dispatching from main and checking out a tag produces a tarball attested to the main commit. A correct-looking attestation pointing at the wrong source is worse than none.

Both are fixed by dropping the input entirely. The dispatch now takes no parameters — you select the tag in the ref picker. The picker only offers refs that exist, github.ref_type == 'tag' rejects anything that is not one, and the event then points at exactly the commit provenance will name. Publish additionally checks out the sha verify resolved, not the ref again, closing the window between them.

always() published through cancellation (cubic P1). Correct — always() runs even when the run is cancelled, so a cancel after verify succeeded would still publish. Now !cancelled(), which keeps the dispatch path working past the skipped release-please without that hole.


One consequence worth deciding before merge: v0.6.1 cannot be recovered by this dispatch. Its tag points at a commit whose release.yml has no workflow_dispatch, so there is nothing to dispatch from there. Options:

  1. Delete the v0.6.1 tag and GitHub release. Nothing was published, so nothing breaks, and the next release from these fixes comes out clean.
  2. Leave it as a tag with no npm counterpart and let the next release be 0.6.2.

I lean toward 1 — a release that exists on GitHub and not on npm is the kind of thing that confuses whoever looks next. Either way the recovery path works from the next tag onward, since those tags will carry this workflow.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4cd4190435

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/release.yml
Comment thread .github/workflows/release.yml Outdated
Publish already consumed the sha verify resolved, but verify still
resolved the ref itself. A tag force-updated between the event and that
checkout would be built and shipped while npm provenance kept naming the
commit the event recorded, so the attestation would describe code that
never ran.

The event sha is immutable, so use it on the dispatch path. The push path
keeps the tag, where the event sha is the main commit rather than the
release.
@jlucaso1

jlucaso1 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

One applied, one already answered.

Verify resolved the ref itself (92a7686). Valid, and a real gap left by the previous round: publish consumed the sha verify resolved, but verify still resolved github.ref. A tag force-updated between the event and that checkout would be built and shipped while provenance kept naming the sha the event recorded — an attestation describing code that never ran. The dispatch path now checks out github.sha, which is immutable. The push path keeps the tag, because there the event sha is the main commit rather than the release.

v0.6.1 cannot be dispatched from its own tag — correct, and it is the tradeoff I described in the previous comment rather than something new. The suggested fix, keeping the dispatcher on a ref that has the fix and passing the release tag separately, is the design the last review round rejected: a ref parameter is what let a dispatch aim the OIDC job at an arbitrary commit, and npm reads the event ref and sha for provenance regardless of what the parameter says. Reintroducing it would trade a correct attestation for the ability to rescue one tag.

So the limitation stands and is worth stating plainly: this recovery path works from the next tag onward, because those tags will carry this workflow. v0.6.1 predates it and has to be handled by hand — my preference is still deleting the tag and the GitHub release, since nothing was published and the next release then comes out clean.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/release.yml:
- Around line 112-123: Update the checkout ref expression in the release
workflow to use github.sha for the push path as well as workflow_dispatch,
rather than checking out the release tag. Retain the explicit tag dereference
and validation before publishing, ensuring the tag target equals the commit
recorded by the verified step for verification and provenance.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 5199ebe4-e0cf-4b2d-88d0-b7285bc7d2a2

📥 Commits

Reviewing files that changed from the base of the PR and between 5e4ad86 and 92a7686.

📒 Files selected for processing (1)
  • .github/workflows/release.yml

Comment thread .github/workflows/release.yml

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 1 file (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread .github/workflows/release.yml
The dispatch path already used the event's sha; the push path still
resolved the tag name, so it kept the window the dispatch fix closed and
built a ref while npm attested the event's commit.

Both paths now check out that sha, which drops the output threading the
resolved commit from verify to publish: there is nothing left to thread
when neither job resolves anything. release-please tags the commit it
merged, and verify asserts that rather than assuming it.
A dispatch runs the workflow file stored in the selected tag, so the
recovery path only covers tags created from this commit onwards. That was
answered in review and nowhere in the file, which is where the next person
looks.
@jlucaso1

jlucaso1 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

The mechanism described is correct, and it is the tradeoff I took knowingly — but the finding is right that the file did not say so. Fixed in 6db11c9: the limitation is now in the workflow itself, not only in a review comment.

# The consequence, which is the price of the above: a dispatch runs the
# workflow file stored in the selected tag, so this only rescues tags created
# from this commit onwards. A tag older than this file has no dispatch trigger
# to select and cannot be recovered here — delete it along with its GitHub
# release and let the next release supersede it, since a tag that never
# reached npm has nothing depending on it.

Not taking the other suggested shape — free-form tag input, gated on the tag resolving to a verified commit. That is the design the earlier review round flagged twice as P1, and the gating does not fix what made it P1: npm reads the event's ref and sha for provenance, so dispatching from a branch with a tag parameter attests the tarball to the branch commit no matter how strictly the input is validated. A gate can stop the wrong code from being built; it cannot stop the attestation from naming the wrong source. Trading a correct attestation on every future release to rescue one tag is the wrong side of that trade.

So v0.6.1 gets handled by hand, and my recommendation stands: delete the tag and its GitHub release. Nothing was published, nothing depends on it, and the next release comes out clean with protoc installed.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6db11c9747

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/release.yml
Comment thread .github/workflows/release.yml
Comment thread .github/workflows/release.yml
Three holes in the recovery path, all reachable only through it.

The concurrency group keyed on `github.ref`, which differs between a
release from main and a recovery on a tag, so both could reach npm
publish at once and race over `latest`. One group for every run instead.

`ref_type == 'tag'` proves the ref is a tag, not that it is a release:
anyone able to push a tag could aim the job holding OIDC rights at
arbitrary code and skip main. A dispatch now has to name a tag that
carries a GitHub release, which is what release-please creates and a
hand-pushed tag lacks, and whose version matches the tree.

npm refuses `latest` implicitly once a higher version is published, so a
recovery attempted after a newer release would have failed at the publish
step, past the whole build. It now fails in the first seconds and says
why.
The event sha and the release commit diverge when the run for a merged
release PR fails or is superseded: a later push creates the release, and
the tag names the earlier merge. Asserting equality against the event
would then fail after the GitHub release exists, leaving that version
unpublishable.

The action reports the commit it tagged, so use it where it exists and
fall back to the event sha on the dispatch path, where the event already
points at the tag.
@jlucaso1

jlucaso1 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

All four applied — three on the recovery path, one shared with the push path.

Concurrency keyed per ref (0fb8edf). Right: a recovery dispatched on a tag has a different github.ref than a release from main, so the two had separate groups and could reach npm publish together. One release group for every run now.

Any tag was accepted (0fb8edf). ref_type == 'tag' proves it is a tag, not that it is a release — anyone able to push a tag could aim the OIDC job at arbitrary code and skip main entirely. A dispatch now has to name a tag that carries a GitHub release, which is what release-please creates and a hand-pushed tag lacks, and whose version matches package.json in that tree.

Recovery would die at the publish step (0fb8edf). npm refuses latest implicitly once a higher version is published, so a recovery attempted after a newer release burned the whole build before failing. Checked in the first seconds instead. Verified against the live registry: with 0.6.0 published, a 0.5.9 tree is rejected with @oxidezap/whatsapp-rust-bridge@0.6.0 is already published; 0.5.9 cannot take latest implicitly, while the real 0.6.1 tree passes.

Event sha assumed to be the release commit (e0da2ff, from the sibling PR review). They diverge when the run for a merged release PR fails or is superseded: a later push creates the release while the tag names the earlier merge. My assertion would then fail after the GitHub release exists, and no later run reports release_created — the version would be stuck unpublishable. The action reports the commit it tagged, so both checkouts and the assertion use that, falling back to the event sha on the dispatch path where the event already points at the tag.

That last one also answers the P2 about retargeting during environment approval: both jobs pin a sha, so moving the tag while publish waits for its gate cannot change what ships.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: adf85df7f3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/release.yml
Comment thread .github/workflows/release.yml
Comment thread .github/workflows/release.yml Outdated
…otoc

npm attests the package to the event's sha whatever the workspace holds.
Checking out the commit the action tagged fixed the tree but moved the
mismatch onto provenance: a release created by a later push than the
merge it names would ship a tarball attested to a commit that was never
built.

Neither choice is safe on the push path, so refuse there and say what to
do: dispatching on the tag publishes the same release with the event
pointing at it, which is the one shape where tree and attestation agree.

protoc was also the last unpinned tool on this path. Verify and publish
install it separately and publish can start much later, behind the
environment gate, so a new default between them would test one generated
codec and ship another.
@jlucaso1

jlucaso1 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Two applied, one declined with reasoning.

Provenance vs the release commit (P1, a06a603). Correct, and it is the flaw in my own previous fix: checking out outputs.sha fixed the tree and moved the mismatch onto the attestation, because npm names the event sha whatever the workspace holds. Neither choice is safe on the push path, so it now refuses there and says what to do — dispatching on the tag is the one shape where tree and attestation agree, and that path exists precisely for this. A stuck release with a clear error beats a package attested to a commit that was never built.

protoc unpinned (P2, a06a603). Right, and it was the last floating tool on this path. Verify and publish install it separately, and publish can start much later behind the environment gate, so a new default between them would test one generated codec and ship another. Pinned to protoc@35.1 in both, like bun and npm already were.

Concurrency evicting a pending recovery (P2). Accurate about the mechanism — cancel-in-progress: false protects the running job, not the queued one, so a pending dispatch can be replaced by a later push. Not changing it, because the alternative is worse: separate groups are what let a release and a recovery reach npm publish at the same time and race over latest, which is the P1 from the previous round. An evicted recovery is visible and re-dispatchable in one click; a lost race publishes the wrong version as latest and needs a dist-tag repair. Serializing everything and accepting that a queued recovery may need re-triggering is the cheaper failure.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a06a60316e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ github.ref_name }}
run: |
if ! gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bind recovery releases to protected-main commits

Fresh evidence after the prior release-tag finding is that this new guard only tests whether gh release view "$TAG" succeeds. That command merely views any matching release, and GitHub allows people with repository write access to create releases manually, including drafts; when tags are mutable, the same access needed to dispatch this workflow can therefore tag arbitrary unmerged code with a matching package version, attach a release record, and pass both checks before that code runs in the OIDC-enabled publish job. Bind recovery to a commit known to have passed through protected main/release-please, or enforce immutable protected release tags, rather than treating the existence of a release record as authorization.

Useful? React with 👍 / 👎.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 1 file (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread .github/workflows/release.yml
A GitHub release proves nothing about where the code came from: anyone
with write access can tag arbitrary unmerged code, set a matching version
in that tree, attach a release record by hand, and dispatch. Both checks
would pass and the code would run in the job holding npm OIDC rights,
which is exactly the protected path being bypassed.

Reachability from main is what the release record was standing in for.
Check it first, and let the release and version checks narrow from there.
@jlucaso1

jlucaso1 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Valid, applied in be326db (and in the sibling PR, 837ef39).

Both reviewers landed on the same hole and they are right: a GitHub release record proves nothing about where the code came from. Anyone with write access can tag arbitrary unmerged code, set a matching version in that tree, attach a release by hand, and dispatch — passing both of my checks and running that code in the job holding npm OIDC rights. That is precisely the protected path the guard was supposed to enforce.

Reachability from main is what the release record was standing in for, so that is now the first check:

status=$(gh api "repos/$GITHUB_REPOSITORY/compare/main...$GITHUB_SHA" --jq .status)
case "$status" in
  identical | behind) ;;
  *) echo "::error::$TAG is at a commit that is not on main ($status)"; exit 1 ;;
esac

The release and version checks stay, narrowing from there: on main, carrying a release, version matching the tree.

Verified the semantics against this repository rather than trusting the docs — compare/main...<commit> returns identical for the real v0.6.1 commit and ahead for this branch's head, which is the shape an attacker's unmerged commit would have. The legitimate case passes and the simulated one exits 1 with the reason.

@jlucaso1
jlucaso1 merged commit d6f5c44 into main Aug 7, 2026
6 checks passed
@jlucaso1
jlucaso1 deleted the fix/release-protoc branch August 7, 2026 03:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant