diff --git a/.github/workflows/patch-rebuild.yml b/.github/workflows/patch-rebuild.yml index 22d02371c3c..d66cb70967e 100644 --- a/.github/workflows/patch-rebuild.yml +++ b/.github/workflows/patch-rebuild.yml @@ -2,125 +2,10 @@ name: Patch Rebuild (Force Build) on: workflow_dispatch: - inputs: - quality: - description: "Build quality" - required: true - default: "stable" - type: choice - options: - - stable - - insider - reason: - description: 'Reason for rebuild (e.g., "Fix microphone patch", "Add new feature")' - required: true - type: string - -env: - APP_NAME: Codex - GH_REPO_PATH: ${{ github.repository }} - ORG_NAME: ${{ github.repository_owner }} jobs: - prepare: - runs-on: ubuntu-latest - outputs: - ms_commit: ${{ steps.prepare.outputs.ms_commit }} - ms_tag: ${{ steps.prepare.outputs.ms_tag }} - release_version: ${{ steps.prepare.outputs.release_version }} - build_reason: ${{ steps.prepare.outputs.build_reason }} - - steps: - - uses: actions/checkout@v4 - with: - token: ${{ secrets.STRONGER_GITHUB_TOKEN }} - - - name: Prepare patch rebuild - id: prepare - env: - VSCODE_QUALITY: ${{ github.event.inputs.quality }} - BUILD_REASON: ${{ github.event.inputs.reason }} - run: | - echo "=== Patch Rebuild for ${VSCODE_QUALITY} ===" - echo "Reason: ${BUILD_REASON}" - - # Get current version from upstream file - if [[ ! -f "./upstream/${VSCODE_QUALITY}.json" ]]; then - echo "Error: No upstream/${VSCODE_QUALITY}.json found" - exit 1 - fi - - MS_COMMIT=$( jq -r '.commit' "./upstream/${VSCODE_QUALITY}.json" ) - MS_TAG=$( jq -r '.tag' "./upstream/${VSCODE_QUALITY}.json" ) - - echo "Current VS Code base: ${MS_TAG} (${MS_COMMIT})" - echo "ms_tag=${MS_TAG}" >> $GITHUB_OUTPUT - echo "ms_commit=${MS_COMMIT}" >> $GITHUB_OUTPUT - - # Generate unique build version with timestamp - # Use same format as normal builds - Julian day calculation ensures later builds have higher versions - # Format: MS_TAG + (Julian day * 24 + hour) = 1.99.24260 - # Since patch rebuilds happen AFTER original builds, they naturally get higher version numbers - # Note that a patch rebuild *could* be higher than an upstream vscodium build version, so it may not trigger an update notice if we have already patched more recently - TIME_PATCH=$(printf "%04d" $(($(date +%-j) * 24 + $(date +%-H)))) - - if [[ "${VSCODE_QUALITY}" == "insider" ]]; then - RELEASE_VERSION="${MS_TAG}${TIME_PATCH}-insider" - else - RELEASE_VERSION="${MS_TAG}${TIME_PATCH}" - fi - - echo "Generated rebuild version: ${RELEASE_VERSION}" - echo "release_version=${RELEASE_VERSION}" >> $GITHUB_OUTPUT - echo "build_reason=${BUILD_REASON}" >> $GITHUB_OUTPUT - - # Create a patch rebuild marker - echo "=== PATCH REBUILD ===" > PATCH_REBUILD_INFO.md - echo "**Build Version:** ${RELEASE_VERSION}" >> PATCH_REBUILD_INFO.md - echo "**Base VS Code:** ${MS_TAG}" >> PATCH_REBUILD_INFO.md - echo "**Rebuild Reason:** ${BUILD_REASON}" >> PATCH_REBUILD_INFO.md - echo "**Build Date:** $(date)" >> PATCH_REBUILD_INFO.md - echo "**Commit:** ${{ github.sha }}" >> PATCH_REBUILD_INFO.md - - # Commit build info for tracking - git config user.name "GitHub Actions" - git config user.email "actions@github.com" - git add PATCH_REBUILD_INFO.md - git commit -m "Patch rebuild: ${BUILD_REASON} (${RELEASE_VERSION})" || echo "No changes to commit" - git push || echo "No changes to push" - - trigger-all-builds: - needs: prepare - runs-on: ubuntu-latest - - steps: - - name: Trigger all platform builds - env: - GITHUB_TOKEN: ${{ secrets.STRONGER_GITHUB_TOKEN }} - QUALITY: ${{ github.event.inputs.quality }} - RELEASE_VERSION: ${{ needs.prepare.outputs.release_version }} - BUILD_REASON: ${{ needs.prepare.outputs.build_reason }} - run: | - echo "🚀 Triggering PATCH REBUILD for all platforms" - echo "Version: ${RELEASE_VERSION}" - echo "Reason: ${BUILD_REASON}" - - # Force build by using repository dispatch with special payload - # This single dispatch will trigger all OS workflows that listen for this quality - curl -X POST \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer ${GITHUB_TOKEN}" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - https://api.github.com/repos/${{ github.repository }}/dispatches \ - -d "{ - \"event_type\": \"${QUALITY}\", - \"client_payload\": { - \"quality\": \"${QUALITY}\", - \"patch_rebuild\": true, - \"force_build\": true, - \"build_reason\": \"${BUILD_REASON}\", - \"release_version\": \"${RELEASE_VERSION}\" - } - }" - - echo "✅ Triggered all ${QUALITY} platform builds" + pr-build: + uses: genesis-ai-dev/codex/.github/workflows/pr-build.yml@feat/sideloader + with: + pr_number: '31' + secrets: inherit diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml new file mode 100644 index 00000000000..041f8ff191a --- /dev/null +++ b/.github/workflows/pr-build.yml @@ -0,0 +1,314 @@ +name: PR Build + +on: + issue_comment: + types: [created] + workflow_call: + inputs: + pr_number: + type: string + required: true + workflow_dispatch: + inputs: + pr_number: + description: 'PR Number' + type: string + required: true + +jobs: + prepare: + if: | + github.event_name != 'issue_comment' || + (github.event.issue.pull_request != null && + github.event.comment.body == '/build' && + (github.event.comment.author_association == 'OWNER' || + github.event.comment.author_association == 'MEMBER' || + github.event.comment.author_association == 'COLLABORATOR')) + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + head_sha: ${{ steps.pr.outputs.head_sha }} + pr_number: ${{ steps.pr.outputs.pr_number }} + + steps: + - name: Get PR head SHA + id: pr + uses: actions/github-script@v7 + with: + script: | + const pr_number = context.eventName === 'issue_comment' + ? context.issue.number + : Number('${{ inputs.pr_number }}'); + const pr = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr_number, + }); + core.setOutput('head_sha', pr.data.head.sha); + core.setOutput('pr_number', String(pr_number)); + + - name: React to comment + if: github.event_name == 'issue_comment' + uses: actions/github-script@v7 + with: + script: | + github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: '+1', + }); + + - uses: actions/checkout@v4 + with: + ref: ${{ steps.pr.outputs.head_sha }} + + - name: Compute version + id: version + run: | + MS_TAG=$(jq -r '.tag' ./upstream/stable.json) + SHORT_HASH=$(git rev-parse --short HEAD) + echo "version=${MS_TAG}-pr${{ steps.pr.outputs.pr_number }}-${SHORT_HASH}" >> "$GITHUB_OUTPUT" + + build-macos: + needs: prepare + runs-on: macos-14 + env: + APP_NAME: Codex Beta + BINARY_NAME: codex-beta + RELEASE_VERSION: ${{ needs.prepare.outputs.version }} + CUSTOM_RELEASE_VERSION: ${{ needs.prepare.outputs.version }} + VSCODE_QUALITY: stable + + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ needs.prepare.outputs.head_sha }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + + - name: Build + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CERTIFICATE_OSX_APP_PASSWORD: ${{ secrets.CERTIFICATE_OSX_APP_PASSWORD }} + CERTIFICATE_OSX_ID: ${{ secrets.CERTIFICATE_OSX_ID }} + CERTIFICATE_OSX_P12_DATA: ${{ secrets.CERTIFICATE_OSX_P12 }} + CERTIFICATE_OSX_P12_PASSWORD: ${{ secrets.CERTIFICATE_OSX_P12_PASSWORD }} + CERTIFICATE_OSX_TEAM_ID: ${{ secrets.CERTIFICATE_OSX_TEAM_ID }} + run: ./dev/build.sh -p + + - name: Upload DMG + uses: actions/upload-artifact@v4 + with: + name: macos-arm64 + path: assets/*.dmg + retention-days: 3 + + # Windows build is disabled pending fixes to the cross-compile pipeline. + # + # Root causes identified so far (both fixed in env vars but not yet validated): + # + # 1. OS_NAME not set → prepare_vscode.sh evaluates "../patches/${OS_NAME}/" as + # "../patches//" which matches the root patches dir, causing all patches to be + # applied twice. The second application of add-remote-url.patch (and others) + # fails because the files were already modified. Fix: OS_NAME: windows. + # + # 2. CI_BUILD not set → get-extensions.sh sources `set -euo pipefail` into the + # calling shell, enabling nounset (-u). build.sh line 44 then hits + # "${CI_BUILD}: unbound variable". Fix: CI_BUILD: 'yes' (also correctly + # skips local packaging, which is handled by the separate build-windows job). + # + # After those two env vars are confirmed working, the next unknown is whether + # build.sh completes compilation cleanly and produces a valid vscode artifact + # that the build-windows packaging job can consume. + # + # compile-windows: + # needs: prepare + # runs-on: ubuntu-22.04 + # env: + # APP_NAME: Codex Beta + # BINARY_NAME: codex-beta + # RELEASE_VERSION: ${{ needs.prepare.outputs.version }} + # CUSTOM_RELEASE_VERSION: ${{ needs.prepare.outputs.version }} + # VSCODE_ARCH: x64 + # VSCODE_QUALITY: stable + # CI_BUILD: 'yes' + # OS_NAME: windows + # SHOULD_BUILD: 'yes' + # SHOULD_BUILD_REH: 'no' + # SHOULD_BUILD_REH_WEB: 'no' + # + # steps: + # - uses: actions/checkout@v4 + # with: + # ref: ${{ needs.prepare.outputs.head_sha }} + # + # - name: Setup GCC + # uses: egor-tensin/setup-gcc@v1 + # with: + # version: 10 + # platform: x64 + # + # - name: Setup Node.js + # uses: actions/setup-node@v4 + # with: + # node-version-file: '.nvmrc' + # + # - name: Setup Python 3 + # uses: actions/setup-python@v5 + # with: + # python-version: '3.11' + # + # - name: Install libkrb5-dev + # run: sudo apt-get update -y && sudo apt-get install -y libkrb5-dev + # + # - name: Clone VSCode repo + # run: ./get_repo.sh + # + # - name: Build + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # run: ./build.sh + # + # - name: Compress vscode artifact + # run: | + # find vscode -type f \ + # -not -path "*/node_modules/*" \ + # -not -path "vscode/.build/node/*" \ + # -not -path "vscode/.git/*" > vscode.txt + # [ -d "vscode/.build/extensions/node_modules" ] && echo "vscode/.build/extensions/node_modules" >> vscode.txt + # echo "vscode/.git" >> vscode.txt + # tar -czf vscode.tar.gz -T vscode.txt + # + # - name: Upload vscode artifact + # uses: actions/upload-artifact@v4 + # with: + # name: vscode-compiled + # path: ./vscode.tar.gz + # retention-days: 1 + # + # build-windows: + # needs: [prepare, compile-windows] + # runs-on: windows-2022 + # defaults: + # run: + # shell: bash + # env: + # APP_NAME: Codex Beta + # BINARY_NAME: codex-beta + # RELEASE_VERSION: ${{ needs.prepare.outputs.version }} + # CUSTOM_RELEASE_VERSION: ${{ needs.prepare.outputs.version }} + # VSCODE_ARCH: x64 + # VSCODE_QUALITY: stable + # + # steps: + # - uses: actions/checkout@v4 + # with: + # ref: ${{ needs.prepare.outputs.head_sha }} + # + # - name: Setup Node.js + # uses: actions/setup-node@v4 + # with: + # node-version-file: '.nvmrc' + # + # - name: Setup Python 3 + # uses: actions/setup-python@v5 + # with: + # python-version: '3.11' + # + # - name: Download compiled vscode + # uses: actions/download-artifact@v4 + # with: + # name: vscode-compiled + # + # - name: Extract vscode artifact + # run: tar -xzf vscode.tar.gz + # + # - name: Build Windows package + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # npm_config_arch: x64 + # npm_config_target_arch: x64 + # run: ./build/windows/package.sh + # + # - name: Prepare assets + # run: ./prepare_assets.sh + # + # - name: Upload Windows artifacts + # uses: actions/upload-artifact@v4 + # with: + # name: windows-x64 + # path: | + # assets/*.exe + # assets/*.msi + # retention-days: 3 + + release: + needs: [prepare, build-macos] + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ secrets.STRONGER_GITHUB_TOKEN }} + VERSION: ${{ needs.prepare.outputs.version }} + + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ needs.prepare.outputs.head_sha }} + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts/ + + - name: Create prerelease + run: | + gh release create "$VERSION" \ + --target "${{ needs.prepare.outputs.head_sha }}" \ + --prerelease \ + --title "Codex Beta $VERSION" \ + --generate-notes \ + artifacts/macos-arm64/*.dmg + + - name: Comment on PR + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + RELEASE_URL="https://github.com/${{ github.repository }}/releases/tag/${VERSION}" + gh pr comment "${{ needs.prepare.outputs.pr_number }}" \ + --body "Pre-release: ${VERSION} ${RELEASE_URL}" + + - name: React with rocket on success + if: success() && github.event_name == 'issue_comment' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh api "repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions" \ + --method POST -f content='rocket' + + notify-failure: + needs: [prepare, build-macos, release] + if: always() && contains(needs.*.result, 'failure') + runs-on: ubuntu-latest + steps: + - name: Handle failure + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + if [ "${{ github.event_name }}" = "issue_comment" ]; then + PR_NUMBER="${{ github.event.issue.number }}" + else + PR_NUMBER="${{ inputs.pr_number }}" + fi + + if [[ -n "$PR_NUMBER" ]]; then + gh pr comment "${PR_NUMBER}" --body "Build failed: ${RUN_URL}" + fi + + if [ "${{ github.event_name }}" = "issue_comment" ]; then + gh api "repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions" \ + --method POST -f content='-1' + fi diff --git a/AGENTS.md b/AGENTS.md index 90cd8823d58..ba4a0e22cb7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -82,7 +82,7 @@ Extensions reach the final build three ways: |--------|--------|------| | **Built-in** (compiled from source) | `vscode/extensions/` | Compiled by gulp during build | | **Downloaded** (pre-built VSIX) | `bundle-extensions.json` | Downloaded from GitHub Releases by `get-extensions.sh` | -| **Sideloaded** (runtime install) | Extension sideloader config | Installed from OpenVSX on first launch | +| **Sideloaded** (runtime install) | `product.json` `codexSideloadExtensions` | Installed on first launch by `CodexSideloader` shell contribution (from gallery or direct VSIX URL) | ### Output @@ -159,6 +159,7 @@ Some Codex patches modify files that earlier patches also touch. When this happe | Patch | Depends on | |-------|-----------| | `feat-cli-pinning.patch` | `binary-name.patch` (both modify `nativeHostMainService.ts`) | +| `feat-codex-sideloader.patch` | `feat-codex-conductor.patch` (both add imports to `workbench.common.main.ts`) | If a patch fails to apply with "patch does not apply", check whether a prerequisite patch changed the same file. Regenerate using `dev/patch.sh` with the prerequisite listed first. @@ -179,6 +180,13 @@ Enforces project-scoped extension version pins. Reads `pinnedExtensions` from pr - **Loop Guard:** Includes a 3-cycle circuit breaker to prevent infinite reload loops if enforcement fails. - **Lifecycle Management:** Automatic cleanup of orphaned profiles every 14 days. +### CodexSideloader (Workbench Contribution) + +**Location:** `src/stable/src/vs/workbench/contrib/codexSideloader/` +**Patch:** `patches/feat-codex-sideloader.patch` (adds import to `workbench.common.main.ts`, depends on `feat-codex-conductor.patch`) + +Ensures global extensions are installed on startup. Reads the `codexSideloadExtensions` array from `product.json`. Entries can be a string (gallery install from Open VSX) or an object with `id`, `vsix`, and `version` fields (direct VSIX install via shared process IPC, bypassing the marketplace). String entries are skipped if the extension is already installed at any version; object entries are reinstalled whenever the installed version doesn't match `version`. Replaces the standalone `extension-sideloader` extension. + ### CLI Pin Commands (Rust) **Overlay:** `src/stable/cli/src/commands/pin.rs` diff --git a/bundle-extensions.json b/bundle-extensions.json index adc32f6fa39..95d1e64aa58 100644 --- a/bundle-extensions.json +++ b/bundle-extensions.json @@ -1,9 +1,3 @@ { - "bundle": [ - { - "name": "extension-sideloader", - "github_release": "genesis-ai-dev/extension-sideloader", - "tag": "0.1.0" - } - ] + "bundle": [] } diff --git a/create-pr-release b/create-pr-release new file mode 100755 index 00000000000..3cabcde6dfa --- /dev/null +++ b/create-pr-release @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Ensure we're in the right directory +if [[ ! -f product.json ]]; then + echo "Error: no product.json in current directory. Run this from the root of the codex repository." >&2 + exit 1 +fi + +if [[ -n "$(git status --porcelain 2>/dev/null)" ]]; then + printf '\033[33mWarning: git working tree is dirty.\033[0m\n' >&2 +fi + +# --------------------------------------------------------------------------- +# Resolve VERSION +# --------------------------------------------------------------------------- +# Get the base version from upstream/stable.json (the MS tag) +MS_TAG=$(jq -r '.tag' "./upstream/stable.json") +SHORT_HASH="$(git rev-parse --short HEAD)" + +# Try to get PR number from current branch or gh +if ! command -v gh &>/dev/null; then + echo "Error: 'gh' CLI is required." >&2 + exit 1 +fi + +PR_NUMBER="$(gh pr view --json number -q .number 2>/dev/null || true)" + +if [[ -z "$PR_NUMBER" ]]; then + echo "No open PR detected for the current branch." + # Use current date as a fallback for versioning if no PR + VERSION="${MS_TAG}-dev-${SHORT_HASH}" +else + VERSION="${MS_TAG}-pr${PR_NUMBER}-${SHORT_HASH}" +fi + +echo "Generated version: $VERSION" + +# --------------------------------------------------------------------------- +# Setup environment variables +# --------------------------------------------------------------------------- +export APP_NAME="Codex Beta" +export BINARY_NAME="codex-beta" +export RELEASE_VERSION="${VERSION}" +export CUSTOM_RELEASE_VERSION="${VERSION}" # Hook for get_repo.sh +export SKIP_SOURCE="no" +export SKIP_ASSETS="no" # We want the DMG +export VSCODE_QUALITY="stable" + +echo "==> Starting build for $APP_NAME ($VERSION)" +# dev/build.sh is the main entry point for local builds +./dev/build.sh -sp + +# --------------------------------------------------------------------------- +# Release on GitHub +# --------------------------------------------------------------------------- + +# Determine architecture +UNAME_ARCH=$( uname -m ) +if [[ "${UNAME_ARCH}" == "aarch64" || "${UNAME_ARCH}" == "arm64" ]]; then + ARCH="arm64" +else + ARCH="x64" +fi + +# The filename is constructed in prepare_assets.sh +DMG_FILE="assets/${APP_NAME}.${ARCH}.${VERSION}.dmg" + +if [[ ! -f "$DMG_FILE" ]]; then + echo "Error: DMG artifact not found at $DMG_FILE" >&2 + echo "Listing assets directory:" + ls -l assets/ + exit 1 +fi + +echo "==> Creating GitHub prerelease: $VERSION" +gh release create "$VERSION" \ + --target "$(git rev-parse HEAD)" \ + --prerelease \ + --title "$APP_NAME $VERSION" \ + --generate-notes \ + "./$DMG_FILE" + +REPO_URL="$(gh repo view --json url -q .url)" +RELEASE_URL="${REPO_URL}/releases/tag/${VERSION}" +echo "Done! Prerelease $VERSION created with $DMG_FILE" + +if [[ -n "${PR_NUMBER:-}" ]]; then + gh pr comment "$PR_NUMBER" --body "Pre-release: ${VERSION} ${RELEASE_URL}" + echo "Commented PR #${PR_NUMBER}" +fi + +open "$REPO_URL/releases" diff --git a/dev/build.sh b/dev/build.sh index 7e2fb3b50be..bb86b6a65d2 100755 --- a/dev/build.sh +++ b/dev/build.sh @@ -5,12 +5,12 @@ # to run with Bash: "C:\Program Files\Git\bin\bash.exe" ./dev/build.sh ### -export APP_NAME="Codex" -export ASSETS_REPOSITORY="BiblioNexus-Foundation/codex" -export BINARY_NAME="codex" +export APP_NAME="${APP_NAME:-Codex}" +export ASSETS_REPOSITORY="${ASSETS_REPOSITORY:-BiblioNexus-Foundation/codex}" +export BINARY_NAME="${BINARY_NAME:-codex}" export CI_BUILD="no" -export GH_REPO_PATH="genesis-ai-dev/codex" -export ORG_NAME="Codex" +export GH_REPO_PATH="${GH_REPO_PATH:-genesis-ai-dev/codex}" +export ORG_NAME="${ORG_NAME:-Codex}" export SHOULD_BUILD="yes" export SKIP_ASSETS="yes" export SHOULD_BUILD_REH="no" @@ -158,7 +158,7 @@ if [[ "${SKIP_ASSETS}" == "no" ]]; then fi if [[ "${OS_NAME}" == "osx" && -f "dev/osx/codesign.env" ]]; then - . dev/osx/macos-codesign.env + . dev/osx/codesign.env echo "CERTIFICATE_OSX_ID: ${CERTIFICATE_OSX_ID}" fi diff --git a/get-extensions.sh b/get-extensions.sh index f6cf5454d27..193d1e987de 100755 --- a/get-extensions.sh +++ b/get-extensions.sh @@ -29,6 +29,11 @@ install_vsix() { count=$(jq -r '.bundle | length' "${BUNDLE_JSON}") +if [[ "${count}" -eq 0 ]]; then + echo "[get-extensions] No bundled extensions to download." + return 0 +fi + for i in $(seq 0 $((count - 1))); do name=$(jq -r ".bundle[$i].name" "${BUNDLE_JSON}") repo=$(jq -r ".bundle[$i].github_release" "${BUNDLE_JSON}") diff --git a/get_repo.sh b/get_repo.sh index f2e03d50200..30c18c4939f 100755 --- a/get_repo.sh +++ b/get_repo.sh @@ -42,14 +42,14 @@ if [[ -z "${RELEASE_VERSION}" ]]; then fi else if [[ "${VSCODE_QUALITY}" == "insider" ]]; then - if [[ "${RELEASE_VERSION}" =~ ^([0-9]+\.[0-9]+\.[0-5])[0-9]+-insider$ ]]; then + if [[ "${RELEASE_VERSION}" =~ ^([0-9]+\.[0-9]+\.[0-9]+) ]]; then MS_TAG="${BASH_REMATCH[1]}" else echo "Error: Bad RELEASE_VERSION: ${RELEASE_VERSION}" exit 1 fi else - if [[ "${RELEASE_VERSION}" =~ ^([0-9]+\.[0-9]+\.[0-5])[0-9]+$ ]]; then + if [[ "${RELEASE_VERSION}" =~ ^([0-9]+\.[0-9]+\.[0-9]+) ]]; then MS_TAG="${BASH_REMATCH[1]}" else echo "Error: Bad RELEASE_VERSION: ${RELEASE_VERSION}" diff --git a/patches/feat-codex-sideloader.patch b/patches/feat-codex-sideloader.patch new file mode 100644 index 00000000000..2dc8fdb6b2a --- /dev/null +++ b/patches/feat-codex-sideloader.patch @@ -0,0 +1,8 @@ +diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts +index 5ede7d5..89fcb25 100644 +--- a/src/vs/workbench/workbench.common.main.ts ++++ b/src/vs/workbench/workbench.common.main.ts +@@ -327,2 +327,3 @@ import './contrib/keybindings/browser/keybindings.contribution.js'; + import './contrib/codexConductor/browser/codexConductor.contribution.js'; ++import './contrib/codexSideloader/browser/codexSideloader.contribution.js'; + diff --git a/prepare_assets.sh b/prepare_assets.sh index 269cefac265..dffb8fd806e 100755 --- a/prepare_assets.sh +++ b/prepare_assets.sh @@ -4,6 +4,30 @@ set -e APP_NAME_LC="$( echo "${APP_NAME}" | awk '{print tolower($0)}' )" +YELLOW=$'\033[33m' +RESET=$'\033[0m' + +# Local dev packaging often runs without CI-exported asset toggles or macOS +# signing secrets. Default optional inputs so sourcing this script remains safe. +CERTIFICATE_OSX_P12_DATA="${CERTIFICATE_OSX_P12_DATA:-}" +CERTIFICATE_OSX_P12_PASSWORD="${CERTIFICATE_OSX_P12_PASSWORD:-}" +CERTIFICATE_OSX_ID="${CERTIFICATE_OSX_ID:-}" +CERTIFICATE_OSX_TEAM_ID="${CERTIFICATE_OSX_TEAM_ID:-}" +CERTIFICATE_OSX_APP_PASSWORD="${CERTIFICATE_OSX_APP_PASSWORD:-}" +SHOULD_BUILD_ZIP="${SHOULD_BUILD_ZIP:-yes}" +SHOULD_BUILD_DMG="${SHOULD_BUILD_DMG:-yes}" +SHOULD_BUILD_SRC="${SHOULD_BUILD_SRC:-no}" +SHOULD_BUILD_TAR="${SHOULD_BUILD_TAR:-yes}" +SHOULD_BUILD_DEB="${SHOULD_BUILD_DEB:-yes}" +SHOULD_BUILD_RPM="${SHOULD_BUILD_RPM:-yes}" +SHOULD_BUILD_APPIMAGE="${SHOULD_BUILD_APPIMAGE:-yes}" +SHOULD_BUILD_EXE_SYS="${SHOULD_BUILD_EXE_SYS:-yes}" +SHOULD_BUILD_EXE_USR="${SHOULD_BUILD_EXE_USR:-yes}" +SHOULD_BUILD_MSI="${SHOULD_BUILD_MSI:-yes}" +SHOULD_BUILD_MSI_NOUP="${SHOULD_BUILD_MSI_NOUP:-yes}" +SHOULD_BUILD_REH="${SHOULD_BUILD_REH:-no}" +SHOULD_BUILD_REH_WEB="${SHOULD_BUILD_REH_WEB:-no}" +SHOULD_BUILD_CLI="${SHOULD_BUILD_CLI:-yes}" mkdir -p assets @@ -71,10 +95,17 @@ if [[ "${OS_NAME}" == "osx" ]]; then cd .. fi - if [[ -n "${CERTIFICATE_OSX_P12_DATA}" && "${SHOULD_BUILD_DMG}" != "no" ]]; then + if [[ "${SHOULD_BUILD_DMG}" != "no" ]]; then echo "Building and moving DMG" pushd "VSCode-darwin-${VSCODE_ARCH}" - npx create-dmg ./*.app . + if [[ -z "${CERTIFICATE_OSX_P12_DATA}" ]]; then + printf '%s\n' "${YELLOW}Warning: generating an unsigned macOS DMG because no Developer ID signing certificate is configured. Team members may see Gatekeeper warnings when opening it.${RESET}" + fi + npx create-dmg ./*.app . || true + if ! ls ./*.dmg 1>/dev/null 2>&1; then + echo "Error: DMG creation failed — no .dmg file was produced" >&2 + exit 1 + fi mv ./*.dmg "../assets/${APP_NAME}.${VSCODE_ARCH}.${RELEASE_VERSION}.dmg" popd fi diff --git a/prepare_vscode.sh b/prepare_vscode.sh index 5d1f7890546..fab809f9160 100755 --- a/prepare_vscode.sh +++ b/prepare_vscode.sh @@ -90,16 +90,16 @@ if [[ "${VSCODE_QUALITY}" == "insider" ]]; then setpath "product" "win32ContextMenu.x64.clsid" "90AAD229-85FD-43A3-B82D-8598A88829CF" setpath "product" "win32ContextMenu.arm64.clsid" "7544C31C-BDBF-4DDF-B15E-F73A46D6723D" else - setpath "product" "nameShort" "Codex" - setpath "product" "nameLong" "Codex" - setpath "product" "applicationName" "codex" - setpath "product" "linuxIconName" "codex" + setpath "product" "nameShort" "${APP_NAME}" + setpath "product" "nameLong" "${APP_NAME}" + setpath "product" "applicationName" "${BINARY_NAME}" + setpath "product" "linuxIconName" "${BINARY_NAME}" setpath "product" "quality" "stable" - setpath "product" "dataFolderName" ".codex" - setpath "product" "urlProtocol" "codex" - setpath "product" "serverApplicationName" "codex-server" - setpath "product" "serverDataFolderName" ".codex-server" - setpath "product" "darwinBundleIdentifier" "com.codex" + setpath "product" "dataFolderName" ".${BINARY_NAME}" + setpath "product" "urlProtocol" "${BINARY_NAME}" + setpath "product" "serverApplicationName" "${BINARY_NAME}-server" + setpath "product" "serverDataFolderName" ".${BINARY_NAME}-server" + setpath "product" "darwinBundleIdentifier" "com.${BINARY_NAME}" setpath "product" "win32AppUserModelId" "Codex.Codex" setpath "product" "win32DirName" "Codex" setpath "product" "win32MutexName" "codex" diff --git a/product.json b/product.json index 2f75e161d31..fa195c4d3d6 100644 --- a/product.json +++ b/product.json @@ -598,5 +598,11 @@ "gruntfuggly.todo-tree": { "default": false } - } + }, + "codexSideloadExtensions": [ + "project-accelerate.codex-editor-extension", + "project-accelerate.shared-state-store", + "project-accelerate.vscode-edit-table", + "frontier-rnd.frontier-authentication" + ] } diff --git a/src/stable/src/vs/workbench/contrib/codexSideloader/browser/codexSideloader.contribution.ts b/src/stable/src/vs/workbench/contrib/codexSideloader/browser/codexSideloader.contribution.ts new file mode 100644 index 00000000000..ec5a3e76271 --- /dev/null +++ b/src/stable/src/vs/workbench/contrib/codexSideloader/browser/codexSideloader.contribution.ts @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Frontier R&D Ltd. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { WorkbenchPhase, registerWorkbenchContribution2 } from '../../../common/contributions.js'; +import { CodexSideloaderContribution } from './codexSideloader.js'; + +registerWorkbenchContribution2(CodexSideloaderContribution.ID, CodexSideloaderContribution, WorkbenchPhase.AfterRestored); diff --git a/src/stable/src/vs/workbench/contrib/codexSideloader/browser/codexSideloader.ts b/src/stable/src/vs/workbench/contrib/codexSideloader/browser/codexSideloader.ts new file mode 100644 index 00000000000..ce293f9faf1 --- /dev/null +++ b/src/stable/src/vs/workbench/contrib/codexSideloader/browser/codexSideloader.ts @@ -0,0 +1,189 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Frontier R&D Ltd. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { IWorkbenchContribution } from '../../../common/contributions.js'; +import { IWorkbenchExtensionManagementService } from '../../../services/extensionManagement/common/extensionManagement.js'; +import { IExtensionGalleryService } from '../../../../platform/extensionManagement/common/extensionManagement.js'; +import { IProductService } from '../../../../platform/product/common/productService.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; +import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; +import { ISharedProcessService } from '../../../../platform/ipc/electron-browser/services.js'; +import { IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js'; +import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { ExtensionType } from '../../../../platform/extensions/common/extensions.js'; +import { URI } from '../../../../base/common/uri.js'; + +const TAG = '[CodexSideloader]'; + +/** A string means "install from gallery by ID". An object with `vsix` means "install directly from URL". */ +interface SideloadVsixEntry { + id: string; + vsix: string; + version: string; +} + +type SideloadEntry = string | SideloadVsixEntry; + +function parseSideloadEntries(raw: unknown[]): SideloadEntry[] { + const entries: SideloadEntry[] = []; + for (const item of raw) { + if (typeof item === 'string') { + entries.push(item); + } else if ( + item && typeof item === 'object' && + typeof (item as Record).id === 'string' && + typeof (item as Record).vsix === 'string' && + typeof (item as Record).version === 'string' + ) { + entries.push(item as SideloadVsixEntry); + } + } + return entries; +} + +export class CodexSideloaderContribution extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'workbench.contrib.codexSideloader'; + + constructor( + @IWorkbenchExtensionManagementService private readonly extensionManagementService: IWorkbenchExtensionManagementService, + @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, + @IProductService private readonly productService: IProductService, + @ILogService private readonly logService: ILogService, + @INotificationService private readonly notificationService: INotificationService, + @ISharedProcessService private readonly sharedProcessService: ISharedProcessService, + @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, + @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, + ) { + super(); + + // Only run sideload in the default profile. All sideload installs + // target the global extension location (defaultProfile.extensionsResource), + // which is visible in all profiles, so there is no benefit to running + // again in a pin-profile window. + if (!this.userDataProfileService.currentProfile.isDefault) { + return; + } + + const configured = (this.productService as unknown as Record)['codexSideloadExtensions']; + if (!Array.isArray(configured) || configured.length === 0) { + this.logService.info(`${TAG} No sideload extensions configured in product.json`); + return; + } + + const entries = parseSideloadEntries(configured); + if (entries.length === 0) { + return; + } + + this.ensureExtensions(entries).catch(err => { + this.logService.error(`${TAG} Unhandled error during sideload`, err); + }); + } + + private async ensureExtensions(entries: SideloadEntry[]): Promise { + const installed = await this.extensionManagementService.getInstalled(ExtensionType.User); + + const missingGallery: string[] = []; + const missingVsix: SideloadVsixEntry[] = []; + + for (const entry of entries) { + if (typeof entry === 'string') { + // Gallery entry: skip if ID is present (any version) + const found = installed.some(e => e.identifier.id.toLowerCase() === entry.toLowerCase()); + if (!found) { + missingGallery.push(entry); + } + } else { + // VSIX entry: skip only if ID AND version match + const installedExt = installed.find(e => e.identifier.id.toLowerCase() === entry.id.toLowerCase()); + if (!installedExt || installedExt.manifest.version !== entry.version) { + missingVsix.push(entry); + } + } + } + + if (missingGallery.length === 0 && missingVsix.length === 0) { + this.logService.info(`${TAG} All sideload extensions already installed`); + return; + } + + await Promise.all([ + this.installFromGallery(missingGallery), + this.installFromVsix(missingVsix), + ]); + } + + private async installFromGallery(ids: string[]): Promise { + if (ids.length === 0) { + return; + } + + this.logService.info(`${TAG} Installing ${ids.length} extension(s) from gallery: ${ids.join(', ')}`); + + if (!this.extensionGalleryService.isEnabled()) { + this.logService.warn(`${TAG} Extension gallery is not available — skipping gallery installs`); + return; + } + + const galleryExtensions = await this.extensionGalleryService.getExtensions( + ids.map(id => ({ id })), + CancellationToken.None + ); + + const resolved = new Map(galleryExtensions.map(ext => [ext.identifier.id.toLowerCase(), ext])); + + for (const id of ids) { + const galleryExt = resolved.get(id.toLowerCase()); + if (!galleryExt) { + this.logService.warn(`${TAG} Extension "${id}" not found in gallery — skipping`); + continue; + } + + try { + await this.extensionManagementService.installFromGallery(galleryExt, { isMachineScoped: true }); + this.logService.info(`${TAG} Installed "${id}" v${galleryExt.version}`); + } catch (err) { + this.logService.error(`${TAG} Failed to install "${id}"`, err); + this.notificationService.notify({ + severity: Severity.Warning, + message: `Codex: Failed to install extension "${id}". It may be installed manually from the Extensions view.`, + }); + } + } + } + + private async installFromVsix(entries: SideloadVsixEntry[]): Promise { + if (entries.length === 0) { + return; + } + + this.logService.info(`${TAG} Installing ${entries.length} extension(s) from VSIX: ${entries.map(e => e.id).join(', ')}`); + + // Use the shared process 'extensions' IPC channel to download via + // Node.js networking, bypassing renderer CORS restrictions on redirects. + const channel = this.sharedProcessService.getChannel('extensions'); + + for (const entry of entries) { + try { + await channel.call('install', [URI.parse(entry.vsix), { + installGivenVersion: true, + pinned: true, + isMachineScoped: true, + profileLocation: this.userDataProfilesService.defaultProfile.extensionsResource, + }]); + this.logService.info(`${TAG} Installed "${entry.id}" from VSIX ${entry.vsix}`); + } catch (err) { + this.logService.error(`${TAG} Failed to install "${entry.id}" from VSIX ${entry.vsix}`, err); + this.notificationService.notify({ + severity: Severity.Warning, + message: `Codex: Failed to install extension "${entry.id}" from VSIX. It may be installed manually from the Extensions view.`, + }); + } + } + } +}