omnigraph/.github/workflows/publish-crates.yml
Andrew Altshuler 587fbeabd8
ci(publish-crates): set User-Agent + treat "already exists" as success (#117)
Two related fixes uncovered while recovering the v0.5.0 publish.

1. crates.io API requires a User-Agent header. The `publish_if_new`
   skip check was doing a bare `curl -fsSL https://crates.io/api/...`
   which crates.io rejects with HTTP 403. With `-f` curl exits
   non-zero, the pipeline returns empty, the script doesn't recognize
   already-published crates, and we fall through to a real publish
   attempt. On a re-run that means cargo publish errors with
   "already exists on crates.io index" for crates that DID publish
   successfully on the previous run.

   Fix: send a `User-Agent: ModernRelay-omnigraph-ci (URL)` header.

2. Defense in depth: even with the UA, the API could hiccup. If the
   skip check misses an existing version and cargo publish errors
   with "already exists on crates.io index", treat as success
   instead of failing the whole run. This makes the workflow
   re-runnable after any partial publish without needing manual
   intervention.

Both fixes are required to recover from the v0.5.0 partial publish
where omnigraph-compiler@0.5.0 made it through but the run failed
before omnigraph-policy / engine / server / cli — re-triggering the
workflow now succeeds end-to-end.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 14:19:17 +01:00

124 lines
4.5 KiB
YAML

name: Publish to crates.io
# Publishes the four workspace crates to crates.io in dependency order.
#
# Triggers:
# - push of any v* tag (future releases auto-publish alongside release.yml)
# - workflow_dispatch with an explicit tag input (catch-up runs for past tags)
#
# Idempotent: each crate's current crates.io version is checked before publish,
# so a partial failure can be re-run without "crate version already exists" errors.
#
# Prerequisite: repo secret CARGO_REGISTRY_TOKEN. The job exits cleanly if absent.
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
tag:
description: "Tag to publish (e.g. v0.4.1). Required for manual dispatches."
required: true
type: string
jobs:
publish_crates:
name: Publish crates
runs-on: ubuntu-latest
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
CARGO_TERM_COLOR: always
steps:
- name: Skip if CARGO_REGISTRY_TOKEN is not configured
if: env.CARGO_REGISTRY_TOKEN == ''
run: |
echo "CARGO_REGISTRY_TOKEN is not set; skipping crates.io publish."
echo "CARGO_PUBLISH_SKIP=1" >> "$GITHUB_ENV"
- name: Resolve ref
if: env.CARGO_PUBLISH_SKIP != '1'
id: ref
run: |
ref="${{ inputs.tag || github.ref_name }}"
echo "ref=${ref}" >> "$GITHUB_OUTPUT"
echo "Publishing from ref: ${ref}"
- name: Checkout source at tag
if: env.CARGO_PUBLISH_SKIP != '1'
uses: actions/checkout@v5.0.1
with:
ref: ${{ steps.ref.outputs.ref }}
- name: Install Linux dependencies
if: env.CARGO_PUBLISH_SKIP != '1'
run: |
sudo apt-get update
sudo apt-get install -y protobuf-compiler libprotobuf-dev
- name: Install Rust stable
if: env.CARGO_PUBLISH_SKIP != '1'
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
- name: Cache Rust build data
if: env.CARGO_PUBLISH_SKIP != '1'
uses: Swatinem/rust-cache@v2
with:
workspaces: |
. -> target
- name: Publish crates in dependency order
if: env.CARGO_PUBLISH_SKIP != '1'
run: |
set -euo pipefail
publish_if_new() {
local crate="$1"
local version
version=$(cargo metadata --format-version=1 --no-deps \
| jq -r --arg c "$crate" '.packages[] | select(.name==$c) | .version')
# crates.io API requires a User-Agent header — without it the
# API responds 403 and the skip check below would silently
# fall through to a real publish attempt that errors with
# "already exists on crates.io index" when re-running after a
# partial publish. Send a UA naming the workflow.
local current
current=$(curl -fsSL \
-A 'ModernRelay-omnigraph-ci (https://github.com/ModernRelay/omnigraph)' \
"https://crates.io/api/v1/crates/${crate}" \
| jq -r '.crate.max_version' || echo "")
if [[ "$current" == "$version" ]]; then
echo "==> ${crate} ${version} already on crates.io, skipping"
return 0
fi
echo "==> publishing ${crate} ${version} (current crates.io: ${current:-none})"
# Defense in depth: if the skip check missed an existing
# version (e.g. crates.io API hiccup), cargo publish errors
# with "already exists on crates.io index". Treat that as
# success so the workflow can be re-run idempotently.
local output
if ! output=$(cargo publish -p "$crate" --locked 2>&1); then
echo "$output"
if echo "$output" | grep -q "already exists on crates.io"; then
echo "==> ${crate} ${version} was already published; treating as success"
return 0
fi
return 1
fi
echo "$output"
}
# Order matters: each crate must precede anything that depends on it.
# omnigraph-compiler and omnigraph-policy have no internal deps;
# omnigraph-engine depends on both; server depends on engine + the
# two leaf crates; cli depends on everything.
publish_if_new omnigraph-compiler
publish_if_new omnigraph-policy
publish_if_new omnigraph-engine
publish_if_new omnigraph-server
publish_if_new omnigraph-cli