Skip to content
Open
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
59 changes: 59 additions & 0 deletions .github/workflows/core-drift.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# SPDX-License-Identifier: Apache-2.0

# Nightly comparison of core main with the pinned core tag; keeps the single
# "Core main drift" issue current (scripts/core-sync/drift-check.sh).
name: core-drift

on:
schedule:
- cron: "41 4 * * *"
workflow_dispatch:

permissions:
contents: read

concurrency:
group: core-drift
cancel-in-progress: false

jobs:
drift:
name: drift
# The bot secrets live in the sdk-bot environment, which only main can
# deploy to, so a pushed branch can't mint a bot token.
environment: sdk-bot
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- id: app-token
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
with:
client-id: ${{ secrets.SDK_BOT_CLIENT_ID }}
private-key: ${{ secrets.SDK_BOT_PRIVATE_KEY }}
permission-issues: write

- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- uses: jdx/mise-action@e6a8b3978addb5a52f2b4cd9d91eafa7f0ab959d # v4.2.0

# Exit 1 with a report means drift; the issue records it. No report
# means the check itself broke, which the failure step records.
- name: Check drift against core main
run: |
scripts/core-sync/drift-check.sh --out drift-report.md || true
test -s drift-report.md
cat drift-report.md >>"$GITHUB_STEP_SUMMARY"

- name: Update the drift issue
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: scripts/core-sync/update-drift-issue.sh drift-report.md

- name: Report failure
if: failure()
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: scripts/core-sync/report-failure.sh core-drift "$RUN_URL"
84 changes: 84 additions & 0 deletions .github/workflows/core-watch.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# SPDX-License-Identifier: Apache-2.0

# Opens one update proposal per new core release tag
# (scripts/core-sync/open-bump-pr.sh).
name: core-watch

on:
schedule:
- cron: "17 * * * *"
workflow_dispatch:
inputs:
tag:
description: Core tag to propose (vX.Y.Z or vX.Y.Z-pre.N); empty proposes every new tag
required: false
default: ""

permissions:
contents: read

concurrency:
group: core-watch
cancel-in-progress: false

