Skip to content

Commit aec020a

Browse files
committed
Stabilize exact CLI release qualification
1 parent 74b1757 commit aec020a

5 files changed

Lines changed: 66 additions & 31 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,16 @@ jobs:
3838
- name: Install Codex CLI
3939
shell: bash
4040
run: |
41-
npm install --no-save @openai/codex
41+
set -euo pipefail
42+
target_version="$(sed -nE 's/^[[:space:]]*public const string TargetVersion = "([0-9]+\.[0-9]+\.[0-9]+)";$/\1/p' CodexSharpSDK/Models/CodexCliCompatibility.cs)"
43+
[[ -n "$target_version" && "$target_version" != *$'\n'* ]] || { echo "Expected exactly one three-part Codex CLI TargetVersion."; exit 1; }
44+
npm install --no-save "@openai/codex@$target_version"
45+
export PATH="$GITHUB_WORKSPACE/node_modules/.bin:$PATH"
46+
version="$(codex --version)"
47+
printf '%s\n' "$version"
48+
actual_version="$(printf '%s\n' "$version" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | sed -n '1p')"
49+
printf 'Installed Codex CLI version: %s\n' "$actual_version"
50+
[[ "$actual_version" == "$target_version" ]]
4251
echo "$GITHUB_WORKSPACE/node_modules/.bin" >> "$GITHUB_PATH"
4352
4453
- name: Restore

‎.github/workflows/codex-cli-watch.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,10 +155,10 @@ jobs:
155155
156156
cli_changed_files="$(printf '%s\n' "$changed_files" | grep -E '^(codex-rs/|docs/|README\.md|CHANGELOG\.md|Cargo\.toml|Cargo\.lock|package\.json)' || true)"
157157
if [ -z "$cli_changed_files" ]; then
158-
cli_changed_files="$(printf '%s\n' "$changed_files" | head -n 200)"
158+
cli_changed_files="$(printf '%s\n' "$changed_files" | sed -n '1,200p')"
159159
fi
160160
161-
commits="$(git -C "$SUBMODULE_PATH" log --no-merges --date=short --pretty=format:'- %h %s (%ad)' "$current_sha..$latest_sha" | head -n 100)"
161+
commits="$(git -C "$SUBMODULE_PATH" log --no-merges --max-count=100 --date=short --pretty=format:'- %h %s (%ad)' "$current_sha..$latest_sha")"
162162
if [ -z "$commits" ]; then
163163
commits='- No commit summary available.'
164164
fi

‎.github/workflows/real-integration.yml‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,18 @@ jobs:
3838
dotnet-version: ${{ env.DOTNET_VERSION }}
3939

4040
- name: Install Codex CLI
41-
run: npm install --no-save @openai/codex
41+
shell: bash
42+
run: |
43+
set -euo pipefail
44+
target_version="$(sed -nE 's/^[[:space:]]*public const string TargetVersion = "([0-9]+\.[0-9]+\.[0-9]+)";$/\1/p' CodexSharpSDK/Models/CodexCliCompatibility.cs)"
45+
[[ -n "$target_version" && "$target_version" != *$'\n'* ]] || { echo "Expected exactly one three-part Codex CLI TargetVersion."; exit 1; }
46+
npm install --no-save "@openai/codex@$target_version"
47+
export PATH="$GITHUB_WORKSPACE/node_modules/.bin:$PATH"
48+
version="$(codex --version)"
49+
printf '%s\n' "$version"
50+
actual_version="$(printf '%s\n' "$version" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | sed -n '1p')"
51+
printf 'Installed Codex CLI version: %s\n' "$actual_version"
52+
[[ "$actual_version" == "$target_version" ]]
4253
4354
- name: Restore
4455
run: dotnet restore ManagedCode.CodexSharpSDK.slnx

‎.github/workflows/release.yml‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,18 @@ jobs:
8787
node-version: ${{ env.NODE_VERSION }}
8888

