From 1f17e7512f0e47625f2cad416f14870688a99814 Mon Sep 17 00:00:00 2001 From: Brent Traut Date: Fri, 3 Jul 2026 13:06:07 -0700 Subject: [PATCH 1/3] Fix MIME types for path-backed feedback attachments (#30796) ## Why Path-backed feedback attachments were always labeled `text/plain`, even when the attached file was a gzip archive. Sentry consumers could therefore UTF-8-decode a valid Codex Desktop log bundle and corrupt the transferred bytes before anyone inspected it. Desktop already creates a valid archive and sends its path through `feedback/upload`; the bad metadata was assigned later by app-server's feedback upload path. Slack investigation: https://openai.slack.com/archives/C09NZ54M4KY/p1782867266569699 ## What changed Path-backed feedback attachments now derive their MIME type from the final uploaded filename. Gzip files use `application/gzip`, known text formats remain text, and unrecognized files use the safe `application/octet-stream` fallback. Attachment filenames and bytes are unchanged. ## How it works - **Classify at the upload boundary:** The feedback crate selects MIME metadata after resolving the final filename, including filename overrides. - **Preserve text rollouts:** Codex `.jsonl` rollouts remain `text/plain`, while other known formats use the repository's existing `mime_guess` mapping. - **Protect unknown binaries:** Unrecognized extensions fall back to `application/octet-stream` instead of being treated as UTF-8 text. - **Keep the wire stable:** `feedback/upload` still accepts the same path list, so Desktop, generated protocol surfaces, and remote-host minimums do not change. ## Verification Added focused coverage for gzip MIME, unknown binary fallback, `.jsonl` text handling, and exact filename/byte preservation. Ran the complete `codex-feedback` test suite (9 tests), crate-scoped Clippy, Rust formatting, Bazel lock refresh, and diff checks successfully. --- codex-rs/Cargo.lock | 1 + codex-rs/feedback/Cargo.toml | 1 + codex-rs/feedback/src/lib.rs | 75 +++++++++++++++++++++++++++++++++++- 3 files changed, 76 insertions(+), 1 deletion(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 3561550db5..6ac51d403f 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -3077,6 +3077,7 @@ dependencies = [ "anyhow", "codex-login", "codex-protocol", + "mime_guess", "pretty_assertions", "sentry", "tracing", diff --git a/codex-rs/feedback/Cargo.toml b/codex-rs/feedback/Cargo.toml index 032f0398be..dd89ddb76b 100644 --- a/codex-rs/feedback/Cargo.toml +++ b/codex-rs/feedback/Cargo.toml @@ -11,6 +11,7 @@ workspace = true anyhow = { workspace = true } codex-login = { workspace = true } codex-protocol = { workspace = true } +mime_guess = { workspace = true } sentry = { version = "0.46" } tracing = { workspace = true } tracing-subscriber = { workspace = true } diff --git a/codex-rs/feedback/src/lib.rs b/codex-rs/feedback/src/lib.rs index 7c27d2b3b0..cd97349c48 100644 --- a/codex-rs/feedback/src/lib.rs +++ b/codex-rs/feedback/src/lib.rs @@ -4,6 +4,7 @@ use std::collections::btree_map::Entry; use std::fs; use std::io::Write; use std::io::{self}; +use std::path::Path; use std::path::PathBuf; use std::sync::Arc; use std::sync::Mutex; @@ -593,10 +594,22 @@ impl FeedbackSnapshot { .map(|s| s.to_string_lossy().to_string()) .unwrap_or_else(|| "extra-log.log".to_string()) }); + let content_type = match Path::new(&filename) + .extension() + .and_then(|extension| extension.to_str()) + { + Some(extension) if extension.eq_ignore_ascii_case("jsonl") => { + "text/plain".to_string() + } + _ => mime_guess::from_path(&filename) + .first_or_octet_stream() + .essence_str() + .to_string(), + }; attachments.push(Attachment { buffer: data, filename, - content_type: Some("text/plain".to_string()), + content_type: Some(content_type), ty: None, }); } @@ -774,6 +787,10 @@ mod tests { b"Connectivity diagnostics\n\n- Proxy environment variables are set and may affect connectivity.\n - HTTPS_PROXY = https://example.com:443".to_vec() ); assert_eq!(attachments_with_diagnostics[3].buffer, b"rollout".to_vec()); + assert_eq!( + attachments_with_diagnostics[3].content_type.as_deref(), + Some("text/plain") + ); assert_eq!( OsStr::new(attachments_with_diagnostics[3].filename.as_str()), OsStr::new(extra_filename.as_str()) @@ -794,6 +811,62 @@ mod tests { fs::remove_file(extra_path).expect("extra attachment should be removed"); } + #[test] + fn path_backed_attachments_use_binary_content_types() { + let suffix = ThreadId::new(); + let gzip_filename = format!("codex-desktop-app-logs-{suffix}.tar.gz"); + let unknown_filename = format!("codex-feedback-extra-{suffix}.binunknown"); + let gzip_path = std::env::temp_dir().join(&gzip_filename); + let unknown_path = std::env::temp_dir().join(&unknown_filename); + let gzip_bytes = b"\x1f\x8b\x08\x00\xff"; + let unknown_bytes = b"\x00\x9f\x92\x96"; + fs::write(&gzip_path, gzip_bytes).expect("gzip attachment should be written"); + fs::write(&unknown_path, unknown_bytes).expect("unknown attachment should be written"); + + let attachments = CodexFeedback::new() + .snapshot(/*session_id*/ None) + .feedback_attachments( + /*include_logs*/ false, + &[], + &[ + FeedbackAttachmentPath { + path: gzip_path.clone(), + attachment_filename_override: None, + }, + FeedbackAttachmentPath { + path: unknown_path.clone(), + attachment_filename_override: None, + }, + ], + /*logs_override*/ None, + ); + + fs::remove_file(gzip_path).expect("gzip attachment should be removed"); + fs::remove_file(unknown_path).expect("unknown attachment should be removed"); + assert_eq!( + attachments + .iter() + .map(|attachment| ( + attachment.filename.as_str(), + attachment.content_type.as_deref(), + attachment.buffer.as_slice(), + )) + .collect::>(), + vec![ + ( + gzip_filename.as_str(), + Some("application/gzip"), + gzip_bytes.as_slice(), + ), + ( + unknown_filename.as_str(), + Some("application/octet-stream"), + unknown_bytes.as_slice(), + ), + ] + ); + } + #[test] fn upload_tags_include_client_tags_and_preserve_reserved_fields() { let mut tags = BTreeMap::new(); From 319d03056e9b345fe9d129873c3a808c5df783df Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Fri, 3 Jul 2026 15:11:19 -0700 Subject: [PATCH 2/3] fix(install): reuse GitHub release metadata (#31056) ## Why The standalone installers currently perform separate unauthenticated GitHub REST API lookups while resolving the latest version, locating the platform package, locating its checksum manifest, and retrieving asset digests. A single install can therefore make up to four release-metadata requests. When GitHub's shared unauthenticated rate limit is exhausted, valid releases fail to install. The shell installer also suppresses the metadata request failure while probing assets, so a `403` is misreported as though the release assets do not exist. This makes the failure both more likely and harder to diagnose. Fixes #28538. ## What changed - Resolve the selected version and fetch its release metadata together. - Reuse that one metadata response for package, checksum, and legacy-package selection in both `install.sh` and `install.ps1`. - Report metadata fetch failures as possible GitHub availability or rate-limit failures instead of missing assets. - Add a mocked-`curl` regression suite covering exact releases, `latest`, and a simulated metadata `403`, and run it in `repo-checks`. For `latest`, the metadata returned by `/releases/latest` now supplies both the resolved version and the asset list. For an explicitly selected version, the installer makes one request to that release's tag endpoint. ## Verification - `python3 -m unittest discover -s scripts/install -p 'test_*.py' -v` - `sh -n scripts/install/install.sh` - Parsed `scripts/install/install.ps1` with the PowerShell language parser. ## Scope This change reduces GitHub API usage and preserves the underlying error, but it does not move release artifacts away from GitHub's CDN. --- .github/workflows/repo-checks.yml | 3 + scripts/install/install.ps1 | 65 ++++++------ scripts/install/install.sh | 69 +++++++------ scripts/install/test_install_sh.py | 159 +++++++++++++++++++++++++++++ 4 files changed, 232 insertions(+), 64 deletions(-) create mode 100644 scripts/install/test_install_sh.py diff --git a/.github/workflows/repo-checks.yml b/.github/workflows/repo-checks.yml index 3a76a202ea..3d0504172d 100644 --- a/.github/workflows/repo-checks.yml +++ b/.github/workflows/repo-checks.yml @@ -28,6 +28,9 @@ jobs: - name: Test Codex package builder run: python3 -m unittest discover -s scripts/codex_package -p 'test_*.py' + - name: Test standalone installer + run: python3 -m unittest discover -s scripts/install -p 'test_*.py' + - name: Setup pnpm uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5 with: diff --git a/scripts/install/install.ps1 b/scripts/install/install.ps1 index 6973d482e2..986f6c6566 100644 --- a/scripts/install/install.ps1 +++ b/scripts/install/install.ps1 @@ -79,11 +79,10 @@ function Assert-ValidReleaseVersion { function Find-ReleaseAssetMetadata { param( [string]$AssetName, - [string]$ResolvedVersion + [object]$ReleaseMetadata ) - $release = Invoke-RestMethod -Uri "https://api.github.com/repos/openai/codex/releases/tags/rust-v$ResolvedVersion" - $asset = $release.assets | Where-Object { $_.name -eq $AssetName } | Select-Object -First 1 + $asset = $ReleaseMetadata.assets | Where-Object { $_.name -eq $AssetName } | Select-Object -First 1 if ($null -eq $asset) { return $null } @@ -99,20 +98,6 @@ function Find-ReleaseAssetMetadata { } } -function Get-ReleaseAssetMetadata { - param( - [string]$AssetName, - [string]$ResolvedVersion - ) - - $metadata = Find-ReleaseAssetMetadata -AssetName $AssetName -ResolvedVersion $ResolvedVersion - if ($null -eq $metadata) { - throw "Could not find release asset $AssetName for Codex $ResolvedVersion." - } - - return $metadata -} - function Test-ArchiveDigest { param( [string]$ArchivePath, @@ -216,22 +201,38 @@ function Remove-StaleInstallArtifacts { } } -function Resolve-Version { +function Resolve-Release { $normalizedVersion = Normalize-Version -RawVersion $Release Assert-ValidReleaseVersion -Version $normalizedVersion - if ($normalizedVersion -ne "latest") { - return $normalizedVersion + + if ($normalizedVersion -eq "latest") { + $requestedRelease = "latest" + $metadataUri = "https://api.github.com/repos/openai/codex/releases/latest" + } else { + $resolvedVersion = $normalizedVersion + $requestedRelease = $resolvedVersion + $metadataUri = "https://api.github.com/repos/openai/codex/releases/tags/rust-v$resolvedVersion" } - $release = Invoke-RestMethod -Uri "https://api.github.com/repos/openai/codex/releases/latest" - if (-not $release.tag_name) { - Write-Error "Failed to resolve the latest Codex release version." - exit 1 + try { + $releaseMetadata = Invoke-RestMethod -Uri $metadataUri + } catch { + throw "Could not fetch GitHub release metadata for Codex $requestedRelease. GitHub API may be unavailable or rate limited. $($_.Exception.Message)" } - $resolvedVersion = Normalize-Version -RawVersion $release.tag_name - Assert-ValidReleaseVersion -Version $resolvedVersion - return $resolvedVersion + if ($normalizedVersion -eq "latest") { + if (-not $releaseMetadata.tag_name) { + throw "Failed to resolve the latest Codex release version." + } + + $resolvedVersion = Normalize-Version -RawVersion $releaseMetadata.tag_name + Assert-ValidReleaseVersion -Version $resolvedVersion + } + + return [PSCustomObject]@{ + Version = $resolvedVersion + Metadata = $releaseMetadata + } } function Get-VersionFromBinary { @@ -746,7 +747,9 @@ if ([string]::IsNullOrWhiteSpace($env:CODEX_INSTALL_DIR)) { } $currentVersion = Get-CurrentInstalledVersion -StandaloneCurrentDir $currentDir -$resolvedVersion = Resolve-Version +$resolvedRelease = Resolve-Release +$resolvedVersion = $resolvedRelease.Version +$releaseMetadata = $resolvedRelease.Metadata $releaseName = "$resolvedVersion-$target" $releaseDir = Join-Path $releasesDir $releaseName @@ -765,12 +768,12 @@ $oldStandaloneBackup = $null $packageAsset = "codex-package-$target.tar.gz" $checksumAsset = "codex-package_SHA256SUMS" -$packageMetadata = Find-ReleaseAssetMetadata -AssetName $packageAsset -ResolvedVersion $resolvedVersion -$checksumMetadata = Find-ReleaseAssetMetadata -AssetName $checksumAsset -ResolvedVersion $resolvedVersion +$packageMetadata = Find-ReleaseAssetMetadata -AssetName $packageAsset -ReleaseMetadata $releaseMetadata +$checksumMetadata = Find-ReleaseAssetMetadata -AssetName $checksumAsset -ReleaseMetadata $releaseMetadata $installLayout = "Package" if ($null -eq $packageMetadata -or $null -eq $checksumMetadata) { $packageAsset = "codex-npm-$npmTag-$resolvedVersion.tgz" - $packageMetadata = Find-ReleaseAssetMetadata -AssetName $packageAsset -ResolvedVersion $resolvedVersion + $packageMetadata = Find-ReleaseAssetMetadata -AssetName $packageAsset -ReleaseMetadata $releaseMetadata if ($null -ne $packageMetadata) { $installLayout = "LegacyPlatformNpm" } else { diff --git a/scripts/install/install.sh b/scripts/install/install.sh index 7513efc429..d44b353860 100755 --- a/scripts/install/install.sh +++ b/scripts/install/install.sh @@ -138,10 +138,36 @@ release_metadata_url() { printf 'https://api.github.com/repos/openai/codex/releases/tags/rust-v%s\n' "$resolved_version" } +resolve_release() { + normalized_version="$(normalize_version "$RELEASE")" + validate_version "$normalized_version" + + if [ "$normalized_version" = "latest" ]; then + requested_release="latest" + metadata_url="https://api.github.com/repos/openai/codex/releases/latest" + else + resolved_version="$normalized_version" + requested_release="$resolved_version" + metadata_url="$(release_metadata_url "$resolved_version")" + fi + + if ! release_json="$(download_text "$metadata_url")"; then + echo "Could not fetch GitHub release metadata for Codex $requested_release. GitHub API may be unavailable or rate limited." >&2 + exit 1 + fi + + if [ "$normalized_version" = "latest" ]; then + resolved_version="$(printf '%s\n' "$release_json" | sed -n 's/.*"tag_name":[[:space:]]*"rust-v\([^"]*\)".*/\1/p' | head -n 1)" + if [ -z "$resolved_version" ]; then + echo "Failed to resolve the latest Codex release version." >&2 + exit 1 + fi + validate_version "$resolved_version" + fi +} + release_asset_digest_or_empty() { asset="$1" - resolved_version="$2" - release_json="$(download_text "$(release_metadata_url "$resolved_version")")" digest="$(printf '%s\n' "$release_json" | awk -v asset="$asset" ' /"name":[[:space:]]*"[^"]+"/ { @@ -190,16 +216,14 @@ release_asset_digest_or_empty() { release_asset_exists() { asset="$1" - resolved_version="$2" - release_asset_digest_or_empty "$asset" "$resolved_version" >/dev/null 2>&1 + release_asset_digest_or_empty "$asset" >/dev/null 2>&1 } release_asset_digest() { asset="$1" - resolved_version="$2" - digest="$(release_asset_digest_or_empty "$asset" "$resolved_version" || true)" + digest="$(release_asset_digest_or_empty "$asset" || true)" if [ -z "$digest" ]; then echo "Could not find SHA-256 digest for release asset $asset." >&2 exit 1 @@ -275,27 +299,6 @@ require_command() { fi } -resolve_version() { - normalized_version="$(normalize_version "$RELEASE")" - validate_version "$normalized_version" - - if [ "$normalized_version" != "latest" ]; then - printf '%s\n' "$normalized_version" - return - fi - - release_json="$(download_text "https://api.github.com/repos/openai/codex/releases/latest")" - resolved="$(printf '%s\n' "$release_json" | sed -n 's/.*"tag_name":[[:space:]]*"rust-v\([^"]*\)".*/\1/p' | head -n 1)" - - if [ -z "$resolved" ]; then - echo "Failed to resolve the latest Codex release version." >&2 - exit 1 - fi - - validate_version "$resolved" - printf '%s\n' "$resolved" -} - pick_profile() { # Use the same shell-specific split Homebrew documents because there is no # universal startup file across macOS/Linux login and interactive shells. @@ -839,14 +842,14 @@ else fi fi -resolved_version="$(resolve_version)" +resolve_release package_asset="codex-package-$vendor_target.tar.gz" checksum_asset="codex-package_SHA256SUMS" -if release_asset_exists "$package_asset" "$resolved_version" && - release_asset_exists "$checksum_asset" "$resolved_version"; then +if release_asset_exists "$package_asset" && + release_asset_exists "$checksum_asset"; then install_layout="package" asset="$package_asset" -elif release_asset_exists "codex-npm-$npm_tag-$resolved_version.tgz" "$resolved_version"; then +elif release_asset_exists "codex-npm-$npm_tag-$resolved_version.tgz"; then install_layout="legacy-platform-npm" asset="codex-npm-$npm_tag-$resolved_version.tgz" else @@ -893,12 +896,12 @@ if ! release_dir_is_complete "$release_dir" "$resolved_version" "$vendor_target" step "Downloading Codex CLI" if [ "$install_layout" = "package" ]; then - checksum_digest="$(release_asset_digest "$checksum_asset" "$resolved_version")" + checksum_digest="$(release_asset_digest "$checksum_asset")" download_file "$checksum_url" "$checksum_path" verify_archive_digest "$checksum_path" "$checksum_digest" expected_digest="$(package_archive_digest "$asset" "$checksum_path")" else - expected_digest="$(release_asset_digest "$asset" "$resolved_version")" + expected_digest="$(release_asset_digest "$asset")" fi download_file "$download_url" "$archive_path" verify_archive_digest "$archive_path" "$expected_digest" diff --git a/scripts/install/test_install_sh.py b/scripts/install/test_install_sh.py new file mode 100644 index 0000000000..556c6213bb --- /dev/null +++ b/scripts/install/test_install_sh.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 + +import json +import os +from pathlib import Path +import subprocess +import tempfile +import textwrap +import unittest + + +INSTALL_SCRIPT = Path(__file__).with_name("install.sh") +VERSION = "0.142.5" + + +class InstallShTest(unittest.TestCase): + def test_metadata_fetch_failure_is_not_reported_as_missing_assets(self) -> None: + result, requests = run_installer(VERSION, metadata_failure=True) + + self.assertNotEqual(result.returncode, 0) + self.assertEqual( + requests, + [ + "https://api.github.com/repos/openai/codex/releases/tags/" + f"rust-v{VERSION}" + ], + ) + self.assertIn( + f"Could not fetch GitHub release metadata for Codex {VERSION}", + result.stderr, + ) + self.assertNotIn("Could not find Codex package", result.stderr) + + def test_exact_release_fetches_metadata_once(self) -> None: + result, requests = run_installer(VERSION) + + self.assertNotEqual(result.returncode, 0) + self.assertEqual( + requests, + [ + "https://api.github.com/repos/openai/codex/releases/tags/" + f"rust-v{VERSION}", + "https://github.com/openai/codex/releases/download/" + f"rust-v{VERSION}/codex-package_SHA256SUMS", + ], + ) + self.assertIn(f"Resolved version: {VERSION}", result.stdout) + + def test_latest_release_reuses_version_metadata(self) -> None: + result, requests = run_installer("latest") + + self.assertNotEqual(result.returncode, 0) + self.assertEqual( + requests, + [ + "https://api.github.com/repos/openai/codex/releases/latest", + "https://github.com/openai/codex/releases/download/" + f"rust-v{VERSION}/codex-package_SHA256SUMS", + ], + ) + self.assertIn(f"Resolved version: {VERSION}", result.stdout) + + +def run_installer( + release: str, *, metadata_failure: bool = False +) -> tuple[subprocess.CompletedProcess[str], list[str]]: + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + bin_dir = root / "bin" + bin_dir.mkdir() + request_log = root / "requests.log" + fake_curl = bin_dir / "curl" + fake_curl.write_text( + textwrap.dedent( + """\ + #!/bin/sh + url="" + for arg in "$@"; do + case "$arg" in + https://*) url="$arg" ;; + esac + done + printf '%s\n' "$url" >>"$CODEX_TEST_REQUEST_LOG" + + case "$url" in + https://api.github.com/*) + if [ "$CODEX_TEST_METADATA_FAILURE" = "1" ]; then + echo "curl: (22) The requested URL returned error: 403" >&2 + exit 22 + fi + printf '%s\n' "$CODEX_TEST_METADATA_JSON" + ;; + *) + exit 22 + ;; + esac + """ + ), + encoding="utf-8", + ) + fake_curl.chmod(0o755) + + env = os.environ.copy() + env.update( + { + "CODEX_HOME": str(root / "codex-home"), + "CODEX_INSTALL_DIR": str(root / "install-bin"), + "CODEX_NON_INTERACTIVE": "1", + "CODEX_RELEASE": release, + "CODEX_TEST_METADATA_FAILURE": "1" if metadata_failure else "0", + "CODEX_TEST_METADATA_JSON": release_metadata(), + "CODEX_TEST_REQUEST_LOG": str(request_log), + "HOME": str(root / "home"), + "PATH": f"{bin_dir}:/usr/bin:/bin", + "SHELL": "/bin/sh", + } + ) + result = subprocess.run( + ["/bin/sh", str(INSTALL_SCRIPT)], + capture_output=True, + check=False, + env=env, + text=True, + ) + requests = ( + request_log.read_text(encoding="utf-8").splitlines() + if request_log.exists() + else [] + ) + return result, requests + + +def release_metadata() -> str: + assets = [ + { + "name": f"codex-package-{target}.tar.gz", + "digest": f"sha256:{'a' * 64}", + } + for target in ( + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "aarch64-unknown-linux-musl", + "x86_64-unknown-linux-musl", + ) + ] + assets.append( + { + "name": "codex-package_SHA256SUMS", + "digest": f"sha256:{'b' * 64}", + } + ) + return json.dumps( + {"tag_name": f"rust-v{VERSION}", "assets": assets}, + indent=2, + ) + + +if __name__ == "__main__": + unittest.main() From 6f67118dfdf04d7a0cb50c65cc9cabe354880209 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Fri, 3 Jul 2026 17:13:46 -0700 Subject: [PATCH 3/3] chore: remove unused git-cliff configuration --- cliff.toml | 46 ---------------------------------------------- 1 file changed, 46 deletions(-) delete mode 100644 cliff.toml diff --git a/cliff.toml b/cliff.toml deleted file mode 100644 index f31e1bd89c..0000000000 --- a/cliff.toml +++ /dev/null @@ -1,46 +0,0 @@ -# https://git-cliff.org/docs/configuration - -[changelog] -header = """ -# Changelog - -You can install any of these versions: `npm install -g @openai/codex@` -""" - -body = """ -{% if version -%} -## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} -{%- else %} -## [unreleased] -{% endif %} - -{%- for group, commits in commits | group_by(attribute="group") %} -### {{ group | striptags | trim }} - -{% for commit in commits %}- {% if commit.scope %}*({{ commit.scope }})* {% endif %}{% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }} -{% endfor %} - -{%- endfor -%} -""" - -footer = """ - -""" - -trim = true -postprocessors = [] - -[git] -conventional_commits = true - -commit_parsers = [ - { message = "^feat", group = "🚀 Features" }, - { message = "^fix", group = "🪲 Bug Fixes" }, - { message = "^bump", group = "🛳️ Release" }, - # Fallback – skip anything that didn't match the above rules. - { message = ".*", group = "💼 Other" }, -] - -filter_unconventional = false -sort_commits = "oldest" -topo_order = false \ No newline at end of file