jobs:
watch:
name: watch
# The bot secrets live in the sdk-bot environment, which only main can
# deploy to, so a pushed branch can't mint a bot token.
environment: sdk-bot
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- id: app-token
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
with:
client-id: ${{ secrets.SDK_BOT_CLIENT_ID }}
private-key: ${{ secrets.SDK_BOT_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write
permission-issues: write

- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
persist-credentials: true

- uses: jdx/mise-action@e6a8b3978addb5a52f2b4cd9d91eafa7f0ab959d # v4.2.0

- name: Set the bot identity
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
run: |
set -euo pipefail
id=$(gh api "users/${APP_SLUG}[bot]" --jq .id)
git config user.name "${APP_SLUG}[bot]"
git config user.email "${id}+${APP_SLUG}[bot]@users.noreply.github.com"

- name: Propose new core tags
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
INPUT_TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
if [ -n "$INPUT_TAG" ]; then
tags=$INPUT_TAG
else
tags=$(scripts/core-sync/core-tags.sh)
fi
if [ -z "$tags" ]; then
echo "No core tags newer than $(mise run --quiet core:version)."
exit 0
fi
for t in $tags; do
scripts/core-sync/open-bump-pr.sh "$t"
done

- name: Report failure
if: failure()
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: scripts/core-sync/report-failure.sh core-watch "$RUN_URL"
10 changes: 10 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ Shared test fixtures come from core's `sdk/conformance/` and are copied into
`tests/fixtures/`. `mise run conformance:check` compares them with core at the
pinned ref.

## Core bumps and drift

Automation watches core for new tags and opens a pull request that moves the
pin, labeled `core-bump` plus `stable` or `prerelease`. If a bump needs SDK
changes, push them to the same branch.

A nightly job checks core's `main` for breaking proto changes and RPCs the SDK
doesn't cover yet, and keeps one issue labeled `core-drift` up to date. See
[scripts/README.md](scripts/README.md) for how both work.

## Releases

SDK releases follow core releases: SDK `X.Y.Z` is built from core `vX.Y.Z`, and
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,21 @@ reads them, and `grpcio-tools` generates the bindings:
pinned protos, and `mise run conformance:check` does the same for the
vendored test fixtures and `tests/e2e/Dockerfile.workload`.

## Keeping up with core

`core-watch.yml` checks core's release tags hourly and opens a PR,
`chore(core): track <tag>`, that moves the pin, regenerates the bindings and
refreshes the files copied from core. It does that for each new stable tag and
for the newest pre-release. `core-drift.yml` compares core `main` with the pin
nightly and keeps one `Core main drift` issue up to date. Both run scripts you
can also run locally; see [scripts/README.md](scripts/README.md).

```shell
mise run core:tags # core tags newer than the pin
mise run core:bump -- v0.1.8 --dry-run # build the bump PR locally
mise run core:drift -- --out drift-report.md # compare core main with the pin
```

## E2E tests

`tests/e2e` runs against a real gateway; `mise run test` skips it. CI runs it
Expand Down
25 changes: 19 additions & 6 deletions mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,17 +65,18 @@ fi
"""

[tasks."conformance:check"]
description = "Fail if vendored fixtures or the E2E workload Dockerfile differ from core at the pinned ref"
description = "Fail if a file copied from core (scripts/vendored-core-files.txt) differs from core at the pinned ref"
run = """
#!/usr/bin/env bash
set -euo pipefail
core=$(mktemp -d)
trap 'rm -rf "$core"' EXIT
scripts/fetch-core.sh "$OPENSHELL_CORE_REF" "$core" sdk/conformance e2e/python/Dockerfile.workload
for fixture in oauth-client-credentials.json oauth-client-credentials.json.license; do
diff -u "$core/sdk/conformance/$fixture" "tests/fixtures/$fixture"
done
diff -u "$core/e2e/python/Dockerfile.workload" tests/e2e/Dockerfile.workload
files=$(grep -v '^#' scripts/vendored-core-files.txt)
# shellcheck disable=SC2046 # one argument per core path
scripts/fetch-core.sh "$OPENSHELL_CORE_REF" "$core" $(awk '{ print $1 }' <<<"$files")
while read -r src dst; do
diff -u "$core/$src" "$dst"
done <<<"$files"
"""

[tasks.lint]
Expand Down Expand Up @@ -143,3 +144,15 @@ done
[tasks.ci]
description = "Run the full CI pipeline"
depends = ["check", "test", "build"]

[tasks."core:tags"]
description = "List core release tags newer than the pin (newest pre-release only, --all for every tag)"
run = "scripts/core-sync/core-tags.sh"

[tasks."core:bump"]
description = "Open the update proposal for a core tag: mise run core:bump -- <tag> [--dry-run]"
run = "scripts/core-sync/open-bump-pr.sh"

[tasks."core:drift"]
description = "Compare core main with the pin: mise run core:drift -- --out <report.md>"
run = "scripts/core-sync/drift-check.sh"
44 changes: 44 additions & 0 deletions scripts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Scripts

The workflows under `.github/workflows/` run these scripts, most through a mise task, so each job can be run locally first. Scripts that write to GitHub take `--dry-run` and print what they would do instead.

## `core-sync/`: keeping up with core

Move the core pin (`OPENSHELL_CORE_REF` in `mise.toml`) to new core releases, and watch core `main` for changes that will break the SDK.

The workflows use a GitHub App token from the `sdk-bot` environment (secrets `SDK_BOT_CLIENT_ID` and `SDK_BOT_PRIVATE_KEY`), which only `main` can deploy to. They never use `GITHUB_TOKEN`, because pushes and PRs made with it don't trigger CI.

| File | mise task | Purpose |
|------|-----------|---------|
| `core-tags.sh [--all]` | `core:tags` | List NVIDIA/OpenShell release tags newer than the pin: every stable tag, plus the newest pre-release when it is newer than all of them. |
| `open-bump-pr.sh <tag> [--dry-run]` | `core:bump` | Open the update proposal for one core tag: branch `core/<tag>`, pin change, regenerated bindings, refreshed copies of core files, proto diff in the body, labels `core-bump` plus `stable` or `prerelease`. Closes older pre-release proposals. |
| `bump-pr-body.md` | | PR body template for `open-bump-pr.sh`. |
| `drift-check.sh --out <file>` | `core:drift` | Compare core `main` with the pin: `buf breaking`, regenerate and run the unit tests, list the RPCs the SDK doesn't call. Writes the markdown report. |
| `update-drift-issue.sh <report.md> [--dry-run]` | | Create, update, or close the single `Core main drift` issue from a report. |
| `report-failure.sh <job> <run-url> [--dry-run]` | | Append a dated failure line to the drift issue. Called from `if: failure()` steps. |
| `drift-issue.sh` | | Shared helpers for the two drift issue scripts (sourced, not run). |

| Workflow | Runs |
|----------|------|
| `core-watch.yml` (hourly) | `core-tags.sh`, then `open-bump-pr.sh` per tag |
| `core-drift.yml` (nightly) | `drift-check.sh`, then `update-drift-issue.sh` |

Bump PRs run E2E against their own pin, because `e2e.yml` defaults to `OPENSHELL_CORE_REF` on the PR branch.

## `e2e/`: the E2E gateway

| File | Purpose |
|------|---------|
| `start-gateway.sh <core-version>` | Run the published gateway of a core version with the Docker driver for the E2E lane, and register it as `e2e`. |
| `stop-gateway.sh` | Stop it, collect its log, and remove its sandboxes. |

`e2e.yml` (reusable, called from `ci.yml`) runs `start-gateway.sh`, `mise run e2e`, then `stop-gateway.sh`.

## Build and checks

| File | mise task | Purpose |
|------|-----------|---------|
| `generate_python_proto.py` | `proto:gen` | Generate `src/openshell/_proto` from `proto/`. |
| `verify-python-wheel.py` | `build` | Check the wheel's contents and version. |
| `vendored-core-files.txt` | `conformance:check` | Files copied from core. `conformance:check` compares them with core at the pin; `core-sync/open-bump-pr.sh` refreshes them. |
| `fetch-core.sh <ref> <dir> <path>...` | `conformance:check` | Copy files from core at a ref. |
16 changes: 16 additions & 0 deletions scripts/core-sync/bump-pr-body.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Move the core pin from `{{OLD}}` to `{{NEW}}` ({{KIND}}).

- Core changes: [{{OLD}}...{{NEW}}]({{COMPARE_URL}})
- Regenerated bindings: {{GENERATED}}
- E2E runs against `{{IMAGE}}`
- Files copied from core:
{{FIXTURES}}

## Proto changes

{{PROTO_DIFF}}

## Before merging

- CI and E2E must pass. If the SDK needs changes for this core version, push them to this branch.
- Pre-release pins are not released; a newer pre-release proposal replaces this one.
68 changes: 68 additions & 0 deletions scripts/core-sync/core-tags.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: Apache-2.0

# core-tags.sh: print NVIDIA/OpenShell release tags newer than the pin,
# oldest first.
#
# Usage: core-tags.sh [--all] [--pin <tag>]
#
# Release tags are vX.Y.Z and vX.Y.Z-pre.N; a pre-release sorts below its
# stable version. By default every newer stable tag is printed, plus the
# newest pre-release when it is newer than all of them: one proposal for the
# current release train instead of one per pre-release. --all prints every
# newer tag. --pin overrides the pin, OPENSHELL_CORE_REF in mise.toml.
set -euo pipefail

repo=${CORE_REPO:-NVIDIA/OpenShell}
all=0
pin=

while [ $# -gt 0 ]; do
case $1 in
--all) all=1; shift ;;
--pin) pin=${2:?--pin needs a tag}; shift 2 ;;
*) echo "usage: $0 [--all] [--pin <tag>]" >&2; exit 2 ;;
esac
done

[ -n "$pin" ] || pin=$(sed -n 's/^OPENSHELL_CORE_REF = "\(.*\)"$/\1/p' mise.toml)
[ -n "$pin" ] || { echo "ERROR: no OPENSHELL_CORE_REF in mise.toml" >&2; exit 1; }

# key <tag>: "major minor patch rank pre" with rank 1 for stable, 0 for pre.
key() {
awk -v t="$1" 'BEGIN {
s = t; sub(/^v/, "", s)
pre = ""; rank = 1
if (index(s, "-pre.")) { pre = substr(s, index(s, "-pre.") + 5); s = substr(s, 1, index(s, "-pre.") - 1); rank = 0 }
split(s, v, ".")
printf "%d %d %d %d %d\n", v[1], v[2], v[3], rank, (pre == "" ? 0 : pre)
}'
}

newer=$(
gh api --paginate "repos/$repo/git/matching-refs/tags/v" --jq '.[].ref' |
sed 's#^refs/tags/##' |
{ grep -E '^v[0-9]+\.[0-9]+\.[0-9]+(-pre\.[0-9]+)?$' || true; } |
while read -r t; do echo "$(key "$t") $t"; done |
awk -v p="$(key "$pin")" '
BEGIN { split(p, k, " ") }
{
for (i = 1; i <= 5; i++) {
if ($i + 0 > k[i] + 0) { print; next }
if ($i + 0 < k[i] + 0) next
}
}' |
sort -n -k1,1 -k2,2 -k3,3 -k4,4 -k5,5 |
awk '{ print $6 }'
)

[ -n "$newer" ] || exit 0
if [ "$all" -eq 1 ]; then
echo "$newer"
exit 0
fi

# Stable tags, then the last tag if it is a pre-release (newer than every stable).
grep -v -- '-pre\.' <<<"$newer" || true
last=$(tail -1 <<<"$newer")
case $last in *-pre.*) echo "$last" ;; esac
Loading
Loading