Skip to content

Commit df083ca

Browse files
authored
ci(core): propose core release bumps and track drift on core main (#13)
Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
1 parent 40e0569 commit df083ca

14 files changed

Lines changed: 824 additions & 13 deletions

‎.github/workflows/core-drift.yml‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
3+
# Nightly comparison of core main with the pinned core tag; keeps the single
4+
# "Core main drift" issue current (mise run core:drift).
5+
name: core-drift
6+
7+
on:
8+
schedule:
9+
- cron: "41 4 * * *"
10+
workflow_dispatch:
11+
12+
permissions:
13+
contents: read
14+
15+
concurrency:
16+
group: core-drift
17+
cancel-in-progress: false
18+
19+
jobs:
20+
drift:
21+
name: drift
22+
# The bot secrets live in the sdk-bot environment, which only main can
23+
# deploy to, so a pushed branch can't mint a bot token.
24+
environment: sdk-bot
25+
runs-on: ubuntu-latest
26+
timeout-minutes: 20
27+
steps:
28+
- id: app-token
29+
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
30+
with:
31+
client-id: ${{ secrets.SDK_BOT_CLIENT_ID }}
32+
private-key: ${{ secrets.SDK_BOT_PRIVATE_KEY }}
33+
permission-issues: write
34+
35+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
36+
with:
37+
persist-credentials: false
38+
39+
- uses: jdx/mise-action@f5be966e4a917d54244728e6a5fdcd1d93a35e6c # v5.2.6
40+
41+
# Exit 1 with a report means drift; the issue records it. No report
42+
# means the check itself broke, which the failure step records.
43+
- name: Check drift against core main
44+
run: |
45+
mise run core:drift -- --out drift-report.md || true
46+
test -s drift-report.md
47+
cat drift-report.md >>"$GITHUB_STEP_SUMMARY"
48+
49+
- name: Update the drift issue
50+
env:
51+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
52+
run: mise run core:drift-issue -- drift-report.md
53+
54+
- name: Report failure
55+
if: failure()
56+
env:
57+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
58+
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
59+
run: mise run core:report-failure -- core-drift "$RUN_URL"

‎.github/workflows/core-watch.yml‎

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
3+
# Opens one update proposal per new core release tag
4+
# (mise run core:bump).
5+
name: core-watch
6+
7+
on:
8+
schedule:
9+
- cron: "17 * * * *"
10+
workflow_dispatch:
11+
inputs:
12+
tag:
13+
description: Core tag to propose (vX.Y.Z or vX.Y.Z-pre.N); empty proposes every new tag
14+
required: false
15+
default: ""
16+
17+
permissions:
18+
contents: read
19+
20+
concurrency:
21+
group: core-watch
22+
cancel-in-progress: false
23+
24+
jobs:
25+
watch:
26+
name: watch
27+
# The bot secrets live in the sdk-bot environment, which only main can
28+
# deploy to, so a pushed branch can't mint a bot token.
29+
environment: sdk-bot
30+
runs-on: ubuntu-latest
31+
timeout-minutes: 20
32+
steps:
33+
- id: app-token
34+
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
35+
with:
36+
client-id: ${{ secrets.SDK_BOT_CLIENT_ID }}
37+
private-key: ${{ secrets.SDK_BOT_PRIVATE_KEY }}
38+
permission-contents: write
39+
permission-pull-requests: write
40+
permission-issues: write
41+
42+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
43+
with:
44+
fetch-depth: 0
45+
token: ${{ steps.app-token.outputs.token }}
46+
persist-credentials: true
47+
48+
- uses: jdx/mise-action@f5be966e4a917d54244728e6a5fdcd1d93a35e6c # v5.2.6
49+
50+
- name: Set the bot identity
51+
env:
52+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
53+
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
54+
run: |
55+
set -euo pipefail
56+
id=$(gh api "users/${APP_SLUG}[bot]" --jq .id)
57+
git config user.name "${APP_SLUG}[bot]"
58+
git config user.email "${id}+${APP_SLUG}[bot]@users.noreply.github.com"
59+
60+
- name: Propose new core tags
61+
env:
62+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
63+
INPUT_TAG: ${{ inputs.tag }}
64+
run: |
65+
set -euo pipefail
66+
if [ -n "$INPUT_TAG" ]; then
67+
tags=$INPUT_TAG
68+
else
69+
tags=$(mise run --quiet core:tags)
70+
fi
71+
if [ -z "$tags" ]; then
72+
echo "No core tags newer than $(mise run --quiet core:version)."
73+
exit 0
74+
fi
75+
for t in $tags; do
76+
mise run core:bump -- "$t"
77+
done
78+
79+
- name: Report failure
80+
if: failure()
81+
env:
82+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
83+
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
84+
run: mise run core:report-failure -- core-watch "$RUN_URL"

‎AGENTS.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ release model.
77
## Commands
88

99
- `mise run ci` runs the full pipeline (format check, lint, type check, proto
10-
check, conformance check, unit tests, build).
10+
check, vendored-file check, unit tests, build).
1111
- `mise run proto:gen` regenerates `src/openshell/_proto` from core at the ref
1212
pinned in `OPENSHELL_CORE_REF` (`mise.toml`). Never edit generated files by
1313
hand.
@@ -22,7 +22,7 @@ release model.
2222
- Unit tests live in `tests/` and are named `*_test.py`; fixtures go in
2323
`tests/fixtures/`.
2424
- Files copied from core must stay identical to core at the pin;
25-
`mise run conformance:check` compares them.
25+
`mise run vendored:check` compares them.
2626
- New files need only the license line `SPDX-License-Identifier: Apache-2.0`
2727
in a comment. Keep the existing headers as they are.
2828

