Skip to content

fix(exec): abort PTY readers after forced termination (#5946) #365

fix(exec): abort PTY readers after forced termination (#5946)

fix(exec): abort PTY readers after forced termination (#5946) #365

Workflow file for this run

name: Preview Build
on:
pull_request:
types: [opened, reopened, ready_for_review, synchronize]
workflow_dispatch: {}
concurrency:
group: preview-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: write
pull-requests: write
actions: read
jobs:
build:
# Skip preview builds for the automated upstream-merge PRs.
# These PRs are large, frequent, and not meant for end-user preview binaries.
if: >-
github.event_name == 'workflow_dispatch' ||
(
github.event_name == 'pull_request' &&
github.event.pull_request.draft == false &&
github.event.pull_request.head.ref != 'upstream-merge'
)
name: Build ${{ matrix.target }}
runs-on: ${{ matrix.os }}
env:
# Consistent Cargo/rustup homes for caching across all workflows
CARGO_HOME: ${{ github.workspace }}/.cargo-home
RUSTUP_HOME: ${{ github.workspace }}/.cargo-home/rustup
RUST_WORKSPACE_DIR: code-rs
CARGO_TARGET_DIR: ${{ github.workspace }}/code-rs/target
strategy:
fail-fast: false
matrix:
include:
# Linux MUSL (static-ish) builds
- os: ubuntu-24.04
target: x86_64-unknown-linux-musl
artifact: code-x86_64-unknown-linux-musl
- os: ubuntu-24.04-arm
target: aarch64-unknown-linux-musl
artifact: code-aarch64-unknown-linux-musl
# macOS builds (both architectures)
- os: macos-14
target: x86_64-apple-darwin
artifact: code-x86_64-apple-darwin
- os: macos-14
target: aarch64-apple-darwin
artifact: code-aarch64-apple-darwin
# Windows build
- os: windows-latest
target: x86_64-pc-windows-msvc
artifact: code-x86_64-pc-windows-msvc.exe
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust toolchain
shell: bash
env:
RUST_TOOLCHAIN: 1.90.0
run: |
rustup set profile minimal
rustup toolchain install "$RUST_TOOLCHAIN" --profile minimal
rustup default "$RUST_TOOLCHAIN"
rustup target add "${{ matrix.target }}"
if [[ "${{ matrix.target }}" == *"unknown-linux-musl"* ]]; then
rustup target add x86_64-unknown-linux-musl aarch64-unknown-linux-musl
fi
- name: Rust cache (target + registries)
uses: Swatinem/rust-cache@v2
with:
prefix-key: v1-preview
shared-key: preview-${{ matrix.target }}-rust-1.90
workspaces: |
code-rs -> target
codex-rs -> target
cache-targets: true
cache-workspace-crates: true
cache-on-failure: true
- name: Setup sccache (GHA backend)
uses: mozilla-actions/sccache-action@v0.0.9
with:
version: v0.10.0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Enable sccache
shell: bash
run: |
echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV"
echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV"
echo "SCCACHE_IDLE_TIMEOUT=1800" >> "$GITHUB_ENV"
echo "SCCACHE_CACHE_SIZE=10G" >> "$GITHUB_ENV"
# Platform tuning (lightweight)
- name: Linux musl tuning
if: contains(matrix.os, 'ubuntu') && contains(matrix.target, 'musl')
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y musl-tools pkg-config zstd
echo 'CC=musl-gcc' >> "$GITHUB_ENV"
case "${{ matrix.target }}" in
x86_64-unknown-linux-musl) echo 'CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER=musl-gcc' >> "$GITHUB_ENV" ;;
aarch64-unknown-linux-musl) echo 'CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=musl-gcc' >> "$GITHUB_ENV" ;;
esac
echo 'PKG_CONFIG_ALLOW_CROSS=1' >> "$GITHUB_ENV"
echo 'OPENSSL_STATIC=1' >> "$GITHUB_ENV"
echo 'RUSTFLAGS=-Awarnings -C debuginfo=0 -C strip=symbols -C panic=abort' >> "$GITHUB_ENV"
- name: macOS tuning
if: startsWith(matrix.os, 'macos-')
shell: bash
run: |
echo 'CC=sccache clang' >> "$GITHUB_ENV"
echo 'CXX=sccache clang++' >> "$GITHUB_ENV"
echo 'RUSTFLAGS=-Awarnings -C debuginfo=0 -C strip=symbols -C panic=abort' >> "$GITHUB_ENV"
- name: Windows tuning
if: matrix.os == 'windows-latest'
shell: pwsh
run: |
"LIBGIT2_SYS_USE_SCHANNEL=1" >> $env:GITHUB_ENV
"CURL_SSL_BACKEND=schannel" >> $env:GITHUB_ENV
if (Get-Command lld-link -ErrorAction SilentlyContinue) {
"RUSTFLAGS=-Awarnings -Clinker=lld-link -C debuginfo=0 -C strip=symbols -C panic=abort -C link-arg=/OPT:REF -C link-arg=/OPT:ICF -C link-arg=/DEBUG:NONE" >> $env:GITHUB_ENV
} else {
"RUSTFLAGS=-Awarnings -C debuginfo=0 -C strip=symbols -C panic=abort -C link-arg=/OPT:REF -C link-arg=/OPT:ICF -C link-arg=/DEBUG:NONE" >> $env:GITHUB_ENV
}
- name: Prefetch deps
working-directory: ${{ env.RUST_WORKSPACE_DIR }}
env:
CARGO_NET_GIT_FETCH_WITH_CLI: "true"
run: cargo fetch --locked
- name: Build code (release)
shell: bash
run: |
cd "${RUST_WORKSPACE_DIR}"
cargo build --release --locked --target "${{ matrix.target }}" --bin code
- name: Prepare artifacts
shell: bash
run: |
mkdir -p artifacts
if [[ "${{ matrix.os }}" == "windows-latest" ]]; then
cp "${RUST_WORKSPACE_DIR}/target/${{ matrix.target }}/release/code.exe" "artifacts/${{ matrix.artifact }}"
else
cp "${RUST_WORKSPACE_DIR}/target/${{ matrix.target }}/release/code" "artifacts/${{ matrix.artifact }}"
fi
- name: Compress artifacts (Windows)
if: matrix.os == 'windows-latest'
shell: pwsh
run: |
Get-ChildItem artifacts -File | ForEach-Object {
$src = $_.FullName
$dst = "$src.zip"
Compress-Archive -Path $src -DestinationPath $dst -Force
Remove-Item $src -Force
}
- name: Compress artifacts (*nix dual-format)
if: matrix.os != 'windows-latest'
shell: bash
run: |
shopt -s nullglob
for f in artifacts/*; do
[ -f "$f" ] || continue
base=$(basename "$f")
# .zst (size-optimized)
if command -v zstd >/dev/null 2>&1; then
zstd -T0 -19 --force -o "artifacts/${base}.zst" "$f" || true
fi
# .tar.gz fallback
tar -C artifacts -czf "artifacts/${base}.tar.gz" "$base"
rm -f "$f"
done
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: preview-${{ matrix.target }}
path: artifacts/
compression-level: 0
release:
name: Publish prerelease (all targets)
needs: [build]
outputs:
slug: ${{ steps.slug.outputs.slug }}
tag: ${{ steps.slug.outputs.tag }}
title: ${{ steps.slug.outputs.title }}
skip: ${{ steps.slug.outputs.skip }}
# Only publish for PRs from the main repo (not forks) to avoid permission failures.
# Fixes: https://github.com/just-every/code/issues/355
# Fixes: https://github.com/just-every/code/issues/356
if: >-
github.event_name == 'pull_request' &&
github.event.pull_request.head.ref != 'upstream-merge' &&
github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- name: Resolve slug and next tag
id: slug
uses: actions/github-script@v7
with:
script: |
const owner = context.repo.owner; const repo = context.repo.repo; const pr = context.payload.pull_request.number;
const { data: pull } = await github.rest.pulls.get({ owner, repo, pull_number: pr });
const marker = /<!--\s*codex-id:\s*([a-z0-9-]{3,})\s*-->/i;
function normalizeSlug(s) {
if (!s) return '';
let x = String(s).toLowerCase().trim();
if (x.startsWith('code/')) x = x.slice(5);
if (x.startsWith('id/')) x = x.slice(3);
// keep only a–z, 0–9 and dashes; collapse repeats
x = x.replace(/[^a-z0-9-]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
if (!/^[a-z0-9-]{3,}$/.test(x)) return '';
return x;
}
let slug = '';
// 1) Prefer PR labels code/<slug> (fallback id/<slug>)
try {
const prLabels = await github.paginate(github.rest.issues.listLabelsOnIssue, { owner, repo, issue_number: pr, per_page: 100 });
const names = prLabels.map(l => (typeof l === 'string' ? l : l.name)).filter(Boolean);
const codeLabel = names.find(n => n.startsWith('code/'));
const idLabel = names.find(n => n.startsWith('id/'));
slug = normalizeSlug(codeLabel || idLabel || '');
} catch {}
// 2) PR body marker
if (!slug) {
const m = (pull.body || '').match(marker);
if (m) slug = normalizeSlug(m[1]);
}
// 3) PR comments marker
if (!slug) {
try {
const prComments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number: pr, per_page: 100 });
for (const c of prComments) { const mm = (c.body || '').match(marker); if (mm) { slug = normalizeSlug(mm[1]); if (slug) break; } }
} catch {}
}
// 4) Linked issue via branch name issue-<n>
let linkedIssue = 0;
const b = pull.head.ref || '';
const im = b.match(/^issue-(\d+)$/);
if (im) linkedIssue = Number(im[1]);
if (!slug && linkedIssue) {
try {
const { data: iss } = await github.rest.issues.get({ owner, repo, issue_number: linkedIssue });
const labels = (iss.labels || []).map(l => (typeof l === 'string' ? l : l.name)).filter(Boolean);
const codeLabel = labels.find(n => typeof n === 'string' && n.startsWith('code/'));
const idLabel = labels.find(n => typeof n === 'string' && n.startsWith('id/'));
slug = normalizeSlug(codeLabel || idLabel || '');
if (!slug) {
const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number: linkedIssue, per_page: 100 });
for (const c of comments) { const mm = (c.body || '').match(marker); if (mm) { slug = normalizeSlug(mm[1]); if (slug) break; } }
}
} catch {}
}
if (!slug) {
core.notice('Missing ID. Skipping prerelease publish; add code/<slug> or <!-- codex-id: ... --> to enable preview binaries.');
core.setOutput('slug', '');
core.setOutput('tag', '');
core.setOutput('title', '');
core.setOutput('skip', 'true');
return;
}
// Best-effort: ensure linked issue carries code/<slug>
if (linkedIssue && slug) {
try {
const { data: iss } = await github.rest.issues.get({ owner, repo, issue_number: linkedIssue });
const labels = (iss.labels || []).map(l => (typeof l === 'string' ? l : l.name)).filter(Boolean);
const need = `code/${slug}`;
if (!labels.includes(need)) {
try { await github.rest.issues.addLabels({ owner, repo, issue_number: linkedIssue, labels: [need] }); } catch {}
}
} catch {}
}
// Determine next tag: preview-<slug> or preview-<slug>-N
const base = `preview-${slug}`;
const rels = await github.paginate(github.rest.repos.listReleases, { owner, repo, per_page: 100 });
let maxN = 0; let hasBase = false;
for (const r of rels) {
const t = r.tag_name || '';
if (t === base) { hasBase = true; maxN = Math.max(maxN, 1); }
const mm = t.match(new RegExp(`^${base}-(\\d+)$`));
if (mm) maxN = Math.max(maxN, parseInt(mm[1], 10));
}
const next = hasBase || maxN > 0 ? `${base}-${maxN+1}` : base;
core.setOutput('slug', slug);
core.setOutput('tag', next);
core.setOutput('title', `Preview for ${slug}`);
core.setOutput('skip', 'false');
- name: Skip summary
if: steps.slug.outputs.skip == 'true'
shell: bash
run: |
printf 'Skipping prerelease upload: missing code/<slug> label or <!-- codex-id: ... --> marker.\n' >> "$GITHUB_STEP_SUMMARY"
- name: Download all artifacts
if: steps.slug.outputs.skip != 'true'
uses: actions/download-artifact@v4
with:
path: artifacts/
- name: Create or update prerelease with assets
if: steps.slug.outputs.skip != 'true'
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ steps.slug.outputs.tag }}
TITLE: ${{ steps.slug.outputs.title }}
shell: bash
run: |
# Add state label triage/building to the linked issue (best-effort)
owner=$(echo "$GITHUB_REPOSITORY" | cut -d/ -f1); repo=$(echo "$GITHUB_REPOSITORY" | cut -d/ -f2)
pr=${{ github.event.pull_request.number }}
issue_number="$(gh pr view "$pr" --json headRefName --jq '.headRefName' | sed -n 's/^issue-\([0-9]\+\)$/\1/p' || true)"
if [ -n "$issue_number" ]; then
gh api -X POST "/repos/$owner/$repo/issues/$issue_number/labels" -f labels[]='triage/building' >/dev/null 2>&1 || true
fi
set -euo pipefail
REPO="${GITHUB_REPOSITORY}"
# Show files for debugging
echo "Artifacts directory:"; ls -R artifacts || true
# Ensure release exists (create if missing)
if ! gh release view "$TAG" -R "$REPO" >/dev/null 2>&1; then
gh release create "$TAG" -R "$REPO" \
--title "$TITLE" \
--notes "Automated prerelease with preview binaries for PR #${{ github.event.pull_request.number }} (run ${{ github.run_id }})." \
--prerelease \
--latest=false \
--target "$GITHUB_SHA"
fi
# Upload/replace all artifacts
mapfile -t files < <(find artifacts -type f -print)
if [ ${#files[@]} -gt 0 ]; then
gh release upload "$TAG" -R "$REPO" "${files[@]}" --clobber
else
echo "No artifacts found to upload." >&2
fi
fallback:
name: Run fallback build-fast
needs: [build, release]
if: >-
always() &&
github.event_name == 'pull_request' &&
github.event.pull_request.head.ref != 'upstream-merge' &&
github.event.pull_request.draft == false &&
(
needs.release.result == 'skipped' ||
needs.release.outputs.skip == 'true'
)
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run build-fast
run: ./build-fast.sh
comment:
name: Post Artifact Links
needs: [build, release]
# Only run if release job ran (which is conditional on non-fork PRs)
if: >-
github.event_name == 'pull_request' &&
github.event.pull_request.head.ref != 'upstream-merge' &&
github.event.pull_request.head.repo.full_name == github.repository &&
needs.release.outputs.skip != 'true'
runs-on: ubuntu-latest
steps:
- name: Resolve slug and latest tag
id: slug
uses: actions/github-script@v7
env:
RELEASE_SLUG: ${{ needs.release.outputs.slug }}
with:
script: |
const owner = context.repo.owner; const repo = context.repo.repo; const pr = context.payload.pull_request.number;
const { data: pull } = await github.rest.pulls.get({ owner, repo, pull_number: pr });
const marker = /<!--\s*codex-id:\s*([a-z0-9-]{3,})\s*-->/i;
function normalizeSlug(s){ if(!s) return ''; let x=String(s).toLowerCase().trim(); if(x.startsWith('code/')) x=x.slice(5); if(x.startsWith('id/')) x=x.slice(3); x=x.replace(/[^a-z0-9-]+/g,'-').replace(/-+/g,'-').replace(/^-|-$/g,''); return /^[a-z0-9-]{3,}$/.test(x)?x:''; }
let slug = normalizeSlug(process.env.RELEASE_SLUG || '');
// PR labels first
try { const labs = await github.paginate(github.rest.issues.listLabelsOnIssue, { owner, repo, issue_number: pr, per_page: 100 });
const names = labs.map(l => (typeof l === 'string' ? l : l.name)).filter(Boolean);
const codeLabel = names.find(n => n.startsWith('code/'));
const idLabel = names.find(n => n.startsWith('id/'));
slug = normalizeSlug(codeLabel || idLabel || '');
} catch {}
// PR body
if (!slug) { const m = (pull.body || '').match(marker); if (m) slug = normalizeSlug(m[1]); }
// PR comments
if (!slug) {
try { const prc = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number: pr, per_page: 100 });
for (const c of prc) { const mm = (c.body || '').match(marker); if (mm) { slug = normalizeSlug(mm[1]); if (slug) break; } }
} catch {}
}
// Linked issue via branch
const b = pull.head.ref || '';
const im = b.match(/^issue-(\d+)$/);
if (!slug && im) {
const issue_number = Number(im[1]);
try {
const { data: iss } = await github.rest.issues.get({ owner, repo, issue_number });
const labels = (iss.labels || []).map(l => (typeof l === 'string' ? l : l.name)).filter(Boolean);
const codeLabel = labels.find(n => typeof n === 'string' && n.startsWith('code/'));
const idLabel = labels.find(n => typeof n === 'string' && n.startsWith('id/'));
slug = normalizeSlug(codeLabel || idLabel || '');
if (!slug) {
const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number, per_page: 100 });
for (const c of comments) { const mm = (c.body || '').match(marker); if (mm) { slug = normalizeSlug(mm[1]); if (slug) break; } }
}
} catch {}
}
if (!slug) throw new Error('Missing ID. Add code/<slug> label or <!-- codex-id: <slug> -->.');
const base = `preview-${slug}`;
const rels = await github.paginate(github.rest.repos.listReleases, { owner, repo, per_page: 100 });
let latest = base; let maxN = 0; let hasBase = false;
for (const r of rels) {
const t = r.tag_name || '';
if (t === base) { hasBase = true; maxN = Math.max(maxN, 1); if (latest === base) latest = base; }
const mm = t.match(new RegExp(`^${base}-(\\d+)$`));
if (mm) { const n = parseInt(mm[1], 10); if (n > maxN) { maxN = n; latest = t; } }
}
core.setOutput('slug', slug);
core.setOutput('tag', latest);
- name: Upsert PR comment with artifact info
if: github.event_name == 'pull_request' && github.event.pull_request.head.ref != 'upstream-merge'
uses: actions/github-script@v7
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const pr_number = context.payload.pull_request.number;
const runId = context.runId;
const runUrl = `https://github.com/${owner}/${repo}/actions/runs/${runId}`;
const slug = `${{ steps.slug.outputs.slug }}`;
const tag = `${{ steps.slug.outputs.tag }}`;
const releaseBase = `https://github.com/${owner}/${repo}/releases/download/${tag}`;
const marker = '<!-- preview-build:artifacts -->';
const body = [
marker,
'### Preview Build',
'',
'You can run this right now! In your terminal just use:',
'```bash',
`code preview ${slug}`,
'```',
'This will run the version of Code we\'ve created for this issue.',
'',
'Direct downloads:',
`- Linux x86_64 (.zst): ${releaseBase}/code-x86_64-unknown-linux-musl.zst`,
`- Linux x86_64 (.tar.gz): ${releaseBase}/code-x86_64-unknown-linux-musl.tar.gz`,
`- Linux aarch64 (.zst): ${releaseBase}/code-aarch64-unknown-linux-musl.zst`,
`- Linux aarch64 (.tar.gz): ${releaseBase}/code-aarch64-unknown-linux-musl.tar.gz`,
`- macOS x86_64 (.zst): ${releaseBase}/code-x86_64-apple-darwin.zst`,
`- macOS x86_64 (.tar.gz): ${releaseBase}/code-x86_64-apple-darwin.tar.gz`,
`- macOS arm64 (.zst): ${releaseBase}/code-aarch64-apple-darwin.zst`,
`- macOS arm64 (.tar.gz): ${releaseBase}/code-aarch64-apple-darwin.tar.gz`,
`- Windows x86_64 (.zip): ${releaseBase}/code-x86_64-pc-windows-msvc.exe.zip`,
'',
'After you run it, please comment here and let me know if it does what you need.',
marker
].join('\n');
// Find existing marker comment from this bot
const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number: pr_number, per_page: 100 });
const mine = comments.find(c => c.user?.type?.toLowerCase().includes('bot') && c.body?.includes(marker));
if (mine) {
await github.rest.issues.updateComment({ owner, repo, comment_id: mine.id, body });
} else {
await github.rest.issues.createComment({ owner, repo, issue_number: pr_number, body });
}
- name: Mark triage complete on linked issue
uses: actions/github-script@v7
with:
script: |
const owner = context.repo.owner; const repo = context.repo.repo;
const pr = context.payload.pull_request.number;
const { data: pull } = await github.rest.pulls.get({ owner, repo, pull_number: pr });
const b = pull.head.ref || '';
const im = b.match(/^issue-(\d+)$/);
if (im) {
const issue_number = Number(im[1]);
try { await github.rest.issues.addLabels({ owner, repo, issue_number, labels: ['triage/complete'] }); } catch {}
try { await github.rest.issues.removeLabel({ owner, repo, issue_number, name: 'triage/building' }); } catch {}
}