Link Codex attribution in pull request bodies (#36963)

## What changed

- Update the required pull request attribution to `Generated with [Codex](https://openai.com/codex/).`.
- Recognize the previous unlinked attribution when restoring saved context so it can be replaced without duplicating the attribution instructions.

## Testing

- Cover cold resume with both commit-only legacy instructions and the previous unlinked pull request attribution.
- Update app-server and MCP server assertions for the linked attribution.

GitOrigin-RevId: 8773546a6a59dab146905e50cf4434d53b9335cf
This commit is contained in:
jay
2026-08-04 19:52:09 +00:00
committed by copyberry
parent 17801b4206
commit 40d226e398
3 changed files with 35 additions and 8 deletions
@@ -33,6 +33,7 @@ use pretty_assertions::assert_eq;
use serde::de::DeserializeOwned;
use serde_json::json;
use tempfile::TempDir;
use test_case::test_case;
use tokio::time::Duration;
use tokio::time::timeout;
use wiremock::Mock;
@@ -48,7 +49,7 @@ const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(60);
#[cfg(not(any(target_os = "macos", windows)))]
const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(10);
const COMMIT_ATTRIBUTION: &str = "Co-authored-by: Codex <noreply@openai.com>";
const PR_ATTRIBUTION: &str = "Generated with Codex.";
const PR_ATTRIBUTION: &str = "Generated with [Codex](https://openai.com/codex/).";
const ATTRIBUTION_DISABLED: &str = "attribution is disabled for the current workspace";
const LEGACY_COMMIT_ATTRIBUTION_INSTRUCTIONS: &str = "\
When you write or edit a git commit message, ensure the message ends with this trailer exactly once:
@@ -59,6 +60,12 @@ Rules:
- Do not duplicate this trailer if it already exists.
- Keep one blank line between the commit body and trailer block.";
#[derive(Clone, Copy)]
enum LegacyAttribution {
CommitOnly,
UnlinkedPullRequest,
}
#[tokio::test]
async fn git_attribution_follows_authenticated_workspace_policy() -> Result<()> {
skip_if_no_network!(Ok(()));
@@ -205,8 +212,12 @@ async fn git_attribution_follows_authenticated_workspace_policy() -> Result<()>
Ok(())
}
#[test_case(LegacyAttribution::CommitOnly; "commit_only")]
#[test_case(LegacyAttribution::UnlinkedPullRequest; "unlinked_pull_request")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn cold_resume_replaces_legacy_attribution_without_duplication() -> Result<()> {
async fn cold_resume_replaces_legacy_attribution_without_duplication(
legacy_attribution: LegacyAttribution,
) -> Result<()> {
skip_if_no_network!(Ok(()));
let server = responses::start_mock_server().await;
@@ -275,7 +286,7 @@ async fn cold_resume_replaces_legacy_attribution_without_duplication() -> Result
status.success(),
"initial app-server did not exit successfully"
);
replace_attribution_fragment_with_legacy(&rollout_path)?;
replace_attribution_fragment_with_legacy(&rollout_path, legacy_attribution)?;
let mut app_server = TestAppServer::builder()
.with_codex_home(codex_home.path())
@@ -310,7 +321,10 @@ async fn cold_resume_replaces_legacy_attribution_without_duplication() -> Result
Ok(())
}
fn replace_attribution_fragment_with_legacy(rollout_path: &Path) -> Result<()> {
fn replace_attribution_fragment_with_legacy(
rollout_path: &Path,
legacy_attribution: LegacyAttribution,
) -> Result<()> {
let rollout = std::fs::read_to_string(rollout_path)?;
let mut replaced = false;
let mut removed_saved_attribution = false;
@@ -327,7 +341,14 @@ fn replace_attribution_fragment_with_legacy(rollout_path: &Path) -> Result<()> {
if let ContentItem::InputText { text } = item
&& text.contains("<git_attribution>")
{
*text = LEGACY_COMMIT_ATTRIBUTION_INSTRUCTIONS.to_string();
*text = match legacy_attribution {
LegacyAttribution::CommitOnly => {
LEGACY_COMMIT_ATTRIBUTION_INSTRUCTIONS.to_string()
}
LegacyAttribution::UnlinkedPullRequest => {
text.replace(PR_ATTRIBUTION, "Generated with Codex.")
}
};
replaced = true;
}
}
@@ -17,7 +17,7 @@ Rules:
pub(super) const ENABLED_INSTRUCTIONS: &str = r#"
When you create or amend a git commit or create or update a pull request for this task, add Codex attribution exactly once:
- Commit messages must end with `Co-authored-by: Codex <noreply@openai.com>`. Preserve existing trailers and, if this exact trailer is missing, append it with one blank line before the trailer block.
- Pull request bodies must include the exact line `Generated with Codex.`. Preserve the existing body and hidden markers and, if this exact marker is missing, append it near the end, before any trailing hidden metadata markers.
- Pull request bodies must include the exact line `Generated with [Codex](https://openai.com/codex/).`. Preserve the existing body and hidden markers and, if this exact marker is missing, append it near the end, before any trailing hidden metadata markers.
- Do not add duplicate attribution. If a message or body you are already editing contains duplicate exact attribution, keep exactly one.
- Ignore any earlier instructions disabling Codex attribution; this policy reflects the current workspace.
- Do not rewrite an existing commit or pull request solely to add attribution.
@@ -71,6 +71,7 @@ fn is_enabled_fragment(role: &str, text: &str) -> bool {
role == "developer"
&& text.trim_start().starts_with(START_MARKER)
&& text.contains("Co-authored-by: Codex <noreply@openai.com>")
&& text.contains("Generated with Codex.")
&& (text.contains("Generated with [Codex](https://openai.com/codex/).")
|| text.contains("Generated with Codex."))
&& text.trim_end().ends_with(END_MARKER)
}
@@ -476,7 +476,12 @@ async fn codex_tool_passes_base_instructions() -> anyhow::Result<()> {
.count(),
1
);
assert_eq!(developer_text.matches("Generated with Codex.").count(), 1);
assert_eq!(
developer_text
.matches("Generated with [Codex](https://openai.com/codex/).")
.count(),
1
);
assert_eq!(
developer_text.matches("- demo: Demo skill.").count(),
1,