‎CONTRIBUTING.md‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ virtual environment. `mise tasks` lists every task. The ones you need most:
1616
| Task | What it does |
1717
|------|--------------|
1818
| `mise run ci` | Everything CI runs: checks, unit tests, build |
19-
| `mise run check` | Format check, lint, type check, proto check, conformance check |
19+
| `mise run check` | Format check, lint, type check, proto check, vendored-file check |
2020
| `mise run format` | Format the code |
2121
| `mise run test` | Unit tests; `UV_PYTHON=3.11` picks the interpreter |
2222
| `mise run build` | Build the wheel and sdist, verify them, and install them on Python 3.11 |
@@ -63,9 +63,19 @@ generated files; change the pin and run `mise run proto:gen`.
6363
Building a wheel or sdist needs neither a core checkout nor network access because the generated bindings are committed. A change to the core pin makes the generated API changes visible in the diff.
6464

6565
Shared test fixtures come from core's `sdk/conformance/` and are copied into
66-
`tests/fixtures/`. `mise run conformance:check` compares them with core at the
66+
`tests/fixtures/`. `mise run vendored:check` compares them with core at the
6767
pinned ref.
6868

69+
## Core bumps and drift
70+
71+
Automation watches core for new tags and opens a pull request that moves the
72+
pin, labeled `core-bump` plus `stable` or `prerelease`. If a bump needs SDK
73+
changes, push them to the same branch.
74+
75+
A nightly job checks core's `main` for breaking proto changes and RPCs the SDK
76+
doesn't cover yet, and keeps one issue labeled `core-drift` up to date. See
77+
[scripts/README.md](scripts/README.md) for how both work.
78+
6979
## Releases
7080

7181
SDK releases follow core releases: SDK `X.Y.Z` is built from core `vX.Y.Z`, and

‎mise.toml‎

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ buf = "1.72.0"
1717
uv = "0.10.12"
1818

1919
[env]
20-
# The single core pin: a release tag (or a full commit SHA).
20+
# The single core pin: a release tag. scripts/core-sync/ requires a tag.
2121
OPENSHELL_CORE_REF = "v0.1.7"
2222
SETUPTOOLS_SCM_PRETEND_VERSION_FOR_OPENSHELL = "0.0.0"
2323

