Skip to content

Commit 43f0db9

Browse files
CSResselnori-agent
andcommitted
ci: Add automated upstream sync workflow
Adds GitHub Actions workflow that: - Runs daily at 9 AM UTC to check for new upstream stable releases - Detects latest stable tag (X.Y.Z only, no alpha/beta) - Updates fork/upstream-sync branch to track release - Creates sync/upstream-vX.Y.Z branch from the tag - Opens draft PR against dev with merge instructions Supports manual trigger with optional tag and dry-run mode. Idempotent - skips if sync branch already exists. Also adds nori-releases.md documenting the branching strategy and sync workflow. 🤖 Generated with [Nori](https://nori.ai) Co-Authored-By: Nori <noreply@tilework.tech>
1 parent 96cafb4 commit 43f0db9

2 files changed

Lines changed: 339 additions & 0 deletions

File tree

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
# Automatically sync stable upstream releases into dev branch.
2+
# Creates a sync branch and draft PR for each new stable release.
3+
#
4+
# Manual trigger: gh workflow run upstream-sync.yml
5+
# With specific tag: gh workflow run upstream-sync.yml -f tag=rust-v0.63.0
6+
# Dry run: gh workflow run upstream-sync.yml -f dry_run=true
7+
8+
name: upstream-sync
9+
10+
on:
11+
schedule:
12+
# Daily at 9 AM UTC (reasonable time for review in US timezones)
13+
- cron: '0 9 * * *'
14+
workflow_dispatch:
15+
inputs:
16+
tag:
17+
description: 'Specific tag to sync (optional, defaults to latest stable)'
18+
required: false
19+
type: string
20+
dry_run:
21+
description: 'Dry run mode (log actions without creating branches/PRs)'
22+
required: false
23+
type: boolean
24+
default: false
25+
26+
env:
27+
UPSTREAM_REMOTE: https://github.com/openai/codex.git
28+
29+
jobs:
30+
sync:
31+
runs-on: ubuntu-latest
32+
permissions:
33+
contents: write
34+
pull-requests: write
35+
steps:
36+
- name: Checkout repository
37+
uses: actions/checkout@v4
38+
with:
39+
fetch-depth: 0
40+
token: ${{ secrets.GITHUB_TOKEN }}
41+
42+
- name: Configure git
43+
run: |
44+
git config user.name "github-actions[bot]"
45+
git config user.email "github-actions[bot]@users.noreply.github.com"
46+
47+
- name: Add upstream remote and fetch tags
48+
run: |
49+
git remote add upstream "$UPSTREAM_REMOTE" || git remote set-url upstream "$UPSTREAM_REMOTE"
50+
git fetch upstream --tags --force
51+
52+
- name: Determine target tag
53+
id: tag
54+
run: |
55+
set -euo pipefail
56+
57+
if [[ -n "${{ inputs.tag }}" ]]; then
58+
target_tag="${{ inputs.tag }}"
59+
echo "Using manually specified tag: $target_tag"
60+
else
61+
# Find latest stable tag (X.Y.Z only, no alpha/beta/rc)
62+
# Pattern: rust-v followed by semver without prerelease suffix
63+
target_tag=$(git tag -l 'rust-v*' \
64+
| grep -E '^rust-v[0-9]+\.[0-9]+\.[0-9]+$' \
65+
| sort -V \
66+
| tail -1)
67+
echo "Detected latest stable tag: $target_tag"
68+
fi
69+
70+
if [[ -z "$target_tag" ]]; then
71+
echo "::error::No stable upstream tag found"
72+
exit 1
73+
fi
74+
75+
# Validate tag exists
76+
if ! git rev-parse "$target_tag" >/dev/null 2>&1; then
77+
echo "::error::Tag $target_tag does not exist"
78+
exit 1
79+
fi
80+
81+
# Extract version for branch naming (rust-v0.63.0 -> v0.63.0)
82+
version="${target_tag#rust-}"
83+
sync_branch="sync/upstream-${version}"
84+
85+
echo "target_tag=$target_tag" >> "$GITHUB_OUTPUT"
86+
echo "version=$version" >> "$GITHUB_OUTPUT"
87+
echo "sync_branch=$sync_branch" >> "$GITHUB_OUTPUT"
88+
89+
- name: Check if sync branch already exists
90+
id: check
91+
run: |
92+
set -euo pipefail
93+
94+
sync_branch="${{ steps.tag.outputs.sync_branch }}"
95+
96+
# Check if branch exists on origin
97+
if git ls-remote --heads origin "$sync_branch" | grep -q "$sync_branch"; then
98+
echo "::notice::Sync branch $sync_branch already exists, skipping"
99+
echo "exists=true" >> "$GITHUB_OUTPUT"
100+
else
101+
echo "Sync branch $sync_branch does not exist, will create"
102+
echo "exists=false" >> "$GITHUB_OUTPUT"
103+
fi
104+
105+
- name: Update fork/upstream-sync tracking branch
106+
if: steps.check.outputs.exists == 'false'
107+
run: |
108+
set -euo pipefail
109+
110+
target_tag="${{ steps.tag.outputs.target_tag }}"
111+
dry_run="${{ inputs.dry_run }}"
112+
113+
echo "Updating fork/upstream-sync to point to $target_tag"
114+
115+
if [[ "$dry_run" == "true" ]]; then
116+
echo "::notice::DRY RUN: Would update fork/upstream-sync to $target_tag"
117+
else
118+
# Create or update the tracking branch
119+
git branch -f fork/upstream-sync "$target_tag"
120+
git push origin fork/upstream-sync --force
121+
fi
122+
123+
- name: Create sync branch
124+
if: steps.check.outputs.exists == 'false'
125+
run: |
126+
set -euo pipefail
127+
128+
target_tag="${{ steps.tag.outputs.target_tag }}"
129+
sync_branch="${{ steps.tag.outputs.sync_branch }}"
130+
dry_run="${{ inputs.dry_run }}"
131+
132+
echo "Creating sync branch $sync_branch from $target_tag"
133+
134+
if [[ "$dry_run" == "true" ]]; then
135+
echo "::notice::DRY RUN: Would create branch $sync_branch from $target_tag"
136+
else
137+
git checkout -b "$sync_branch" "$target_tag"
138+
git push origin "$sync_branch"
139+
fi
140+
141+
- name: Create draft PR
142+
if: steps.check.outputs.exists == 'false'
143+
env:
144+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
145+
TARGET_TAG: ${{ steps.tag.outputs.target_tag }}
146+
SYNC_BRANCH: ${{ steps.tag.outputs.sync_branch }}
147+
DRY_RUN: ${{ inputs.dry_run }}
148+
run: |
149+
set -euo pipefail
150+
151+
# Count commits between dev and the tag for context
152+
commit_count=$(git rev-list --count origin/dev.."$TARGET_TAG" 2>/dev/null || echo "unknown")
153+
154+
pr_title="Sync upstream $TARGET_TAG"
155+
156+
# Build PR body using heredoc
157+
pr_body=$(cat <<EOF
158+
## Upstream Sync
159+
160+
This PR syncs changes from upstream release \`$TARGET_TAG\`.
161+
162+
### Summary
163+
164+
- **Upstream tag:** \`$TARGET_TAG\`
165+
- **Commits to merge:** ~$commit_count
166+
- **Release notes:** [GitHub Release](https://github.com/openai/codex/releases/tag/$TARGET_TAG)
167+
168+
### Merge Instructions
169+
170+
1. Review the changes for conflicts with our ACP fork work
171+
2. Resolve any merge conflicts:
172+
\`\`\`bash
173+
git checkout dev
174+
git merge $SYNC_BRANCH --no-ff
175+
# Resolve conflicts if any
176+
\`\`\`
177+
3. Run tests: \`cd codex-rs && cargo test\`
178+
4. Update snapshot tests if needed: \`cargo insta review\`
179+
5. Mark as ready for review when satisfied
180+
181+
### After Merge
182+
183+
- Delete the \`$SYNC_BRANCH\` branch
184+
- Consider tagging a new nori release if significant changes
185+
EOF
186+
)
187+
188+
if [[ "$DRY_RUN" == "true" ]]; then
189+
echo "::notice::DRY RUN: Would create PR with title: $pr_title"
190+
echo "PR body would be:"
191+
echo "$pr_body"
192+
else
193+
gh pr create \
194+
--base dev \
195+
--head "$SYNC_BRANCH" \
196+
--title "$pr_title" \
197+
--body "$pr_body" \
198+
--draft
199+
200+
echo "::notice::Created draft PR for $TARGET_TAG"
201+
fi
202+
203+
- name: Summary
204+
run: |
205+
echo "## Upstream Sync Summary" >> "$GITHUB_STEP_SUMMARY"
206+
echo "" >> "$GITHUB_STEP_SUMMARY"
207+
echo "- **Target tag:** ${{ steps.tag.outputs.target_tag }}" >> "$GITHUB_STEP_SUMMARY"
208+
echo "- **Sync branch:** ${{ steps.tag.outputs.sync_branch }}" >> "$GITHUB_STEP_SUMMARY"
209+
echo "- **Branch existed:** ${{ steps.check.outputs.exists }}" >> "$GITHUB_STEP_SUMMARY"
210+
echo "- **Dry run:** ${{ inputs.dry_run || 'false' }}" >> "$GITHUB_STEP_SUMMARY"

nori-releases.md

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
## Git Repository Analysis
2+
3+
Current State
4+
5+
| Branch/Ref | Based On | Commits Behind Upstream | Unique Commits |
6+
|-------------|------------------------|-------------------------|----------------|
7+
| origin/dev | upstream/main @ Nov 18 | 188 commits | 48 commits |
8+
| origin/main | (separate) | varies | varies |
9+
10+
Fork Point Details:
11+
12+
- Fork commit: b035c604b (Nov 18, 2025)
13+
- Fork is between: rust-v0.58.0 (Nov 13) and rust-v0.59.0 (Nov 19)
14+
- Current Cargo.toml: version = "0.0.0" (decoupled from upstream versioning)
15+
16+
## Upstream Release Cadence
17+
18+
Upstream releases are very rapid (multiple releases per week):
19+
20+
- rust-v0.58.0: Nov 13
21+
- rust-v0.59.0: Nov 19
22+
- rust-v0.60.1: Nov 19 (same day!)
23+
- rust-v0.61.0: Nov 20
24+
- rust-v0.62.0: Nov 21
25+
- rust-v0.63.0: Nov 21 (same day!)
26+
27+
Release Workflow (from rust-release.yml):
28+
29+
1. Manual process: git tag -a rust-vX.Y.Z → git push origin rust-vX.Y.Z
30+
2. CI validates tag matches codex-rs/Cargo.toml version
31+
3. Builds multi-platform binaries with code signing
32+
4. Publishes to GitHub Releases and npm
33+
34+
## Branching Strategy
35+
36+
```
37+
upstream/main ──●──●──●──●──●──●──●──●──●──●──●──●──●──●──●──●───→
38+
│ ▲ ▲ ▲
39+
│ │0.61.0 │0.63.0 │future release
40+
│ │ │ │
41+
▼ │ │ │
42+
fork/upstream-sync ──────┴──────────────┴──────────────┴───────→
43+
│ │
44+
│ merge │ merge
45+
▼ ▼
46+
origin/dev ─────●────●────●────●────────●────●────●────────────→ (your ACP work)
47+
```
48+
49+
Branch Roles:
50+
51+
| Branch | Purpose |
52+
|--------------------|-----------------------------------------------|
53+
| origin/main | Stable releases of your fork |
54+
| origin/dev | Active development (ACP features) |
55+
| fork/upstream-main | Tracks upstream/main exactly (already exists) |
56+
| fork/upstream-sync | NEW: Sync point branch for merges |
57+
58+
## Automated Sync (CI)
59+
60+
The `upstream-sync` GitHub Actions workflow automatically detects new stable
61+
upstream releases and creates draft PRs.
62+
63+
**Trigger:** Daily at 9 AM UTC (scheduled) or manual via workflow_dispatch
64+
65+
**What it does:**
66+
67+
1. Fetches upstream tags
68+
2. Finds latest stable tag (X.Y.Z only, no alpha/beta)
69+
3. Updates `fork/upstream-sync` branch to point to the tag
70+
4. Creates `sync/upstream-vX.Y.Z` branch from the tag
71+
5. Opens a draft PR against `dev` with merge instructions
72+
73+
**Manual trigger:**
74+
75+
```bash
76+
# Sync latest stable release
77+
gh workflow run upstream-sync.yml
78+
79+
# Sync specific tag
80+
gh workflow run upstream-sync.yml -f tag=rust-v0.63.0
81+
82+
# Dry run (test without creating branches/PRs)
83+
gh workflow run upstream-sync.yml -f dry_run=true
84+
```
85+
86+
**Idempotency:** If a sync branch already exists, the workflow skips that release.
87+
88+
## Manual Sync Workflow
89+
90+
For manual syncing (or if CI is unavailable):
91+
92+
1. Update tracking branch
93+
```bash
94+
git fetch upstream --tags
95+
git branch -f fork/upstream-sync rust-v0.63.0
96+
git push origin fork/upstream-sync --force
97+
```
98+
99+
2. Create sync branch from the release tag
100+
```bash
101+
git checkout -b sync/upstream-v0.63.0 rust-v0.63.0
102+
git push origin sync/upstream-v0.63.0
103+
```
104+
105+
3. Merge into dev with conflict resolution
106+
```bash
107+
git checkout dev
108+
git merge sync/upstream-v0.63.0 --no-ff -m "Sync upstream rust-v0.63.0"
109+
```
110+
111+
4. Resolve conflicts, test, push
112+
```bash
113+
cd codex-rs && cargo test
114+
cargo insta review # if snapshot tests need updating
115+
git push origin dev
116+
```
117+
118+
## Downstream Nori Releases
119+
120+
For now we will maintain our own separate versioning scheme, to avoid dependency
121+
on the upstream releases for our release tagging.
122+
123+
For example for nori-v0.2.0 or similar:
124+
125+
git checkout main
126+
git merge dev --no-ff
127+
git tag -a nori-v0.2.0 -m "Nori release 0.2.0"
128+
git push origin main --tags
129+

0 commit comments

Comments
 (0)