Skip to content
Merged
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
90 changes: 90 additions & 0 deletions .github/scripts/fork-upstream-drift.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
# Drift check for the fork's upstream sync cadence (WI-1125).
#
# Read-only: it fetches tags and fails when the fork has fallen behind, but it
# never rebases, pushes or edits a workflow. The rebase itself stays the
# runbook at docs/fork-upstream-sync.md, because replaying the carried series
# needs a person when a patch no longer applies.
#
# Two checks:
# 1. The newest stable rust-vX.Y.Z tag on the upstream remote (alpha and dev
# tags skipped) is recorded either as the rebase base in FORK.md and the
# upstream_version stamp in .github/workflows/fork-release.yml, or as the
# separate "Upstream fixes taken through" marker in FORK.md. The marker is
# for fixes taken by cherry-pick, which does not move the rebase base,
# because published fork-<sha> tags must stay reachable.
# 2. The carried-patch table in FORK.md has at most CARRIED_PATCH_CAP rows
# (about 10, the target in FORK.md).
set -euo pipefail

cap="${CARRIED_PATCH_CAP:-10}"
remote="${UPSTREAM_REMOTE:-upstream}"
root="$(git rev-parse --show-toplevel)"

git -C "$root" fetch --tags "$remote"

# Newest stable tag by version, not by tag date. `sort -V` orders the numeric
# suffix; a tag with a pre-release suffix (alpha, dev) is excluded first.
newest="$(
git -C "$root" tag -l 'rust-v*' |
grep -E '^rust-v[0-9]+(\.[0-9]+){2}$' |
sort -V |
tail -n1
)"
if [[ -z "$newest" ]]; then
echo "no stable rust-vX.Y.Z tag found on ${remote}" >&2
exit 1
fi
newest_version="${newest#rust-v}"

recorded_tag="$(
grep -oE 'rust-v[0-9]+(\.[0-9]+){2}' "$root/FORK.md" |
head -n1
)"
taken_through="$(
sed -n 's/^[[:space:]-]*Upstream fixes taken through:[[:space:]]*//p' "$root/FORK.md" |
head -n1
)"
stamped_version="$(
sed -n 's/^[[:space:]]*upstream_version="\([0-9][0-9.]*\)".*/\1/p' \
"$root/.github/workflows/fork-release.yml" |
head -n1
)"

failed=0
if [[ "$taken_through" == "$newest" ]]; then
echo "FORK.md: upstream fixes taken through ${taken_through} (rebase base ${recorded_tag:-<none>})"
elif [[ "$recorded_tag" != "$newest" || "$stamped_version" != "$newest_version" ]]; then
if [[ "$recorded_tag" != "$newest" ]]; then
echo "FORK.md records base ${recorded_tag:-<none>}; newest stable tag is ${newest}" >&2
fi
if [[ "$stamped_version" != "$newest_version" ]]; then
echo "fork-release.yml stamps upstream_version=${stamped_version:-<none>}; newest is ${newest_version}" >&2
fi
if [[ -n "$taken_through" ]]; then
echo "FORK.md upstream fixes taken through ${taken_through}, not ${newest}" >&2
fi
failed=1
fi

# A carried-patch row is a table row under "## Carried patches" whose first
# cell is not the header and not the separator.
count="$(
awk '
/^## Carried patches/ { in_table = 1; next }
in_table && /^## / { exit }
in_table && /^\| — \|/ { n++ }
END { print n + 0 }
' "$root/FORK.md"
)"
if (( count > cap )); then
echo "carried-patch count is ${count}, over the cap of ${cap}" >&2
failed=1
fi

if (( failed )); then
echo "drift: rebase onto ${newest} per docs/fork-upstream-sync.md" >&2
exit 1
fi

echo "in sync: ${newest}, ${count} carried patch(es), cap ${cap}"
30 changes: 30 additions & 0 deletions .github/workflows/fork-upstream-drift.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Fork-only drift check (WI-1125). Fails when a newer upstream stable tag has
# shipped than the one recorded in FORK.md and stamped in fork-release.yml, or
# when the carried-patch table grows past its cap. It reports drift; it does
# not rebase. New file with a fork-unique name; no upstream workflow is edited.
name: fork-upstream-drift

# No schedule trigger. GitHub runs schedule only from the repository's default
# branch, which here is main, the upstream mirror. This file lives on fork/main,
# so a cron would never fire. Run it with workflow_dispatch, or from the sync
# runbook at docs/fork-upstream-sync.md.
on:
workflow_dispatch:

permissions:
contents: read

jobs:
drift:
name: upstream tag and carried-patch cap
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.0.1
with:
persist-credentials: false
fetch-depth: 0

- name: Check upstream drift
env:
UPSTREAM_REMOTE: https://github.com/openai/codex.git
run: bash .github/scripts/fork-upstream-drift.sh
30 changes: 24 additions & 6 deletions FORK.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,18 @@ core edits.
committed to it, and it is never the base of a fork pull request.
- `fork/main` is the fork line: an upstream stable tag plus the carried patch
series. It currently sits on `c1382380d`, the commit of upstream tag
`rust-v0.162.0`. The tag lives on a release branch, not on upstream `main`:
`2351d9e1b` (upstream `main` at fork time) is 114 commits away and not
descended from the tag, so it is not a valid base.
`rust-v0.162.0`.
- Upstream fixes taken through: rust-v0.162.1
This line is separate from the rebase base above. It records fixes taken by
cherry-pick, which does not move `fork/main`, because published `fork-<sha>`
tags must stay reachable. The two fix commits are `f715d50688` (daemon
feature compatibility checks only on explicit CLI overrides, #52648) and
`1a1335eefe` (multiline async questions keep line breaks and links, #51866).
The tag commit `092d3acd6b` is not taken: it only bumps the workspace version
to `0.162.1`, and the fork keeps its `0.162.0` stamp. Branch
`fork/wi-1125-upstream-0-162-1`. The tag lives on a release branch, not on
upstream `main`: `2351d9e1b` (upstream `main` at fork time) is 114 commits
away and not descended from the tag, so it is not a valid base.
- All fork work branches off `fork/main` and every fork pull request targets
`fork/main`. Never open, comment on, or push anything to openai/codex.
- Vogt pins `fork/main` commit SHAs, not tags and not `main`.
Expand All @@ -26,8 +35,10 @@ OpenAI repository and carry `GitOrigin-RevId` trailers; leave them intact.

## Rebase procedure

Run this for each new upstream stable tag (`rust-vX.Y.Z`), weekly or whenever
a tag ships:
Run this for each new upstream stable tag (`rust-vX.Y.Z`), whenever a tag
ships. There is no weekly schedule: GitHub runs `schedule` only from the
default branch, which is the upstream mirror, so the drift check is run on
`workflow_dispatch` and from the runbook at `docs/fork-upstream-sync.md`.

1. `git fetch upstream --tags` and check out a fresh branch from the tag.
2. Cherry-pick (or rebase) the carried patch series from `fork/main` onto it,
Expand All @@ -44,6 +55,10 @@ a tag ships:
`fork-<full sha>`, not the bare SHA.

Record the new base tag and any dropped or reworked patch in the table below.
`.github/workflows/fork-upstream-drift.yml` fails when a newer stable tag has
shipped than both the rebase base recorded here and the "Upstream fixes taken
through" marker above, or when the table below passes ten rows. The marker
passing on its own is enough: a cherry-pick does not move the base.

## Carried patches

Expand Down Expand Up @@ -75,4 +90,7 @@ registered yet and so cannot be disabled ahead of time: after the next mirror
sync of `main`, cancel its run and `gh workflow disable postmerge-ci`.
`fork-release` builds x86_64 only, on the self-hosted GCP `rust-heavy` runner,
whose image bakes the musl build packages (WI-1514); only its short publish job
runs GitHub-hosted.
runs GitHub-hosted. `fork-upstream-drift` is the check that the recorded base tag, or the
"Upstream fixes taken through" marker, is still the newest stable tag
(WI-1125). It has no schedule: run it on `workflow_dispatch`, or from the
sync runbook.
11 changes: 11 additions & 0 deletions codex-rs/cli/tests/daemon_startup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,17 @@ async fn daemon_startup(command: &str) -> Result<()> {
ensure!(text.contains("Cannotusethesharedbackgroundserver:Thissessionrequiresapi_key_model_discoverytobedisabled."));
} else {
ensure!(text.contains("Server:Localbackgroundserver") == restart);
if restart {
let settings: serde_json::Value = serde_json::from_slice(
&fs::read(home.path().join("app-server-daemon/settings.json"))?
)?;
let flag = if disabling && !persisted {
"auth_elicitation"
} else {
"api_key_model_discovery"
};
assert_eq!(settings["featureOverrides"], serde_json::json!({(flag): false}));
}
}
} else if command == "start" {
ensure!(text.contains("Server:Localbackgroundserver"));
Expand Down
26 changes: 17 additions & 9 deletions codex-rs/tui/src/bottom_pane/async_questions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,16 +158,24 @@ impl AsyncQuestions {
pub(super) fn wrapped_question_lines(&self, width: u16) -> Vec<HyperlinkLine> {
self.current_question()
.map(|q| {
let source = annotate_web_urls_in_line(q.title.clone().into());
let wrapped = wrap_ranges_trim(&q.title, usize::from(width.max(1)))
.into_iter()
.map(|range| WrappedLine {
line: (&q.title[range.clone()]).into(),
range,
prefix_bytes: 0,
// Line::from removes line endings, so wrap and annotate each logical line
// separately to keep source byte ranges aligned with the visible text.
q.title
.split('\n')
.flat_map(|text| {
let text = text.strip_suffix('\r').unwrap_or(text);
let source = annotate_web_urls_in_line(text.to_owned().into());
let wrapped = wrap_ranges_trim(text, usize::from(width.max(1)))
.into_iter()
.map(|range| WrappedLine {
line: (&text[range.clone()]).into(),
range,
prefix_bytes: 0,
})
.collect();
remap_source_wrapped_line(&source, wrapped)
})
.collect();
remap_source_wrapped_line(&source, wrapped)
.collect()
})
.unwrap_or_default()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
source: tui/src/bottom_pane/async_questions/state_tests.rs
expression: "rendered[0]"
---

1 of 2

Run this command:

```sh
printf '診 断 '
```
Then review (https://example.com/diagnostics?view=full)?


Type your answer

enter submit ⌃] skip ⇧→ main prompt
⇧← next question
38 changes: 38 additions & 0 deletions codex-rs/tui/src/bottom_pane/async_questions/state_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,44 @@ fn wrapped_question_url_keeps_its_complete_destination() {
}
}

#[test]
fn multiline_question_preserves_line_breaks_and_link_destinations() {
let url = "https://example.com/diagnostics?view=full";
let title = format!("Run this command:\n\n```sh\nprintf '診断'\n```\nThen review ({url})?\n");
let mut rendered = Vec::new();
for line_ending in ["\n", "\r\n"] {
let mut editor = editor();
editor.state.pending[0].question.title = title.replace('\n', line_ending);

for width in [20, 60] {
let height = editor.desired_height(width);
let buffer = render_editor(&editor, width, height);
let linked_text = buffer
.content
.iter()
.filter_map(|cell| {
let symbol = cell.symbol();
symbol.contains("\x1b]8;;").then(|| {
let text = crate::terminal_hyperlinks::strip_osc8(symbol);
assert_eq!(
symbol,
crate::terminal_hyperlinks::osc8_hyperlink(url, &text)
);
text
})
})
.collect::<String>();
assert_eq!(linked_text, url);
let text = crate::terminal_hyperlinks::strip_osc8(&buffer_text(&buffer));
if width == 60 {
rendered.push(text);
}
}
}
assert_eq!(rendered[0], rendered[1]);
insta::assert_snapshot!("question_multiline", rendered[0]);
}

#[test]
fn selected_other_renders_as_a_dim_placeholder() {
let mut editor = editor();
Expand Down
13 changes: 11 additions & 2 deletions codex-rs/tui/src/daemon_recovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,15 @@ pub(super) async fn check(
startup: &mut StartupDraft,
target: &AppServerTarget,
config: &Config,
cli_kv_overrides: &[(String, toml::Value)],
managed_daemon: bool,
) -> io::Result<Option<String>> {
let issue = match startup
.run_until(daemon_startup::compatibility_warning(target, config))
.run_until(daemon_startup::compatibility_warning(
target,
config,
cli_kv_overrides,
))
.await?
{
Ok(warning) => return Ok(warning),
Expand Down Expand Up @@ -104,7 +109,11 @@ pub(super) async fn check(
})
.await?;
startup
.run_until(daemon_startup::compatibility_warning(target, config))
.run_until(daemon_startup::compatibility_warning(
target,
config,
cli_kv_overrides,
))
.await?
.map_err(io::Error::other)
}
Expand Down
Loading
Loading