@@ -64,18 +64,19 @@ if ! git diff --exit-code --stat -- src/openshell/_proto \
6464
fi
6565
"""
6666

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

8182
[tasks.lint]
@@ -116,7 +117,7 @@ run = "uv run --frozen pytest tests/e2e -n ${E2E_PARALLEL:-5}"
116117

117118
[tasks.check]
118119
description = "Run formatting, lint, type, and drift checks"
119-
depends = ["format:check", "lint", "typecheck", "proto:check", "conformance:check"]
120+
depends = ["format:check", "lint", "typecheck", "proto:check", "vendored:check"]
120121

121122
[tasks.build]
122123
description = "Build the wheel and sdist, verify the wheel, and install both on the oldest Python"
@@ -143,3 +144,23 @@ done
143144
[tasks.ci]
144145
description = "Run the full CI pipeline"
145146
depends = ["check", "test", "build"]
147+
148+
[tasks."core:tags"]
149+
description = "List core release tags newer than the pin (newest pre-release only, --all for every tag)"
150+
run = "scripts/core-sync/core-tags.sh"
151+
152+
[tasks."core:bump"]
153+
description = "Open the update proposal for a core tag: mise run core:bump -- <tag> [--dry-run]"
154+
run = "scripts/core-sync/open-bump-pr.sh"
155+
156+
[tasks."core:drift"]
157+
description = "Compare core main with the pin: mise run core:drift -- --out <report.md>"
158+
run = "scripts/core-sync/drift-check.sh"
159+
160+
[tasks."core:drift-issue"]
161+
description = "Create, update, or close the core main drift issue from a report: mise run core:drift-issue -- <report.md> [--dry-run]"
162+
run = "scripts/core-sync/update-drift-issue.sh"
163+
164+
[tasks."core:report-failure"]
165+
description = "Record a failed automation run in the drift issue: mise run core:report-failure -- <job> <run-url> [--dry-run]"
166+
run = "scripts/core-sync/report-failure.sh"

‎scripts/README.md‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Scripts
2+
3+
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.
4+
5+
## `core-sync/`: keeping up with core
6+
7+
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.
8+
9+
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.
10+
11+
| File | mise task | Purpose |
12+
|------|-----------|---------|
13+
| `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. |
14+
| `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. |
15+
| `bump-pr-body.md` | | PR body template for `open-bump-pr.sh`. |
16+
| `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. |
17+
| `update-drift-issue.sh <report.md> [--dry-run]` | `core:drift-issue` | Create, update, or close the single `Core main drift` issue from a report. |
18+
| `report-failure.sh <job> <run-url> [--dry-run]` | `core:report-failure` | Append a dated failure line to the drift issue. Called from `if: failure()` steps. |
19+
| `drift-issue.sh` | | Shared helpers for the two drift issue scripts (sourced, not run). |
20+
21+
| Workflow | Runs (mise tasks) |
22+
|----------|------|
23+
| `core-watch.yml` (hourly) | `core:tags`, then `core:bump` per tag |
24+
| `core-drift.yml` (nightly) | `core:drift`, then `core:drift-issue` |
25+
26+
Bump PRs run E2E against their own pin, because `e2e.yml` defaults to `OPENSHELL_CORE_REF` on the PR branch.
27+
28+
## `e2e/`: the E2E gateway
29+
30+
| File | Purpose |
31+
|------|---------|
32+
| `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`. |
33+
| `stop-gateway.sh` | Stop it, collect its log, and remove its sandboxes. |
34+
35+
`e2e.yml` (reusable, called from `ci.yml`) runs `start-gateway.sh`, `mise run e2e`, then `stop-gateway.sh`.
36+
37+
## Build and checks
38+
39+
| File | mise task | Purpose |
40+
|------|-----------|---------|
41+
| `generate_python_proto.py` | `proto:gen` | Generate `src/openshell/_proto` from `proto/`. |
42+
| `verify-python-wheel.py` | `build` | Check the wheel's contents and version. |
43+
| `vendored-core-files.txt` | `vendored:check` | Files copied from core. `vendored:check` compares them with core at the pin; `core-sync/open-bump-pr.sh` refreshes them. |
44+
| `fetch-core.sh <ref> <dir> <path>...` | `vendored:check` | Copy files from core at a ref. |

‎scripts/core-sync/bump-pr-body.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
Move the core pin from `{{OLD}}` to `{{NEW}}` ({{KIND}}).
2+
3+
- Core changes: [{{OLD}}...{{NEW}}]({{COMPARE_URL}})
4+
- Regenerated bindings: {{GENERATED}}
5+
- E2E runs against `{{IMAGE}}`
6+
- Files copied from core:
7+
{{FIXTURES}}
8+
9+
## Proto changes
10+
11+
{{PROTO_DIFF}}
12+
13+
## Before merging
14+
15+
- CI and E2E must pass. If the SDK needs changes for this core version, push them to this branch.
16+
- Pre-release pins are not released; a newer pre-release proposal replaces this one.

‎scripts/core-sync/core-tags.sh‎

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#!/usr/bin/env bash
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
# core-tags.sh: print NVIDIA/OpenShell release tags newer than the pin,
5+
# oldest first.
6+
#
7+
# Usage: core-tags.sh [--all] [--pin <tag>]
8+
#
9+
# Release tags are vX.Y.Z and vX.Y.Z-pre.N; a pre-release sorts below its
10+
# stable version. By default every newer stable tag is printed, plus the
11+
# newest pre-release when it is newer than all of them: one proposal for the
12+
# current release train instead of one per pre-release. --all prints every
13+
# newer tag. --pin overrides the pin, OPENSHELL_CORE_REF in mise.toml. The pin
14+
# must be a release tag; a commit SHA fails.
15+
set -euo pipefail
16+
17+
repo=${CORE_REPO:-NVIDIA/OpenShell}
18+
all=0
19+
pin=
20+
21+
while [ $# -gt 0 ]; do
22+
case $1 in
23+
--all) all=1; shift ;;
24+
--pin) pin=${2:?--pin needs a tag}; shift 2 ;;
25+
*) echo "usage: $0 [--all] [--pin <tag>]" >&2; exit 2 ;;
26+
esac
27+
done
28+
29+
[ -n "$pin" ] || pin=$(sed -n 's/^OPENSHELL_CORE_REF = "\(.*\)"$/\1/p' mise.toml)
30+
[ -n "$pin" ] || { echo "ERROR: no OPENSHELL_CORE_REF in mise.toml" >&2; exit 1; }
31+
32+
[[ "$pin" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-pre\.[0-9]+)?$ ]] ||
33+
{ echo "ERROR: pin '$pin' is not a release tag; commit SHA pins are not supported by core-sync" >&2; exit 1; }
34+
35+
# key <tag>: "major minor patch rank pre" with rank 1 for stable, 0 for pre.
36+
key() {
37+
awk -v t="$1" 'BEGIN {
38+
s = t; sub(/^v/, "", s)
39+
pre = ""; rank = 1
40+
if (index(s, "-pre.")) { pre = substr(s, index(s, "-pre.") + 5); s = substr(s, 1, index(s, "-pre.") - 1); rank = 0 }
41+
split(s, v, ".")
42+
printf "%d %d %d %d %d\n", v[1], v[2], v[3], rank, (pre == "" ? 0 : pre)
43+
}'
44+
}
45+
46+
newer=$(
47+
gh api --paginate "repos/$repo/git/matching-refs/tags/v" --jq '.[].ref' |
48+
sed 's#^refs/tags/##' |
49+
{ grep -E '^v[0-9]+\.[0-9]+\.[0-9]+(-pre\.[0-9]+)?$' || true; } |
50+
while read -r t; do echo "$(key "$t") $t"; done |
51+
awk -v p="$(key "$pin")" '
52+
BEGIN { split(p, k, " ") }
53+
{
54+
for (i = 1; i <= 5; i++) {
55+
if ($i + 0 > k[i] + 0) { print; next }
56+
if ($i + 0 < k[i] + 0) next
57+
}
58+
}' |
59+
sort -n -k1,1 -k2,2 -k3,3 -k4,4 -k5,5 |
60+
awk '{ print $6 }'
61+
)
62+
63+
[ -n "$newer" ] || exit 0
64+
if [ "$all" -eq 1 ]; then
65+
echo "$newer"
66+
exit 0
67+
fi
68+
69+
# Stable tags, then the last tag if it is a pre-release (newer than every stable).
70+
grep -v -- '-pre\.' <<<"$newer" || true
71+
last=$(tail -1 <<<"$newer")
72+
case $last in *-pre.*) echo "$last" ;; esac

0 commit comments

Comments
 (0)