diff --git a/.github/workflows/codex-version-watch.yml b/.github/workflows/codex-version-watch.yml index 1f2fc64..3c3e601 100644 --- a/.github/workflows/codex-version-watch.yml +++ b/.github/workflows/codex-version-watch.yml @@ -134,7 +134,7 @@ jobs: git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git checkout -B "$branch" origin/main node scripts/update-codex-candidate-manifest.js "$CANDIDATE_VERSION" "$candidate_state" - git add config/codex-termux-release-manifest.json + git add config/codex-termux-release-manifest.json packages/codex-termux/config/codex-termux-release-manifest.json git commit -m "Track Codex ${CANDIDATE_VERSION} candidate" git push --force-with-lease origin "$branch" echo "branch=$branch" >> "$GITHUB_ENV" diff --git a/.github/workflows/promote-and-publish.yml b/.github/workflows/promote-and-publish.yml index 0e2dad2..5f50a7e 100644 --- a/.github/workflows/promote-and-publish.yml +++ b/.github/workflows/promote-and-publish.yml @@ -59,6 +59,18 @@ jobs: - name: Publish guard check run: node scripts/check-public-publish-guard.js --skip-runtime-files + - name: Verify previous_stable_version matches current npm latest + if: (inputs.npm_tag || 'latest') == 'latest' + run: | + set -eu + CURRENT_LATEST=$(npm view "@bash0816/codex-termux" dist-tags.latest 2>/dev/null || echo "") + PREV=$(node -p "JSON.parse(require('fs').readFileSync('config/codex-termux-release-manifest.json','utf8')).previous_stable_version") + if [ "$CURRENT_LATEST" != "$PREV" ]; then + echo "ERROR: previous_stable_version ($PREV) does not match npm registry latest ($CURRENT_LATEST). Fix config/codex-termux-release-manifest.json before promoting." >&2 + exit 1 + fi + echo "previous_stable_version ($PREV) matches npm latest ($CURRENT_LATEST), OK" + - name: Promote dist-tag env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/config/codex-termux-release-manifest.json b/config/codex-termux-release-manifest.json index ff23883..e6449c0 100644 --- a/config/codex-termux-release-manifest.json +++ b/config/codex-termux-release-manifest.json @@ -5,7 +5,7 @@ "support_status": "active", "latest_audited_version": "0.144.4", "latest_candidate_version": "0.144.5", - "previous_stable_version": "0.144.3", + "previous_stable_version": "0.144.4", "candidate_state": "published", "candidate_state_status": "ready_to_publish", "audit_evidence_ref": "", diff --git a/packages/codex-termux/config/codex-termux-release-manifest.json b/packages/codex-termux/config/codex-termux-release-manifest.json index cfca08d..68a40c5 100644 --- a/packages/codex-termux/config/codex-termux-release-manifest.json +++ b/packages/codex-termux/config/codex-termux-release-manifest.json @@ -4,8 +4,8 @@ "public_distribution_status": "published", "support_status": "active", "latest_audited_version": "0.144.4", - "latest_candidate_version": "0.144.4", - "previous_stable_version": "0.144.3", + "latest_candidate_version": "0.144.5", + "previous_stable_version": "0.144.4", "candidate_state": "published", "candidate_state_status": "ready_to_publish", "tracked_versions": [ diff --git a/scripts/update-codex-candidate-manifest.js b/scripts/update-codex-candidate-manifest.js index c97b194..2d429ce 100644 --- a/scripts/update-codex-candidate-manifest.js +++ b/scripts/update-codex-candidate-manifest.js @@ -1,25 +1,80 @@ const fs = require('fs'); const path = require('path'); -const repoRoot = path.resolve(__dirname, '..'); -const manifestPath = path.join(repoRoot, 'config', 'codex-termux-release-manifest.json'); +/** + * Update both root and package manifests with candidate version and state. + * Ensures updated_at timestamp is identical in both files. + * + * @param {Object} opts + * @param {string} opts.rootPath - path to root manifest + * @param {string} opts.packagePath - path to package manifest + * @param {string} opts.candidateVersion - new latest_candidate_version + * @param {string} opts.candidateState - new candidate_state_status + */ +function updateManifests({ + rootPath, + packagePath, + candidateVersion, + candidateState, +}) { + if (!rootPath || !packagePath || !candidateVersion || !candidateState) { + throw new Error('All parameters (rootPath, packagePath, candidateVersion, candidateState) are required'); + } -function usage() { - console.error('usage: node scripts/update-codex-candidate-manifest.js '); - process.exit(2); -} + // Generate timestamp once, use for both files + const updatedAt = new Date().toISOString(); -const candidateVersion = process.argv[2]; -const candidateState = process.argv[3]; + // Update root manifest + const rootManifest = JSON.parse(fs.readFileSync(rootPath, 'utf8')); + rootManifest.latest_candidate_version = candidateVersion; + rootManifest.candidate_state_status = candidateState; + rootManifest.updated_at = updatedAt; + fs.writeFileSync(rootPath, `${JSON.stringify(rootManifest, null, 2)}\n`); -if (!candidateVersion || !candidateState) { - usage(); + // Update package manifest + // Read existing package manifest to preserve distribution-specific fields + const packageManifest = JSON.parse(fs.readFileSync(packagePath, 'utf8')); + packageManifest.latest_candidate_version = candidateVersion; + packageManifest.candidate_state_status = candidateState; + packageManifest.updated_at = updatedAt; + fs.writeFileSync(packagePath, `${JSON.stringify(packageManifest, null, 2)}\n`); } -const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); -manifest.latest_candidate_version = candidateVersion; -manifest.candidate_state_status = candidateState; -manifest.updated_at = new Date().toISOString(); +// CLI interface +if (require.main === module) { + const repoRoot = path.resolve(__dirname, '..'); + const rootManifestPath = path.join(repoRoot, 'config', 'codex-termux-release-manifest.json'); + const packageManifestPath = path.join( + repoRoot, + 'packages', + 'codex-termux', + 'config', + 'codex-termux-release-manifest.json' + ); + + function usage() { + console.error('usage: node scripts/update-codex-candidate-manifest.js '); + process.exit(2); + } + + const candidateVersion = process.argv[2]; + const candidateState = process.argv[3]; -fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`); + if (!candidateVersion || !candidateState) { + usage(); + } + + try { + updateManifests({ + rootPath: rootManifestPath, + packagePath: packageManifestPath, + candidateVersion, + candidateState, + }); + } catch (err) { + console.error('Error updating manifests:', err.message); + process.exit(1); + } +} +module.exports = { updateManifests }; diff --git a/scripts/update-codex-candidate-manifest.test.js b/scripts/update-codex-candidate-manifest.test.js new file mode 100644 index 0000000..bcbc3cf --- /dev/null +++ b/scripts/update-codex-candidate-manifest.test.js @@ -0,0 +1,258 @@ +const { test } = require('node:test'); +const { strict: assert } = require('assert'); +const fs = require('fs'); +const path = require('path'); +const { updateManifests } = require('./update-codex-candidate-manifest.js'); + +// Test helper to create a manifest with defaults +function createManifest(overrides = {}) { + return { + canonical_package_name: '@bash0816/codex-termux', + canonical_package_status: 'published', + public_distribution_status: 'published', + support_status: 'active', + latest_audited_version: '0.144.4', + latest_candidate_version: '0.144.4', + previous_stable_version: '0.144.3', + candidate_state: 'published', + candidate_state_status: 'ready_to_publish', + tracked_versions: ['0.141.0', '0.144.4'], + historical_baselines: ['0.131.0'], + gate_summary: '0.144.4 published as @latest.', + install_command: 'npm install -g @bash0816/codex-termux@latest', + update_strategy: 'npm_reinstall_audited_package', + update_command: 'codex update', + update_guidance: 'Run codex update to install the latest audited package version.', + manifest_url: 'https://raw.githubusercontent.com/bash0816/Codex-Termux/main/config/codex-termux-release-manifest.json', + rollback_version: '0.144.1', + rollback_policy: 'Reinstall an explicit published version with npm to roll back.', + distribution_strategy: 'public-npm-package', + audited_at: '2026-07-01T00:00:00+09:00', + updated_at: '2026-07-15T12:00:47.575Z', + platform: 'android', + arch: 'arm64', + planned_wrapper_package: { + name: '@bash0816/codex-termux', + status: 'published', + scope: 'wrapper_package', + }, + ...overrides, + }; +} + +// Test helper to create a package-specific manifest +function createPackageManifest(overrides = {}) { + return { + canonical_package_name: '@bash0816/codex-termux', + canonical_package_status: 'published', + public_distribution_status: 'published', + support_status: 'active', + latest_audited_version: '0.144.4', + latest_candidate_version: '0.144.4', + previous_stable_version: '0.144.3', + candidate_state: 'published', + candidate_state_status: 'ready_to_publish', + tracked_versions: ['0.141.0', '0.144.4'], + historical_baselines: ['0.131.0'], + gate_summary: '0.144.4 published as @latest.', + rollback_version: '0.144.1', + install_command: 'npm install -g @bash0816/codex-termux@latest', + update_strategy: 'npm_reinstall_audited_package', + manifest_url: 'https://raw.githubusercontent.com/bash0816/Codex-Termux/main/config/codex-termux-release-manifest.json', + rollback_policy: 'Reinstall an explicit published version with npm to roll back.', + distribution_strategy: 'public-npm-package', + audited_at: '2026-07-01T00:00:00+09:00', + updated_at: '2026-07-15T12:00:47.575Z', + platform: 'android', + arch: 'arm64', + planned_wrapper_package: { + name: '@bash0816/codex-termux', + status: 'published', + scope: 'wrapper_package', + }, + // Package-specific fields + update_command: 'npm install -g @bash0816/codex-termux@latest', + update_guidance: 'Use npm to reinstall the latest published package. Do not install the upstream @openai/codex package directly on Termux.', + ...overrides, + }; +} + +test('updateManifests: updates latest_candidate_version in root manifest', () => { + const tmpDir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'test-')); + try { + const rootPath = path.join(tmpDir, 'root-manifest.json'); + const packagePath = path.join(tmpDir, 'package-manifest.json'); + + fs.writeFileSync(rootPath, JSON.stringify(createManifest(), null, 2) + '\n'); + fs.writeFileSync(packagePath, JSON.stringify(createPackageManifest(), null, 2) + '\n'); + + updateManifests({ + rootPath, + packagePath, + candidateVersion: '0.144.5', + candidateState: 'ready_to_publish', + }); + + const rootManifest = JSON.parse(fs.readFileSync(rootPath, 'utf8')); + assert.equal(rootManifest.latest_candidate_version, '0.144.5'); + } finally { + fs.rmSync(tmpDir, { recursive: true }); + } +}); + +test('updateManifests: updates candidate_state_status in root manifest', () => { + const tmpDir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'test-')); + try { + const rootPath = path.join(tmpDir, 'root-manifest.json'); + const packagePath = path.join(tmpDir, 'package-manifest.json'); + + fs.writeFileSync(rootPath, JSON.stringify(createManifest(), null, 2) + '\n'); + fs.writeFileSync(packagePath, JSON.stringify(createPackageManifest(), null, 2) + '\n'); + + updateManifests({ + rootPath, + packagePath, + candidateVersion: '0.144.5', + candidateState: 'codex_build_dispatched', + }); + + const rootManifest = JSON.parse(fs.readFileSync(rootPath, 'utf8')); + assert.equal(rootManifest.candidate_state_status, 'codex_build_dispatched'); + } finally { + fs.rmSync(tmpDir, { recursive: true }); + } +}); + +test('updateManifests: updates latest_candidate_version in package manifest', () => { + const tmpDir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'test-')); + try { + const rootPath = path.join(tmpDir, 'root-manifest.json'); + const packagePath = path.join(tmpDir, 'package-manifest.json'); + + fs.writeFileSync(rootPath, JSON.stringify(createManifest(), null, 2) + '\n'); + fs.writeFileSync(packagePath, JSON.stringify(createPackageManifest(), null, 2) + '\n'); + + updateManifests({ + rootPath, + packagePath, + candidateVersion: '0.144.5', + candidateState: 'ready_to_publish', + }); + + const packageManifest = JSON.parse(fs.readFileSync(packagePath, 'utf8')); + assert.equal(packageManifest.latest_candidate_version, '0.144.5'); + } finally { + fs.rmSync(tmpDir, { recursive: true }); + } +}); + +test('updateManifests: updates candidate_state_status in package manifest', () => { + const tmpDir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'test-')); + try { + const rootPath = path.join(tmpDir, 'root-manifest.json'); + const packagePath = path.join(tmpDir, 'package-manifest.json'); + + fs.writeFileSync(rootPath, JSON.stringify(createManifest(), null, 2) + '\n'); + fs.writeFileSync(packagePath, JSON.stringify(createPackageManifest(), null, 2) + '\n'); + + updateManifests({ + rootPath, + packagePath, + candidateVersion: '0.144.5', + candidateState: 'codex_build_dispatched', + }); + + const packageManifest = JSON.parse(fs.readFileSync(packagePath, 'utf8')); + assert.equal(packageManifest.candidate_state_status, 'codex_build_dispatched'); + } finally { + fs.rmSync(tmpDir, { recursive: true }); + } +}); + +test('updateManifests: ensures updated_at is identical in both root and package manifests', () => { + const tmpDir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'test-')); + try { + const rootPath = path.join(tmpDir, 'root-manifest.json'); + const packagePath = path.join(tmpDir, 'package-manifest.json'); + + fs.writeFileSync(rootPath, JSON.stringify(createManifest(), null, 2) + '\n'); + fs.writeFileSync(packagePath, JSON.stringify(createPackageManifest(), null, 2) + '\n'); + + updateManifests({ + rootPath, + packagePath, + candidateVersion: '0.144.5', + candidateState: 'ready_to_publish', + }); + + const rootManifest = JSON.parse(fs.readFileSync(rootPath, 'utf8')); + const packageManifest = JSON.parse(fs.readFileSync(packagePath, 'utf8')); + + assert.equal(rootManifest.updated_at, packageManifest.updated_at); + } finally { + fs.rmSync(tmpDir, { recursive: true }); + } +}); + +test('updateManifests: preserves package-specific update_command field', () => { + const tmpDir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'test-')); + try { + const rootPath = path.join(tmpDir, 'root-manifest.json'); + const packagePath = path.join(tmpDir, 'package-manifest.json'); + + const rootManifest = createManifest({ + update_command: 'codex update', + }); + const packageManifest = createPackageManifest({ + update_command: 'npm install -g @bash0816/codex-termux@latest', + }); + + fs.writeFileSync(rootPath, JSON.stringify(rootManifest, null, 2) + '\n'); + fs.writeFileSync(packagePath, JSON.stringify(packageManifest, null, 2) + '\n'); + + updateManifests({ + rootPath, + packagePath, + candidateVersion: '0.144.5', + candidateState: 'ready_to_publish', + }); + + const updatedPackageManifest = JSON.parse(fs.readFileSync(packagePath, 'utf8')); + assert.equal(updatedPackageManifest.update_command, 'npm install -g @bash0816/codex-termux@latest'); + } finally { + fs.rmSync(tmpDir, { recursive: true }); + } +}); + +test('updateManifests: preserves package-specific update_guidance field', () => { + const tmpDir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'test-')); + try { + const rootPath = path.join(tmpDir, 'root-manifest.json'); + const packagePath = path.join(tmpDir, 'package-manifest.json'); + + const rootManifest = createManifest({ + update_guidance: 'Run codex update to install the latest audited package version.', + }); + const packageManifest = createPackageManifest({ + update_guidance: 'Use npm to reinstall the latest published package. Do not install the upstream @openai/codex package directly on Termux.', + }); + + fs.writeFileSync(rootPath, JSON.stringify(rootManifest, null, 2) + '\n'); + fs.writeFileSync(packagePath, JSON.stringify(packageManifest, null, 2) + '\n'); + + updateManifests({ + rootPath, + packagePath, + candidateVersion: '0.144.5', + candidateState: 'ready_to_publish', + }); + + const updatedPackageManifest = JSON.parse(fs.readFileSync(packagePath, 'utf8')); + assert.equal( + updatedPackageManifest.update_guidance, + 'Use npm to reinstall the latest published package. Do not install the upstream @openai/codex package directly on Termux.' + ); + } finally { + fs.rmSync(tmpDir, { recursive: true }); + } +});