8989
- name: Install Codex CLI
90+
shell: bash
9091
run: |
91-
npm install --no-save @openai/codex
92+
set -euo pipefail
93+
target_version="$(sed -nE 's/^[[:space:]]*public const string TargetVersion = "([0-9]+\.[0-9]+\.[0-9]+)";$/\1/p' CodexSharpSDK/Models/CodexCliCompatibility.cs)"
94+
[[ -n "$target_version" && "$target_version" != *$'\n'* ]] || { echo "Expected exactly one three-part Codex CLI TargetVersion."; exit 1; }
95+
npm install --no-save "@openai/codex@$target_version"
96+
export PATH="$GITHUB_WORKSPACE/node_modules/.bin:$PATH"
97+
version="$(codex --version)"
98+
printf '%s\n' "$version"
99+
actual_version="$(printf '%s\n' "$version" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | sed -n '1p')"
100+
printf 'Installed Codex CLI version: %s\n' "$actual_version"
101+
[[ "$actual_version" == "$target_version" ]]
92102
echo "$GITHUB_WORKSPACE/node_modules/.bin" >> "$GITHUB_PATH"
93103
94104
- name: Extract version from Directory.Build.props

‎CodexSharpSDK.Tests/Unit/CliInstallationTests.cs‎

Lines changed: 31 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -82,20 +82,27 @@ public sealed class CliInstallationTests
8282
const shim = process.platform === 'win32' ? '@echo off\r\n' : `#!/bin/sh\nexec ${shellQuote(process.execPath)} ${shellQuote(path.join(packageRoot, 'PACKAGE_ENTRY'))} "$@"\n`;
8383
fs.writeFileSync(shimPath, shim);
8484
if (process.platform !== 'win32') fs.chmodSync(shimPath, 0o755);
85+
function writeProcessId(filePath) {
86+
const temporaryPath = filePath + '.tmp';
87+
fs.writeFileSync(temporaryPath, String(process.pid));
88+
fs.renameSync(temporaryPath, filePath);
89+
}
8590
if (fs.existsSync(path.join(__dirname, 'hang'))) {
86-
fs.writeFileSync(path.join(__dirname, 'child.pid'), String(process.pid));
91+
writeProcessId(path.join(__dirname, 'child.pid'));
8792
setInterval(() => {}, 1000);
8893
}
8994
if (fs.existsSync(path.join(__dirname, 'pipe-holder'))) {
90-
const { spawnSync } = require('node:child_process');
91-
const shellCommand = `setsid /bin/sh -c 'echo $$ > ${shellQuote(path.join(__dirname, 'pipe-holder.pid'))}; exec /bin/sleep 60' holder &`;
92-
spawnSync('/bin/sh', ['-c', shellCommand], { stdio: 'inherit' });
95+
const { spawn } = require('node:child_process');
96+
const childScript = 'printf "%s" "$$" > "$1.tmp" && mv "$1.tmp" "$1" && exec /bin/sleep 60';
97+
const holder = spawn('setsid', ['/bin/sh', '-c', childScript, 'holder',
98+
path.join(__dirname, 'pipe-holder.pid')], { detached: true, stdio: 'inherit' });
99+
holder.unref();
93100
}
94101
if (fs.existsSync(path.join(__dirname, 'overflow'))) {
95102
process.stdout.write('x'.repeat(8192));
96103
setInterval(() => {}, 1000);
97104
}
98-
fs.writeFileSync(path.join(__dirname, 'child.pid'), String(process.pid));
105+
writeProcessId(path.join(__dirname, 'child.pid'));
99106
console.log('installation output');
100107
console.error('installation diagnostics');
101108
""";
@@ -176,9 +183,7 @@ public async Task InstallOrUpdate_ReportsFinalOutputWhenConsumerResumesAfterProc
176183
await Assert.That(updates.Current.Stage).IsEqualTo(CliInstallationStage.PackageManagerStarted);
177184
var childPidPath = Path.Combine(fixture.NpmRoot, BinDirectoryName, ChildPidFileName);
178185
using var markerTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(3));
179-
await WaitForFileAsync(childPidPath, markerTimeout.Token);
180-
var childPid = int.Parse(await File.ReadAllTextAsync(childPidPath),
181-
System.Globalization.CultureInfo.InvariantCulture);
186+
var childPid = await WaitForProcessIdAsync(childPidPath, markerTimeout.Token);
182187
await WaitForProcessExitAsync(childPid, markerTimeout.Token);
183188

184189
var sawFinalOutput = false;
@@ -219,9 +224,7 @@ public async Task InstallOrUpdate_EnforcesTimeoutWhileConsumerIsPausedAfterStart
219224
await Assert.That(updates.Current.Stage).IsEqualTo(CliInstallationStage.PackageManagerStarted);
220225

221226
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(5));
222-
await WaitForFileAsync(childPidPath, deadline.Token);
223-
var childPid = int.Parse(await File.ReadAllTextAsync(childPidPath),
224-
System.Globalization.CultureInfo.InvariantCulture);
227+
var childPid = await WaitForProcessIdAsync(childPidPath, deadline.Token);
225228
await WaitForProcessExitAsync(childPid, deadline.Token);
226229
await Assert.That(IsProcessRunning(childPid)).IsFalse();
227230

@@ -266,12 +269,8 @@ await File.WriteAllTextAsync(Path.Combine(fixture.NpmRoot, BinDirectoryName, Pip
266269
fixture.LocalApplicationDataRoot, CancellationToken.None).GetAsyncEnumerator();
267270
await Assert.That(await updates.MoveNextAsync()).IsTrue();
268271
using var markerTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(3));
269-
await WaitForFileAsync(rootPidFile, markerTimeout.Token);
270-
await WaitForFileAsync(holderPidFile, markerTimeout.Token);
271-
var rootPid = int.Parse(await File.ReadAllTextAsync(rootPidFile),
272-
System.Globalization.CultureInfo.InvariantCulture);
273-
holderPid = int.Parse(await File.ReadAllTextAsync(holderPidFile),
274-
System.Globalization.CultureInfo.InvariantCulture);
272+
var rootPid = await WaitForProcessIdAsync(rootPidFile, markerTimeout.Token);
273+
holderPid = await WaitForProcessIdAsync(holderPidFile, markerTimeout.Token);
275274
await WaitForProcessExitAsync(rootPid, markerTimeout.Token);
276275
await Assert.That(IsProcessRunning(rootPid)).IsFalse();
277276
await Assert.That(IsProcessRunning(holderPid)).IsTrue();
@@ -350,7 +349,7 @@ public async Task InstallOrUpdate_CancellationStopsAndJoinsPackageManager()
350349
await Task.Delay(TimeSpan.FromMilliseconds(10), markerTimeout.Token);
351350
}
352351

353-
var childPid = int.Parse(await File.ReadAllTextAsync(childPidPath), System.Globalization.CultureInfo.InvariantCulture);
352+
var childPid = await WaitForProcessIdAsync(childPidPath, markerTimeout.Token);
354353
var competingOptions = fixture.Options with { InstallationLockTimeout = TimeSpan.FromMilliseconds(100) };
355354
var lockException = await Assert.That(async () =>
356355
await RunInstallationAsync(fixture, competingOptions)).ThrowsException();
@@ -403,12 +402,8 @@ public async Task CancellationWithDetachedPipeHolderSurfacesUnconfirmedCleanup()
403402
fixture.LocalApplicationDataRoot, cancellation.Token).GetAsyncEnumerator();
404403
await Assert.That(await updates.MoveNextAsync()).IsTrue();
405404
using var markerTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(3));
406-
await WaitForFileAsync(packageManagerPidFile, markerTimeout.Token);
407-
await WaitForFileAsync(holderPidFile, markerTimeout.Token);
408-
var packageManagerPid = int.Parse(await File.ReadAllTextAsync(packageManagerPidFile),
409-
System.Globalization.CultureInfo.InvariantCulture);
410-
holderProcessId = int.Parse(await File.ReadAllTextAsync(holderPidFile),
411-
System.Globalization.CultureInfo.InvariantCulture);
405+
var packageManagerPid = await WaitForProcessIdAsync(packageManagerPidFile, markerTimeout.Token);
406+
holderProcessId = await WaitForProcessIdAsync(holderPidFile, markerTimeout.Token);
412407
await Assert.That(IsProcessRunning(packageManagerPid)).IsTrue();
413408
await Assert.That(IsProcessRunning(holderProcessId)).IsTrue();
414409

@@ -510,10 +505,20 @@ private static string FindExecutablePath(string executableName)
510505
return Path.GetFullPath(executable ?? throw new InvalidOperationException(SetsidRequiredMessage));
511506
}
512507

513-
private static async Task WaitForFileAsync(string path, CancellationToken cancellationToken)
508+
private static async Task<int> WaitForProcessIdAsync(string path, CancellationToken cancellationToken)
514509
{
515-
while (!File.Exists(path))
510+
while (true)
516511
{
512+
if (File.Exists(path))
513+
{
514+
var contents = await File.ReadAllTextAsync(path, cancellationToken);
515+
if (int.TryParse(contents, System.Globalization.CultureInfo.InvariantCulture, out var processId) &&
516+
processId > 0)
517+
{
518+
return processId;
519+
}
520+
}
521+
517522
await Task.Delay(TimeSpan.FromMilliseconds(10), cancellationToken);
518523
}
519524
}

0 commit comments

Comments
 (0)